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 из своего кэша (этот файл попал в кэш тогда, когда страница загружалась в прошлый раз). Затем сервис-воркер перенаправит запрос к серверу. Сервер вернёт либо сообщение о том, что файл не изменился, либо файл, который будет сохранён в кэше.
Я ленился, поэтому не изучил и не использовал новейшее свойство 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-модулями в браузере?