Node.js - 异步模块加载

Mar*_*jer 52 javascript node.js

是否可以异步加载Node.js模块?

这是标准代码:

var foo = require("./foo.js"); // waiting for I/O
foo.bar();
Run Code Online (Sandbox Code Playgroud)

但我想写这样的东西:

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

有办法怎么做?或者是否有充分理由require不支持回调?

Gol*_*den 121

虽然require是同步的,并且Node.js不提供开箱即用的异步变体,但您可以轻松地为自己构建一个.

首先,您需要创建一个模块.在我的例子中,我将编写一个从文件系统异步加载数据的模块,当然还有YMMV.所以,首先是老式的,不想要的同步方法:

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

module.exports = passwords;
Run Code Online (Sandbox Code Playgroud)

您可以像往常一样使用此模块:

var passwords = require('./passwords');
Run Code Online (Sandbox Code Playgroud)

现在,您要做的是将其转换为异步模块.由于你无法延迟 module.exports,你所做的就是立即导出一个异步完成工作的函数,一旦完成就会回调你.所以你将模块转换为:

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

当然你可以通过直接为调用提供callback变量来缩短它readFile,但我想在这里明确它以用于演示目的.

现在,当您需要此模块时,首先没有任何反应,因为您只获得对异步(匿名)函数的引用.您需要做的是立即调用它并提供另一个函数作为回调:

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

当然,使用这种方法可以将任意同步模块初始化转换为异步模块.但技巧总是一样的:导出一个函数,从require调用中直接调用它,并提供一个在异步代码运行后继续执行的回调.

请注意,对某些人来说

require('...')(function () { ... });
Run Code Online (Sandbox Code Playgroud)

可能看起来很混乱.因此,使用异步函数或类似的东西导出对象可能更好(尽管这取决于您的实际场景)initialize:

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

然后,您可以使用此模块

require('./passwords').initialize(function (err, passwords) {
  // ...
});
Run Code Online (Sandbox Code Playgroud)

这可能稍微好一些.

当然你也可以使用promises或任何其他使你的语法看起来更好的异步机制,但最后,它(内部)总是归结为我刚才描述的模式.基本上,承诺和合作.只不过是回调的语法糖.

一旦你构建了这样的模块,你甚至可以构建一个requireAsync像你最初在问题中建议的那样工作的函数.您所要做的就是坚持使用初始化函数的名称,例如initialize.然后你可以这样做:

var requireAsync = function (module, callback) {
  require(module).initialize(callback);
};

requireAsync('./passwords', function (err, passwords) {
  // ...
});
Run Code Online (Sandbox Code Playgroud)

请注意,当然,由于功能的限制,加载模块仍然是同步的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);
  });
};
Run Code Online (Sandbox Code Playgroud)

请注意,此代码不"好",并且它缺少任何错误处理和原始require函数的任何其他功能,但基本上,它满足了您能够异步加载同步设计模块的要求.

无论如何,您可以将此功能与模块一起使用

module.exports = 'foo';
Run Code Online (Sandbox Code Playgroud)

并使用以下方式加载:

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

当然你也可以输出任何其他东西.也许,为了与原始require功能兼容,运行可能更好

callback(null, module.exports);
Run Code Online (Sandbox Code Playgroud)

作为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…
  });
};
Run Code Online (Sandbox Code Playgroud)

希望这可以帮助.

  • 谢谢.但我想要实现的是:"请注意,当然,由于`require`函数的限制,加载模块仍然是同步的,但所有其余的都将是异步的,如你所愿. " 我正在使用`vm`模块(不稳定)或私有函数`module._compile()`尝试一些肮脏的技巧,但我无法生成有效的解决方案.我只想使用某种`require`函数,它不包含用于加载模块源代码的`fs.readFileSync()`调用. (3认同)