ES Modules — единственная модульная система которая должна остаться в современной JavaScript разработке
В прошлом месяце опубликовал статью, в которой рассказал о разработке JavaScript-полифила для псевдокласса :has(), в которой я задел тему проблем одновременной поддержки работы различных видов модулей в JavaScript и TypeScript. Т.к. это проблема еще очень актуальна, я решил немного рассказать своими словами, какие типы модулей все еще существуют в мире JavaScript и почему стоит заканчивать этот цирк и поддерживать только EcmaScript Modules.
Небольшой исторический экскурс по модульным системам в JS
В JavaScript до недавнего времени не существовало нативной поддерки разбиения кода на отдельные модули на уровне языка. Можно конечно упомянуть про поочередную загрузку через тег <script> в браузерах, но это всетаки относится не к JavaScript, а к браузерному API.
Но т.к. никто не мешает нам загружать по сети код и запускать его через тот же eval, логично что стали появляться касточные решения для обеспечения модульности:
- ESM (EcmaScript Modules) - спасительная звезда современной модульной системы в JavaScript, которая пришла с появлением стандарта ECMAScript 6 и соответственно имеет нативную поддержку в языке.
- IIFE-модули - из названия следует, что это тупо код, который следуя соглашению тупо оборачивается в IIFE (Immediately Invoked Function Expression).
- CommonJS - модульная система, которой спасались в NodeJS до появления официальной поддержки ESM.
- AMD (Asynchronous Module Definition) - древнее решение, которое еще вполне может встретиться в старом коде.
- UMD (Universal Module Definition) - тот случай, когда разработчики посмотрели на зоопарк из IIFE, AMD, CommonJS и решили сделать свое универсальное и совместимое со всем решение.
- Всякие экзотические решения, которые не снискали широкой популярности.
В общем как видите, разработчикам на JavaScript раньше не приходилось скучать пр разработке больших приложений.
Использование модульных систем в современном JavaScript коде
Можно конечно в статье долго и нужно расписывать нюансы каждой из вышеперечисленных модульных систем, но этой информации и так в интернете полно, а я просто скажу вам одно - пишите новый код только с использованием современной ES Modules, ибо все остальные решения являются костылями на бедным JavaScript.
В настоящее время ESM широко поддерживается в современных браузерах, в т.ч. динамические импорты, а в NodeJS поддержка есть уже с 8-ой версии.
В старом коде, который приходится сопровождать, конечно придется пользоваться той модульной системой, которая там уже работает и к сожалению на текущий момент эта тема все еще актуальна.
NodeJS и ESM
Вопрос поддержки модульности в старом коде особо остро стоит в NodeJS. Хоть NodeJS и давно поддерживается ESM, там уже понаписано огромное количество кода под CommonJS, который вполне работает и не факт что этот код будет переписан под ESM. Поэтому NodeJS на текущее время вынужден официально поддерживать распространение кода как в формате CommonJS, так и ESM.
Одним из удобных моментов в CommonJS было отсутствие необходимости указывать расширение файла отдельно подключаемого модуля, в то время как ESM требует расширение файла.
Ранее в NodeJS, подключать модули без указания расширения файла можно было добавив при запуске NodeJS параметр --experimental-specifier-resolution=node, но в 19-ой версии эту возможность убрали. Особенно эта проблема актуальна в случае с TypeScript, поскольку он при компиляции не модифицирует импорты, при том что в нем допустимы импорты без расширения файла, а указывать расширение .js в импортах TS выглядит жутко неестественно, а также иногда является.причиной неприятных сюрпризов. Проблему с импортами в скомпилированных ESM модулях позволяют в итоге решить различные бандлеры (Rollup, Webpack, ...), которые вполне умеют разруливать различные виды импортов (с расширением и без него). Но если вы в дальнейшем планируете использовать бандл с TypeScript, то помимо сборки бандла придется озаботиться сборкой единого файла деклараций .d.ts, что обычно решается отдельными плагинами к бандлам. В общем там немало проблем и при создании полифила я с многими из них столкнулся и хочу сказать, что тема с расширениями файлов в импортах TypeScript на самом деле очень широкая и с большим количеством неоднозначностей, что однозначно выходит за рамки текущей статьи.
Но вернемся к JavaScript. Не удивлюсь, если со временем (но вероятно очень нескоро) разработчики NodeJS примут решение оставить поддержку только ESM, тем самым поставив окончательную точку в вопросе различных соглашений по JavaScript модулям.
UMD для старого кода
Тем не менее, хоть я и настаиваю на написании кода с использованием только ESM, для работы кода в окружении которое не поддерживает ESM (старые версии NodeJS и браузеров), вы вполне можете компилировать ESM модули в бандлы в виде универсального UMD модуля.
Почему именно UMD? Суть в том, что UMD является универсальным решением, которое может работать и как CommonJS и как AMD и соответственно может их заменить. Также UMD можно использовать напрямую в браузере через тег <script src="...">, без необходимости в каких-либо сторонних загрузчиках. В общем универсальное решение для работы с устаревшим кодом.
Заключение
В современном JavaScript коде я рекомендую использовать только ESM, ибо это стандарт, который призван избавить нас от всего этого модульного зоопарка. Все эти модульные системы, которые были до ESM нужно забыть как страшный сон и оставить их в прошлом. Ну разве что еще можно пугать ими JavaScript новичков.
Я помню как непомерно радовался первым нативным реализациям ES Modules в браузерах, поскольку это был довольно таки серьезный шаг в развитии JavaScript и по возможности я сразу стал использовать их в проектах и с тех пор ниразу не пожалел об этом решении.
Сейчас еще существуют проблемы совместной работы ESM и старых модульных систем, ибо как я уже говорил, старого кода уже понаписано много и он вполне работает. Но с момента первых реализаций ESM уже прошло достаточно много времени и проблема с каждым годом становится все менее актуальной и со временем окончательно исчезнет.