Рубрики
Без рубрики

Понимание у Node.js архитектура

Обновление: эта статья теперь является частью моей книги «Node.js за пределы основы». Проверьте обновленную версию этого контента и далее о узле на jscomplete.com/node-beyond-basics.mide.Мастым из объектов узлов, Ответы и потоки – реализовать модуль Eventemitter, чтобы они могли предоставить путь

Автор оригинала: FreeCodeCamp Community Member.

Большинство объектов узла – как HTTP-запросы, ответы и потоки – реализовать Eventemitter Модуль, поэтому они могут предоставить способ выделять и слушать события.

Самая простая форма характера, ориентированного на события, является стиль обратного вызова некоторых из популярных функций Node.js – например, Fs.readfile Отказ В этой аналогии событие будет уволено один раз (когда узел готов к вызова обратного вызова) и обратный вызов действует как обработчик событий.

Давайте сначала исследовать эту основную форму.

Позвони мне, когда вы будете готовы, узел!

Оригинальный узел узел, занимающийся асинхронными событиями, был с обратным вызовом. Это было давно, до того, как JavaScript имел родные обещает поддержку и функцию Async/a ждать.

Обратные вызовы в основном просто функции, которые вы передаете в другие функции. Это возможно в JavaScript, потому что функции являются объектами первого класса.

Важно понимать, что обратные вызовы не указывают на асинхронный вызов в коде. Функция может вызвать обратный вызов как синхронно и асинхронно.

Например, вот хост функции Файловые файлы который принимает функцию обратного вызова CB и может вызывать эту функцию обратного вызова как синхронно и асинхронно на основе условия:

function fileSize (fileName, cb) {
  if (typeof fileName !== 'string') {
    return cb(new TypeError('argument should be string')); // Sync
  }
  fs.stat(fileName, (err, stats) => {
    if (err) { return cb(err); } // Async
    cb(null, stats.size); // Async
  });
}

Обратите внимание, что это плохая практика, которая приводит к неожиданным ошибкам. Функции проектирования хоста для потребления обратного вызова либо всегда синхронно или всегда асинхронно.

Давайте рассмотрим простой пример типичной функции асинхронного узла, который написан с стилем обратного вызова:

const readFileAsArray = function(file, cb) {
  fs.readFile(file, function(err, data) {
    if (err) {
      return cb(err);
    }
    const lines = data.toString().trim().split('\n');
    cb(null, lines);
  });
};

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

Вот пример используете для него. Предполагая, что у нас есть файл номера .txt В том же каталоге с контентом, как это:

10
11
12
13
14
15

Если у нас есть задача считать нечетные числа в этом файле, мы можем использовать ReadfiLeasarray Чтобы упростить код:

readFileAsArray('./numbers.txt', (err, lines) => {
  if (err) throw err;
  const numbers = lines.map(Number);
  const oddNumbers = numbers.filter(n => n%2 === 1);
  console.log('Odd numbers count:', oddNumbers.length);
});

Код читает содержимое номеров в массив строк, анализирует их как цифры и считает нечетные.

Стиль обратного вызова узна используется чисто здесь. Обратный вызов имеет ошибку – первый аргумент Err Это исчезнет, и мы передаем обратный вызов как последний аргумент для функции хоста. Вы всегда должны делать это в ваших функциях, потому что пользователи, вероятно, предполагают это. Сделайте функцию Host получать обратный вызов в качестве последнего аргумента и сделать обратный вызов ожидаю, что объект ошибок в качестве первого аргумента.

Современный JavaScript альтернативы обратным вызовам

В современном JavaScript у нас есть объекты обещания. Обещания могут быть альтернативой обратным вызовам для асинхронных API. Вместо того, чтобы пропустить обратный вызов в качестве аргумента и обработки ошибки в том же месте, объект обещания позволяет нам обрабатывать случаи успеха и ошибок отдельно, и он также позволяет нам цеплять несколько асинхронных вызовов, а не вкладывать их.

Если ReadfiLeasarray Функция поддерживает обещания, мы можем использовать его следующим образом:

readFileAsArray('./numbers.txt')
  .then(lines => {
    const numbers = lines.map(Number);
    const oddNumbers = numbers.filter(n => n%2 === 1);
    console.log('Odd numbers count:', oddNumbers.length);
  })
  .catch(console.error);

