Модуль — часть кода (пространство имен, класс, метод, блок кода), которая инкапсулирует детали реализации и предоставляет открытый API для использования другим кодом. Инкапсуляция подразумевает сокрытие внутренней структуры данных и реализации методов объекта от остальной программы. Однако при этом другим объектам доступен интерфейс объекта, через который осуществляется всё взаимодействие с ним.
По сути, модуль — это блок кода или инструкция (statement), которая вызывается:
- прямо, при «запуске» файла программистом, или
- косвенно, в результате импорта другим модулем.
Достоинства модулей:
- Удобная поддержка (maintainability): хорошо спроектированный модуль призван уменьшить взаимозависимость частей кодовой базы. Обновить один модуль с определенной функциональностью гораздо проще, когда он отделён от других частей кода.
- "Чистое" пространство имён (namespacing): в JavaScript переменные, которые находятся за пределами функций верхнего уровня, считаются глобальными, что может привести к "загрязнению пространства имён (namespace pollution)" и поломке логики программы. Модули позволяют избежать загрязнения глобального пространства имён путём создания приватных пространств для переменных.
- Повторное использование (reusability): повторное использование ранее написанного модуля с определенной функциональностью.
Это привело к появлению в JS сначала паттерна программирования «Модуль», а затем и отдельных форматов: CommonJS, AMD, UMD, и специальных инструментов для работы с ними.
Нативная система модулей в JavaScript добавилась в спецификации ECMAScript 6.
Степень взаимосвязи элементов модуля (пространства имен, класса, метода, блока кода) характеризуется связанность.
Есть два вида связанности:
- сильная (предпочтительнее, предполагает, что элементы модуля должны фокусироваться исключительно на одной задаче) и
- слабая.
Сильная связанность позволяет модулю быть:
- сфокусированным и понятным (легче понять, какую задачу выполняет модуль);
- легко поддерживаемым и поддающимся рефакторингу (изменение модуля влияет на меньшее количество элементов);
- многоразовым (если модуль ориентирован на выполнение одной задачи, то его легче использовать повторно);
- простым для тестирования (проще тестировать модуль, ориентированный на одну задачу).
Сильная связность, сопровождающаяся слабым зацеплением, является характеристикой хорошо спроектированной системы.
Так как блок кода сам по себе может считаться небольшим модулем, то чтобы извлечь выгоду из преимуществ сильной связанности, нужно держать переменные как можно ближе к блоку кода, который их использует. Например, если переменная нужна только для использования в определенном блоке кода, тогда можно объявить её и разрешить существовать только внутри нужного блока (используя const или let для объявления).
Модуль обычно содержит класс или библиотеку с функциями. В ES6 соблюдается соотношение: один файл — один модуль. Каждый модуль имеет отдельную область видимости (Lexical environment) — т. е. все объявления переменных, функций и классов не будут доступны за пределами модуля (файла), если не экспортированы явно.
Шаблон «Модуль» основывается на немедленно вызываемой функции (IIFE - Immediately (немедленно) Invoked (вызываемое) Function (функциональное) Expression (выражение)):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let MODULE = (function() { let privateVariable = 1; function privateMethod() { // ... } // внешний API return { moduleProperty: 1, moduleMethod: function() { // ... } }; })(); |
Немедленно вызываемая функция образует локальную область видимости, в которой можно объявить необходимые приватные свойства и методы. Результат исполнения функции — объект, содержащий публичные свойства и методы.
Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
let foo = (function oneModule() { // внутренняя логика let something = "cool"; let another = [1, 2, 3]; function doSomething() { console.log(something); } function doAnother() { console.log(another.join(" ! ")); } // внешний API return { doSomething: doSomething, doAnother: doAnother }; })(); foo.doSomething(); // cool foo.doAnother(); // 1 ! 2 ! 3 |
oneModule() — это функция, которая при вызове создает объект модуля. Без её выполнения (как внешней функции) не произойдет ни создания внутренней области видимости, ни создания замыканий.
После вызова функция oneModule() возвращает объект, выполненный в синтаксисе объектного литерала { key: value, ... }.
У объекта, который возвращает oneModule(), есть ссылки на внутренние функции doSomething() и doAnother(), но не на внутренние переменные something и another. Модуль хранит их скрытыми (приватными).
Возвращаемое функцией oneModule() значение (объект) по существу является публичным API нашего модуля, которое в итоге и присваивается внешней переменной foo. Доступ к методам в API осуществляется через вызовы foo.doSomething() и foo.doAnother().
Ещё один пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
let mod = (function CoolModule(id) { function change() { publicAPI.identify = identify2; // изменение публичного API } function identify1() { console.log(id); } function identify2() { console.log(id.toUpperCase()); } let publicAPI = { change: change, identify: identify1 }; return publicAPI; })("mod module"); mod.identify(); // mod module mod.change(); mod.identify(); // MOD MODULE |
Сохраняя внутреннюю ссылку на объект публичного API внутри экземпляра модуля вы можете менять эту ссылку на модуль изнутри, включая добавление и удаление методов, свойств и изменение их значений.
С помощью функций-конструкторов удобно создавать экземпляры одного «класса»:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Объявление модуля let Module = function () { // внутренняя логика модуля function sayHello() { console.log("Hello"); } function sayBy() { console.log("By"); } // внешнее API return { sayHello: sayHello, sayBy: sayBy, }; }; // Инициализируем модуль для доступа к внешнему API let module = new Module(); console.log(module) // {sayHello: ƒ, sayBy: ƒ} module.sayHello(); // Hello module.sayBy(); // By |
В ECMAScript 6 const и let являются блочными или локальными (видны только внутри блока с областью видимости, ограниченной текущим блоком кода), поэтому вы можете использовать просто область блока, ограниченную {...} для реализации шаблона модуля:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{ const foo = "bar"; // область видимости переменных - блок кода let counter = 0; // this === window на верхнем уровне, в браузере this.someGlobalModule = () => { counter++; return { foo, counter }; }; } // доступ к foo или counter только через someGlobalModule() let val = someGlobalModule(); console.log(val.foo === "bar"); // true console.log(val.counter === 1); // true |
API (программный интерфейс приложения, интерфейс прикладного программирования) (англ. application programming interface, API [эй-пи-ай]) — описание способов (набор классов, процедур, функций, структур или констант), которыми одна компьютерная программа может взаимодействовать с другой программой.
Использующиеся форматы модулей JS
Формат модуля — это синтаксис определения и подключения модуля.
С точки зрения структуры модуль CommonJS — это часть JavaScript-кода, которая экспортирует определенные переменные, объекты или функции, делая их доступными для любого зависимого кода.
Код модуля CommonJS состоит из двух частей:
- module.exports содержит объекты, которые модуль хочет сделать доступными;
- функция require() используется для импорта других модулей.
Пример модуля в формате CommonJS:
1 2 3 4 5 6 7 8 9 10 11 12 |
// COMMONJS определяем функцию, которую хотим экспортировать function foobar() { this.foo = function() { console.log("Hello foo"); }; this.bar = function() { console.log("Hello bar"); }; } // делаем ее доступной для других модулей module.exports.foobar = foobar; |
Пример кода, использующего модуль:
1 2 3 4 |
var foobar = require("./foobar").foobar, test = new foobar(); test.bar(); // 'Hello bar' |
В CommonJS наибольшее внимание было уделено спецификации модулей, которые в конечном итоге были реализованы в Node.JS.
Вы можете явно отметить свой код как CommonJS следующими признаками:
- файлы с расширением .cjs;
- файлы с расширением .js или без расширения вообще, при условии что ближайший родительский package.json содержит значение "type": "commonjs";
- в теге скрипта указана строка type="commonjs" (например, <script type="commonjs" src="index.js"></script>);
- код, переданный через аргумент --eval или STDIN c явным флагом --input-type=commonjs.
В основе формата AMD (Asynchronous Module Definition) лежат две функции:
- define() для определения именованных или безымянных модулей и
- require() для импорта зависимостей.
Функция define() имеет следующую сигнатуру:
1 2 3 4 5 |
define( module_id /*необязательный*/, [dependencies] /*необязательный*/, definition function /*функция для создания экземпляра модуля или объекта*/ ); |
Параметр module_id необязательный, он обычно требуется только при использовании не-AMD инструментов объединения. Когда этот аргумент опущен, модуль называется анонимным. Параметр dependencies представляет собой массив зависимостей, которые требуются определяемому модулю, а третий аргумент (definition function) — это функция, которая выполняется для создания экземпляра модуля.
Пример модуля:
1 2 3 4 5 6 7 8 9 10 11 12 |
define( "myModule", ["foo", "bar"], function(foo, bar) { // зависимости (foo и bar) передаются в функцию // создаем модуль var myModule = { doStuff: function() { console.log("Hi!"); } }; return myModule; // возвращаем модуль }); |
Функция require() используется для импорта модулей:
1 2 3 |
require(["foo", "bar"], function(foo, bar) { foo.doSomething(); }); |
Также с помощью require() можно динамически импортировать зависимости в модуль:
1 2 3 4 5 6 7 8 9 10 |
define(function(require) { var foobar; require(["foo", "bar"], function(foo, bar) { foobar = foo() + bar(); }); // возвращаем модуль, обратите внимание на другой шаблон определения модуля return { foobar: foobar }; }); |
Существование двух форматов модулей, несовместимых друг с другом, не способствовало развитию экосистемы JavaScript. Для решения этой проблемы был разработан формат UMD (Universal Module Definition). Этот формат позволяет использовать один и тот же модуль и с инструментами AMD, и в средах CommonJS.
Суть подхода UMD заключается в проверке поддержки того или иного формата и объявлении модуля соответствующим образом. Пример такой реализации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
(function(define) { define(function() { var bar = "foo"; return { foo: function() { // ... } }; }); })( typeof module === "object" && module.exports && typeof define !== "function" ? function(factory) { module.exports = factory(); } : define ); |
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-модули в следующих случаях:
- файлы с расширением .mjs;
- файлы с расширением .js или без расширения вообще, при условии что ближайший к ним родительский package.json содержит значение "type": "module";
- в теге скрипта указана строка type = "module" (например, <script type="module" src="index.js"></script>);
- код, переданный через аргумент --eval или STDIN, вместе с флагом --input-type=module;
Во всех остальных случаях код будет считаться CommonJS, поэтому целесообразно указывать тип модулей явным образом.
Если модуль экспортирует только одно значение, можно использовать экспорт по умолчанию. Например:
1 2 3 4 5 6 7 8 |
// модуль экспортирует функцию: export default function () { ··· } // модуль экспортирует класс: export default class { ··· } // модуль экспортирует выражение: export default 5 * 7; |
Один модуль может экспортировать несколько значений:
1 2 3 4 5 6 7 |
export const pi = Math.PI; export function sum(x, y) { return x + y; } export function multiply(x, y) { return x * y; } |
Можно перечислить все, что вы хотите экспортировать, в конце модуля:
1 2 3 4 5 |
export const pi = Math.PI; export function sum(x, y) { return x + y; } export { pi, sum }; |
И переименовать:
1 |
export { pi as PI, sum }; |
Импортировать модули также можно несколькими способами:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Импорт значения по умолчанию import localName from 'utils'; // Импорт отдельных функций import { sum, multiply } from 'utils'; sum(4, 3); // Импорт всего модуля import * as utils from 'utils'; utils.sum(4, 3); // Можно переименовать импортируемое значение import { pi as PI, sum } from 'utils'; // Или не импортировать ничего (в этом случае выполнится код инициализации модуля, // но ничего не будет импортировано) import 'utils'; |
Особенности модулей ECMAScript 2015:
- Модули могут загружать друг друга и обмениваться функциональностью, вызывая функции одного модуля из другого.
- Каждый модуль имеет свою собственную область видимости (переменные и функции, объявленные в модуле, не видны в других скриптах).
- Код в модуле выполняется только один раз при импорте. Если один и тот же модуль используется в нескольких местах, то его код выполнится только один раз, после чего экспортируемая функциональность передаётся всем импортёрам.
- Код модуля выполняется в строгом режиме ('use strict').
- В модуле на верхнем уровне this не определён (undefined).
- Ключевые слова import и export могут использоваться только на верхнем уровне, их нельзя использовать в функции или в блоке:
1 2 3 |
if (Math.random()) { import 'foo'; // SyntaxError (нельзя использовать в функции или блоке) } |
- Импорт из модуля поднимается в начало области видимости:
1 2 |
foo(); import { foo } from 'test_module'; |
- Модули всегда выполняются в отложенном режиме, как и скрипты с атрибутом defer , т.е.:
- загрузка внешних модулей, таких как <script type="module" src="...">, не блокирует обработку HTML;
- модули, даже если загрузились быстро, ожидают полной загрузки HTML документа, и только затем выполняются;
- сохраняется относительный порядок скриптов: скрипты, которые идут раньше в документе, выполняются раньше;
- модули всегда видят полностью загруженную HTML-страницу, включая элементы под ними.
-
Для модулей атрибут async работает на любых скриптах: модуль выполнится сразу после загрузки, не ожидая других скриптов, даже если HTML документ ещё не будет загружен, например:
1234<script async type="module">import {main} from './main.js';main.key();</script> - В браузере import должен содержать относительный или абсолютный путь к модулю (модули без пути называются «голыми» (bare) не разрешены в import):
1 2 |
import {sayHi} from 'sayHi'; // ошибка, "голый" модуль import {sayHi} from './sayHi.js'; // правильно |
Загрузчики модулей JS
AMD и CommonJS — это форматы модулей, а не реализации. Для поддержки AMD, например, необходима реализация функций define() и require(), для поддержки CommonJS — реализация module.exports и require().
Для поддержки модулей во время выполнения используются загрузчики модулей, которые имеют следующий принцип работы:
- Вы подключаете скрипт загрузчика в браузере и сообщаете ему, какой файл загрузить в качестве основного.
- Загрузчик модулей загружает основной файл приложения.
- Загрузчик модулей загружает остальные файлы по мере необходимости.
Популярные загрузчики модулей:
- RequireJS загружает модули в формате AMD.
- curl.js загружает модули AMD и CommonJS.
- SystemJS загружает модули AMD и CommonJS.
Сборщики модулей JS
В отличие от загрузчиков модулей, которые работают в браузере и загружают зависимости «на лету», сборщики модулей позволяют заранее подготовить один файл со всеми зависимостями (например, bundle.js, бандл).
Существует ряд инструментов, позволяющих заранее собирать модули в один файл:
- Browserify поддерживает формат CommonJS;
- Webpack поддерживает AMD, CommonJS и ES6 модули;
- Rollup поддерживает ES6 модули.
Для работы сборщика webpack необходимо установить:
- платформу Node.JS (https://nodejs.org) - подробнее читай Node.js как среда выполнения JS;
- терминал Git Bash (приложение для сред Microsoft Windows, которое предоставляет эмуляцию bash (командной оболочки), используемую для запуска Git из командной строки) (https://git-scm.com/) - подробнее читай Git Bash в VS Code.