[Перевод] Управление зависимостями в Node.js
Писать 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-проектами?