Вместо того, чтобы проходить в функции обратного вызова, мы позвонили .then Функция на возвращенном значении функции хоста. Это .then Функция обычно дает нам доступ к одинаковым массивам, которые мы получаем в версии обратного вызова, и мы можем сделать нашу обработку на нем как раньше. Чтобы обрабатывать ошибки, мы добавляем .catch Позвоните в результате, и это дает нам доступ к ошибке, когда это происходит.

Создание функции Host поддерживает интерфейс обещания, проще в современном JavaScript благодаря новому объекту обещания. Вот …| ReadfiLeasarray Функция модифицирована для поддержки интерфейса обещания в дополнение к интерфейсу обратного вызова он уже поддерживает:

const readFileAsArray = function(file, cb = () => {}) {
  return new Promise((resolve, reject) => {
    fs.readFile(file, function(err, data) {
      if (err) {
        reject(err);
        return cb(err);
      }
      const lines = data.toString().trim().split('\n');
      resolve(lines);
      cb(null, lines);
    });
  });
};

Итак, мы делаем функцию возвращать объект обещания, который обертывает Fs.readfile async call. Объект обещания открывает два аргумента, а решить Функция и A Отклонить функция.

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

Единственное, что нам нужно было сделать в этом случае, состоит в том, чтобы иметь значение по умолчанию для этого аргумента обратного вызова в случае использования кода с интерфейсом обещания. Мы можем использовать простую пустую функцию по умолчанию в аргументе для этого: () => {}.

Потребление обещаний с async/ждать

Добавление интерфейса для обещания делает ваш код намного легче работать с тем, когда есть необходимость в цикле по функции async. С обратными вызовами все становится грязным.

Обещания улучшают это немного, и генераторы функций улучшаются на этом немного больше. Это сказано, что более поздняя альтернатива для работы с Async Code – использовать async Функция, которая позволяет нам лечить ASYNC код, как если бы она была синхронно, что делает его намного более читаемой в целом.

Вот как мы можем потреблять ReadfiLeasarray Функция с async/a ждать:

async function countOdd () {
  try {
    const lines = await readFileAsArray('./numbers');
    const numbers = lines.map(Number);
    const oddCount = numbers.filter(n => n%2 === 1).length;
    console.log('Odd numbers count:', oddCount);
  } catch(err) {
    console.error(err);
  }
}
countOdd();

Сначала мы создаем функцию async, которая просто нормальная функция со словом async перед этим. Внутри асинхронизации мы называем ReadfiLeasarray Функция, как если бы она возвращает переменную линии, и сделать эту работу, мы используем ключевое слово ждать Отказ После этого мы продолжаем код, как будто ReadfiLeasarray Звонок был синхронно.

Чтобы запускать вещи, мы выполняем функцию async. Это очень простое и читаемое. Работать с ошибками, нам нужно обернуть асинхронный звонок в попробуйте / поймать утверждение.

