Модульность в JavaScript: CommonJS, AMD, ES Modules

6fc19530e7ae41b9366a5ed5059daf67.png

Привет, Хабр!

Начало истории модульности в JavaScript положило хаос: глобальные переменные, конфликты имен и сложности с зависимостями. Со временем сообщество предложило несколько подходов для организации модулей, начиная от CommonJS, которое легло в основу Node.js, до AMD, предпочтительного для асинхронной загрузки кода в браузерах. И приближаясь к настоящему времени появился ES Modules стандартизированный и встроенный в язык механизма модулей, который стал частью ECMAScript в 2015 году.

В этой статье рассмотрим кратко про CommonJS, AMD, и наконец — как появился ES Modules.

CommonJS

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

В Node.js каждый файл считается модулем и в нем была принята спецификация CommonJS как де-факто стандарт для организации модулей.

Система модулей CommonJS в Node.js поддерживает как синхронную загрузку модулей, так и ленивую загрузку. Node.js использует кэширование загруженных модулей, что сокращает время их повторной загрузки и, как следствие, ускоряет выполнение программы.

В CommonJS, каждый файл JavaScript считается отдельным модулем. Если нужно функции, объекты или примитивы доступными вне текущего модуля, можно использоватьmodule.exports:

// myModule.js
const myFunction = () => {
  console.log("Привет из myFunction");
}

const myVariable = 123;

module.exports = { myFunction, myVariable };

Здесь экспортируем объект, содержащий функцию myFunction и переменную myVariable.

Для использования экспортированных значений в другом модуле CommonJS есть функция require(). Эта функция принимает один аргумент — путь к модулю, который нужно импортировать, и возвращает объект, экспортированный целевым модулем:

// anotherModule.js
const { myFunction, myVariable } = require('./myModule');

myFunction(); // "Привет из myFunction"
console.log(myVariable); // 123

Импортируем функциональность из myModule.js в anotherModule.js, используя деструктуризацию объекта для доступа к myFunction и myVariable.

В Node.js exports является сокращением для module.exports. Изначально exports и module.exports ссылаются на один и тот же объект. Тем не менее, если назначается module.exports новому объекту, это не повлияет на exports. Обычно юзают module.exports для экспорта, чтобы избежать путаницы:

// myModule.js
exports.myFunction = () => {
  console.log("Экспортируется через exports");
}

Код аналогичен предыдущему примеру с module.exports, но здесь добавляем myFunction напрямую к exports.

Та же функция require() используется для импорта встроенных модулей Node.js, таких как fs для работы с файловой системой, а также для импорта сторонних библиотек, установленных через NPM, например с fs:

const fs = require('fs');

fs.readFile('path/to/file', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
});

AMD

AMD — это стандарт, позволяющий определять модули JavaScript и их зависимости в асинхронном стиле. Обычно скрипты загружаются синхронно, что может привести к заметной задержке в интерактивности страницы, если скрипт находится на удаленном сервер. AMD в целом создан, чтобы решить эту проблему.

Среди различных реализаций AMD, самый годный — это RequireJS. Она служит стандартом де-факто для разрабов, стремящихся внедрить асинхронную загрузку модулей в проекты.

RequireJS предлагает простой API для асинхронной загрузки JavaScript-файлов и управления зависимостями. С его помощью можно определить модули, указать их зависимости и загрузить их только тогда, когда это действительно необходимо.

Каждый модуль определяется с помощью функции define(), принимающей список зависимостей и фабричную функцию. Простой модуль без зависимостей:

// hello.js
define(function() {
    return function hello() {
        console.log('Привет, мир!');
    };
});

Модуль hello.js может быть загружен и использован следующим образом:

require(['hello'], function(hello) {
    hello(); // "Привет, мир!"
});

Допустим, есть модуль math.js, который предоставляет функции сложения и умножения:

// math.js
define(function() {
    return {
        add: function(a, b) {
            return a + b;
        },
        multiply: function(a, b) {
            return a * b;
        }
    };
});

И нужно использовать эти функции в другом модуле calculator.js:

