Сегодня мы рады начать нашу новую серию из трех статей о правильном способе кодирования общих шаблонов проектирования 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 более распространенными шаблонами и как не злоупотреблять ими!