Alfa Brain

Погружение в модули JavaScript. Типы модулей, форматы, загрузчики и сборщики модулей.


Превью статьи Погружение в модули JavaScript. Типы модулей, форматы, загрузчики и сборщики модулей.

Разрабатывая веб сайты или веб приложения мы все чаще переносим выполнение кода на клиент, в браузер. Это и обычные страницы с множеством ajax вызовов, и популярные в наше время SPA (Single Page Application), а так же полноценные приложения по типу Figma и прочих конструкторов, где 90% работы выполняет именно браузер.

Но по мере того как расширяется кодовая база клиентского приложения возникают разумные вопросы: Как управлять большой кодовой базой? Как разделить код на части и переиспользовать их? Как создать гибкую и расширяемую архитектуру приложения?

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

Что такое Модуль?

Что же такое модуль? Википедия нам сообщает: Модульное программирование — это метод проектирования программного обеспечения, в котором основное внимание уделяется разделению функциональности программы на независимые взаимозаменяемые модули, каждый из которых содержит все необходимое для выполнения только одного аспекта желаемой функциональности.

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

Взгляд вокруг

Вопрос модульности кода встал перед программистами еще в середине прошлого века, с 60-х и 70-х годов.

<img src="https://storage.yandexcloud.net/alfa-code-public-bucket/Blog/Images/punched_card_program_deck.jpg" alt="Стопка перфокарт" style="max-width: 100%; height: auto;"/> *Стопка перфокарт - в некотором роде, это первые программные модули*

Давайте взглянем на пару примеров реализации модульности у собратьев постарше. Мы не будем углубляться в их реализации, просто взглянем для общего мировоззрения.

C++

В C++ есть возможность включать части кода одного файла в другой благодаря команде #include. Можете попробовать данный пример в любом из онлайн компиляторов, например тут.

Пример модуля на C++:

// Подключаем модуль для работы с потоками ввода/вывода #include <iostream> int main() { // Используем API модуля для вывода текста. std::cout << "Hello world!"; return 0; }

Грубо говоря, при компиляции мы копируем весь код из одного файла и вставляем в другой. В современном C++ есть более современная система модулей, но сейчас мы ее опустим.

Java

Реализация модулей в Java появилась еще в самой первой версии языка. В Java это называется пакетами. Пакеты, по сути, являются файловой и логической структурой связей классов в мире java.

Обычно создается один или несколько классов в одном файле. Этот файл располагается в файловой структуре вашей ОС (Операционной системы). Существуют соглашения, как нужно называть ваши пакеты и где их располагать в иерархии пакетов. В другом файле вашей программы можно получить доступ к классам этих пакетов с помощью директивы import. Это пример простой программы с использованием пакета.

Пример модульности в Java:

// Импортируем пакет с различными вспомогательными утилитами, например для работы с датами import java.util.*; public class Main { public static void main(String[] args) { Date date = new Date(); System.out.println("Hello, World! Today is " + date); } }

В Java 9 появились настоящие модули. По сути это коллекции пакетов, но сейчас мы это так же опустим.

Python

Если C++ и Java это компилируемые языка, и модули становились доступными на этапе компиляции программы, то Python это языка скриптовых сценариев и он в каком то смысле ближе к JavaScript, чем предыдуще языки.

Модуль в Python — это файл, содержащий определения и операторы. Имя файла — это имя модуля с добавленным расширением .py. Внутри модуля имя модуля доступно как значение глобальной переменной __name__.

Вот небольшой пример:

Имеем файл с именем fibo.py

# Модуль считающий числа Фибоначчи def fib(n): a, b = 0, 1 while a < n: print(a, end=' ') a, b = b, a+b print()

Теперь, благодаря директиве >>> import мы можем получить доступ к этому модулю из других частей нашей программы:

# Подключаем модуль по его имени >>> import fibo # Используем функции из подключенного модуля >>> fibo.fib(1000) # 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

Заключение по примерам модулей в других языках

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

Ок, с примерами из других языков закончили. Перейдем к JavaScript.

История модульности JavaScript

JavaScript был создан в 1995 году Брэнданом Эйхом (Brendan Eich) и в начале своей "карьеры" не содержал модульности. На самом деле официальная модульность в языке появилась только в 2015 году с выходом нового стандарта языка ES6 или EcmaScript2015. Почему так поздно? Прошло целых 20 лет. Неужели в модулях не было потребности? Конечно же была. Проблемы здесь те же, что и в других языках программирования.

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

