Node.js использует 2 встроенных модуля для управления зависимостями модулей :
require
служит для подключения модулей.module
служит для организации модулей.Подключение модулей в Node делается примерно так :
const config = require('/path/to/file');
Когда Node вызывает функцию require()
, то делает следующее :
require
и module
появляются в локальной области видимости подключаемого файла.Рассмотрим каждый из этих этапов подробнее.
Весь код буду располагать в папке c:\js>
.
Рассмотрим объект module
. Посмотрим на него в обычном REPL режиме :
В свойстве id
обычно используется полный путь к файлу. В режиме REPL это просто <repl>
.
require
.
Путь к файлу может быть указан как относительный, однако перед загрузкой файла в память, Node опеределяет его абсолютный путь.
Когда мы запрашиваем модуль без указания пути к нему :
require('find-me');
Node будет искать файл find-me.js
во всех путях указанных в module.paths
, которые мы видели на скриншоте выше. Пути будут перебираться по порядку, начиная с папки node_modules
которая находится в текущей директории.
Создадим папку node_modules
в текущей директории и создадим в ней файл find-me.js
такого создержания :
console.log('I am not lost');
Тогда require('find-me');
найдёт этот модуль.
c:\js>node
> require('find-me');
I am not lost
{}
>
Если существует другой файл find-me.js
по другому пути из module.paths
, например в node_modules
который в родительской директории, то он не будет загружен. Загрузится тот который будет найден раньше, а искать его Node будет именно в том порядке в котором указаны пути в module.paths
.
find-me
может быть папкой в node_modules
. В таком случае require('find-me')
будет искать файл index.js
в этой папке.
Файл index.js
будет использован по умолчанию когда мы подключаем папку. Однако мы можем поменять это поведение с помощью файла package.json
который расположим в папке с модулем. В package.json
в свойстве main
можно указать другое имя файла который будет загружен вместо index.js
. По сути новая точка входа.
Несмотря на то что по умолчанию модули подключаются из node_modules
, их можно подключать из любой папки используя относительные пути (./ и ../) или абсолютные которые начинаются с /.
Если к примеру файл find-me.js
находится в папке lib
, то мы можем подключить его так :
require('./lib/find-me');
Создадим файл lib/util.js
:
console.log('In util', module);
И файл index.js
console.log('In index', module); require('./lib/util');
Теперь запускаем файл index.js
:
c:\js>node index.js
In index Module {
id: '.',
exports: {},
parent: null,
filename: 'c:\\js\\index.js',
loaded: false,
children: [],
paths: [ 'c:\\js\\node_modules', 'c:\\node_modules' ] }
In util Module {
id: 'c:\\js\\lib\\util.js',
exports: {},
parent: // ссылается на родительский модуль index
Module {
id: '.', // родительский модуль
exports: {},
parent: null,
filename: 'c:\\js\\index.js',
loaded: false,
children: [ [Circular] ],
/* [Circular] это циклическая ссылка. Указывает на модуль lib/util. Здесь не показано истинное значение чтобы не попасть в бесконечный цикл */
paths: [ 'c:\\js\\node_modules', 'c:\\node_modules' ] },
filename: 'c:\\js\\lib\\util.js',
loaded: false,
children: [],
paths:
[ 'c:\\js\\lib\\node_modules',
'c:\\js\\node_modules',
'c:\\node_modules' ] }
Обратите внимание как главный модуль index (id: '.')
сейчас указан в качестве родителя parent
для модуля lib/util
.
Можно даже подключить модуль index
из lib/util
. Получится циклическая зависимость модулей. Это разрешено. Чтобы разобраться с этим получше, сначала разберёмся с другими концепциями объекта module
.
В любом модуле есть специальный объект exports
. Вы могли заметить его в листингах выше. Можно добавить любые атрибуты к этому объекту. К примеру :
// Добавим в начало файла lib/util.js
exports.id = 'lib/util';
// Добавим в начало файла index.js
exports.id = 'index';
Теперь запустим index.js
и увидим эти атрибуты :
c:\js>node index.js
In index Module {
id: '.',
exports: { id: 'index' },
parent: null,
filename: 'c:\\js\\index.js',
loaded: false,
... }
In util Module {
id: 'c:\\js\\lib\\util.js',
exports: { id: 'lib/util' },
parent:
Module {
id: '.',
exports: { id: 'index' },
parent: null,
filename: 'c:\\js\\index.js',
loaded: false,
... },
... }
Немного сократил выдачу чтобы лучше было видно. Объект exports
теперь имеет те атрибуты которые мы задавали в каждом из модулей. Можете использовать сколько угодно атрибутов, и можно заменить весь объект на чтонибудь другое. К примеру можно заменить его на функцию :
// Добавим это в index.js
module.exports = function() {};
c:\js>node index.js
In index Module {
id: '.',
exports: [Function],
... }
...
Обратите внимание что мы не написали
exports = function() {}
. Потомучто переменная exports
в каждом модуле это всего лишь ссылка ( reference ) на module.exports
. Если мы переопределим переменную exports
, то мы потеряем эту ссылку и по сути просто создадим новую переменную, вместо того чтобы изменить module.exports
, который на самом деле экспортирует свойства.
Объект module.exports
который есть в каждом модуле, это то что возвращает функция require
из этого модуля.
Поменяем файл index.js
:
const UTIL = require('./lib/util');
console.log('UTIL:', UTIL);
Свойства из lib/util
экспортируются в констранту UTIL
:
c:\js>node index.js
In util Module {
id: 'c:\\js\\lib\\util.js',
exports: { id: 'lib/util' },
...
UTIL: { id: 'lib/util' }
Рассмотрим атрибут loaded
который есть в каждом модуле. В листингах выше можно заметить что loaded
каждый раз имел значение false
. Модуль module
использует этот атрибут для отслеживая какой модуль был загружен ( значение true
), а какой еще загружается. Мы можем к примеру увидеть что модуль index.js
был полностью загружен когда проверим объект module
на следующем цикле event loop
, используя вызов setImmediate
:
// Файл index.js
const UTIL = require('./lib/util');
setImmediate(() => {
console.log('The index.js module object is now loaded!', module)
});
c:\js>node index.js
In util Module {
id: 'c:\\js\\lib\\util.js',
exports: { id: 'lib/util' },
parent:
Module {
id: '.',
exports: {},
parent: null,
filename: 'c:\\js\\index.js',
loaded: false,
children: [ [Circular] ],
paths: [ 'c:\\js\\node_modules', 'c:\\node_modules' ] },
filename: 'c:\\js\\lib\\util.js',
loaded: false,
children: [],
paths:
[ 'c:\\js\\lib\\node_modules',
'c:\\js\\node_modules',
'c:\\node_modules' ] }
The index.js module object is now loaded! Module {
id: '.',
exports: {},
parent: null,
filename: 'c:\\js\\index.js',
loaded: true,
children:
[ Module {
id: 'c:\\js\\lib\\util.js',
exports: [Object],
parent: [Circular],
filename: 'c:\\js\\lib\\util.js',
loaded: true,
... }
В отложенном запуске console.log
видим что lib/util.js
и index.js
полностью загрузились.
Объект exports
становится завершённым когда Node завершает загрузку модуля и делает пометку loaded: true. Весь процесс подключения и загрузки модуля происходит синхронно. Это означает что нам нельзя изменять exports
асинхронно. К примеру нельзя делать так :
fs.readFile('/etc/passwd', (err, data) => {
if (err) throw err;
exports.data = data; // Will not work.
});
Что случится если модуль 1 запросит модуль 2, а 2-й при этом запрашивает 1-й ? Чтобы ответить на этот вопрос создадим 2 файла в папке lib, module1.js и module2.js и чтобы каждый из них запрашивал другой.
// lib/module1.js
exports.a = 1;
require('./module2');
exports.b = 2;
exports.c = 3;
// lib/module2.js
const Module1 = require('./module1');
console.log('Module1 is partially loaded here', Module1);
Запускаем module1.js
и видим следующее :
c:\js>node lib/module1.js
Module1 is partially loaded here { a: 1 }
Мы подключаем module2
до того как module1
был полностью загружен. Т.е. module2
запрашивает module1
до того как тот полностью загрузился. В объекте exports
мы получим те свойства которые были определены до начала круговой зависимости. Видим только свойство a
, т.к. b
и c
были экспортированы после подключения module2
, который вывел на экран то что экспортирует module1
.
Node делает это очень просто. Во время загрузки модуля, он выстраивает объект exports
. Вы можете подключать модуль и не ждать когда он закончит загрузку, просто получите частичный exports
с тем что в нём определено на данный момент.
Можно нативно подключать файлы JSON и аддоны на C++ всё тойже функцией require
. Вам даже не надо указываеть расширение файла.
Если расширение файла не указано, Node сначала будет искать .js файл. Если его нет, то попробует искать .json файл. Если и .json файл будет не найден, то попробует найти бинарный .node файл. Однако для избежания неоднозначности вам возможно лучше будет указать расширение файла, когда подключаете чтото кроме .js файла.
Подключение JSON файлов полезно в случае конфигурационных файлов. К примеру :
{
"host": "localhost",
"port": 8080
}
Подключается он так :
const { host, port } = require('./config');
console.log(`Server will run at http://${host}:${port}`);
Как писать аддоны на C++ можно почитать тут.
Чтобы понять как это работает, вспомните чем отличается exports
и module.exports
. exports
это всеголишь ссылка на module.exports
.
exports.id = 42; // Правильно
exports = { id: 42 }; // Неправильно
module.exports = { id: 42 }; // Правильно
Откуда же появляется объект exports
, который виден глобально в пределах модуля ?
Вспомним как работают глобальные переменные в браузере. Когда мы объявляем :
var answer = 42; // Переменная становится видна во всех скриптах
В Node это происходит по другому. Когда мы объявляем переменную в одном модуле, в других модулях она не видна.
Перед компиляцией модуля, Node оборачивает весь код модуля в функцию, которую мы можем увидеть в свойстве wrapper
объекта module
.
c:\js>node
> require('module').wrapper
[ '(function (exports, require, module, __filename, __dirname) { ',
'\n});' ]
>
Код модуля становится телом этой функции. Поэтому все переменные верхнего уровня в модуле имеют область видимости только в нём и не видны из других модулей.
У функции 5 аргументов : exports, require, module, __filename, и __dirname. Они только кажутся глобальными, но на самом деле у каждого модуля они свои. И получают свое значение когда Node запускает эту функцию враппер.
exports
становится ссылкой на module.exports
.
require
и module
это экземпляры связанные с файлом модуля который мы запустили и они не являются глобальными.
__filename
содержит абсолютный путь к файлу, а __dirname
соответственно к директории в которой он находится.
Также их можно увидеть через ключевое слово arguments
.
// файл index.js
console.log(arguments)
c:\js>node index.js
{ '0': {},
'1':
{ [Function: require]
resolve: { [Function: resolve] paths: [Function: paths] },
main:
Module {
id: '.',
exports: {},
parent: null,
filename: 'c:\\js\\index.js',
loaded: false,
children: [],
paths: [Array] },
extensions: { '.js': [Function], '.json': [Function], '.node': [Function] },
cache: { 'c:\js\index.js': [Module] } },
'2':
Module {
id: '.',
exports: {},
parent: null,
filename: 'c:\\js\\index.js',
loaded: false,
children: [],
paths: [ 'c:\\js\\node_modules', 'c:\\node_modules' ] },
'3': 'c:\\js\\index.js',
'4': 'c:\\js' }
Оборачивающая функция возвращает значение module.exports
.
Это выглядит примерно так :
function (require, module, __filename, __dirname) {
let exports = module.exports;
// Здесь ваш код ...
return module.exports;
}
Объект который обычно используется как функция. Аргумент имя модуля или путь, и возвращает module.exports
.
Её можно переопределить к примеру так :
require = function() {
return { mocked: true };
}
Объект require
имеет свойство resolve
. Это функция которая только резолвит модуль ( т.е. определяет абсолютный путь к нему ), но не запускает его на выполнение. Можно использовать чисто для проверки наличия модуля. Если модуля нет, то поймаешь исключение ( error, exception ).
Есть полезное свойство main
, которое можно использовать для определения того запущен модуль напрямую или был подключен в другом модуле.
if (require.main === module) {
// файл запущен напрямую ( не был подключен из другого модуля )
}
Модули кэшируются в объекте require.cache
. Почистить кэш можно просто удалив ключ из этого объекта, и при следующем запросе модуля, он будет перезагружен. Однако это не работает на нативных аддонах, и перезагрузка вызовет ошибку.
Допустим у нас есть модуль такой :
let dep = {
created: Date.now()
};
module.exports = dep;
Если мы подключаем этот модуль несколько раз, то дата будет одной и тойже, т.к. экземпляр модуля берётся из кэша. По сути это как singleton.
// файл index.js
let dep1 = require('./lib/dep');
setTimeout(function(argument) {
let dep2 = require('./lib/dep');
if(dep1.created === dep2.created){
console.log('даты совпадают'); // получится это
}else{
console.log('даты разные');
}
}, 50);
А теперь почистим кэш и посмотрим что получится :
// файл index.js
let dep1 = require('./lib/dep');
delete require.cache[require.resolve('./lib/dep')]; // Почистили кэш
setTimeout(function(argument) {
let dep2 = require('./lib/dep');
if(dep1.created === dep2.created){
console.log('даты совпадают');
}else{
console.log('даты разные'); // получится это
}
}, 50);