[Перевод] Управление зависимостями в Node.js

Управление зависимостями — это часть повседневной работы Node.js-программиста. Сегодня мы поговорим о разных подходах к работе с зависимостями в Node.js, и о том, как система загружает и обрабатывает зависимости.

Писать Node.js-приложения можно так, чтобы абсолютно весь код, обеспечивающий их функционирование, находился бы в одном .js-файле. Но при такой организации кода не используется модульный подход, когда фрагменты кода оформляют в виде отдельных файлов. Node.js даёт нам чрезвычайно простые механизмы для написания модульного кода.

gxgaogf6cgbo1kitv9ducuvahum.jpeg

Прежде чем мы перейдём к разговору об управлении зависимостями, поговорим о модулях. Что это такое? Зачем разработчику задумываться о неких «фрагментах кода», вместо того, чтобы просто писать весь код в одном файле?
Если говорить по-простому, то модуль — это код, собранный в одном файле для того, чтобы им было удобнее обмениваться с другими программистами и многократно использовать. Модули, в результате, позволяют нам разбивать сложные приложения на небольшие фрагменты. Это помогает улучшить понятность кода, упрощает поиск ошибок. Подробности о системах работы с модулями, применяемых в JavaScript-проектах, можно почитать здесь.

Node.js поддерживает разные способы работы с модулями. В частности, одна из них, основанная на CommonJS, предусматривает применение ключевого слова require. При её использовании, перед тем, как некий функционал окажется доступным в программе, у платформы нужно затребовать подключение этого функционала.

Я исхожу из предположения о том, что вы уже владеете основами Node.js. Если нужно — можете, прежде чем продолжать читать этот материал, посмотреть мою статью, посвящённую основам Node.js.

Подготовка приложения и эксперименты по экспорту и импорту


Начнём с простых вещей. Я создал директорию для проекта и, используя команду npm init, инициализировал проект. Затем я создал два JavaScript-файла: app.js и appMsgs.js. Ниже показан внешний вид структуры проекта в VS Code. Этот проект мы будем использовать в роли отправной точки наших экспериментов. Вы можете, прорабатывая этот материал, делать всё сами, а можете упростить себе работу, воспользовавшись готовым кодом. Его можно найти в репозитории, ссылку на который я приведу в конце статьи.

710864339f3692a94ee1939e36bccab1.pngСтруктура базового проекта

В данный момент оба .js-файла пусты. Внесём в файл appMsgs.js следующий код:

590f9940cfe7c8494aae3edc7232164a.png
Экспорт значений простых типов и объектов в appMsgs.js

Тут можно видеть конструкцию module.exports. Она используется для того, чтобы вывести во внешний мир некие сущности, описанные в файле (они могут быть представлены простыми типами, объектами, функциями), которыми потом можно воспользоваться в других файлах. В нашем случае мы кое-что экспортируем из файла appMsgs.js, а пользоваться этим собираемся в app.js.

В app.js воспользоваться тем, что экспортировано из appMsgs.js, можно, прибегнув к команде require:

ce7a1d1c6aa44f7c08d87a2b285ce371.png
Импорт модуля appMsgs.js в app.js

Система, выполнив команду require, вернёт объект, который будет представлять обособленный фрагмент кода, описанный в файле appMsgs.js. Мы назначаем этот объект переменной appMsgs, а затем просто пользуемся свойствами этого объекта в вызовах console.log. Ниже показан результат выполнения кода app.js.

d129130244b6f2c34e177f753bdc883d.png
Выполнение app.js

Команда require выполняет код файла appMsgs.js и конструирует объект, дающий нам доступ к функционалу, экспортируемому файлом appMsgs.js.

Это может быть функция-конструктор или обычная функция, это может быть объект с какими-то свойствами и методами или набор значений простых типов. Есть разные подходы к организации экспорта.

В результате оказывается, что мы, пользуясь конструкциями require и module.exports, можем создавать модульные приложения.

При импорте модуля код этого модуля загружается и выполняется лишь один раз. Повторно этот код не выполняется. Получается, что если попытаться, повторно воспользовавшись require, подключить к файлу модуль, который уже был к нему подключён, код этого модуля ещё раз выполняться не будет, require вернёт кешированную версию соответствующего объекта.

Выше мы рассматривали экспорт объектов и значений простых типов. Посмотрим теперь на то, как экспортировать функции и как потом этими функциями пользоваться. Уберём из appMsgs.js старый код и введём в него следующее:

cb50d076120a01b51b1e96765d45fe7d.png
Экспорт функции из appMsgs.js

Теперь мы экспортируем из appMsgs.js функцию. Код этой функции выполняется каждый раз, когда код, импортировавший её, её вызывает.

Попробуем воспользоваться этой функцией в app.js, приведя код этого файла к следующему виду:

91dbfbb40883e25094ecc8196a004130.png
Использование импортированной функции в app.js

Тут мы пользуемся тем, что, после экспорта, попадает в переменную appMsgs, как функцией. В результате оказывается, что каждый раз, когда мы вызываем импортированную функцию, её код выполняется.

Вот результат запуска этого кода:

984004943608c2361633b8e045994a57.png
Выполнение app.js

