Владислав Власов, инженер-программист в 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, включая синтаксические элементы и импорты/экспорты модулей.

От редакции


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

Комментарии (18)


  1. Drag13
    21.03.2018 17:54

    Попридираюсь

    ES6 модули загружаются и исполняются отложенным образом по умолчанию

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

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

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

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


    1. Synoptic
      21.03.2018 18:16

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

      Как я понимаю ограничение на параллельно исполняющиеся запросы было вызвано ограничением на количество TCP-соединений на одного юзера на сервере. С мультиплексированием через HTTP/2 у нас один TCP-коннект для всего, через который гоняется неограниченное число запросов в параллель(кто знает — действительно неограниченное?) и такой проблемы не возникает.


      1. Drag13
        21.03.2018 21:23

        И это все и полностью правда. Но пока у нас нет HTTP/2 будем сидеть на бандлах.


        1. barkalov
          21.03.2018 22:26

          1. Drag13
            22.03.2018 09:11
            -1

            Да я собтвенно и не против, но где этот дивный новый мир вокруг?


            1. barkalov
              22.03.2018 09:19

              1. Drag13
                22.03.2018 10:14
                -1

                Простите, но

                HTTP/2 is used by 24.4% of all the websites.

                Это не снаружи. Это пока где-то сбоку.


                1. barkalov
                  22.03.2018 10:35
                  +2

                  Ну не знаю, как по мне это дофига. Это — мейнстрим.

                  Google.com
                  Youtube.com
                  Facebook.com
                  Wikipedia.org
                  Yahoo.com
                  Twitter.com
                  Vk.com
                  Instagram.com

                  Или вы будете ждать, пока последний цветочный магазин на Битриксе переедет на HTTP/2?


  1. webschik
    21.03.2018 18:36

    К сожалению не все так гладко с загрузкой bundle для старых браузеров и только модулей для новых. Некоторые из браузеров начинают загружать все подряд — github.com/philipwalton/webpack-esnext-boilerplate/issues/1


  1. ohm
    21.03.2018 20:39

    transpile-ингом тебе по голове


  1. ohm
    21.03.2018 20:41
    +1

    есть всем понятные слова: транспилер, полифил, байндинг, бандл, билд


  1. kashey
    22.03.2018 02:31

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

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

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

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

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

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


    1. Synoptic
      22.03.2018 10:25
      +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. kashey
        22.03.2018 10:31
        +1

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


        1. Synoptic
          22.03.2018 10:37
          +2

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

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


          1. kashey
            22.03.2018 10:45

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

            Все понимается в сравнении, у меня есть одно старое приложение которым все еще пользуются миллионы, с оно занимает до gzip меньше меньше чем наше новое приложение после. Раза так в 2. И логики там раз в 10 больше. А кода меньше.
            Мистика последние годы с фронтендом творятся. Просто мистика.


            1. Synoptic
              22.03.2018 10:54

              Ну так разговор не про тех кому все пофиг:) И не всегда CDN имеет смысл, бывают секьюрные политики, не разрешающие внешние зависимости, плюс CDN при всех его плюсах — это дополнительный DNS fetch, дополнительный TCP-connect. Надо смотреть по ситуации в общем.

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

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

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

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


              1. kashey
                22.03.2018 10:59
                +1

                Я только вчера смотрел в терминал, где крутился старина gulp. 500 ns на ребилд (очень) простого приложения.