我目前正在使用Node.js创建一些实验性项目。我之前用Spring编写了很多Java EE Web应用程序,喜欢那里的依赖注入的方便性。
现在我很好奇:如何在Node.js中实现依赖注入?或者说:我是否需要它?因为编程风格是不同的,是否有替代概念?
我主要讨论一些简单的事情,比如共享数据库连接对象,但迄今为止我还没有找到令我满意的解决方案。
我目前正在使用Node.js创建一些实验性项目。我之前用Spring编写了很多Java EE Web应用程序,喜欢那里的依赖注入的方便性。
现在我很好奇:如何在Node.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
模式来管理依赖关系的一个无聊的例子。希望这可以更有帮助一些。
require('my_logger_library')
,那么使用我的组件的人将不得不覆盖该语句以使用自己的库。相反,我可以允许人们传递一个回调函数,将日志记录器实现包装到组件的“构造函数”或“初始化”方法中。这就是依赖注入(DI)的目的。 - moteutschclass 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();
}
}
用这种方式编写代码存在一些问题。
Car
类与Horn
类中喇叭的特定实现紧密耦合。如果我们想要更改汽车使用的喇叭类型,即使其使用的喇叭没有变化,也必须修改Car
类。这也使得测试变得困难,因为我们无法将Car
类与其依赖项Horn
类隔离地测试。Car
类负责Horn
类的生命周期。在这个简单的例子中,这不是一个大问题,但在真正的应用程序中,依赖关系会有依赖关系,依此类推。 Car
类需要负责创建其所有依赖关系的树。这不仅复杂而且重复,而且违反了类的“单一职责”。它应该专注于成为一辆汽车,而不是创建实例。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
类中进行测试。// 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();
人们通常处理测试问题的方式是使用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...
// 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();
index.js
模块负责实例生命周期和连接。单元测试非常简单,你可以只需将mocks/stubs传递给函数即可。如果这对你的应用程序足够好,请使用它。
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();
proxyquire
的方法,比如sinon
,它允许您进行非常简洁的模拟,例如 let readFileStub = sinon.stub(fs, 'readFile').yields(new Error('something went wrong'));
,然后对fs.readFile
的后续调用将返回错误,直到您通过 readFileStub.restore()
恢复存根。就个人而言,我认为DI的用途值得怀疑,因为我觉得它几乎需要使用类/对象,这是对JavaScript功能倾向的一个可疑假设。 - Kevinrequire()
和最近的ES模块(import
)是在Node.js中管理依赖项的方法,它直观且有效,但也有其局限性。require()
或import
相比,使用Node.js DI容器可以实现什么?就像与软件开发相关的任何事情一样,选择DI还是require()
/import
取决于您的要求、系统的复杂性和编程风格。
我还编写了一个模块来实现这个,它叫做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!
});
vm
来评估测试模块,实际上我使用的是node自带的require。这样你的模块就像使用require()
一样(除了你的修改)。同时完全支持调试。 require
是我的一个特别不满。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
的依赖项,并将实例作为导出函数的参数注入。connect()
抛出异常时,你所发布的代码会发生什么?虽然我不熟悉Node的MySql API,但我预期这个调用是异步的,因此这个示例并不十分清晰。 - Andrey Agibalovioc.create
。单元测试应该仅测试被测模块,而不引入其他依赖项,包括 Electrolyte。按照这个建议,你应该使用 objToTest = require('modulename')(mockObj1, mockObj2);
。 - Jared Hanson我自己查看了一下。我不喜欢引入提供机制来劫持我的模块导入的魔术依赖工具库。相反,我为我的团队提出了一个“设计指南”,以明确声明可以通过在我的模块中引入工厂函数导出来模拟的依赖项。
我广泛使用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语法的人,请原谅。
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容器的想法,可以看看InversifyJS。最新版本(2.0.0)支持许多用例:神话1:JavaScript中没有IoC容器的位置
神话2:我们不需要IoC容器,我们已经有模块加载器!
神话3:依赖反转===注入依赖项
我最近查看了这个帖子,和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...
};
var myDatabaseService = loader.load('dataBaseService');
对于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
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'))
container.compile()
与其他平台一样,Node.js同样需要依赖注入。如果你正在构建一个庞大的项目,使用依赖注入将使得模拟代码依赖项和彻底测试代码变得更加容易。
例如,你的数据库层模块不应该只在业务代码模块中被引用,因为当对这些业务代码模块进行单元测试时,DAO会加载并连接到数据库。
一种解决方案是将依赖项作为模块参数传递:
module.exports = function (dep1, dep2) {
// private methods
return {
// public methods
test: function(){...}
}
}