
CommonJS, RequireJS, UMD: модули JavaScript простыми словами
Фронтенд в простом виде это HTML, JS и CSS файлы. Когда веб-сайты были простыми, а JS использовался лишь для обработки событий нажатия, все было нормально. Постепенно JS как язык становился более продвинутым, а логика фронтенд-приложений усложнялась. Появилась необходимость разбивать скрипты на отдельные модули.
Если вы, как и я, не понимаете, что вообще происходит, когда дело доходит до модулей JS, то эта серия статей вам поможет разобраться.
Почему в JS не было модулей из коробки?
В Python, например, модуль - это файл. Создадим hello.py со следующим кодом:
// hello.py
def say_hello(name)
print(f"Hello dear {name}")
Его можно импортировать в другой модуль:
//main.py
import hello
hello.say_hello('Dima')
Все просто и удобно. Не так ли? Но что там в JS? JS развивался как упрощенная версия Java для дизайнеров, поэтому многих вещей в языке не было, включая модулей, которые появились в стандарте лишь в 2015 году с выходом ES6. Лишь некоторое время назад большинство браузеров стали подддерживать ES-модули, но о них чуть позже.
Раньше в языке вся логика описывалась в одном файле. И все переменные и методы, объявленные внутри файла, были доступны остальным файлам. В самом простом виде есть index.html, куда мы можем вставлять тег script и писать JS-код прямо в верстке.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
<div id='greetingElem'>hello</div>
<script>
document.querySelector('greetingElem').innerText = 'hello world';
</script>
</head>
<body>
</body>
</html>
Сможем ли мы так сделать аналог Facebook? Сможем, но тогда нам придется разом загружать весь код вебсайта при входе на страницу, а это мегабайты кода. Да и поддерживать такой сайт будет огромной проблемой.
Чтобы отделить верстку от кода, давайте создадим script.js файл.
//index_1.js
var name = 'Boris';
function getNameLength() {
return name.length;
}
Постепенно сайт обрастает функциями, кода становится много, и держать всю логику в одном файле довольно сложно. Так мы начинаем разбивать его на несколько файлов и вставлять их в HTML через script-тег.
/*файловая структура
- index_1.js
- index_2.js
- index_3.js
- index.html
*/
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
User: <span id="user"></span>
</div>
<script src="./index_1.js"></script>
<script src="./index_2.js"></script>
<script src="./index_3.js"></script>
</body>
</html>
//index_1.js
var name = 'Boris';
function getNameLength() {
return name.length;
}
//index_2.js
var secondName = 'Boris';
function getSecondNameLength() {
return secondName.length;
}
//index_3.js
console.log(name, getNameLength()); //Boris 5
Так мы сталкиваемся с первой проблемой - отсутствие изоляции. Переменные и методы доступны всем файлам. Проблему решают IIFE-модули.
IIFE-модули
Нам нужно изолировать переменные в файлах. Но как? В JS у функций есть своя область видимости, определив функцию внутри файла и указав переменные внутри функции, мы изолируем их в границах файла.
// index_1.js
function initModuleOne() {
var _name = 'Boris';
function getNameLength() {
return _name.length;
}
function getName() {
return _name;
}
function setName(name) {
_name = name;
}
return {getNameLength, getName, setName};
}
var moduleOne = initModuleOne();
//index_3.js
console.log(_name); //Uncaught ReferenceError: _name is not defined
console.log(moduleOne.getName()); //Boris
Но можно сделать еще удобнее с помощью IIFE. Immediately invoked function expression - самовызывающиеся функции. В JS можно объявить функцию и сразу же её вызвать. Это позволяет делать так:
//index_1.js
var moduleOne = (function() {
var _name = 'Boris';
function getNameLength() {
return _name.length;
}
function getName() {
return _name;
}
function setName(name) {
_name = name;
}
return {getNameLength, getName, setName};
})();
//index_2.js
var moduleTwo = (function() {
var _secondName = 'Johnson';
function getSecondNameLength() {
return _secondName.length;
}
function getSecondName() {
return _secondName;
}
function setSecondName(secondName) {
_secondName = secondName;
}
return {getSecondNameLength, getSecondName, setSecondName};
})();
//index_3.js
console.log(moduleOne.getName(), moduleTwo.getSecondName()); //Boris Johnson
Можно взять в пример популярную когда-то либу jQuery. При подключении в проект она экспортировала символ "$", который можно было использовать во всех скриптах, инциализированных в HTML после jQuery.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
User: <span id="user"></span>
</div>
<script src="./jquery.js"></script> //скрипты раньше загружались браузером синхронно, поэтому если мы хотим, чтобы jquery был доступен скриптам ниже, нужно подключать его выше остальных скриптов
<script src="./index_1.js"></script>
<script src="./index_2.js"></script>
<script src="./index_3.js"></script>
</body>
</html>
jQuery тоже использовал изоляцию переменных на уровне скоупа функции, объявляя внутри файла лишь переменную $:
function jQuery(a,c) {
...
}
var $ = jQuery;
Стоит заметить, что первая версия jQuery (1800 строк кода) сильно отличается от последней (10800 строк кода). В последних версиях jQuery учитывает наличие CommonJS, RequireJS-модулей, об этом ниже.
CommonJS модули
Вы наверняка знаете такой синтаксис подключения и экспорта скриптов:
var index1 = require('./index1.js');
var index2 = require('./index2.js');
var name = 'Boris';
module.exports = {name};
В 2009 году появился NodeJS. Если с браузерами все понятно - там можно добавлять скрипты через script-тег, то вот на сервере поддержка модулей нужна была, иначе как вообще писать приложения. Но так как поддержки модулей в самом языке не было даже на уровне спецификаций, ребятам пришлось придумывать свои решения. Так в NodeJS добавили поддержку модулей CommonJS, которые соответствовали спецификации CommonJS. CommonJS-модули синхронные, поэтому использовать их в браузере нельзя, ведь это было бы странно, если бы мы импортировали jQuery модуль и ждали бы пока он загрузится, блокируя инициализацию всего остального кода. Каждый файл согласно спецификации это отдельный модуль.
Под капотом весь контент файла NodeJS оборачивает в функцию:
(function (exports, require, module, __filename, __dirname) {
function add (a, b) {
return a + b
}
module.exports = add
})
NodeJS кеширует все модули, поэтому импортировав модуль в нескольких местах, приложение будет использовать лишь один инстанс этого модуля. Исходники модуля NodeJS, который отвечает за реализацию CommonJS-модулей. Функция Module.prototype._compile в исходниках отвечает за оборачивание файла (пример выше), а Module._load отвечает за кеширование модулей.
CommonJS - синхронные (ждут загрузки других) модули для бэкендов на NodeJS
С CJS чуть разобрались, идем дальше.
AMD-спецификация и RequireJS-модули
AMD - это спецификация. То есть то, какое API должно быть у реализации модулей AMD. А вот RequireJS - это непосредственно реализация AMD-спецификации модулей. И она, в первую очередь, создана для браузерных скриптов. На странице проекта подробно объяснены варианты реализации загрузки модулей в браузере. Показаны все проблемы и то, как их решили с помощью AMD-модулей.
Давайте скачаем RequireJS-скрипт с офф. сайта и подключим к нашему проекту:
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
User: <span id="user"></span>
</div>
<script src="./jquery.js"></script>
<script src="./require.js"></script>
<script src="./index_3.js"></script>
</body>
</html>
//index_1.js
define(
'./index_1.js', function() {
var _name = 'Boris';
function getNameLength() {
return _name.length;
}
function getName(name) {
return _name;
}
function setName(name) {
$('#user').text(name);
return _name = name;
}
return {getNameLength, getName, setName};
});
//index_2.js
define(
'./index_2.js', function() {
var _secondName = 'Johnson';
function getSecondNameLength() {
return _secondName.length;
}
function getSecondName() {
return _secondName;
}
function setSecondName(secondName) {
return _secondName = secondName;
}
return {getSecondNameLength, getSecondName, setSecondName};
});
//index_3.js
require(['./index_1.js', './index_2.js'], function(moduleOne, moduleTwo) {
console.log(moduleOne.getName(), moduleTwo.getSecondName());
})
Хочу заметить, что теперь мы не подключаем скрипты index_1.js, index_2.js к проекту, requireJS автоматически добавляет их к HTML:


