🔥

Тред (Павел Лосев)


Первой темой будут системы модулей в JavaScript. На данный момент самые популярные - это ECMAScript Modules (ESM), CommonJS и UMD. Узнаем кто такие эти ваши модули, почему у JS аж 3 (и более, если считать легаси) модульные системы, и как там у ESM в Node.

Самое простое определение: модуль — это переиспользуемый кусок кода, который содержит реализацию API. Обычно это кусок кода, который экспортирует функционал, чтобы позже его мог импортировать другой кусок кода. Вроде разобрались, теперь немного истории.

Давным давно, ещё до существования Node.js и ES6, в JavaScript не было никакого другого способа разделять JavaScript, как добавлять глобальную переменную, указывающую на библиотеку.

Таким образом работает IIFE (Immediately Invoked Function Expression) - он сразу же вызывает ф-цию с обёрнутым модулём и создаёт глобальную переменную, которая содержит весь функционал либы. Включить такую библиотеку было довольно легко, всего лишь надо подключить <script>.

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

Были вспомогательные модульные системы по типу System.js и AMD (Async Module Definition), но это не было стандартом, т.к. всё это требовало загрузки ещё одного скрипта самой модульной системы.

В 2009 появилась такая штука как Node.js. И у неё тоже появилась своя модульная система, которая называется CommonJS (это который require). Очень активно используется до сих пор, и появилась она опять же ввиду того, что не было стандарта модулей. Пришлось изобретать своё.

Помимо уже трёх самых популярных IIFE, AMD и CommonJS появилась ещё одна система, которая объединяет в себе сразу все 3 - UMD (Universal Module Definition). До сих пор очень популярный сегодня формат. Нашёл статью с примером того, как выглядит UMD внутри: syntaxsuccess.com/viewarticle/ii….

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

Из-за того что модули вошли в язык как стандарт довольно поздно, до сих пор абсолютное большинство JavaScript кода в конечном счётё использует либо UMD (в основном на фронте, но и на бэке тоже), либо CommonJS (на бэке).

Вернёмся к ESM. Благодаря тому, что в ESM существуют именованные импорты, сборщикам гораздо проще делать tree-shaking - включение в бандл только того, что импортировалось.

Это касается только сборщиков, насколько я знаю в Node.js рантайме никакого tree-shaking нет, но им можно воспользоваться при помощи сборщиков (Rollup).

А что там у ноды? В Node.js (без флагов) ESM появился только лишь в 2019 году в версии 13.2.0. Чтобы запустить файл с ESM, нужно поменять расширение на .mjs, или добавить "type": "module" в package.json и продолжать юзать .js. И тогда для CJS модулей использовать .cjs.

В чём смысл Node ESM? Во-первых, это следование стандарту (и унификация BE и FE кода). Во-вторых, именованные импорты, то есть вместо экспорта объекта в CJS (module.exports = { a, b }), можно экспортировать их сразу, без всяких объектов (export const fn = () ⇒ { ... }).

Однако, не всё так просто с ESM. Большинство модулей всё ещё CJS-only. Т.к. CommonJS не умеет в именованные экспорты, импортировать CJS модули приходится через import mod from 'mod'. Node.js их спокойно хавает из импортирует внутри вашего ESM кода.

Ещё есть один важный момент - в относительных путях всегда надо указывать расширение файла, иначе будет Cannot find module. Ещё и вдобавок это не будет работать с CommonJS (если попытаться импортировать ESM в CJS). Для решения этих проблем в Node.js было добавлено поле "exports".

Это поле позволяет инкапсулировать модуль, и экспортировать только определённые файлы, без всяких расширений. Причём можно везде прописывать фоллбеки на CommonJS, чтобы ваш модуль работал как и в старых, так и в новых версиях Node.js.

Пример заполнения такого поля: github.com/talentlessguy/…

Теперь надо подумать, как это всё собирать, чтобы не писать параллельно несколько файлов для CJS и ESM? Существует море сборщиков, которые могут облегчить это дело, генерируя сразу 2 версии из одного исходника.

Из самых популярных можно взять Rollup. esbuild также поддерживает как CJS так и ESM. Лично я использую обёртку над esbuild - tsup. Там легче указывать флаги, и ещё можно генерить типы после компиляции (esbuild пока не поддерживает .d.ts генерацию).


ещё вспомнил - microbundle: github.com/developit/micr…

Я сейчас коллекционирую модули с Node ESM поддержкой, если кому интересно (добавить свой / посмотреть), то вот ссылка: github.com/talentlessguy/… Не очень густо, т.к. как я говорил ранее, 90% модулей только на CommonJS

Вспомнил ещё один приём, чтобы не собирать две версии отдельно, можно импортировать CJS в ESM и потом прописать именованные экспорты nodejs.org/api/esm.html#e… Довольно годный вариант, чтобы ничего не собирать Но это только для JavaScript библиотек, у TS легче dual bundling

@jsunderhood Ещё одно дополнение (zanuda mode on). CommonJS как инициатива по разработке унифицированных API (не только модулей) появилась до Node.js. Node.js взял оттуда только модульную систему. Вся инициатива сошла на нет, и CommonJS стал означать только модульную систему
Поправочка, CommonJS существовал до Node.js twitter.com/myshov/status/…

@jsunderhood Может я не понимаю слово «вспомогательные», но в своё время AMD очень мощно конкурировал с CommonJS, и никогда себя не позиционировал как служебный формат модулей (в отличие от System.js)
Тут ошибка, AMD можно было собирать бандлером (через r.js), а не только через лоадер twitter.com/myshov/status/…