Модули в JavaScript

Модуль — часть кода (пространство имен, класс, метод, блок кода), которая инкапсулирует детали реализации и предоставляет открытый API для использования другим кодом. Инкапсуляция подразумевает сокрытие внутренней структуры данных и реализации методов объекта от остальной программы. Однако при этом другим объектам доступен интерфейс объекта, через который осуществляется всё взаимодействие с ним.

По сути, модуль — это блок кода или инструкция (statement), которая вызывается:

  • прямо, при «запуске» файла программистом, или
  • косвенно, в результате импорта другим модулем.

Достоинства модулей:

  1. Удобная поддержка (maintainability): хорошо спроектированный модуль призван уменьшить взаимозависимость частей кодовой базы. Обновить один модуль с определенной функциональностью гораздо проще, когда он отделён от других частей кода.
  2. "Чистое" пространство имён (namespacing): в JavaScript переменные, которые находятся за пределами функций верхнего уровня, считаются глобальными, что может привести к "загрязнению пространства имён (namespace pollution)" и поломке логики программы. Модули позволяют избежать загрязнения глобального пространства имён путём создания приватных пространств для переменных.
  3. Повторное использование (reusability): повторное использование ранее написанного модуля с определенной функциональностью.

Это привело к появлению в JS сначала паттерна программирования «Модуль», а затем и отдельных форматов: CommonJS, AMD, UMD, и специальных инструментов для работы с ними.

Нативная система модулей в JavaScript добавилась в спецификации ECMAScript 6.

Степень взаимосвязи элементов модуля (пространства имен, класса, метода, блока кода) характеризуется связанность.

Подробнее о связанности

Есть два вида связанности:

  1. сильная (предпочтительнее, предполагает, что элементы модуля должны фокусироваться исключительно на одной задаче) и
  2. слабая.

Модули в JavaScript: связанность

Сильная связанность позволяет модулю быть:

  1. сфокусированным и понятным (легче понять, какую задачу выполняет модуль);
  2. легко поддерживаемым и поддающимся рефакторингу (изменение модуля влияет на меньшее количество элементов);
  3. многоразовым (если модуль ориентирован на выполнение одной задачи, то его легче использовать повторно);
  4. простым для тестирования (проще тестировать модуль, ориентированный на одну задачу).

Сильная связность, сопровождающаяся слабым зацеплением, является характеристикой хорошо спроектированной системы.

Так как блок кода сам по себе может считаться небольшим модулем, то чтобы извлечь выгоду из преимуществ сильной связанности, нужно держать переменные как можно ближе к блоку кода, который их использует. Например, если переменная нужна только для использования в определенном блоке кода, тогда можно объявить её и разрешить существовать только внутри нужного блока (используя const или let для объявления).

Подробнее о связанности

[свернуть]

Модуль обычно содержит класс или библиотеку с функциями. В ES6 соблюдается соотношение: один файл — один модуль. Каждый модуль имеет отдельную область видимости (Lexical environment) — т. е. все объявления переменных, функций и классов не будут доступны за пределами модуля (файла), если не экспортированы явно.

Модуль на основе немедленно вызываемой функции (IIFE)

Шаблон «Модуль» основывается на немедленно вызываемой функции (IIFE - Immediately (немедленно) Invoked (вызываемое) Function (функциональное) Expression (выражение)):

Немедленно вызываемая функция образует локальную область видимости, в которой можно объявить необходимые приватные свойства и методы. Результат исполнения функции — объект, содержащий публичные свойства и методы.

Например:

oneModule() — это функция, которая при вызове создает объект модуля. Без её выполнения (как внешней функции) не произойдет ни создания внутренней области видимости, ни создания замыканий.

После вызова функция oneModule() возвращает объект, выполненный в синтаксисе объектного литерала { key: value, ... }.

У объекта, который возвращает oneModule(), есть ссылки на внутренние функции doSomething() и doAnother(), но не на внутренние переменные something и another. Модуль хранит их скрытыми (приватными).

Возвращаемое функцией oneModule() значение (объект) по существу является  публичным API нашего модуля, которое в итоге и присваивается внешней переменной foo. Доступ к методам в API осуществляется через вызовы foo.doSomething() и foo.doAnother().

Ещё один пример:

Сохраняя внутреннюю ссылку на объект публичного API внутри экземпляра модуля вы можете менять эту ссылку на модуль изнутри, включая добавление и удаление методов, свойств и изменение их значений.

[свернуть]
Модуль как функция-конструктор

С помощью функций-конструкторов удобно создавать экземпляры одного «класса»:

[свернуть]
Модуль на основе блока кода { ... }

В ECMAScript 6 const и let являются блочными или локальными (видны только внутри блока с областью видимости, ограниченной текущим блоком кода), поэтому вы можете использовать просто область блока, ограниченную {...} для реализации шаблона модуля:

[свернуть]
Что такое API

API (программный интерфейс приложения, интерфейс прикладного программирования) (англ. application programming interface, API [эй-пи-ай]) — описание способов (набор классов, процедур, функций, структур или констант), которыми одна компьютерная программа может взаимодействовать с другой программой.

[свернуть]

Использующиеся форматы модулей JS

Отсюда...

Формат модуля — это синтаксис определения и подключения модуля.

CommonJS

С точки зрения структуры модуль CommonJS — это часть JavaScript-кода, которая экспортирует определенные переменные, объекты или функции, делая их доступными для любого зависимого кода.

Код модуля CommonJS состоит из двух частей:

  1. module.exports содержит объекты, которые модуль хочет сделать доступными;
  2. функция require() используется для импорта других модулей.

Пример модуля в формате CommonJS:

Пример кода, использующего модуль:

В CommonJS наибольшее внимание было уделено спецификации модулей, которые в конечном итоге были реализованы в Node.JS.

Вы можете явно отметить свой код как CommonJS следующими признаками:

  1. файлы с расширением .cjs;
  2. файлы с расширением .js или без расширения вообще, при условии что ближайший родительский package.json содержит значение "type": "commonjs";
  3. в теге скрипта указана строка type="commonjs" (например, <script type="commonjs" src="index.js"></script>);
  4. код, переданный через аргумент --eval или STDIN c явным флагом --input-type=commonjs.

[свернуть]

AMD

В основе формата AMD (Asynchronous Module Definition) лежат две функции:

  1. define() для определения именованных или безымянных модулей и
  2. require() для импорта зависимостей.

Функция define() имеет следующую сигнатуру:

Параметр module_id необязательный, он обычно требуется только при использовании не-AMD инструментов объединения. Когда этот аргумент опущен, модуль называется анонимным. Параметр dependencies представляет собой массив зависимостей, которые требуются определяемому модулю, а третий аргумент (definition function) — это функция, которая выполняется для создания экземпляра модуля.

Пример модуля:

Функция require() используется для импорта модулей:

Также с помощью require() можно динамически импортировать зависимости в модуль:

[свернуть]

UMD

Существование двух форматов модулей, несовместимых друг с другом, не способствовало развитию экосистемы JavaScript. Для решения этой проблемы был разработан формат UMD (Universal Module Definition). Этот формат позволяет использовать один и тот же модуль и с инструментами AMD, и в средах CommonJS.

Суть подхода UMD заключается в проверке поддержки того или иного формата и объявлении модуля соответствующим образом. Пример такой реализации:

UMD — это скорее подход, а не конкретный формат. Различных реализаций может быть множество.

[свернуть]

Модули формата ECMAScript 2015

Подробнее о модулях ECMAScript 2015 и Node.JS https://nodejs.org/docs/latest/api/esm.html

В стандарте ECMAScript 2015 появились нативные модули JavaScript, которые могут загружать друг друга и использовать директивы export и import, чтобы обмениваться функциональностью, вызывая функции одного модуля из другого:

  • export отмечает переменные и функции, которые должны быть доступны вне текущего модуля;
  • import позволяет импортировать функциональность из других модулей.

