我是否需要NodeJS中的依赖注入,或者如何处理...?

Eri*_*rik 208 dependency-injection inversion-of-control node.js

我目前正在使用nodejs创建一些实验项目.我已经用Spring编写了很多Java EE Web应用程序,并且很欣赏那里的依赖注入的简易性.

现在我很好奇:如何使用节点进行依赖注入?或者:我甚至需要它吗?是否有替换概念,因为编程风格不同?

我说的是简单的事情,比如分享数据库连接对象,到目前为止,但我还没有找到满足我的解决方案.

JP *_*son 102

简而言之,您不需要像在C#/ Java中那样使用依赖注入容器或服务定位器.由于Node.js利用了module pattern它,因此不必执行构造函数或属性注入.虽然你还可以.

关于JS的好处是你可以修改任何东西来实现你想要的东西.这在测试方面很方便.

看看我非常蹩脚的人为例子.

MyClass.js:

var fs = require('fs');

MyClass.prototype.errorFileExists = function(dir) {
    var dirsOrFiles = fs.readdirSync(dir);
    for (var d in dirsOrFiles) {
        if (d === 'error.txt') return true;
    }
    return false;
};
Run Code Online (Sandbox Code Playgroud)

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
    });
});
Run Code Online (Sandbox Code Playgroud)

注意如何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;
Run Code Online (Sandbox Code Playgroud)

方法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);

}
Run Code Online (Sandbox Code Playgroud)

关键是要利用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    
}
Run Code Online (Sandbox Code Playgroud)

然后,任何需要数据库连接的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. 
Run Code Online (Sandbox Code Playgroud)

不要逐字地遵循这个例子.这是一个蹩脚的例子,试图通过您利用module模式来管理您的依赖关系.希望这会有所帮助.

  • 在测试方面也是如此,但DI有其他好处; 通过使用DI,您可以编程到接口,而不是实现. (39认同)
  • @JPRichardson如何在不依赖任何一个库的情况下编写使用记录器的组件?如果我'require('my_logger_library')`,使用我的组件的人将不得不覆盖require以使用他们自己的库.相反,我可以允许人们将包含记录器实现的回调传递给组件"constructor"或"init"方法.这就是DI的目的. (14认同)
  • 截至2014年中期 - https://www.npmjs.org/package/proxyquire使得模拟"需要"依赖关系变得微不足道. (4认同)
  • 我不明白,在一个模块中替换require不会在另一个模块中替换它.如果我在测试中设置了require函数然后要求测试模块,则要测试的对象中的require语句不使用测试模块中设置的函数.这如何注入依赖关系? (4认同)
  • @moteutsch 不知道为什么会这样,因为 JS 没有像大多数静态语言那样的接口概念。您真正拥有的只是实现,即使您想使用一些预先商定的记录在案的“接口”。 (3认同)
  • 所有描述的方法完全违背TDD哲学.很难理解并将测试错误地与您的实现联系起来.我不建议遵循以下任何建议. (2认同)
  • 这个"例子"的一个主要问题是,实际上,问题是创建和连接依赖本身很复杂.显示MyClass依赖于可轻易模拟的fs是一回事.但DI不是关于嘲笑,而是关于MyClass不应该也不需要知道如何设置依赖链的事实.如果'fs'取决于其他依赖于其他东西的东西等.从寻找真正DI的人来看,这根本不是答案. (2认同)

Mar*_*rio 69

require是在Node.js中管理依赖关系方式,当然它是直观和有效的,但它也有其局限性.

我的建议是看看今天可用于Node.js的一些依赖注入容器,以了解它们的优缺点.他们之中有一些是:

仅举几个.

现在真正的问题是,使用Node.js DI容器可以实现什么,与简单相比require

优点:

  • 更好的可测试性:模块接受它们的依赖关系作为输入
  • 控制反转:决定如何在不触及应用程序主代码的情况下连接模块.
  • 用于解析模块的可定制算法:依赖项具有"虚拟"标识符,通常它们不绑定到文件系统上的路径.
  • 更好的可扩展性:由IoC和"虚拟"标识符启用.
  • 其他花哨的东西可能:
    • 异步初始化
    • 模块生命周期管理
    • DI容器本身的可扩展性
    • 可以轻松实现更高级别的抽象(例如AOP)

缺点:

  • 与Node.js"体验"不同:不使用require肯定感觉就像你偏离了Node的思维方式.
  • 依赖项与其实现之间的关系并不总是明确的.可以在运行时解决依赖性并且受各种参数的影响.代码变得更难以理解和调试
  • 启动时间较慢
  • 成熟度(目前):目前没有一种解决方案真的很受欢迎,因此没有那么多教程,没有生态系统,没有经过测试.
  • 一些DI容器不能与Browserify和Webpack等模块捆绑器一起使用.

