Модули и среда исполнения
Что это такое
Модуль — это файл, чьи экспорты использует другой файл, а среда исполнения — всё, что даёт платформа помимо языка: файлы, сетевые сокеты, процессы, таймеры и буферы. Система модулей Node существует в двух стилях, которые сосуществуют — один с явными импортами и статическим разрешением, другой с функцией, которая загружает модуль по имени во время исполнения, — и именно выбор между ними вместе со знанием того, какие части платформы встроены, делает серверную кодовую базу на JavaScript обозримой.
Почему это важно
Со стороны платформы всё решает знание того, какие части встроены, а какие приходят зависимостью: от него зависит, останется ли дерево зависимостей честным. Система модулей при этом решает, что может проанализировать сборщик, что можно выбросить как неиспользуемое и как зависимость загружается при старте. Два стиля по-разному ведут себя с циклическими ссылками и со значениями, которые нужно вычислить до использования, поэтому неосторожное смешивание порождает ошибки, которые всплывают только при старте в продакшене.
Ключевые идеи
Статические модули разрешаются до исполнения, динамические — во время него
Стиль с явными импортами анализируется до запуска программы, поэтому его привязки видны инструментам и поддаются оптимизации. Динамический стиль вычисляет цель в тот момент, когда строка исполняется: условную или ленивую загрузку делает возможной именно он, и он же прячет зависимость от статического анализа. Частый сбой — смешать их: модуль, загруженный одним способом и затребованный другим, может быть инициализирован дважды.
import { readFile } from 'node:fs' -> разрешается до исполнения
const mod = await import(specifier) -> разрешается, когда выполняется эта строка
Стандартная библиотека намеренно мала
Файлы, сеть, процессы, потоки, буферы, криптография и разбор URL встроены. Почти всё остальное — веб-фреймворк, клиент базы данных, библиотека тестирования — приходит зависимостью. Так среда остаётся небольшой; однако из этого следует, что поверхность безопасности проекта — в основном дерево его зависимостей, и об этом стоит помнить, когда пакет добавляют ради одной функции.
Потоки — способ обрабатывать большие данные, не загружая их целиком
Поток отдаёт данные кусками по мере поступления, поэтому файл или тело ответа можно обрабатывать, не держа целиком в памяти. Перекачка читаемого потока в записываемый — стандартная форма, а забыть обработать ошибку на любой из сторон — обычный способ тихо застопорить поток или получить утечку.
читаемый файл -> преобразование -> записываемый сетевой сокет
куски приходят и записываются; ничего не буферизуется целиком
Буферы и кодировки важны на границе
Двоичные данные обрабатываются в буферах фиксированной длины, а для перевода их в текст и обратно нужно выбрать кодировку и быть последовательным. Большинство багов порчи данных и кракозябр на этом уровне — несоответствие кодировки чтения и кодировки записи. Явное указание кодировки на каждой границе их предотвращает.
Конкурентность и управление процессами доступны, а не неявны
Среда исполнения умеет порождать дочерние процессы, использовать рабочие потоки и отдавать окружение и код выхода процесса. Именно так выгружается работа, которая иначе заблокировала бы цикл событий, и так процесс сообщает сбой своему супервизору. Сервер, который никогда не выставляет ненулевой код выхода, неотличим от того, что завершился корректно.
Версия среды — часть контракта
Возможности языка и API стандартной библиотеки приходят с версиями, а поддерживаемые версии проекта следует указывать и проверять. Развёртывание приложения, которое опирается на новую возможность, на старой среде исполнения падает при старте — поэтому версия принадлежит конфигурации проекта, а не памяти коллеги.
Практика
Проследите порядок загрузки модулей
Возьмите проект с несколькими модулями и побочным эффектом при старте и определите порядок их вычисления.
Готово, когда: вы можете назвать модуль, который выполняется первым, и тот, чей побочный эффект появляется последним, и объяснить почему.
Передавайте большой файл потоком вместо чтения целиком
Возьмите эндпоинт, который читает файл целиком в память, и переделайте его так, чтобы файл уходил в ответ потоком, а ошибки обрабатывались с обеих сторон.
Готово, когда: потребление памяти остаётся ровным при отдаче файла крупнее, чем позволила бы куча процесса.
Частые ошибки
- Смешивание двух стилей модулей для одной зависимости — она может инициализироваться дважды или не загрузиться; исправить: выберите один стиль и держитесь его.
- Добавление зависимости для того, что встроено — поверхность безопасности растёт без пользы; исправить: сначала проверьте стандартную библиотеку.
- Игнорирование ошибок потока — сбойный поток застопорится или потечёт вместо громкого падения; исправить: обрабатывайте ошибки и на читаемой, и на записываемой стороне.
- Предположение о кодировке — двоичные данные становятся испорченным текстом; исправить: указывайте кодировку явно на каждой границе.
- Код выхода никогда не выставляется — упавший процесс выглядит для супервизора успешным; исправить: завершайтесь с ненулевым кодом при сбое старта или работы.
Что читать дальше
- Цикл событий (05-backend-data/02-nodejs/01-the-event-loop) — модель, внутри которой используются эти модули и API платформы.
- Node.js (05-backend-data/02-nodejs) — среда исполнения, о которой этот дочерний урок.
- Express.js (05-backend-data/08-expressjs) — слой фреймворка, который большинство проектов кладёт поверх встроенного HTTP-сервера.