javascript Node.js -异步模块加载

lyfkaqu1  于 2022-12-25  发布在  Java
关注(0)|答案(5)|浏览(169)

是否可以异步加载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...

有没有办法做到这一点?或者有没有一个很好的理由为什么require中的回调不受支持?

8nuwlpux

8nuwlpux1#

虽然require是同步的,并且Node.js没有提供现成的异步变体,但是您可以轻松地为自己构建一个。
首先,你需要创建一个模块。在我的例子中,我将写一个从文件系统异步加载数据的模块,当然是YMMV。所以,首先是老式的,不需要的,同步的方法:

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

module.exports = passwords;

您可以照常使用此模块:

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

现在,你要做的是把它转换成一个异步模块。因为你不能 * delay * module.exports,你要做的是立即导出一个异步执行工作的函数,并在完成后回调你。所以你要把你的模块转换成:

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

当然,您可以通过直接向readFile调用提供callback变量来缩短时间,但是出于演示的目的,我想在这里明确地说明它。
现在,当你需要这个模块的时候,一开始什么都没有发生,因为你只得到了一个异步(匿名)函数的引用,你需要做的是立即调用它,并提供另一个函数作为回调函数:

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) {
  // ...
});

其可以稍微更好地读取。
当然,你也可以使用promises或者其他异步机制来使你的语法看起来更好,但是最终,它(内部)总是归结为我刚刚描述的模式。基本上,promises & co.只不过是回调上的语法糖。
一旦你像这样构建了你的模块,你甚至可以构建一个requireAsync函数,就像你最初在问题中建议的那样工作,你所要做的就是为初始化函数指定一个名字,比如initialize,然后你可以做:

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

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

请注意,当然,由于require函数的限制,* 加载 * 模块仍然是同步的,但所有其他操作都将是异步的。
最后一点:如果你想让 * loading * 模块异步运行,你 * 可以 * 实现一个使用fs.readFile异步加载文件的函数,然后通过eval调用运行它来实际执行模块,但是我强烈建议不要这样做:一方面,你失去了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);
  });
};

请注意,这段代码并不"漂亮",它缺乏任何错误处理,以及原始require函数的任何其他功能,但基本上,它满足了您的需求,即能够异步加载同步设计的模块。
无论如何,您可以将此函数与以下模块一起使用

module.exports = 'foo';

并使用以下命令加载:

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

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

callback(null, module.exports);

作为requireAsync函数的最后一行,这样就可以直接访问exports对象(在本例中是字符串foo)。由于您将加载的代码 Package 在一个立即执行的函数中,因此该模块中的所有内容都是私有的,与外部世界的唯一接口是您传入的module对象。
当然,有人可能会说evil的这种用法不是最好的主意,因为它会打开安全漏洞等等--但是如果你require一个模块,你基本上除了eval评估它之外什么也不做。如果你不信任代码,那么evalrequire一样是个坏主意,因此在这种特殊情况下,它可能是好的。
如果您使用的是strict模式,那么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…
  });
};
frebpwbc

frebpwbc2#

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(),这是已接受答案中讨论的一种技术,它将bluebird作为依赖项。

  • (此解决方案出现在之前的回答中,但在审查时被删除。)*
ql3eal8s

ql3eal8s3#

是的-导出函数接受回调,甚至可能导出全功能的承诺对象。

// 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();
});
bbmckpt7

bbmckpt74#

Andrey的代码是最简单的答案,但是他的代码有一个小错误,所以我在这里发布更正作为答案。另外,我只是使用回调函数,而不是像Andrey的代码那样使用bluebird / promises。

/* 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 - e.g the "It Works!" argument */
4ktjp1zp

4ktjp1zp5#

对于任何使用ESM模块和顶级await的人来说,这将是开箱即用的,而不需要使用commonJSrequire的回调或安装任何类似async-require的包。

// In foo.mjs
await doIOstuffHere();
export foo;

// in bar.mjs
import foo from "./foo.mjs";
foo.bar(); // this function would not run till the async work in foo.mjs is finished

相关问题