Кроме того, разработчики быстро нашли способ разделять программу на изолированные части и использовать их в общей программе.

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

В браузере мы имеем EventLoop - бесконечный цикл событий, обработки html, css и javascript. Скрипты javascript могут загружаться по частям и даже создаваться в момент работы веб страницы.

Ниже приведен пример того как веб страница загружает скрипты javascript:

<!DOCTYPE html> <html lang="en"> <head> <!-- Загружается первая часть скрипта --> <script src="/script_1.js"></script> <!-- Загружается вторая часть скрипта --> <script src="/script_2.js"></script> </head> <body> <!-- Тут и вовсе код скрипта вставлен прямо в html --> <script> console.log('Hello World!'); </script> </body> </html>

Но все же это еще не модули. Все переменные, все функции загружаемых скриптов попадают в глобальную область видимости программы, что может привести к коллизии имен переменных разных скриптов. Нам нужно как то изолировать скрипты друг от друга, но в то же время иметь возможность пользоваться их функционалом. На помощь приходит IIFE.

Немедленно вызываемая функция (Immediately Invoked Function Expression или IIFE)

Сначала термин: Немедленно вызываемая функция (Immediately Invoked Function Expression или IIFE) — это функция которая вызывается сразу после ее объявления.

А теперь по порядку, о том зачем нужно вызывать функцию сразу после ее создания. Все дело в том, что до 2015 года, когда появился стандарт ES6 (EcmaScript2015), не было никакой возможности скрыть объявления переменных от остальной части программы, кроме как объявить эти переменные внутри функции. При вызове этой функции создается отдельная область видимости программы, и тем самым достигается изолированность участка кода от остальной программы - создается "модуль". По правде функция нам бы и не нужна была, но это единственный способ спрятать переменный от основного окружения. Именно поэтому мы создаем ее и сразу вызываем, ведь мы хотим что бы содержимое нашего модуля было исполнено.

Ниже пример вызова IIFE:

