Подтвердить что ты не робот

Node.js - загрузка асинхронного модуля

Можно ли асинхронно загрузить модуль 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 не поддерживаются?

4b9b3361

Ответ 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) {
  // ...
});

который может быть немного лучше читаемым.

Конечно, вы также можете использовать promises или любой другой асинхронный механизм, который делает ваш синтаксис более приятным, но, в конце концов, он (внутренне) всегда сводится к шаблону, который я только что описал здесь. В принципе, promises и co. это не что иное, как синтаксический сахар над обратными вызовами.

Как только вы создадите свои модули, вы можете даже создать функцию requireAsync, которая работает так, как вы изначально предложили в своем вопросе. Все, что вам нужно сделать, это указать имя для функции инициализации, например initialize. Затем вы можете сделать:

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

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

Обратите внимание, что, конечно, загрузка модуля по-прежнему будет синхронной из-за ограничений функции require, но все остальное будет асинхронным, как вы пожелаете.

Последнее замечание: если вы хотите сделать асинхронные модули загрузки, вы можете реализовать функцию, которая использует 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). Из-за того, что вы завершаете загруженный код внутри немедленно исполняемой функции, все в этом модуле остается закрытым, и единственным интерфейсом для внешнего мира является объект 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…
  });
};

Надеюсь, что это поможет.

Ответ 2

Да - функция экспорта, принимающая обратный вызов или, возможно, даже экспортирующий полный функциональный объект обещания.

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

Ответ 3

Код Андрея ниже - самый простой ответ, который работает, но у него была небольшая ошибка, поэтому я отправляю исправление здесь как ответ. Кроме того, я просто использую обратные вызовы, а не 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 */

Ответ 4

Модуль 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(), метод, обсуждаемый в принятом ответе. Он имеет синюю птицу как зависимость.

(Это решение появилось в более раннем ответе, но это было удалено путем обзора.)