Перейти к основному содержимому

Точка входа в Node.js — require.main и import.meta

Разработчику

Точка входа — это главный, стартовый файл приложения или скрипта, с которого начинается выполнение всей программы, и который служит корневым узлом в графе зависимостей, инициируя загрузку всех остальных необходимых модулей, конфигураций и сервисов. В контексте Node.js точкой входа обычно является файл, указанный в поле "main" файла package.json, или файл с именем index.js, index.mjs или index.cjs в корне проекта, и именно этот файл передается интерпретатору node при запуске команды, например, node app.js. Точка входа несет ответственность за первоначальную настройку окружения, создание экземпляров ключевых компонентов, запуск сервера, подключение к базам данных и определение глобальных обработчиков ошибок, а также за импорт всех необходимых модулей, без которых приложение не сможет функционировать. В сложных проектах точка входа часто сводится к минимальному коду, который лишь делегирует запуск более специализированным файлам или фреймворкам, таким как Express или NestJS, позволяя сохранить чистоту архитектуры и упростить тестирование. Выбор правильной точки входа критически важен для организации структуры проекта, поскольку она задает контекст выполнения, определяет относительные пути для импортов и влияет на то, как сборщики или упаковщики, такие как Webpack или Rollup, обрабатывают код при подготовке к production. Кроме того, точка входа может различаться для разных сред, например, отдельный файл для разработки с горячей перезагрузкой и отдельный для продакшена с оптимизированными настройками, что позволяет гибко управлять поведением приложения в зависимости от окружения.

В Python есть if __name__ == "__main__":. В Node.js тот же вопрос звучит так: файл запустили командой node file.js или подключили через require / import?

См. также: Первая программа на Node.js · модули Node · ESM в браузере · Python — if name == "main" · карта Node.js.


Запуск файла и подключение как модуль

Запуск файла — это процесс передачи интерпретатору Node.js конкретного JavaScript-файла для немедленного выполнения, в результате чего код этого файла читается, компилируется движком V8 и исполняется строка за строкой в порядке их следования, при этом все синхронные операции выполняются последовательно, а асинхронные — планируются через event loop. Когда разработчик вводит команду node filename.js в терминале, операционная система запускает процесс Node.js, загружает указанный файл, оборачивает его в специальную функцию-обертку, которая предоставляет доступ к объектам require, module, exports, __filename и __dirname, и затем выполняет эту функцию, создавая уникальную область видимости для данного модуля. Запуск файла может быть как основным, когда файл является точкой входа всего приложения, так и второстепенным, когда разработчик запускает отдельный скрипт для выполнения утилитарных задач, например, миграции базы данных, генерации документации или запуска тестов. В процессе запуска файла Node.js также обрабатывает аргументы командной строки через объект process.argv, что позволяет передавать в скрипт дополнительные параметры и флаги, меняющие его поведение без изменения исходного кода. Важно понимать, что запуск файла в Node.js всегда однопоточен с точки зрения выполнения пользовательского JavaScript-кода, но тяжелые операции ввода-вывода делегируются libuv и выполняются асинхронно, не блокируя основной поток выполнения. Также стоит отметить, что при запуске файла Node.js инициализирует глобальные объекты, настраивает цикл событий и после выполнения всего синхронного кода переходит в режим ожидания для обработки асинхронных колбэков, таймеров и сетевых запросов, завершая процесс только тогда, когда не остается ни одной активной задачи.

Подключение как модуль — это процесс загрузки одного JavaScript-файла внутрь другого с целью использования его экспортированных функций, объектов, классов или переменных, причем этот файл не выполняется как самостоятельное приложение, а становится частью более крупной системы, дополняя её функциональность. В экосистеме Node.js подключение как модуль означает, что код из подключаемого файла выполняется в контексте вызывающего модуля, но при этом сохраняет свою собственную область видимости, и единственное, что становится доступным снаружи, — это то, что было явно объявлено через экспорт, будь то через module.exports или exports в CommonJS, либо через ключевое слово export в ESM. Когда один файл подключает другой как модуль, Node.js загружает этот файл, кеширует его результат после первого выполнения, чтобы при повторных подключениях не выполнять код заново, и возвращает ссылку на экспортированный объект, которую можно использовать в вызывающем коде. Это принципиально отличает подключение как модуль от запуска файла: в первом случае файл становится библиотекой или зависимостью, во втором — он становится главным исполняемым процессом, управляющим жизненным циклом приложения. Подключение как модуль лежит в основе архитектурного паттерна композиции, позволяя разбивать огромные системы на множество маленьких, переиспользуемых кусочков, каждый из которых отвечает за свою узкую область ответственности, что значительно упрощает поддержку и масштабирование кодовой базы. Кроме того, механизм подключения как модуля обеспечивает инкапсуляцию, скрывая внутренние детали реализации и предоставляя наружу только четко определенный контракт интерфейса, что защищает от случайного изменения состояния извне и способствует слабой связанности компонентов.

Файл calculator.js экспортирует функции и внизу демонстрирует их:

function add(a, b) {
return a + b;
}

console.log(add(5, 3));
module.exports = { add };

Команда node -e "require('./calculator')" выведет 8, хотя нужна была только функция add. Демо-код стоит выполнять только при прямом запуске.


CommonJS — require.main === module

