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
调用提供callback
variables来缩短这个时间,但为了演示的目的,我想在这里明确指出。
现在当你需要这个模块的时候,起初什么都不会发生,因为你只能得到对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.readFile
asynchronous加载一个文件,然后运行它通过一个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
它。 重点是:如果你不相信代码, eval
和require
是一样的坏主意。 因此,在这个特殊的情况下,这可能是好的。
如果你使用严格模式, 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出现在更早的答案中,但是通过审查被删除了。)