虽然require 是同步的,并且 Node.js 不提供开箱即用的异步变体,但您可以轻松地为自己构建一个。
首先,您需要创建一个模块。在我的示例中,我将编写一个从文件系统异步加载数据的模块,当然还有 YMMV。所以,首先是老式的、不需要的、同步的方法:
var fs = require('fs');
var passwords = fs.readFileSync('/etc/passwd');
module.exports = passwords;
你可以照常使用这个模块:
var passwords = require('./passwords');
现在,您要做的是把它变成一个异步模块。由于您不能延迟 module.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 () { ... });
可能看起来很混乱。因此,可能最好使用异步 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 或任何其他异步机制,使您的语法看起来更好,但最终,它(内部)总是归结为我刚才描述的模式。基本上,promises & co。只不过是回调的语法糖。
像这样构建模块后,您甚至可以构建一个 requireAsync 函数,该函数的工作方式与您最初在问题中建议的一样。您所要做的就是为初始化函数命名,例如initialize。然后你可以这样做:
var requireAsync = function (module, callback) {
require(module).initialize(callback);
};
requireAsync('./passwords', function (err, passwords) {
// ...
});
请注意,当然,由于require 函数的限制,加载模块仍将是同步的,但所有其余部分将按照您的意愿异步。
最后一点:如果你想真正使加载模块异步,你可以实现一个使用fs.readFile异步加载文件的函数,然后运行它通过eval 调用来实际执行该模块,但我强烈 建议不要这样做:一方面,您失去了request 的所有便利功能,例如缓存和co。 ,另一方面,您将不得不处理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);
});
};
请注意,这段代码并不“好”,它缺少任何错误处理,以及原始 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-uating。关键是:如果您不信任代码,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…
});
};
希望这会有所帮助。