Углубленный курс Node.js для новичков

Этот курс поможет вам освоить бэкенд-разработку с нуля, начиная с базовых концепций JavaScript и заканчивая созданием масштабируемых серверных приложений. Вы изучите асинхронное программирование, работу с базами данных и современные фреймворки, опираясь на актуальные требования индустрии [netology.ru](https://netology.ru/programs/nodejs) и [otus.ru](https://otus.ru/lessons/node/).

1. Введение в Node.js и основы JavaScript: Event Loop и асинхронность

Введение в Node.js и основы JavaScript: Event Loop и асинхронность

Представьте себе ресторан, в котором работает всего один официант. Если он примет заказ у первого столика и будет стоять на кухне, ожидая, пока повар приготовит блюдо, остальные посетители просто уйдут. Именно так работают традиционные многопоточные серверы, выделяя отдельный поток (официанта) на каждый запрос. Но что, если официант передаст заказ на кухню и сразу пойдет обслуживать следующий столик, а когда блюдо будет готово, просто заберет его? Это и есть фундаментальный принцип работы Node.js.

Среда выполнения Node.js построена на движке V8 и позволяет выполнять код JavaScript вне браузера. Главная особенность этой среды — однопоточность в сочетании с неблокирующим вводом-выводом.

Синхронность и асинхронность: математика времени выполнения

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

Рассмотрим затраты времени на выполнение двух независимых операций: запрос к базе данных занимает 500 миллисекунд, а чтение файла с диска — 300 миллисекунд.

В синхронной модели общее время выполнения вычисляется путем сложения:

Где — общее время синхронного выполнения, — время первой операции (500 мс), — время второй операции (300 мс). В итоге сервер потратит 800 миллисекунд.

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

Где — общее время асинхронного выполнения, а функция выбирает наибольшее значение из и . В нашем случае сервер справится за 500 миллисекунд, сэкономив 37,5% времени.

Пропускная способность сервера напрямую зависит от времени обработки запроса. Ее можно выразить формулой:

Где — количество запросов в секунду, — общее количество запросов, — время в секундах. Если сервер обрабатывает 1000 запросов синхронно по 0,8 секунды каждый, общее время составит 800 секунд. Пропускная способность будет равна 1,25 запроса в секунду. В асинхронной модели, где 1000 запросов инициируются почти мгновенно и каждый ждет ответа 0,5 секунды, общее время составит около 1,5 секунд (с учетом накладных расходов). Пропускная способность взлетит до 666 запросов в секунду.

| Характеристика | Синхронный подход | Асинхронный подход (Node.js) | | --- | --- | --- | | Потребление памяти | Высокое (нужен новый поток на каждый запрос) | Низкое (один поток обрабатывает всё) | | Скорость при I/O операциях | Низкая (поток простаивает в ожидании) | Высокая (поток переключается на другие задачи) | | Сложность кода | Простая, линейная логика | Требует понимания коллбэков и промисов |

Анатомия цикла событий

Сердцем асинхронности в Node.js является Event Loop (цикл событий). Это механизм, который позволяет однопоточному JavaScript выполнять неблокирующие операции, делегируя тяжелую работу операционной системе.

> Event Loop — это то, что позволяет Node.js выполнять асинхронные операции ввода-вывода, несмотря на то, что JavaScript является однопоточным, путем выгрузки операций в ядро системы, когда это возможно. > > Официальная документация Node.js

Чтобы понять, как код не превращается в хаос, нужно разобрать четыре основных компонента архитектуры:

  • Call Stack (стек вызовов) — структура данных, которая записывает, где именно мы находимся в программе. Если мы вызываем функцию, она помещается на вершину стека. Когда функция завершается, она удаляется оттуда.
  • Node APIs — интерфейсы, предоставляемые средой (например, таймеры, работа с файловой системой, сетевые запросы). Они выполняются вне основного потока JavaScript.
  • Task Queue (очередь макрозадач) — место, куда попадают функции обратного вызова (коллбэки) после завершения асинхронных операций из Node APIs.
  • Microtask Queue (очередь микрозадач) — специальная очередь с повышенным приоритетом, предназначенная в первую очередь для обработчиков промисов (Promises).
  • Фазы Event Loop в Node.js: заглядываем под капот

    В отличие от браузера, где Event Loop относительно прост, в Node.js он реализован на базе библиотеки libuv и состоит из нескольких строго определенных фаз. Каждая фаза имеет свою собственную очередь коллбэков.

  • Timers (Таймеры): на этой фазе выполняются коллбэки, запланированные функциями setTimeout и setInterval.
  • Pending Callbacks (Ожидающие коллбэки): здесь выполняются коллбэки системных операций, например, ошибки TCP-сокетов.
  • Idle, Prepare: используются только для внутренних нужд движка.
  • Poll (Опрос): самая важная фаза. Здесь Node.js получает новые события ввода-вывода (чтение файлов, сетевые запросы). Если очередь пуста, движок может заснуть на этой фазе, ожидая новых событий.
  • Check (Проверка): фаза, специально созданная для функции setImmediate.
  • Close Callbacks (События закрытия): здесь обрабатываются события закрытия соединений, например socket.on('close', ...).
  • Между каждой из этих фаз Node.js останавливается, чтобы проверить очередь микрозадач. Если там есть задачи, они выполняются до полного опустошения очереди, и только потом Event Loop переходит к следующей фазе.

    Рассмотрим пример с числами, чтобы понять разницу между setTimeout и setImmediate. Допустим, мы запускаем обе функции в пустом файле:

    Порядок вывода в данном случае непредсказуем. Задержка в 0 миллисекунд на самом деле округляется до 1 миллисекунды. Если Event Loop запустится быстрее, чем пройдет 1 миллисекунда, фаза Timers окажется пустой, и движок перейдет к фазе Check, выведя Immediate первым.

    Однако, если мы поместим этот же код внутрь коллбэка чтения файла (фаза Poll):

    Здесь порядок гарантирован. После завершения чтения файла мы находимся на фазе Poll. Следующая фаза по кругу — Check. Поэтому setImmediate выполнится первым, а setTimeout останется ждать следующего витка цикла событий, чтобы выполниться на фазе Timers.

    Макрозадачи и микрозадачи: иерархия приоритетов

    Разделение на макро- и микрозадачи — ключевой момент для понимания асинхронности.

    К макрозадачам относятся:

  • setTimeout и setInterval
  • Операции ввода-вывода (чтение файлов, сетевые запросы)
  • Обработчики событий
  • К микрозадачам относятся:

  • Обработчики промисов (.then, .catch, .finally)
  • Функции, вызванные через queueMicrotask
  • process.nextTick (специфично для Node.js, имеет наивысший приоритет даже среди микрозадач)
  • Рассмотрим классический пример:

    Проследим за выполнением этого кода с точки зрения движка:

  • Первая строка помещается в Call Stack, выполняется и выводит Шаг 1.
  • Движок встречает setTimeout. Он передает таймер в Node APIs. Поскольку задержка равна 0 миллисекунд, коллбэк почти мгновенно попадает в Task Queue.
  • Движок встречает Promise.resolve(). Обработчик .then немедленно помещается в Microtask Queue.
  • Последняя строка помещается в Call Stack, выполняется и выводит Шаг 4.
  • Синхронный код завершен, Call Stack пуст. Event Loop начинает проверку очередей.
  • Сначала проверяется Microtask Queue. Там лежит коллбэк промиса. Он отправляется в Call Stack и выводит Шаг 3.
  • Снова проверяется Microtask Queue. Она пуста.
  • Event Loop переходит к Task Queue, берет оттуда коллбэк таймера, отправляет в Call Stack, и мы видим Шаг 2.
  • Несмотря на то, что таймер был установлен с нулевой задержкой и объявлен раньше промиса, микрозадача всегда имеет приоритет над макрозадачей. Если микрозадачи будут бесконечно добавлять новые микрозадачи в очередь, Event Loop никогда не доберется до макрозадач, что приведет к зависанию программы.

    Итоги

  • Node.js использует однопоточную неблокирующую модель ввода-вывода, что позволяет эффективно обрабатывать тысячи одновременных соединений без выделения памяти под новые потоки.
  • Асинхронный подход экономит время, выполняя долгие операции параллельно на уровне операционной системы, а не в основном потоке JavaScript.
  • Event Loop в Node.js состоит из нескольких фаз (Timers, Poll, Check и др.), каждая из которых обрабатывает свой тип коллбэков.
  • Микрозадачи (промисы, process.nextTick) всегда имеют приоритет над макрозадачами (таймеры, I/O) и выполняются сразу после очистки стека вызовов или между фазами Event Loop.
  • 2. Создание веб-сервера и маршрутизация с помощью Express.js

    Создание веб-сервера и маршрутизация с помощью Express.js

    В прошлой теме мы разобрали, как однопоточный Event Loop позволяет Node.js жонглировать тысячами асинхронных операций ввода-вывода, не блокируя основной поток. Но как применить эту мощь на практике? Если вы попробуете написать полноценный веб-сайт, используя только встроенный модуль http, вы быстро утонете в десятках проверок URL-адресов и ручном разборе заголовков.

    Представьте, что вам нужно построить дом. Вы можете сами рубить деревья, делать доски и мешать цемент (это базовый модуль http), а можете заказать готовый каркас, где уже проложены трубы и проводка, оставив себе только планировку комнат. Таким "каркасом" в мире Node.js выступает Express.js.

    > Express — это минималистичный и гибкий веб-фреймворк для Node.js, предоставляющий обширный набор функций для мобильных и веб-приложений. > > Официальная документация Express

    Анатомия веб-сервера: от установки до первого запуска

    Сколько времени нужно, чтобы запустить сервер, способный принимать реальные запросы? С Express.js счет идет на секунды.

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

    Теперь создадим файл index.js и напишем минимальный сервер:

    ``javascript const express = require('express'); const app = express(); const port = 3000;

    app.get('/', (request, response) => { response.send('Привет, мир!'); });

    app.listen(port, () => { console.log(Сервер запущен на порту {user}, пост N = 50BSN{currentTime}] Получен запрос к limit > 100$), сервер отказывает в выполнении операции, возвращая статус 400 (Bad Request) и сообщение об ошибке в формате JSON.

    Итоги

  • Express.js — это минималистичный фреймворк, который берет на себя рутинную работу по обработке HTTP-запросов, позволяя разработчику сфокусироваться на бизнес-логике.
  • Маршрутизация в Express строится на комбинации HTTP-методов (GET, POST и др.) и путей (URL), направляя каждый запрос в соответствующую функцию-обработчик.
  • Динамические маршруты с использованием заполнителей (например, :id) позволяют создавать гибкие URL-адреса для работы с большими объемами однотипных данных.
  • Функции Middleware работают как конвейер, позволяя перехватывать, изменять и логировать запросы до того, как они достигнут финального обработчика.
  • Объекты req и res предоставляют удобный интерфейс для извлечения данных от клиента и формирования правильных ответов, включая JSON и HTTP-статусы.