Писать Node.js-приложения можно так, чтобы абсолютно весь код, обеспечивающий их функционирование, находился бы в одном .js-файле. Но при такой организации кода не используется модульный подход, когда фрагменты кода оформляют в виде отдельных файлов. Node.js даёт нам чрезвычайно простые механизмы для написания модульного кода.
Прежде чем мы перейдём к разговору об управлении зависимостями, поговорим о модулях. Что это такое? Зачем разработчику задумываться о неких фрагментах кода, вместо того, чтобы просто писать весь код в одном файле?
Если говорить по-простому, то модуль это код, собранный в одном файле для того, чтобы им было удобнее обмениваться с другими программистами и многократно использовать. Модули, в результате, позволяют нам разбивать сложные приложения на небольшие фрагменты. Это помогает улучшить понятность кода, упрощает поиск ошибок. Подробности о системах работы с модулями, применяемых в JavaScript-проектах, можно почитать здесь.
Node.js поддерживает разные способы работы с модулями. В частности, одна из них, основанная на CommonJS, предусматривает применение ключевого слова
require
. При её использовании, перед
тем, как некий функционал окажется доступным в программе, у
платформы нужно затребовать подключение этого функционала.Я исхожу из предположения о том, что вы уже владеете основами Node.js. Если нужно можете, прежде чем продолжать читать этот материал, посмотреть мою статью, посвящённую основам Node.js.
Подготовка приложения и эксперименты по экспорту и импорту
Начнём с простых вещей. Я создал директорию для проекта и, используя команду
npm init
, инициализировал проект.
Затем я создал два JavaScript-файла: app.js
и
appMsgs.js
. Ниже показан внешний вид структуры проекта
в VS Code. Этот проект мы будем использовать в роли отправной точки
наших экспериментов. Вы можете, прорабатывая этот материал, делать
всё сами, а можете упростить себе работу, воспользовавшись готовым
кодом. Его можно найти в репозитории, ссылку на который я приведу в
конце статьи.Структура базового проекта
В данный момент оба .js-файла пусты. Внесём в файл
appMsgs.js
следующий код:Экспорт значений простых типов и объектов в appMsgs.js
Тут можно видеть конструкцию
module.exports
. Она
используется для того, чтобы вывести во внешний мир некие сущности,
описанные в файле (они могут быть представлены простыми типами,
объектами, функциями), которыми потом можно воспользоваться в
других файлах. В нашем случае мы кое-что экспортируем из файла
appMsgs.js
, а пользоваться этим собираемся в
app.js
.В
app.js
воспользоваться тем, что экспортировано из
appMsgs.js
, можно, прибегнув к команде
require
:Импорт модуля appMsgs.js в app.js
Система, выполнив команду
require
, вернёт объект,
который будет представлять обособленный фрагмент кода, описанный в
файле appMsgs.js
. Мы назначаем этот объект переменной
appMsgs
, а затем просто пользуемся свойствами этого
объекта в вызовах console.log
. Ниже показан результат
выполнения кода app.js
.Выполнение app.js
Команда
require
выполняет код файла
appMsgs.js
и конструирует объект, дающий нам доступ к
функционалу, экспортируемому файлом appMsgs.js
.Это может быть функция-конструктор или обычная функция, это может быть объект с какими-то свойствами и методами или набор значений простых типов. Есть разные подходы к организации экспорта.
В результате оказывается, что мы, пользуясь конструкциями
require
и module.exports
, можем создавать
модульные приложения.При импорте модуля код этого модуля загружается и выполняется лишь один раз. Повторно этот код не выполняется. Получается, что если попытаться, повторно воспользовавшись
require
,
подключить к файлу модуль, который уже был к нему подключён, код
этого модуля ещё раз выполняться не будет, require
вернёт кешированную версию соответствующего объекта.Выше мы рассматривали экспорт объектов и значений простых типов. Посмотрим теперь на то, как экспортировать функции и как потом этими функциями пользоваться. Уберём из
appMsgs.js
старый код и введём в него следующее:Экспорт функции из appMsgs.js
Теперь мы экспортируем из
appMsgs.js
функцию. Код этой
функции выполняется каждый раз, когда код, импортировавший её, её
вызывает.Попробуем воспользоваться этой функцией в
app.js
,
приведя код этого файла к следующему виду:Использование импортированной функции в app.js
Тут мы пользуемся тем, что, после экспорта, попадает в переменную
appMsgs
, как функцией. В результате оказывается, что
каждый раз, когда мы вызываем импортированную функцию, её код
выполняется.Вот результат запуска этого кода:
Выполнение app.js
Мы рассмотрели два подхода к использованию
module.exports
. Ещё одним способом применения
module.exports
является экспорт функций-конструкторов,
используемых, с ключевым словом new
, для создания
объектов. Рассмотрим пример:Экспорт функции-конструктора из appMsgs.js
А вот обновлённый код
app.js
, в котором используется
импортированная функция-конструктор:Использование в app.js функции-конструктора, импортированной из appMsgs.js
Тут всё, в целом, выглядит так же, как если бы мы создали функцию-конструктор в коде, а потом воспользовались бы ей.
Вот что получится, если выполнить новый вариант
app.js
:Выполнение app.js
Я добавил в проект файл
userRepo.js
и внёс в него
следующий код:Файл userRepo.js
Вот файл
app.js
, в котором используется то, что
экспортировано из userRepo.js
:Использование в app.js того, что экспортировано из userRepo.js
Запустим
app.js
:Выполнение app.js
Команду
require
достаточно часто используют для
подключения к файлам с кодом других файлов, но существует и другой
подход к использованию require
, предусматривающий
импорт в файлы директорий, содержащих особым образом оформленные
файлы.Импорт директорий
Давайте ненадолго вернёмся к тому, о чём мы уже говорили. Вспомним о том, как
require
используется для импорта
зависимостей:
var appMsgs = require("./appMsgs")
Node.js, выполняя эту команду, будет искать файл
appMsgs.js
, но систему будет интересовать и директория
appMsgs
. То, что она найдёт первым, она и
импортирует.Теперь давайте посмотрим на код, в котором используется эта возможность.
Я создал папку
logger
, а в ней файл
index.js
. В этот файл я поместил следующий код:Код файла index.js из папки logger
А вот файл
app.js
, в котором команда
require
используется для импорта этого модуля:Файл app.js, в котором require передаётся не имя файла, а имя папки
В данном случае можно было бы воспользоваться такой командой:
var logger = require("./logger/index.js")
Эта совершенно правильная конструкция, она позволила бы нам импортировать в код нужный модуль. Но вместо этого мы пользуемся такой командой:
var logger = require("./logger")
Так как система не может обнаружить файл
logger.js
,
она ищет соответствующую папку. По умолчанию импортируется файл
index.js
, являющейся точкой входа в модуль. Именно
поэтому я и дал .js-файлу, находящемуся в папке, имя
index.js
.Попробуем теперь выполнить код
app.js
:Выполнение app.js
Тут у вас может появиться мысль о том, зачем усложнять себе жизнь, создавая, вместо единственного файла, папку и файл, расположенный в ней.
Причина в том, что при таком подходе можно собрать в одной папке файлы-зависимости того модуля, который нужен в нашем коде. У этих зависимостей могут быть и собственные зависимости. В результате получится довольно сложная конструкция, о которой не нужно знать тому коду, который нуждается лишь в том функционале, который даёт ему модуль. В нашем случае речь идёт о модуле
logger
.Это разновидность инкапсуляции. Получается, что, разрабатывая достаточно сложные модули, мы можем разбивать их на части, расположенные в нескольких файлах. А код, являющийся потребителем модуля, имеет дело лишь с единственным файлом. Это говорит о том, что применение папок это хороший способ управления подобными зависимостями.
Npm
Мы кратко поговорим и о ещё одном аспекте работы с зависимостями в Node.js. Это npm (Node Package Manager, менеджер пакетов Node.js). Вы, вероятно, уже знакомы с npm. Если кратко описать его суть, то окажется, что он даёт разработчикам простой механизм для включения в их проекты необходимого им функционала, оформленного в виде npm-пакетов.
Установить нужную зависимость с помощью npm (в данном случае библиотеку
underscore
) можно так:
npm install underscore
Потом эту библиотеку можно подключить в коде с помощью
require
:Импорт библиотеки, установленной с помощью npm
На предыдущем рисунке показан процесс работы с тем, что оказалось в нашем распоряжении после импорта пакета
underscore
.
Обратите внимание на то, что при импорте этого модуля путь к файлу
не указывают. Используют лишь имя модуля. Node.js загружает его из
папки node_modules
, находящейся в директории
приложения.Пример использования underscore в app.js
Выполним этот код.
Выполнение app.js
Итоги
Мы поговорили о работе с зависимостями в Node.js, рассмотрели несколько распространённых приёмов написания модульного кода. Вот репозиторий, в котором можно найти приложение, с которым мы экспериментировали.
Применяете ли вы модульный подход при работе над своими Node.js-проектами?