Возможно Вам доводилось слышать о том что Node.js идеален для создания веб-серверов. В этой статье я объясню, почему оно так и какие архитектурные принципы заложенные в основу серверного JavaScript, делают его таким подходящим для приложений с высокой интенсивностью ввода/вывода.
Параллелизм, однопоточность, многопоточность
Среда Node.js асинхронна по своей природе и эта особенность, позволяет реализовывать приложения с высокой степенью параллелизма, способных обрабатывать множество запросов одновременно. Весь этот параллелизм, однопоточен, а значит не будет возникать проблем с отладкой и синхронизацией, множества исполняемых потоков. Таким образом мы получаем параллелизм, присущий другим языкам (Java, C#), но никак не можем угодить в состояние гонки (race condition), так как вся работа происходит в одном потоке. И при этом, среда Node.js крайне экономна в вопросе оперативной памяти!
Но если вам этого мало и хочется полноценную многопоточку с блэкджеком и параллельными вычислениями, то в Node, как и в клиентском JavaScript, для этого есть веб-воркеры. Они тоже спроектированы, так что Вы не сможете прострелить себе колено, угадив в состояние гонки, круто-же! Веб-воркеры могут пригодиться для вычислений с интенсивным использованием процессора. Такая необходимость на серверах написанных на Node, возникает не часто, так что оставим их обсуждение на потом и сосредоточимся на классической асинхронке в Node.
Асинхронность
Вся среда Node изначально асинхронна. Тем не менее многие функции Node, на всякий случай, имеют синхронные аналоги блокирующие поток. Обычно они имеют постфикс Sync в названиях.
Асинхронность в Node.js появилась до промисов, поэтому она основана на обратных вызовах. Функции обратного вызова в Node, как правило, принимает 2 аргумента:
-
Ошибка (принцип error-frist callback). Если в ходе ошибок нет, то аргумент равен null, а если ошибки есть в нём будет объект Error либо числовой код ошибки. Ошибка ставится первым аргументом, для того чтобы её нельзя было пропустить. Коллбэк функция всегда должна проверять наличие ошибок.
-
Данные.
Для примера напишем простенький асинхронный скрипт читающий файл:
const fileSystemHandler = require('node:fs') // Подключаем модуль для работы с файловой системой fileSystemHandler.readFile('text.txt', 'utf8', (error, data) => { if (error) { // Обязательно в начале функции проверяем наличие ошибок console.error(error) return } console.log(data) })
А вот то же самое, только с синхронной версией функции:
const fileSystemHandler = require('node:fs') // Подключаем модуль для работы с файловой системой const fileData = fileSystemHandler.readFileSync('text.txt', 'utf8') console.log(fileData)
Всё простенько и со вкусом синхронно, как в каком-нибудь PHP.
Жизненный цикл программ на Node.js
Работа любого приложения в среде Node делится на 2 этапа:
-
Исполнение кода. Если в коде нет асинхронных функций, то работа программы на этом завершается;
-
Если же асинхронные функции были, то после выполнения кода стартует второй этап: обработка событий. На данном этапе среда Node, «засыпает» и стартует только для запуска обработчиков событий. Завершается эта стадия, только после обработки всех зарегистрированных событий, но в принципе может длиться бесконечно.
Цикл обработки событий
Второй этап жизненного цикла программы на Node.js, работает по следующему алгоритму:
-
Регистрация обработчика события в операционной системе;
-
Сохранение в памяти функции обработчика, который должен быть вызван на данное событие;
-
Операционная система уведомляет Node, о возникновении ранее зарегистрированного события;
-
Node вызывает функцию-обработчик, повешенную на данное событие. В свою очередь данная функция, может регистрировать новые обработчики событий.
Такой жизненный цикл идеально подходит для серверов, которые большую часть времени проводят в ожидании ввода/вывода.
Промисы в Node.js
ES6 принёс в JavaScript, клёвую фичу промисы. Незаменимая штука для реализации асинхронного кода. Но как было написано выше, Node.js изначально был асинхронным и не мог ждать, когда в стандарте опишут инструментарий, который требуется уже здесь и сейчас. Но когда в стандарте появилась долгожданная асинхронность, разработчики среды Node, не могли проигнорировать столь важные изменения и поэтому в Node 10, был добавлен интерфейс util.promisify, позволяющий переделать любой метод, работающий через функции обратного вызова, на промисы. Переделаем примеры с чтением файла на промисы:
const fileSystemHandler = require('node:fs') // Подключаем модуль для работы с файловой системой const util = require('util') // Подключаем модуль util, который поможет нам преобразовать методы на обратных вызовах, в методы на промисах const promiseFileReader = util.promisify(fileSystemHandler.readFile) // Просто передаём методу promisify, метод который нужно переделать на промисы promiseFileReader('text.txt', 'utf8').then((data) => { // Передаём те-же аргументы, что и раньше, передавались функции обратного вызова, за исключением, первого аргумента с ошибкой. В ответ получаем промис console.log(data) })
Вместо использования интерфейса promisify, пример выше можно упростить, импортировав модуль для чтения файлов, который изначально работает на промисах:
const promiseFileSystemHandler = require('node:fs/promises') promiseFileSystemHandler.readFile('text.txt', { encoding: 'utf8' }).then((data) => { console.log(data) })
Вывод
Node.js благодаря своей дефолтной асинхронности и особенностям её реализации, является во многом уникальным языком, что делает его незаменимым для решения определённого пула задач, а именно создание крайне нетребовательных к ресурсам веб-серверов. В частности популярная в наших суровых краях, не менее суровая CMS Битрикс имеет в своём окружении Push and Pull сервер, сделанный на Node.js. Но как уже говорилось в предыдущих статьях по JS, интерфейсы данного языка часто оказываются избыточными, и серверная версия не стал исключением из данного правила, имея аж по 3 версии практически каждого метода. Это даёт определённую гибкость в разработке, но в то же время может привести к рассогласованности в стилистике кода. Поэтому перед стартом разработки на Node, команде стоит договориться между собой о формате методов которые будут доминировать в проекте. Лично я бы отдал предпочтение версиям на промисах, так как они:
-
Соответствуют стандарту ECMAScript;
-
Лучше передают суть асинхронности;
-
Имеют более глубокий инструментарий, встроенный в современные версии JavaScript (async/await, for/await и т.д.), что даёт большую гибкость.
-
Будут знакомы разработчикам клиентского JavaScript.
Отдавать приоритет синхронным функциям вообще не имеет никого смысла, так как они нивелируют главную фичу Node.js: асинхронность из коробки и уже не проще ли тогда реализовать задачу на PHP. Их стоит использовать, только в конкретных ситуациях, где требуется именно блокирующее поведение кода.
ссылка на оригинал статьи https://habr.com/ru/articles/890190/
Добавить комментарий