Название модуля в define является путем к файлу. Здесь можно было не указывать расширение .js но для наглядности я его оставил.
Как же теперь подключить jQuery?
jQuery пока подключен глобально и переменная "$" доступна внутри модулей. Однако AMD модули не рекомендуют использование глобальных переменных. jQuery распознает наличие RequireJS и автоматически объявляет модуль "jquery", который можно подключить к любому модулю вот так:
define(
'index_1', ['jquery'], function(jquery) {
var _name = 'Boris';
function getNameLength() {
return _name.length;
}
function getName(name) {
return _name;
}
function setName(name) {
console.log(jquery);
jquery('#user').text(name);
return _name = name;
}
return {getNameLength, getName, setName};
});
Но все-таки рекомендуется явно объявлять библиотеки через глобальный конфиг:
//путь к jquery: js/lib/jquery-1.9.0.js
requirejs.config({
baseUrl: 'js/lib',
paths: {
jquery: 'jquery-1.9.0'
}
});
Если мы попытаемся убрать глобальную переменную "$", столкнемся с кейсом, когда другие зависящие от jQuery плагины, которые не являются AMD-модулями, перестанут работать. В документации есть способ сделать глобальные переменные локальными, но мы опустим этот вопрос.
RequireJS также можно использовать и с синтаксисом CommonJS-модулей, так как колбэк в define принимает require функцию для динамического импорта зависимостей.
define(
'index_1', ['require', 'jquery'], function(require, jquery) {
const moduleTwo = require('index_2');
console.log(moduleTwo); // Object { getSecondNameLength: getSecondNameLength(), getSecondName: getSecondName(), setSecondName: setSecondName(secondName) }
}
)
Для консистентности бэка и фронта RequireJS можно использовать и с NodeJS. А можно даже подключить React.
С RequireJS разобрались. Это либа для браузера, которая автоматически импортирует скрипты в HTML, убирает необходимость в использовании глобальных переменных и сама передает файлам нужные зависимости. Уже неплохо!
UMD-модули
Здесь стоит сразу сказать - UMD-модули это лишь паттерны. Это не библиотека, которую можно подключить к проекту. Их можно изучить на странице проекта.
UMD-модули необходимы, чтобы обрабатывать кейсы, когда на проекте используются модули разного типа. Например, представьте, что у вас есть модуль для работы со строками, который переносится из проекта к проекту. В одном проекте AMD-модулей может не быть, а в другом проекте вы захотите использовать его на бэке с NodeJS, где CJS (CommonSJ) модули. И чтобы вам каждый раз не приходилось адаптировать модуль под разные кейсы, были придуманы UMD-паттерны.
Например, давайте сделаем так, чтобы если на проекте нет RequreJS библиотеки, модули становились частью глобального объекта window:
//index_1.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['index_1'], ['index_2', 'jquery'], factory);
} else {
// Browser globals
root.moduleOne = factory(root.moduleTwo, root.$);
}
}(typeof self !== 'undefined' ? self : this, function(moduleTwo, jquery) {
var _name = 'Boris';
function getNameLength() {
return _name.length;
}
function getName(name) {
return _name;
}
function setName(name) {
console.log(jquery);
jquery('#user').text(name);
return _name = name;
}
return {getNameLength, getName, setName};
}));
//index_2.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['index_2'], factory);
} else {
// Browser globals
root.moduleTwo = factory();
}
}(typeof self !== 'undefined' ? self : this, function() {
var _secondName = 'Johnson';
function getSecondNameLength() {
return _secondName.length;
}
function getSecondName() {
return _secondName;
}
function setSecondName(secondName) {
return _secondName = secondName;
}
return {getSecondNameLength, getSecondName, setSecondName};
}));
ES-модули
В 2015 году с выходом ES6 впервые появились модули. Их синтаксис всем вам знаком:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
User: <span id="user"></span>
</div>
<script type="module" src="./index_1.js"></script>
<script type="module" src="./index_2.js"></script>
<script type="module" src="./index_3.js"></script>
</body>
</html>
//index_1.js
const name = 'Boris';
export const getNameLength = () => {
return name.length;
}
export const getName = (name) => {
return name;
}
export const setName = (name) => {
$('#user').text(name);
}
//index_2.js
const name = 'Boris';
export const getSecondNameLength = () => {
return name.length;
}
export const getSecondName = (name) => {
return name;
}
//index_3.js
import { getName } from "./index_1";
console.log(getName()); //Boris
Для того, чтобы браузер понял, что работает с модулем, необходимо указать в script теге type='module', в случае с NodeJS необходимо создавать файлы формата .mjs.
Можно заметить, что каждый модуль импортируется вручную в HTML, что не очень круто. В реальном мире в сыром виде модули не используются. Обычно используют сборщики, но о них мы поговорим в следующем материале.
Каждый модуль имеет свою область видимости. Импортируются зависимости через import, экспортируются через export. Для сравнения, в Python по дефолту все переменные экспортируются автоматически (конечно, можно сделать неэкспортируемые), и такое решение как-будто кажется более удобным. Вспомните кейсы, когда вы не экспортировали переменные в JS? Вот и я о том же. Как правило, функциональность внутри модуля в очень редких случаях используется только внутри него.
Deep Dive статья о работе ES-модулей.
Выводы
Итак, мы немного разобрались с тем как работают модули разного типа в JS. Сгодня в чистом виде их используют редко. Нам еще многое предстоить изучить. Например, что такое Bower и Browserify, как работают сборщики, траспиляторы (Babel), чем отличается современный и быстрый Vite сборщик от Webpack и Gulp и почему сборщик Rollup идеален для разработки библиотек. Об этом мы поговорим в следующих материалах.