Мы рассмотрели два подхода к использованию module.exports. Ещё одним способом применения module.exports является экспорт функций-конструкторов, используемых, с ключевым словом new, для создания объектов. Рассмотрим пример:

9831450d26745e8a83802ef74a7978bc.png
Экспорт функции-конструктора из appMsgs.js

А вот — обновлённый код app.js, в котором используется импортированная функция-конструктор:

14ad332a7a2104b0d1e0c1eef6ce945f.pngИспользование в app.js функции-конструктора, импортированной из appMsgs.js

Тут всё, в целом, выглядит так же, как если бы мы создали функцию-конструктор в коде, а потом воспользовались бы ей.

Вот что получится, если выполнить новый вариант app.js:

7a98dceae494be1f3f7e0d203dbd2531.png
Выполнение app.js

Я добавил в проект файл userRepo.js и внёс в него следующий код:

d8826be78552785b41f65ab887f264d5.png
Файл userRepo.js

Вот — файл app.js, в котором используется то, что экспортировано из userRepo.js:

fa88a6e5dfd87913d053992ce5cb0581.png
Использование в app.js того, что экспортировано из userRepo.js

Запустим app.js:

b35c04ffc07838e0e0735abbc98a954b.png
Выполнение app.js

Команду require достаточно часто используют для подключения к файлам с кодом других файлов, но существует и другой подход к использованию require, предусматривающий импорт в файлы директорий, содержащих особым образом оформленные файлы.

Импорт директорий


Давайте ненадолго вернёмся к тому, о чём мы уже говорили. Вспомним о том, как require используется для импорта зависимостей:
var appMsgs = require("./appMsgs")

Node.js, выполняя эту команду, будет искать файл appMsgs.js, но систему будет интересовать и директория appMsgs. То, что она найдёт первым, она и импортирует.

Теперь давайте посмотрим на код, в котором используется эта возможность.

Я создал папку logger, а в ней — файл index.js. В этот файл я поместил следующий код:

5fc0f936b702de8cc83aaa567daf1e3a.png
Код файла index.js из папки logger

А вот — файл app.js, в котором команда require используется для импорта этого модуля:

f88aa8ec67743a315942d1fab0533680.png
Файл app.js, в котором require передаётся не имя файла, а имя папки

В данном случае можно было бы воспользоваться такой командой:

var logger = require("./logger/index.js")

Эта — совершенно правильная конструкция, она позволила бы нам импортировать в код нужный модуль. Но вместо этого мы пользуемся такой командой:
var logger = require("./logger")

Так как система не может обнаружить файл logger.js, она ищет соответствующую папку. По умолчанию импортируется файл index.js, являющейся точкой входа в модуль. Именно поэтому я и дал .js-файлу, находящемуся в папке, имя index.js.

Попробуем теперь выполнить код app.js:

ff2d8225dfc3786d12e6c16fe999fcbc.png
Выполнение app.js

Тут у вас может появиться мысль о том, зачем усложнять себе жизнь, создавая, вместо единственного файла, папку и файл, расположенный в ней.

Причина в том, что при таком подходе можно собрать в одной папке файлы-зависимости того модуля, который нужен в нашем коде. У этих зависимостей могут быть и собственные зависимости. В результате получится довольно сложная конструкция, о которой не нужно знать тому коду, который нуждается лишь в том функционале, который даёт ему модуль. В нашем случае речь идёт о модуле logger.

Это — разновидность инкапсуляции. Получается, что, разрабатывая достаточно сложные модули, мы можем разбивать их на части, расположенные в нескольких файлах. А код, являющийся потребителем модуля, имеет дело лишь с единственным файлом. Это говорит о том, что применение папок — это хороший способ управления подобными зависимостями.

Npm


Мы кратко поговорим и о ещё одном аспекте работы с зависимостями в Node.js. Это — npm (Node Package Manager, менеджер пакетов Node.js). Вы, вероятно, уже знакомы с npm. Если кратко описать его суть, то окажется, что он даёт разработчикам простой механизм для включения в их проекты необходимого им функционала, оформленного в виде npm-пакетов.

Установить нужную зависимость с помощью npm (в данном случае — библиотеку underscore) можно так:

npm install underscore

Потом эту библиотеку можно подключить в коде с помощью require:

9cd63dc92b894da59d75ad63523e2fd8.png
Импорт библиотеки, установленной с помощью npm

На предыдущем рисунке показан процесс работы с тем, что оказалось в нашем распоряжении после импорта пакета underscore. Обратите внимание на то, что при импорте этого модуля путь к файлу не указывают. Используют лишь имя модуля. Node.js загружает его из папки node_modules, находящейся в директории приложения.

2a90c90773e465bda74ee1348f9ec1dc.png
Пример использования underscore в app.js

Выполним этот код.

fda88246ce25ee3e28d195458e240ee3.png
Выполнение app.js

Итоги


Мы поговорили о работе с зависимостями в Node.js, рассмотрели несколько распространённых приёмов написания модульного кода. Вот репозиторий, в котором можно найти приложение, с которым мы экспериментировали.

Применяете ли вы модульный подход при работе над своими Node.js-проектами?

72c231bb14cabc532efed2135b0126e7.jpg

oug5kh6sjydt9llengsiebnp40w.png

© Habrahabr.ru