依赖注入库 - 重命名注入值

Dependency injection library - renaming injected values

我想按名称注入 lodash,如下所示:

let val = function(lodash){
   // lodash will be injected, simply by using require('lodash');
};

但是说我想重命名导入,我想做这样的事情:

let val = function({lodash:_}){

};

let val = function(lodash as _){

};

有没有办法用 ES6/ES7/ES8 或 TypeScript 做到这一点?

请注意,此 DI 框架所做的工作不仅仅是 require('x')...它会首先尝试注入其他值,如果不存在其他值,那么它会尝试要求该值。

另请注意,这里的要求是当您调用 val.toString() 时,"lodash" 将被视为参数名称。但是 _ 而不是 lodash 会在运行时在函数体内看到。这是因为为了注入 lodash,我们调用 fn.toString() 来获取参数名称。

我做了一些可能对你有用的事情, 但您可以随时更改它并使用总体思路。

它是用 ES6 特性编写的,但您可以轻松删除它们。

let di = function() {
    const argumentsLength = arguments.length;

    //you must call this func with at least a callback
    if (argumentsLength === 0) return;
    //this will be called with odd amount of variables,
    //pairs of key and assignment, and the callback
    //means: 1,3,5,7.... amount of args
    if (argumentsLength%2 === 0) throw "mismatch of args";

    //here we will assing the variables to "this"
    for (key in arguments) {
        //skip the callback
        if(key===argumentsLength-1) continue;
        //skip the "key", it will be used in the next round
        if(key%2===0) continue;
        const keyToSet = arguments[key-1];
        const valToSet = arguments[key];
        this[keyToSet] = valToSet;
    }

    arguments[argumentsLength-1].apply(this);
}

di("name", {a:"IwillBeName"}, "whatever", "IwillBeWhatever", () => {
    console.log(whatever);
    console.log(name);
});

在最后一行,你调用 func "di" 传入这些参数:

di("_", lodash, callback);

现在在你的回调代码中,你可以用“_”引用 "lodash"

更新

这里有一个link到npm package di-proxy(受此答案启发),代码覆盖率100%,支持memoization提高性能,兼容Node.js>=6.0.0.

旧答案

这是我在修改 object destructuring and Proxy 时想出的一个很棒的解决方案:

/* MIT License */
/* Copyright 2017 Patrick Roberts */
// dependency injection utility
function inject(callbackfn) {
  const handler = {
    get(target, name) {
      /* this is just a demo, swap these two lines for actual injection */
      // return require(name);
      return { name };
    }
  };
  const proxy = new Proxy({}, handler);

  return (...args) => callbackfn.call(this, proxy, ...args);
}

// usage

// wrap function declaration with inject()
const val = inject(function ({ lodash: _, 'socket.io': sio, jquery: $, express, fs }, other, args) {
  // already have access to lodash, no need to even require() here
  console.log(_);
  console.log(sio);
  console.log($);
  console.log(express);
  console.log(fs);
  console.log(other, args);
});

// execute wrapped function with automatic injection
val('other', 'args');
.as-console-wrapper {
  max-height: 100% !important;
}

工作原理

通过对象解构将参数传递给函数会为对象文字上的每个 属性 调用 getter 方法,以便在执行函数时确定值。

如果被解构的对象被初始化为Proxy,您可以intercept each getter invocation引用试图解析的属性名称,并且return一个您选择用来解决它的值。在这种情况下,解析应该是 require(name),它通过在函数对象参数中将其指定为 属性 名称来注入模块。

下面是一个 link 演示,您可以在其中实际看到它在 Node.js 中运行。

Try it online!

该演示中的代码仅供参考,因为它更大程度地演示了对象解构:

/* MIT License */
/* Copyright 2017 Patrick Roberts */
// dependency injection utility
function inject(callbackfn) {
  const handler = {
    get(target, name) {
      return require(name);
    }
  };
  const proxy = new Proxy({}, handler);

  return (...args) => callbackfn.call(this, proxy, ...args);
}

