【问题标题】:JavaScript Dependency InjectionJavaScript 依赖注入
【发布时间】:2013-12-02 05:30:02
【问题描述】:

我是 JavaScript 新手。我想知道如何在 JavaScript 中实现依赖注入?我搜索了互联网,但找不到任何东西。

【问题讨论】:

  • @qwertynl:从 Yusufaytas 的回答中可以看出,如何自动连接依赖项的机制与在 .NET 中执行此操作完全不同。使用 .NET,您必须反映类型系统,我们根据参数的类型注入类型。使用 Javascript,您必须“反映”参数的名称,因为 javascript 中没有(可发现的)类型。

标签: javascript dependency-injection


【解决方案1】:

让我们通过一个超级简单的真实示例来学习它:)

我要在这里讨论的示例类是一个Printer,它需要一个driver 来打印一些东西。我已经通过 4 个步骤展示了依赖注入设计模式的优势,最终得出最佳解决方案。

案例1:没有使用依赖注入:

class Printer {
   constructor() {
      this.lcd = '';
   }

   /* umm! Not so flexible! */
   print(text) {
     this.lcd = 'printing...';
     console.log(`This printer prints ${text}!`);
   }
}
    
// Usage:
var printer = new Printer();
printer.print('hello');

使用简单,用这种方法制作新打印机很容易,但是这个打印机不灵活。

案例 2:print 方法中的功能抽象为一个名为 Driver 的新类:

class Printer {
  constructor() {
    this.lcd = '';
    this.driver = new Driver();
  }

  print(text) {
    this.lcd = 'printing...';
    this.driver.driverPrint(text);
  }
}

class Driver {
  driverPrint(text) {
    console.log(`I will print the ${text}`);
  }
}

// Usage:
var printer = new Printer();
printer.print('hello');

所以我们的Printer 类现在更模块化干净容易理解,但又不灵活了。任何时候你使用new 关键字,你实际上是在硬编码 的东西。在这种情况下,您正在构建打印机内部的驱动程序,它在现实世界中是带有永远不会更改的内置驱动程序的打印机示例!

案例 3:将已制作的驱动程序注入您的打印机

更好的版本是在我们构建打印机时注入驱动程序 这意味着您可以制作任何类型的打印机,彩色或黑白,因为这 驱动程序被隔离并在Printer 类之外创建的时间,然后 给定(注入!)到Printer

class Printer {
  constructor(driver) {
    this.lcd = '';
    this.driver = driver;
  }

  print(text) {
    this.lcd = 'printing...';
    this.driver.driverPrint(text);
  }
}

class BWDriver {
  driverPrint(text) {
    console.log(`I will print the ${text} in Black and White.`);
  }
}

class ColorDriver {
  driverPrint(text) {
    console.log(`I will print the ${text} in color.`);
  }
}

// Usage:
var bwDriver = new BWDriver();
var printer = new Printer(bwDriver);
printer.print('hello'); // I will print the hello in Black and White.

现在用法不同了,作为用户,为了拥有一台打印机,您首先需要 构造(制作)一个驱动程序(您选择!),然后将此驱动程序传递给您的打印机。看起来最终用户现在需要更多地了解系统,但是这种结构给了他们更多的灵活性。只要有效,用户就可以通过任何驱动程序!例如,假设我们有一个BWDriver(黑白)类型的驱动程序;用户可以创建一个这种类型的新驱动程序,并使用它来制作一台可以打印黑白的新打印机。

到目前为止一切顺利!但是你认为我们可以做得更好,你认为还有一些空间可以在这里解决吗?!我相信你也能看到!

我们正在创建一个新的打印机每次我们需要我们的打印机来打印 不一样的司机!那是因为我们将我们选择的驱动程序传递给 构建时的Printer 类;如果用户想要使用另一个驱动程序他们需要使用该驱动程序创建一个新的打印机。例如,如果我现在想进行彩色打印,我需要这样做:

