【问题标题】:Dependency injection library - renaming injected values依赖注入库 - 重命名注入的值
【发布时间】:2017-11-20 23:23:06
【问题描述】:

我想按名称注入 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() 来获取参数名称。

【问题讨论】:

  • 您想创建一个 DI 库,您可以在其中拥有一个设置了变量的函数并且这些函数是必需的?
  • 我想在按名称注入后重命名变量。 Lodash 匹配依赖名称,但我想让他们从函数范围内自动重命名它。
  • 好的,它与 webpack 一起使用,还是您希望它是通用的?我的意思是所有包都已经加载了,你只是想让它们在函数 scoop 中重命名?
  • @AlexanderMills npm init 默认为 1.0.0。我不知道为什么,但我总是这样做。
  • 好吧,考虑到我用 mocha 测试了所有宣传的功能并且它具有 100% 的代码覆盖率,我想说它已经准备好生产了。

标签: javascript node.js typescript dependency-injection ecmascript-next


【解决方案1】:

更新

这是一个指向npm package di-proxy(受此答案启发)的链接,具有 100% 的代码覆盖率,并支持记忆化以提高性能,与 Node.js >=6.0.0 兼容。

旧答案

这是我在修补 object destructuringProxy 时发现的一个很棒的解决方案:

/* 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 引用试图解析的属性名称,并返回您选择用来解析它的值。在这种情况下,解析应该是require(name),它只是通过在函数对象参数中将其指定为属性名称来注入模块。

下面是一个演示链接,您可以在其中实际看到它在 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 包来实现这个概念。如果您喜欢这种语法并且想要比这个非常基本的示例更高效和经过测试的东西,我建议您检查一下。

【讨论】:

  • 是的,我认为代理功能是做到这一点的唯一方法!
  • @AlexanderMills 公平地说,还有其他解决方案,但它们充其量只是肮脏的黑客攻击,因为它们要求注入的函数是非本地的才能访问其声明为一个字符串,将其传递给某种 AST 生成器并确定要注入的属性。如果不支持Proxy,这一切都是必要的,因此我可能会考虑使用后备来完成所有这些以实现向后兼容性。
  • 最老的支持代理的 JS 和 Node.js 版本是什么?我想知道。出于我的目的,我认为使用 Proxy 很好,因为我只支持 Node.js 版本 > 4.0.0。
  • 呵呵,不知道 TypeScript/Babel 能不能把 Proxy 转译成 ES5……?
  • @AlexanderMills 不,非本地实现是不可能的。
【解决方案2】:

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 和 TypeScript 自己的 emitDecoratorMetadata feature

发出的构造函数类型存储为各个类的元数据,并且可以使用Reflect API 检索以解决依赖关系。这是基于类的 DI,因为装饰器仅在类上受支持,所以它最适用于 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 令牌类的实例。此方法主要对注入到类中的类依赖项有效。

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

【讨论】:

  • 您阅读其他答案了吗?我认为 Proxy 在这里也有一些希望。
  • @AlexanderMills 我做到了。这看起来是个好主意,但不切实际。代理的性能严重限制了它们在实际应用中的潜在用途。考虑到其他 DI 模式的性能损失几乎为零,这种权衡是不公平的。
  • IIRC 我在惩罚之前所做的基准测试是从 x100 到 x1000... 不完全是在经常调用的函数上受欢迎的东西。
  • 是的,在这种情况下它可能会被频繁调用,所以是的
  • 我认为 Proxy 最适合用于性能不那么关键的测试
【解决方案3】:

我做了一些可能对你有用的事情, 但你可以随时改变它并使用一般的想法。

它是用 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);
});

在底线中,您将函数称为“di” 传入这些参数:

di("_", lodash, callback);

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

【讨论】:

    【解决方案4】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-09-21
      • 1970-01-01
      • 2019-04-26
      • 2023-02-20
      • 1970-01-01
      • 1970-01-01
      • 2011-01-10
      • 1970-01-01
      相关资源
      最近更新 更多