// Файл: calculator.js
define(['math'], function(math) {
    console.log(math.add(1, 2)); // Выводит: 3
    console.log(math.multiply(3, 4)); // Выводит: 12
});

Чтобы подключить RequireJS и начать загрузку модулей из HTML-файла, добавляем data-main атрибут в тег

В файле scripts/main.js можно использовать require() для загрузки модулей:

// scripts/main.js
require(['calculator'], function() {
    // модуль calculator и его зависимости загружены и выполнены
});

RequireJS поддерживает плагины для загрузки не только JavaScript модулей, но и других типов ресурсов, к примеру пример использования плагина text для загрузки текстового файла:

// загрузка текстового содержимого файла mydata.txt
define(['text!../data/mydata.txt'], function(data) {
    console.log(data); // выводит содержимое mydata.txt
});

Чтобы использовать плагин text, его необходимо сначала подключить.

ES Modules

ES Modules представлят собой одно из самых значимых добавлений к стандарту ECMAScript 2015. Этот стандарт внес изменения в подход к модульности в JS, предложив нативную поддержку модулей в языке. Разработчики JS долгое время использовали различные способы для организации и модуляризации своего кода, такие как CommonJS и AMD. Однако до ES6 ни один из этих способов не был частью самого языка JavaScript.

ES Modules обладает статистической структурой, которая позволяет js-движкам анализировать зависимости модулей на этапе разбора кода, еще до его выполнения. Это отличается от CommonJS, где модули и их зависимости определяются и загружаются во время выполнения программы.

ES Modules их поддержка реализована во всех современных браузерах, и так можно использовать модули напрямую без необходимости применения инструментов, как Webpack или Babel, для транспиляции кода.

Основной задачей модулей является экспорт частей кода чтобы они могли быть использованы в других файлах. Именованный экспорт:

// экспорт отдельных функций
export function myFunction() { ... }
export const myConstant = 123;

// экспорт списка
const someConstant = 456;
function someFunction() { ... }
export { someFunction, someConstant };

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

Экспорт по умолчанию:

// экспорт функции по умолчанию
export default function() { ... }

// экспорт класса по умолчанию
export default class MyClass { ... }

Каждый модуль может иметь только один default экспорт.

Для использования экспортированных значений ES Modules есть оператор import.

import { myFunction, myConstant } from './myModule.js';х

Можно импортировать только необходимые части модуля, указав их имена в фигурных скобках.

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

import { myFunction as functionOne, myConstant as constantOne } from './myModule.js';

Импорт всех именнованных экспортов объектов:

import * as myModule from './myModule.js';

Такой импорт собирает все именованные экспорты модуля в один объект.

Импорт экспорта по умолчанию:

import myDefault from './myModule.js';

Еще есть динамический импорт с помощью функции import(), которая возвращает Promise:

import('./myModule.js').then((module) => {
    // использование модуля
});

Можно использовать , указав путь к модулю. Это сообщает браузеру заранее загрузить модуль, еще до его фактического запроса в коде.

В заключение составим табличку сравнения для трех основных систем модулей в JavaScript: CommonJS, AMD и ES Modules:

Признак

CommonJS

AMD

ES Modules

Синхронность

Синхронный

Асинхронный

Асинхронный*

Основное использование

Node.js

браузеры

браузеры и Node.js

Синтаксис

require/module.exports

define и require

import/export

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

Во время выполнения

Во время выполнения

Статический анализ

Живые привязки

Нет

Нет

Да

Динамическая загрузка

Ограничено

Да

Да (import())

Кэширование модулей

Да

Да

Да

Распространенность

Популярен в Node.js

Используется в специфичных случаях

Стандарт ECMAScript

*ES Modules могут загружаться асинхронно в браузерах, но также поддерживают статический анализ зависимостей на этапе компиляции.

В конечном счете, ES Modules, постепенно становясь стандартом де-факто для модульности в JavaScript, предлагает наиболее гармоничное и решение, но знание всех трех систем делает нас лучше и компетентней в этой теме.

Напоследок хочу пригласить вас набесплатный вебинар про прототипное наследование в JavaScript. Регистрируйтесь, будет интересно.

© Habrahabr.ru