(function(){ // ... })()

Что бы объявить функцию и сразу вызвать ее с помощью () - мы должны обернуть само объявление функции в круглые скобки. Таков синтаксис языка, иначе будет ошибка:

// Uncaught SyntaxError function(){ // ... }()

Так как функция нам нужна всего один раз, она может быть анонимной т.е. без имени.

Ниже пример использования IIFE:

(function(){ // Все переменные объявленные внутри IIFE будут недоступны в глобальной области видимости var x = 10; console.log(x); })()

Так же можете попробовать этот код в онлайн редакторе.

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

Ну и ради веселья добавлю, что IIFE можно определить и вызвать другими способами. Попробуйте, это может быть весело:

(function() { console.log("Скобки вокруг функции"); })(); (function() { console.log("Скобки вокруг всего"); }()); +function() { console.log("Выражение начинается с унарного плюса"); }(); !function() { console.log("Выражение начинается с логического оператора NOT"); }();

Паттерн Module

IIFE уже позволяет спрятать часть программы, но приносит и проблему. При подключении модуля функция IIFE вызывается, код модуля выполняется и на этом все. Получить доступ из глобальной области видимости в область видимости IIFE обычными средствами невозможно - это значит то, что все данные что мы объявляли в области видимости IIFE будут недоступны для остальной программы. Это не всегда ожидаемое поведение. Хорошо то, что исправить это можно благодаря паттерну Module.

// Вызываем IIFE var module = (function(){ // Инкапсуляция. Переменная count доступна только внутри области видимости IIFE var count = 0; // Возвращаем публичный интрфейс модуля return { count: function() { ++count; console.log('count:', count); } } })(); // Теперь мы можем использовать публичный интерфейс модуля module.count(); // count: 1 module.count(); // count: 2

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

Таким методом пользуются большинство подключаемых библиотек, например такие как lodash или Jquery.

Из минусов данного подхода можно отметить то, что паттерн Module так же как и IIFE не дает средства управления зависимостями. К примеру, если наш модуль зависит от внешней библиотеки Jquery, то мы должны самостоятельно убедиться что библиотека Jquery будет загружена до нашего модуля. Это неудобно, и при большом количестве скриптов может привести к большой путанице.

В заключении по IIFE и паттерну Module

Как мы увидели выше, синтаксис языка позволял реализовать подобие модуля и этого хватало на ранних этапах развития веба. В те старые годы программы на JavaScript были маленькие и данных подходов хватало для решения текущих задач. Но время шло, программы становились все объемнее, и потребности в модульных системах росли пропорционально. На помощь пришли реализации от сообщества разработчиков.

Модули от сообщества и стандартные реализации

Как я уже писал ранее, до стандарта ES6 (ES2015), в JavaScript не было официального синтаксиса для определения модулей. Это привело к тому, что разработчики изобрели свои собственные модульные системы. Ниже я привожу пример несколько наиболее известных реализаций:

  • AMD или Asynchronous Module Definition
  • UMD или Universal Module Definition
  • SystemJS

Часть из этих модульных систем потеряла свою актуальность. Но большинство из них все еще поддерживаются, а сборщики модулей (мы их разберем чуть ниже) все еще транспилируют их синтаксис. Так, что знать и различать эти системы модулей важно.

В рамках этой темы мы так же поговорим о модулях в NodeJS(CommonJS) и нативной реализации модулей в JS (ES6 modules)

  • CommonJS
  • ES6 modules

Разница между Загрузчиками модулей, Сборщиками модулей и Транспиляторами кода

Перед тем как мы начнем изучать различные типы модулей важно уяснить значение нескольких терминов.

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

Загрузчик модуля запускается в среде исполнения:

  1. В Браузере первым делом загружается загрузчик модулей.
  2. Вы сообщаете загрузчику, какой главный файл приложения загрузить и запустить, так называемую точку входа.
  3. Загрузчик модулей скачивает модуль точки входа и запускает код модуля.
  4. Загрузчик модулей скачивает дополнительные модули-зависимости по мере необходимости.

Если вы откроете вкладку «Сеть» в консоли разработчика в своём браузере, вы увидите, что многие файлы были загружены по запросу именно загрузчика модулей (Initiator).

Вот несколько популярных загрузчиков:

RequireJS: загрузчик модулей в формате AMD

SystemJS: загрузчик модулей в форматах System.register, AMD и нескольких других.

Сборщики модулей (Modules bundlers)

В свою очередь сборщик модулей заменяет собой загрузчик модулей. Однако в отличие от загрузчика модулей, сборщик модулей не работает в рантайме, его нужно запускать для сборки модулей в один бандл:

  1. Сначала у вас есть несколько модулей написанных в некотором формате модулей.
  2. Вы запускаете сборщик - сборщик объединяет все файлы в один файл.
  3. Вы подключаете итоговый файл (бандл) в браузер.

Если вы откроете вкладку «Сеть» в консоли разработчика в своём браузере, вы увидите, что загружен всего лишь один файл. Таким образом, отпадает необходимость в загрузчике модулей: весь код и так включён в один единый файл.

Несколько популярных сборщиков:

Browserify: сборщик для модулей CommonJS.

Webpack: сборщик для модулей AMD, CommonJS, ES6.

Rollup - так же как и вебпак, поддерживает разные форматы.

ViteJS - новичок, но уже очень популярен. Тоже многоформатный.

Транспиляторы JavaScript (JavaScript Transpilers)

Так же нужно сказать пару слов о транспиляторах кода JavaScript. Это не имеет прямого отношения к теме модулей, но транспиляторы помогают превратить код нового стандарта в более старый или добавить полифил. Работают они зачастую в паре с загрузчиками модулей и транспиляторами.

Транспиляторы JavaScript (также называемые transcompilers) — это компиляторы кода, которые преобразуют исходный код на диалектах языка JavaScript (CoffeeScript, TypeScript, LiveScript и т. д.), или современные версии языка JavaScript (ES2015, ES2017, ESNext и т. д.) в код с тем же функционалом, но для старых браузеров или в целях минификации.

Популярные примеры:

Babel - самый популярный транспилятор JavaScript.

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

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

CommonJS

Думая о модульных реализациях от разработчиков хочется начать со спецификации AMD. Но картина будет не полной если мы не разберем для начала CommonJS.

Если совсем просто, то формат CommonJS применяется в NodeJS (реализация JavaSctipt на сервере) и использует для определения зависимостей и модулей специальный синтаксис require и module.exports. Вот небольшой пример для визуализации:

// в одном файле, например module.js определяем модуль module.exports = function(){ // свойство exports может быть любым типом, хоть функция, хоть объект, хоть переменная }
// В другом месте программы импортируем модуль var module = require('./module.js'); // Вызываем наш модуль, ведь мы экспортировали модуль как функцию. module();

Теперь немного подробнее.

CommonJS — это самостоятельный проект (Ранее он назывался ServerJS), целью которого является определение набора спецификаций, помогающих в разработке серверных приложений JavaScript. Одной из задач, которую пытается решить команда CommonJS, являются модули. Разработчики NodeJS изначально намеревались следовать спецификации CommonJS, но позже отказались от нее. Почему? Ведь модули в NodeJS отлично работают. Так как же они от нее отказались если require и module.exports присутствуют в NodeJS.

Уточню еще раз. Спецификация CommonJS включает в себя не только работу с модулями. Она пытается решить ряд проблем при попытке вынести язык JavaScript на сервер. До популяризации NodeJS были и другие попытки реализовать JavaScript на сервере, при этом появилось довольно много проблем. Спецификация CommonJS как раз и предназначалась чтобы решить эти проблемы.

Это и работа и с вводом/выводом, работа с файловой системой, интерфейсы операционной системы и т.д. Подробнее о CommonJS смотрите в вики их группы.

Команда NodeJS сначала пыталась соответствовать этой спецификации, но позже реализовала из всей спецификации CommonJS только модульную систему (и только частично). Вот такая вот путаница.

NodeJS используют лишь часть спецификации CommonJS, а модули называются CommonJS модулями, хоть и не являются точной реализацией из спеки.

Почему же так вышло? Вот обьяснение создателя NPM Исаак З. Шлютера:

Однажды вечером в Joyent (компания в которой он тогда работал), когда я упомянул, что немного расстроен какой то нелепой функцией из спецификации CommonJS, которая, как я знал, была ужасной идеей, он сказал мне: «Забудь о CommonJS. Он мертв. Мы — серверный JavaScript». - Создатель NPM Исаак З. Шлютер цитирует создателя Node.js Райана Даля.

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

Итого.

CommonJS - спецификация для реализации серверного JavaScript.

CommonJS modules в NodeJS - реализация спецификации CommonJS по части модульности в NodeJS.

Если же быть еще точнее, модульная система в NodeJS отличается от той которая описана в спецификации CommonJS. Но над модульной системой NodeJS есть абстракции в виде библиотек, которые устраняют различия между модулями NodeJS и CommonJS и сохраняют их синтаксис.

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

Своеобразное различие между реализацией NodeJS и спецификацией модулей CommonJS возникает в форме объекта module.exports. В Node module.exports — это реальный специальный объект, который экспортируется, а exports — это просто переменная, которая по умолчанию привязывается к module.exports.

Спецификация модулей CommonJS, с другой стороны, не имеет объекта module.exports. Фактически в Node невозможно экспортировать полностью предварительно сконструированный объект без прохождения через module.exports. Обязательно посмотрите этот пример - вам сразу станет яснее.

// Это не сработает, замена переменной exports полностью нарушит привязку к modules.exports. modules.exports останется тем же объектом. exports = (width) => { return { area: () => width * width }; } // А вот это работает как и ожидается module.exports = (width) => { return { area: () => width * width }; } // Заметьте, что замена module.exports автоматом меняет и ссылку на переменной exports. Это может быть контринтуитивно.

Модули CommonJS были разработаны для работы на стороне сервера. Естественно, их API синхронный. Другими словами, модули загружаются в момент выполнения скрипта и в том порядке, в котором они определены внутри исходного скрипта.

Плюсы:

  • Простота: разработчик может понять концепцию, не заглядывая в документацию.
  • Управление зависимостями уже интегрировано: модули могут зависеть от других модулей и загружаются в нужном порядке.
  • require можно вызывать где угодно: модули можно загружать программно. Циклические зависимости так же поддерживаются.

Минусы:

  • Синхронный API таких модулей не дает возможности реализовать их стороне клиента.
  • Для работы в браузерах требуется специальная библиотека загрузчик или транспиляция кода.
  • Статическим анализаторам сложнее разбирать такой код.

Реализация модулей CommonJS

Чуть выше мы уже поговорили о частичной реализации модулей из спецификации CommonJS в Node.js.

Что же касается браузеров, то нужно использовать транспиляторы и бандлеры. Вот пара популярных вариантов: webpack и browserify. Конечно, это не настоящая имплементация данной спецификации, но данные "бандлеры" (сборщики) могут анализировать ваш код с использованием синтаксиса require и объединять все зависимости в один итоговый файл. Таким образом и достигается возможность работы таких модулей на стороне клиента в браузере.

Вот что на главной Browserify: "Browserify lets you require('modules') in the browser by bundling up all of your dependencies." - "Browserify позволяет вам использовать require('modules') в браузере, объединяя все ваши зависимости." Мне этот заголовок особенно понравился, все сразу понятно.

Browserify был специально разработан для синтаксического анализа модулей, подобных Node (многие пакеты в Node готовы к транспиляции при помощи Browserify и работе в браузере) и объединении вашего кода и кода из этих модулей в одном файле, который содержит все зависимости.

Webpack же был разработан для создания сложных конвейеров трансформаций исходных файлов перед их публикацией. Эти трансформации включает в себя объединение модулей CommonJS.

<img src="https://storage.yandexcloud.net/alfa-code-public-bucket/Blog/Images/webpack.png" alt="Логотип Webpack" style="max-width: 100%"/>

Если еще проще - то и Webpack, и Browserify заменяют ваши вызовы require на сами эти модули, конечно же не обычной вставкой кода, а с использованием своих собственных реализаций модульности.

<img src="https://storage.yandexcloud.net/alfa-code-public-bucket/Blog/Images/browseryfy-logo.png" alt="Логотип Browseryfy" style="max-width: 100%"/>

Таким образом модули CommonJS могут быть так же и на клиенте, в браузере, хоть и с большими оговорками.

AMD (Asynchronous Module Definition или Асинхронное определение модуля)

Теперь, изучив реализацию модулей CommonJS, мы наконец можем вернуться к спецификации AMD.

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

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

Спецификация предоставляет следующее API:

// Объявляем текущий модуль // В массиве передаем список зависимостей этого модуля // Колбек - тело модуля, который в аргументах функции получит загруженные зависимости define(['dep1', 'dep2'], function (dep1, dep2) { // Определяем значение текущего модуля, возвращая значение. return function () {}; }); // Так же мы можем использовать такой синтаксис define(function (require) { var dep1 = require('dep1'), dep2 = require('dep2'); return function () {}; }); // Мне он нравится больше, так как он больше похож на реализацию модулей в CommonJS

Асинхронная загрузка стала возможной благодаря использованию замыкания в JavaScript: колбек вызывается, только когда модули-зависимости полностью загрузились. Определение модуля и импорт модуля выполняются одной и той же функцией: когда модуль определен, его зависимости становятся явно описанными. Таким образом, загрузчик AMD модулей может иметь полное представление о графе зависимостей для проекта во время выполнения. А это значит что библиотеки, которые не зависят друг от друга при загрузке, могут быть загружены параллельно. Это особенно важно для браузеров, где время загрузки и запуска приложения имеет важное значение для удобного взаимодействия с пользователем.

Плюсы:

  • Асинхронная загрузка (лучшее время запуска).
  • Циклические зависимости поддерживаются.
  • Синтаксис немного похож на require и exports.
  • Удобное управление зависимостями.
  • При необходимости модули можно разделить на несколько файлов.
  • Поддержка плагинов.

Минусы:

  • Cложнее синтаксически.
  • Для таких модулей необходима специальная библиотека загрузчик.
  • Статическим анализаторам сложнее разбирать такой код.

Реализация модулей AMD

В настоящее время наиболее популярными реализациями AMD являются require.js и Dojo.

<img src="https://storage.yandexcloud.net/alfa-code-public-bucket/Blog/Images/requirejs-logo.svg" alt="Логотип requirejs" style="max-width: 100%"/>

Использовать require.js довольно просто: включите библиотеку в свой HTML-файл и используйте атрибут data-main, чтобы сообщить require.js, какой модуль следует загрузить первым.

<!DOCTYPE html> <html> <head> <title>My Sample Project</title> <!-- data-main атрибут сообщает require.js загрузить scripts/main.js после загрузки самой require.js --> <script data-main="scripts/main" src="scripts/require.js"></script> </head> <body> <h1>My Sample Project</h1> </body> </html>

Внутри main.js вы можете использовать requirejs() для загрузки любых других скриптов, которые вам нужно запустить. Это обеспечивает единую точку входа.

define(["helper/util"], function(util) { // Эта функция вызывается после загрузки scripts/helper/util.js. Если сам модуль util.js вызывает define(), то эта функция не запускается до тех пор, пока зависимости util не будут загружены. });

У Dojo похожая установка.

ES2015 (ES6) Modules

К счастью, команда ECMA, отвечающая за стандартизацию JavaScript, решила заняться проблемой модулей. Результат можно увидеть в стандарте ECMAScript2015 (ранее именовался как ECMAScript 6). Результат синтаксически приятен и совместим как с синхронным, так и с асинхронным режимами работы.

Пример работы нативных модулей ES2015:

//------ lib.js ------ export const sqrt = Math.sqrt; export function square(x) { return x * x; } export function diag(x, y) { return sqrt(square(x) + square(y)); } //------ main.js ------ import { square, diag } from 'lib'; console.log(square(11)); // 121 console.log(diag(4, 3)); // 5

Директиву import можно использовать для переноса модулей в нужную нам область видимости. Эта директива, в отличие от require и define, не является динамической (т. е. ее нельзя вызывать в любом месте). Директива export, с другой стороны, может использоваться, чтобы явно сделать элементы общедоступными.

"Нативная" природа директивы import and export позволяет статическим анализаторам строить полное дерево зависимостей без запуска кода. ES2015 не поддерживал динамические импорты, но к счастью стандарт ES2020 уже отлично их поддерживает.

// Работает это с помощью Promise import('./module.mjs').then((module) => { // Экспортируемые переменные доступны через объект модуля module.myFunction(); });

Плюсы:

  • Поддерживается синхронная и асинхронная загрузка.
  • Синтаксически просто.
  • Поддержка инструментов статического анализа.
  • Интегрирован в язык (в итоге поддерживается везде, библиотеки не нужны).
  • Поддерживаются циклические зависимости.

Минусы:

Реализация модулей ES2015 (ES6)

Стоит отметить что спецификация EcmaScript ES2015 Modules оговаривает синтаксис API, но реализация в разных средах может отличается.

На данный момент модули ES2015 реализованы почти во всех популярных браузерах, а так же на стороне сервера в NodeJS. Да, NodeJS теперь тоже поддерживает модули стандарта ES2015, наравне с CommonJS. Поведение модулей в браузере и в NodeJS немного отличаются.

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

Для старых браузеров присутствует полифил.

Помимо "нативной" реализации в браузерах и на стороне сервера в NodeJS, можно использовать транспиляторы кода. Например такой транспилятор как Babel (с настройкой транспиляции ES2015) легко полифилит эти модули.

<img src="https://storage.yandexcloud.net/alfa-code-public-bucket/Blog/Images/babel.png)" alt="Логотип Babel" style="max-width: 100%"/>

Стоит, также отметить, что бандлер Webpack так же отлично понимает синтаксис модулей ES2015 и при бандлинге (объединении в один файл) подменяет импорты ES2015 на свою систему вызова модулей в одном файле. Сейчас в эту тему сильно погружаться так же не будем. Как нибудь в другой раз.

System.js

System.js — это еще один вид загрузчик модулей (И по совместительству тип модуля System.register), пытающийся реализовать стандарты модулей ES2015 до внедрения этих самых стандартов в браузеры. Предназначен он для работы в браузере.

SystemJS был разработан еще в 2013-м году для проекта jspm, в то время, когда RequireJS был еще очень распространенным загрузчиком модулей. Параллельно разрабатывался стандарт ES6, но нативная реализация модулей была еще очень далека от интеграции в браузеры. Идея SystemJS была простой: создать инструмент, для загрузки модулей в браузере с максимально похожей на стандарты реализацией.

К сожалению, API библиотеки часто переписывался, что негативно влияло на популярность библиотеки. На данный момент, с поддержкой нативных модулей ES2015 в браузерах, потребность в данной библиотеке и вовсе отпадает.

Так же отмечу, что данная библиотека - ярчайший пример того, как не стоит документировать библиотеки. Как говорится - "без поллитру не разберешься!"

Минимальный пример использования:

<!-- В HTML загружаем system.js --> <script src="./node_modules/systemjs/dist/system.js"></script> <!-- Указываем точку входа --> <script type="systemjs-module" src="./js/module_1.js"></script>

Пример модуля:

// В массиве указываются зависимости System.register(['jquery'], function (_export, _context) { return { setters: [], execute: function () { // Тело модуля } }; });

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

В данном репозитории можно ознакомиться с примерами использования, а на этой странице есть живое демо.

Плюсы:

  • Поддержка загрузки AMD и CommonJS
  • Транспиляция кода на лету.
  • Поддержка динамических карт импорта.
  • Поддержка загрузки типов модулей .css, .wasm, .json в соответствии с существующими спецификациями модулей.

Минусы:

  • Сложен синтаксически
  • Сложен синтаксический анализ
  • Дурацкая документация

UMD (Universal Module Definition)

Универсальное решение для загрузки модулей как на сервере, так и на клиенте.

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

Шаблон UMD обычно пытается обеспечить совместимость с наиболее популярными загрузчиками скриптов (например, RequireJS среди прочих). Во многих случаях он использует AMD в качестве основы с добавлением специальной оболочки для обеспечения совместимости с CommonJS. Т.е. Шаблон UMD работает поверх вышеописанных инструментов для достижения универсальности таких модулей.

Сами же шаблоны реализуют такие сборщики как Webpack или Rollup.

Так же не могу, не вставить цитату из одного блога. Уж очень хорошо получилось: Образец UMD, по общему признанию, уродлив, но совместим как с AMD, так и с CommonJS, а также поддерживает определение «глобальных» переменных старого стиля (стиль «когда твой папка писал под ie8») :

В данной статье хорошо объясняется как именно это работает. Не поленитесь, ознакомьтесь.

В наше время, с учетом хорошей поддержки модулей ES2015 и на стороне браузера, и на стороне клиента, необходимость в данной библиотеке так же отпадает.

Теперь когда вы знакомы с различными типами модулей, в том числе и с UMD можете заглянуть в node_modules в наш любимый React и обнаружить там папки cjs и umd. Множество библиотек собираются в такой конфигурации, ведь никто не может предсказать как и в каком окружении будет использоваться библиотека. Теперь мы знаем что cjs это синтаксис CommonJS в NodeJS, но может быть преобразован с помощью бандлеров. UMD сборка может быть сразу использована в итоговом html, без дополнительного преобразования. Вот наглядный пример.

Плюсы:

  • Универсальный. Подходит для любых сред и любых загрузчиков модулей.

Минусы:

  • Сложен синтаксически

Заключение

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

<img src="https://storage.yandexcloud.net/alfa-code-public-bucket/Blog/Images/js-modules.jpeg" alt="Запутанность модулей в JavaScript" style="max-width: 100%"/>

Давайте повторим пройденное:

Модуль — это некоторая переиспользуемая часть кода. Ее можно легко подключить в другом участке кода.

Для общего мировоззрения мы посмотрели на синтаксис модулей в языках С++, Java, Python.

Формат модуля — это синтаксис, который используется для определения модулей. В JavaScript существуют разные форматы модулей: AMD, CommonJS, UMD и System.register, а так же нативный формат модулей ES2015.

Загрузчик модуля интерпретирует и загружает модуль, написанный в требуемом формате, во время выполнения (в браузере). Распространённые — RequireJS и SystemJS.

Сборщик модуля заменяет собой загрузчик модулей и собирает пакет, обычно один файл. Популярные сборщики — Browserify, Webpack, Rollup, Vitejs.

Мы посмотрели на историю развития модульности в JS. Выяснили что создание модулей и обработка зависимостей в прошлом были очень громоздкими. Новые решения в виде библиотек или модулей ES2015 устранили большую часть проблем. Если вы хотите начать новый проект, стандарт модулей ES2015 — правильный выбор. Он уже отлично поддерживается в браузерах, а так же реализован в NodeJS. Проблемы поддержки в старых браузерах легко решаются при помощи транспиляторов и полифилами.

С другой стороны, если вы имеете большое количество легаси кода в вашем проекте можно использовать простой старый синтаксис ES5, и обычным выбором остается разделение между AMD (или SystemJS) для клиента и CommonJS/Node для сервера.

И хотя старые модульные реализации постепенно отходят на второй план, знать о них необходимо, так как они все еще поддерживаются и их использование всплывает то тут - то там.

Надеюсь мне удалось развеять туман модульности JavaScript, и в следующий раз вы с легкостью сможете распознать тот или иной модуль.

Не стесняйтесь писать комментарии, указывать на неточности - ведь мы вместе делаем общее дело.

Делитесь статьей, если она показалась вам полезной.

И если вы дочитали до конца - вы это заслужили!


Помимо прочих ссылок, для написания статьи я использовал эти материалы:

A 10 minute primer to JavaScript modules, module formats, module loaders and module bundlers

JavaScript Module Systems Showdown: CommonJS vs AMD vs ES2015

Поделиться: