Node имеет простую систему загрузки модулей, файлы и модули в которой являются,
в каком-то смысле, синонимами. В примере foo.js загружает модуль circle.js,
находящийся в той же директории.
Содержимое foo.js:
var circle = require('./circle.js');
console.log( 'The area of a circle of radius 4 is '
+ circle.area(4));
Содержимое circle.js:
var PI = Math.PI;
exports.area = function (r) {
return PI * r * r;
};
exports.circumference = function (r) {
return 2 * PI * r;
};
Модуль circle.js экспортирует функции area() и circumference(). Для этого
достаточно добавить экспортируемые функции/объекты к специальному объекты exports.
(В качетве альтернативы можно использовать this вместо exports.) Переменные,
локальные для модуля, не будут видны извне. В этом примере переменная PI видна
только внутри модуля circle.js.
Вместе с Node поставляется несколько стандартных встроенных модулей, большинство из которых описано ниже.
Стандартные модули можно найти в папке lib/ исходного кода node.
Стандартные модули всегда имеют приоритет при загрузке с помощью require().
Например, require('http') всегда возвратит стандартный модуль HTTP, даже если
существует другой файл с таким именем.
Если файла с именем, переданным в require(), не существует, то node сначала
пытается загрузить файлы с этим именем и дополнительным расширением .js потом .node.
.js файлы трактуются как текстовые файлы с JavaScript-кодом, а .node файлы
трактуются как скомпилированные дополнения и загружаются с помощью dlopen.
Имена, начинающиеся на '/', считаются абсолютными путями. Например,
require('/home/marco/foo.js') будет загружать файл /home/marco/foo.js.
Модули, имена которых начинаются на './' считаются относительными для
вызывающего require() модуля. Это означает, что в примере выше circle.js
должен находиться в той же папке, что и foo.js, тогда require('./circle')
будет работать.
В случае отсутствия '/' или './', которые указывают на необходимость поиска файла,
модуль является илбо стандартным модулем, либо загружается из папки node_modules.
Если идентификатор модуля, переданный в require() не представляет стандартный модуль
и не начинается на '/', '../' или './', то node берёт папку текущего модуля,
добалвет к ней '/node_modules' и пытается загрузить модуль из этой папки.
Если по этому пути модуль не будет найден, то node переходит к родительской папке и так далее, пока не будет достигнут корень файловой системы.
Например, если файл '/home/ry/projects/foo.js' вызывает require('bar.js'),
то node будет искать в следующей последовательности:
/home/ry/projects/node_modules/bar.js/home/ry/node_modules/bar.js/home/node_modules/bar.js/node_modules/bar.jsЭто позволяет программам локализовывать их зависимости, чтобы они не конфликтовали.
Довольно удобно организовывать программы в виде вложеннных папок, предоставляя
единственную точку входа для библиотеки. Есть три способа, которыми папки могут
быть переданы в качестве аргумента require().
Первым является создание в папке файла package.json, который определяет
главный модуль. Например, package.json может быть таким:
{ "name" : "some-library",
"main" : "./lib/some-library.js" }
Если он находится в папке ./some-library, то require('./some-library') будет
пытаться загрузить файл ./some-library/lib/some-library.js.
Этим ограничивается осведомлённость node о файлах package.json.
Если файла package.json в папке нет, то node будет пытаться загрузить index.js
или index.node в этой папке. При этом require('./some-library') попробует
загрузить:
./some-library/index.js./some-library/index.nodeМодули кешируются при первой загрузке. Это, кроме остального, означает, что
каждый вызов require('foo') возвращает точно тотже объект, если модуль
разрешается в тоже самое имя файла.
Множественные вызовы require('foo') не вызывают повторной компиляции кода.
Это очень важно. С помощью этого можно возвращать "частично готовые" объекты, позволяя транзитивным зависимостям загружаться даже если в нормальной ситуации это вызовет цикл зависимостей.
Если вы хотите выполнять код модуля несколько раз, то вам следует экспортировать из него функцию и исполнять её в вашем коде.
Модули кешируются в зависимости от имён файлов, в которые они разрешаются.
Так как один и тот же модуль может разрешаться в разные файлы
в зависимости от того, из какого модуля он вызывается (например
при загрузке из папки node_modules), ничто не гарантирует, что
require('foo') всегда будет возвращать один и тот же объект.
Объект exports создаётся системой модулей. Это не всегда удобно, когда вам хочется,
чтобы модуль был экземпляром какого-то класса. Для того, чтобы сделать это,
вам нужно присвоить экспортируемый объект переменной module.exports.
Например, вы можете создать модуль a.js:
var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();
// Do some work, and after some time emit
// the 'ready' event from the module itself.
setTimeout(function() {
module.exports.emit('ready');
}, 1000);
Тогда в другом модуле вы можете использовать его следующим образом:
var a = require('./a');
a.on('ready', function() {
console.log('module a is ready');
});
Нужно иметь в виду, что присваивание module.exports должно происходить
в основном коде модуля, а не в каких-либо коллбеках. Следующий код работать не будет:
x.js:
setTimeout(function() {
module.exports = { a: "hello" };
}, 0);
y.js:
var x = require('./x');
console.log(x.a);
Для того, чтобы определить, какой модуль был загружен при вызове require(),
можно воспользоваться функцией require.resolve().
Учитывая всё вышесказанное, можно составить следующий высокоуровневый псевдокод
для require():
require(X) from module at path Y
1. If X is a core module,
a. return the core module
b. STOP
2. If X begins with `./` or `/` or '../'
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
3. LOAD_NODE_MODULES(X, dirname(Y))
4. THROW "not found"
LOAD_AS_FILE(X)
1. If X is a file, load X as JavaScript text. STOP
2. If X.js is a file, load X.js as JavaScript text. STOP
3. If X.node is a file, load X.node as binary addon. STOP
LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
a. Parse X/package.json, and look for "main" field.
b. let M = X + (json main field)
c. LOAD_AS_FILE(M)
2. LOAD_AS_FILE(X/index)
LOAD_NODE_MODULES(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
a. LOAD_AS_FILE(DIR/X)
b. LOAD_AS_DIRECTORY(DIR/X)
NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let ROOT = index of first instance of "node_modules" in PARTS, or 0
3. let I = count of PARTS - 1
4. let DIRS = []
5. while I > ROOT,
a. if PARTS[I] = "node_modules" CONTINUE
c. DIR = path join(PARTS[0 .. I] + "node_modules")
b. DIRS = DIRS + DIR
c. let I = I - 1
6. return DIRS
В node также есть массив require.paths строк, представляющих папки, где также
будет производится поиск модулей, идентификаторы которых не начинаются на '/',
'./' или '../'. Например, пусть require.paths содержит:
[ '/home/micheil/.node_modules',
'/usr/local/lib/node_modules' ]
Тогда вызов require('bar/baz.js') будет проверять следующие файлы:
'/home/micheil/.node_modules/bar/baz.js''/usr/local/lib/node_modules/bar/baz.js'Массив require.paths может быть изменён во время выполнения программы.
Изначально содержимое берётся из переменной окружения NODE_PATH, которая
содержит разделённые с помощью двоеточия пути. В предыдуще случае NODE_PATH
должна быть установлена таким образом:
/home/micheil/.node_modules:/usr/local/lib/node_modules
Загрузка из require.paths предпринимается только в том случае, если алгоритм
с использованием node_modules, описанный выше, не принёс успеха. Глобальные
модули имеют меньший приоритет, чем зависимости, включённые в текущий модуль.
Переменная require.paths будет поддерживаться только в стабильной ветке v0.4.
Она удалена в ветке v0.5 и не будет присутствовать в стабильной ветке v0.6.
На данный момент это выглядит разумно и представляет простор для экспериментов.
Но на практике изменение require.paths часто является причиной проблем и головной боли.
Этот код делает не то, что ожидается:
require.paths = [ '/usr/lib/node' ];
Всё, чего вы добьётесь, так это потеря ссылки на реальный массив require.paths.
Если вы сделаете:
require.paths.push('./lib');
то в массив будет добавлен не реальный путь, соответствующий ./lib
в файловой системе. Напротив, в массив будет добавлена строка './lib'.
Соответственно, если вы вызовете require('y.js') в модуле /a/b/x.js,
то будет подключен модуль /a/b/lib/y.js, а если вы вызовете require('y.js')
в модуле /l/m/n/o/p.js, то будет подключен модуль /l/m/n/o/lib/y.js.
На практике некоторые используют это при включении зависимостей в модуль, но это хрупкая техника.
К сожалению, есть только один массив require.paths, используемый всеми модулями.
В результате, если один модуль полагается на это поведение, оно может быть изменено другими модулями, загруженными в этом процессе node. Как только приложение становится большим, труднопредсказуемое поведение может стать большой проблемой.
Когда файл исполняется напрямую из Node, переменной require.main
устанавливается значение module. Таким образом можно определить
как вызван файл с помощью проверки
require.main === module
Для файла foo.js это будет верно в случае вызова node foo.js
и не верно в случае вызова require('./foo').
Так как module предоставляет свойство filename (обычно равное __filename),
то точка входа приложения может быть определена с помощью require.main.filename.
Прим. пер.: Эффективные менеджеры могут не читать этот раздел.
Семантика работы require() была разработана так, чтобы поддерживать различные
структуры папок. Пакетные менеджеры, такие как dpkg, rpm и npm, скорее
всего позволят собирать пакеты из Node.js модулей без модификаций.
Ниже мы приводим предлагаемую структуру каталогов, которая должна быть.
Предположим, мы хотим иметь папку /usr/lib/node/<some-package>/<some-version>,
содержащую определёную версию пакета.
Пакет может зависеть от какого-то другого пакета. Соответственно, перед установкой
пакета foo вы должны установить пакет определёную версию пакета bar. Пакет
bar может иметь свои зависимости, и ,возможно, эти зависимости будут формировать циклы.
Так как node определяет realpath каждого загружаемого модуля (т.е. разрешает
символические ссылки), и потом ищет их зависимости в папках node_modules,
как описано выше, эту ситуацию легко решить с помощью следующей архитектуры:
/usr/lib/node/foo/1.2.3/ - Содержимое модуля foo версии 1.2.3./usr/lib/node/bar/4.3.2/ - Содержимое модуля bar, от которого зависит foo./usr/lib/node/foo/1.2.3/node_modules/bar - Символическая ссылка на
/usr/lib/node/bar/4.3.2/./usr/lib/node/bar/4.3.2/node_modules/* - Символические ссылки на модули,
от которых зависит bar.Таким образом, даже если встретится цикл или другой конфликт зависимостей, каждый модуль сможет получить ту пакета, от которой он зависит.
Когда код из пакета foo вызывает require('bar'), он получит версию,
связанную с /usr/lib/node/foo/1.2.3/node_modules/bar.
Когда код из пакета bar вызывает require('quux'), он получит версию,
связанную с /usr/lib/node/bar/4.3.2/node_modules/quux.
Кроме того, чтобы сделать процесс поиска модулей более оптимальным, мы можем
поместить модули не в папку /usr/lib/node, а в /usr/lib/node_modules/<name>/<version>.
Тогда node не будет пытаться искать отсутствующие зависимости в /usr/node_modules
и /node_modules.
Чтобы сделать модули доступными и в REPL, может быть полезно добавить путь
/usr/lib/node_modules в переменную окружения $NODE_PATH. Так как поиск модулей
с помощью папок node_modules однован на реальных путях в файловой системе,
разрешаемых во время вызова require(), то пакеты могут располагаться где угодно.