Модульность в JavaScript: CommonJS, AMD, ES Modules
Привет, Хабр!
Начало истории модульности в 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 |
Синтаксис |
|
|
|
Загрузка модулей | Во время выполнения | Во время выполнения | Статический анализ |
Живые привязки | Нет | Нет | Да |
Динамическая загрузка | Ограничено | Да | Да ( |
Кэширование модулей | Да | Да | Да |
Распространенность | Популярен в Node.js | Используется в специфичных случаях | Стандарт ECMAScript |
*ES Modules могут загружаться асинхронно в браузерах, но также поддерживают статический анализ зависимостей на этапе компиляции.
В конечном счете, ES Modules, постепенно становясь стандартом де-факто для модульности в JavaScript, предлагает наиболее гармоничное и решение, но знание всех трех систем делает нас лучше и компетентней в этой теме.
Напоследок хочу пригласить вас набесплатный вебинар про прототипное наследование в JavaScript. Регистрируйтесь, будет интересно.