С этой функцией Async/ждут, нам не нужно было использовать какие-либо специальные API (например. Мы только что пометили функции по-разному и использовали чистый JavaScript для кода.

Мы можем использовать функцию async/a ждать с любой функцией, которая поддерживает интерфейс обещания. Тем не менее, мы не можем использовать его с async-функциями в стиле обратного вызова (например, сетутом).

Модуль eventemitter.

Eventemitter – это модуль, который облегчает связь между объектами в узле. Eventemitter находится в ядре узла асинхронной архитектуры, управляемой событием. Многие из встроенных модулей узла наследуют от Eventemitter.

Концепция проста: объекты эмиттера выделяют названные события, которые вызывают ранее зарегистрированные слушатели. Итак, объект эмиттера в основном имеет две основные функции:

  • Излучающие имени события.
  • Регистрация и незарегистрированные функции слушателя.

Для работы с Eventemitter мы просто создаем класс, который расширяет Eventemitter.

class MyEmitter extends EventEmitter {}

Объекты излучателей – это то, что мы создали от классов на основе Eventemitter:

const myEmitter = new MyEmitter();

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

myEmitter.emit('something-happened');

Выпуская событие является сигналом, которое произошло некоторое условие. Это условие обычно связано с изменением состояния в исходном объекте.

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

События

Давайте посмотрим на пример:

const EventEmitter = require('events');

class WithLog extends EventEmitter {
  execute(taskFunc) {
    console.log('Before executing');
    this.emit('begin');
    taskFunc();
    this.emit('end');
    console.log('After executing');
  }
}

const withLog = new WithLog();

withLog.on('begin', () => console.log('About to execute'));
withLog.on('end', () => console.log('Done with execute'));

withLog.execute(() => console.log('*** Executing task ***'));

Класс Отлоги это эмиттер событий. Определяет функцию одного экземпляра Выполнить Отказ Это Выполнить Функция получает один аргумент, функция задач и включает его выполнение с операторами журнала. Он запускает события до и после выполнения.

Чтобы увидеть последовательность того, что произойдет здесь, мы регистрируем слушателей на обоих именованных событиях, и, наконец, выполняйте задачу образца для запуска вещей.

Вот вывод этого:

Before executing
About to execute
*** Executing task ***
Done with execute
After executing

То, что я хочу, чтобы вы заметили о выходе выше, состоит в том, что все это происходит синхронно. В этом коде нет ничего асинхронного.

  • Мы получаем «перед выполнением» линию первой.
  • начать Затем названное событие вызывает линию «о выполнении».
  • Фактическая линия выполнения затем выводит линию «*** выполняемая задача ***».
  • конец Именованное событие, затем вызывает линию «сделано с выполнением»
  • Мы получаем «после выполнения» Line Line.

Также как простые обратные обратные вызовы, не предполагайте, что события означают синхронный или асинхронный код.

Это важно, потому что если мы пройдем асинхронный taskfunc к Выполнить , излучаемые события больше не будут точными.

Мы можем имитировать случай с Setimmediate вызов:

// ...

withLog.execute(() => {
  setImmediate(() => {
    console.log('*** Executing task ***')
  });
});

Теперь вывод будет:

Before executing
About to execute
Done with execute
After executing
*** Executing task ***

Это не верно. Линии после асинкового вызова, которые были вызваны «выполненными с выполнением» и «после выполнения» вызовов, больше не точны.

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

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

Асинхронные события

Преобразуем пример синхронного образца в нечто асинхронное и немного более полезное.

const fs = require('fs');
const EventEmitter = require('events');

class WithTime extends EventEmitter {
  execute(asyncFunc, ...args) {
    this.emit('begin');
    console.time('execute');
    asyncFunc(...args, (err, data) => {
      if (err) {
        return this.emit('error', err);
      }

      this.emit('data', data);
      console.timeEnd('execute');
      this.emit('end');
    });
  }
}

const withTime = new WithTime();

withTime.on('begin', () => console.log('About to execute'));
withTime.on('end', () => console.log('Done with execute'));

withTime.execute(fs.readFile, __filename);

Со временем класс выполняет asyncfunc и сообщает время, которое принято этим asyncfunc Использование Консоль. Время и Console.Timeend звонки. Это испускает правильную последовательность событий до и после выполнения. А также излучает ошибки/события данных для работы с обычными сигналами асинхронных вызовов.

Мы тестируем со временем Эмиттер, передавая это Fs.readfile Вызов, который является асинхронной функцией. Вместо обработки файлов данных с обратным вызовом, теперь мы можем слушать событие данных.

Когда мы выполняем этот код, мы получаем правильную последовательность событий, как и ожидалось, и мы получаем сообщенное время для выполнения, что полезно:

About to execute
execute: 4.507ms
Done with execute

Обратите внимание, как нам нужно было объединить обратный вызов с помощью Emitter Emitter для достижения этого. Если asynfunc Поддерживаемые обещания, мы могли бы использовать функцию Async/ждут, чтобы сделать то же самое:

class WithTime extends EventEmitter {
  async execute(asyncFunc, ...args) {
    this.emit('begin');
    try {
      console.time('execute');
      const data = await asyncFunc(...args);
      this.emit('data', data);
      console.timeEnd('execute');
      this.emit('end');
    } catch(err) {
      this.emit('error', err);
    }
  }
}

Я не знаю о вас, но это гораздо более читается для меня, чем код на основе обратного вызова или любые линии. Функция Async/a enait приносит нам как можно ближе к самому языку JavaScript, который, я думаю, является большой победой.

События аргументов и ошибок

В предыдущем примере было два события, которые были излучены дополнительными аргументами.

Событие ошибки испускается объектом ошибки.

this.emit('error', err);

Событие данных испускается объектом данных.

this.emit('data', data);

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

Например, чтобы работать с событием данных, функция слушателя, которую мы регистрируем, получат доступ к аргументу данных, который был передан в исходное событие, и этот объект данных является именно то, что asyncfunc выдержки.

withTime.on('data', (data) => {
  // do something with data
});

Ошибка . Мероприятие обычно является особенным. В нашем примере на основе обратного вызова, если мы не обрабатываем событие ошибки со слушателем, процесс узла фактически выйдет.

Чтобы продемонстрировать, что сделайте другой звонок в метод Execute с плохой аргументом:

class WithTime extends EventEmitter {
  execute(asyncFunc, ...args) {
    console.time('execute');
    asyncFunc(...args, (err, data) => {
      if (err) {
        return this.emit('error', err); // Not Handled
      }

      console.timeEnd('execute');
    });
  }
}

const withTime = new WithTime();

withTime.execute(fs.readFile, ''); // BAD CALL
withTime.execute(fs.readFile, __filename);

Первый приведенный выше вызовов выполнит ошибку. Процесс узла собирается сбой и выйти:

events.js:163
      throw er; // Unhandled 'error' event
      ^
Error: ENOENT: no such file or directory, open ''

Второй вызов Execute будет затронут этот крах и потенциально не будет выполнен вообще.

Если мы зарегистрируем слушателя для специальных Ошибка . Событие, поведение процесса узла изменится. Например:

withTime.on('error', (err) => {
  // do something with err, for example log it somewhere
  console.log(err)
});

Если мы сделаем вышеизложенное, будет сообщено ошибка с первого вызова выполнения, но процесс узла не будет сбиваться и выходить. Другой вызов Execute завершится нормально:

{ Error: ENOENT: no such file or directory, open '' errno: -2, code: 'ENOENT', syscall: 'open', path: '' }
execute: 4.276ms

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

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: ENOENT: no such file or directory, open ''
DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Другой способ обрабатывать исключения из испускаемых ошибок – зарегистрировать слушателя для глобального uncuptexception Процесс события. Тем не менее, добивание ошибок глобально с этим событием – плохое представление.

Стандартный совет о uncuptexception Чтобы избежать его использования, но если вы должны сделать (скажем, чтобы сообщить, что произошло или делают очистки), вы должны просто позволить выходу процесса в любом случае:

process.on('uncaughtException', (err) => {
  // something went unhandled.
  // Do any cleanup and exit anyway!

  console.error(err); // don't do just that.

  // FORCE exit the process too.
  process.exit(1);
});

Однако представьте, что несколько событий ошибок происходят в то же время. Это означает uncuptexception Слушатель выше будет вызван несколько раз, что может быть проблемой для некоторых кода очистки. Примером этого является когда выполняется несколько вызовов в действие отключения базы данных.

Eventemitter Модуль обнажает Однажды метод. Этот метод сигнализирует, чтобы вызвать слушателя только один раз, не каждый раз, когда это происходит. Итак, это практическое использование случая для использования с беспрепятственным применением, потому что с первым неосторожным исключением мы начнем выполнить очистку, и мы знаем, что мы собираемся выйти из процесса в любом случае.

Порядок слушателей

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

// प्रथम
withTime.on('data', (data) => {
  console.log(`Length: ${data.length}`);
});

// दूसरा
withTime.on('data', (data) => {
  console.log(`Characters: ${data.toString().length}`);
});

withTime.execute(fs.readFile, __filename);

Приведенный выше код приведет к вошению линии «длина» перед линейкой «символов», потому что это порядок, в котором мы определили эти слушатели.

Если вам нужно определить новый слушатель, но имейте ли этот слушатель, вы можете использовать PrependListener Метод:

// प्रथम
withTime.on('data', (data) => {
  console.log(`Length: ${data.length}`);
});

// दूसरा
withTime.prependListener('data', (data) => {
  console.log(`Characters: ${data.toString().length}`);
});

withTime.execute(fs.readFile, __filename);

Вышеприведенное приведет к вошению линии «символов», чтобы войти в систему.

И, наконец, если вам нужно удалить слушателя, вы можете использовать Removelistener метод.

Это все, что у меня есть для этой темы. Спасибо за прочтение! До скорого!

Изучение реагировать или узел? Оформить заказ моих книг: