...

пятница, 4 июня 2021 г.

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

Управление зависимостями — это часть повседневной работы Node.js-программиста. Сегодня мы поговорим о разных подходах к работе с зависимостями в 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-проектами?


Adblock test (Why?)

Комментариев нет:

Отправить комментарий