Этот материал завершает серию переводов руководства по Node.js. Сегодня мы поговорим о модулях os, events и http, обсудим работу с потоками и базами данных, затронем вопрос использования Node.js при разработке приложений и в продакшне.
Модуль
Здесь имеются несколько полезных свойств, которые, в частности, могут пригодиться при работе с файлами.
Так, свойство
Надо отметить, что упоминая тут «Linux и macOS», мы говорим о POSIX-совместимых платформах. Ради краткости изложения менее популярные платформы мы тут не упоминаем.
Свойство
Свойство
Теперь рассмотрим основные методы модуля
Этот метод возвращает строку, идентифицирующую архитектуру системы, например —
Возвращает информацию о процессорах, доступных в системе. Например, эти сведения могут выглядеть так:
Возвращает
Возвращает количество свободной системной памяти в байтах.
Возвращает путь к домашней директории текущего пользователя. Например —
Возвращает имя хоста.
Возвращает, в виде массива, данные о средних значениях нагрузки, вычисленные операционной системой. Эта информация имеет смысл только в Linux и macOS. Выглядеть она может так:
Возвращает сведения о сетевых интерфейсах, доступных в системе. Например:
Возвращает сведения о платформе, для которой был скомпилирован Node.js. Вот некоторые из возможных возвращаемых значений:
Возвращает строку, идентифицирующую номер релиза операционной системы.
Возвращает путь к заданной в системе директории для хранения временных файлов.
Возвращает общее количество системной памяти в байтах.
Возвращает сведения, позволяющие идентифицировать операционную систему. Например:
Возвращает время работы системы в секундах с последней перезагрузки.
Модуль
Объект класса
Рассмотрим наиболее полезные методы объектов класса
Псевдоним для метода
Генерирует событие. Синхронно вызывает все обработчики события в том порядке, в котором они были зарегистрированы.
Возвращает массив, который содержит зарегистрированные события.
Возвращает максимальное число обработчиков, которые можно добавить к объекту класса
Возвращает количество обработчиков события, имя которого передаётся данному методу в качестве параметра:
Возвращает массив обработчиков события для соответствующего события, имя которого передано этому методу:
Псевдоним для метода
Регистрируеn коллбэк, который вызывается при генерировании события. Вот как им пользоваться:
Регистрирует коллбэк, который вызывается только один раз — при первом возникновении события, для обработки которого зарегистрирован этот коллбэк. Например:
При регистрации обработчика с использованием методов
Этот метод похож на предыдущий. А именно, когда обработчик, предназначенный для однократного вызова, регистрируется с помощью метода
Данный метод удаляет все обработчики для заданного события, зарегистрированные в соответствующем объекте. Пользуются им так:
Удаляет заданный обработчик, который нужно передать данному методу. Для того чтобы сохранить обработчик для последующего удаления соответствующий коллбэк можно назначить переменной. Выглядит это так:
Этот метод позволяет задать максимальное количество обработчиков, которые можно добавить к отдельному событию в экземпляре класса
В восьмой части этой серии материалов мы уже говорили о стандартном модуле Node.js
В его состав входят свойства, методы и классы. Поговорим о них.
В этом свойстве перечисляются все поддерживаемые методы HTTP:
Здесь содержатся коды состояния HTTP и их описания:
Данное свойство указывает на глобальный экземпляр класса
Возвращает новый экземпляр класса
Позволяет выполнить HTTP-запрос к серверу, создавая экземпляр класса
Этот метод похож на
Модуль HTTP предоставляет 5 классов —
Глобальный экземпляр класса
Объект класса
Экземпляры этого класса используются для создания серверов с применением команды
Этот объект создаётся классом
В таких обработчиках, после того, как ответ сервера будет готов к отправке клиенту, вызывают метод
Вот методы, которые используются для работы с HTTP-заголовками:
После обработки заголовков их можно отправить клиенту, вызвав метод
Для отправки данных клиенту в теле ответа используют метод
Если до этого заголовки ещё не были установлены командой
Объект класса
Его можно использовать для работы с данными ответа. А именно:
Данные ответа представлены в виде потока так как объект
Потоки — это одна из фундаментальных концепций, используемых в Node.js-приложениях. Потоки — это инструменты, которые позволяют выполнять чтение и запись файлов, организовывать сетевое взаимодействие систем, и, в целом — эффективно реализовывать операции обмена данными.
Концепция потоков не уникальна для Node.js. Они появились в ОС семейства Unix десятки лет назад. В частности, программы могут взаимодействовать друг с другом, передавая потоки данных с использованием конвейеров (с применением символа конвейера —
Если представить себе, скажем, чтение файла без использования потоков, то, в ходе выполнения соответствующей команды, содержимое файла будет целиком считано в память, после чего с этим содержимым можно будет работать.
Благодаря использованию механизма потоков файлы можно считывать и обрабатывать по частям, что избавляет от необходимости хранить в памяти большие объёмы данных.
Модуль Node.js stream представляет собой основу, на которой построены все API, поддерживающие работу с потоками.
Потоки, в сравнении с другими способами обработки данных, отличаются следующими преимуществами:
Традиционный пример работы с потоками демонстрирует чтение файла с диска.
Сначала рассмотрим код, в котором потоки не используются. Стандартный модуль Node.js
Метод
Метод
Если размер файла велик, то эта операция займёт немало времени. Вот тот же пример переписанный с использованием потоков:
Вместо того, чтобы ждать того момента, когда файл будет полностью прочитан, мы начинаем передавать его данные клиенту сразу после того, как первая порция этих данных будет готова к отправке.
В предыдущем примере мы использовали конструкцию вида
Его вызывают для потока, представляющего собой источник данных. В данном случае это — файловый поток, который отправляют в HTTP-ответ.
Возвращаемым значением метода
Это равносильно такой конструкции:
Потоки — полезный механизм, в результате многие модули ядра Node.js предоставляют стандартные возможности по работе с потоками. Перечислим некоторые из них:
Существует четыре типа потоков:
Поток для чтения можно создать и инициализировать, воспользовавшись возможностями модуля
Теперь в поток можно поместить данные, которые позже сможет прочесть потребитель этих данных:
Для того чтобы создать записываемый поток нужно расширить базовый объект
Затем реализуем его метод
Теперь к такому потоку можно подключить поток, предназначенный для чтения:
Для того чтобы получить данные из потока, предназначенного для чтения, воспользуемся потоком для записи:
Команда
Работать с потоками для чтения можно и напрямую, обрабатывая событие
Для отправки данных в поток для записи используется метод
Для того чтобы сообщить потоку для записи о том, что запись данных в него завершена, можно воспользоваться его методом
Этот метод принимает несколько необязательных параметров. В частности, ему можно передать последнюю порцию данных, которые надо записать в поток.
MySQL является одной из самых популярных СУБД в мире. В экосистеме Node.js имеется несколько пакетов, которые позволяют взаимодействовать с MySQL-базами, то есть — сохранять в них данные, получать данные из баз и выполнять другие операции.
Мы будем использовать пакет mysqljs/mysql. Этот проект, который существует уже очень давно, собрал более 12000 звёзд на GitHub. Для того чтобы воспроизвести следующие примеры, вам понадобится MySQL-сервер.
Для установки этого пакета воспользуйтесь такой командой:
Сначала подключим пакет в программе:
После этого создадим соединение:
Теперь попытаемся подключиться к базе данных:
В вышеприведённом примере объект
На самом деле этих параметров существует гораздо больше. В том числе — следующие:
Теперь всё готово к выполнению SQL-запросов к базе данных. Для выполнения запросов используется метод соединения
При формировании запроса можно использовать значения, которые будут автоматически встроены в строку запроса:
Для передачи в запрос нескольких значений можно, в качестве второго параметра, использовать массив:
Запросы
Если у таблицы, в которую добавляются данные, есть первичный ключ со свойством
После того как работа с базой данных завершена и пришло время закрыть соединение — воспользуйтесь его методом
Это приведёт к правильному завершению работы с базой данных.
Создавая приложения в среде Node.js можно использовать различные конфигурации для окружения разработки и продакшн-окружения.
По умолчанию платформа Node.js работает в окружении разработки. Для того чтобы указать ей на то, что код выполняется в продакшн-среде, можно настроить переменную окружения
Обычно это делается в командной строке. В Linux, например, это выглядит так:
Лучше, однако, поместить подобную команду в конфигурационный файл наподобие
Настроить значение переменной окружения можно, воспользовавшись следующей конструкцией при запуске приложения:
Эта переменная окружения широко используется во внешних библиотеках для Node.js. Установка
Например, Pug — библиотека для работы с шаблонами, используемая Express, готовится к работе в режиме отладки в том случае, если переменная
Express предоставляет конфигурационные хуки для каждого окружения. То, какой именно будет вызван, зависит от значения
Например, с их помощью можно использовать различные обработчики событий для разных режимов:
Надеемся, освоив это руководство, вы узнали о платформе Node.js достаточно много для того, чтобы приступить к работе с ней. Полагаем, теперь вы, даже если начали читать первую статью этого цикла, совершенно не разбираясь в Node.js, сможете начать писать что-то своё, с интересном читать чужой код и с толком пользоваться документацией к Node.js.
Уважаемые читатели! Если вы прочли эту серию публикаций, не имея знаний о разработке для Node.js, просим рассказать о том, как вы оцениваете свой уровень теперь.
[Советуем почитать] Другие части цикла
Часть 1: Общие сведения и начало работы
Часть 2: JavaScript, V8, некоторые приёмы разработки
Часть 3: Хостинг, REPL, работа с консолью, модули
Часть 4: npm, файлы package.json и package-lock.json
Часть 5: npm и npx
Часть 6: цикл событий, стек вызовов, таймеры
Часть 7: асинхронное программирование
Часть 8: протоколы HTTP и WebSocket
Часть 9: работа с файловой системой
Часть 10: стандартные модули, потоки, базы данных, NODE_ENV
Полная PDF-версия руководства по Node.js
Часть 2: JavaScript, V8, некоторые приёмы разработки
Часть 3: Хостинг, REPL, работа с консолью, модули
Часть 4: npm, файлы package.json и package-lock.json
Часть 5: npm и npx
Часть 6: цикл событий, стек вызовов, таймеры
Часть 7: асинхронное программирование
Часть 8: протоколы HTTP и WebSocket
Часть 9: работа с файловой системой
Часть 10: стандартные модули, потоки, базы данных, NODE_ENV
Полная PDF-версия руководства по Node.js
Модуль Node.js os
Модуль
os
даёт доступ ко многим функциям, которые можно использовать для получения информации об операционной системе и об аппаратном обеспечении компьютера, на котором работает Node.js. Это стандартный модуль, устанавливать его не надо, для работы с ним из кода его достаточно подключить:const os = require('os')
Здесь имеются несколько полезных свойств, которые, в частности, могут пригодиться при работе с файлами.
Так, свойство
os.EOL
позволяет узнать используемый в системе разделитель строк (признак конца строки). В Linux и macOS это \n
, в Windows — \r\n
.Надо отметить, что упоминая тут «Linux и macOS», мы говорим о POSIX-совместимых платформах. Ради краткости изложения менее популярные платформы мы тут не упоминаем.
Свойство
os.constants.signals
даёт сведения о константах, используемых для обработки сигналов процессов наподобие SIGHUP
, SIGKILL
, и так далее. Здесь можно найти подробности о них.Свойство
os.constants.errno
содержит константы, используемые для сообщений об ошибках — наподобие EADDRINUSE
, EOVERFLOW
. Теперь рассмотрим основные методы модуля
os
.▍os.arch()
Этот метод возвращает строку, идентифицирующую архитектуру системы, например —
arm
, x64
, arm64
.▍os.cpus()
Возвращает информацию о процессорах, доступных в системе. Например, эти сведения могут выглядеть так:
[ { model: 'Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz',
speed: 2400,
times:
{ user: 281685380,
nice: 0,
sys: 187986530,
idle: 685833750,
irq: 0 } },
{ model: 'Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz',
speed: 2400,
times:
{ user: 282348700,
nice: 0,
sys: 161800480,
idle: 703509470,
irq: 0 } } ]
▍os.endianness()
Возвращает
BE
или LE
в зависимости от того, какой порядок байтов (Big Engian или Little Endian) был использован для компиляции бинарного файла Node.js.▍os.freemem()
Возвращает количество свободной системной памяти в байтах.
▍os.homedir()
Возвращает путь к домашней директории текущего пользователя. Например —
'/Users/flavio'
.▍os.hostname()
Возвращает имя хоста.
▍os.loadavg()
Возвращает, в виде массива, данные о средних значениях нагрузки, вычисленные операционной системой. Эта информация имеет смысл только в Linux и macOS. Выглядеть она может так:
[ 3.68798828125, 4.00244140625, 11.1181640625 ]
▍os.networkInterfaces()
Возвращает сведения о сетевых интерфейсах, доступных в системе. Например:
{ lo0:
[ { address: '127.0.0.1',
netmask: '255.0.0.0',
family: 'IPv4',
mac: 'fe:82:00:00:00:00',
internal: true },
{ address: '::1',
netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
family: 'IPv6',
mac: 'fe:82:00:00:00:00',
scopeid: 0,
internal: true },
{ address: 'fe80::1',
netmask: 'ffff:ffff:ffff:ffff::',
family: 'IPv6',
mac: 'fe:82:00:00:00:00',
scopeid: 1,
internal: true } ],
en1:
[ { address: 'fe82::9b:8282:d7e6:496e',
netmask: 'ffff:ffff:ffff:ffff::',
family: 'IPv6',
mac: '06:00:00:02:0e:00',
scopeid: 5,
internal: false },
{ address: '192.168.1.38',
netmask: '255.255.255.0',
family: 'IPv4',
mac: '06:00:00:02:0e:00',
internal: false } ],
utun0:
[ { address: 'fe80::2513:72bc:f405:61d0',
netmask: 'ffff:ffff:ffff:ffff::',
family: 'IPv6',
mac: 'fe:80:00:20:00:00',
scopeid: 8,
internal: false } ] }
▍os.platform()
Возвращает сведения о платформе, для которой был скомпилирован Node.js. Вот некоторые из возможных возвращаемых значений:
- darwin
- freebsd
- linux
- openbsd
- win32
▍os.release()
Возвращает строку, идентифицирующую номер релиза операционной системы.
▍os.tmpdir()
Возвращает путь к заданной в системе директории для хранения временных файлов.
▍os.totalmem()
Возвращает общее количество системной памяти в байтах.
▍os.type()
Возвращает сведения, позволяющие идентифицировать операционную систему. Например:
Linux
— Linux.Darwin
— macOS.Windows_NT
— Windows.
▍os.uptime()
Возвращает время работы системы в секундах с последней перезагрузки.
Модуль Node.js events
Модуль
events
предоставляет нам класс EventEmitter
, который предназначен для работы с событиями на платформе Node.js. Мы уже немного говорили об этом модуле в седьмой части этой серии материалов. Вот документация к нему. Здесь рассмотрим API этого модуля. Напомним, что для использования его в коде нужно, как это обычно бывает со стандартными модулями, его подключить. После этого надо создать новый объект EventEmitter
. Выглядит это так:const EventEmitter = require('events')
const door = new EventEmitter()
Объект класса
EventEmitter
пользуется стандартными механизмами, в частности — следующими событиями:newListener
— это событие вызывается при добавлении обработчика событий.removeListener
— вызывается при удалении обработчика.
Рассмотрим наиболее полезные методы объектов класса
EventEmitter
(подобный объект в названиях методов обозначен как emitter
).▍emitter.addListener()
Псевдоним для метода
emitter.on()
.▍emitter.emit()
Генерирует событие. Синхронно вызывает все обработчики события в том порядке, в котором они были зарегистрированы.
▍emitter.eventNames()
Возвращает массив, который содержит зарегистрированные события.
▍emitter.getMaxListeners()
Возвращает максимальное число обработчиков, которые можно добавить к объекту класса
EventEmitter
. По умолчанию это 10. При необходимости этот параметр можно увеличить или уменьшить с использованием метода setMaxListeners()
.▍emitter.listenerCount()
Возвращает количество обработчиков события, имя которого передаётся данному методу в качестве параметра:
door.listenerCount('open')
▍emitter.listeners()
Возвращает массив обработчиков события для соответствующего события, имя которого передано этому методу:
door.listeners('open')
▍emitter.off()
Псевдоним для метода
emitter.removeListener()
, появившийся в Node 10.▍emitter.on()
Регистрируеn коллбэк, который вызывается при генерировании события. Вот как им пользоваться:
door.on('open', () => {
console.log('Door was opened')
})
▍emitter.once()
Регистрирует коллбэк, который вызывается только один раз — при первом возникновении события, для обработки которого зарегистрирован этот коллбэк. Например:
const EventEmitter = require('events')
const ee = new EventEmitter()
ee.once('my-event', () => {
//вызвать этот коллбэк один раз при первом возникновении события
})
▍emitter.prependListener()
При регистрации обработчика с использованием методов
on()
или addListener()
этот обработчик добавляется в конец очереди обработчиков и вызывается для обработки соответствующего события последним. При использовании метода prependListener()
обработчик добавляется в начало очереди, что приводит к тому, что он будет вызываться для обработки события первым.▍emitter.prependOnceListener()
Этот метод похож на предыдущий. А именно, когда обработчик, предназначенный для однократного вызова, регистрируется с помощью метода
once()
, он оказывается последним в очереди обработчиков и последним вызывается. Метод prependOnceListener()
позволяет добавить такой обработчик в начало очереди.▍emitter.removeAllListeners()
Данный метод удаляет все обработчики для заданного события, зарегистрированные в соответствующем объекте. Пользуются им так:
door.removeAllListeners('open')
▍emitter.removeListener()
Удаляет заданный обработчик, который нужно передать данному методу. Для того чтобы сохранить обработчик для последующего удаления соответствующий коллбэк можно назначить переменной. Выглядит это так:
const doSomething = () => {}
door.on('open', doSomething)
door.removeListener('open', doSomething)
▍emitter.setMaxListeners()
Этот метод позволяет задать максимальное количество обработчиков, которые можно добавить к отдельному событию в экземпляре класса
EventEmitter
. По умолчанию, как уже было сказано, можно добавить до 10 обработчиков для конкретного события. Это значение можно изменить. Пользуются данным методом так:door.setMaxListeners(50)
Модуль Node.js http
В восьмой части этой серии материалов мы уже говорили о стандартном модуле Node.js
http
. Он даёт в распоряжение разработчика механизмы, предназначенные для создания HTTP-серверов. Он является основным модулем, применяемым для решения задач обмена данными по сети в Node.js. Подключить его в коде можно так:const http = require('http')
В его состав входят свойства, методы и классы. Поговорим о них.
▍Свойства
http.METHODS
В этом свойстве перечисляются все поддерживаемые методы HTTP:
> require('http').METHODS
[ 'ACL',
'BIND',
'CHECKOUT',
'CONNECT',
'COPY',
'DELETE',
'GET',
'HEAD',
'LINK',
'LOCK',
'M-SEARCH',
'MERGE',
'MKACTIVITY',
'MKCALENDAR',
'MKCOL',
'MOVE',
'NOTIFY',
'OPTIONS',
'PATCH',
'POST',
'PROPFIND',
'PROPPATCH',
'PURGE',
'PUT',
'REBIND',
'REPORT',
'SEARCH',
'SUBSCRIBE',
'TRACE',
'UNBIND',
'UNLINK',
'UNLOCK',
'UNSUBSCRIBE' ]
http.STATUS_CODES
Здесь содержатся коды состояния HTTP и их описания:
> require('http').STATUS_CODES
{ '100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'205': 'Reset Content',
'206': 'Partial Content',
'207': 'Multi-Status',
'208': 'Already Reported',
'226': 'IM Used',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Found',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'307': 'Temporary Redirect',
'308': 'Permanent Redirect',
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Timeout',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Failed',
'413': 'Payload Too Large',
'414': 'URI Too Long',
'415': 'Unsupported Media Type',
'416': 'Range Not Satisfiable',
'417': 'Expectation Failed',
'418': 'I\'m a teapot',
'421': 'Misdirected Request',
'422': 'Unprocessable Entity',
'423': 'Locked',
'424': 'Failed Dependency',
'425': 'Unordered Collection',
'426': 'Upgrade Required',
'428': 'Precondition Required',
'429': 'Too Many Requests',
'431': 'Request Header Fields Too Large',
'451': 'Unavailable For Legal Reasons',
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Timeout',
'505': 'HTTP Version Not Supported',
'506': 'Variant Also Negotiates',
'507': 'Insufficient Storage',
'508': 'Loop Detected',
'509': 'Bandwidth Limit Exceeded',
'510': 'Not Extended',
'511': 'Network Authentication Required' }
http.globalAgent
Данное свойство указывает на глобальный экземпляр класса
http.Agent
. Он используется для управления соединениями. Его можно считать ключевым компонентом HTTP-подсистемы Node.js. Подробнее о классе http.Agent
мы поговорим ниже.▍Методы
http.createServer()
Возвращает новый экземпляр класса
http.Server
. Вот как пользоваться этим методом для создания HTTP-сервера:const server = http.createServer((req, res) => {
//в этом коллбэке будут обрабатываться запросы
})
http.request()
Позволяет выполнить HTTP-запрос к серверу, создавая экземпляр класса
http.ClientRequest
.http.get()
Этот метод похож на
http.request()
, но он автоматически устанавливает метод HTTP в значение GET
и автоматически же вызывает команду вида req.end()
.▍Классы
Модуль HTTP предоставляет 5 классов —
Agent
, ClientRequest
, Server
, ServerResponse
и IncomingMessage
. Рассмотрим их.http.Agent
Глобальный экземпляр класса
http.Agent
, создаваемый Node.js, используется для управления соединениями. Он применяется в качестве значения по умолчанию всеми HTTP-запросами и обеспечивает постановку запросов в очередь и повторное использование сокетов. Кроме того, он поддерживает пул сокетов, что позволяет обеспечить высокую производительность сетевой подсистемы Node.js. При необходимости можно создать собственный объект http.Agent
.http.ClientRequest
Объект класса
http.ClientRequest
, представляющий собой выполняющийся запрос, создаётся при вызове методов http.request()
или http.get()
. При получении ответа на запрос вызывается событие response
, в котором передаётся ответ — экземпляр http.IncomingMessage
. Данные, полученные после выполнения запроса, можно обработать двумя способами:- Можно вызвать метод
response.read()
. - В обработчике события
response
можно настроить прослушиватель для событияdata
, что позволяет работать с потоковыми данными.
http.Server
Экземпляры этого класса используются для создания серверов с применением команды
http.createServer()
. После того, как у нас имеется объект сервера, мы можем воспользоваться его методами:- Метод
listen()
используется для запуска сервера и организации ожидания и обработки входящих запросов. - Метод
close()
останавливает сервер.
http.ServerResponse
Этот объект создаётся классом
http.Server
и передаётся в качестве второго параметра событию request
при его возникновении. Обычно подобным объектам в коде назначают имя res
:const server = http.createServer((req, res) => {
//res - это объект http.ServerResponse
})
В таких обработчиках, после того, как ответ сервера будет готов к отправке клиенту, вызывают метод
end()
, завершающий формирование ответа. Этот метод необходимо вызывать после завершения формирования каждого ответа.Вот методы, которые используются для работы с HTTP-заголовками:
getHeaderNames()
— возвращает список имён установленных заголовков.getHeaders()
— возвращает копию установленных HTTP-заголовков.setHeader('headername', value)
— устанавливает значение для заданного заголовка.getHeader('headername')
— возвращает установленный заголовок.removeHeader('headername')
— удаляет установленный заголовок.hasHeader('headername')
— возвращаетtrue
если в ответе уже есть заголовок, имя которого передано этому методу.headersSent()
— возвращаетtrue
если заголовки уже отправлены клиенту.
После обработки заголовков их можно отправить клиенту, вызвав метод
response.writeHead()
, который, в качестве первого параметра, принимает код состояния. В качестве второго и третьего параметров ему можно передать сообщение, соответствующее коду состояния, и заголовки.Для отправки данных клиенту в теле ответа используют метод
write()
. Он отправляет буферизованные данные в поток HTTP-ответа.Если до этого заголовки ещё не были установлены командой
response.writeHead()
, сначала будут отправлены заголовки с кодом состояния и сообщением, которые заданы в запросе. Задавать их значения можно, устанавливая значения для свойств statusCode
и statusMessage
:response.statusCode = 500
response.statusMessage = 'Internal Server Error'
http.IncomingMessage
Объект класса
http.IncomingMessage
создаётся в ходе работы следующих механизмов:http.Server
— при обработке событияrequest
.http.ClientRequest
— при обработке событияresponse
.
Его можно использовать для работы с данными ответа. А именно:
- Для того чтобы узнать код состояния ответа и соответствующее сообщение используются свойства
statusCode
иstatusMessage
. - Заголовки ответа можно посмотреть, обратившись к свойству
headers
илиrawHearders
(для получения списка необработанных заголовков). - Метод запроса можно узнать, воспользовавшись свойством
method
. - Узнать используемую версию HTTP можно с помощью свойства
httpVersion
. - Для получения URL предназначено свойство
url
. - Свойство
socket
позволяет получить объектnet.Socket
, связанный с соединением.
Данные ответа представлены в виде потока так как объект
http.IncomingMessage
реализует интерфейс Readable Stream
.Работа с потоками в Node.js
Потоки — это одна из фундаментальных концепций, используемых в Node.js-приложениях. Потоки — это инструменты, которые позволяют выполнять чтение и запись файлов, организовывать сетевое взаимодействие систем, и, в целом — эффективно реализовывать операции обмена данными.
Концепция потоков не уникальна для Node.js. Они появились в ОС семейства Unix десятки лет назад. В частности, программы могут взаимодействовать друг с другом, передавая потоки данных с использованием конвейеров (с применением символа конвейера —
|
).Если представить себе, скажем, чтение файла без использования потоков, то, в ходе выполнения соответствующей команды, содержимое файла будет целиком считано в память, после чего с этим содержимым можно будет работать.
Благодаря использованию механизма потоков файлы можно считывать и обрабатывать по частям, что избавляет от необходимости хранить в памяти большие объёмы данных.
Модуль Node.js stream представляет собой основу, на которой построены все API, поддерживающие работу с потоками.
▍О сильных сторонах использования потоков
Потоки, в сравнении с другими способами обработки данных, отличаются следующими преимуществами:
- Эффективное использование памяти. Работа с потоком не предполагает хранения в памяти больших объёмов данных, загружаемых туда заранее, до того, как появится возможность их обработать.
- Экономия времени. Данные, получаемые из потока, можно начать обрабатывать гораздо быстрее, чем в случае, когда для того, чтобы приступить к их обработке, приходится ждать их полной загрузки.
▍Пример работы с потоками
Традиционный пример работы с потоками демонстрирует чтение файла с диска.
Сначала рассмотрим код, в котором потоки не используются. Стандартный модуль Node.js
fs
позволяет прочитать файл, после чего его можно передать по протоколу HTTP в ответ на запрос, полученный HTTP-сервером:const http = require('http')
const fs = require('fs')
const server = http.createServer(function (req, res) {
fs.readFile(__dirname + '/data.txt', (err, data) => {
res.end(data)
})
})
server.listen(3000)
Метод
readFile()
, использованный здесь, позволяет прочесть файл целиком. Когда чтение будет завершено, он вызывает соответствующий коллбэк.Метод
res.end(data)
, вызываемый в коллбэке, отправляет содержимое файла клиенту.Если размер файла велик, то эта операция займёт немало времени. Вот тот же пример переписанный с использованием потоков:
const http = require('http')
const fs = require('fs')
const server = http.createServer((req, res) => {
const stream = fs.createReadStream(__dirname + '/data.txt')
stream.pipe(res)
})
server.listen(3000)
Вместо того, чтобы ждать того момента, когда файл будет полностью прочитан, мы начинаем передавать его данные клиенту сразу после того, как первая порция этих данных будет готова к отправке.
▍Метод pipe()
В предыдущем примере мы использовали конструкцию вида
stream.pipe(res)
, в которой вызывается метод файлового потока pipe()
. Этот метод берёт данные из их источника и отправляет их в место назначения.Его вызывают для потока, представляющего собой источник данных. В данном случае это — файловый поток, который отправляют в HTTP-ответ.
Возвращаемым значением метода
pipe()
является целевой поток. Это очень удобно, так как позволяет объединять в цепочки несколько вызовов метода pipe()
:src.pipe(dest1).pipe(dest2)
Это равносильно такой конструкции:
src.pipe(dest1)
dest1.pipe(dest2)
▍API Node.js, в которых используются потоки
Потоки — полезный механизм, в результате многие модули ядра Node.js предоставляют стандартные возможности по работе с потоками. Перечислим некоторые из них:
process.stdin
— возвращает поток, подключённый кstdin
.process.stdout
— возвращает поток, подключённый кstdout
.process.stderr
— возвращает поток, подключённый кstderr
.fs.createReadStream()
— создаёт читаемый поток для работы с файлом.fs.createWriteStream()
— создаёт записываемый поток для работы с файлом.net.connect()
— инициирует соединение, основанное на потоке.http.request()
— возвращает экземпляр классаhttp.ClientRequest
, предоставляющий доступ к записываемому потоку.zlib.createGzip()
— сжимает данные с использованием алгоритмаgzip
и отправляет их в поток.zlib.createGunzip()
— выполняет декомпрессиюgzip
-потока.zlib.createDeflate()
— сжимает данные с использованием алгоритмаdeflate
и отправляет их в поток.zlib.createInflate()
— выполняет декомпрессиюdeflate
-потока.
▍Разные типы потоков
Существует четыре типа потоков:
- Поток для чтения (
Readable
) — это поток, из которого можно читать данные. Записывать данные в такой поток нельзя. Когда в такой поток поступают данные, они буферизуются до того момента пока потребитель данных не приступит к их чтению. - Поток для записи (
Writable
) — это поток, в который можно отправлять данные. Читать из него данные нельзя. - Дуплексный поток (
Duplex
) — в такой поток можно и отправлять данные и читать их из него. По существу это — комбинация потока для чтения и потока для записи. - Трансформирующий поток (
Transform
) — такие потоки похожи на дуплексные потоки, разница заключается в том, что то, что поступает на вход этих потоков, преобразует то, что из них можно прочитать.
▍Создание потока для чтения
Поток для чтения можно создать и инициализировать, воспользовавшись возможностями модуля
stream
:const Stream = require('stream')
const readableStream = new Stream.Readable()
Теперь в поток можно поместить данные, которые позже сможет прочесть потребитель этих данных:
readableStream.push('hi!')
readableStream.push('ho!')
▍Создание потока для записи
Для того чтобы создать записываемый поток нужно расширить базовый объект
Writable
и реализовать его метод _write()
. Для этого сначала создадим соответствующий поток:const Stream = require('stream')
const writableStream = new Stream.Writable()
Затем реализуем его метод
_write()
:writableStream._write = (chunk, encoding, next) => {
console.log(chunk.toString())
next()
}
Теперь к такому потоку можно подключить поток, предназначенный для чтения:
process.stdin.pipe(writableStream)
▍Получение данных из потока для чтения
Для того чтобы получить данные из потока, предназначенного для чтения, воспользуемся потоком для записи:
const Stream = require('stream')
const readableStream = new Stream.Readable()
const writableStream = new Stream.Writable()
writableStream._write = (chunk, encoding, next) => {
console.log(chunk.toString())
next()
}
readableStream.pipe(writableStream)
readableStream.push('hi!')
readableStream.push('ho!')
readableStream.push(null)
Команда
readableStream.push(null)
сообщает об окончании вывода данных.Работать с потоками для чтения можно и напрямую, обрабатывая событие
readable
:readableStream.on('readable', () => {
console.log(readableStream.read())
})
▍Отправка данных в поток для записи
Для отправки данных в поток для записи используется метод
write()
:writableStream.write('hey!\n')
▍Сообщение потоку для записи о том, что запись данных завершена
Для того чтобы сообщить потоку для записи о том, что запись данных в него завершена, можно воспользоваться его методом
end()
:writableStream.end()
Этот метод принимает несколько необязательных параметров. В частности, ему можно передать последнюю порцию данных, которые надо записать в поток.
Основы работы с MySQL в Node.js
MySQL является одной из самых популярных СУБД в мире. В экосистеме Node.js имеется несколько пакетов, которые позволяют взаимодействовать с MySQL-базами, то есть — сохранять в них данные, получать данные из баз и выполнять другие операции.
Мы будем использовать пакет mysqljs/mysql. Этот проект, который существует уже очень давно, собрал более 12000 звёзд на GitHub. Для того чтобы воспроизвести следующие примеры, вам понадобится MySQL-сервер.
▍Установка пакета
Для установки этого пакета воспользуйтесь такой командой:
npm install mysql
▍Инициализация подключения к базе данных
Сначала подключим пакет в программе:
const mysql = require('mysql')
После этого создадим соединение:
const options = {
user: 'the_mysql_user_name',
password: 'the_mysql_user_password',
database: 'the_mysql_database_name'
}
const connection = mysql.createConnection(options)
Теперь попытаемся подключиться к базе данных:
connection.connect(err => {
if (err) {
console.error('An error occurred while connecting to the DB')
throw err
}
}
▍Параметры соединения
В вышеприведённом примере объект
options
содержал три параметра соединения:const options = {
user: 'the_mysql_user_name',
password: 'the_mysql_user_password',
database: 'the_mysql_database_name'
}
На самом деле этих параметров существует гораздо больше. В том числе — следующие:
host
— имя хоста, на котором расположен MySQL-сервер, по умолчанию —localhost
.port
— номер порта сервера, по умолчанию —3306
.socketPath
— используется для указания сокета Unix вместо хоста и порта.debug
— позволяет работать в режиме отладки, по умолчанию эта возможность отключена.trace
— позволяет выводить сведения о трассировке стека при возникновении ошибок, по умолчанию эта возможность включена.ssl
— используется для настройки SSL-подключения к серверу.
▍Выполнение запроса SELECT
Теперь всё готово к выполнению SQL-запросов к базе данных. Для выполнения запросов используется метод соединения
query
, который принимает запрос и коллбэк. Если операция завершится успешно — коллбэк будет вызван с передачей ему данных, полученных из базы. В случае ошибки в коллбэк попадёт соответствующий объект ошибки. Вот как это выглядит при выполнении запроса на выборку данных:connection.query('SELECT * FROM todos', (error, todos, fields) => {
if (error) {
console.error('An error occurred while executing the query')
throw error
}
console.log(todos)
})
При формировании запроса можно использовать значения, которые будут автоматически встроены в строку запроса:
const id = 223
connection.query('SELECT * FROM todos WHERE id = ?', [id], (error, todos, fields) => {
if (error) {
console.error('An error occurred while executing the query')
throw error
}
console.log(todos)
})
Для передачи в запрос нескольких значений можно, в качестве второго параметра, использовать массив:
const id = 223
const author = 'Flavio'
connection.query('SELECT * FROM todos WHERE id = ? AND author = ?', [id, author], (error, todos, fields) => {
if (error) {
console.error('An error occurred while executing the query')
throw error
}
console.log(todos)
})
▍Выполнение запроса INSERT
Запросы
INSERT
используются для записи данных в базу. Например, запишем в базу данных объект:const todo = {
thing: 'Buy the milk'
author: 'Flavio'
}
connection.query('INSERT INTO todos SET ?', todo, (error, results, fields) => {
if (error) {
console.error('An error occurred while executing the query')
throw error
}
})
Если у таблицы, в которую добавляются данные, есть первичный ключ со свойством
auto_increment
, его значение будет возвращено в виде results.insertId
:const todo = {
thing: 'Buy the milk'
author: 'Flavio'
}
connection.query('INSERT INTO todos SET ?', todo, (error, results, fields) => {
if (error) {
console.error('An error occurred while executing the query')
throw error
}}
const id = results.resultId
console.log(id)
)
▍Закрытие соединения с базой данных
После того как работа с базой данных завершена и пришло время закрыть соединение — воспользуйтесь его методом
end()
:connection.end()
Это приведёт к правильному завершению работы с базой данных.
О разнице между средой разработки и продакшн-средой
Создавая приложения в среде Node.js можно использовать различные конфигурации для окружения разработки и продакшн-окружения.
По умолчанию платформа Node.js работает в окружении разработки. Для того чтобы указать ей на то, что код выполняется в продакшн-среде, можно настроить переменную окружения
NODE_ENV
:NODE_ENV=production
Обычно это делается в командной строке. В Linux, например, это выглядит так:
export NODE_ENV=production
Лучше, однако, поместить подобную команду в конфигурационный файл наподобие
.bash_profile
(при использовании Bash), так как в противном случае такие настройки не сохраняются после перезагрузки системы.Настроить значение переменной окружения можно, воспользовавшись следующей конструкцией при запуске приложения:
NODE_ENV=production node app.js
Эта переменная окружения широко используется во внешних библиотеках для Node.js. Установка
NODE_ENV
в значение production
обычно означает следующее:- До минимума сокращается логирование.
- Используется больше уровней кэширования для оптимизации производительности.
Например, Pug — библиотека для работы с шаблонами, используемая Express, готовится к работе в режиме отладки в том случае, если переменная
NODE_ENV
не установлена в значение production
. Представления Express, в режиме разработки, генерируются при обработке каждого запроса. В продакшн-режиме они кэшируются. Есть и множество других подобных примеров.Express предоставляет конфигурационные хуки для каждого окружения. То, какой именно будет вызван, зависит от значения
NODE_ENV
:app.configure('development', () => {
//...
})
app.configure('production', () => {
//...
})
app.configure('production', 'staging', () => {
//...
})
Например, с их помощью можно использовать различные обработчики событий для разных режимов:
app.configure('development', () => {
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
})
app.configure('production', () => {
app.use(express.errorHandler())
})
▍Итоги
Надеемся, освоив это руководство, вы узнали о платформе Node.js достаточно много для того, чтобы приступить к работе с ней. Полагаем, теперь вы, даже если начали читать первую статью этого цикла, совершенно не разбираясь в Node.js, сможете начать писать что-то своё, с интересном читать чужой код и с толком пользоваться документацией к Node.js.
Уважаемые читатели! Если вы прочли эту серию публикаций, не имея знаний о разработке для Node.js, просим рассказать о том, как вы оцениваете свой уровень теперь.