Require — это синхронная функция, встроенная в систему CommonJS в Node.js, которая используется для импорта модулей, загружая их содержимое, выполняя их код в момент вызова и возвращая объект экспорта, который затем может быть присвоен переменной и использован в текущем файле. Функция require принимает строковый аргумент, который может быть именем встроенного модуля (например, 'fs', 'http', 'path'), относительным или абсолютным путем к пользовательскому файлу (например, './utils', '../config'), либо именем пакета из директории node_modules (например, 'express', 'lodash'), и Node.js выполняет сложный алгоритм разрешения пути, начиная с текущей директории и поднимаясь вверх по иерархии файловой системы в поисках запрашиваемого модуля. При первом вызове require для конкретного модуля Node.js загружает файл, оборачивает его в функцию-обертку, выполняет и кеширует результат в объекте require.cache, поэтому все последующие вызовы require для того же модуля возвращают уже закешированный объект экспорта, что гарантирует, что код модуля выполняется только один раз за время работы приложения. Важной особенностью require является её синхронность, что означает, что выполнение программы приостанавливается до тех пор, пока модуль не будет полностью загружен и выполнен, что делает эту функцию отличным выбором для серверной среды, где файлы читаются с локального диска с высокой скоростью, но абсолютно неприемлемо для браузеров или сетевых сред. Кроме того, require поддерживает динамическую загрузку, позволяя импортировать модули условно внутри функций, циклов или блоков if, что дает невероятную гибкость при создании плагинов, реализаций фич-флагов или ленивой инициализации тяжелых зависимостей. Функция require также умеет автоматически обрабатывать расширения файлов, подставляя .js, .json и .node, если расширение не указано явно, и предоставляет доступ к специальным объектам __dirname и __filename, которые содержат информацию о расположении текущего файла.

В модулях CommonJS (require, module.exports) Node задаёт:

ОбъектСмысл
moduleТекущий модуль
require.mainМодуль, с которого начался процесс
require.main === moduletrue, если этот файл — точка входа
// calculator.cjs
function add(a, b) {
return a + b;
}

module.exports = { add };

if (require.main === module) {
console.log(add(5, 3));
}
СпособПоведение
node calculator.cjsПоказывает демо
require('./calculator.cjs')Только экспорт, без console.log

Часто логику выносят в main() и вызывают её внутри проверки — как в Python.


ESM — import.meta.url и process.argv[1]

Import — это ключевое слово и синтаксическая конструкция, введенная в спецификации ECMAScript 2015, представляющая собой официальный стандарт импорта модулей в современном JavaScript, которая работает асинхронно, статически анализируется на этапе компиляции и позволяет загружать зависимости строго декларативным способом в начале файла. В отличие от синхронной функции require, import является частью языка, а не функцией среды выполнения, и он может использоваться в двух основных формах: статический импорт с фиксированным синтаксисом import ... from 'module', который анализируется парсером до выполнения кода и не может быть вызван динамически, и динамический импорт через функцию import(), возвращающую промис, которая позволяет загружать модули асинхронно и условно в любой точке программы. Статический импорт обладает неоспоримыми преимуществами, такими как возможность проведения tree-shaking, то есть удаления неиспользуемых экспортов из итогового бандла, а также обеспечения более быстрой загрузки благодаря предварительному анализу графа зависимостей до начала исполнения, что особенно важно для браузерных приложений. В Node.js поддержка import была добавлена значительно позже, начиная с версии 12, и требует либо использования расширения .mjs для файлов, либо настройки "type": "module" в package.json, причем важно понимать, что import и require не могут быть смешаны в одном файле без специальных обходных путей, поскольку они относятся к разным системам модульности с различными моделями выполнения и разрешения путей. Импорт поддерживает множество вариаций синтаксиса, включая импорт всего пространства имен объекта (import * as Module), импорт конкретных именованных экспортов (import { named }), импорт экспорта по умолчанию (import anyName from), а также их комбинации, что позволяет разработчику гибко управлять тем, какие именно сущности попадают в текущую область видимости. Переход от require к import является глобальным трендом в экосистеме JavaScript, поскольку import предлагает более строгую, предсказуемую и оптимизируемую модель загрузки, соответствующую современным стандартам веб-разработки и поддерживаемую как браузерами, так и серверными средами, хотя этот переход и сопровождается определенными сложностями из-за различий в поведении, особенно в отношении циклических зависимостей и разрешения путей.

В ES-модулях ("type": "module" в package.json или расширение .mjs) переменной require.main нет. Сравнивают путь запущенного файла с URL текущего модуля:

// calculator.mjs

import { fileURLToPath } from 'node:url';

export function add(a, b) {
return a + b;
}

const isMain =
process.argv[1] === fileURLToPath(import.meta.url);

if (isMain) {
console.log(add(5, 3));
}
  • import.meta.url — URL текущего файла; по нему в ESM понимают, какой модуль выполняется.
  • process.argv[1] — путь, который передали в node ….
Браузер и bundler

В браузере скрипт подключают через <script type="module">; отдельной точки входа в стиле CLI там нет. Паттерн с require.main и проверкой import.meta.url нужен в Node.js, утилитах и тестах.


Когда писать проверку

СитуацияНужна ли
Одноразовый скрипт в npm runПо желанию
Модуль с функциями + локальное демоДа
Express-приложение в app.js, импортируемое в тестахДаlisten только при прямом запуске файла
Только export без кода верхнего уровняЧасто достаточно без обёртки

Сравнение с Python

PythonNode.js (CJS)Node.js (ESM)
Проверка__name__ == "__main__"require.main === moduleprocess.argv[1] и import.meta.url
Импорт выполняет код верхнего уровняДаДаДа

Общий обзор по языкам — в Python — if name == "main".


Связанные материалы

ТемаСтатья
npm, package.jsonПервая программа на Node.js — Первая программа
CommonJS и ESM подробнееNode.js - серверный JavaScript — Node.js
import.meta в браузере40 — ESM
Карта блока Nodeintro