Начнем урок с изучения объекта модуль, он содержит множество важных свойств, которые понадобиться в более сложных сценариях поведения.
Код урока можно скачать тут.
Объект module это переменная, которая существует в каждом модуле.
Сделаем запись в index.js и выведем ее:
console.log(module);
Переменная модуль есть в каждом файле и содержит информацию об объекте данного модуля которая по мере того как Node.js обрабатывает файл постепенно заполняется.
Кратко пройдемся по его основным свойствам:
- ID – как правило содержит полный путь к файлу, если операционная система/файловая поддерживает символические ссылки то все символические ссылки будут отрисованы.
- Exports – это то что выдается наружу, мы говорили об этом на прошлом уроке.
- Parent – это ссылка на родительский модуль, т.е. на тот модуль который required данный.
- Filename – полное имя файла
- Loaded –показывает загрузился ли модуль. На момент вывода модуля в консоль он еще не обработан.
- Children – это те модули которые данный модуль подключил через require.
- Path – это некое внутреннее id
Сейчас для нас важны два свойства: parent и exports. Давайте обсудим module.parent.
Бывает так что файл может работать как в режиме прямого запуска (node server.js) так и в качестве модуля. Например, наш server.js будет работать если он будет запущен явно. А если нет, если он был подключен другим модулем, то пусть он этот функционал экспортирует. Разделить эти два случая можно при помощи проверки (пишем в server.js):
var user = require('./user'); function run() { var tim = new user.User("Tim"); var tom = new user.User("Tom"); tim.hello(tom); } if (module.parent) { exports.run = run; } else { run(); }
Если модуль parent есть (10 строчка) значит что server.js кто-то подключил. Значит экспортируем функционал заключив его в функцию run. А если модуля parent нет это означает что он запущен сам по себе, в этом случае запустим run() прямо сейчас.
А теперь сделаю модуль app.js подключу в нем сервер.
var server = require('./server');
Попробую запустить node app.js. Обратите внимание, ничего такого не произошло. То есть, user она подключила, что происходит в любом случае, но run не запустилась, потому что есть module.parent. Он работает только в режиме модуля. Уберу лишний console.log:
console.log("user.js is required!");
Соответственно, если в app.js я захочу вызвать сервер, я его вызову.
server.run();
Как правило, такой прием используется в тех случаях, когда пишется какая-то консольная утилита или какое-то независимое приложение, которое может работать, как часть чего-то другого в том числе.
Следующий прием по работе с модулями, который мы изучим, касается правильного использования module.exports.
Когда мы записываем свойства в exports для того, чтобы вынести из модуля, на самом деле, мы пишем их в module.exports. Это и есть то самое истинное свойство объекта модуль, которое выходит наружу, а exports и this в контексте модуля являются ссылками на него.
// module.exports = exports = this
Поэтому, я могу написать в index.js
exports.User = User;
а могу написать
this.User = User;
Разницы не будет. Могу написать полностью
module.exports.User = User;
Обычно используют exports по двум причинам. Первая – это то, что короче, чем module.exports, а второе – это то, что this еще короче, но он менее универсален, потому что this на уровне модуля – это то же самое, что и exports, а вот this функции уже будет другим. Поэтому для унификации используют везде обычно exports. А по какой-либо причине используют module.exports. А какая эта особая причина? Если внимательно посмотреть на код в server.js, то можно увидеть некую несуразность.
var user = require('./user'); function run() { var tim = new user.User("Tim"); var tom = new user.User("Tom"); tim.hello(tom); } if (module.parent) { exports.run = run; } else { run(); }
На самом деле, из этого модуля (в index.js) мы хотим выдать наружу только функцию user.
function User(name){
Мы не хотим выдать что-то еще, поэтому объект нам не нужен. Нам бы хотелось иметь код такого вида в server.js:
var User = require('./user'); // получили функцию, использовали var tim = new User("Tim"); var tom = new User("Tom");
без всякого промежуточного объекта. Это возможно, если записать exports в index.js напрямую:
module.exports = User;
Экспортируемый объект как раз и будет функцией user. Обратите внимание, записывать необходимо только так, а в случае записи:
exports = User; - было бы невозможно, это не работает.
Потому что у нас есть module.exports, а exports – это всего лишь ссылка на него. Если заменить эту ссылку, то на module.exports это не повлияет. Так работают объекты в JavaScript. Если я меняю что-то по ссылке, то оно изменится внутри объекта, а если я заменяю саму ссылку, то ничего не произойдет с другими ссылками. Итак, проверяем. Все хорошо! Наш код стал еще проще.
Следующим этапом мы добавим к проекту базу данных. Пока это будет неполноценная база данных (о ней мы поговорим позже). Разговор пойдет на уровне структуры. Мы узнаем, как это реализуется и почему работает. Сделаем каталог db, и здесь буду файлы index.js (создадим его) и база данных ru.json.
У объекта базы есть метод connect. При вызове этого метода пусть она загружает фразы. Сейчас это просто так, а в реальной жизни это будет подсоединено к базе данных, из которой потом можно будет делать запросы. Как? Пусть у нас будет метод:
var phrases; exports.connect = function() { phrases = require('./en'); }; exports.getPhrase = function(name) { if (!phrases[name]) { throw new Error("There is no such a phrase: " + name); } return phrases[name]; };
Этот метод будет возвращать соответствующую фразу, а если нет, то выдавать ошибку.
Теперь используем эту базу в объекте user (user/index.js). Фразы теперь будут получатьcя из базы данных.
var phrases = require('../db');
И вместо phrases в строчке с Hello мы делаем
console.log(db.getPhrases("Hello") + ", " + who.name);
Вот так. Запускаем. Ошибка вышла. Мы не загрузили ничего из базы. Давайте исправим это.
var phrases = require('../db'); db.connect();
Действительно, когда мы обращаемся к базе, нужно сначала connect, а потом все остальное. Запускам еще раз. Работает. А теперь, так как эта база глобальная для всего нашего проекта, давайте ею воспользуюсь и в сервере тоже. Подключаю объект базы в server.js (1 строчка) и выведу (11 строчка)
var db = require('./db'); var User = require('./user'); function run() { var tim = new User("Tim"); var tom = new User("Tom"); tim.hello(tom); console.log(db.getPhrase("Run successful")); } if (module.parent) { exports.run = run; } else { run(); }
Успешный запуск. Добавлю эту фразу в базу. (В наш JSON)
{ "Hello": "Hi", "Run successful": "Run successful" }
Теперь несколько слов о том, как это все будет работать. Когда Node.js первый раз загружает модуль, он полностью создает соответствующий объект module с учетом parent, exports и аналогичных свойств. Запоминает его у себя. Модуль id, тот, который является обычно полным путем к файлу, служит идентификатором для внутреннего cache. Node.js как бы запоминает: файл такой-то, для него создан объект модуля такой-то. В следующий раз, когда мы получаем тот же файл, а файл, по сути дела, одинаковый, просто пути к нему разные, но абсолютный путь одинаковый. Node.js обращается к cache и берет все тот же объект. Получается, что в server.js и user/index.js будет использован один и тот же объект базы данных. Соответственно, прием здесь такой: первый раз, когда подключается модуль, он инициализуется. В данном случае (в server.js) используется connect в базе данных. В дальнейшем модуль уже инициализован, поэтому, просто его берем и пользуемся.Теперь сделаем следующий логичный шаг. Дело в том, что в server.js db мы получаем из текущей директории, а в user/index.js из родительской:
var db = require('../db');
Давайте подумаем, что произойдет, когда мы user будем развивать, и у директора user будут поддиректории. Получается, что в index.js я делаю require вот так:
var db = require('../db');
а в поддиректории мне надо будет делать require вот так:
var db = require('../../db');
А если я буду переносить файл, то мне нужно уследить, чтобы пути автоматически правильно обновлялись. Конечно, ide нам поможет в этом, но тем не менее.
Как бы нам сделать такую простую штуку, чтобы база подключалась просто так? Зачем мне указывать явно путь к базе, если мы знаем, что имеется в виду одна, которая в корне, главная? Для того, чтобы так сделать, нам нужно понимать порядок поиска модулей в Node.js. Для этого обратимся к документации.
https://nodejs.org/api/modules.html#modules_all_together
Здесь, в дальнейшем, я буду по возможности именно комментировать документацию, которая уже есть, разъяснять всякие тонкие моменты, нежели, чем писать свои какие-то выводы. На документацию нужно ориентироваться, ее нужно понимать. В данном случае, здесь есть описание ряда свойств, о которых мы говорили. Нас интересует порядок поиска модуля.
Вот так. Это то, что происходит, когда вызывают require. Скорее всего, если вы видите это первый раз, то не очень понятно. Я постараюсь разъяснить. Итак, require модуль. Вообще в Node.js много встроенных модулей, например, модуль по работе с файловой системой fs. Если есть такой встроенный модуль, то require сработает тут же.
Если я указал путь к require, в данном случае это (‘./db’) скажем
var db = require('./db');
тогда Node.js поищет файл по этому пути, попытается либо найти данный файл, либо попытается получить этот файл, как директорию. Здесь есть упоминание package.json, мы позже поговорим про это. В нашем случае он возьмет db/index.js, это и будет файлом модуля. Ну, и наконец, третий пункт LOAD_NODE_MODULES (X, dirname(Y)). Он сработает по алгоритму ниже только в том случае, если я не указал путь, и, при этом, это не встроенный модуль. Тогда Node.js будет его искать. Но как? Есть специальное название директории, которое называется “node_modules”. Он поищет эту директорию сначала в текущем местоположении, то есть, в– start/node_modules (создадим такую директорию node_modules и поместим в нее нашу директорию db). Проверим пути подключения в файлах user/index.js, server.js. Если он ее найдет, то попытается взять модуль из нее. Проверим. Запускаю. Все сработало. Оно ее нашло в директории node_modules.
Либо, если в директории node_modules нету, то оно поищет директорию node_modules выше. Там не найдет – ищет еще выше и т.д. Помните, мы смотрели module Path. Module path – это как раз те пути, по которым она будет искать. Иначе говоря, это просто текущий путь и все выше него. Это нужно для того чтобы создавать директорию node_modules, в нее ставятся внешние пакеты, внешние модули, и потом они доступны. Эта директория может быть выше, выше, на любом уровне. Может быть несколько директорий – первая найденная, где этот модуль есть, послужит точкой остановки. Таким образом, я могу использовать вот такой вот путь (‘./db’), если db есть в node_modules.
А что если я не хочу помещать ее в node_modules (удалим эту директорию, вынесем db в корень )? После того, как оно поищет все эти node_modules и ничего не найдет, оно использует еще одно место для поиска. Это переменная NODE_PATH. В ней можно указать несколько путей, по которым оно еще будет искать. Я переместил db и запуск не сработает, модуль не найден. Но если указать:
NODE_PATH = . переменное окружение.
Если вы работаете под Windows, то именно окружение ставится при помощи set-команды, если вы с командной строки:
Windows:
>set NODE_PATH = .
>node server.js
Либо переменное окружение вы можете поправить в настройках вашего файлового менеджера или с места, откуда вы запускаете. Соответственно:
NODE_PATH =. node server.js
Все хорошо. Потому что текущий путь добавился в список тех, по которым происходит поиск. И, наконец, кроме NODE_PATH, по историческим причинам происходит поиск еще в таких директориях:
- 1:$HOME/.node_modules
- 2:$HOME/.node_libraries
- 3:$PREFIX/lib/node
Вообще, на это можно не ориентироваться, так как в реальной жизни мы этим пользоваться не будем, нам это не нужно. Когда-то давно было по-другому, и с того времени остались эти пути.
Итак, что мы получили? Мы получили следующие дополнительные приемы работы с модулями.
- Кеширование модулей.
Подключаем модуль один раз, инициализуем и в дальнейшем пользуемся уже объектом. То есть, заново файл модуля никогда не читается. Хотя, существуют такие специальные команды, которые позволяют сделать пустым cache Node.js, то есть, убрать модуль из cache. Можно поэкспериментировать, если хотите, в документации. Но обычно этого никто не делает. Итак, просчитали модуль, добавили в cache, идем дальше.
- Если мы хотим, чтобы модуль у нас был глобальным, то есть, искал себе пути, то он должен быть в node_modules, либо по NODE_PATH должен искаться.
- Следующий прием, который мы рассмотрим, называется модуль-фабрика. Он используется для того, чтобы передавать модулю параметры. Посмотрим этот прием на практическом примере, а именно в процессе подключения logger к нашему приложению. Logger – это отдельный модуль, который мы назовем logger.js. (создадим его в корневой папке). Мы ничего пока сюда записывать не будем, а сосредоточимся на том, что нужно для того, чтобы им пользоваться. А именно, сделаем следующую запись в user/index.js:
var log = require('../logger')
Что мы хотим? Мы хотим, чтобы, когда я вызываю log,то вводилась соответствующая строка, и перед ней было название модуля, который ее вызывает (изменим console.log таким образом):
log(db.getPhrase("Hello") + ", " + who.name);
Соответственно для того, чтобы логгер выводил именно название текущего модуля, мне нужно передать ему текущий модуль. Давайте это сделаем:
var log = require('../logger’)(module);
Паттерн модуль фабрика заключается в том что я подключаю модуль и тут же передаю ему параметры. Параметры при подключении я не могу передать, например через запятую.
Я могу получить логгер и, таким образом, создать нужную функцию. То есть, внутри логгера я делаю следующее:
1. // var log = require('logger')(module); 2. module.exports = function(module) { 3. 4. return function(/* ... */) { 5. var args = [module.filename].concat([].slice.call(arguments)); 6. console.log.apply(console, args); 7. }; 8. };
function(module) – фабричная функция, которая получает название модуля, который нужно логгировать и который, исходя из этого модуля, делает функцию логгер. Эта функция получает какие-то параметры, и давайте она будет передавать их console.log. Конечно, файлы можно вводить в базу и куда угодно. Мы передадим все аргументы в console.log. Давайте, не просто их передадим, а еще кое-что добавим – делаем из arguments массив и прибавляем ему filename. Вначале мы прибавили имя файла и передали все аргументы в консоль. Давайте проверим, работает ли оно. Запускаем. Теперь каждый модуль мы видим:
Можно, конечно же, прибавлять не названия модуля, а, например, только последние два элемента. Поле для экспериментов тут большое. В дальнейшем мы поговорим о некоторых уже готовых логгерах, которые уже можно поставить и использовать.
Итак, на этом занятии мы поговорили о различных приемах работы с модулями:
- о том, что такое объект module;
- как запустить модуль в различных режимах приложения или компонента;
- как экспортировать то, что нам надо, а не обязательно объект;
- как работает кеширование модулей (именно кеширование позволяет избежать глобальных перемен);
- как происходит поиск модулей и как реализуется передача параметров модулей при помощи модуль-фабрики.
На нашем следующем занятии мы поговорим о пакетном менеджере NPM и обсудим некоторые модули, которые используем в дальнейшем в процессе разработки.
Код нашего урока Вы можете скачать тут.
Материал урока взят из следующего скринкаста.
We are looking forward to meeting you on our website soshace.com