- PVSM.RU - https://www.pvsm.ru -
Владислав Власов, инженер-программист в Developer Soft [1] и преподаватель курса Нетологии [2], специально для блога написал цикл статей о EcmaScript6. В первой части [3] на примерах рассмотрели динамический анализ кода в EcmaScript с помощью Iroh.js, во второй [4] сосредоточились на реализации отменяемых Promises. В этой статье поговорим об истории ES6-модулей.
История языка EcmaScript простирается от простого языка сценариев в браузере вплоть до современного языка общего назначения, работающего в различных хост-окружениях. Вместе с усложнением языка появилась и необходимость организации модульной структуры и переиспользования кода с помещением его в библиотеки. Первые библиотеки импортировались за счет загрузки соответствующего JS-файла с хоста поставщика или CDN, а взаимодействие производилось, как правило, посредством экспорта функций и классов с заранее известными именами в глобальное пространство — объект window.
Такая схема применялась достаточно долго, и в простых случаях работает она вполне успешно.
Сложности начинаются, когда библиотек и взаимосвязей между ними становится слишком много.
Во-первых, засоряется глобальный хост-объект, и все подключаемые библиотеки должны импортировать не конфликтующие уникальные имена. Во-вторых, нет никакого явного способа обеспечить взаимодействие между библиотеками и осуществить переиспользование.
Вопрос вложенных зависимостей может решаться с помощью
dynamic <script> injection
в DOM-модели, а переиспользование может достигаться за счет экспорта с известным именем в глобальном хост-объекте, однако это не универсальное решение и строится оно исключительно на неявном соглашении между авторами библиотек и использующим их клиентском сценарии. Частично согласование имен решается посредством передачи CDN-серверу в query string
параметров, специфицирующим пространства имен для загружаемой JS-библиотеки, но это также не универсально.
Остаются некоторое фундаментальные проблемы, связанные с асинхронной загрузкой и взаимодействием с DOM-моделью. Некоторые библиотеки должны быть загружены раньше других, если вторые имеют зависимость от первых. В случае с динамическим импортом это требует правильной установки async-флага или манипуляции с событием readystatechanged
, в зависимости от вендора и версии браузера.
Конечно, и для этого общего случая есть решение, описанное в статье [6]. Однако, во-первых, оно требует тщательного слежения за зависимостями во всех загружаемых библиотеках, и во-вторых, если некоторые библиотеки представляют собой polyfills
, которым требуется отслеживание DOM-состояния и событий. В случае defer fallfack
это не заработает.
Для универсального решения вышеописанных задач было разработано несколько стандартов организации библиотечных модулей для JS, самые известные из них — AMD [7] (Asyncronous module definition), UMD [8] (Universal module definition) и CommonJS [9]. За счет следования авторами модулей общего формата декларации и наличия общего загрузчика файлов, большинство проблем было решено.
Тем временем активно развивалась платформа Node.JS, где зависимости модулей были решены совершенно другим способом — посредством синхронного require-вызова, а модули имели соответствующий специфичный формат. Тогда технический комитет TC-39 начал разработку универсального средства импорта модулей, которое должно было решать все вышеобозначенные задачи и при этом работать одинаково на сервере и клиенте и обеспечивать синхронную и асинхронную семантику загрузки модуля. Таким средством стали ES6-модули.
С появлением спецификации Ecmascript 262 version 6 и последующих редакций, в язык добавлялось множество новых синтаксических конструкций и native-функций. Как правило, большинство из них могло легко запускаться и на старых версиях JS-движков за счет предварительного transpile-инга — для синтаксических конструкций, и добавления polyfills — для недостающих функций.
ES6-модули [10] же обеспечивали синхронную не блокирующую семантику загрузки, binding-привязки для экспортируемых/импортируемых сущностей, модульная область видимости и другие аспекты, которые не просто обеспечить обычным transpile-ингом.
Разработчики хотели создавать веб-приложения на актуальном диалекте Ecmascript 6, 7, 8-й и поздних версиях [11], а для этого требовалось удобство по выполнению transpile-инга и добавлению соответствующих polyfills
для приложений автоматическим образом, чтобы разработанное приложение могло работать и в относительно старых браузеров без проблем.
Совокупным решением этих задач стали bundle builder
, настраиваемые вместе с подключаемыми transpilers
и polyfills
. Идея состоит в том, что код приложения преобразуется в эталонный диалект, который считается поддерживаемыми всеми актуальными браузерами, например, ES3 или ES5 — в зависимости от задачи. После этого все файлы библиотечных модулей и кода приложения соединяются в один большой файл — так называемый bundle. Этот файл отправляется на клиент и уже не требует никаких синхронных или асинхронных импортов, поскольку весь необходимый код уже находится в bundle
и доступен по кодовым номерам.
Известные решения, имплементирующие соответствующий подход: Browserify [12] и Webpack [13], причем последний в настоящее время является фактически стандартом де-факто. Транспайлером де-факто является Babel [14]. Предложенная схема имеет большое количество преимуществ.
Во-первых, благодаря наличию в схеме transpiler-а, исходный проект может быть фактически написан на любом языке. Как правило, это EcmaScript или TypeScript последней версии, но возможности по расширению синтаксиса практически безграничны. Одно из известных расширений для ES — JSX [15], используемый в библиотеке React и ее производных.
Во-вторых, за счет контроля преобразования кода в фазе transpile-инга, имеется возможность внедрения поддержки даже такой функциональности, как ES6 proxy [16] или рефлексивной информации в коде [17].
Среди интересных следствий применения bundle-ирования кода — возможность написание клиентского кода на языке F# [18] или Ocaml [19] и многое другое.
Помимо очевидных преимуществ, решение с bundle-ированием имеет и ряд очевидных недостатков.
Во-первых, результирующий bundle
, даже с учетом возможного сжатия, имеет довольно большой объем и может быть ощутим при мобильном трафике. Во-вторых, bundle
включает в себя абсолютно все зависимости веб-приложения, которые будут загружены и интерпретированы в браузере пользователя, даже если тот не воспользуется элементами веб-приложения, в которых они нужны. В-третьих, пропадает возможность кэширования библиотечных зависимостей, поскольку bundle
или полностью актуален, или требует полного обновления.
Негативные эффекты проявляются и при разработке и отладке приложения. Поскольку bundle-ирование почти всегда сопряжено с transpile
, то процесс получения нового bundle
, особенно для крупного проекта, может идти долго. Это означает, что в процессе разработки и отладки, после внесения очередного изменения, требуется пересборка bundle
и загрузка его новой версии на клиент. Кроме того, за счет машинного преобразования исходного кода, он становится практически нечитаем, что ведет к сложностям в использовании отладчика в браузере.
Конечно, большая часть обозначенных выше проблем имеет свои решения. Для того чтобы в production-режиме не загружать в браузер весь код приложения целиком, в webpack используется технология code chunk splitting [20]. Можно использовать и динамическую версию импорта, возвращающую Promise и обеспечивающую асинхронную загрузку целевого модуля.
Для отладочных целей тоже имеются решения. Просмотр оригинального исходного кода, и даже навигация по нему в отладчике браузера достигается посредством спецификации source maps
, внедряемых в целевой bundle
в режиме разработки. Частичное обновление без полной перезагрузки bundle
решается при помощи Hot Module Reload, хотя действительно инкрементальное обновление корректно работает только в простых случаях.
Схема с bundle-ированием зависимостей была актуальной для своего времени, но на текущий момент все современные браузеры имеют нативную поддержку ES6-модулей.
Это требует пересмотра взгляда на сборку современных web-приложений, поскольку bundle
были необходимостью из-за несовершенства и отсутствия требуемой функциональности в браузерах. После её появления, использование нативных конструкций обеспечивает гораздо лучший результат.
Во-первых, излишний transpile
синтаксических конструкций и замены его на эмулирующий код приводит к замедлению и затруднению оптимизаций. Это касается async и generator-функций, заменяемых на regenerator runtime [21], и лексических переменных let
/const
, преобразуемых в неоптимальные var-декларации.
Конечно, это не имеет прямого отношения к ES6-модулям как таковым, но обычно определяется схемой сборки и доставки на клиент приложения. В этом смысле это взаимосвязанные вещи.
Во-вторых, модули эффективны с точки зрения производительности. ES6 модули загружаются и исполняются отложенным образом по умолчанию. Это значит, что невозможно по ошибке осуществить добавление блокирующих модулей к веб-приложению, и соответственно из коробки нет никакой SPOF [22] проблемы.
Для сохранения работоспособности в старых браузерах, не имеющих поддержку ES6-модулей, можно иметь собранный bundle
и отдавать его для старых агентов. При этом, благодаря особенностям конструкции импорта ES6-модулей, не требуется условной настройки webpack с сегрегацией поставляемого кода в зависимости от User-Agent строки браузера, или средств feature discovery.
Для разграничения достаточно следующего кода:
<html>
<head>
<script src="app/index.js" type="module"></script>
<script src="dist/bundle.js" defer nomodule></script>
</head>
<!-- … -->
</html>
Браузер без поддержки ES6-модулей просто загрузит dist/bundle.js и будет работать по старой схеме. Современный браузер возьмет app/index.js в качестве точки входа и будет загружать зависимые ресурсы автоматически.
О вопросах эффективной настройки webpack-а для рассмотренной выше схемы, асинхронной и отложенной загрузки модулей, кэшировании зависимостей, inline-модулях и CORS-политиках для них можно прочесть более детально: «ES6 modules support lands in browsers: is it time to rethink bundling? [23]» и «ECMAScript modules in browsers [24]».
Язык EcmaScript прошел большую историю и продолжает развиваться по сей день. Многие решения были актуальны для своего времени и позволяли решать задачи, в том числе упреждающую поддержку функциональности, еще не встроенной в клиентских агентах. Сейчас браузеры и Node.js-сервер выпускает обновление версий достаточно часто, добавляя в них современную функциональность EcmaScript.
В итоге решения, позволяющие в прошлом обеспечивать эмуляцию поддержки новых возможностей в популярных версиях браузеров на сегодня применимы к устаревшим агентам, которые, в зависимости от задачи, имеет смысл поддерживать отдельно или вообще исключить.
Предварительное разрешение и связывание модулей и их последующее bundle-ирование, еще недавно бывшее основным способом поддержки ES6 modules в большинстве браузеров, сейчас оказывает на них негативное влияние и мешает оптимизациям и средствам кэширования.
Таким образом, настраивая сборку веб-приложения, целесообразно предоставлять современным агентам код на современном EcmaScript, включая синтаксические элементы и импорты/экспорты модулей.
Курсы «Нетологии» по теме:
Автор: blognetology
Источник [30]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/275857
Ссылки в тексте:
[1] Developer Soft: http://www.developersoft.ru/
[2] Нетологии: https://netology.ru/?utm_source=blog&utm_medium=747&utm_campaign=habr
[3] первой части: https://habrahabr.ru/company/netologyru/blog/347516/
[4] второй: https://habrahabr.ru/company/netologyru/blog/348632/
[5] Image: https://habrahabr.ru/company/netologyru/blog/351704/
[6] описанное в статье: https://www.html5rocks.com/en/tutorials/speed/script-loading/#toc-aggressive-optimisation
[7] AMD: https://github.com/amdjs/amdjs-api/wiki/AMD
[8] UMD: https://github.com/umdjs/umd
[9] CommonJS: http://www.commonjs.org/
[10] ES6-модули: http://www.ecma-international.org/ecma-262/6.0/#sec-modules
[11] поздних версиях: https://tc39.github.io/ecma262/
[12] Browserify: http://browserify.org/
[13] Webpack: https://webpack.js.org/
[14] Babel: https://babeljs.io/
[15] JSX: https://babeljs.io/docs/plugins/preset-react/
[16] ES6 proxy: https://www.npmjs.com/package/babel-plugin-proxy
[17] рефлексивной информации в коде: https://www.npmjs.com/package/babel-plugin-object-source
[18] F#: http://fable.io/
[19] Ocaml: https://github.com/ocsigen/js_of_ocamlv
[20] code chunk splitting: https://webpack.js.org/guides/code-splitting/
[21] regenerator runtime: https://www.npmjs.com/package/regenerator-runtime
[22] SPOF: https://www.stevesouders.com/blog/2010/06/01/frontend-spof/
[23] ES6 modules support lands in browsers: is it time to rethink bundling?: https://www.contentful.com/blog/2017/04/04/es6-modules-support-lands-in-browsers-is-it-time-to-rethink-bundling/
[24] ECMAScript modules in browsers: https://jakearchibald.com/2017/es-modules-in-browsers/
[25] Frontend-разработчик: https://netology.ru/programs/front-end?utm_source=blog&utm_medium=747&utm_campaign=habr
[26] Веб-разработчик: https://netology.ru/programs/web-developer?utm_source=blog&utm_medium=747&utm_campaign=habr
[27] Основной курс по JavaScript: https://netology.ru/programs/javascript?utm_source=blog&utm_medium=747&utm_campaign=habr
[28] Node, AngularJS и MongoDB: разработка полноценных веб-приложений: https://netology.ru/programs/node?utm_source=blog&utm_medium=747&utm_campaign=habr
[29] JavaScript в браузере: создаем интерактивные веб-страницы: https://netology.ru/programs/html-javascript?utm_source=blog&utm_medium=747&utm_campaign=habr
[30] Источник: https://habrahabr.ru/post/351704/?utm_source=habrahabr&utm_medium=rss&utm_campaign=351704
Нажмите здесь для печати.