我目前正在用nodejs创建一些实验性项目。我用Spring编写过很多Java EE web应用程序,非常欣赏依赖注入的便利性。
现在我很好奇:如何对节点进行依赖注入?或者:我真的需要它吗?因为编程风格不同,是否存在替代概念?
到目前为止,我谈论的是一些简单的事情,比如共享数据库连接对象,但我还没有找到一个让我满意的解决方案。
我目前正在用nodejs创建一些实验性项目。我用Spring编写过很多Java EE web应用程序,非常欣赏依赖注入的便利性。
现在我很好奇:如何对节点进行依赖注入?或者:我真的需要它吗?因为编程风格不同,是否存在替代概念?
到目前为止,我谈论的是一些简单的事情,比如共享数据库连接对象,但我还没有找到一个让我满意的解决方案。
当前回答
I worked with .Net, PHP and Java for long time so I wanted to have a convenient Dependency Injection in NodeJS too. People said the built-in DI in NodeJS is enough as we can get it with Module. But it didn't satisfy me well. I wanted to keep a Module no more than a Class. Additionally, I wanted the DI to have a full support for Module life cycle management (singleton module, transient module etc.) but with Node module, I had to write manual code very often. Lastly, I wanted to make Unit Test easier. That's why I created a Dependency Injection for myself.
如果您正在寻找DI,请尝试一下。可以在这里找到:https://github.com/robo-creative/nodejs-robo-container。它有完整的文档。并对依赖注入中常见的问题进行了分析,并提出了面向对象的解决方法。希望能有所帮助。
其他回答
require()和最近的ES模块(import)是管理Node.js中的依赖关系的方法,当然它是直观和有效的,但它也有其局限性。
我的建议是看一看目前Node.js中可用的依赖注入容器,了解它们的优缺点。其中一些是:
awilix injection-js bottlejs inversify node-dependency-injection
举几个例子。
现在真正的问题是,与简单的require()或import相比,使用Node.js DI容器可以实现什么?
优点:
better testability: modules accepts their dependencies as input Inversion of Control: decide how to wire your modules without touching the main code of your application. a customizable algorithm for resolving modules: dependencies have "virtual" identifiers, usually they are not bound to a path on the filesystem. Better extensibility: enabled by IoC and "virtual" identifiers. Other fancy stuff possible: Async initialization Module lifecycle management Extensibility of the DI container itself Can easily implement higher level abstractions (e.g. AOP)
缺点:
与Node.js的“体验”不同:使用DI绝对感觉你偏离了Node的思维方式。 依赖项与其实现之间的关系并不总是明确的。依赖项可能在运行时被解析,并受到各种参数的影响。代码变得更加难以理解和调试 启动时间较慢 大多数DI容器不能很好地与Browserify和Webpack等模块捆绑器一起使用。
与软件开发相关的任何事情一样,在DI或require()/import之间的选择取决于您的需求、系统复杂性和编程风格。
这取决于应用程序的设计。显然,你可以做一个类似java的注入,在那里创建一个类的对象,并将依赖项传递给构造函数,就像这样。
function Cache(store) {
this._store = store;
}
var cache = new Cache(mysqlStore);
如果你不是在javascript中做OOP,你可以做一个init函数来设置一切。
然而,还有另一种方法可以采用,这种方法在基于事件的系统(如node.js)中更为常见。如果您可以将应用程序建模为仅(大多数时候)对事件进行操作,那么您所需要做的就是设置所有内容(我通常通过调用init函数来完成)并从存根发出事件。这使得测试相当容易和易读。
我制作《电解质》就是为了这个目的。其他依赖注入解决方案对我来说太有侵入性了,而混淆全局require是我特别不满的地方。
电解质包含模块,特别是那些导出“设置”功能的模块,就像你在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' ];
您在底部看到的是注释,这是一个额外的元数据,电解质使用它来实例化和注入依赖关系,自动将应用程序的组件连接在一起。
创建一个数据库连接。
var db = electrolyte.create('database');
电解质传递遍历@require'd依赖项,并将实例作为参数注入导出函数。
关键是这是微创的。这个模块是完全可用的,独立于电解质本身。这意味着您的单元测试可以只测试被测试的模块,传入模拟对象,而不需要额外的依赖关系来重新连接内部。
当运行完整的应用程序时,电解质在模块间级别介入,将东西连接在一起,而不需要全局变量、单例或过多的管道。
我一直很喜欢IoC的简单理念——“你不需要了解任何环境,需要的时候有人会叫你。”
但是我看到的所有IoC实现都完全相反——它们用更多的东西使代码变得混乱。所以,我创建了我自己的IoC,就像我想要的那样-它保持隐藏和不可见的90%的时间。
它用于MonoJS web框架http://monojs.org
我说的是简单的事情,比如共享一个数据库连接对象 但我还没有找到一个让我满意的解决办法。
它是这样做的——在配置中注册组件一次。
app.register 'db', ->
require('mongodb').connect config.dbPath
可以在任何地方使用
app.db.findSomething()
你可以在https://github.com/sinizinairina/mono/blob/master/mono.coffee这里看到完整的组件定义代码(包括DB Connection和其他组件)
这是你必须告诉IoC该做什么的唯一地方,之后所有这些组件都将自动创建和连接,你不再需要在应用程序中看到IoC特定的代码。
国际奥委会本身https://github.com/alexeypetrushin/miconjs
我知道这个帖子在这一点上是相当老的,但我想我会在这个问题上发表我的想法。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.