我需要在NodeJS中使用依赖注入吗,还是该如何处理...?

294

我目前正在使用Node.js创建一些实验性项目。我之前用Spring编写了很多Java EE Web应用程序,喜欢那里的依赖注入的方便性。

现在我很好奇:如何在Node.js中实现依赖注入?或者说:我是否需要它?因为编程风格是不同的,是否有替代概念?

我主要讨论一些简单的事情,比如共享数据库连接对象,但迄今为止我还没有找到令我满意的解决方案。


1
如果你决定使用 DI,OpenTable 最近开源了一个相关库:https://github.com/opentable/spur-ioc。我曾经在那里工作并使用过它,可以说它非常简单,并且非常适合进行测试。 - tybro0103
22个回答

143
简言之,你不需要像在C#/Java中那样使用依赖注入容器或服务定位器。由于Node.js利用模块模式,因此不需要进行构造函数或属性注入,但仍然可以这样做。JavaScript的优点是你可以修改几乎任何东西以实现你想要的结果。当涉及到测试时,这非常方便。以下是我非常无聊的编造示例。MyClass.js:
var fs = require('fs');

MyClass.prototype.errorFileExists = function(dir) {
    var dirsOrFiles = fs.readdirSync(dir);
    for (var d of dirsOrFiles) {
        if (d === 'error.txt') return true;
    }
    return false;
};

MyClass.test.js:

describe('MyClass', function(){
    it('should return an error if error.txt is found in the directory', function(done){
        var mc = new MyClass();
        assert(mc.errorFileExists('/tmp/mydir')); //true
    });
});

注意到 MyClass 依赖于 fs 模块了吗? 正如 @ShatyemShekhar 提到的,你确实可以像其他语言一样进行构造函数或属性注入。但在 Javascript 中并非必要。

在这种情况下,你可以有两种选择。

你可以存根 fs.readdirSync 方法,或者在调用 require 时返回一个完全不同的模块。

方法 1:

var oldmethod = fs.readdirSync;
fs.readdirSync = function(dir) { 
    return ['somefile.txt', 'error.txt', 'anotherfile.txt']; 
};

*** PERFORM TEST ***
*** RESTORE METHOD AFTER TEST ****
fs.readddirSync = oldmethod;

方法2:

var oldrequire = require
require = function(module) {
    if (module === 'fs') {
        return {
            readdirSync: function(dir) { 
                return ['somefile.txt', 'error.txt', 'anotherfile.txt']; 
            };
        };
    } else
        return oldrequire(module);
            
}

关键是利用Node.js和Javascript的强大功能。请注意,我更擅长使用CoffeeScript,因此我的JS语法可能有错误。同时,我并不是说这是最好的方法,但它是一种方法。Javascript专家们可能会有其他解决方案。

更新:

这应该能够解决您关于数据库连接的具体问题。我会创建一个单独的模块来封装您的数据库连接逻辑。类似这样:

MyDbConnection.js:(记得选择一个更好的名字)

var db = require('whichever_db_vendor_i_use');

module.exports.fetchConnection() = function() {
    //logic to test connection
    
    //do I want to connection pool?
    
    //do I need only one connection throughout the lifecyle of my application?
    
    return db.createConnection(port, host, databasename); //<--- values typically from a config file    
}

接下来任何需要数据库连接的模块都将包含您的 MyDbConnection 模块。

SuperCoolWebApp.js:

var dbCon = require('./lib/mydbconnection'); //wherever the file is stored

//now do something with the connection
var connection = dbCon.fetchConnection(); //mydbconnection.js is responsible for pooling, reusing, whatever your app use case is

//come TEST time of SuperCoolWebApp, you can set the require or return whatever you want, or, like I said, use an actual connection to a TEST database. 

不要照字面意思去跟随这个示例。它只是试图传达您如何使用module模式来管理依赖关系的一个无聊的例子。希望这可以更有帮助一些。


