- шаринг зависимостей через npm пакет
- песочница с HMR для виджета
- настроенный vite
- генерация npm-loader
- preview собранного виджета
- встроенная аналитика бандла
- изоляция бандла через same-origin iframe
Мы стремимся узнать, есть ли в open source потребность в такой технологии. Если вы заинтересовались, дайте нам знать: поставьте звездочку в github, оставьте issue, напишите мне в telegram @vtolstikov.
Выполни из корня:
npm install && npm run build
Затем из папки packages/template
выполни:
npm start
В консоли появится урл, по которому можно открыть страницу с песочницей и виджетом.
Браузер не сможет открыть страницу, т.к. он не доверяет серту, который в проекте. Нужно либо доверится серту, либо сгенерировать свой.
Чтобы сгенерировать свой:
- Сделать как предлагают тут. Вместо example.com нужно поставить
localhost
. - Серт в проекте нужно заменить на сгенерированный. Серт находится в папке
packages/https-test-certificate
Далее в любом случае нужно добавить сертификат в доверенные. Для этого можно использовать эту инструкцию и Google Chrome.
После этого приложение должно открыться. Чтобы загрузить виджет нажмите два тогла "Импортировать виджет" и "Отрисовать виджет".
Описаны в тут.
- Полная поддержка esm (type: module)
- Typescript >= 5
Подключи пакет
npm install @skbkontur/widget-platform
Платформа требует следующие файлы для работы:
📦Project
├ 📂.platform
│ └ 📜config.ts
├ 📂exports
│ ├ 📜index.ts
│ └ 📜platformTypes.ts
├ 📂jsLoader
│ ├ 📜index.ts
│ └ 📜vite.config.ts
├ 📂playground
│ ├ 📜environmentConfigs.ts
│ ├ 📜index.tsx
│ └ 📜vite.config.ts
├ 📂widget
│ ├ 📜index.tsx
│ └ 📜vite.config.ts
└ 📜package.json
Используется для npm-loader и для виджета. Подключай в dependencies только то, что должно быть в npm-loader. Остальное - в devDependencies. Также в dependencies есть несколько служебных зависимостей, которые подключаются во время генерации npm-loader:
- @skbkontur/loader-builder
- @skbkontur/operation-result
Осторожно меняй поля в этом package.json, т.к. они используются не только для виджета, но и попадают в node_modules потребителя. Например, поле sideEffects: false
может быть полезен для npm пакета, но сломать рантайм виджета.
Чтобы подключить npm-loader в песочницу, достаточно импортировать модуль с именем в этом package.json.
Должен содержать файл index.tsx?
. Это входная точка для компонента песочницы. Должен экспортировать функцию, которая принимает в параметры настройки из конфига, HTMLElement
и урл виджета.
Должен содержать файл index.tsx?
. Это входная точка для виджета. Должен экспортировать функцию createWidgetApi
как default
.
js-loader - это промежуточный чанк перед загрузкой основного скрипта виджета. Нужен, чтобы создать same-origin iframe. Также позволяет сделать некоторые дополнительные настройки.
Папка должна содержать файл index.ts
. Это входная точка. Файл должен содержать экспорты getJsLoaderDependencies
и generateCsp
.
getJsLoaderDependencies
- позволяет передать в виджет свои зависимости, чтобы они не попали в iframe.
generateCsp
- позволяет донастроить политики CSP.
sharedModules
– Зависимости, которые нужно переиспользовать между потребителем и виджетом. Как подключаются зависимости, читай в статье. Платформа использует window. Пакет, который переиспользуется, обязательно должен быть в dependencies package.json. Если нужно поднять версию пакета, который переиспользуется, делай это по гайду.playground:
port
- порт для dev-servercheckersConfig
- конфигурация vite-plugin-checker.
jsLoader:
sharedModules
- АналогичноsharedModules
платформы, но поддерживает явное указание, в каком формате импортируется зависимость:namespace
,imports
checkersConfig
- конфигурация vite-plugin-checker.
widget
checkersConfig
- конфигурация vite-plugin-checker.
Не обязателен.
Если вам требуется переопределить некоторые настройки vite, то нужно добавить файл vite.config.ts в соответствующую папку. Этот конфиг перезапишет найстройки платформы. Перезапишутся не все настройки, а только указанные. Вот так может выглядеть файл:
import {defineConfig} from 'vite'
export default defineConfig(_config => ({
server: {
port: 5555
}
}))
Не рекомендуется настраивать outDir, т.к. может некорректно работать команда preview
. Лучше сделать батник в проекте, который скопирует скрипты куда надо.
Папка для npm-loader.
Во время сборки платформы собирается npm-loader. В нем создается публичный метод importWidgetModule
. Чтобы он был типизирован корректно, нужно указать свои типы в файле platformTypes.ts
.
Чтобы экпортировать дополнительные типы в npm-loader, нужно указать их в файле index.ts
Если есть типы, которые нужны для index.ts
, но которые не нужно экспортировать, создай их в другом файле.
Настройки песочницы для разных окружений или локальной разработки. Для каждого окружения — функция, возвращающая его настройки.
Пример:
// playground/environmentConfigs.ts
import type { GetPlaygroundDevConfig, GetPlaygroundEnvironmentConfig } from "@skbkontur/widget-platform/browser";
type MyEnvironmentConfig = { apiUrl: URL };
export const getDevConfig: GetPlaygroundDevConfig<MyEnvironmentConfig> = async () => {
return {
environmentConfig: {
apiUrl: new URL("https://internal.domain.ru/template-widget/api/"), // локально используем облачный
},
};
};
export const getCloudConfig: GetPlaygroundEnvironmentConfig<MyEnvironmentConfig> = async () => {
return {
loaderUrlPrefix: new URL("./widget/", window.location.href),
environmentConfig: {
apiUrl: new URL("./api/", window.location.href), // урл до бэкенда на текущей площадке с виджетом
},
};
};
export const getProdConfig: GetPlaygroundEnvironmentConfig<EnvironmentConfig> = async () => {
return {
loaderUrlPrefix: new URL("./widget/", window.location.href),
environmentConfig: {
apiUrl: new URL("https://domain.ru/template-widget/api/"),
},
};
};
loaderUrlPrefix
— урл до задеплоенной папки с артефактами виджета. Плейграунд будет искать лоудер виджета по урлу${loaderUrlPrefix}/loader.js
.environmentConfig
— дополнительные настройки плейграунда. Их тип ты задаешь сам — смотря что хочешь менять между площадками: урлы, апи-ключи.
environmentConfig
из текущего окружения будет приходить в renderPlayground
в поле environmentConfig
, а урл до лоудера виджета ${loaderUrlPrefix}/loader.js
— в поле widgetUrl
.
Функция getDevConfig
должна быть всегда, настройки из нее применяются, когда запускаешь widget-platform start
, preview
или watch
. В getDevConfig
поле loaderUrlPrefix
определять не нужно: при локальной разработке урлом раздачи виджета управляет сама платформа.
Остальные окружения ты определяешь сам, в .platform/config.ts
. В объекте playground.htmlConfigs
для каждого окружения укажи:
- ключ — имя html-ки, точки входа в плейграунд в этом окружении,
- значение — имя функции из
environmentConfigs.ts
с настройками этого окружения.
// .platform/config.ts
export default (): Config => ({
...
playground: {
htmlConfigs: {
"index.cloud.html": "getCloudConfig",
"index.prod.html": "getProdConfig",
},
...
},
})
Команда widget-platform build
сгенерирует файл index.{env}.html
для каждого окружения из playground.htmlConfigs
и положит его в .artifacts/playground/
. Каждый index.{env}.html
открывает плейграунд с настройками своего окружения.
Пример: На каждую ветку монорепы создается тестовая площадка со своими доменами, отдельно — для апи, отдельно — для статики, отдельно — для плейграунда:
https://my-widget-{branch123}.domain.ru/loader.js
— лоудерhttps://my-playground-{branch123}.domain.ru/
— плейграундhttps://my-api-{branch123}.domain.ru/
— АПИ
Чтобы не перечислять все ветки в конфиге, создай для них одно окружение и вычисляй настройки по window.location.href
:
export const getStagingConfig: GetPlaygroundEnvironmentConfig<MyEnvironmentConfig> = async () => {
const branch = extractBranchName(window.location.href);
return {
loaderUrlPrefix: new URL(`https://my-widget-${branch}.domain.ru/`),
environmentConfig: {
apiUrl: new URL(`https://my-api-${branch}.domain.ru/`),
},
};
};
Если нужен урл на том же домене, что и плейграунд, создавай его относительно window.location.href
:
environmentConfig: {
apiUrl: new URL("./api/", window.location.href),
},
environmentConfigs.ts
— это реализация способа "несколько index.<env>.html
в бандле". Чтобы реализовать другие, оставь в environmentConfigs.ts
пустой getDevConfig
и getIndexHtmlConfig
с loaderUrlPrefix
и вычисляй настройки в renderPlayground
по-другому:
// playground/environmentConfigs.ts
import type { GetPlaygroundDevConfig, GetPlaygroundEnvironmentConfig } from "@skbkontur/widget-platform/browser";
export const getDevConfig: GetPlaygroundDevConfig = async () => {
return { environmentConfig: undefined };
};
export const getIndexHtmlConfig: GetPlaygroundEnvironmentConfig<EnvironmentConfig> = async () => {
return {
loaderUrlPrefix: new URL("./widget/", window.location.href),
environmentConfig: undefined,
};
};
// .platform/config.ts
export default (): Config => ({
...
playground: {
htmlConfigs: {
"index.html": "getIndexHtmlConfig",
},
...
},
})
environmentConfigs.ts
работают только в плейграунде и никак не влияют на работу виджетов внутри реальных потребителей. Чтобы управлять настройками в этих случаях, возвращай их с бэка в АПИ или используй другие способы.
start — создает vite server через js api. Использует конфиги платформы. HMR работает одинаково и для песочницы для и для виджета, т.е. изменение кода виджета моментально его применяет.
Конфиги из getDevConfig
в playground/environmentConfigs.ts попадают в renderPlayground
.
build — собирает виджет и песочницу. Все операции идут параллельно. На выходе в проекте появляется папка dist. К этим файлам добавляется stats.html с информацией о билде.
Типизацию в приложении ни start, ни build, не проверяют. Чтобы проверять, подключи vite-plugin-checker, как в примере.
И start, и build собирают пакет npm-loader, готовый к публикации.
preview — Хостит собранную песочницу и скрипты виджета на одном порту. Дает возможность проверить работу production сборки. Включает в себя команду build.
watch - Запускает preview и watch файлов одновременно.
На диаграмме ниже показано, куда складываются артефакты сборки:
📦Project
┣ 📂.artifacts
┃ ┣ 📂cache
┃ ┣ 📂npm-loader
┃ ┣ 📂playground
┃ ┣ 📂stats
┃ ┗ 📂widget
widget-platform
только собирает все нужные файлы в папку .artifacts
. Чтобы опубликовать их в интернет, тебе нужен сервер статики.
Сервер статики должен раздавать 2 папки: .artifacts/playground
и .artifacts/widget
. Урлы можешь выбирать свои, но часто делают так:
https://some-url.kontur.ru/[maybe-some-path]/
— папка.artifacts/playground
, в корне, чтоб долго не искать.https://some-url.kontur.ru/[maybe-some-path]/widget/
— папка.artifacts/widget
.
Остальные папки не раздавай: там есть внутренняя информация о системе сборки, которую опасно показывать наружу.
Если пишешь свой сервер, учти такие сложности:
Разные правила кэширования для файлов с хэшами и без
Файлы в папках .artifacts/{playground или widget}/assets
с хэшами в имени. Кэшируй их навечно: Cache-Control: public, max-age=31536000, immutable
.
Файлы снаружи assets
— без хэшей. Ревалидируй их на сервере: Cache-Control: public, no-cache
200 вместо 304 из-за етега
Nginx понижает etag-и до weak, когда сжимает. Из-за этого при серверной ревалидации файл перестает считаться тем же, который был, и вместо быстрого 304 сервер отдает долгий 200.
Решение: Зипуй в своем сервисе, в .NET — используй UseResponseCompression
.
404 при обновлениях
Чтобы загрузить виджет, нужно несколько запросов за скриптами и большинство из них имеют хэши в названии, которые меняются при релизах. Если запрос за loader.js
попадет на реплику с новой версией, а assets/index-1234abcd.js
уйдет на реплику со старой, она не найдет файл с таким именем, вернет 404 и пользователи не смогут загрузить виджет.
Решение: храни на сервере одновременно и новую, и старую версию бандла. Начинай раздавать новую только тогда, когда все реплики обновились.
Просто раздай папку .artifacts/widget
своим сервером статики.
Раздай папку .artifacts/playground
своим сервером статики.
Если используешь environmentConfigs.ts, научи свой сервер раздачи понимать, в каком окружении он работает, и по запросу плейграунда возвращать его index.{env}.html
. Остальные html-ки нужно не отдавать, иначе на проде засветятся настройки тестовых окружений.
Платформа виджетов под капотом использует same-origin iframe. Это помогает изолировать рантайм виджета, подписываться на onerror ошибки и выставлять свои csp политики. Но это создает некоторые ограничения.
Same-origin iframe исполняет js код виджета в новом iframe на том же домене. При этом отрисовка DOM выполняется в родителе. Нужно учитывать это при использовании UI библиотек. Если такая библиотека использует window напрямую, то она получит window не родителя, а виджета. Такое поведение можно обходить несколькими способами:
Если библиотека для доступа к window использует @skbkontur/global-object
(например, @skbkontur/react-ui
), то поместить @skbkontur/global-object
в sharedModules
у jsLoader. Т.е. global-object возвращает глобальные объекты в контексте потребителя, а не виджета.
Если библиотека не использует @skbkontur/global-object
, но позволяет переопределить window, то можно определить его как @skbkontur/global-object
.
Если библиотека не позволяет переопределить window, то можно ее вынести в js-loader. Для этого зависимость нужно добавить в getJsLoaderDependencies
в файле jsLoader/index.ts
. js-loader исполняется в контексте потребителя, а значит будет использован нужный window.
Второй способ решения этой проблемы - транспилировать код библиотеки так, чтобы все использования window стали использовать @skbkontur/global-object
.
Если ты шаришь зависимости через опцию shared, то нужно правильно обновлять зависимости. Иначе виджет может сломаться в рантайме. Чтобы этого не произошло, следуй инструкции:
- Убери из shared зависимость, которую обновляешь. Выпусти виджет.
- Опубликуй новую версию npm-loader.
- Подключи потребителям новую версию npm-loader.
- Дождись выпуска всех потребителей.
- Верни в shared свою зависимость. Выпусти виджет.
Почему именно такая последовательность? Если коротко: зависимости, которые мы переиспользуем, находятся в бандле пользователя. Поэтому без перевыпуска потребителя, мы не можем начать использовать новую версию зависимости.
Эта инструкция справедлива как для изменения мажорных версий, так и при некоторых минорных. Для минорных версий это нужно делать, когда используется новое апи, которое не поддерживается в версии потребителя.
- Система плагинов для более точной настройки платформы
- Телеметрия
- esm для шаринга зависимостей
-
Transforming destructuring to the configured target environment ("chrome49", "edge112", "firefox102", "ios15.6", "safari15.6" + 2 overrides) is not supported yet
Решение: в browserslist должны быть указаны только те браузеры, которые поддерживает vite. -
504 (Outdated Optimize Dep) - пересобрать или попробовать обновить страницу. Происходит, если ту же платформу подключить к другому виджету