company_banner

ES6-модули в браузере: готовы они уже или нет?

Автор оригинала: David Gilbertson
  • Перевод
Слышали об использовании ES6-модулей в браузере? Собственно — это обычные ES6-модули. Только применяются они в коде, предназначенном для браузеров.



Если кто не знает — вот как это выглядит.

Имеется страница index.html:

<!DOCTYPE html>
<html>
  <body>
    <div id="app"></div>
  
    <script type="module" src="index.mjs"></script>
    <!--обратите внимание на тип скрипта "module" -->
  </body>
</html>

Есть файл index.mjs, представляющий собой модуль, подключённый к странице:

import { doSomething } from './utils.mjs';

doSomething('Make me a sandwich');

Этот модуль, в свою очередь, импортирует функцию doSomething из модуля utils.mjs:

export const doSomething = message => {
  console.log('No.');
};

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

Саспенс!

О поддержке модулей браузерами


Прежде чем я расскажу о том, что я понял, предлагаю взглянуть на поддержку модулей браузерами. Этот материал я пишу в начале 2020 года, поэтому уровень поддержки модулей составляет довольно внушительные 90%.


Поддержка модулей браузерами по сведениям caniuse.com

Меня вполне устраивают эти 90% (я не принимаю во внимание нужды 10% пользователей), хотя вы, возможно, хотите быть более ответственным. Но, даже учитывая это, если ваш проект не рассчитан на IE, или UC, или Opera Mini, такой уровень поддержки означает, что практически 100% вашей целевой аудитории сможет без проблем работать с вашим проектом.

Правда, поддержка модулей браузерами — это только начало. Здесь я хочу найти ответ на три вопроса, которые возникли у меня в самом начале пути в сторону модулей:

  • Есть ли у использования модулей в браузерах какие-нибудь плюсы?
  • А минусы?
  • Уверен, у меня был и третий вопрос, но сейчас я его вспомнить не могу.

Давайте с этим разберёмся…

Каковы плюсы использования модулей в браузерах?


Это — чистый JavaScript! Никакого конвейера сборки проекта, никакого 400-строчного конфигурационного файла Webpack, никакого Babel, никаких плагинов и пресетов, и никаких дополнительных 45 npm-модулей. Только вы и ваш код.

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

Итак. Плюсы ES6-модулей — это чистый JavaScript, это отсутствие сборки, конфигурирования проекта, это отказ от ставших ненужными зависимостей. А что ещё? Есть же что-то ещё?

Нет, больше ничего.

Каковы минусы использования модулей в браузерах?


▍Сравнение модулей и бандлеров


Размышляя о том, стоит ли использовать ES6-модули в браузере, на самом деле, приходится выбирать между использованием модулей и бандлеров наподобие Webpack, Parcel или Rollup.

Можно (вероятно) использовать и то и другое, но в реальности, если планируется пропускать код через бандлер, нет причины применять конструкцию <script type="module"> для загрузки файлов бандлов.

Итак, исходя из предположения о том, что некто рассматривает возможность использования ES6-модулей вместо бандлера, вот то, от чего ему придётся отказаться:

  • Минификация готового кода.
  • Сверхсовременные возможности JavaScript.
  • Синтаксис, отличающийся от синтаксиса JavaScript (React, TypeScript и прочее подобное).
  • Npm-модули.
  • Кэширование.

Рассмотрим первые три пункта этого списка подробнее.

▍Размер файлов


Размер моего проекта, в котором используются модули, составляет 34 Кб. Так как я не применяю шаг сборки проекта, код, передаваемый по сети, содержит весьма длинные имена переменных, в нём присутствует великое множество комментариев. Он, к тому же, представляет собой целую кучу маленьких файлов, что не очень хорошо в плане сжатия данных.

Если бы я собрал всё это в бандл с использованием Parcel, то размер того, что получилось бы, составил бы 18 Кб. Мой калькулятор Casual Casio сообщает, что это «примерно половина» кода проекта, в котором бандлер не используется. Это отчасти от того, что Parcel минифицирует файлы, но ещё и от того, что материалы при таком подходе лучше сжимаются с помощью gzip.

Сжатие данных обращает наше внимание на ещё одну проблему: то, как организованы файлы (в смысле удобства разработки) напрямую переносится на то, как файлы передаются по сети. А то, как организованы файлы при разработке, совсем необязательно соответствует тому, как их хочется видеть в работе сайта. Например, 150 модулей (файлов) могут иметь смысл в ходе работы над проектом. А те же материалы, передаваемые в браузер, может быть оправдано организовать в 12 бандлов.

