Статьи

3 распространенных шаблона проектирования Node.js, которые используются не по назначению

Сегодня мы рады начать нашу новую серию из трех статей о правильном способе кодирования общих шаблонов проектирования node.js от участника Node.js и инженера AppNeta Стивена Белангера . С 6 лет опыта, Стивен помог сообществу Node.js процветать , а также способствует надежной производительности приложений мониторинга благодаря своей работе на инструментовки AppNeta для Node.js .

От отправителей событий и потоков (включенных ниже) до конструкторов (включенных в часть 2) и обещаний (включенных в часть 3) шаблоны являются центральными для разработки Node.js. Хотя в мире JavaScript существуют традиционные шаблоны проектирования, многие из них были модифицированы и обновлены, чтобы использовать асинхронный характер Node.js. Существует много способов использования наиболее распространенных шаблонов проектирования, но ниже приведены общие примеры использования и типичные ошибки, которые даже опытные разработчики допустят, начиная с Node.js. Чтобы проиллюстрировать эти шаблоны, в пояснения были включены примеры, а также ссылки на соответствующие документы на сайте nodejs.org .

Общие паттерны

Со многими новыми разработчиками в сообществе Node.js я видел много недопонимания относительно некоторых общих шаблонов проектирования, которые вытекают из аналогичных шаблонов в других языках. Node.js может быть сложным, но он также может быть быстрым, если все сделано правильно. Ниже я обрисовал в общих чертах первые 3 образца в серии постов, где мы рассмотрим, как вы должны использовать Node.js.

Callbacks

Обратные вызовы — это, возможно, самая центральная модель разработки node.js. Большинство API в ядре node.js основаны на обратных вызовах, поэтому важно понимать, как они работают.

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

keyValueStore.get('my-data', function (err, data) {
})

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

Функции, принимающие обратные вызовы в Node.js, почти всегда ожидают ошибок, за исключением fs.exists , который выглядит следующим образом:

fs.exists('/etc/passwd', function (exists) {
 console.log(exists ? "it's there" : 'no passwd!')
})

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

Вызов `fs.readFile` является асинхронным:

fs.readFile('something', function (err, data) {
  console.log('this is reached second')
})

console.log('this is reached first')

Но вызов `list.forEach` является синхронизированным.

var list = [1]

list.forEach(function (v) {
  console.log('this is reached first')
})

console.log('this is reached second')

Смущает, верно? Это сбивает с толку большинство новых пользователей Node.js. Чтобы понять это, любой код, взаимодействующий с данными вне памяти процесса, должен быть асинхронным, поскольку диски и сети работают медленно, поэтому мы не хотим их ждать.

Эмитенты событий

Генератор событий — это специальная конструкция, предназначенная для того, чтобы интерфейс мог назначать множество обратных вызовов для множества различных вариантов поведения, которые могут происходить один раз, много раз или даже никогда. В Node.js источники событий работают, выставляя общий API для объекта, предоставляя несколько функций для регистрации и запуска этих обратных вызовов. Этот интерфейс часто присоединяется через наследование .

Рассмотрим этот пример:

var emitter = new EventEmitter

emitter.on('triggered', function () {
  console.log('The event was triggered')
})

emitter.emit('triggered')

Источники событий сами по себе являются синхронными, но то, к чему они могут быть прикреплены, иногда нет, что является еще одним источником путаницы в отношении асинхронности в Node.js. Точка от вызова `emitter.emit (…)` до момента запуска обратного вызова, переданного в `emitter.on (…)`, является синхронной, но объект может быть передан, и функция emit может быть использована в будущем. ,

Streams

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

Обычное использование потоков — чтение файлов. Загрузка большого файла в память одновременно не масштабируется, поэтому вы можете использовать потоки, чтобы позволить вам работать с фрагментами файла. Чтобы прочитать файл в виде потока, вы должны сделать что-то вроде этого:

var file = fs.createReadStream('something')

file.on('error', function (err) {
  console.error('an error occured', err)
})

file.on('data', function (data) {
 console.log('I got some data: ' + data)
})

file.on('end', function () {
 console.log('no more data')
})

Событие «data» фактически является частью «текущего» или «push» режима, представленного в API потоков 1. Он проталкивает данные через канал так быстро, как может. Часто для хорошего масштабирования действительно необходим поток извлечения, который можно выполнить с помощью события «readable» и функции «read (…)».

file.on('readable', function () {
  var chunk = file.read()
  if (chunk) {
    console.log('I got some data: ' + data)
  }
})

Обратите внимание, что вам нужно проверить, является ли чанк нулевым, поскольку потоки теряют нулевое значение.

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

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

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

socket.pipe(jsonParseStream()).pipe(eachObject(function (item) {
  console.log('got an item', item)
}))

`JsonParseStream` будет излучать каждый элемент анализируемого массива в виде потока объектного режима. Поток `eachObject` мог бы затем получать каждое из этих событий объектного режима и выполнять с ними некоторые операции, в этом случае регистрируя их.

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

socket
  .on('error', function (err) {
    console.error('a socket error occurred', err)
  })

  .pipe(jsonParseStream())

  .on('error', function (err) {

    console.error('a json parsing error occurred', err)
  })

  .pipe(iterateStream(function (item) {
    console.log('got an item', item)
  }))

  .on('error', function (err) {
    console.error('an iteration error occurred', err)
  })

Что дальше?

Настройтесь на следующую неделю, где я продолжу эту серию с 3 более распространенными шаблонами и как не злоупотреблять ими!