Модули в JavaScript
17.05.2017

Модули в JavaScript

Модуль — часть кода, которая инкапсулирует детали реализации и предоставляет открытый 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.

Первое использование этого подхода было замечено в 2003 году, когда Ричард Корнфорд привел его в качестве примера использования замыканий.

В 2005-2006 годах разработчики YUI использовали этот подход для своего фреймворка.

Наибольшую популярность шаблон «Модуль» получил после того, как Дуглас Крокфорд описал его в своей книге «JavaScript the Good Part».

В 2009 году сотрудник Mozilla Кевин Дангур опубликовал сообщение с призывом присоединиться к неофициальному комитету для обсуждения и разработки серверного JavaScript API, который назывался ServerJS.

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

Во время работы над спецификацией CommonJS обсуждалась возможность асинхронной загрузки модулей. Другой разработчик из Mozilla Джеймс Берк предложил свой формат, который назывался AMD (Asynchronous Module Definition).

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

Впервые такой подход использовали Джеймс Берк и Крис Ковал при разработке библиотеки Q, а Эдди Османи собрал похожие шаблоны в одном репозитории и назвал UMD

Работа над системой модулей началась в 2010 году. Главным разработчиком этой спецификации был Дейв Херман, директор по стратегическому развитию Mozilla.