Любая переменная, объявленная в модуле, доступна за его пределами, только если явно экспортирована из модуля.

Node.js будет обрабатывать код как ES-модули в следующих случаях:

  1. файлы с расширением .mjs;
  2. файлы с расширением .js или без расширения вообще, при условии что ближайший к ним родительский package.json содержит значение "type": "module";
  3. в теге скрипта указана строка type = "module" (например, <script type="module" src="index.js"></script>);
  4. код, переданный через аргумент --eval или STDIN, вместе с флагом --input-type=module;

Во всех остальных случаях код будет считаться CommonJS, поэтому целесообразно указывать тип модулей явным образом.

Если модуль экспортирует только одно значение, можно использовать экспорт по умолчанию. Например:

Один модуль может экспортировать несколько значений:

Можно перечислить все, что вы хотите экспортировать, в конце модуля:

И переименовать:

Импортировать модули также можно несколькими способами:

Особенности модулей ECMAScript 2015:

  • Модули могут загружать друг друга и обмениваться функциональностью, вызывая функции одного модуля из другого.
  • Каждый модуль имеет свою собственную область видимости (переменные и функции, объявленные в модуле, не видны в других скриптах).
  • Код в модуле выполняется только один раз при импорте. Если один и тот же модуль используется в нескольких местах, то его код выполнится только один раз, после чего экспортируемая функциональность передаётся всем импортёрам.
  • Код модуля выполняется в строгом режиме ('use strict').
  • В модуле на верхнем уровне this не определён (undefined).
  • Ключевые слова import и export могут использоваться только на верхнем уровне, их нельзя использовать в функции или в блоке:

  • Импорт из модуля поднимается в начало области видимости:

  • Модули всегда выполняются в отложенном режиме, как и скрипты с атрибутом defer , т.е.:
    1. загрузка внешних модулей, таких как <script type="module" src="...">, не блокирует обработку HTML;
    2. модули, даже если загрузились быстро, ожидают полной загрузки HTML документа, и только затем выполняются;
    3. сохраняется относительный порядок скриптов: скрипты, которые идут раньше в документе, выполняются раньше;
    4. модули всегда видят полностью загруженную HTML-страницу, включая элементы под ними.
  • Для модулей атрибут async работает на любых скриптах: модуль выполнится сразу после загрузки, не ожидая других скриптов, даже если HTML документ ещё не будет загружен, например:

  • В браузере import должен содержать относительный или абсолютный путь к модулю (модули без пути называются «голыми» (bare) не разрешены в import):

Загрузчики модулей JS

AMD и CommonJS — это форматы модулей, а не реализации. Для поддержки AMD, например, необходима реализация функций define() и require(), для поддержки CommonJS — реализация module.exports и require().

Для поддержки модулей во время выполнения используются загрузчики модулей, которые имеют следующий принцип работы:

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

Популярные загрузчики модулей:

  • RequireJS загружает модули в формате AMD.
  • curl.js загружает модули AMD и CommonJS.
  • SystemJS загружает модули AMD и CommonJS.

Сборщики модулей JS

В отличие от загрузчиков модулей, которые работают в браузере и загружают зависимости «на лету», сборщики модулей позволяют заранее подготовить один файл со всеми зависимостями (например, bundle.js, бандл).

Существует ряд инструментов, позволяющих заранее собирать модули в один файл:

  1. Browserify поддерживает формат CommonJS;
  2. Webpack поддерживает AMD, CommonJS и ES6 модули;
  3. Rollup поддерживает ES6 модули.

Для работы сборщика webpack необходимо установить:

  1. платформу Node.JS (https://nodejs.org) - подробнее читай Node.js как среда выполнения JS;
  2. терминал Git Bash (приложение для сред Microsoft Windows, которое предоставляет эмуляцию bash (командной оболочки), используемую для запуска Git из командной строки) (https://git-scm.com/) - подробнее читай Git Bash в VS Code.

Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.