我目前正在用nodejs创建一些实验性项目。我用Spring编写过很多Java EE web应用程序,非常欣赏依赖注入的便利性。

现在我很好奇:如何对节点进行依赖注入?或者:我真的需要它吗?因为编程风格不同,是否存在替代概念?

到目前为止,我谈论的是一些简单的事情,比如共享数据库连接对象,但我还没有找到一个让我满意的解决方案。


当前回答

我还写了一个模块来完成这一点,它被称为rewire。只需使用npm安装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注射剂的启发,但使用了不同的方法。我不使用vm来评估测试模块,事实上我使用节点自己的要求。这样,您的模块的行为与使用require()完全相同(除了您的修改)。此外,完全支持调试。

其他回答

事实上,你可以在没有IoC容器的情况下测试node.js,因为JavaScript是一种真正动态的编程语言,你可以在运行时修改几乎所有东西。

考虑以下几点:

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容器的想法。您可以在文章《JavaScript中依赖倒置的当前状态》中了解更多关于此思想的内容。这篇文章试图揭穿一些“JavaScript IoC容器神话”:

误解1:JavaScript中没有IoC容器的位置 误解2:我们不需要IoC容器,我们已经有模块加载器了! 误解3:依赖倒置===注入依赖

如果你也喜欢使用IoC容器的想法,你可以看看InversifyJS。最新版本(2.0.0)支持多种用例:

内核模块 内核的中间件 使用类、字符串或符号作为依赖标识符 常数值注入 类构造函数的注入 工厂注入 汽车工厂 提供程序的注入(异步工厂) 激活处理程序(用于注入代理) 多注射 标记绑定 自定义标记装饰器 指定绑定 上下文绑定 友好的异常(例如循环依赖)

你可以在InversifyJS上了解更多。

这取决于应用程序的设计。显然,你可以做一个类似java的注入,在那里创建一个类的对象,并将依赖项传递给构造函数,就像这样。

function Cache(store) {
   this._store = store;
}

var cache = new Cache(mysqlStore);

如果你不是在javascript中做OOP,你可以做一个init函数来设置一切。

然而,还有另一种方法可以采用,这种方法在基于事件的系统(如node.js)中更为常见。如果您可以将应用程序建模为仅(大多数时候)对事件进行操作,那么您所需要做的就是设置所有内容(我通常通过调用init函数来完成)并从存根发出事件。这使得测试相当容易和易读。

简而言之,您不需要像在c# /Java中那样的依赖注入容器或服务定位器。因为Node.js利用了模块模式,所以没有必要执行构造函数或属性注入。尽管你仍然可以。

JS的伟大之处在于你可以修改任何东西来达到你想要的效果。这在测试时非常有用。

看看我这拙劣的例子吧。

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。方法,或者在调用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. 

不要一字不差地照搬这个例子。这是一个蹩脚的例子,试图说明您利用模块模式来管理依赖项。希望这对你们有帮助。

我自己调查过了。我不喜欢引入神奇的依赖utils库,它提供了劫持模块导入的机制。相反,我为我的团队提出了一个“设计指南”,相当明确地说明可以通过在模块中引入工厂函数导出来模拟哪些依赖项。

我大量使用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的人,请原谅它的语法。

TypeDI是这里提到的最可爱的,看看TypeDI中的代码

import "reflect-metadata";
import {Service, Container} from "typedi";

@Service()
class SomeClass {

    someMethod() {
    }

}

let someClass = Container.get(SomeClass);
someClass.someMethod();

看看这段代码:

import {Container, Service, Inject} from "typedi";

// somewhere in your global app parameters
Container.set("authorization-token", "RVT9rVjSVN");

@Service()
class UserRepository {

    @Inject("authorization-token")
    authorizationToken: string;

}