76
在测试方面确实如此,但依赖注入还有其他好处:通过使用依赖注入,您可以编写针对接口而非实现的程序。 - moteutsch
26
我该如何编写一个使用日志记录器但不依赖于任何库的组件?如果我使用require('my_logger_library'),那么使用我的组件的人将不得不覆盖该语句以使用自己的库。相反,我可以允许人们传递一个回调函数,将日志记录器实现包装到组件的“构造函数”或“初始化”方法中。这就是依赖注入(DI)的目的。 - moteutsch
4
我不明白,将一个模块中的 require 替换并不会在另一个模块中替换它。如果我在测试中将 require 设置为一个函数,然后需要被测试的模块,那么被测试对象中的 require 语句不会使用在测试模块中设置的函数。这样怎样注入依赖? - HMR
15
这个“示例”的一个重要问题是实际上创建和连接依赖本身就很复杂。展示 MyClass 依赖于可以很容易被 mock 的 fs 是一回事,但 DI 不是关于模拟,而是关于 MyClass 不应该也不能被要求知道如何设置依赖链。如果 'fs' 依赖于其他东西,而其他东西又依赖于其他东西等等。对于寻找真正的 DI 的人来说,这根本不是答案。 - dmansfield
显示剩余16条评论

110
我知道这个帖子相当古老了,但是我认为我应该谈谈我的想法。简言之,由于 JavaScript 是一种无类型、动态的语言,你实际上可以在不使用依赖注入(DI)模式或使用 DI 框架的情况下完成很多工作。然而,随着应用程序变得越来越大和复杂,DI 肯定可以帮助你提高代码的可维护性。

C#中的DI

为了理解为什么 JavaScript 中不需要 DI,看看像 C# 这样的强类型语言会很有帮助。(对不知道 C# 的人表示歉意,但应该很容易理解。)假设我们有一个描述汽车及其喇叭的应用程序。你需要定义两个类:
class Horn
{
    public void Honk()
    {
        Console.WriteLine("beep!");
    }
}

class Car
{
    private Horn horn;

    public Car()
    {
        this.horn = new Horn();
    }

    public void HonkHorn()
    {
        this.horn.Honk();
    }
}

class Program
{
    static void Main()
    {
        var car = new Car();
        car.HonkHorn();
    }
}

用这种方式编写代码存在一些问题。

  1. Car类与Horn类中喇叭的特定实现紧密耦合。如果我们想要更改汽车使用的喇叭类型,即使其使用的喇叭没有变化,也必须修改Car类。这也使得测试变得困难,因为我们无法将Car类与其依赖项Horn类隔离地测试。
  2. Car类负责Horn类的生命周期。在这个简单的例子中,这不是一个大问题,但在真正的应用程序中,依赖关系会有依赖关系,依此类推。 Car类需要负责创建其所有依赖关系的树。这不仅复杂而且重复,而且违反了类的“单一职责”。它应该专注于成为一辆汽车,而不是创建实例。
  3. 没有办法重用相同的依赖项实例。同样,在这个玩具应用程序中这并不重要,但考虑数据库连接。通常会有一个共享整个应用程序的单个实例。
现在,让我们重构它以使用依赖注入模式。
interface IHorn
{
    void Honk();
}

class Horn : IHorn
{
    public void Honk()
    {
        Console.WriteLine("beep!");
    }
}

class Car
{
    private IHorn horn;

    public Car(IHorn horn)
    {
        this.horn = horn;
    }

    public void HonkHorn()
    {
        this.horn.Honk();
    }
}

class Program
{
    static void Main()
    {
        var horn = new Horn();
        var car = new Car(horn);
        car.HonkHorn();
    }
}