Поясню эту мысль. Я не говорю о том, что использование модулей означает, что файлы нельзя собирать в бандлы и минифицировать (то, что делать этого нельзя — иллюзия). Я просто имею в виду то, что нет смысла делать и то и другое.

Да, вот забавное наблюдение. В моём приложении, до того момента, пока я написал этот раздел, использовались (подчёркиваю!) модули. Я установил Parcel для вычисления правильного размера сборки. А теперь я не вижу причин возвращаться к обычным модулям. Ну не интересно ли!

▍Жизнь без транспиляции


За годы работы я сильно привык к использованию самых свежих синтаксических конструкций JavaScript, и к замечательному инструменту Babel, который превращает их в код, который понимают все браузеры. Я так к этому привык, что редко хотя бы задумывался о браузерной поддержке (естественно, за исключением DOM и CSS).

Когда я впервые попробовал type=«module», я собирался делать всё сам. Моей целью были свежие браузеры, поэтому я думал, что смогу использовать современный JavaScript, к которому я привык.

Но реальность оказалась не столь радужной. Попробуйте ответить на следующие вопросы быстро и никуда не заглядывая. Поддерживает ли Edge flatMap()? Поддерживает ли Safari деструктурирующее присваивание (объекты и массивы)? Поддерживает ли Chrome запятые после последних аргументов функций? Поддерживает ли Firefox оператор возведения в степень?

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

Это означает ещё и то, что я не смогу использовать самое замечательное новшество JavaScript со времён появления метода массивов slice() — так называемый оператор опциональной последовательности. Я пользовался волшебными конструкциями наподобие prop?.value всего что-то около месяца (с того момента, когда инструмент Create React App начал их поддерживать без дополнительных настроек). Но мне уже неудобно работать без них.

▍Тяготы кэширования


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

Как вы, уверен, знаете, когда материалы обрабатывают с помощью бандлера, каждый из получившихся на выходе файлов получает уникальное имя — вроде index.38fd9e.js. Содержимое файла с таким именем никогда (вообще никогда) не меняется. Поэтому он может быть кэширован браузером на неограниченный срок. Если такой файл однажды загружен, загружать его снова не придётся.

Это — восхитительная система — если только не пытаться найти ответ на вопрос о том, как очистить кэш.

Модули загружают с помощью конструкции наподобие <script type="module" src="index.mjs"></script>. Хэш в имени файла при этом не используется. Как в такой ситуации предполагается сообщать браузеру о том, откуда ему загружать index.mjs — из кэша или из сети?

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

  • Надо установить заголовок всех ответов cache-control в значение no-cache. Невероятно, но no-cache — это не значит «не кэшировать». Это означает, что файл кэшировать надо, но файл не должен «использоваться для удовлетворения последующего запроса без успешной проверки на исходном сервере».
  • Надо использовать служебный заголовок ETag. Если не знаете — это (обычно) хэш содержимого файла, который, в виде заголовка, отправляют вместе с файлом. Как правило, 38fd9e перемещают из имени файла в заголовок.
  • Нужно пользоваться хорошим CDN-сервисом с кэшем, которым удобно управлять. Браузер будет проверять заголовки ETag для каждого файла при каждой загрузке сайта. Таких проверок будет очень много. Поэтому используемый CDN-сервис должен быть быстрым. И кэш его нужно будет обновлять (записывая в него новые заголовки ETag) при каждом выпуске новой версии сайта. (Это, например, делается автоматически на хостинге Firebase).
  • Нужно настроить сервис-воркер, действующий в роли кэша, находящегося «на шаг позади» реальной ситуации. Он будет перехватывать все запросы и отдавать то, что уже есть в кэше, а потом, в фоне, обновлять кэш из сети.

В результате, когда посетитель повторно зайдёт на сайт, браузер заявит: «Мне нужно загрузить файл index.mjs. Вижу, в моём кэше этот файл уже есть, его ETag — 38fd9e. Запрошу этот файл у сервера, но скажу ему, чтобы он прислал мне его только в том случае, если его ETag — не 38fd9e». Сервис-воркер этот запрос перехватит, проигнорирует ETag и вернёт index.mjs из своего кэша (этот файл попал в кэш тогда, когда страница загружалась в прошлый раз). Затем сервис-воркер перенаправит запрос к серверу. Сервер вернёт либо сообщение о том, что файл не изменился, либо файл, который будет сохранён в кэше.

