我目前正在用nodejs创建一些实验性项目。我用Spring编写过很多Java EE web应用程序,非常欣赏依赖注入的便利性。
现在我很好奇:如何对节点进行依赖注入?或者:我真的需要它吗?因为编程风格不同,是否存在替代概念?
到目前为止,我谈论的是一些简单的事情,比如共享数据库连接对象,但我还没有找到一个让我满意的解决方案。
我目前正在用nodejs创建一些实验性项目。我用Spring编写过很多Java EE web应用程序,非常欣赏依赖注入的便利性。
现在我很好奇:如何对节点进行依赖注入?或者:我真的需要它吗?因为编程风格不同,是否存在替代概念?
到目前为止,我谈论的是一些简单的事情,比如共享数据库连接对象,但我还没有找到一个让我满意的解决方案。
当前回答
对于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')
这个类现在灵活得多,因为您已经将传输的选择从实现中分离出来,并将其放入容器中。
现在邮件服务已经在容器中,您可以将其作为其他类的依赖项注入。如果你有一个这样的newsletter ttermanager类:
class NewsletterManager {
construct (mailer, fs) {
this._mailer = mailer
this._fs = fs
}
}
export default NewsletterManager
在定义newsletter tter_manager服务时,邮件服务还不存在。使用Reference类告诉容器在初始化通讯管理器时注入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()
其他回答
我最近创建了一个名为circuitbox的库,它允许你在node.js中使用依赖注入。与我见过的许多基于依赖项查找的库相比,它实现了真正的依赖项注入。Circuitbox还支持异步创建和初始化例程。下面是一个例子:
假设下面的代码位于名为consoleMessagePrinter.js的文件中
'use strict';
// Our console message printer
// deps is injected by circuitbox with the dependencies
function ConsoleMessagePrinter(deps) {
return {
print: function () {
console.log(deps.messageSource.message());
}
};
}
module.exports = ConsoleMessagePrinter;
假设main.js文件中包含以下内容
'use strict';
// our simple message source
// deps is injected by circuitbox with the dependencies
var simpleMessageSource = function (deps) {
return {
message: function () {
return deps.message;
}
};
};
// require circuitbox
var circuitbox = require('../lib');
// create a circuitbox
circuitbox.create({
modules: [
function (registry) {
// the message to be used
registry.for('message').use('This is the message');
// define the message source
registry.for('messageSource').use(simpleMessageSource)
.dependsOn('message');
// define the message printer - does a module.require internally
registry.for('messagePrinter').requires('./consoleMessagePrinter')
.dependsOn('messageSource');
}
]
}).done(function (cbx) {
// get the message printer and print a message
cbx.get('messagePrinter').done(function (printer) {
printer.print();
}, function (err) {
console.log('Could not recieve a printer');
return;
});
}, function (err) {
console.log('Could not create circuitbox');
});
Circuitbox允许您定义组件并将它们的依赖项声明为模块。一旦初始化,它允许您检索组件。Circuitbox自动注入目标组件所需的所有组件,并将其提供给您使用。
该项目处于alpha版本。欢迎您的评论、想法和反馈。
希望能有所帮助!
我知道这个帖子在这一点上是相当老的,但我想我会在这个问题上发表我的想法。TL;DR是由于JavaScript的无类型、动态特性,您实际上可以在不依赖依赖注入(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();
}
}
以这种方式编写代码几乎没有什么问题。
The Car class is tightly coupled to the particular implementation of the horn in the Horn class. If we want to change the type of horn used by the car, we have to modify the Car class even though its usage of the horn doesn't change. This also makes testing difficult because we can't test the Car class in isolation from its dependency, the Horn class. The Car class is responsible for the lifecycle of the Horn class. In a simple example like this it's not a big issue, but in real applications dependencies will have dependencies, which will have dependencies, etc. The Car class would need to be responsible for creating the entire tree of its dependencies. This is not only complicated and repetitive, but it violates the "single responsibility" of the class. It should focus on being a car, not creating instances. There is no way to reuse the same dependency instances. Again, this isn't important in this toy application, but consider a database connection. You would typically have a single instance that is shared across your application.
现在,让我们重构它以使用依赖注入模式。
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类的情况下为car引入一种新的喇叭类型:
class FrenchHorn : IHorn
{
public void Honk()
{
Console.WriteLine("le beep!");
}
}
主程序可以直接注入一个FrenchHorn类的实例。这也极大地简化了测试。您可以创建一个MockHorn类注入到Car构造函数中,以确保您只测试Car类。
上面的例子展示了手动依赖注入。典型的依赖注入是通过框架完成的(例如c#世界中的Unity或Ninject)。这些框架将通过遍历依赖关系图并根据需要创建实例来为您完成所有依赖关系连接。
标准的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需要缓存所有内容,所以模块本质上是存储在容器中的单例。对horn模块执行require的任何其他模块都将获得完全相同的实例。这使得共享单例对象(如数据库连接)非常容易。
现在仍然存在一个问题,即car模块负责获取它自己的依赖喇叭。如果希望汽车使用不同的喇叭模块,则必须更改car模块中的require语句。这不是一件很常见的事情,但它确实会导致测试问题。
通常人们处理测试问题的方法是使用proxyquire。由于JavaScript的动态特性,proxyquire会拦截需要的调用,并返回您提供的存根/mock。
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模块负责实例生命周期和连接。单元测试非常简单,因为您只需将mock /存根传递给函数。同样,如果这对您的应用程序来说足够好,那么就使用它。
Bolus DI框架
与c#不同的是,没有现成的标准依赖注入框架来帮助你管理依赖项。npm注册表中有许多框架,但没有一个被广泛采用。这些选项中的许多已经在其他答案中被引用。
我对所有的选项都不是特别满意,所以我写了自己的bolus。Bolus的设计是为了与上面的DI风格编写的代码一起工作,并试图非常DRY和非常简单。使用上面完全相同的car.js和horn.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 supports a bunch of nifty features like optional dependencies and test globals, but there are two key benefits I've seen relative to the standard Node.js approach. First, if you have a lot of similar applications, you can create a private npm module for your base that creates an injector and registers useful objects on it. Then your specific apps can add, override, and resolve as needed much like how AngularJS's injector works. Second, you can use bolus to manage various contexts of dependencies. For example, you could use middleware to create a child injector per request, register the user id, session id, logger, etc. on the injector along with any modules depending on those. Then resolve what you need to serve requests. This gives you instances of your modules per request and prevents having to pass the logger, etc. along to every module function call.
这取决于应用程序的设计。显然,你可以做一个类似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.
不要一字不差地照搬这个例子。这是一个蹩脚的例子,试图说明您利用模块模式来管理依赖项。希望这对你们有帮助。
我还写了一个模块来完成这一点,它被称为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()完全相同(除了您的修改)。此外,完全支持调试。