While require
是同步的,Node.js 不提供开箱即用的异步变体,您可以轻松地自己构建一个。
首先,您需要创建一个模块。在我的示例中,我将编写一个从文件系统异步加载数据的模块,当然是 YMMV。因此,首先是老式的、不需要的同步方法:
var fs = require('fs');
var passwords = fs.readFileSync('/etc/passwd');
module.exports = passwords;
您可以像往常一样使用此模块:
var passwords = require('./passwords');
现在,您要做的就是将其变成异步模块。正如你不能delaymodule.exports,您所做的就是立即导出一个异步完成工作的函数,并在完成后回调您。因此,您将模块转换为:
var fs = require('fs');
module.exports = function (callback) {
fs.readFile('/etc/passwd', function (err, data) {
callback(err, data);
});
};
当然,您可以通过直接提供来缩短此时间callback
变量为readFile
调用,但出于演示目的,我想在这里明确说明。
现在,当您需要此模块时,首先什么也不会发生,因为您只获得对异步(匿名)函数的引用。您需要做的是立即调用它并提供另一个函数作为回调:
require('./passwords')(function (err, passwords) {
// This code runs once the passwords have been loaded.
});
当然,使用这种方法,您可以将任意同步模块初始化转换为异步模块初始化。但技巧总是相同的:导出一个函数,直接从require
调用并提供一个回调,该回调在异步代码运行后继续执行。
请注意,对于某些人来说
require('...')(function () { ... });
可能看起来很混乱。因此它may更好(尽管这取决于您的实际场景)使用异步导出对象initialize
函数或类似的东西:
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) {
// ...
});
这可能会稍微好一点可读性。
当然,您也可以使用 Promise 或任何其他异步机制,这使您的语法看起来更好,但最终,它(内部)总是归结为我刚刚在这里描述的模式。基本上,承诺和合作。只不过是回调的语法糖而已。
一旦你像这样构建了模块,你甚至可以构建一个requireAsync
功能就像您最初在问题中建议的那样工作。您所要做的就是坚持初始化函数的名称,例如initialize
。然后你可以这样做:
var requireAsync = function (module, callback) {
require(module).initialize(callback);
};
requireAsync('./passwords', function (err, passwords) {
// ...
});
请注意,当然,loading由于限制,该模块仍将同步require
函数,但其余的都将按照您的意愿异步进行。
最后一点:如果你想真正做到loading模块异步,你could实现一个函数,使用fs.readFile
异步加载文件,然后通过eval
调用实际执行模块,但我会highly建议反对这一点:一方面,你失去了所有的便利功能request
例如缓存等,另一方面,你必须处理eval
- 众所周知,评估是邪恶的。所以不要这样做。
尽管如此,如果你still想要这样做,基本上它的工作原理是这样的:
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);
});
};
请注意,此代码并不“好”,并且它缺乏任何错误处理以及原始代码的任何其他功能require
功能,但基本上,它满足了您能够异步加载同步设计的模块的需求。
无论如何,您可以将此功能与类似的模块一起使用
module.exports = 'foo';
并使用以下命令加载它:
requireAsync('./foo.js', function (err, module) {
console.log(module.exports); // => 'foo'
});
当然,您也可以导出其他任何内容。可能是为了兼容原版require
函数,运行可能会更好
callback(null, module.exports);
作为你的最后一行requireAsync
函数,这样您就可以直接访问exports
对象(这是字符串foo
在这种情况下)。由于您将加载的代码包装在立即执行的函数中,因此该模块中的所有内容都保持私有,并且与外部世界的唯一接口是module
你传入的对象。
当然,有人可能会争辩说,这种用法evil
不是世界上最好的主意,因为它会带来安全漏洞等等 - 但如果你require
一个模块,你基本上什么也不做,无论如何,eval
-评估它。重点是:如果你不信任代码,eval
是同样的坏主意require
。因此,在这种特殊情况下,可能没问题。
如果您使用严格模式,eval
对你没有好处,你需要跟着vm
模块并使用其runInNewContext
功能。然后,解决方案如下所示:
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…
});
};