// usage

// wrap function declaration with inject()
const val = inject(function ({
  fs: { readFile: fsRead, writeFile: fsWrite },
  child_process: { fork: cpF, spawn: cpS, exec: cpE },
  events: { EventEmitter }
}, other, args) {
  // already have access to modules, no need to require() here
  console.log('fs:', { fsRead, fsWrite });
  console.log('child_process:', { fork: cpF, spawn: cpS, exec: cpE });
  console.log('EventEmitter:', EventEmitter);
  console.log(other, args);
});

// execute wrapped function with automatic injection
val('other', 'args');

如上所述,我已经发布了一个完整的 npm 包来实现这个概念。如果您喜欢这种语法并且想要比这个非常基本的示例性能更高且经过测试的东西,我建议您检查一下。

JavaScript 中没有语法支持这种映射。即使编写了自定义函数签名解析器来为 function({lodash:_}) ... 等解构参数提供所需的行为,它也会因转译函数而失败,这是一个主要缺陷。最直接的处理方法是

function foo(lodash){
  const _ = lodash;
  ...
}

而且它显然不适用于 lodash.pick.

这样的无效变量名

DI 配方执行此操作的常见做法是提供注释。所有描述的注释都可以组合在一起。它们特别在 Angular DI 中实现。 Angular 注入器可作为 injection-js 库独立使用(包括 Node)。

注解属性

这样函数签名和依赖项列表就不必匹配了。这个配方可以在 AngularJS 中看到。

属性 包含 DI 令牌列表。它们可以是将使用 require 或其他名称加载的依赖项的名称。

// may be more convenient when it's a string
const ANNOTATION = Symbol();

...

foo[ANNOTATION] = ['lodash'];
function foo(_) {
  ...
}

bar[ANNOTATION] = ['lodash'];
function bar() {
  // doesn't need a param in signature
  const _ = arguments[0];
  ...
}

DI 的执行方式类似

const fnArgs = require('fn-args');
const annotation = foo[ANNOTATION] || fnArgs(foo);
foo(...annotation.map(depName => require(depName));

这种注解风格倾向于使用函数定义,因为为了方便起见,提升允许将注解放在函数签名之上。

数组注释

函数签名和依赖项列表不必匹配。这个菜谱也可以在AngularJS中看到。

当函数表示为数组时,表示它是带注解的函数,其参数应视为注解,最后一个是函数本身。

const foo = [
  'lodash',
  function foo(_) {
  ...
  }
];

...

const fn = foo[foo.length - 1];
const annotation = foo.slice(0, foo.length - 1);
foo(...annotation.map(depName => require(depName));

TypeScript 类型注释

此配方可以在 Angular(2 及更高版本)中看到,并且依赖于 TypeScript 类型。可以从构造函数签名中提取类型并用于 DI。使之成为可能的是 Reflect metadata proposal and TypeScript's own emitDecoratorMetadata feature.

发出的构造函数类型存储为相应 classes 的元数据,可以使用 Reflect API 检索以解决依赖关系。这是基于 class 的 DI,因为装饰器仅在 classes 上受支持,它最适合 DI 容器:

import 'core-js/es7/reflect';

abstract class Dep {}

function di(target) { /* can be noop to emit metadata */ }

@di
class Foo {
  constructor(dep: Dep) {
    ...
  }
}

...

const diContainer = { Dep: require('lodash') };
const annotations = Reflect.getMetadata('design:paramtypes', Foo);
new (Foo.bind(Foo, ...annotations.map(dep => diContainer [dep]))();

这将生成可行的 JS 代码,但会产生类型问题,因为 Lodash 对象不是 Dep 令牌 class 的实例。此方法主要对注入 classes 的 class 依赖项有效。

对于非class DI,需要回退到其他注释。

鉴于答案,我仍然认为 Angular 1.x(和 RequireJS)所做的是最高效的,尽管可能不是最容易使用的:

let  = createSomething('id', ['lodash', function(_){


}]);