Мне тут на днях попеняли, что, мол, я не в курсе, «что из esm до сих пор нельзя собрать бандл без транспиляции«. Ну что я могу сказать… я действительно не в курсе 🙂 На мой взгляд, es-модули придумали как раз для того, чтобы загружать по мере необходимости JS-код непосредственно в браузер, и собирать модули в бандлы — это, ну… как гладить кошку против шерсти.
Я понимаю, что традиции / привычки / требования бизнеса / обратная совместимость / корпоративная этика и т.п. говорят о том, что код для браузерных приложений должен поставляться в бандлах и точка! Тем не менее, в некоторых случаях (малые приложения, быстрое прототипирование, распределённая разработка) сборка бандлов является излишней и код в браузер можно и нужно загружать непосредственно в виде es-модулей.
Статический импорт
В качестве примера я приведу код приложения, состоящего из одного файла, который все необходимые модули загружает через unpkg.com (глобальный CDN для npm-модулей). Можно весь свой код оформить в виде набора npm-пакетов, а у своего хостера держать только один головной файл index.html. Демо-приложение довольно бесполезное с практической точки зрения — я взял два первых попавшихся, несвязанных между собой, npm-пакета, которые написаны в виде es-модулей:
-
store-esm: хранение данных в браузере в виде «ключ/значение» (первые версии оригинального пакета вышли в 2010-м году, сейчас пакет популярностью не пользуется);
-
@cloudfour/twing-browser-esm: использование Twig-шаблонов в браузере.
Главное в них, что это не специально мной созданные для демонстрации пакеты, а действительно первые попавшиеся. Первый пакет (store-esm
) состоит из отдельных es-модулей, второй пакет (@cloudfour/twing-browser-esm
) — самый что ни на есть бандл в виде одного единственного es-модуля «весом» в 1.7Мб, чисто для сравнения поведения модулей в браузере.
Код всего HTML-файла
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>The static import</title> <script type="module"> // IMPORT import engine from 'https://unpkg.com/store-esm@3.0.0/src/store-engine.js'; import storages from 'https://unpkg.com/store-esm@3.0.0/storages/all.js'; import plugins from 'https://unpkg.com/store-esm@3.0.0/plugins/all.js'; import { TwingEnvironment, TwingLoaderArray } from 'https://unpkg.com/@cloudfour/twing-browser-esm@5.1.1/dist/index.mjs'; // FUNCS function useStore(engine, storages, plugins) { const store = engine.createStore(storages, plugins); store.set('user', {name: 'Alex Gusev'}); console.log(store.get('user')); } function useTwing(TwingEnvironment, TwingLoaderArray) { const templates = { 'index.twig': ` <h1>{{ title }}</h1> <p>{{ message }}</p> ` }; const loader = new TwingLoaderArray(templates); const twing = new TwingEnvironment(loader); const context = { title: 'Hello, guys!', message: 'Welcome to using Twing in the browser.' }; twing.render('index.twig', context).then((output) => { document.body.innerHTML = output; }).catch((err) => { console.error('Error rendering template:', err); }); } // MAIN useStore(engine, storages, plugins); useTwing(TwingEnvironment, TwingLoaderArray); </script> </head> <body></body> </html>
Код загрузки es-модулей и их использования:
<script type="module"> // IMPORT import engine from 'https://unpkg.com/store-esm@3.0.0/src/store-engine.js'; import storages from 'https://unpkg.com/store-esm@3.0.0/storages/all.js'; import plugins from 'https://unpkg.com/store-esm@3.0.0/plugins/all.js'; import { TwingEnvironment, TwingLoaderArray } from 'https://unpkg.com/@cloudfour/twing-browser-esm@5.1.1/dist/index.mjs'; // FUNCS function useStore(engine, storages, plugins) {...} function useTwing(TwingEnvironment, TwingLoaderArray) {...} // MAIN useStore(engine, storages, plugins); useTwing(TwingEnvironment, TwingLoaderArray); </script>
Основные преимущества
Использование CDN unpkg.com значительно снижает требования к собственному хостеру, а если хостер «берёт денег» за объём скачиваемой информации, то тут уже видна и явная финансовая выгода.
Оптимизация использования интернета за счёт возможности кэширования на разных уровнях GET-запросов к единому источнику. Это преимущество из области «защита окружающей среды при помощи сортировки мусора на собственной кухне«, и тем не менее, на больших объёмах оно работает.
Возможность использования разных версий одного и того же пакета в разных частях приложения. Когда я интегрировал различные плагины в электронные магазины на платформе «Magento«, количество различных экземпляров библиотеки jQuery, которые плагины тянули с собой, иногда поднималось под десяток. У бекэнд-приложений, собираемых при помощи npm, это вызвало бы конфликт версий, а в браузере — пожалуйста. Я уверен, что и при сборке бандла так же можно, но думаю, что цена решения вопроса будет чуть выше, чем просто указать номер версии в адресе экспорта.
Ну и самое главное лично для меня преимущество — возможность работать в браузерном приложении с тем же кодом, что и в IDE. Да, конечно же «сорсмапы врдое бы уже изобрели«, но на мой взгляд отладка с сорсмапами, это как битва Персея с Медузой Горгоной с её визуализацией через полированный щит. Если бы Персей мог обходиться без щита, оно было бы тупо быстрее.
![Исходный код модулей в браузере Исходный код модулей в браузере](https://habrastorage.org/getpro/habr/upload_files/586/503/b2a/586503b2af29bad1129543394f19211a.png)
Основные недостатки
Основные недостатки проистекают из основных преимуществ. Вместо того, чтобы загрузить в браузер один большой файл, приходится загружать много маленьких. Это хорошо видно на примере twing-библиотеки:
![Размер файла и время загрузки es-бандла Размер файла и время загрузки es-бандла](https://habrastorage.org/getpro/habr/upload_files/d30/0cc/a39/d300cca39c0d5fcd357cd829557c329b.png)
В сжатом состоянии бандл весом в 1.7Мб занимает 487Кб и загружается 124мс из которых 18.36мс занимает ожидание ответа сервера и 104.64мс — загрузка содержимого:
![Тайминг для бандла Тайминг для бандла](https://habrastorage.org/getpro/habr/upload_files/fc9/959/c0a/fc9959c0a371fca174bb26bfc60973dc.png)
Следующий за ним util.js
весит почти в 500 раз меньше, а время загрузки меньше всего в два раза:
![Тайминг для отдельного модуля Тайминг для отдельного модуля](https://habrastorage.org/getpro/habr/upload_files/e43/be1/46b/e43be146b381174b06d85943696bba25.png)
Основные потери времени — ожидание ответа сервера, что в случае большого количества es-модулей (файлов) складывается в приличное суммарное время. Это несколько сглаживается наличием дискового кэша браузера:
![Время загрузки es-модулей из дискового кэша Время загрузки es-модулей из дискового кэша](https://habrastorage.org/getpro/habr/upload_files/382/501/501/382501501f2458689bd03029a492f7fd.png)
и очень сильно — наличием кэша в оперативной памяти:
![Время загрузки es-модулей из кэша оперативной памяти Время загрузки es-модулей из кэша оперативной памяти](https://habrastorage.org/getpro/habr/upload_files/b29/c41/88b/b29c4188bcd60d78d2e0d04332edc2db.png)
Если приложению для первоначального запуска нужны абсолютно все модули, то смысла в загрузке файлов по-одиночке нет, выгоднее бандлом. А вот если приложение допускает подгрузку модулей пакетами по паре десятков штук, то есть смысл загружать и помодульно.
Динамический импорт
Преимущества динамического импорта перед статическим в том, что у нас появляется возможность связывать весь наш код не только на этапе его написания, но и на этапе его выполнения:
const rnd = Math.floor(Math.random() * 2); if (rnd) { const {default: engine} = await import('https://unpkg.com/store-esm@3.0.0/src/store-engine.js'); const {default: storages} = await import('https://unpkg.com/store-esm@3.0.0/storages/all.js'); const {default: plugins} = await import('https://unpkg.com/store-esm@3.0.0/plugins/all.js'); useStore(engine, storages, plugins); } else { const { TwingEnvironment, TwingLoaderArray } = await import('https://unpkg.com/@cloudfour/twing-browser-esm@5.1.1/dist/index.mjs'); useTwing(TwingEnvironment, TwingLoaderArray); }
В приведённом выше примере в случайном порядке загружается либо пакет store-esm
, либо пакет @cloudfour/twing-browser-esm
.
![Загруженные исходники для `rnd = 0` Загруженные исходники для `rnd = 0`](https://habrastorage.org/getpro/habr/upload_files/511/be2/ec9/511be2ec9a1e4613a466988c06d67ee0.png)
Как следствие, при помощи динамического импорта мы можем не только уменьшить количество загружаемого в браузер кода, но, и что гораздо важнее, можем в режиме выполнения решать, какую имплементацию функционала подгружать — например, аутентификацию по паролю или по отпечатку пальца. Или загружать UI-компоненты в зависимости от прав текущего пользователя и установленных в приложении плагинов.
В общем, если статический импорт es-модулей в браузерных приложениях можно было бы сравнить с перемещением по поверхности земли, то динамический импорт добавляет нам третье измерение — даёт возможность летать. В принципе, ничего не мешает при помощи динамического импорта использовать в нашем одностраничном приложении любой пакет из имеющихся на unpkg.com (если он написан в es-модулях и для браузера, разумеется).
«IoC через DI»
Ну, а раз уж мы «легализовали» использование es-модулей в браузере и даже перешли от «статического связывания кода на момент его написания» к «динамическому на момент его выполнения«, то тут уже остаётся всего пару шагов до окончательного грехопадения — до использования в коде инверсии контроля через внедрение зависимостей.
Вот содержимое типового es-модуля из тех, которые я использую в своих приложениях:
export default class Fl32_Auth_Front_Mod_User { /** * @param {Fl32_Auth_Front_Defaults} DEF * @param {TeqFw_Core_Shared_Api_Logger} logger - instance * @param {TeqFw_Web_Api_Front_Web_Connect} api * @param {Fl32_Auth_Shared_Web_Api_User_Create} endUserCreate * @param {Fl32_Auth_Shared_Web_Api_User_ReadKey} endReadKey * @param {Fl32_Auth_Shared_Web_Api_User_Register} endUserReg * @param {Fl32_Auth_Front_Mod_Crypto_Key_Manager} modKeyMgr * @param {Fl32_Auth_Front_Mod_Password} modPassword * @param {Fl32_Auth_Front_Store_Local_User} storeUser */ constructor( { Fl32_Auth_Front_Defaults$: DEF, TeqFw_Core_Shared_Api_Logger$$: logger, TeqFw_Web_Api_Front_Web_Connect$: api, Fl32_Auth_Shared_Web_Api_User_Create$: endUserCreate, Fl32_Auth_Shared_Web_Api_User_ReadKey$: endReadKey, Fl32_Auth_Shared_Web_Api_User_Register$: endUserReg, Fl32_Auth_Front_Mod_Crypto_Key_Manager$: modKeyMgr, Fl32_Auth_Front_Mod_Password$: modPassword, Fl32_Auth_Front_Store_Local_User$: storeUser, } ) {...} }
В нём нет ни статических, ни даже динамических импортов. В конструкторе класса описываются все зависимости, которые нужны объектам этого класса для работы. В таком виде es-модуль может быть использован и в браузере, и в nodejs-приложении — нужно при вызове конструктора создать все требующиеся зависимости и передать их через параметры. Это может сделать либо программист, вручную, либо Контейнер Объектов, автоматически — согласно правилам преобразования идентификаторов зависимостей в пути к исходникам.
«IoC через DI» очень давняя технология, можно сказать почти древняя. Очень широко использовалась в PHP (Magento, Zend, Symfony, …) и до этого в Java (Spring). Это из того, что я лично пользовал. В PHP даже есть стандарт для описания интерфейса контейнера объектов — PSR-11. Разработчики на C# Mark Seemann и Steven van Deursen книгу про DI написали, где очень подробно объяснили плюсы/минусы технологии и сравнили с другими IoC-подходами. В общем, «IoC через DI» давно завоевала и прочно утвердила свою репутацию во многих языках программирования.
С учётом возможности преобразования, в зависимости от контекста, в Контейнере Объектов одних идентификаторов в другие, а также с учётом возможности постобработки готовых объектов перед их внедрением, можно сказать что разработка приложения с использованием DI настолько же сильно отличается от разработки с использованием динамического импорта, насколько разработка с использованием динамического импорта отличается от разработки с использованием только статического.
![Преобразование идентификатора зависимости в объект перед внедрением Преобразование идентификатора зависимости в объект перед внедрением](https://habrastorage.org/getpro/habr/upload_files/a28/1ab/842/a281ab842a5b119fff1558d3c679de54.png)
Заключение
Я действительно не использую транспиляцию для сборки бандла из es-модулей, я загружаю es-модули в браузер напрямую. А для ускорения этого процесса я на live-сервере собираю обычный zip-архив из файлов, которые предполагается использовать на фронте, загружаю его на клиента и распаковываю в cacheStorage браузера. После чего использую этот кэш в Service Worker’е при обращениях к esm.
Ну, как-то так получилось, что у меня и бандл из esm есть, и транспиляции нет.
Disclaimer
Подход к загрузке es-модулей, описанный в данной статье, не является общепринятым в JS-коммьюнити и не может быть рекомендован для повседневного употребления. Используйте его по своему усмотрению. Все примеры надуманные и не несут практической пользы.
ссылка на оригинал статьи https://habr.com/ru/articles/824860/
Добавить комментарий