与软件开发相关的任何内容一样,在DI之间进行选择,或者require取决于您的要求,系统复杂性和编程风格.

  • 你的意思是10天前?:) (12认同)
  • 我使用module.exports = function(deps){}种模式"实现"DI.是的,它有效,但它不太理想. (4认同)
  • 你认为自09年以来情况发生了很大变化吗? (3认同)
  • *模块接受它们的依赖关系作为输入*和*依赖关系不明确*听起来像是一个矛盾. (3认同)
  • 拿去.12月9日......应该知道. (2认同)

Dav*_*son 47

我知道这个帖子在这一点上已经很老了,但我想我会想到这个问题.TL; DR是由于JavaScript的无类型动态特性,实际上可以做很多事情而不依赖于依赖注入(DI)模式或使用DI框架.但是,随着应用程序变得越来越大,越来越复杂,DI肯定有助于代码的可维护性.

DI中的C#

要理解为什么DI在JavaScript中没有那么大的需求,看看像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();
    }
}
Run Code Online (Sandbox Code Playgroud)

以这种方式编写代码几乎没有问题.

  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();
    }
}
Run Code Online (Sandbox Code Playgroud)

我们在这里做了两件关键的事情.首先,我们介绍了我们的Horn类实现的接口.这使我们可以将Car类编码到接口而不是特定的实现.现在代码可以采取任何实现IHorn.其次,我们已经将喇叭实例化出来Car并将其传递出去.这解决了上述问题,并将其留给应用程序的主要功能来管理特定实例及其生命周期.

这意味着可以为汽车引入一种新型喇叭,而无需触及Car班级:

class FrenchHorn : IHorn
{
    public void Honk()
    {
        Console.WriteLine("le beep!");
    }
}
Run Code Online (Sandbox Code Playgroud)

主要可以只注入FrenchHorn类的实例.这也大大简化了测试.您可以创建一个MockHorn类来注入Car构造函数,以确保您只是Car孤立地测试该类.

上面的示例显示了手动依赖注入.通常DI使用框架(例如C#世界中的UnityNinject)完成.这些框架将通过遍历依赖关系图并根据需要创建实例来为您执行所有依赖关系连接.

标准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();
Run Code Online (Sandbox Code Playgroud)

因为JavaScript是无类型的,所以我们没有完全相同的紧耦合.不需要接口(也不存在),因为car模块将尝试honkhorn模块导出的任何内容上调用该方法.

另外,因为Node require缓存了所有内容,所以模块本质上是存储在容器中的单例.其执行任何其他模块require上的horn模块将得到完全相同的实例.这使得共享单个对象(如数据库连接)非常容易.

现在仍然存在car模块负责获取其自身依赖性的问题horn.如果您希望汽车为其号角使用不同的模块,则必须更改模块中的require语句car.这不是很常见的事情,但确实会导致测试问题.

人们处理测试问题的常用方法是使用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...
Run Code Online (Sandbox Code Playgroud)

这对于大多数应用来说已经足够了.如果它适用于您的应用程序,那么请使用它.但是,根据我的经验,随着应用程序变得越来越大,越来越复杂,维护这样的代码会变得更难.

在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();
Run Code Online (Sandbox Code Playgroud)

这与之前的C#方法非常相似,index.js模块负责实例生命周期和布线.单元测试非常简单,因为您可以将模拟/存根传递给函数.再次,如果这对你的应用程序来说足够好,那就去吧.

Bolus DI框架

与C#不同,没有建立标准的DI框架来帮助您进行依赖关系管理.npm注册表中有许多框架,但没有一个被广泛采用.其他答案中已经引用了许多这些选项.

我对任何可用的选项都不是特别满意所以我写了自己的名为bolus.Bolus旨在使用上面的DI风格编写的代码,并尝试非常干燥和非常简单.使用完全相同的car.jshorn.js上面的模块,您可以index.js使用bolus 重写模块:

// index.js
var Injector = require("bolus");
var injector = new Injector();
injector.registerPath("**/*.js");

var car = injector.resolve("car");
car.honkHorn();
Run Code Online (Sandbox Code Playgroud)

基本思想是创建一个注入器.您在进样器中注册了所有模块.然后你只需解决你需要的东西.Bolus将遍历依赖图,并根据需要创建和注入依赖项.你不会在这样的玩具示例中节省太多,但是在具有复杂依赖树的大型应用程序中,节省的费用是巨大的.

Bolus支持一系列漂亮的功能,如可选的依赖项和测试全局变量,但是我相对于标准的Node.js方法有两个主要的好处.首先,如果你有很多类似的应用程序,你可以为你的基础创建一个私有的npm模块,它创建一个注入器并在其上注册有用的对象.然后,您的特定应用程序可以根据需要添加,覆盖和解决,就像AngularJS的注入器工作方式一样.其次,您可以使用bolus来管理各种依赖关系的上下文.例如,您可以使用中间件为每个请求创建子注入器,在注入器上注册用户ID,会话ID,记录器等以及任何模块,具体取决于那些.然后解决您提供请求所需的内容.这为您提供了每个请求的模块实例,并防止必须将记录器等传递给每个模块函数调用.

  • 很好的答案。我想知道你在 2019 年的想法是什么。对于大型项目,根据个人喜好,你更喜欢哪个 - Node 中的 DI/IoC,或者只是用 `jest`、`rewire`、`proxyquire` stubbing/mocking , 等等。?谢谢。 (2认同)

Joh*_*ald 37

我还写了一个模块来完成这个,它被称为重新连接.只需使用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!
});
Run Code Online (Sandbox Code Playgroud)

