История ES модулей

История ES модулей

Ilya Yurkin

Введение

Большая часть работы в JS - это манипулирование переменными. Присвоение значений переменным, преобразование одной переменной в другую, и так далее. Из этого факта следует, что организация переменных тесно связана с качеством кода и его поддержкой. Гораздо проще держать в уме несколько значений, чем думать о переменных из всего проекта, не правда ли?

Здесь на помощь приходит область видимости (scope). Переменные, которые находятся в одной области видимости, недоступны из соседней. Это одновременно и плюс, и минус. Плюс в том, что мы должны думать только о переменных в рамках одной функции, но из-за этой же особенности проблематично использовать одну и ту же переменную между разными функциями.

Что делать, если переменная нужна за пределами скопа? Обычно в таких случаях переменная помещается на скоп выше, например, в глобальный (global/window) скоп. Думаю, старички помнят, как приходилось подтягивать jQuery или lodash на самом верху страницы, и следить за порядком подключения других скриптов.

Небольшой пример.

Допустим, есть ванильный проект, который использует вышеупомянутые библиотеки. Подключение скриптов должно происходить именно в таком порядке. Например, если script_1 расположить в самом верху, то он загрузится первым и начнет использовать jQuery еще до того как сам jQuery загрузится и будет доступен в глобальной области видимости. Из-за этого сайт не будет работать должным образом. Отношение между script_1 и script_2 тоже не явно. Зависят ли она как-то друг от друга? Не изменяют ли они одну и ту же переменную? Чтобы получить ответы на эти вопросы нужно знать как устроены оба скрипта.

Если масштабировать этот пример на реальный проект с десятками файлов, которые могут быть зависимы друг от друга, то станет понятно, насколько больно расширять и поддерживать подобный сайт.

Module паттерн

До 2015 года широко использовались самодельные модули (Module паттерн). На тот момент в JS не было встроенной модульной системы и для реализации приватных методов и свойств приходилось прибегать к возможностям языка, а конкретно к IIFE.

Например, данная функция weekDay инкапсулирует переменную names из общей области видимости, а ее интерфейс состоит из функций name и number.

Module паттерн решает проблему области видимости, но не делает зависимости явными. Заметили lodash на 5 строчке? Точно ли он подключен на сайте? А если подключен, то, до или после нашего скрипта?

CommonJs (CJS) модули

На смену Module паттерну пришли CommonJs модули. Основной фичей CommonJs является require функция. Когда вы вызываете ее с именем модуля из зависимостей (или путем до файла), она гарантирует, что модуль будет загружен и возвращает его интерфейс.

Загрузчик заключает код модуля в функцию, поэтому модули получают свою собственную локальную область видимости. Все, что им нужно сделать - это вызвать require для доступа к своим зависимостям и поместить свой интерфейс в объект, привязанный к exports.

Примерно вот так может выглядеть функция weekDay из прошлого раздела на CommonJS.

В CommonJS зависимости стали более явными и появилась возможность контролировать область видимости, но все еще остались маленькие проблемы с удобством.

  • require - это обычный вызов функции, в который можно передать любой аргумент, а не только строку. Эта особенность может осложнить отладку и определение зависимостей без запуска кода;
  • Неудобно работать в рамках модуля. То, что добавляется в export - недоступно в локальном скопе.

ECMAScript модули (ESM)

В 2015 году JavaScript представляет новый, собственный, стандарт модулей - ECMAScript модули. Основные концепции остаются такими же, как и в CommonJs, но реализация отличается. 

  • В язык добавлены специальные ключевые слова и конструкции. Типа import и export;
  • Во время импортирования из другого модуля делается привязка к переменной, а не к конкретному значению. Это означает, что экспортирующий модуль может изменить свое значение в любой момент, и модули, которые его слушают, будут видеть это новое значение.
  • Импорт модулей происходит ДО того, как скрипт запустится. Из-за этой особенности нельзя делать импорт внутри функции, а имена обязательно должны быть строками. Динамические зависимости возможны, но имеют немного другой синтаксис.

Как видите, кроме замены require на import ничего особо и не изменилось.

Заключение

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

На данный момент и ES и CommonJs модули существуют бок о бок. Все современные браузеры поддерживают ESM. Поэтому, большинство свежих фронтенд-приложений использует именно их. А Node.js все еще использует CommonJs по историческим причинам. Следующей осенью 20 версия станет LTS, в которой ESM будет официальным стандартом и для Node.js. Исходя из этого, я бы рекомендовал просто понимать CommonJs, а писать новые проекты на ES6 синтаксисе (если вы разрабатываете не на ноде, конечно). Если по какой-то причине вам нужна поддержка старых браузеров - используйте сборщик, например, Webpack.


P.S. Кстати, в тему сборщиков. Думаю, стоит добавить, что код, который вы видите в своем редакторе и код, который потом крутится в продакшене - выглядит совсем по-разному. Все дело в сборщиках (Webpack, rollup, vite.js) и транскомпиляторах (babel), которые преобразуют ваш код во время билда. Они позволяют писать код, используя ESM модули, а во время сборки могут преобразовывать его в CommonJS или даже Модульный паттерн. Но тема сборщиков уже за рамками этой статьи.

P.P.S. Примерно так может выглядеть код из ESM или CJS раздела, после обработки Webpack’ом:





Report Page