Одна из концепций, которые нужно понять в Node.js, — это использование require
, module.exports
и exports
. Среда выполнения Node расширяет Javascript файловым вводом-выводом и возможностью экспортировать файлы и использовать их функциональные возможности в других файлах, благодаря чему достигается модульность.
Начиная с ES6, ядро Javascript выполняет то же самое, но с немного другой реализацией и ключевыми словами import
и export
. В будущем все может измениться, но пока Node использует собственную реализацию.
Внутренняя работа
Начнем с простого примера: модуль с функцией вычисления длины окружности и еще один, использующий эту функцию через require
. Запустите пример с node index
, чтобы увидеть вывод консоли.
Что здесь происходит, шаг за шагом?
- Все файлы, которые выполняются nodejs, сначала получают объект
module
, который всегда будет вам доступен. Он содержит информацию дляid
иfilename
. - Свойство
exports
этого объекта инициируется ссылкой на пустой объект{}
- Будучи простым объектом javascript, вы можете назначать его свойства, как мы это делаем в первой строке:
module.exports.prop = ...
. - Функция
require()
используется для построения иерархии междуparent
иchildren
, которая также регистрируется в объектеmodule
. - Функция
require('./requiredfile')
возвращает ссылку на тот же объект, что и свойствоmodule.exports
требуемого файла.
Вы можете думать, что require
наивно определяется следующим образом:
const require = path => {
// ...
return module.exports;
};
Мы можем провести небольшой эксперимент, чтобы продемонстрировать, что require()
и module.exports
на самом деле указывают на один и тот же объект.
Как и прежде, мы создаем ссылку с именем circle
в нашем индексном файле, требуя модуль Circle. На этот раз мы добавляем функцию area
к объекту circle
и снова требуем модуль Circle. Оказывается, module.exports
теперь также содержит новое добавленное свойство (поскольку оба относятся к одному и тому же объекту).
В сторону: как узел создает модуль?
Вы можете отлаживать свое приложение, чтобы просмотреть исходный код узла, который отвечает за создание модуля. Он находится в функции loader.js
, в частности, в функции Module.wrap
. Вот что происходит:
(function (exports, require, module, __filename, __dirname) { // YOUR CODE GOES HERE });
Он в основном читает содержимое файла, помещает его в функцию-конструктор и выполняет эту функцию.
экспорт против module.exports
Node предоставляет ярлык exports
. Это ссылка на тот же объект, на который указывает module.exports
и результат require()
. Ничего волшебного. Вы можете думать об этом как о явном:
let exports = module.exports;
Тем не менее, его следует использовать с осторожностью, потому что в javascript принципиально отличается присвоение свойств объекту или присвоение нового значения переменной. Во втором случае в памяти будет создано новое значение, и переменная будет содержать ссылку на это значение.
Короче говоря, когда вы делаете exports = someValue
, он больше не будет использовать общую ссылку с module.exports
. Теперь, когда require()
создает ссылку, она будет относиться к тому же объекту, что и module.exports
, а не к значению, на которое ссылается exports
.
Возможно, лучше всего это показано в другом примере кода:
Эмпирическое правило состоит в том, чтобы никогда не присваивать новое значение exports
(но присваивание exports.someProperty
вполне нормально).