【问题标题】:Node.js - asynchronous module loadingNode.js - 异步模块加载
【发布时间】:2013-12-01 18:06:40
【问题描述】:

是否可以异步加载 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 中的回调?

【问题讨论】:

  • Node.JS load module async 的可能重复项
  • 您的用例是什么?这不是加载模块的典型用法。如果您要处理大量数据,require() 可能不是最佳选择。
  • 模板引擎 - 模板将被编译为 .js 文件(Node.js 模块),我想在不阻塞事件循环的情况下加载它们。
  • 我看到的典型模式是只有一个导出函数来进行初始化。当然,该函数可以接受回调: var foo = require("./foo.js").foo(function(bar) {..});
  • 只需在服务器初始化时编译它们。当你连接到你的数据库服务器时,它们就会被编译。

标签: javascript node.js


【解决方案1】:

虽然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…
  });
};

希望这会有所帮助。

【讨论】:

  • 谢谢。但我想要实现的正是这样的:“请注意,由于require 函数的限制,加载模块当然仍然是同步的,但所有其余部分都将是异步的,如你所愿。 "我正在尝试使用 vm 模块(不稳定)或私有函数 module._compile() 进行一些肮脏的技巧,但我无法产生有效的解决方案。我只想使用某种require 函数,它不包含用于加载模块源代码的fs.readFileSync() 调用。
  • 谢谢。我将用vm.runInThisContext 替换eval(我使用ES5/Strict,所以没有eval),看看我能不能让它工作......
  • vm.runInThisContext 的问题(恕我直言)是您无法访问本地范围,这是修改 module 变量所必需的。但是vm.runInNewContext 应该做你想做的事:你指定代码作为第一个参数,module 对象作为第二个参数(documentation 调用这个sandbox。然后它应该工作。
  • 我尝试了您的requireAsync 方法,它仅在模块不需要任何其他模块时才有效(由于某种原因,其他模块的路径未正确解析)。无论如何,我可以将我需要的其他模块直接添加到沙箱中。它可能不是一个通用的解决方案,但它对我有用,所以谢谢:-)
  • 您基本上是对的,解决方案并不完美,但是,嘿,我在 15 分钟左右将它从 0 速写到 100 ;-)。但是,不客气,我很高兴能帮助你:-)。如果你愿意,如果你能将我的答案标记为 answer ,我会很高兴 :-)。
【解决方案2】:

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 作为依赖项。

(此解决方案出现在 an earlier answer 但已被审查删除。)

【讨论】:

    【解决方案3】:

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

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

    【讨论】:

    • 它们都不起作用。 TypeError: object is not a function 或 TypeError: Object #<Object> has no method 'then' .
    • 它不是内置的。您需要构造promise对象并将其分配给模块中的module.exports。 (或将功能分配给 module.exports)。如果仍然不清楚,我会用示例更新答案
    • 下次请明确说明...我或其他人可能会理解这些快捷方式,但大多数人 - 尤其是初学者 - 不会。
    • 这对我有用,但您的代码中有小错误。 //usage 部分应该是 require("./foo.js")(function(err, foo) 而不是 require("./foo. js")(function(foo) 没有第一个参数 err。这就是上面的 martin majer 得到类型错误的原因。
    【解决方案4】:

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

    /* 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 */
    

    【讨论】:

      猜你喜欢
      • 2012-12-04
      • 1970-01-01
      • 2013-11-09
      • 2018-12-01
      • 1970-01-01
      • 2015-10-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多