Модули и зависимости проекта
Файловое чтение DeskQueue работает, но server.js одновременно знает путь к данным, разбирает JSON и считает заявки. Выделим функцию загрузки в собственный модуль. Точка входа останется владельцем порядка действий: сначала получить данные, затем вычислить и показать результат. Такое разделение подготовит замену источника без переписывания каждой части приложения.
В архиве stages/03 содержит server.js, ticket-source.js, data/tickets.json и package.json. Работаем с ES modules Node.js 24. В этой ступени нет сторонних библиотек; первый внешний пакет появится при переходе к Express. Различайте собственный модуль, встроенный модуль Node.js и пакет из реестра: их происхождение и способ выбора версии отличаются.
Явный экспорт функции
Полный файл ticket-source.js получает данные и проверяет верхнюю форму. Имя функции описывает действие. Внутренний адрес файла не требуется каждому вызывающему модулю, поэтому его не экспортируем.
import { readFile } from 'node:fs/promises';
const source = new URL('./data/tickets.json', import.meta.url);
export async function loadTickets() {
const text = await readFile(source, 'utf8');
const tickets = JSON.parse(text);
if (!Array.isArray(tickets)) throw new Error('Ticket list must be an array');
return tickets;
}
Полная новая точка входа обращается к функции через относительный импорт. Для ES modules сохраняем расширение .js. Это уменьшает неоднозначность выбора файла и отличается от привычных сокращений некоторых CommonJS-примеров.
import { loadTickets } from './ticket-source.js';
try {
const tickets = await loadTickets();
const open = tickets.filter((ticket) => ticket.status === 'open');
console.log(JSON.stringify({ service: 'DeskQueue', open: open.length }));
} catch (error) {
console.error(`Cannot load tickets: ${error.message}`);
process.exitCode = 1;
}
Ожидаемый вывод снова не изменился. Именно это и требуется от первого разделения: программа приобрела внутреннюю границу, сохранив наблюдаемое поведение. Если loadTickets позже начнёт получать исходные данные другим способом, вызывающий код всё ещё получит массив либо ошибку. Однако имя и тип результата становятся договором. Изменить массив на объект с полем items без исправления вызывающего кода уже нельзя.
Встроенный модуль указан с префиксом node:. Читатель видит, что это часть среды, а не локальный файл и не скачанный пакет с похожим именем. Для локального модуля нужен путь ./ticket-source.js. Для внешней библиотеки позднее используем имя express. Такие различия помогают объяснить ошибку поиска модуля: разные источники требуют разных условий доступности.
package.json описывает проект
package.json является данными о пакете и его командах, а не выполняемым JavaScript-файлом. В начальном состоянии содержимое небольшое. Поле private защищает от случайного опубликования учебного пакета в реестр через стандартный процесс npm.
{
"name": "deskqueue-lab",
"version": "0.1.0",
"private": true,
"type": "module",
"engines": { "node": ">=24 <25" },
"scripts": { "start": "node server.js" }
}
type: module устанавливает трактовку .js в границах пакета. Если удалить это поле и полагаться на случайное определение синтаксиса, поведение станет менее очевидным. Явная настройка лучше объясняет выбор. engines описывает предназначенную ветку среды; это не обещание, что любой способ запуска автоматически запретит другую версию. Читатель отдельно проверяет среду своего стенда.
Команда start связывает понятное действие проекта с конкретной точкой входа. npm start не превращает программу в сервис операционной системы и не держит её постоянно работающей после выхода из терминала. В нашем состоянии программа завершается после вывода. Позднее слушающий сервер изменит время жизни процесса, а сопровождение сервиса будет отдельной задачей.
Описание допустимых полей находится в официальном руководстве npm. Не добавляйте зависимость только потому, что похожая функция существовала в старом примере. Например, для случайного UUID современный Node.js предоставляет встроенную функцию. Внешний пакет нужен, когда он решает конкретную задачу и его договор подходит проекту.
Диапазон версий и фактическое дерево
Когда добавим Express, поле dependencies опишет разрешённый диапазон. Диапазон и реально установленная версия не одно и то же. Без lockfile два разрешения зависимостей в разное время способны выбрать разные версии внутри диапазона. Для воспроизводимого проекта обычно сохраняют настоящий результат разрешения, а затем используют процесс установки, который ему соответствует.
В этой редакционной работе установка запрещена пользователем, поэтому архив не содержит выдуманного package-lock.json. Позднейшие состояния описывают совместимые семейства зависимостей, но не утверждают, что дерево пакетов уже разрешено и проверено. Читатель, выполняющий учебный проект на своём стенде, получает реальные пакеты и сохраняет настоящий lockfile после установки. Создать файл с похожими числами вручную означало бы скрыть отсутствие проверки.
Также не следует копировать весь node_modules между состояниями курса. Это устанавливаемый результат, зависящий от платформы и разрешённых версий, а не объяснение проекта. В архиве сохраняются собственные файлы, исходные данные и инструкции. Каждая ступень показывает, какие обязанности добавились, вместо распространения неизвестного дерева стороннего кода.
У модуля есть время инициализации. Код верхнего уровня выполняется при загрузке, а экспортированная функция — при вызове. Поэтому не стоит незаметно запускать сервер или подключать базу в модуле, который лишь предоставляет общие функции. Явная точка входа делает владение ресурсами заметным. В следующей ступени это станет особенно важно для событий: факт изменения заявки и реакция журнала получат разные обязанности.
Теперь вы можете проследить три уровня: точка входа задаёт последовательность, модуль загрузки возвращает данные, package.json описывает среду и действие запуска. Сохранены те же заявки и тот же ожидаемый результат, но программа уже готова к постепенному расширению.
Экспорт следует выбирать по необходимому результату. Не нужно отдавать вызывающему модулю сам путь источника, открытый файловый дескриптор и внутренние переменные только потому, что это возможно. Чем больше деталей стало общедоступным договором, тем сложнее заменить реализацию. Наш потребитель просит заявки и получает массив; он не открывает исходный файл самостоятельно. Позднее контроллер попросит одну заявку и не будет знать текст SQL. Эта последовательность развивает одну и ту же границу, а не набор несвязанных примеров импорта.
Разделение также помогает заметить неправильное направление зависимости. Если модуль загрузки импортирует точку входа ради переменной, программа начинает зависеть от порядка инициализации двух файлов. Ошибка тогда выглядит как отсутствие значения, хотя проблема архитектурная. Передавайте необходимое обычным аргументом либо выделите общий договор в независимый модуль. Не создавайте глобальный объект со всеми частями приложения ради удобного доступа из любого места: он скрывает время жизни ресурсов. Пока файлов мало, их связи легко проследить глазами; это подходящий момент закрепить правило до появления HTTP, базы и пользователей.
Команды проекта также являются интерфейсом для читателя. Если в одном состоянии точка входа называется server.js, а в другом её переименовали без объяснения, обычное действие запуска перестанет работать. Поэтому сохраняем имя до последнего шага, где отдельно вводим фабрику приложения. Версия пакета относится к нашему проекту, а не выражает версию Node.js. Эти два числа отвечают на разные вопросы и не должны автоматически копироваться друг в друга.
Полные файлы учебного проекта. Примеры при подготовке текста не запускались; ответы и вывод в уроке описывают ожидаемый результат читателя.