На мой взгляд — всё это очень уж хлопотно.

Вот, если кому интересно, код сервис-воркера:

self.addEventListener('fetch', e => {
  e.respondWith(
    (async () => {
      // Загрузить ресурс из кэша и обновить кэш
      const cacheResponse = await caches.match(e.request);
      
      // Обновить кэш (асинхронно), возвращая кэшированный ответ
      // (ETag-заголовки предотвратят ненужные загрузки файлов)
      fetch(e.request).then(fetchResponse => {
        caches
          .open(CACHE_NAME)
          .then(cache => cache.put(e.request, fetchResponse));
      });
      
      if (cacheResponse) return cacheResponse;
      
      return fetch(e.request);
    })()
  );
});

Я ленился, поэтому не изучил и не использовал новейшее свойство FetchEvent.navigationPreload. Дело в том, что к тому моменту я потратил больше времени на кэширование, чем на написание приложения (к вашему сведению — я потратил на эти дела, соответственно, 10 и 11 часов).

Да, хочу отметить, что предложение «карты импорта» направлено на решение некоторых из вышеописанных проблем. Оно позволяет организовать нечто вроде мэппинга index.js на index.38fd9e.mjs. Но для генерирования хэша, всё равно, понадобится некий сборочный конвейер, карты импорта придётся внедрять в HTML-файл. Это означает, что тут понадобится бандлер… Собственно говоря — при таком раскладе модули в браузере уже не нужны.

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

Но ведь, вероятно, не все используют бандлеры?


Я писал этот материал, исходя из предположения о том, что все пишут код в модулях, применяя конструкции import/export или require, а затем собирают код в продакшн-бандлы с помощью Webpack, Grunt, Gulp или чего-то такого.

Существуют ли разработчики, которые не пользуются бандлерами? Есть ли кто-то, кто размещает свой JavaScript-код во множестве файлов и отправляет их в продкшн без бандлинга? Может быть один из таких людей — вы? Если так — мне хотелось бы узнать всё о вашей жизни.

Итоги


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

К сожалению, использование модулей в браузерах не способствуют ни тому, ни другому.

Если завтра я начну работу над новым проектом, то в голове у меня не возникнет вопроса о том, надо ли запускать старый добрый create-react-app. Все первоначальные настройки займут секунд тридцать, и хотя начальный размер проекта в 40 Кб чуть великоват, для большинства сайтов это не сыграет никакой роли.

А вот — другая ситуация. Предположим, мне нужно было бы собрать воедино немного HTML/CSS и JavaScript для некоего эксперимента, и при этом такой эксперимент представлял бы собой проект, включающий в себя чуть больше файлов, чем «несколько». Если, работая над этим проектом, я не планировал бы тратить время на настройку системы для его сборки, тогда я, возможно, воспользовался бы модулями в браузере. И то — если бы меня не волновала производительность этого проекта.

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

Уважаемые читатели! Пользовались ли вы ES6-модулями в браузере?

RUVDS.com
RUVDS – хостинг VDS/VPS серверов

Похожие публикации

Комментарии 17

    +3
    Я конечно всё понимаю, но может быть не стоит перепечатывать какой-то технически откровенно провальный материал?
    Итак, исходя из предположения о том, что некто рассматривает возможность использования ES6-модулей вместо бандлера, вот то, от чего ему придётся отказаться

    Все перечисленные автором пункты не имеют никакого отношения к реальности. Использование модулей нативно абсолютно никак не мешает «билдить» код, пропуская его через транспиляторы, минификаторы, и вообще всё что угодно. Нативные модули никак не мешают использовать NPM.
    Как максимум нужно будет настроить инструменты так, чтоб они не трогали ваши импорты.

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

    Мне так видится, что у автора вообще в принципе очень расплывчатое понимание того, как работают бандлеры и нативные модули. Или же очередное «я сделал вот так-то и так-то» автор пытается выдать за «у всех будет вот так-то и так-то».
      0
      В смысле привязка к вебсерверу? Относительный url (работает) почему не считается?
        +1
        А это не привязка? Это же явное допущение, что сервер должен отдавать файлы с того же корня, где и html лежит, и их пути должны совпадать с тем, как импорты прописаны. Относительный url будет относителен странице (html), а не некоторому js-модулю, в котором относительный импорт на другой js-модуль.

        Это конечно не то, чтоб прямо огромное допущение, но оно присутствует. Оно, например, очень сильно помешает, если делать не сайт целиком, а библиотеку (компоненты или чего-нибудь еще).
          0
          Позицию понял. Не понял как помешает делать компонет? пока делаешь компонент вообще никак не думаешь о том как он там грузится будет.
            0
            Компонент не обязательно состоит из одного js-модуля. А точнее, если это что-то интереснее кнопки — то скорее всего. И как «не думать» о том, как импортировать из компонента другие компоненты и модули?
              0
              Ну просто передаешь через функцию «компонента А» в свою компоненту «B». А как-там «компонента А» создалась не наше дело. dashboardcode.github.io/BsMultiSelect/snippetEsm.html вот так мой компонент мультиселект получает компоненту popper.js Я это не о том что самый умный, я о том что не знаю что в «большом программировании» используется, так по наитию пишу.
            +1
            Относительный url будет относителен странице (html), а не некоторому js-модулю, в котором относительный импорт на другой js-модуль.

            es6 модули разрешают пути относительно скрипта, из которого был сделан импорт


            // index.html
            
            <script type="module" src="./src/main.js"></script>
            
            // src/main.js
            import "./some/module.js"
            

            Если сервер на http://example.com, то запросы будут:
            http://example.com/src/main.js
            http://example.com/src/some/module.js

              0
              Да, прошу прощения, наврал.
        0

        Идея использовать модули нативно, конечно хороша. Но слишком далека от продакшен-варианта, даже, если отбросить 10% браузеров и (1-5% аудитории в зависимости от).


        Минификация и обфускация кода нужна, как ни крути. Настроенный и хорошо работающий кеш, а также режим для разработки вместе с CI/CD требуют все равно какого-то вебпака, или инструмента сборки, которого, к сожалению, пока нет.

          +2
          требуют все равно какого-то вебпака, или инструмента сборки, которого, к сожалению, пока нет

          Сюрприз, но — сказать «не бандлить» можно даже вебпаку, который полностью заточен под то, чтоб бандлить. Другое дело, что пользоваться вебпаком в этом случае будет мягко говоря неэффективно, он сам почти не будет работать.

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

          Минификатор вообще никто не мешает гонять.
          0
          Запрошу этот файл у сервера, но скажу ему, чтобы он прислал мне его только в том случае, если его ETag — не 38fd9e». Сервис-воркер этот запрос перехватит, проигнорирует ETag и вернёт index.mjs из своего кэша (этот файл попал в кэш тогда, когда страница загружалась в прошлый раз). Затем сервис-воркер перенаправит запрос к серверу. Сервер вернёт либо сообщение о том, что файл не изменился, либо файл, который будет сохранён в кэше.

          я что-то не вкурил, зачем сервис-воркер сразу возвращает потенциально протухшую версию файла, которая, возможно, не будет стыковаться с текущими версиями html и css.
            +1
            А, ну и вдогонку. Пропиарю import maps, для которых есть хорошая shim-реализация. Почему этот нарождающийся стандарт ценен? Потому что позволяет централизованно (т.е. силами одной конфигурации) отвязать исходный код от структуры вебсервера. Ну и хеш-алиасы (вида [module_name].[hash].js) формировать гораздо проще, вообще никак не трогая исходный код и импорты в нём.
              0

              А есть транспиляторы, транспилирующие код в es6 модули?

                0

                Не хватает пункта про serverPush и latency. Суть проста: у вашего древа импортов есть глубина. Положим она равна 7. Вам нужно сделать минимум 7 последовательных запросов. Если у пользователя высокий latency, то это займёт прорву времени, из которой браузер почти всё время будет тупо ждать ответа от сервера. Можно вылечить тем что сервер заранее посредством serverPush протолкнёт всё что надо со своей стороны с первым же запросом, но это ещё пойди настрой, не тривиально. Либо то же самое в html теги preload воткнуть, тоже решение на любителя.


                Ну и ещё про ограничение на количество одновременных запросов. Скажем делать 4 параллельных запроса на 1.5KiB...

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

                  Ну и HTTP/2 надо конечно, иначе лимит на запросы сделает всем больно.
                    0
                    достаточно лишь сформировать (хоть руками, хоть во время сборки) модуль, в котором будут прописаны все статические импорты из проекта

                    Единственная проблема — зачем тогда нужны эти type=module в таком случае? :)

                  0
                  Я когда-то работал на проекте, где использовался angularjs но не использовались бандлеры. Мне очень нравилось.

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