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

    Владислав Власов, инженер-программист в Developer Soft и преподаватель курса Нетологии, специально для блога написал цикл статей о EcmaScript6. В первой части на примерах рассмотрели динамический анализ кода в EcmaScript с помощью Iroh.js, во второй сосредоточились на реализации отменяемых Promises. В этой статье поговорим об истории ES6-модулей.



    История языка EcmaScript простирается от простого языка сценариев в браузере вплоть до современного языка общего назначения, работающего в различных хост-окружениях. Вместе с усложнением языка появилась и необходимость организации модульной структуры и переиспользования кода с помещением его в библиотеки. Первые библиотеки импортировались за счет загрузки соответствующего JS-файла с хоста поставщика или CDN, а взаимодействие производилось, как правило, посредством экспорта функций и классов с заранее известными именами в глобальное пространство — объект window.

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

    Вопрос вложенных зависимостей может решаться с помощью dynamic <script> injection в DOM-модели, а переиспользование может достигаться за счет экспорта с известным именем в глобальном хост-объекте, однако это не универсальное решение и строится оно исключительно на неявном соглашении между авторами библиотек и использующим их клиентском сценарии. Частично согласование имен решается посредством передачи CDN-серверу в query string параметров, специфицирующим пространства имен для загружаемой JS-библиотеки, но это также не универсально.

    Остаются некоторое фундаментальные проблемы, связанные с асинхронной загрузкой и взаимодействием с DOM-моделью. Некоторые библиотеки должны быть загружены раньше других, если вторые имеют зависимость от первых. В случае с динамическим импортом это требует правильной установки async-флага или манипуляции с событием readystatechanged, в зависимости от вендора и версии браузера.

    Конечно, и для этого общего случая есть решение, описанное в статье. Однако, во-первых, оно требует тщательного слежения за зависимостями во всех загружаемых библиотеках, и во-вторых, если некоторые библиотеки представляют собой polyfills, которым требуется отслеживание DOM-состояния и событий. В случае defer fallfack это не заработает.

    Для универсального решения вышеописанных задач было разработано несколько стандартов организации библиотечных модулей для JS, самые известные из них — AMD (Asyncronous module definition), UMD (Universal module definition) и CommonJS. За счет следования авторами модулей общего формата декларации и наличия общего загрузчика файлов, большинство проблем было решено.

    Тем временем активно развивалась платформа Node.JS, где зависимости модулей были решены совершенно другим способом — посредством синхронного require-вызова, а модули имели соответствующий специфичный формат. Тогда технический комитет TC-39 начал разработку универсального средства импорта модулей, которое должно было решать все вышеобозначенные задачи и при этом работать одинаково на сервере и клиенте и обеспечивать синхронную и асинхронную семантику загрузки модуля. Таким средством стали ES6-модули.

    Поддержка ES6-модулей посредством transpile и bundle builder


    С появлением спецификации Ecmascript 262 version 6 и последующих редакций, в язык добавлялось множество новых синтаксических конструкций и native-функций. Как правило, большинство из них могло легко запускаться и на старых версиях JS-движков за счет предварительного transpile-инга — для синтаксических конструкций, и добавления polyfills — для недостающих функций.

    ES6-модули же обеспечивали синхронную не блокирующую семантику загрузки, binding-привязки для экспортируемых/импортируемых сущностей, модульная область видимости и другие аспекты, которые не просто обеспечить обычным transpile-ингом.

    Разработчики хотели создавать веб-приложения на актуальном диалекте Ecmascript 6, 7, 8-й и поздних версиях, а для этого требовалось удобство по выполнению transpile-инга и добавлению соответствующих polyfills для приложений автоматическим образом, чтобы разработанное приложение могло работать и в относительно старых браузеров без проблем.

    Совокупным решением этих задач стали bundle builder, настраиваемые вместе с подключаемыми transpilers и polyfills. Идея состоит в том, что код приложения преобразуется в эталонный диалект, который считается поддерживаемыми всеми актуальными браузерами, например, ES3 или ES5 — в зависимости от задачи. После этого все файлы библиотечных модулей и кода приложения соединяются в один большой файл — так называемый bundle. Этот файл отправляется на клиент и уже не требует никаких синхронных или асинхронных импортов, поскольку весь необходимый код уже находится в bundle и доступен по кодовым номерам.

    Известные решения, имплементирующие соответствующий подход: Browserify и Webpack, причем последний в настоящее время является фактически стандартом де-факто. Транспайлером де-факто является Babel. Предложенная схема имеет большое количество преимуществ.

    Во-первых, благодаря наличию в схеме transpiler-а, исходный проект может быть фактически написан на любом языке. Как правило, это EcmaScript или TypeScript последней версии, но возможности по расширению синтаксиса практически безграничны. Одно из известных расширений для ES — JSX, используемый в библиотеке React и ее производных.

    Во-вторых, за счет контроля преобразования кода в фазе transpile-инга, имеется возможность внедрения поддержки даже такой функциональности, как ES6 proxy или рефлексивной информации в коде.

    Среди интересных следствий применения bundle-ирования кода — возможность написание клиентского кода на языке F# или Ocaml и многое другое.

    Помимо очевидных преимуществ, решение с bundle-ированием имеет и ряд очевидных недостатков.

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

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

    Конечно, большая часть обозначенных выше проблем имеет свои решения. Для того чтобы в production-режиме не загружать в браузер весь код приложения целиком, в webpack используется технология code chunk splitting. Можно использовать и динамическую версию импорта, возвращающую Promise и обеспечивающую асинхронную загрузку целевого модуля.

    Для отладочных целей тоже имеются решения. Просмотр оригинального исходного кода, и даже навигация по нему в отладчике браузера достигается посредством спецификации source maps, внедряемых в целевой bundle в режиме разработки. Частичное обновление без полной перезагрузки bundle решается при помощи Hot Module Reload, хотя действительно инкрементальное обновление корректно работает только в простых случаях.

    Нативная поддержка ES6-модулей


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


    Это требует пересмотра взгляда на сборку современных web-приложений, поскольку bundle были необходимостью из-за несовершенства и отсутствия требуемой функциональности в браузерах. После её появления, использование нативных конструкций обеспечивает гораздо лучший результат.

    Во-первых, излишний transpile синтаксических конструкций и замены его на эмулирующий код приводит к замедлению и затруднению оптимизаций. Это касается async и generator-функций, заменяемых на regenerator runtime, и лексических переменных let/const, преобразуемых в неоптимальные var-декларации.

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

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

    Для сохранения работоспособности в старых браузерах, не имеющих поддержку 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?» и «ECMAScript modules in browsers».

    Итоги


    Язык EcmaScript прошел большую историю и продолжает развиваться по сей день. Многие решения были актуальны для своего времени и позволяли решать задачи, в том числе упреждающую поддержку функциональности, еще не встроенной в клиентских агентах. Сейчас браузеры и Node.js-сервер выпускает обновление версий достаточно часто, добавляя в них современную функциональность EcmaScript.

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

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

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

    От редакции


    Курсы «Нетологии» по теме:

    Нетология 41,96
    Университет интернет-профессий
    Поделиться публикацией
    Комментарии 18
    • 0
      Попридираюсь
      ES6 модули загружаются и исполняются отложенным образом по умолчанию

      Если вы блокируете основной поток загрузкой скриптов (когда это не нужно, а иногда даже нужно, потому что без них у вас не приложение а белый лист) то кто-то сам себе злобный буратино.

      А если серьезно (т.е. почему сейчас это все еще не актуально)

      … поскольку bundle были необходимостью из-за несовершенства и отсутствия требуемой функциональности в браузерах...

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

      • +1
        Кроме вышеперечисленного, бандлинг решал проблему ограниченого кол-ва параллельных запросов в браузере.

        Как я понимаю ограничение на параллельно исполняющиеся запросы было вызвано ограничением на количество TCP-соединений на одного юзера на сервере. С мультиплексированием через HTTP/2 у нас один TCP-коннект для всего, через который гоняется неограниченное число запросов в параллель(кто знает — действительно неограниченное?) и такой проблемы не возникает.
        • –2
          И это все и полностью правда. Но пока у нас нет HTTP/2 будем сидеть на бандлах.
            • –2
              Да я собтвенно и не против, но где этот дивный новый мир вокруг?
                • –1
                  Простите, но
                  HTTP/2 is used by 24.4% of all the websites.

                  Это не снаружи. Это пока где-то сбоку.
                  • +2
                    Ну не знаю, как по мне это дофига. Это — мейнстрим.
                    Google.com
                    Youtube.com
                    Facebook.com
                    Wikipedia.org
                    Yahoo.com
                    Twitter.com
                    Vk.com
                    Instagram.com

                    Или вы будете ждать, пока последний цветочный магазин на Битриксе переедет на HTTP/2?
      • +1
        К сожалению не все так гладко с загрузкой bundle для старых браузеров и только модулей для новых. Некоторые из браузеров начинают загружать все подряд — github.com/philipwalton/webpack-esnext-boilerplate/issues/1
        • –3
          transpile-ингом тебе по голове
          • +5
            есть всем понятные слова: транспилер, полифил, байндинг, бандл, билд
            • +1
              >Во-первых, излишний transpile синтаксических конструкций и замены его на эмулирующий код приводит к замедлению и затруднению оптимизаций.

              Смешались вместе, бандлеры и бабел…

              >Предварительное разрешение и связывание модулей и их последующее bundle-ирование, еще недавно бывшее основным способом поддержки ES6 modules в большинстве браузеров, сейчас оказывает на них негативное влияние и мешает оптимизациям и средствам кэширования

              Двойку за подготовку. Команда webpack производила исследования после появления HTTP/2, которое показало что HTTP/2 не решает ничего. Пару месяцев назад я перепроверил их результаты — конечно же ничего не изменилось.

              Так как узнать зависимости загружаемого файла можно только после его загрузки — загрузка реального приложения разбивается на «волны» дозагрузок, которых можно быть под два десятка без проблем. Берем «обычный» сервер в Германии с пингом 60мс — получили 1с потраченого времени.

              В настоящий момент бандер жизненно необходим для практически любого приложения. Это факт с которым поспорить сложно — слишком легко проверяем. Слишком логичен.
              • +1
                With HTTP/2 you don’t need to bundle your modules anymore.

                Я бы сказал, с HTTP/2 вы больше не захотите бандлить ваши модули. Возможность выкинуть из процесса громоздкий тул — чрезвычайно соблазнительное предложение.

                В статье как-то очень скромно про такую важную тему, как кэширование с бандлером и без него:
                So we need to find the middle ground to get the best for both worlds. We put the modules into n bundles where n is greater than 1 and smaller than the number of modules. Changing one module invalidates the cache for one bundle which is only a part of the complete application. The remaining application is still cached.

                И сколько в итоге делать бандлов? 2, 3, 10? А без бандлера не кусок приложения инвалидируется, а только поменявшийся файл.
                • +1
                  Будем честны — с какой вероятностью пользователь зайдет на конкретно ваш сайт через неделю, месяц, год?
                  Для 99% приложений один бандл — оптимален. Для 1% выделение vendor chunk имеет смысл.
                  Code splitting? Отдельная песня, и webpack 4 пытается ее решить через авторазбиение, и достаточно успешно.
                  Но code splitting не зависит от бандлера или «не-бандлера» — он либо есть, либо его нет. Текущие ES6 модули это всегда статическая линковка.
                  • +2
                    Буду честен, я и еще куча народу в других компаниях разрабатывает веб-приложения, которыми пользуются тысячи, и даже миллионы людей. Пользователи продуктов пользуются им регулярно, многие ежедневно. Не все делают сайты, и эта ситуация, скажем так, не редка.

                    И при частых релизах, а у нормальных продуктов они частые, на регулярной инвалидации кешей ваших мегабандлов вы потеряете намного больше сэкономленного на значительно более редких раундтрипов до сервера(если руками не сбрасывать, то они нужны в начале и при каждой инвадидации кэша, а она тут для файлов, а не для бандлов).
                    • 0
                      Если бы люди на самом деле заботились бы о «кеше» — они бы загружали React или, прости господи, moment.js, с какого либо CDN, того же unpkg.
                      Опять же — большая часть «приложения» сидит в node_modules и при ребилде не меняется.
                      Vendor chunk чтука хорошая, но как зашарить общие для всех продуктов компании зависимости?

                      Все понимается в сравнении, у меня есть одно старое приложение которым все еще пользуются миллионы, с оно занимает до gzip меньше меньше чем наше новое приложение после. Раза так в 2. И логики там раз в 10 больше. А кода меньше.
                      Мистика последние годы с фронтендом творятся. Просто мистика.
                      • 0
                        Ну так разговор не про тех кому все пофиг:) И не всегда CDN имеет смысл, бывают секьюрные политики, не разрешающие внешние зависимости, плюс CDN при всех его плюсах — это дополнительный DNS fetch, дополнительный TCP-connect. Надо смотреть по ситуации в общем.

                        Опять же — большая часть «приложения» сидит в node_modules и при ребилде не меняется.

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

                        Мистика последние годы с фронтендом творятся. Просто мистика.

                        Попробуйте Polymer без бандлера(по слухам, Vue тоже ничего) просто в качестве побаловаться. Мне при всей нестабильности так понравилась эта идея отсутствия сборки, что смотреть на бандлеры теперь тошно.
                        • +1
                          Я только вчера смотрел в терминал, где крутился старина gulp. 500 ns на ребилд (очень) простого приложения.

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

              Самое читаемое