var cDriver = new ColorDriver();
var printer = new Printer(cDriver); // Yes! This line here is the problem!
printer.print('hello'); // I will print the hello in color.

案例 4: 提供 setter 函数 随时设置打印机的驱动程序!

class Printer {
  constructor() {
    this.lcd = '';
  }

  setDriver(driver) {
    this.driver = driver;
  }

  print(text) {
    this.lcd = 'printing...';
    this.driver.driverPrint(text);
  }
}

class BWDriver {
  driverPrint(text) {
    console.log(`I will print the ${text} in Black and White.`);
  }
}

class ColorDriver {
  driverPrint(text) {
    console.log(`I will print the ${text} in color.`);
  }
}

// Usage:
var bwDriver = new BWDriver();
var cDriver = new ColorDriver();
var printer = new Printer(); // I am happy to see this line only ONCE!

printer.setDriver(bwDriver);
printer.print('hello'); // I will print the hello in Black and White.

printer.setDriver(cDriver);
printer.print('hello'); // I will print the hello in color.

依赖注入并不是一个很难理解的概念。这个词可能有点过头了,但一旦你意识到它的用途,你就会发现自己大部分时间都在使用它。

【讨论】:

  • 您不应将code markup 用于常规文本,这会降低可读性。
  • 好用又全面
  • 干得好!真的很棒的例子!写得很好。
  • 这个答案应该作为构造函数依赖注入到名为 Life 的class 中:-)
  • 我在这个问题上见过的最好的解释之一,谢谢。
【解决方案2】:

对我来说yusufaytas 答案正是我所需要的!唯一缺少的功能是:

  1. 使用自定义参数获取依赖项。
  2. 使用回调注册依赖项。

我希望有能力做这样的事情:

Injector.register('someDependency', function () {
        return new ConcreteDependency();
});

function SomeViewModel(userId, someDependency) {
    this.userId = userId;
    this.someDependency = someDependency;
}

var myVm = Injector.get(SomeViewModel, { "userId": "1234" });

所以我最终得到了以下代码:

var Injector = {

    factories = {},        
    singletons = {},

    register: function (key, factory) {
        this.factories[key] = factory;
    },

    registerSingle: function (key, instance) {
        this.singletons[key] = instance;
    },

    get: function (CTor, params) {            

        var dependencies = this.resolveDependencies(CTor, params);

        // a workaround to allow calling a constructor through .apply
        // see https://stackoverflow.com/questions/1606797/use-of-apply-with-new-operator-is-this-possible
        function MiddlemanCTor() {
            CTor.apply(this, dependencies);
        }

        MiddlemanCTor.prototype = CTor.prototype;

        return new MiddlemanCTor();
    },

    resolveDependencies: function(CTor, params) {
        params = params || {};
        var args = this.getArguments(CTor);

        var dependencies = [];
        for (var i = 0; i < args.length; i++) {
            var paramName = args[i];
            var factory = this.factories[paramName];

            // resolve dependency using:
            // 1. parameters supplied by caller
            // 2. registered factories
            // 3. registered singletons
            var dependency = params[paramName] ||
                (typeof factory === "function" ? factory() : undefined) ||
                this.singletons[paramName];

            dependencies.push(dependency);
        }
        return dependencies;
    }

    getArguments: func(func) {
        // Regex from require.js
        var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
        var args = func.toString().match(FN_ARGS)[1].split(',').map(function (str) {
            return str.trim();
        });
        return args;
    }
};

更新 - 21.5.2018

我已经使用这个解决方案几年了。当我将代码库迁移到 TypeScript 时,解决方案随之发展,同时支持 TypeScript 和 JavaScript。在代码在生产中运行了一段时间后,我最近(两天前)发布了一个基于此解决方案的库。随时查看、打开问题等。

peppermint-di

