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

Асинхронная инициализация модуля Node.js

Я хотел бы инициализировать модуль асинхронным способом и создать пару идей. Мне нужен объект DB со списком коллекций из Mongo и других данных, но список файлов в ./ будет выполняться для краткости.

Я не могу экспортировать функцию или класс, потому что мне нужно require('db') возвращать тот же объект каждый раз.


Первым и простейшим, что мне пришло в голову, - назначить module.exports на Object и заполнить его позже:

var exports = {};
module.exports = exports;

require('fs').readdir('.', function(err, files) {
  exports.error = err;
  exports.files = files;
});

Плохая вещь - я действительно не знаю извне, когда список готов и нет хорошего способа проверить наличие ошибок.


Второй способ, которым я воспользовался, - наследовать EventEmitter и уведомлять всех о том, что БД готова или произошла ошибка. Если все в порядке - продолжайте.

var events = require('events');
var util = require('util');

function Db() {
  events.EventEmitter.call(this);
  this.ready = false;
  this.files = null;
  this.initialize();
}

util.inherits(Db, events.EventEmitter);

Db.prototype.initialize = function() {
  if (this.ready)
    return this.emit('ready');

  var self = this;
  require('fs').readdir('.', function(err, files) {
    if (err)
      return self.emit('error', err);

    self.files = files;
    self.ready = true;
    self.emit('ready');
  });
};

module.exports = new Db();

И сейчас Я думаю, что более разумно:

// db.js
var exports = {init: init};
module.exports = exports;

function init(callback) {
  callback = (typeof callback === 'function') ? callback : function() {};
  require('fs').readdir('.', function(err, files) {
    delete exports.init;
    exports.result = files; // that pretty much what I need,
                            // so don't mind result slightly differs
                            // from previous cases
    callback(err);
  });
}
// main.js
var db = require('./db');

// check for `db.init` presence maybe...

db.init(function(err) {
  return err ? console.error('Bad!')
             : console.log(db); // It works!
});

Что я должен выбрать и почему? Насколько плохо такая идея вообще и мои варианты в частности?

Спасибо за отзывы.

4b9b3361

Ответ 1

TL; DR: Используйте readdirSync() вместо readdir(), если вы планируете читать локальные файлы во время запуска. Если вы планируете фактически считывать данные из удаленной базы данных или делать какие-либо операции ввода-вывода во время выполнения, используйте свой вариант №2 - обратный вызов. Объяснение и примеры кода ниже.

Подробное объяснение:

Хотя сначала это может показаться вопросом, связанным с модулем /dependecy/require -related, на самом деле это не так. Это общий вопрос о том, как обрабатывать асинхронный код. Позвольте мне объяснить:

require() - это в основном единственная синхронная функция, широко используемая во всем node, которая имеет дело с I/O (для этого требуются другие модули из файловой системы). Синхронный означает, что он фактически возвращает данные в качестве возвращаемого значения вместо вызова обратного вызова.

Самое основное правило 101 в асинхронном программировании:

Вы можете никогда взять асинхронный кусок кода и создать для него синхронный API.

require используется специальная синхронная версия readFile, называемая readFileSync. Поскольку модули действительно загружаются только в начале программы, то факт, что он блокирует выполнение node.js при чтении модуля, не является проблемой.

В вашем примере, однако, вы пытаетесь выполнить дополнительный асинхронный ввод-вывод - readdir(), выполненный во время этапа require. Таким образом, вам нужно либо использовать синхронную версию этой команды, либо API необходимо изменить...

Итак, у вас есть фон для вашей проблемы.

Вы определили два основных варианта:

  • используя обещание (которое по существу совпадает с вашим примером EventEmitter)
  • с помощью обратного вызова (ваш второй пример показывает это хорошо) и третий:
  • с помощью синхронной версии команды readdir() под названием readdirSync()

Я использовал бы опцию # 3 для простоты, но только если вы планируете просто прочитать пару файлов во время запуска, как показывает ваш пример. Если позже ваш модуль БД действительно собирается подключиться к базе данных - или если вы планируете сделать что-либо из этого во время выполнения, скачайте лодку сейчас и идите с асинхронным API.

Не так много людей это помнят, но promises на самом деле были исходным дефолтом, как обрабатывать async в node.js. В node 0.1.30 однако обещания были удалены и заменены стандартным обратным вызовом с подписью function(err, result). Это было сделано в основном по причинам простоты.

В наши дни подавляющее большинство ваших асинхронных вызовов принимает этот стандартный обратный вызов в качестве последнего параметра. Ваш драйвер базы данных делает это, ваша веб-инфраструктура делает это - везде. Вы должны остаться с преобладающим дизайном и использовать его тоже.

Единственная причина предпочитать promises или события - это если у вас есть несколько разных результатов, которые могут произойти. Например, сокет можно открыть, получить данные, закрыть, сбросить и т.д.

Это не ваше дело. Ваш модуль всегда делает то же самое (читает некоторые файлы). Так что вариант # 2 - это (если вы не можете оставаться синхронным).

Наконец, вот два варианта выигрыша, переписанные немного:

Синхронная опция:
хорошо только для локальной файловой системы во время запуска

// db.js
var fs = require('fs');
exports = fs.readdirSync('.');

// main.js
var db = require('./db');
// insert rest of your main.js code here

Асинхронная опция:
когда вы хотите использовать БД и т.д.

// db.js
var fs = require('fs'), cached_files;

exports.init = function(callback) {
  if (cached_files) {
    callback(null, cached_files);
  } else {
    fs.readdir('.', function(err, files) {
      if (!err) {
        cached_files = files;
      }
      callback(err, files);
    });
  }
};

// main.js
require('./db').init(function(err, files) {
  // insert rest of your main.js code here
});

Ответ 2

В общем, очень плохая идея иметь какое-либо состояние в модуле. Модули должны раскрывать функции, а не данные (да, для этого требуется немного изменить структуру кода). Просто передайте ссылки на ваши данные в функции модулей как параметры.

( edit: только понял, что это подход из вашего последнего примера. Мое голосование за него)

module1:

module.exports = function(params, callback) { ... }

module2:

var createSomething = require('module1');
module.exports = function(params, callback) { 
   ...
   var db = createSomething(params, function(err, res) {
       ...
       callback(err, res);
   }
}

Основной код:

var createSomethingOther = require('module2');
createSomethingOther(err, result) {
    // do stuff
}

Ответ 3

На моей стороне такой модуль является функцией, которая принимает обратный вызов (и если внутренне настроенная с помощью promises также возвращает обещание (см. https://github.com/medikoo/deferred));

Единственная проблема с обратным вызовом заключается в том, что по соглашению она всегда должна вызываться в nextTick, поэтому даже когда вы вызываете функцию своего модуля, когда все данные собираются, вы все равно должны будете вызвать ваш обратный вызов в следующем тике с набором результатов.