Node.js – asynchronous模块加载

是否有可能asynchronous加载Node.js模块?

这是标准的代码:

var foo = require("./foo.js"); // waiting for I/O foo.bar(); 

但是我想写这样的东西:

 require("./foo.js", function(foo) { foo.bar(); }); // doing something else while the hard drive is crunching... 

有没有办法如何做到这一点? 还是有一个很好的理由,为什么不支持callbackrequire

虽然require是同步的,而Node.js并不提供asynchronous变体,你可以轻松地为自己构build一个。

首先,你需要创build一个模块。 在我的例子中,我将编写一个从文件系统asynchronous加载数据的模块,但当然是YMMV。 所以,首先是老式的,不想要的同步方法:

 var fs = require('fs'); var passwords = fs.readFileSync('/etc/passwd'); module.exports = passwords; 

你可以照常使用这个模块:

 var passwords = require('./passwords'); 

现在,你想要做的是把它变成一个asynchronous模块。 因为你不能延迟 module.exports,你所做的就是立即导出一个asynchronous执行工作的函数,一旦完成,就调用你。 所以你把你的模块转换成:

 var fs = require('fs'); module.exports = function (callback) { fs.readFile('/etc/passwd', function (err, data) { callback(err, data); }); }; 

当然,你可以通过直接向readFile调用提供callbackvariables来缩短这个时间,但为了演示的目的,我想在这里明确指出。

现在当你需要这个模块的时候,起初什么都不会发生,因为你只能得到对asynchronous(匿名)函数的引用。 你需要做的是立即调用它,并提供另一个函数作为callback:

 require('./passwords')(function (err, passwords) { // This code runs once the passwords have been loaded. }); 

当然,使用这种方法,您可以将任意同步模块初始化转换为asynchronous模式。 但是技巧总是相同的:导出一个函数,从require调用它,并提供一个callback,一旦asynchronous代码已经运行,继续执行。

请注意,对于一些人

 require('...')(function () { ... }); 

可能看起来很混乱。 因此,它可能会更好(虽然这取决于您的实际情况)导出一个asynchronousinitialize函数或类似的东西:

 var fs = require('fs'); module.exports = { initialize: function (callback) { fs.readFile('/etc/passwd', function (err, data) { callback(err, data); }); } }; 

然后你可以使用这个模块

 require('./passwords').initialize(function (err, passwords) { // ... }); 

这可能会稍微好一些。

当然,你也可以使用承诺或任何其他的asynchronous机制,使你的语法看起来更好,但最终,它(内部)总是归结于我刚刚描述的模式。 基本上,承诺&co。 除了callback之外,它们都是语法糖。

一旦你这样构build你的模块,你甚至可以build立一个requireAsync函数,就像你最初在你的问题中提出的那样工作。 你所要做的就是坚持初始化函数的名字,比如initialize 。 那你可以这样做:

 var requireAsync = function (module, callback) { require(module).initialize(callback); }; requireAsync('./passwords', function (err, passwords) { // ... }); 

请注意,当然,由于require函数的限制, 加载模块仍然是同步的,但是其余部分将根据您的需要asynchronous执行。

最后一个注意事项:如果你想实际上使加载模块asynchronous,你可以实现一个函数,它使用fs.readFileasynchronous加载一个文件,然后运行它通过一个eval调用来实际执行模块,但我强烈build议反之:一方面,你失去了caching等所有request的便利特性,另一方面你必须处理eval – 众所周知, eval是邪恶的 。 所以不要这样做。

不过,如果你仍然想这样做,基本上是这样工作的:

 var requireAsync = function (module, callback) { fs.readFile(module, { encoding: 'utf8' }, function (err, data) { var module = { exports: {} }; var code = '(function (module) {' + data + '})(module)'; eval(code); callback(null, module); }); }; 

请注意,这段代码并不“好”,并且没有任何error handling和原始require函数的任何其他function,但基本上满足了您能够asynchronous加载同步devise的模块的需求。

无论如何,你可以使用这个function,像一个模块

 module.exports = 'foo'; 

并使用它来加载它:

 requireAsync('./foo.js', function (err, module) { console.log(module.exports); // => 'foo' }); 

当然你也可以输出其他东西。 也许,为了与原来的require函数兼容,运行可能会更好

 callback(null, module.exports); 

作为您的requireAsync函数的最后一行,因为您可以直接访问exports对象(在这种情况下是stringfoo )。 由于你将加载的代码包装在一个立即执行的函数中,所以这个模块中的所有东西都是保密的,唯一的外界接口是你传入的module对象。

当然,人们可以争辩说,这种evil使用并不是世界上最好的想法,因为它打开了安全漏洞等等。但是,如果你require一个模块,那么基本上什么也不做,而不是eval它。 重点是:如果你不相信代码, evalrequire是一样的坏主意。 因此,在这个特殊的情况下,这可能是好的。

如果你使用严格模式, eval对你没有好处,你需要使用vm模块并使用它的runInNewContext函数。 然后,解决scheme如下所示:

 var requireAsync = function (module, callback) { fs.readFile(module, { encoding: 'utf8' }, function (err, data) { var sandbox = { module: { exports: {} } }; var code = '(function (module) {' + data + '})(module)'; vm.runInNewContext(code, sandbox); callback(null, sandbox.module.exports); // or sandbox.module… }); }; 

希望这可以帮助。

是的 – 导出function接受callback,甚至可能导出全function的承诺对象。

 // foo.js + callback: module.exports = function(cb) { setTimeout(function() { console.log('module loaded!'); var fooAsyncImpl = {}; // add methods, for example from db lookup results fooAsyncImpl.bar = console.log.bind(console); cb(null, fooAsyncImpl); }, 1000); } // usage require("./foo.js")(function(foo) { foo.bar(); }); // foo.js + promise var Promise = require('bluebird'); module.exports = new Promise(function(resolve, reject) { // async code here; }); // using foo + promises require("./foo.js").then(function(foo) { foo.bar(); }); 

安德烈的下面的代码是最简单的答案,但他有一个小错误,所以我在这里发布更正作为答案。 此外,我只是使用callback,而不是像安德烈的代码蓝鸟/承诺。

 /* 1. Create a module that does the async operation - request etc */ // foo.js + callback: module.exports = function(cb) { setTimeout(function() { console.log('module loaded!'); var foo = {}; // add methods, for example from db lookup results foo.bar = function(test){ console.log('foo.bar() executed with ' + test); }; cb(null, foo); }, 1000); } /* 2. From another module you can require the first module and specify your callback function */ // usage require("./foo.js")(function(err, foo) { foo.bar('It Works!'); }); /* 3. You can also pass in arguments from the invoking function that can be utilised by the module - eg the "It Works!" argument */ 

npm模块async-require可以帮助你做到这一点。

安装

 npm install --save async-require 

用法

 var asyncRequire = require('async-require'); // Load script myModule.js asyncRequire('myModule').then(function (module) { // module has been exported and can be used here // ... }); 

该模块使用vm.runInNewContext() ,这是接受的答案中讨论的一种技术。 它有蓝鸟作为依赖。

(这个解决scheme出现在更早的答案中,但是通过审查被删除了。)