Объект module
Нашей следующей темой будет изучение объекта «module». Объект «module» это основополагающий объект в понимании модулей. В нем есть множество важных свойств которые нам понадобятся в более сложных сценариях поведения, к которым мы скоро с вами перейдем.
Объект «module» является переменной которая существует в каждом модуле, выведем ее в консоль:
Переменная модуль, есть в каждом файле и содержит информацию об объекте данного модуля, который по мере того как Node.JS обрабатывает файл постепенно заполняется. Кратко пройдемся по его основным свойствам. Давайте для наглядности скопируем вывод модуля вот сюда:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
Module { id: 'C:\\start\\user\\index.js', // 1 exports: { User: [Function: User] }, // 2 parent: /* 3 Module { * id: '.', * exports: {}, * parent: null, * filename: 'C:\\start\\server.js', * loaded: false, * children: [ [Circular] ], * paths: [ 'C:\\start\\node_modules', 'C:\\node_modules' ] }, */ filename: 'C:\\start\\user\\index.js', // 4 loaded: false, // 5 children: /* 6 [ Module { * id: 'C:\\start\\user\\ru.json', * exports: [Object], * parent: [Circular], * filename: 'C:\\start\\user\\ru.json', * loaded: true, * children: [], * paths: [Object] } ], */ paths: /*7 [ 'C:\\start\\user\\node_modules', * 'C:\\start\\node_modules', * 'C:\\node_modules' ] } */ |
Итак:
- «id» как правило содержит полный путь к файлу. Если операционная система поддерживает символические ссылки и файловая система их поддерживает конечно же, то все символические ссылки будут здесь отображены. Если вы являетесь пользователем Windows и не знаете, что такое символические ссылки, то ничего страшного, для понимания это не критично. «id» используется внутри Node.JS. Как правило мы сами его использовать не будем.
- «exports» мы уже знаем. Это экспорты того, что выдается наружу.
- «prent» Это ссылка на родительский модуль, то есть на тот модуль который required данный.
- «filename» Имя файла, полное, с учетом пути.
- «loaded» Загрузился ли модуль. На момент, когда мы выводим в консоль модуль, этот модуль еще до конца не обработан, файл то не выполнен до конца, поэтому «loaded: false».
- «children» Это соответственно те модули, которые данный модуль подключил, через «require()». В данном случае тут только один модуль, наш «ru.json», который, обратите внимание, «loaded: true».
- «path:» Это тоже внутренняя переменная как и «id» и мы ее не будем использовать, я скажу о ней несколько слов, позже, когда мы будем разбирать порядок поиска модулей с учетом путей.
Из этого обширного списка нам важны два свойства, которые на практике используются наиболее часто. Это свойство «parent» и «exports», по этому о них мы в дальнейшем поговорим более подробно.
Модуль или приложение? module.parent
Первый, можно сказать разминочный прием который мы с вами обсудим, это использование «module.parent». Бывает так, что какой то JS файл, может работать как в режиме прямого запуска, через Node, как например «node server.js», так и в качестве модуля, для чего то другого.
Например модуль «server.js» будет запускать свой функционал, только в том случае, если он запущен явно. А если нет, если какой то другой модуль его подключил? То пусть он этот функционал экспортирует. Разделить эти два случая можно при помощи проверки.
Если «module.parent» есть? это означает, что «server.js» кто то подключил в этом случае имеет смысл экспортировать весь функционал например заключив его в функцию «run()»
|
function run(){ var vasya = new user.User("Вася"); var petya = new user.User("Петя"); vasya.hello(petya); } |
и сделаем
|
if(module.parent){ exports.run = run; } |
А если «module.parent» отсутствует и результат проверки «false»? Это означает, что сервер запущен сам по себе. В этом случае давайте запустим эту функцию прямо сейчас
Приведем «server.js» целиком
|
var user = require('./user'); function run(){ var vasya = new user.User("Вася"); var petya = new user.User("Петя"); vasya.hello(petya); } if(module.parent){ exports.run = run; }else{ run(); } |
Проверяем, пока что работает?
Работает! Сработала «else».
Давайте сделаем новый модуль и назовем его «app.js». Подключу в нем сервер и запустим его
|
var server = require('./server'); server.run(); |
Теперь запустим «app.js» командой «node app.js»
Отлично, наша проверка сработала, определила наличие «module.parent», соответственно поместила функцию «run()» в «exports», тем самым позволило ее вызвать в «app.js».
Как правило, такой прием используется в тех случаях, когда пишется какая то консольная утилита или какое то независимое приложение которое, однако, может работать как часть чего то другого, в том числе.
Модуль-функция
module.exports = function
Следующий прием по работе с модулями который мы изучим касается правильного использования «module.exports».
Когда мы записываем свойства в «exports», для того, чтобы вынести из модуля, на самом деле мы пишем их в «module.exports». «module.exports» это и есть то самое истинное свойство объекта «module» которое наружу выходит. А «exports» и «this», в контексте модуля, являются ссылками на него.
|
module.exports = exports = this |
По этому я могу написать
и могу написать
разницы не будет. Могу написать полностью
|
module.exports.User = User; |
Обычно используют
по двум причинам:
- Это короче чем «module.exports».
- «this» еще короче, но «this» менее универсален. Потому что «this» на уровне модуля это то же самое, что «exports», а вот «this» функции уже будет другим. По этому для унификации используют везде обычно «exports», если нет какой то особой причины использовать «module.exports».
А какая эта особая причина? если мы внимательно посмотрим сейчас на наш код:
|
var user = require('./user'); function run(){ var vasya = new user.User("Вася"); var petya = new user.User("Петя"); vasya.hello(petya); } if(module.parent){ exports.run = run; }else{ run(); } |
то тут мы видим некую несуразность. Мы на самом деле из модуля «user/index.js» хотим наружу выдать только функцию «function User(name)». Мы не хотим выдать что то еще и по этому объект здесь не нужен. Нам бы хотелось так:
|
var User = require('./user'); function run(){ var vasya = new User("Вася"); var petya = new User("Петя"); vasya.hello(petya); } if(module.parent){ exports.run = run; }else{ run(); } |
Получили функцию и использовали, без всякого промежуточного объекта. Это возможно если в «user/index.js» вместо
записать вот так напрямую:
То есть экспортируемый объект как раз и будет наша функция «User()». Обратите внимание, именно так как написано выше. Записать вот так вот:
было бы не возможно, это не работает. Потому что смотрите, у нас есть «module.exports», а «exports» это всего лишь ссылка на него. Если заменить эту ссылку, то на «module.exports» это не повлияет. Так работают объекты в JavaScript — если я меняю что то по ссылке, то оно изменится здесь внутри объекта. А если я заменяю саму ссылку то ничего не произойдет с другими ссылками. Итак проверяем:
Все хорошо, наш код стал еще проще.
Кеширование модулей
Следующим этапом мы добавим к проекту базу данных. Пока что это будет не полноценная база данных, о ней мы поговорим позже, а просто, на уровне структуры, мы поговорим о том, как это реализуется и почему работает.
База данных будет файлом «db.js», создадим его. И в нем у нас для базы данных прямой кандидат, это наши фразы в «ru.js». По этому давайте лучше создадим каталог «db», поместим туда «db.js» и переименуем его в «idex.js». Туда же поместим и «ru.json». Наша структура проекта имеет следующий вид:
У объекта базы есть метод «connect» и при вызове этого метода пусть она загружает фразы. Сейчас это просто так, а конечно же, в реальной жизни, это будет подсоединение к базе данных из которой потом можно делать запросы. Запросы как «getPhrase». Этот метод будет возвращать соответствующую фразу, а если ее нету, то выдавать ошибку. Вот так выглядит код «db/index.js» целиком:
|
var phrases; exports.connect = function(){ phrases = require('./ru'); } exports.getPhrase = function(name){ if(!phrases[name]){ throw new Error("Нет такой фразы: " + name); } return phrases[name]; }; |
Теперь меняем меняем «user/index.js»:
|
var db = require('../db'); db.connect(); function User(name){ this.name = name; } User.prototype.hello = function(who){ console.log(db.getPhrase("Hello") + ", " + who.name); }; console.log("user.js is required!"); module.exports = User; |
Что мы здесь поменяли? В первой строке подключили файл базы, во второй создали коннект (такие правила работы с базой) и в восьмой строке вместо «phrases.Hello» теперь соответственно вызываем метод «db.getPhrase(«Hello»)». Запускаем:
А теперь, так как эта база данных глобальная для нашего проекта, давайте я ей воспользуюсь и в сервере тоже. Подключу объект базы в «server.js». И выведу «console.log(db.getPhrase(«Run successful»));» наш «server.js» теперь имеет такой вид:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
var db = require('./db'); var User = require('./user'); function run(){ var vasya = new User("Вася"); var petya = new User("Петя"); vasya.hello(petya); console.log(db.getPhrase("Run successful")); } if(module.parent){ exports.run = run; }else{ run(); } |
Изменим «ru.json», добавив еще одну фразу:
|
{ "Hello": "Привет", "Run successful": "Запуск успешен" } |
И теперь несколько слов о том как это все будет работать.
Когда Node.JS первый раз загружает модуль, в файле «server.js»( на первой строке), он полностью создает соответствующий объект «module», с учетом «parent», «exports» и аналогичных свойств. И запоминает его у себя. «module.id», тот самый который обычно является полным путем к файлу, служит идентификатором для внутреннего кеша. Node.JS как бы запоминает файл такой то для него создан объект модуль такой то. И в следующий раз, когда мы получаем тот же файл в «user/index.js», получается, что и в «server.js» и в «user/index.js» будет использован один и тот же объект базы данных который мы запрашиваем в «require()». Соответственно прием здесь такой — первый раз когда подключается модуль(в файле «server.js») он инициализуется и мы вызываем «db.connect()». Теперь «server.js» выглядит так
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
var db = require('db'); db.connect(); var User = require('./user'); function run(){ var vasya = new User("Вася"); var petya = new User("Петя"); vasya.hello(petya); console.log(db.getPhrase("Run successful")); } if(module.parent){ exports.run = run; }else{ run(); } |
В дальнейшем в «user/index.js» он уже инициализован поэтому мы просто его берем и пользуемся.
|
var db = require('db'); function User(name){ this.name = name; } User.prototype.hello = function(who){ console.log(db.getPhrase("Hello") + ", " + who.name); }; console.log("user.js is required!"); module.exports = User; |
Расположение модулей: порядок поиска
А теперь сделаем следующий логический шаг. Дело в том что в «server.js» мы «db» получаем из текущей директории, а в «user/index.js» из родительской. Давайте подумаем что будет когда мы разовьем «user» и у него появятся поддиректории. Сейчас мы вызываем «db» вот так
|
var db = require('../db'); |
из поддиректории будем вот так
|
var db = require('../../db'); |
а если я буду переносить файлы, то мне нужно следить, чтоб пути правильно обновлялись. Вот и вопрос, как бы нам сделать такую штуку, чтоб «db» подключалась просто вот так:
Зачем мне указывать явно путь к базе, если мы знаем, что имеется ввиду одна база которая в корне, главная. Для того чтоб так сделать — «var db = require(‘db’);» нам нужно понимать порядок поиска модулей в Node.JS. Для этого обратимся к документации. Заходим в Google и ищем «node module» и первая же ссылка даст нам документацию «https://nodejs.org/api/modules.html» по модулям в Node.JS. В дальнейшем я буду, по возможности, именно, комментировать документацию которая уже есть, разъяснять всякие тонкие моменты нежели, чем писать какие то свои выводы, просто потому, что в документации все равно нужно ориентироваться, ее нужно понимать. В данном случае здесь есть описание ряда свойств о которых мы говорили. Нас интересует порядок поиска. Вот он:
Переходим, видим:
Это то, что происходит когда вызывается «require()». Скорее всего, если вы видите это в первый раз, то не очень понятно, я постараюсь разъяснить. Итак, «require()» модуль. Вообще в Node.JS есть много встроенных модулей, например модуль по работе с файловой системой «fs». И если это, такой, встроенный модуль, то «require(‘fs’);» сработает тут же и все готово. Как описано в нашем изображении, вариант номер один.
|
1. If X is a core module, a. return the core module b. STOP |
Дальше, если я указал путь к «require(./..)» — вариант номер два:
|
2. If X begins with './' or '/' or '../' a. LOAD_AS_FILE(Y + X) b. LOAD_AS_DIRECTORY(Y + X) |
В данном случае это «./db». Тогда Node.js поищет файл по этому пути, попытается либо найти данный файл как указано здесь
|
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.json is a file, parse X.json to a JavaScript Object. STOP 4. 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. If X/index.js is a file, load X/index.js as JavaScript text. STOP 3. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP 4. If X/index.node is a file, load X/index.node as binary addon. STOP |
Здесь есть упоминание о «pakage.json», LOAD_AS_DIRECTORY(X)/пункт 1, мы об этом позже поговорим. В нашем случае он возьмет «db/index.js», LOAD_AS_DIRECTORY(X)/пункт 3. Это и будет файлом модуля.
Ну и на конец, третий пункт.
|
3. LOAD_NODE_MODULES(X, dirname(Y)) |
Третий сработает по вышеизложенному алгоритму, в том случае если я путь не указал и при этом это не встроенный модуль. Тогда Node.JS будет его искать. Есть специальное название директории которое называется «node_modules» из
|
NODE_MODULES_PATHS(START) 1. let PARTS = path split(START) 2. let I = count of PARTS - 1 3. let DIRS = [] 4. while I >= 0, 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 5. return DIRS |
И он поищет эту директорию, сначала в текущем местоположении. То есть если мы вызываем из «server.js» изменим его так
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
var db = require('db'); var User = require('./user'); function run(){ var vasya = new User("Вася"); var petya = new User("Петя"); vasya.hello(petya); console.log(db.getPhrase("Run successful")); } if(module.parent){ exports.run = run; }else{ run(); } |
то Node.JS будет искать папку «node_modules», чтоб в ней найти «db». Искать ее он будет сначала в своей директории, потом на уровень выше и выше и выше, пока не найдет. На первом же совпадении поиск остановится. Помните мы смотрели такую штуку «module path». «module path» это как раз те пути по которым он будет искать. Иначе говоря это просто текущий путь и все выше него. Это нужно для того, чтоб можно было создать директорию «node_modules» и в нее поместить внешние модули и они были бы доступны просто по «require(‘module_name’)». Директорий «node_modules» может быть несколько. Первая в которой будет найден «module_name» послужит точкой остановки поиска.
Таким образом я могу в «server.js» использовать такой путь «require(‘db’)» если «db» есть в «node_modules». Давайте проверим.
Работает! Поехали дальше!
А что если я не хочу помещать свой «db» в «node_modules»? «db» итак директория, зачем ее помещать в еще одну директорию, не к чему. Хочу просто в корень проекта кинуть «db» и чтоб работало. Можно и так! Потому что после того как Node.JS поищет все эти «node_modules» и ничего не найдет node использует еще одно место для поиска. Это переменная «NODE_PATH».
В «NODE_PATH» можно указать еще несколько путей по которым еще будет искать. Давайте вернем директорию «db» в корень проекта и запустим «server.js», предварительно установив переменную окружения «NODE_PATH=.» при помощи команды «set» в Windows.
Все хорошо, потому что «.». то есть текущий путь добавился в список тех, по которым происходит поиск.
И наконец, кроме «NODE_PATH», по историческим причинам, происходит поиск еще в таких директориях
|
Additionally, Node.js will search in the following locations: 1: $HOME/.node_modules 2: $HOME/.node_libraries 3: $PREFIX/lib/node |
На это можно не ориентироваться, потому, что в реальной жизни мы не будем этим пользоваться. Это не нужно, просто так повелось, давно когда то было по другому и от давно остались вот эти пути.
Ну и наконец следующий прием который мы с вами рассмотрим называется «Модуль-фабрика».
Передаем параметры: модуль-фабрика
Мы посмотрим этот прием на практическом примере, а именно в процессе подключения логгера к нашему приложению.
Логгер, это отдельный модуль который мы назовем «logger.js». Создадим такой файл в корне нашего проекта. Мы пока в этот файл ничего записывать не будем, а сосредоточимся на том, что нужно для того чтоб им пользоваться. А именно изменим наш «user/index.js»
|
var db = require('db'); var log = require('logger') (module); function User(name){ this.name = name; } User.prototype.hello = function(who){ log(db.getPhrase("Hello") + ", " + who.name); }; console.log("user.js is required!"); module.exports = User; |
Что мы здесь изменили? Ну первое мы в строке два создали переменную «log» и присвоили ей объект модуля «logger.js» вернув его через «require(‘logger’)». Мы хотим что? Мы хотим когда я вызываю в строке восемь «log(db.getPhrase(«Hello») + «, » + who.name)» то выводилась в консоль соответствующая строка, как и прежде, но перед ней было бы название модуля который ее вызывает. Соответственно, чтоб логгер выводил мне название текущего модуля мне нужно текущий модуль ему передать. Давайте я это сделаю
|
var log = require('logger') (module); |
Патерн модуль фабрика заключается в том, что я подключаю модуль — «require(‘logger’)» и тут же передаю ему параметры. — «(module)» Параметры при подключении я не могу передать как либо иначе чем
|
var log = require('logger') (module); |
Давайте заполним «logger.js» и вы поймете как это работает, если уже не догадались:
|
module.exports = function(module){ return function(/*...*/){ var args = [module.filename].concat([].slice.call(arguments)); console.log.apply(console, args); }; }; |
Сначала я делаю «module.exports» равно фабричная функция которая получает название модуля который нужно логгировать — «function(module)» и которая исходя из этого модуля делает «функцию логгер». Эта функция она, что делает, она получает какие то параметры — «function(/*…*/)» и давайте она будет передавать их в «console.log.apply(console, arguments);», конечно она может их и в базу выводить или в файл или куда угодно. И давайте не просто их передадим, а кое что еще добавим. Делаем из «arguments» обычный массив — «var args = [].slice.call(arguments)» и прибавляем ему «[module.filename]» получается
|
var args = [module.filename].concat([].slice.call(arguments)); |
вот сначала мы прибавили имя файла и предали все аргументы в косоль
|
console.log.apply(console, args); |
Что ж, давайте проверим, работает ли оно
Как видим каждый модуль теперь выведен. «index.js» скзал «Привет, Петя», а «server.js» сказал «Запуск успешен».
В дальнейшем мы поговорим о уже готовых логгерах, которые можно поставить и использовать.
Итого на этом занятии мы поговорили о различных приемах работы с модулями.
- Объект module. О том что такое объект module
- Модуль или приложение? module.parent Как запустить модуль в различных режимах. Приложения или компонента.
- Модуль-функция module.exports=function. Как экспортировать то что нам надо, а не обязательно объект.
- Кеширование модулей. Подключаем модуль, один раз инициализуем и в дальнейшем пользуемся уже объектом. То есть заново файл модуля никогда не читается. Хотя существуют такие команды которые позволяют сделать пустым кеш Node.JS, то есть убрать модуль из кеша. Можно поэкспериментировать, если хотите, в документации есть информация на этот счет, но обычно этого никто не делает. И именно это кеширование помогает избежать глобальных переменных.
- Расположение модулей: порядок поиска. Если мы хотим, чтобы модуль у нас был глобальным, то есть искался без пути, то он должен быть в «node_modules» либо по «NODE_PATH» должен искаться.
- Передаем параметры: модуль-фабрика. Как реализуется передача параметров в модули, при помощи модуль-фабрики.
На нашем следующем занятии мы поговорим о пакетном менеджере NPM и обсудим некоторые модули, которые будем использовать в дальнейшем в процессе разработки.