Модуль — часть кода, которая инкапсулирует детали реализации и предоставляет открытый API для использования другим кодом.
JavaScript изначально не имел встроенной поддержки модулей, в отличие от большинства других языков программирования, но с усложнением фронтенда стала очевидна необходимость структурирования кода.
Это привело к появлению сначала паттерна программирования «Модуль», а затем и отдельных форматов: CommonJS, AMD, UMD, и специальных инструментов для работы с ними. Нативная система модулей в JavaScript добавилась в спецификации ECMAScript 6, а разработчики браузеров работают над ее поддержкой.
Шаблон Модуль
Шаблон «Модуль» основывается на немедленно вызываемой функции (IIFE):
var MODULE = (function() { var privateVariable = 1; function privateMethod() { // ... } return { moduleProperty: 1, moduleMethod: function() { // ... } }; }());
Немедленно вызываемая функция образует локальную область видимости, в которой можно объявить необходимые приватные свойства и методы. Результат функции — объект, содержащий публичные свойства и методы.
С помощью передачи параметров в немедленно вызываемую функцию можно импортировать зависимости в модуль. Это увеличивает скорость разрешения переменных, поскольку импортированные значения становятся локальными переменными внутри функции.
(function ($) { // ... }(jQuery));
Форматы модулей
CommonJS
С точки зрения структуры модуль CommonJS — это часть JavaScript-кода, которая экспортирует определенные переменные, объекты или функции, делая их доступными для любого зависимого кода.
Модули CommonJS состоят из двух частей: module.exports содержит объекты, которые модуль хочет сделать доступными, а функция require() используется для импорта других модулей.
Пример модуля в формате CommonJS:
// определяем функцию, которую хотим экспортировать function foobar() { this.foo = function() { console.log('Hello foo'); } this.bar = function() { console.log('Hello bar'); } } // делаем ее доступной для других модулей module.exports.foobar = foobar;
Пример кода, использующего модуль:
var foobar = require('./foobar').foobar, test = new foobar(); test.bar(); // 'Hello bar'
AMD
В основе формата AMD (Asynchronous Module Definition) лежат две функции: define() для определения именованных или безымянных модулей и require() для импорта зависимостей.
Функция define() имеет следующую сигнатуру:
define( module_id /*необязательный*/, [dependencies] /*необязательный*/, definition function /*функция для создания экземпляра модуля или объекта*/ );
Параметр module_id необязательный, он обычно требуется только при использовании не-AMD инструментов объединения. Когда этот аргумент опущен, модуль называется анонимным. Параметр dependencies представляет собой массив зависимостей, которые требуются определяемому модулю, а третий аргумент (definition function) — это функция, которая выполняется для создания экземпляра модуля.
Пример модуля:
define('myModule', ['foo', 'bar'], // зависимости (foo и bar) передаются в функцию function(foo, bar) { // создаем модуль var myModule = { doStuff: function() { console.log('Hi!'); } } // возвращаем модуль return myModule; } );
Функция require() используется для импорта модулей:
require(['foo', 'bar'], function(foo, bar) { foo.doSomething(); });
Также с помощью require() можно динамически импортировать зависимости в модуль:
define(function(require) { var foobar; require(['foo', 'bar'], function (foo, bar) { foobar = foo() + bar(); }); // возвращаем модуль // обратите внимание на другой шаблон определения модуля return { foobar: foobar }; });
UMD
Существование двух форматов модулей, несовместимых друг с другом, не способствовало развитию экосистемы JavaScript. Для решения этой проблемы был разработан формат UMD (Universal Module Definition). Этот формат позволяет использовать один и тот же модуль и с инструментами AMD, и в средах CommonJS.
Суть подхода UMD заключается в проверке поддержки того или иного формата и объявлении модуля соответствующим образом. Пример такой реализации:
(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 появились нативные модули JavaScript. На текущий момент модули ES6 поддерживаются в Safari 10.1 и за флагом в Firefox 54, Chrome 60 и Edge 15.
В основе ES6 модулей лежат ключевые слова export и import. Любая переменная, объявленная в модуле, доступна за его пределами, только если явно экспортирована из модуля.
Синтаксис
Если модуль экспортирует только одно значение, можно использовать экспорт по умолчанию. Например, модуль экспортирует функцию:
export default function () { ··· }
Или класс:
export default class { ··· }
Или даже выражение:
export default 5 * 7;
Один модуль может экспортировать несколько значений:
export const pi = Math.PI; export function sum(x, y) { return x + y; } export function multiply(x, y) { return x * y; }
Можно перечислить все, что вы хотите экспортировать, в конце модуля:
export const pi = Math.PI; export function sum(x, y) { return x + y; } export { pi, sum };
И переименовать:
export { pi as PI, sum };
Импортировать модули также можно несколькими способами:
// Импорт значения по умолчанию 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';
Тонкости Ключевые слова import и export могут использоваться только на верхнем уровне, их нельзя использовать в функции или в блоке:
if (Math.random()) { import 'foo'; // SyntaxError }
Импорт из модуля поднимается в начало области видимости:
foo(); import { foo } from 'test_module';
Модули ES6 выполняются отложено, только когда документ полностью проанализирован.
Код модуля выполняется в строгом режиме.
Загрузчики модулей
AMD и CommonJS — это форматы модулей, а не реализации. Для поддержки AMD, например, необходима реализация функций define() и require(), для поддержки CommonJS — реализация module.exports и require().
Для поддержки модулей во время выполнения используются загрузчики модулей. Существует несколько различных загрузчиков, они имеют похожий принцип работы:
- Вы подключаете скрипт загрузчика в браузере и сообщаете ему, какой файл загрузить в качестве основного.
- Загрузчик модулей загружает основной файл приложения.
- Загрузчик модулей загружает остальные файлы по мере необходимости.
Популярные загрузчики модулей:
- RequireJS загружает модули в формате AMD.
- curl.js загружает модули AMD и CommonJS.
- SystemJS загружает модули AMD и CommonJS.
Сборщики модулей
В отличие от загрузчиков модулей, которые работают в браузере и загружают зависимости «на лету», сборщики модулей позволяют заранее подготовить один файл со всеми зависимостями (бандл).
Существует ряд инструментов, позволяющих заранее собирать модули в один файл:
- Browserify поддерживает формат CommonJS.
- Webpack поддерживает AMD, CommonJS и ES6 модули.
- Rollup поддерживает ES6 модули.
Заключение
Чтобы лучше ориентироваться в современных инструментах фронтенд-разработки, необходимо понимать такие концепции как модули, форматы модулей, загрузчики модулей и сборщики.
Итак:
Модуль — это многократно используемая часть кода, инкапсулирующая детали реализации и предоставляющая открытый API.
Формат модуля — это синтаксис определения и подключения модуля.
Загрузчик модулей загружает модуль определенного формата во время выполнения непосредственно в браузере. Популярные загрузчики — RequireJS и SystemJS.
Сборщик модулей заранее объединяет модули в один файл, который подключается на странице. Примеры сборщиков — Webpack и Browserify.