【讨论】:

    【解决方案3】:

    我编写了自己的名为 Di-Ninja 的 JavaScript 依赖注入框架 https://github.com/di-ninja/di-ninja

    据我所知,它功能齐全,目前是 javascript 中唯一一个实现 Composition-Root 设计模式的, 帮助您将所有事物解耦并将应用程序组件和配置连接到一个唯一的根位置。 http://blog.ploeh.dk/2011/07/28/CompositionRoot/

    它适用于 NodeJS 和 Webpack

    任何反馈将不胜感激

    【讨论】:

    • 如果我错了,请纠正我,但说 di 容器实现组合根设计模式对我来说没有意义。使用 di 容器的方式决定了您使用或不使用组合根设计模式。
    • 它将组合根作为配置实现,这种方法允许您扁平化您的实现规范。它不仅仅是一个容器,它是一个依赖关系解析框架,从上到下执行,深入了解 doc,你会发现它与其他 DiC 的区别以及它是如何以最友好的方式处理组合根的。检查:github.com/di-ninja/di-ninja#3-dependencies-resolution
    【解决方案4】:

    尽管这是一个老问题,但我还是有这种冲动。 ;)

    //dependency injection
    class Thing1 {
        constructor(aThing){
            this.otherThing = aThing;
        }
    }
    class Thing2 {}
    
    const thing = new Thing1(new Thing2())
    
    //dependency inversion
    class Thing1 {
        constructor({
            read = null
        } = {}){
            if(typeof read !== 'function'){
                //establish a simple contract
                throw new TypeError(read + ' is not a function.');
            }
            this._read = read;
            //Somewhere an instance of Thing1()
            //will call this._read()
        }
    }
    
    class Thing2 {
        read(){
           //read something
        }
    }
    
    const thing2 = new Thing2();
    const thing1 = new Thing1({
        read(){
            //Here is the equivalent to the so called "interface"
            return thing2.read();
        }
    });
    

    【讨论】:

      【解决方案5】:

      我是 JavaScript 新手。我想知道如何在 JavaScript 中实现依赖注入?我搜索了互联网,但找不到任何东西。

      老实说,在使用 JavaScript(主要在服务器端)和整个生态系统工作了几年之后,我感觉依赖注入(更不用说容器)并没有真正成为常规 JS程序员的工具箱。这可能是那里没有太多关于它的信息的原因(虽然它正在变得更好)。

      与 Java 等语言相反,您不能依赖 JavaScript 中的静态类型。仅这一事实就排除了通过接口声明依赖关系的传统方式。您当然可以向 JS 添加类型(请参阅Flow),但这些在代码执行之前会被忽略。这同样适用于 TypeScript,但我相信有一种方法可以将类型保留为非强制元数据。此外,JavaScript 不支持注释(尽管有一个 proposal 用于它)。

      人们一直在以各种方式绕过这些限制。一些容器解析函数/类定义(如在传递的函数/类上调用 .toString() 并解析结果字符串)并根据名称查找依赖项,有些容器需要函数/类提供属性/静态方法获取依赖列表。

      我一直在开发一个名为Ashley 的容器,它只是要求将依赖项作为绑定过程的一部分。无需进一步检查。

      container.instance('Client', Client, ['DependencyA', 'DependencyB']);
      container.instance('DependencyA', DependencyA, ['DependencyC']);
      container.instance('DependencyB', DependencyB, ['DependencyC']);
      container.instance('DependencyC', DependencyC, [], {
        scope: 'Prototype', // Defaults to Singleton
        initialize: true,
        deinitialize: true
      });
      
      const client = await container.resolve('Client');
      

      GitHub 上的更多示例。

      【讨论】:

        【解决方案6】:

        我想说 DI 是 JS/ES2015 的开箱即用功能。 :-) 当然,它不是功能齐全的 IOC 容器,但看起来很有用,不是吗?看看下面的例子!

        const one = () => 1;
        const two = ({one}) => one + one;
        const three = ({one, two}) => one + two;
        
        // IOC container
        const decimalNumbers = {
          get one() { return one(this); },
          get two() { return two(this); },
          get three() { return three(this); }
        };
        
        const binaryTwo = ({one}) => one + 9;
        
        // child IOC container
        const binaryNumbers = Object.create(decimalNumbers, {
          two: { get() { return binaryTwo(this); } }
        });
        
        console.log(`${decimalNumbers.three} is ${binaryNumbers.three} in binary`);

        您可以将依赖项包装在 _.once(请参阅 underscorelodash)中以将它们变成单例。

        const rand = function() {
          return (min, max) => min + Math.random() * (max - min) | 0;
        };
        
        const pair = function({rand} = this) {
          return [rand(10, 100), rand(100, 1000)];
        };
        
        // IOC container
        const ioc = Object.create({}, {
          rand: {get: rand},
          pair: {get: _.once(pair)} // singleton
        });
        
        console.log(`${[ioc.pair, ioc.pair === ioc.pair]}`);
        &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"&gt;&lt;/script&gt;

        【讨论】:

          【解决方案7】:

          bubble-di 是一个用于 Javascript 和 Typescript 的轻量级 DI 容器。

          它使您能够注册工厂方法(回调)或实例。下面是一个简单的例子 (more examples)。

          npm install --save bubble-di

          var {DiContainer} = require("bubble-di");
          // import { DiContainer } from "bubble-di";
          
          DiContainer.setContainer(new DiContainer());
          
          class Bar { sayBar(){ console.log("bar"); } }
          class Baz { sayBaz(){ console.log("baz"); } }
          class Foo { 
              constructor (bar, baz)
              {
                  bar.sayBar();
                  baz.sayBaz();
                  // ...
              }
          };
          
          DiContainer.getContainer().registerInstance("bar", new Bar());
          DiContainer.getContainer().registerInstance("baz", new Baz());
          DiContainer.getContainer().register("foo", {
              dependencies: ["bar", "baz"],
              factoryMethod: (bar, baz) => new Foo(bar, baz) },
          );
          const foo = DiContainer.getContainer().resolve("foo"); // will print "bar" and "baz".
          

          【讨论】:

            【解决方案8】:

            Injecting 是一个轻量级但功能强大的 DI 容器,它可以很好地处理 Promise 注入。

            Injecting Homepage

            Source Code 仅 100+ 行。

            Test Cases 查看其示例。

            【讨论】:

              【解决方案9】:

              candiJS 是一个轻量级的隐式依赖注入和对象创建库。 Have a look

              例子:

              candi.provider.singleton('ajax', function() {
                  return {
                      get: function() { /* some code */ },
                      put: function() { /* some code */ }
                  };
              });
              
              candi.provider.singleton('carService', function(ajax) {
                  return {
                      getSpecs: function(manufacturer, year, model, trim) {
                          return ajax.get();
                      }
                  };
              });
              
              var Car = candi.provider.instance('Car', function(carService, year, manufacturer, model, trim) {
                  this.year = year;
                  this.manufacturer = manufacturer;
                  this.model = model;
                  this.trim = trim;
                  this.specs = carService.getSpecs(manufacturer, year, model, trim);
              });
              
              var car = new Car(2009, 'honda', 'accord', 'lx');
              

              【讨论】:

                【解决方案10】:

                在 Flyspeck 掠夺:https://gist.github.com/elfet/11349215

                var c = new Flyspeck();
                
                c.set('name', 'GistHub');
                
                c.set('config', {
                    server: 'https://gist.github.com'
                });
                
                c.set('user', function (c) {
                    return new User(c.get('name'));
                });
                
                c.extend('user', function (user, c) {
                    return new ProxyUser(user);
                });
                
                c.set('app', function (c) {
                    return new Application(c.get('config'), c.get('user'));
                });
                
                var app = c.get('app');
                

                【讨论】:

                  【解决方案11】:

                  您可以使用 AngularJS 作为示例。是不是好事,还是要自己判断。我一周前写了一个 article about demistifying dependency injection in AngularJS。在这里你可以阅读文章中的代码:

                  // The following simplified code is partly taken from the AngularJS source code:
                  // https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L63
                  
                  function inject(fn, variablesToInject) {
                      var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
                      var FN_ARG_SPLIT = /,/;
                      var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
                      var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
                  
                      if (typeof fn === 'function' && fn.length) {
                          var fnText = fn.toString(); // getting the source code of the function
                          fnText = fnText.replace(STRIP_COMMENTS, ''); // stripping comments like function(/*string*/ a) {}
                  
                          var matches = fnText.match(FN_ARGS); // finding arguments
                          var argNames = matches[1].split(FN_ARG_SPLIT); // finding each argument name
                  
                          var newArgs = [];
                          for (var i = 0, l = argNames.length; i < l; i++) {
                              var argName = argNames[i].trim();
                  
                              if (!variablesToInject.hasOwnProperty(argName)) {
                                  // the argument cannot be injected
                                  throw new Error("Unknown argument: '" + argName + "'. This cannot be injected.");
                              }
                  
                              newArgs.push(variablesToInject[argName]);
                          }
                  
                          fn.apply(window, newArgs);
                      }
                  }
                  
                  function sum(x, y) {
                      console.log(x + y);
                  }
                  
                  inject(sum, {
                      x: 5,
                      y: 6
                  }); // should print 11
                  
                  inject(sum, {
                      x: 13,
                      y: 45
                  }); // should print 58
                  
                  inject(sum, {
                      x: 33,
                      z: 1 // we are missing 'y'
                  }); // should throw an error: Unknown argument: 'y'. This cannot be injected.
                  

                  【讨论】:

                    【解决方案12】:
                    var Injector = {
                       dependencies: {},
                       add : function(qualifier, obj){
                          this.dependencies[qualifier] = obj; 
                       },
                       get : function(func){
                          var obj = new func;
                          var dependencies = this.resolveDependencies(func);
                          func.apply(obj, dependencies);
                          return obj;
                       },
                       resolveDependencies : function(func) {
                          var args = this.getArguments(func);
                          var dependencies = [];
                          for ( var i = 0; i < args.length; i++) {
                             dependencies.push(this.dependencies[args[i]]);
                          }
                          return dependencies;
                       },
                       getArguments : function(func) {
                          //This regex is from require.js
                          var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
                          var args = func.toString().match(FN_ARGS)[1].split(',');
                          return args;
                       }
                    };
                    

                    首先我们需要一个配置来提供带有限定符的必要依赖项。为此,我们在 Injector 类中定义了一个依赖集作为依赖。我们使用依赖集作为我们的容器,它将处理映射到限定符的对象实例。为了将带有限定符的新实例添加到依赖集,我们定义了一个 add 方法。之后,我们定义 get 方法来检索我们的实例。在这个方法中,我们首先找到参数数组,然后将这些参数映射到依赖项。之后,我们只需使用依赖项构造对象并返回它。如需更多信息和示例,请参阅我博客上的post

                    【讨论】:

                    • 到目前为止,对我来说,如何在 javascript 中获取函数的参数名称对我来说完全是个谜。你的回答既可怕又酷:-)
                    • 虽然这很好,但“get”函数并没有正确创建对象。我的对象是在包含 'var obj = new func' 的行上创建的,没有传递给它们的参数。将该函数提取到 jsFiddle (jsfiddle.net/JJf2M/2) 中,您可以看到代码将调用构造函数两次。一次没有争论,一次有争论。这个答案:stackoverflow.com/questions/3362471/… 有一个替代策略,它似乎可以解决问题
                    • 因为我有几个项目需要一个简单的依赖注入模块,所以我决定编写一个简单的 commonJS 模块:github.com/nerdbeere/simpledi
                    • 请注意,此示例不会处理箭头函数
                    • 如今大多数代码都被缩小和混淆了。由于此代码基于作为缩小目标的参数名称,因此此代码不会支持此类后处理实践。
                    猜你喜欢
                    • 1970-01-01
                    • 2023-03-18
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2012-01-11
                    • 1970-01-01
                    • 2011-04-08
                    • 2014-06-12
                    相关资源
                    最近更新 更多