我受到了Nathan MacInnes注射的启发,但采用了不同的方法.我不使用vmeval测试模块,实际上我使用node自己的require.这样,您的模块就像使用一样require()(除了您的修改).完全支持调试.

  • 截至2014年中期 - https://www.npmjs.org/package/proxyquire使得模拟"需要"依赖关系变得微不足道. (7认同)

Jar*_*son 17

我为此目的建造了Electrolyte.那里的其他依赖注入解决方案对我的口味来说太过于侵略,而且对全球化require的不满是我的特别不满.

Electrolyte包含模块,特别是那些导出"设置"功能的模块,就像你在Connect/Express中间件中看到的那样.从本质上讲,这些类型的模块只是它们返回的某些对象的工厂.

例如,创建数据库连接的模块:

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' ];
Run Code Online (Sandbox Code Playgroud)

您在底部看到的是注释,Electrolyte用于实例化和注入依赖项的额外元数据,自动将应用程序的组件连接在一起.

要创建数据库连接:

var db = electrolyte.create('database');
Run Code Online (Sandbox Code Playgroud)

电解质传递遍历@require'd依赖关系,并将实例作为参数注入导出的函数.

关键是这是微创的.该模块完全可用,独立于Electrolyte本身.这意味着您的单元测试可以测试被测模块,传递模拟对象而无需额外的依赖关系来重新连接内部.

当运行完整的应用程序时,Electrolyte会在模块间级别进行操作,将所有东西连接在一起,而不需要全局,单例或过多的管道.


ctr*_*usb 9

我亲自调查了一下.我不喜欢引入魔法依赖工具库,它提供了劫持我的模块导入的机制.相反,我想出了一个"设计指南",让我的团队通过在我的模块中引入工厂函数导出来明确说明可以模拟哪些依赖项.

我广泛使用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();
Run Code Online (Sandbox Code Playgroud)

以下是它的用法示例

import { factory } from './bar';

const underTest = factory({ $bob: () => 'BOB!' }); // only override bob!
const result = underTest();
Run Code Online (Sandbox Code Playgroud)

请原谅那些不熟悉它的人的ES6语法.


sun*_*ung 5

我最近检查了这个线程的原因与OP大致相同 - 我遇到的大多数库都暂时重写了require语句.我用这种方法取得了不同程度的成功,因此我最终使用了以下方法.

在快速应用程序的上下文中 - 我将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();
Run Code Online (Sandbox Code Playgroud)

传递给加载器的对象映射如下所示:

// live loader config
module.exports = {
    'dataBaseService': '/lib/dataBaseService.js'
}

// test loader config
module.exports = {
    'dataBaseService': '/mocks/dataBaseService.js'
    'otherService' : {other: 'service'} // takes objects too...
};
Run Code Online (Sandbox Code Playgroud)

然后,而不是直接调用require ...

var myDatabaseService = loader.load('dataBaseService');
Run Code Online (Sandbox Code Playgroud)

如果加载器中没有别名 - 那么它将默认为常规需求.这有两个好处:我可以交换任何版本的类,并且它不需要在整个应用程序中使用相对路径名(因此如果我需要在当前文件下方或上方使用自定义库,我不需要遍历,并且require将针对相同的密钥缓存模块).它还允许我在应用程序的任何位置指定模拟,而不是在即时测试套件中.

为方便起见,我刚刚发布了一个小的npm模块:

https://npmjs.org/package/nodejs-simple-loader