我们在这里做了两件关键的事情。首先,我们引入了一个接口,Horn类实现了该接口。这使得我们可以将Car类编码为接口而不是特定的实现。现在,代码可以接受任何实现IHorn的东西。其次,我们将喇叭实例化从Car中移出,并进行传递。这解决了上述问题,并将特定实例及其生命周期的管理留给应用程序的主函数。
这意味着我们可以引入一种新型号的汽车喇叭而不触及Car类:
class FrenchHorn : IHorn
{
    public void Honk()
    {
        Console.WriteLine("le beep!");
    }
}

主函数可以直接注入FrenchHorn类的实例。这也大大简化了测试。您可以创建一个MockHorn类来注入到Car构造函数中,以确保您仅在隔离的Car类中进行测试。
上面的示例展示了手动依赖注入。通常使用框架(例如C#世界中的UnityNinject)进行DI。这些框架将通过遍历您的依赖图并根据需要创建实例来完成所有依赖关系的连接。

标准Node.js方法

现在让我们看一下在Node.js中相同示例的情况。我们可能会将代码分成3个模块:
// horn.js
module.exports = {
    honk: function () {
        console.log("beep!");
    }
};

// car.js
var horn = require("./horn");
module.exports = {
    honkHorn: function () {
        horn.honk();
    }
};

// index.js
var car = require("./car");
car.honkHorn();

因为JavaScript是无类型的,所以我们没有像以前那样紧密的耦合。没有必要使用接口(也不存在),因为car模块将尝试在horn模块导出的任何内容上调用honk方法。
此外,由于Node的require缓存了所有内容,模块本质上是存储在容器中的单例。对horn模块执行require的任何其他模块都将获得完全相同的实例。这使得共享单例对象(例如数据库连接)非常容易。
现在仍然存在一个问题,即car模块负责获取其自己的依赖项horn。如果您希望汽车使用不同的模块作为其喇叭,则必须更改car模块中的require语句。这并不是很常见的事情,但它确实会导致测试方面的问题。

人们通常处理测试问题的方式是使用proxyquire。由于JavaScript的动态特性,proxyquire拦截对require的调用,并返回您提供的任何存根/模拟。

var proxyquire = require('proxyquire');
var hornStub = {
    honk: function () {
        console.log("test beep!");
    }
};

var car = proxyquire('./car', { './horn': hornStub });

// Now make test assertions on car...

这对大多数应用来说已经足够了。如果它适用于你的应用,请使用它。然而,根据我的经验,在应用程序变得越来越大、越来越复杂时,像这样维护代码变得更加困难。
JavaScript 中的 DI
Node.js 非常灵活。如果您对上述方法不满意,可以使用依赖注入模式编写自己的模块。在这种模式下,每个模块都会导出一个工厂函数(或类构造函数)。
// horn.js
module.exports = function () {
    return {
        honk: function () {
            console.log("beep!");
        }
    };
};

// car.js
module.exports = function (horn) {
    return {
        honkHorn: function () {
            horn.honk();
        }
    };
};

// index.js
var horn = require("./horn")();
var car = require("./car")(horn);
car.honkHorn();

这与之前的C#方法非常类似,index.js模块负责实例生命周期和连接。单元测试非常简单,你可以只需将mocks/stubs传递给函数即可。如果这对你的应用程序足够好,请使用它。

Bolus DI框架

与C#不同的是,没有既定的标准DI框架来帮助管理依赖关系。npm注册表中有许多框架,但没有被广泛采用的。这些选项中的许多已经在其他答案中提到了。
我对现有的任何选项都不是特别满意,所以我写了自己的bolus。Bolus旨在与上述DI风格的代码配合使用,并尽可能地DRY和简单。使用完全相同的car.jshorn.js模块,你可以使用bolus重写index.js模块:
// index.js
var Injector = require("bolus");
var injector = new Injector();
injector.registerPath("**/*.js");

var car = injector.resolve("car");
car.honkHorn();

基本思想是创建一个注入器。您在注入器中注册所有模块。然后只需解决您所需的问题。Bolus将遍历依赖关系图,并根据需要创建和注入依赖项。在玩具示例中,您不会节省多少,但在具有复杂依赖树的大型应用程序中,节省非常巨大。
Bolus支持许多方便的功能,例如可选依赖项和测试全局变量,但相对于标准Node.js方法,我看到了两个关键好处。首先,如果您有许多类似的应用程序,可以为其基础创建一个私有npm模块,该模块创建一个注入器并在其中注册有用的对象。然后,您的特定应用程序可以根据需要添加、覆盖和解决,就像 AngularJS的注入器一样。其次,您可以使用bolus来管理各种依赖关系上下文。例如,您可以使用中间件为每个请求创建一个子注入器,在注入器上注册用户ID、会话ID、记录器等以及任何依赖于这些内容的模块。然后解决您需要服务请求的内容。这为您提供了每个请求的模块实例,并防止将记录器等传递给每个模块函数调用。

1
还有一些替代proxyquire的方法,比如sinon,它允许您进行非常简洁的模拟,例如 let readFileStub = sinon.stub(fs, 'readFile').yields(new Error('something went wrong'));,然后对fs.readFile的后续调用将返回错误,直到您通过 readFileStub.restore() 恢复存根。就个人而言,我认为DI的用途值得怀疑,因为我觉得它几乎需要使用类/对象,这是对JavaScript功能倾向的一个可疑假设。 - Kevin
5
很好的回答。我想知道你在2019年的想法。对于大型项目,出于个人偏好,你更喜欢在Node中使用DI/IoC还是只使用“jest”、“rewire”、“proxyquire”等工具进行存根/模拟?谢谢。 - user6233283

102
  • require()和最近的ES模块(import)是在Node.js中管理依赖项的方法,它直观且有效,但也有其局限性。
  • 我的建议是看看今天为Node.js提供的一些依赖注入容器,以了解它们的优缺点。其中一些如下:
  • 现在真正的问题是,与简单的require()import相比,使用Node.js DI容器可以实现什么?
  • 优点:
    • 更好的可测试性:模块接受其依赖项作为输入参数
    • 控制反转:决定如何连接模块而不触及应用程序的主要代码。
    • 可定制的模块解析算法:依赖关系具有“虚拟”标识符,通常不绑定到文件系统上的路径。
    • 更好的可扩展性:由IoC和“虚拟”标识符启用。
    • 其他花哨的东西也可能是可能的:
      • 异步初始化
      • 模块生命周期管理
      • DI容器本身的可扩展性
      • 可以轻松实现更高级别的抽象(例如AOP)
  • 缺点:
    • 相较于Node.js的“经验”,使用DI确实感觉偏离了Node的思考方式。
    • 依赖关系和其实现之间的关系并不总是显式的。一个依赖可能会在运行时解析,并受到各种参数的影响。这使得代码变得更难理解和调试。
    • 启动时间较慢。
    • 大多数DI容器与模块打包工具(如Browserify和Webpack)不兼容。

    就像与软件开发相关的任何事情一样,选择DI还是require()/import取决于您的要求、系统的复杂性和编程风格。


    3
    你认为自2009年以来情况是否发生了相当大的变化? - Juho Vepsäläinen
    21
    你的意思是自从10天前开始吗? :) - Mario
    5
    Nooo. Dec 9... Should have known. 不,12月9日……早该知道的。 - Juho Vepsäläinen
    5
    我使用类似于 "module.exports = function(deps) {}" 的模式来实现 DI。是的,它能够工作,但并不是很理想。 - Juho Vepsäläinen
    5
    “modules accepts their dependencies as input”和“Dependencies are not explicit”听起来对我来说像是矛盾的。 (注:原文没有上下文,因此我的回答可能不完全准确。) - Anton Rudeshko
    显示剩余5条评论

    37

    我还编写了一个模块来实现这个,它叫做rewire。只需使用npm install rewire然后:

    var rewire = require("rewire"),
        myModule = rewire("./path/to/myModule.js"); // exactly like require()
    
    // Your module will now export a special setter and getter for private variables.
    myModule.__set__("myPrivateVar", 123);
    myModule.__get__("myPrivateVar"); // = 123
    
    
    // This allows you to mock almost everything within the module e.g. the fs-module.
    // Just pass the variable name as first parameter and your mock as second.
    myModule.__set__("fs", {
        readFile: function (path, encoding, cb) {
            cb(null, "Success!");
        }
    });
    myModule.readSomethingFromFileSystem(function (err, data) {
        console.log(data); // = Success!
    });
    

    我受到Nathan MacInnes的injectr的启发,但采用了不同的方法。我不使用vm来评估测试模块,实际上我使用的是node自带的require。这样你的模块就像使用require()一样(除了你的修改)。同时完全支持调试。

    7
    截至2014年中期,https://www.npmjs.org/package/proxyquire 可以轻松地模拟“require”依赖项。 - arcseldon
    proxyquire也很棒!他们现在使用内部的“module”模块,这比使用node的vm要好得多。但归根结底,这只是一种风格问题。我喜欢我的模块使用原始的require,并稍后替换依赖项。此外,rewire还允许覆盖全局变量。 - Johannes Ewald
    非常有趣,一直在寻找这样的东西来用于工作,这个模块是否也会影响下游模块? - akst
    针对 proxyquire,在描述中说它用于测试,“代理 nodejs require 以允许在测试期间覆盖依赖项。” 不是用于 DI(依赖注入),对吗? - Marwen Trabelsi

    17
    我为此特别构建了 Electrolyte。其他依赖注入解决方案对我的口味来说过于具有侵略性,而且干扰全局 require 是我的一个特别不满。
    Electrolyte采用模块化方法,特别是那些像Connect / Express中间件中看到的导出“setup”函数的模块。基本上,这些类型的模块只是返回某个对象的工厂。
    例如,创建数据库连接的模块:
    var mysql = require('mysql');
    
    exports = module.exports = function(settings) {
      var connection = mysql.createConnection({
        host: settings.dbHost,
        port: settings.dbPort
      });
    
      connection.connect(function(err) {
        if (err) { throw err; }
      });
    
      return connection;
    }
    
    exports['@singleton'] = true;
    exports['@require'] = [ 'settings' ];
    

    你在底部看到的是注释,这是Electrolyte用来实例化和注入依赖项的额外元数据,自动将应用程序组件连接在一起。
    要创建数据库连接:
    var db = electrolyte.create('database');
    

    Electrolyte会传递遍历@require的依赖项,并将实例作为导出函数的参数注入。
    关键在于这是最小程度的侵入性。该模块完全可用,与Electrolyte本身无关。这意味着您的单元测试可以测试仅测试模块,传入模拟对象而无需额外的依赖项来重新连接内部。
    在运行完整应用程序时,Electrolyte在模块间级别介入,将事物连接在一起,无需全局变量、单例或过多的管道。

    1
    你能否澄清一下当调用connect()抛出异常时,你所发布的代码会发生什么?虽然我不熟悉Node的MySql API,但我预期这个调用是异步的,因此这个示例并不十分清晰。 - Andrey Agibalov
    目前正在使用电解质。您声称可以通过exports['@require']轻松注入模块。但是,如果我必须存根所需的模块之一,那么如何在电解质中实现呢?目前,如果我们需要模块,这可以很容易地实现。但对于电解质来说,这是一个巨大的负面因素...您是否有示例,可以在测试用例中使用存根版本的模块并在实例化/ ioc.use时动态传递它。因此,在单元测试中,如果我们可以执行ioc.create('modulename'),然后进行依赖模块(但是存根的)的注入,那将是理想的... - user1102171
    1
    你不应该在单元测试中调用 ioc.create。单元测试应该仅测试被测模块,而不引入其他依赖项,包括 Electrolyte。按照这个建议,你应该使用 objToTest = require('modulename')(mockObj1, mockObj2); - Jared Hanson

    12

    我自己查看了一下。我不喜欢引入提供机制来劫持我的模块导入的魔术依赖工具库。相反,我为我的团队提出了一个“设计指南”,以明确声明可以通过在我的模块中引入工厂函数导出来模拟的依赖项。

    我广泛使用ES6特性进行参数和解构,以避免一些样板代码,并提供命名的依赖项覆盖机制。

    这里有一个例子:

    import foo from './utils/foo';
    import bob from './utils/bob';
    
    // We export a factory which accepts our dependencies.
    export const factory = (dependencies = {}) => {
      const {
        // The 'bob' dependency.  We default to the standard 'bob' imp if not provided.
        $bob = bob, 
        // Instead of exposing the whole 'foo' api, we only provide a mechanism
        // with which to override the specific part of foo we care about.
        $doSomething = foo.doSomething // defaults to standard imp if none provided.
      } = dependencies;  
    
      return function bar() {
        return $bob($doSomething());
      }
    }
    
    // The default implementation, which would end up using default deps.
    export default factory();
    

    以下是它的使用示例:

    import { factory } from './bar';
    
    const underTest = factory({ $bob: () => 'BOB!' }); // only override bob!
    const result = underTest();
    

    对于那些不熟悉ES6语法的人,请原谅。


    非常巧妙! - arnold
    这完全是一个手动处理的容器。
    • 这基本上让我们面临手动容器(集中式DI)与容器库的选择(也是集中式DI)。- 那些库有什么问题?
    - undefined

    4
    事实上,由于JavaScript是一种非常动态的编程语言,几乎可以在运行时修改任何东西,因此您可以在没有IoC容器的情况下测试您的node.js。请考虑以下内容:
    import UserRepository from "./dal/user_repository";
    
    class UserController {
        constructor() {
            this._repository = new UserRepository();
        }
        getUsers() {
            this._repository.getAll();
        }
    }
    
    export default UserController;
    

    因此,您可以在运行时覆盖组件之间的耦合。我认为我们应该努力使JavaScript模块解耦。

    实现真正的解耦唯一的方法是删除对UserRepository的引用:

    class UserController {
        constructor(userRepository) {
            this._repository = userRepository;
        }
        getUsers() {
            this._repository.getAll();
        }
    }
    
    export default UserController;
    

    这意味着您需要在其他地方进行对象组合:
    import UserRepository from "./dal/user_repository";
    import UserController from "./dal/user_controller";
    
    export default new UserController(new UserRepository());
    

    我喜欢将对象组成委托给IoC容器的想法。您可以在文章The current state of dependency inversion in JavaScript中了解更多关于这个想法的内容。该文章试图揭穿一些“JavaScript IoC容器神话”:

    神话1:JavaScript中没有IoC容器的位置

    神话2:我们不需要IoC容器,我们已经有模块加载器!

    神话3:依赖反转===注入依赖项

    如果您也喜欢使用IoC容器的想法,可以看看InversifyJS。最新版本(2.0.0)支持许多用例:
    • 内核模块
    • 内核中间件
    • 使用类、字符串字面量或符号作为依赖标识符
    • 注入常量值
    • 注入类构造函数
    • 注入工厂
    • 自动工厂
    • 提供者注入(异步工厂)
    • 激活处理程序(用于注入代理)
    • 多重注入
    • 带标签的绑定
    • 自定义标签装饰器
    • 命名绑定
    • 上下文绑定
    • 友好的异常(例如循环依赖关系)
    您可以在InversifyJS了解更多信息。

    4

    我最近查看了这个帖子,和OP一样的原因是,我遇到的大多数库都会暂时重写require语句。我在使用这种方法时成功的程度参差不齐,所以我最终采用了以下方法。

    在express应用程序的上下文中,我将app.js包装在bootstrap.js文件中:

    var path = require('path');
    var myapp = require('./app.js');
    
    var loader = require('./server/services/loader.js');
    
    // give the loader the root directory
    // and an object mapping module names 
    // to paths relative to that root
    loader.init(path.normalize(__dirname), require('./server/config/loader.js')); 
    
    myapp.start();
    

    传递给加载器的对象映射如下所示:
    // live loader config
    module.exports = {
        'dataBaseService': '/lib/dataBaseService.js'
    }
    
    // test loader config
    module.exports = {
        'dataBaseService': '/mocks/dataBaseService.js'
        'otherService' : {other: 'service'} // takes objects too...
    };
    

    然后,与其直接调用require(...),我们可以将模块内容分配给一个局部变量:
    var myDatabaseService = loader.load('dataBaseService');
    

    如果加载器中没有别名,它将默认为常规 require。这有两个好处:我可以替换任何版本的类,并且它消除了在整个应用程序中使用相对路径名称的需要(因此如果我需要在当前文件下面或上面使用自定义 lib,则不需要遍历,并且 require 将缓存模块以针对相同的键)。它还允许我在应用程序的任何地方指定模拟对象,而不是在立即测试套件中指定。我刚刚发布了一个小型的 npm 模块以方便使用:https://npmjs.org/package/nodejs-simple-loader

    2

    对于ES6,我开发了这个容器 https://github.com/zazoomauro/node-dependency-injection

    (说明:该句为原文,已无需翻译)
    import {ContainerBuilder} from 'node-dependency-injection'
    
    let container = new ContainerBuilder()
    container.register('mailer', 'Mailer')
    

    然后,您可以在容器中设置例如交通方式的选择:

    import {ContainerBuilder} from 'node-dependency-injection'
    
    let container = new ContainerBuilder()
    container
      .register('mailer', 'Mailer')
      .addArgument('sendmail')
    

    现在这个类更加灵活了,因为你已经将交通工具的选择从实现中分离出来,放到了容器中。

    现在邮件服务已经在容器中,你可以将其注入到其他类中作为依赖项。如果你有一个类似于 NewsletterManager 的类:

    class NewsletterManager {
        construct (mailer, fs) {
            this._mailer = mailer
            this._fs = fs
        }
    }
    
    export default NewsletterManager
    

    在定义newsletter_manager服务时,mailer服务尚不存在。使用Reference类来告诉容器在初始化newsletter manager时注入mailer服务:
    import {ContainerBuilder, Reference, PackageReference} from 'node-dependency-injection'
    import Mailer from './Mailer'
    import NewsletterManager from './NewsletterManager'
    
    let container = new ContainerBuilder()
    
    container
      .register('mailer', Mailer)
      .addArgument('sendmail')
    
    container
      .register('newsletter_manager', NewsletterManager)
      .addArgument(new Reference('mailer'))
      .addArgument(new PackageReference('fs-extra'))
    

    你可以使用配置文件,如Yaml、Json或JS文件来设置容器。
    服务容器可以编译出多种版本。这些版本包括检查潜在问题(如循环引用)和使容器更加高效等原因。
    container.compile()
    

    1

    与其他平台一样,Node.js同样需要依赖注入。如果你正在构建一个庞大的项目,使用依赖注入将使得模拟代码依赖项和彻底测试代码变得更加容易。

    例如,你的数据库层模块不应该只在业务代码模块中被引用,因为当对这些业务代码模块进行单元测试时,DAO会加载并连接到数据库。

    一种解决方案是将依赖项作为模块参数传递:

    module.exports = function (dep1, dep2) {
    // private methods
    
       return {
        // public methods
           test: function(){...}
       }
    }
    

    这样做可以轻松、自然地模拟依赖项,让您专注于测试代码,而不使用任何棘手的第三方库。
    还有其他解决方案(如broadway、architect等),它们可以帮助您完成此操作。但它们可能会做更多的事情或使用更多的混乱。

    几乎是通过自然演化,我最终也做了同样的事情。我将一个依赖项作为参数传递,并且这对于测试非常有效。 - munkee

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接