Про паттерн Circuit Breaker можно почитать у Мартина Фаулера - https://martinfowler.com/bliki/CircuitBreaker.html
Мы в ряде фронтовых приложений Тинькофф используем CB уровня приложения, реализацию можно посмотреть на гитхабе - https://github.com/TinkoffCreditSystems/tinkoff-request/blob/master/packages/plugin-circuit-breaker/src/CircuitBreaker.ts
Можно считать это продвинутым механизмом таймаутов, который позволяет при проблемах с API незамедлительно на проблемные запросы отдавать ответы из кэша, либо последнюю ошибку, не дожидаясь таймаутов.
Дополнительно, это облегчает нагрузку на проблемный API
Мы в ряде фронтовых приложений Тинькофф используем CB уровня приложения, реализацию можно посмотреть на гитхабе - https://github.com/TinkoffCreditSystems/tinkoff-request/blob/master/packages/plugin-circuit-breaker/src/CircuitBreaker.ts
Можно считать это продвинутым механизмом таймаутов, который позволяет при проблемах с API незамедлительно на проблемные запросы отдавать ответы из кэша, либо последнюю ошибку, не дожидаясь таймаутов.
Дополнительно, это облегчает нагрузку на проблемный API
martinfowler.com
bliki: Circuit Breaker
You use software circuit breakers on connections to remote services. These breakers trip when the supplier becomes unresponsive, once tripped the breaker no longer calls the supplier until reset.
Отдельно ряд ссылочке про протокол QUIC - его активное распространение просто невероятно вдохновляет!
Реализация и успешное внедрение QUIC протокола от Facebook - https://engineering.fb.com/2020/10/21/networking-traffic/how-facebook-is-bringing-quic-to-billions/
Обзорная статья про QUIC от Cloudfire, внутри есть много ссылок с подробной информацией - https://blog.cloudflare.com/last-call-for-quic/
Кстати, рекомендую блоги инженеров Cloudfire и Fastly, много интересного контента.
https://www.fastly.com/blog
Реализация и успешное внедрение QUIC протокола от Facebook - https://engineering.fb.com/2020/10/21/networking-traffic/how-facebook-is-bringing-quic-to-billions/
Обзорная статья про QUIC от Cloudfire, внутри есть много ссылок с подробной информацией - https://blog.cloudflare.com/last-call-for-quic/
Кстати, рекомендую блоги инженеров Cloudfire и Fastly, много интересного контента.
https://www.fastly.com/blog
Engineering at Meta
How Facebook is bringing QUIC to billions
We are replacing the de facto protocol the internet has used for decades with QUIC, the latest and most radical step we’ve taken to optimize our network protocols to create a better experience for …
К разговору об отказоустойчивости, хочу поделиться частным случаем, как повысить надежность загрузки ресурсов фронтовых приложений.
Рецепт узнал у коллег в Тинькофф, также используем в продакшене.
Допустим, у нас есть несколько CDN для загрузки ресурсов - основной, и дополнительный.
Ошибку загрузки
Основная сложность в том, что бы запросить упавший ресурс с дополнительного CDN - это необходимость делать повторный запрос синхронно - например, повторная попытка загрузить
Оказывается, что обработчик
Таким образом, остается только синхронно повторно загрузить упавший ресурс.
На помощь приходит старый товарищ
Минимальный пример кода:
В этот код можно добавить health check запросы к CDN, добавить подробностей в ошибки, например информацию из
Статья в блоге Fastly про NEL - https://www.fastly.com/blog/network-error-logging
Демка на github с повторными запросами:
https://superoleg39.github.io/retry-resources/
https://github.com/SuperOleg39/retry-resources
Рецепт узнал у коллег в Тинькофф, также используем в продакшене.
Допустим, у нас есть несколько CDN для загрузки ресурсов - основной, и дополнительный.
Ошибку загрузки
css
или js
ресурса можно отловить с помощью обработчика window.onerror
.Основная сложность в том, что бы запросить упавший ресурс с дополнительного CDN - это необходимость делать повторный запрос синхронно - например, повторная попытка загрузить
vendor
скрипт, содержищий React
, должен завершиться до выполнения остальных скриптов приложения.Оказывается, что обработчик
window.onerror
выполняется синхронно, и блокирует выполнение JavaScript кода.Таким образом, остается только синхронно повторно загрузить упавший ресурс.
На помощь приходит старый товарищ
XMLHttpRequest
, который позволяет нам загрузить код скрипта с fallback CDN, и вставить его на страницу как inline
скрипт.Минимальный пример кода:
// этот inline скрипт должен быть расположен перед всеми загружаемыми скриптами и стилями
const primaryDomain = 'https://cdn-first.com/';
const fallbackDomain = 'https://cdn-second.com/';
const retriedResourcesList = {};
const maxRetries = 1;
function retry(event) {
// тут будет ссылка на DOM элемент script или link
const tag = event.target;
// если упал файл стилей, получим `LINK` в `tagName`, если скрипт - то `SCRIPT`
const tagName = tag.tagName && tag.tagName.toLowerCase();
if (tagName !== 'link' && tagName !== 'script') {
return;
}
const isLink = tagName === 'link';
const failedUrl = isLink ? tag.href : tag.src;
const fallbackUrl = getFallbackUrl(failedUrl);
// простой вариант не уйти в цикл повторных запросов
if (!retriedResourcesList[fallbackUrl]) {
retriedResourcesList[fallbackUrl] = 1;
} else if (retriedResourcesList[fallbackUrl] < maxRetries) {
retriedResourcesList[fallbackUrl] = retriedResourcesList[fallbackUrl] + 1;
} else {
return;
}
const newTag = isLink ? createLink(tag, fallbackUrl) : createScript(tag, fallbackUrl);
// вставляем новый тег рядом с упавшим ресурсом
tag.parentNode.insertBefore(newTag, tag);
}
function getFallbackUrl(url) {
if (url.indexOf(primaryDomain) === 0) {
return url.replace(primaryDomain, fallbackDomain);
}
return url;
}
function createLink(tag, fallbackUrl) {
const newTag = document.createElement('link');
newTag.href = fallbackUrl;
newTag.rel = 'stylesheet';
return newTag;
}
function createScript(tag, fallbackUrl) {
const newTag = document.createElement('script');
// тут можно обработать и "обогатить" ошибку
const xhr = new XMLHttpRequest();
xhr.open('GET', fallbackUrl, false);
xhr.send();
newTag.text = xhr.responseText;
return newTag;
}
// пока не выполнится обработчик retry, выполнение JS будет заблокировано!
window.addEventListener('error', retry, true);
В этот код можно добавить health check запросы к CDN, добавить подробностей в ошибки, например информацию из
window.navigator.connection
Отдельно порекомендую механизм NEL - Network Error Logging, который поддерживается в Chromium браузерах, и позволяет отправить невероятно подробные отчеты о сетевых ошибках.Статья в блоге Fastly про NEL - https://www.fastly.com/blog/network-error-logging
Демка на github с повторными запросами:
https://superoleg39.github.io/retry-resources/
https://github.com/SuperOleg39/retry-resources
Fastly
Network error logging: collecting failure conditions from end users | Fastly
In this post, we’ll explore the NEL framework, how it provides visibility, and ways to collect and process the resulting data.
Привет!
Рецепт ретраев загрузки статических ресурсов будет не полным, если не рассмотреть альтернативы.
На самом деле, самый простой и логичный механизм для этого - Service Worker.
Мы можем перехватить ошибку любого запроса, убедиться что это был запрос на статику на наш CDN, и продолжить цепочку промисов запросом на тот же ресурс с другого CDN.
Код в воркере может выглядеть примерно так:
Service Worker позволяет сделать нам гораздо более гибкие стратегии для повторных запросов, сам процесс ретраев будет полностью прозрачным.
Есть и минусы такого подхода:
- SW может быть еще не зарегистрирован, т.е. для первых пользователей сайта ретраи будут не доступны
- Если на домене доступны несколько приложений, но общий Service Worker, в теории эти приложения могут использовать разные CDN, и будет чуть труднее сделать логику ретраев подходящей для всех
- Используется
Но даже с учетом этих нюансов, использование Service Worker кажется более перспективным решением для механизма ретраев.
Альтернативный вариант реализации ретраев - интеграция этого механизма в Webpack.
Мне не нравится тем, что вариант самый не прозрачный и не очевидный, но возможно это дело вкуса)
Как я понимаю, основной вариант реализации ретраев - это расширение возможностей методов webpack_require , например плагин webpack-retry-chunk-load-plugin расширяет webpack_require.e (это require.ensure, который используется для динамических импортов), и в обработчике ошибок загрузки чанка продолжает цепочку промисов повторной загрузкой.
Ссылка на плагин - https://github.com/mattlewis92/webpack-retry-chunk-load-plugin
Никогда не помешает ссылка на великолепный инструмент workbox - https://developers.google.com/web/tools/workbox
Давно не заходил к ним на сайт, появился раздел с рецептами, в том числе пример offline fallback - несколько лет назад мне очень не хватало этого рецепта) https://developers.google.com/web/tools/workbox/modules/workbox-recipes?hl=en#offline_fallback
Рецепт ретраев загрузки статических ресурсов будет не полным, если не рассмотреть альтернативы.
На самом деле, самый простой и логичный механизм для этого - Service Worker.
Мы можем перехватить ошибку любого запроса, убедиться что это был запрос на статику на наш CDN, и продолжить цепочку промисов запросом на тот же ресурс с другого CDN.
Код в воркере может выглядеть примерно так:
self.addEventListener('fetch', event => {
return fetch(event.request).catch((error) => {
const requestToCDN = event.request.url.startsWith(primaryCDN);
const requestToAsset = event.request.url.endsWith('.css') || event.request.url.endsWith('.js'));
if (requestToCDN && requestToAsset) {
const nextUrl = event.request.url.replace(primaryCDN, fallbackCDN);
return fetch(new Request(nextUrl, event.request));
}
throw error;
});
});
Service Worker позволяет сделать нам гораздо более гибкие стратегии для повторных запросов, сам процесс ретраев будет полностью прозрачным.
Есть и минусы такого подхода:
- SW может быть еще не зарегистрирован, т.е. для первых пользователей сайта ретраи будут не доступны
- Если на домене доступны несколько приложений, но общий Service Worker, в теории эти приложения могут использовать разные CDN, и будет чуть труднее сделать логику ретраев подходящей для всех
- Используется
workbox
, и вклиниться в существующий процесс кэширования статики будет сложнееНо даже с учетом этих нюансов, использование Service Worker кажется более перспективным решением для механизма ретраев.
Альтернативный вариант реализации ретраев - интеграция этого механизма в Webpack.
Мне не нравится тем, что вариант самый не прозрачный и не очевидный, но возможно это дело вкуса)
Как я понимаю, основной вариант реализации ретраев - это расширение возможностей методов webpack_require , например плагин webpack-retry-chunk-load-plugin расширяет webpack_require.e (это require.ensure, который используется для динамических импортов), и в обработчике ошибок загрузки чанка продолжает цепочку промисов повторной загрузкой.
Ссылка на плагин - https://github.com/mattlewis92/webpack-retry-chunk-load-plugin
Никогда не помешает ссылка на великолепный инструмент workbox - https://developers.google.com/web/tools/workbox
Давно не заходил к ним на сайт, появился раздел с рецептами, в том числе пример offline fallback - несколько лет назад мне очень не хватало этого рецепта) https://developers.google.com/web/tools/workbox/modules/workbox-recipes?hl=en#offline_fallback
GitHub
GitHub - mattlewis92/webpack-retry-chunk-load-plugin: A webpack plugin to retry loading of chunks that failed to load
A webpack plugin to retry loading of chunks that failed to load - mattlewis92/webpack-retry-chunk-load-plugin
👍1
Привет!
Несколько дней назад на Github состоялся релиз фреймворка
На самом деле, это нельзя назвать новым релизом - во-первых,
В open source уже существует несколько интересных SSR фреймворков (если говорить про React экосистему): Next.js, Fusion.js от Uber, Umi.js, Remix от создателей react-router.
Отдельно хочу отметить Next.js, как самый активно развивающийся и передовой инструмент.
Мейнтейнеры Next, к примеру, в данный момент портируют используемые
Я бы хотел в серии постов рассказать об особенностях tramvai , которые считаю уникальными, и которыми продолжаю вдохновляться, и возможно немного о наших планах на будущее.
И конечно ссылочки:
- Next.js - https://github.com/vercel/next.js/
- Fusion.js - https://github.com/fusionjs/fusionjs
- Umi.js - https://github.com/umijs/umi
- Remix - https://remix.run/features
Несколько дней назад на Github состоялся релиз фреймворка
tramvai
- https://github.com/TinkoffCreditSystems/tramvaitramvai
- это фреймворк для создания SSR приложений на React, внутренняя разработка Тинькофф, и последние полтора года я работаю в небольшой технической команде, которая занимается поддержкой этого инструмента.На самом деле, это нельзя назвать новым релизом - во-первых,
tramvai
развивается уже много лет, во-вторых, в рамках постепенной миграции фреймворка в open source, мы настроили зеркалирование всех изменений кодовой базы на Github, но продолжаем вести разработку внутри компании.В open source уже существует несколько интересных SSR фреймворков (если говорить про React экосистему): Next.js, Fusion.js от Uber, Umi.js, Remix от создателей react-router.
Отдельно хочу отметить Next.js, как самый активно развивающийся и передовой инструмент.
Мейнтейнеры Next, к примеру, в данный момент портируют используемые
babel
плагины на Rust, для интеграции swc
и радикального улучшения Developer Experience!Я бы хотел в серии постов рассказать об особенностях tramvai , которые считаю уникальными, и которыми продолжаю вдохновляться, и возможно немного о наших планах на будущее.
И конечно ссылочки:
- Next.js - https://github.com/vercel/next.js/
- Fusion.js - https://github.com/fusionjs/fusionjs
- Umi.js - https://github.com/umijs/umi
- Remix - https://remix.run/features
Одна из ключевых особенностей
Встроенный механизм внедрения зависимостей позволяет собирать приложений из модулей как конструктор из кубиков, и при хорошем проектировании модулей, открывает потрясающие возможности для расширения или замены возможностей в нашем приложении.
Dependency Injection все активнее используется в мире фронтенда, но как мне кажется, ассоциируется только с Angular или Nest.js.
При этом увидеть использование DI контейнеров в React приложениях можно гораздо реже.
Учитывая активное развитие SSR приложений, меня даже немного удивляет, что этот паттерн не получил широкое распространение, далее постараюсь объяснить почему.
Несколько лет назад я занимался разработкой базовой инфраструктуры для миграции PHP приложения (с классической связкой - шаблонизатор на сервере и React на клиенте) на универсальное React приложения (SSR на NodeJS), в онлайн кинотеатре ivi.ru.
Начиная запускать код существующего клиентского JS приложения на сервере, сталкиваешься с рядом проблем, основная из которых - "универсальность" кода, т.е. насколько код сильно завязан на окружение, в котором будет выполняться.
Т.е. все обращения к
Как раз в это время я читал замечательную книгу "Шаблоны проектирования Node.JS", в которой познакомился с Dependency Injection и его реализациями в виде DI container или Service Locator. К слову, раньше мне очень трудно давалось понимание этого паттерна)
В какой-то момент в голове сложились все паззлы, и пришло понимание что DI дает нам очень удобную абстракцию для универсального кода, и лучше всего рассмотреть это на примере работы с
Для начала, пример итогового использования сервиса:
Далее, напишем реализации интерфейса CookieService:
И остается только зарегистрировать нужные реализации в подходящем окружении:
Таким образом мы можем практически полностью избавиться от постоянных проверок в коде на текущее окружение.
Также, мы можем изолировать DI на разные запросы в приложение, и использовать уникальные Request и Response, создавая отдельный DI контейнер на каждый запрос.
Другая фича, которую дает DI, и которая реализована в Angular, Nest.js и tramvai - это возможность расширения приложения отдельными модулями, дополняющими приложение конкретным функционалом с помощью набора токенов и провайдеров (интерфейсы и реализации зависимостей).
Если посмотреть на другие фронтенд фреймворки, можно увидеть разные механизмы для расширения кода - системы плагинов, огромные файлы конфигураци, или паттерн middleware.
tramvai- Dependency Injection.
Встроенный механизм внедрения зависимостей позволяет собирать приложений из модулей как конструктор из кубиков, и при хорошем проектировании модулей, открывает потрясающие возможности для расширения или замены возможностей в нашем приложении.
Dependency Injection все активнее используется в мире фронтенда, но как мне кажется, ассоциируется только с Angular или Nest.js.
При этом увидеть использование DI контейнеров в React приложениях можно гораздо реже.
Учитывая активное развитие SSR приложений, меня даже немного удивляет, что этот паттерн не получил широкое распространение, далее постараюсь объяснить почему.
Несколько лет назад я занимался разработкой базовой инфраструктуры для миграции PHP приложения (с классической связкой - шаблонизатор на сервере и React на клиенте) на универсальное React приложения (SSR на NodeJS), в онлайн кинотеатре ivi.ru.
Начиная запускать код существующего клиентского JS приложения на сервере, сталкиваешься с рядом проблем, основная из которых - "универсальность" кода, т.е. насколько код сильно завязан на окружение, в котором будет выполняться.
Т.е. все обращения к
location, работа с
localStorage, чтение
cookies, не должны происходить на сервере, либо должны содержать проверки на окружение, и например читать куки не из
document.cookies, а из
req.cookies, если мы используем
expressна сервере.
Как раз в это время я читал замечательную книгу "Шаблоны проектирования Node.JS", в которой познакомился с Dependency Injection и его реализациями в виде DI container или Service Locator. К слову, раньше мне очень трудно давалось понимание этого паттерна)
В какой-то момент в голове сложились все паззлы, и пришло понимание что DI дает нам очень удобную абстракцию для универсального кода, и лучше всего рассмотреть это на примере работы с
cookies.
Для начала, пример итогового использования сервиса:
// создадим общий контейнер зависимостей
const di = createContainer();
// опишем интерфейс сервиса для работы с куками
interface CookieService {
get(key): string;
set(key, value, options): void;
}
// React компонент, который завязан только на интерфейс, но не на реализацию
const Component = ({ cookieKey }) => {
// не важно, на сервере или на клиенте будет отрендерен компонент,
// главное что бы в di была подходящая реализация сервиса
const cookieService = useDI<CookieService>('cookie service');
const cookieValue = cookieService.get(cookieKey);
return ...;
}
Далее, напишем реализации интерфейса CookieService:
// серверная реализация, работает с объектом Request
class ServerCookieService implements CookieService {
constructor(request: Request) {}
get(key) { ... }
set(key, value, options) { ... }
}
// серверная реализация, работает с document.cookie
class ClientCookieService implements CookieService {
constructor() {}
get(key) { ... }
set(key, value, options) { ... }
}
И остается только зарегистрировать нужные реализации в подходящем окружении:
// server.js
// получаем зависимость
const request = di.get('request');
// регистрируем зависимость на сервере
di.provide('cookie service', new ServerCookieService(request));
// client.js
// регистрируем зависимость на клиенте
di.provide('cookie service', new ClientCookieService());
Таким образом мы можем практически полностью избавиться от постоянных проверок в коде на текущее окружение.
Также, мы можем изолировать DI на разные запросы в приложение, и использовать уникальные Request и Response, создавая отдельный DI контейнер на каждый запрос.
Другая фича, которую дает DI, и которая реализована в Angular, Nest.js и tramvai - это возможность расширения приложения отдельными модулями, дополняющими приложение конкретным функционалом с помощью набора токенов и провайдеров (интерфейсы и реализации зависимостей).
Если посмотреть на другие фронтенд фреймворки, можно увидеть разные механизмы для расширения кода - системы плагинов, огромные файлы конфигураци, или паттерн middleware.
На мой взгляд, модульный подход дает максимальную гибкость, и позволяет сделать минимальное зацепление кода у различных модулей.
Подробнее про реализацию DI в tramvai можно почитать в документации - https://tramvai.dev/docs/concepts/overview
И ссылочки на различные DI контейнеры:
- https://github.com/inversify/InversifyJS
- https://github.com/mgechev/injection-js
- https://github.com/microsoft/tsyringe
- https://github.com/typestack/typedi
Многие из этих библиотек используют механизм рефлексии, позволяющий делать магию с DI - https://www.npmjs.com/package/reflect-metadata
Рефлексия имеет ряд плюсов и недостатков, и на данный момент не используется в tramvai.
Подробнее про реализацию DI в tramvai можно почитать в документации - https://tramvai.dev/docs/concepts/overview
И ссылочки на различные DI контейнеры:
- https://github.com/inversify/InversifyJS
- https://github.com/mgechev/injection-js
- https://github.com/microsoft/tsyringe
- https://github.com/typestack/typedi
Многие из этих библиотек используют механизм рефлексии, позволяющий делать магию с DI - https://www.npmjs.com/package/reflect-metadata
Рефлексия имеет ряд плюсов и недостатков, и на данный момент не используется в tramvai.
tramvai.dev
Introduction to tramvai | tramvai
tramvai is a lightweight web framework for building SSR applications with a modular system and DI to quickly extend the functionality of applications.
Привет!
Хочу рассказать еще про одну особенность tramvai, связанную с жизненным циклом приложения, и запросами в это приложение.
Для начала рассмотрим несколько популярных вариантов, как запускаются и конфигурируются стандартные приложения, написанные на Express или React.
Если это
Если это
И это подходит для многих приложений, но при этом несет ряд проблем, при росте кодовой базы:
- Добавление новой независимой фичи - со своими страницами, сторами, провайдерами - потребует вносить изменения в несколько разных мест
- Не связанный код зацеплен друг с другом
- Если мы говорим про middleware, не связанный код может еще и ломать друг друга, подробнее в классном докладе - https://www.youtube.com/watch?v=RS8x73z4csI
- Сайд-эффекты могут быть размазаны по всему приложению, и для серверного рендеринга, нужно постараться, что бы выполнить все асинхронные действия до
А если в нашем SSR приложении мы хотим рендерить страницу как можно быстрее?
Для этого нужно запускать асинхронные действия максимально параллельно, и иметь возможность исключить действия, выходящие за определенный таймаут.
Затем, было бы неплохо выполнить эти сайд-эффекты, которые не уложились в тайминги, повторно на клиенте.
Звучит как не тривиальная задача, и некоторые из фреймворков пытаются решить ее тем или иным способом, например
Если обобщить, помимо производительности, страдает расширяемость и модульность приложения.
Хочу рассказать еще про одну особенность tramvai, связанную с жизненным циклом приложения, и запросами в это приложение.
Для начала рассмотрим несколько популярных вариантов, как запускаются и конфигурируются стандартные приложения, написанные на Express или React.
Если это
express
сервер, обычно создается одна точка входа, где создается приложение, к нему добавляются роуты и middleware, и запускается сервер через app.listen
Если это
React
приложение, мы так же в большинстве случаев увидим одну точку входа, где происходит инициализация стейт-менеджера, композиция провайдеров, и создание App
компонента. Если это SSR, то в серверной реализации App
дополнительно будет логика по загрузке всех данных для приложения, а в клиентской реализации - гидрация этих данных.И это подходит для многих приложений, но при этом несет ряд проблем, при росте кодовой базы:
- Добавление новой независимой фичи - со своими страницами, сторами, провайдерами - потребует вносить изменения в несколько разных мест
- Не связанный код зацеплен друг с другом
- Если мы говорим про middleware, не связанный код может еще и ломать друг друга, подробнее в классном докладе - https://www.youtube.com/watch?v=RS8x73z4csI
- Сайд-эффекты могут быть размазаны по всему приложению, и для серверного рендеринга, нужно постараться, что бы выполнить все асинхронные действия до
renderToString
нашего приложенияА если в нашем SSR приложении мы хотим рендерить страницу как можно быстрее?
Для этого нужно запускать асинхронные действия максимально параллельно, и иметь возможность исключить действия, выходящие за определенный таймаут.
Затем, было бы неплохо выполнить эти сайд-эффекты, которые не уложились в тайминги, повторно на клиенте.
Звучит как не тривиальная задача, и некоторые из фреймворков пытаются решить ее тем или иным способом, например
Appolo
просто рендерит наше приложение на сервере несколько раз, пока не выполнит все query - и это очень не эффективно, т.к. renderToString
это самая медленная часть нашего SSR, к тому же синхронно блокирующий event loop.Если обобщить, помимо производительности, страдает расширяемость и модульность приложения.
YouTube
Node.js Middleware – никогда больше! [ru] / Тимур Шемсединов
Видео с онлайн-конференции JavaScript fwdays'20 autumn, которая прошла 22 сентября 2020 года.
Описание доклада:
Почему приложение работает нестабильно, происходит утечка памяти и процесс часто вылетает? Почему вам сложно найти ошибку и нужно долго делать…
Описание доклада:
Почему приложение работает нестабильно, происходит утечка памяти и процесс часто вылетает? Почему вам сложно найти ошибку и нужно долго делать…
Что интересного предлагает
С помощью Dependency Injection и механизма Command Line Chain, фреймворк дает возможность вклиниться на ряд этапов жизненного цикла приложения:
- Различные этапы инициализации веб-сервера (т.к. мы используем
- Различные этапы запуска
- Этапы для загрузки данных, необходимых для рендеринга страницы (один из них как раз создан для сайд-эффектов)
- Этап генерации страницы (на нем сервер делает
- Этапы для загрузки данных при SPA-переходах
Механизм Command Line Chain во-первых, предоставляет наглядный флоу жизни приложения и запросов.
Во-вторых, выполняет все действия параллельно, на каждом этапе.
Во-вторых, вместе с механизмом Actions, позволяет установить таймауты на выполнение действий, отдать ответ на клиент не дожидаясь этих действий, и выполнить их повторно на клиенте.
Пример добавления нового действия на этап получения информации о пользователе, и документация - https://coretech-frontend.pages.devplatform.tcsbank.ru/tramvai/docs/concepts/command-line-runner#resolve_user_deps
При необходимости, с помощью провайдеров можно предоставлять хуки и guard'ы для переходов роутера, например для проверки доступов пользователя к конкретной странице - https://tramvai.dev/docs/references/libs/router#router-guards
Также, есть провайдеры, позволяющие добавить скрипты, стили или meta-теги в HTML разметку итоговой страницы.
Пример такого провайдера, и документация - https://coretech-frontend.pages.devplatform.tcsbank.ru/tramvai/docs/references/modules/render#как-добавить-загрузку-ассетов-на-странице
Такая гибкость дает нам возможность создавать отдельные модули, которые по сути являются списком провайдеров, вместе реализующих определенную фичу, и подключать эти модули простым добавлением в список modules при создании нашего приложения.
tramvai
, для решения этих кейсов?С помощью Dependency Injection и механизма Command Line Chain, фреймворк дает возможность вклиниться на ряд этапов жизненного цикла приложения:
- Различные этапы инициализации веб-сервера (т.к. мы используем
express
под капотом, можно добавить express middleware или роуты)- Различные этапы запуска
listen
сервера (можно добавить обработчики uncaughtException
и unhandledRejection
, запустить на другом порту мокер, или static сервер)- Этапы для загрузки данных, необходимых для рендеринга страницы (один из них как раз создан для сайд-эффектов)
- Этап генерации страницы (на нем сервер делает
renderToString
, а клиент hydrate
)- Этапы для загрузки данных при SPA-переходах
Механизм Command Line Chain во-первых, предоставляет наглядный флоу жизни приложения и запросов.
Во-вторых, выполняет все действия параллельно, на каждом этапе.
Во-вторых, вместе с механизмом Actions, позволяет установить таймауты на выполнение действий, отдать ответ на клиент не дожидаясь этих действий, и выполнить их повторно на клиенте.
Пример добавления нового действия на этап получения информации о пользователе, и документация - https://coretech-frontend.pages.devplatform.tcsbank.ru/tramvai/docs/concepts/command-line-runner#resolve_user_deps
// загружаем в store данные, необходимые для последующего рендеринга страницы
{
provide: commandLineListTokens.resolveUserDeps,
multi: true,
useFactory: ({ store }) => {
return async function updateCounterStore() {
await someAsyncAction();
store.dispatch(incrementEvent());
};
},
deps: {
store: STORE_TOKEN,
},
}
При необходимости, с помощью провайдеров можно предоставлять хуки и guard'ы для переходов роутера, например для проверки доступов пользователя к конкретной странице - https://tramvai.dev/docs/references/libs/router#router-guards
Также, есть провайдеры, позволяющие добавить скрипты, стили или meta-теги в HTML разметку итоговой страницы.
Пример такого провайдера, и документация - https://coretech-frontend.pages.devplatform.tcsbank.ru/tramvai/docs/references/modules/render#как-добавить-загрузку-ассетов-на-странице
// регистрируем meta viewport, который будет добавлятся на каждую страницу
{
provide: RENDER_SLOTS,
multi: true,
useValue: {
type: ResourceType.asIs,
slot: ResourceSlot.HEAD_META,
payload:
'<meta name="viewport" content="width=device-width, initial-scale=1">',
},
}
Такая гибкость дает нам возможность создавать отдельные модули, которые по сути являются списком провайдеров, вместе реализующих определенную фичу, и подключать эти модули простым добавлением в список modules при создании нашего приложения.
tramvai.dev
router | tramvai
Routing library. It can work both on the server and on the client. Designed primarily for building isomorphic applications.
Привет!
В ближайших постах хочу рассказать про устройство монорепозитория
Наш приватный репозиторий содержит около 150 npm библиотек, инфраструктуру для сборки, тестирования и публикации этих пакетов, сайт документации и различные утилиты.
Версиями пакетов, публикациями и генерацией ченджлогов управляет внутренняя разработка Тинькофф - библиотека
На каждый мерж в master ветку,
На каждый релизный тег запускается пайплайн публикации пакетов в приватный npm registry, и одновременно зеркалирование публичного кода на Github.
Для управления версиями пакетов в репозитории мы используем два важных подхода - сквозное версионирование, и хранение версий в релизных тегах.
Сквозное версионирование используется для всех пакетов, имеющий непосредственное отношение к фреймворку (cli, ядро фреймворка, многочисленные модули), этот термин означает что все unified пакеты общую версию, и должны обновляться одновременно.
Такой подход вы можете увидеть у
Основной плюс unified версионирования - гарантируется совместимость между пакетами одной версии. Это очень крутая возможность, т.к. раньше у пользователя был только один простой вариант поднять версию фреймворка, не потеряв совместимость - устанавливать все пакеты latest версии.
Один из минусов подхода - любое обновление пакета из списка unified, требует поднять версии и опубликовать все эти пакеты из списка, что значительно замедляет CI.
Для удобной работы со сквозными версиями tramvai предлагает два механизма:
- cli команда
- утилита
Хранение версий в релизных тегах само по себе не дает преимуществ, и у нас используется вместе со stub версиями пакетов в исходных
Допустим, у нас был пакет с зависимостями:
Раньше, каждый крупный Merge Request сопровождался конфликтами, если в master ветке обновлялась версии пакетов , а затронутые библиотеки в MR содержали изменения в
Теперь, наш пакет выглядит так:
Версия
Раньше я не встречался с таким подходом, и в целом не работал с монорепозиториями, но на своем опыте увидел, как это упростило разработку.
Также, для монорепы мы используем
На Github репозитории, для публикации кода в npm, версии пакетов поставляются в файле https://github.com/TinkoffCreditSystems/tramvai/blob/main/packages-versions.json
В ближайших постах хочу рассказать про устройство монорепозитория
tramvai
, и про решение зеркалировать публичный код на Github, вместо полной миграции.Наш приватный репозиторий содержит около 150 npm библиотек, инфраструктуру для сборки, тестирования и публикации этих пакетов, сайт документации и различные утилиты.
Версиями пакетов, публикациями и генерацией ченджлогов управляет внутренняя разработка Тинькофф - библиотека
pvm
. На каждый мерж в master ветку,
pvm
определяет какие пакеты были изменены, поднимает их версии согласно conventional commits
, и создает релизный тег, где и хранятся все версии. На каждый релизный тег запускается пайплайн публикации пакетов в приватный npm registry, и одновременно зеркалирование публичного кода на Github.
Для управления версиями пакетов в репозитории мы используем два важных подхода - сквозное версионирование, и хранение версий в релизных тегах.
Сквозное версионирование используется для всех пакетов, имеющий непосредственное отношение к фреймворку (cli, ядро фреймворка, многочисленные модули), этот термин означает что все unified пакеты общую версию, и должны обновляться одновременно.
Такой подход вы можете увидеть у
Angular
, и с некоторыми ограничениями, в монорепозиториях использующих Lerna
- https://github.com/lerna/lerna#fixedlocked-mode-defaultОсновной плюс unified версионирования - гарантируется совместимость между пакетами одной версии. Это очень крутая возможность, т.к. раньше у пользователя был только один простой вариант поднять версию фреймворка, не потеряв совместимость - устанавливать все пакеты latest версии.
Один из минусов подхода - любое обновление пакета из списка unified, требует поднять версии и опубликовать все эти пакеты из списка, что значительно замедляет CI.
Для удобной работы со сквозными версиями tramvai предлагает два механизма:
- cli команда
tramvai update
для легкого обновления версий - утилита
@tramvai/tools-check-versions
, которая поставляется вместе с core
пакетом, и на этапе postinstall
проверяет версии установленных зависимостей в приложении. Хранение версий в релизных тегах само по себе не дает преимуществ, и у нас используется вместе со stub версиями пакетов в исходных
package.json
файлах.Допустим, у нас был пакет с зависимостями:
{
"name": "@tramvai/foo",
"version": "0.1.0",
"dependencies": {
"@tramvai/bar": "^1.1.0",
"@tramvai/baz": "^2.0.0"
}
}
Раньше, каждый крупный Merge Request сопровождался конфликтами, если в master ветке обновлялась версии пакетов , а затронутые библиотеки в MR содержали изменения в
dependencies
. Теперь, наш пакет выглядит так:
{
"name": "@tramvai/foo",
"version": "0.0.0-stub",
"dependencies": {
"@tramvai/bar": "0.0.0-stub",
"@tramvai/baz": "0.0.0-stub"
}
}
Версия
0.0.0-stub
никогда не вызовет конфликтов слияния, а вычисление реальных версий происходит только в CI - при создании нового релизного тега и публикации, внутри библиотеки pvm
. Раньше я не встречался с таким подходом, и в целом не работал с монорепозиториями, но на своем опыте увидел, как это упростило разработку.
Также, для монорепы мы используем
yarn workspaces
, и воркспейсы замечательно работают со stub версиями. На Github репозитории, для публикации кода в npm, версии пакетов поставляются в файле https://github.com/TinkoffCreditSystems/tramvai/blob/main/packages-versions.json
Про development и production сборку пакетов.
Все tramvai библиотеки написаны на
Для локальной разработки, мы используем
Project References дают возможность одной командой запускать сборку всех пакетов в репозитории, и при этом делать пересборку только измененного кода.
Также, при пересборке учитываются, кто зависит от изменившегося пакета, и выполняется пересборка итогового графа зависимостей - это позволяет сразу увидеть, как изменения одного пакета могут поломать другие.
Минимальное время старта команды
К сожалению, Typescript не дает инструмента для автоматической генерации Project References и поддержки их в актуальном состоянии - поэтому в tramvai монорепе используется самописная утилита
Вторая проблема Project References - это отсутствие сгенерированных
Мы решаем эту проблему так:
- В
- Перед публикацией, все поля
Для эффективной production сборки библиотек мы используем
- es2019 код с CommonJS модулями для запуска через старые версии NodeJS, бандл будет указан в
- es2019 код с ES modules для современных бандлеров (
- И если для пакета указано поле brow
Таким образом, на пакет, который указывает отдельные точки входа для серверной и клиентской сборки, сгенерируется три бандла - serv
Почему мы публикуем код в es20
Фронтенд библиотеки в Тинькофф (в части проектов, в основном это React экосистема) следуют принципу, когда публикуются только современный JavaScript код, а разработчики приложений транспилируют зависимости в подходящий стандарт кода.
Немного мотивации к публикации modern кода:
- https://web.dev/publish-modern-javascript/
- https://github.com/sindresorhus/ama/issues/446
- https://dev.to/garylchew/bringing-modern-javascript-to-libraries-432c
- https://babeljs.io/blog/2018/06/26/on-consuming-and-publishing-es2015+-packages
@tra
- Общий serv
- Для клиентского кода у нас есть два набора brow
Кажется это должно улучшаться в лучшую сторону с каждым обновлением существующих JS движков.
В итоге, мы получили неплохой Developer Experience при разработке в монорепе, и поставляем код библиотек минимального размера, с поддержкой tree-shaking и различных target окружений, для наших пользователей.
Все tramvai библиотеки написаны на
Typescript
, и комфортная разработка требует тщательной настройки инфраструктуры монорепозитория. Для локальной разработки, мы используем
tsc
и Project References - https://www.typescriptlang.org/docs/handbook/project-references.html, в этой связке watch
режим имеет минимальное время старта, и очень быструю пересборку. Project References дают возможность одной командой запускать сборку всех пакетов в репозитории, и при этом делать пересборку только измененного кода.
Также, при пересборке учитываются, кто зависит от изменившегося пакета, и выполняется пересборка итогового графа зависимостей - это позволяет сразу увидеть, как изменения одного пакета могут поломать другие.
Минимальное время старта команды
tsc --build --watch
достигается за счет кэширования информации о сборке каждого пакета.К сожалению, Typescript не дает инструмента для автоматической генерации Project References и поддержки их в актуальном состоянии - поэтому в tramvai монорепе используется самописная утилита
@tramvai-monorepo/fix-ts-references
Вторая проблема Project References - это отсутствие сгенерированных
.d.ts
файлов при первой сборке, и возможные ошибки этой сборки. Мы решаем эту проблему так:
- В
package.json
каждого пакета поле typings
смотрит на исходные .ts
файлы в папке src
- Перед публикацией, все поля
typings
заменяются на собранные .d.ts
файлы из папки lib
Для эффективной production сборки библиотек мы используем
rollup
, поверх которого написана утилита @tramvai/build
- https://tramvai.dev/docs/references/tools/build @tramvai/build
собирает код каждого пакета в несколько общих бандлов: - es2019 код с CommonJS модулями для запуска через старые версии NodeJS, бандл будет указан в
"main"
поле в package.json
- es2019 код с ES modules для современных бандлеров (
@tramvai/cli
использует webpack@5), бандл будет указан в "module"
поле в package.json
- И если для пакета указано поле brow
ser в p
ackage.json, со
бирается отдельный es2019 код с ES modules, содержащий код для браузерного окружения Таким образом, на пакет, который указывает отдельные точки входа для серверной и клиентской сборки, сгенерируется три бандла - serv
er.js, se
rver.es.js и b
rowser.js
Спецификацию поля browser мож
но посмотреть тут - https://github.com/defunctzombie/package-browser-field-spec Почему мы публикуем код в es20
19, вм
есто es5? Фронтенд библиотеки в Тинькофф (в части проектов, в основном это React экосистема) следуют принципу, когда публикуются только современный JavaScript код, а разработчики приложений транспилируют зависимости в подходящий стандарт кода.
Немного мотивации к публикации modern кода:
- https://web.dev/publish-modern-javascript/
- https://github.com/sindresorhus/ama/issues/446
- https://dev.to/garylchew/bringing-modern-javascript-to-libraries-432c
- https://babeljs.io/blog/2018/06/26/on-consuming-and-publishing-es2015+-packages
@tra
mvai/cli соб
ирает бандлы приложения по такой логике: - Общий serv
er.js бан
дл с серверным кодом, содержащий все зависимости, и код стандарта es5.
И
нтересный факт, эксперименты с использованием es2019/es2020 код
а на сервере показали ухудшение производительности примерно на 10%! - Для клиентского кода у нас есть два набора brow
serslist кон
фигов - default и modern, и es2019 код
библиотек транспилируется в формат для целевых браузеров с помощью babel, г
енерируются отдельные чанки с default и modern кодом, и затем для браузера выбираются нужные через проверку User-Agent
Вт
орой интересный факт, modern сбо
рка может весить значительно меньше default, но
как и в случае с серверным кодом, может работать медленнее. Кажется это должно улучшаться в лучшую сторону с каждым обновлением существующих JS движков.
В итоге, мы получили неплохой Developer Experience при разработке в монорепе, и поставляем код библиотек минимального размера, с поддержкой tree-shaking и различных target окружений, для наших пользователей.
www.typescriptlang.org
Documentation - Project References
How to split up a large TypeScript project
Добавлю парочку полезных ссылок:
Топовые советы в Wiki Typescript по ускорению сборки, об этой страничке я узнал в канале @wild_wild_web - https://github.com/microsoft/TypeScript/wiki/Performance
Например, совет по ограничению поля "types" почти в два раза ускорил нашу production сборку - https://github.com/microsoft/TypeScript/wiki/Performance#controlling-types-inclusion
Проблема была в том, что монорепозиторий содержит огромное количество тайпингов в зависимостях, и для транспиляции даже файла с одной строчкой кода, TS тратил очень много времени на резолв этих тайпингов, из которых реально нужные, без прямого импорта - это
Утилита для сборки пакетов
И мощный фреймворк для организации монорепозиториев NX - https://nx.dev/
Это достаточно сложный для освоения и интеграции инструмент, но потенциально может в разы ускорять CI процессы в больших монорепозиториях.
Пример работы инструмента Nx Cloud, с распределенным выполнением задач и кэшированием - https://www.youtube.com/watch?v=Exs64pscwxA&ab_channel=Nrwl-NarwhalTechnologiesInc.
Топовые советы в Wiki Typescript по ускорению сборки, об этой страничке я узнал в канале @wild_wild_web - https://github.com/microsoft/TypeScript/wiki/Performance
Например, совет по ограничению поля "types" почти в два раза ускорил нашу production сборку - https://github.com/microsoft/TypeScript/wiki/Performance#controlling-types-inclusion
Проблема была в том, что монорепозиторий содержит огромное количество тайпингов в зависимостях, и для транспиляции даже файла с одной строчкой кода, TS тратил очень много времени на резолв этих тайпингов, из которых реально нужные, без прямого импорта - это
node
и jest
Утилита для сборки пакетов
microbundle
, наш референс при разработке @tramvai/build
- https://github.com/developit/microbundleИ мощный фреймворк для организации монорепозиториев NX - https://nx.dev/
Это достаточно сложный для освоения и интеграции инструмент, но потенциально может в разы ускорять CI процессы в больших монорепозиториях.
Пример работы инструмента Nx Cloud, с распределенным выполнением задач и кэшированием - https://www.youtube.com/watch?v=Exs64pscwxA&ab_channel=Nrwl-NarwhalTechnologiesInc.
GitHub
Performance
TypeScript is a superset of JavaScript that compiles to clean JavaScript output. - microsoft/TypeScript
👍1
Привет!
Про выход tramvai в open source.
Зачем это нужна для команды разработчиков, и для Тинкофф в целом?
Одна из важнейших вещей, это еще одно громкое заявление - "В Тинькофф крутой фронтенд!"
Разработчики Тинькофф делают все больше отличных open source проектов, пишут все больше интересных статей, выступают на конференциях - из таких активностей формируется образ компании, привлекательный для новых разработчиков.
Мы активно нанимаем коллег, и у нас классные интервьюеры 😉
Для мейнтейнеров tramvai, это новый источник вдохновения для развития фреймворка, возможность получить свежий взгляд со стороны.
И один из плюсов, это быстрое прототипирование с помощью JS песочниц, недавно мы добавили базовый шаблон трамвая на Codesandbox - https://codesandbox.io/s/tramvai-new-qgk90
Далее, опишу ряд проблем, которые могут стоять на пути у проприетарной разработки, усложняющие переход в OSS:
- Код, документация и история VCS могут содержать чувствительную информацию - самый простой пример, это ссылки на внутренние ресурсы
- Часть пакетов подходит для OSS, часть предназначена только для внутреннего использования
- Пакеты публикуются в приватный регистр пакетов
- Проект использует приватный CI
- Отсутствут локализация документации
Таким образом, полный переход Github требует отделить публичную часть проекта, перенести код без сохранения истории коммитов, настроить в Github Actions линтинг, unit и интеграционное тестирование, версионирование и публикацию пакетов, перевод, сборку и деплой документации.
Например, такой переход осуществил огненный Angular ui-kit от наших коллег - https://taiga-ui.dev/
Taiga содержит ряд проприетарных компонентов в приватном репозитории внутри Тинькофф, и приватный репозитрий использует публичный с помощью git submodules.
Почему этот путь не подошел для tramvai:
- Теряется история коммитов - это важный источник информации для разработчика
- Проект имеет достаточно сложный CI, и в версионировании и публикации пакетов участвует другая внутренняя разработка Тинькофф, о которой я писал в одном из предыдущих постов
- Все tramvai пакеты объединены сквозным версионированием, отделение публичных означает, что два набора пакетов разойдутся в версиях, и вернутся проблемы, связанные с обновлением зависимостей
- Не удобно делать глобальный рефакторинг, который затрагивает все пакеты
Каждая из этих проблем кажется неизбежным злом, но вместе порождают очень много сложностей, которые мы постарались решить инкрементальным выходом в OSS.
Про выход tramvai в open source.
Зачем это нужна для команды разработчиков, и для Тинкофф в целом?
Одна из важнейших вещей, это еще одно громкое заявление - "В Тинькофф крутой фронтенд!"
Разработчики Тинькофф делают все больше отличных open source проектов, пишут все больше интересных статей, выступают на конференциях - из таких активностей формируется образ компании, привлекательный для новых разработчиков.
Мы активно нанимаем коллег, и у нас классные интервьюеры 😉
Для мейнтейнеров tramvai, это новый источник вдохновения для развития фреймворка, возможность получить свежий взгляд со стороны.
И один из плюсов, это быстрое прототипирование с помощью JS песочниц, недавно мы добавили базовый шаблон трамвая на Codesandbox - https://codesandbox.io/s/tramvai-new-qgk90
Далее, опишу ряд проблем, которые могут стоять на пути у проприетарной разработки, усложняющие переход в OSS:
- Код, документация и история VCS могут содержать чувствительную информацию - самый простой пример, это ссылки на внутренние ресурсы
- Часть пакетов подходит для OSS, часть предназначена только для внутреннего использования
- Пакеты публикуются в приватный регистр пакетов
- Проект использует приватный CI
- Отсутствут локализация документации
Таким образом, полный переход Github требует отделить публичную часть проекта, перенести код без сохранения истории коммитов, настроить в Github Actions линтинг, unit и интеграционное тестирование, версионирование и публикацию пакетов, перевод, сборку и деплой документации.
Например, такой переход осуществил огненный Angular ui-kit от наших коллег - https://taiga-ui.dev/
Taiga содержит ряд проприетарных компонентов в приватном репозитории внутри Тинькофф, и приватный репозитрий использует публичный с помощью git submodules.
Почему этот путь не подошел для tramvai:
- Теряется история коммитов - это важный источник информации для разработчика
- Проект имеет достаточно сложный CI, и в версионировании и публикации пакетов участвует другая внутренняя разработка Тинькофф, о которой я писал в одном из предыдущих постов
- Все tramvai пакеты объединены сквозным версионированием, отделение публичных означает, что два набора пакетов разойдутся в версиях, и вернутся проблемы, связанные с обновлением зависимостей
- Не удобно делать глобальный рефакторинг, который затрагивает все пакеты
Каждая из этих проблем кажется неизбежным злом, но вместе порождают очень много сложностей, которые мы постарались решить инкрементальным выходом в OSS.
Первый этап, на котором сейчас находится проект - это зеркалирование публичного кода на Github, при этом вся разработка ведется в приватном репозитории.
На этапе исследования, мы нашли мощный инструмент, созданный для аналогичных целей внутри Google,
При чем, кажется это вторая или третья итерация по созданию такого инструмента от разработчиков Google.
Кстати, для copybara есть гибкая обвязка под Github Actions - https://github.com/Olivr/copybara-action и публичный Docker образ - https://github.com/anipos/copybara-docker-image
Каждый релиз трамвая, запускается джоба с генерацией конфига и запуском copybara.
Конфиг содержит список публичных файлов и трансформации для кода - например, замена ссылок приватной документации на https://tramvai.dev
Copybara проверяет изменения с последнего коммита в Github репе, схлопывает все новые коммиты в один релизный коммит, и пушить его на Github.
Затем в Github Actions запускается сборка и деплой документации, и сборки и публикация пакетов в npm.
Я несколько раз накосячил с публикацией пакетов и откатывал их, оказалось, что npm запрещает публикацию удаленных пакетов в течение 24 часов, и запрещает публиковать ту же версию, что была удалена 😥
По итогу, мы всегда имеем актуальный код на Github, наш фреймворк доступен для публичного использования, и при этом наши процессы разработки не поменялись.
Следующий шаг миграции в OSS - это зеркалирование MR от контрибьюторов из Github в наш приватный репозиторий, на данный момент мы можем делать это только вручную.
Двусторонняя синхронизация репозиториев это потенциальный источник проблем, поэтому мы не спешим с переходом на этот этап.
В будущем хочется полностью перейти на Github, отказавшись от синхронизации репозиториев.
На этапе исследования, мы нашли мощный инструмент, созданный для аналогичных целей внутри Google,
copybara
- https://github.com/google/copybara.При чем, кажется это вторая или третья итерация по созданию такого инструмента от разработчиков Google.
Кстати, для copybara есть гибкая обвязка под Github Actions - https://github.com/Olivr/copybara-action и публичный Docker образ - https://github.com/anipos/copybara-docker-image
Каждый релиз трамвая, запускается джоба с генерацией конфига и запуском copybara.
Конфиг содержит список публичных файлов и трансформации для кода - например, замена ссылок приватной документации на https://tramvai.dev
Copybara проверяет изменения с последнего коммита в Github репе, схлопывает все новые коммиты в один релизный коммит, и пушить его на Github.
Затем в Github Actions запускается сборка и деплой документации, и сборки и публикация пакетов в npm.
Я несколько раз накосячил с публикацией пакетов и откатывал их, оказалось, что npm запрещает публикацию удаленных пакетов в течение 24 часов, и запрещает публиковать ту же версию, что была удалена 😥
По итогу, мы всегда имеем актуальный код на Github, наш фреймворк доступен для публичного использования, и при этом наши процессы разработки не поменялись.
Следующий шаг миграции в OSS - это зеркалирование MR от контрибьюторов из Github в наш приватный репозиторий, на данный момент мы можем делать это только вручную.
Двусторонняя синхронизация репозиториев это потенциальный источник проблем, поэтому мы не спешим с переходом на этот этап.
В будущем хочется полностью перейти на Github, отказавшись от синхронизации репозиториев.
GitHub
GitHub - google/copybara: Copybara: A tool for transforming and moving code between repositories.
Copybara: A tool for transforming and moving code between repositories. - google/copybara
😁1
Привет!
Хочу рассказать про одно не законченное исследование, и возможно таким образом дать себе инерцию завершить это дело)
Фронтенд экосистема Тинькофф состоит из двух мощных экосистем - Angular и React, примерно 100+ разработчиков на каждый фреймворк.
Для каждой экосистемы разработан и поддерживается свой ui-kit - один для React, и второй для Angular.
Целью исследования было проверить, можно ли создать функциональный фреймворк-агностик ui-kit на одной технологии, и эффективно переиспользовать его в каждом нашем фреймворке.
Спойлер - реализации прототипа только на одной технологии мне хватило для вывода, что реализовать такой kit, не потеряв при его использовании developer experience, сравнительно с нативным китом, невозможно, или крайне сложно, но мне по прежнему очень интересно, чего можно добиться с различными технологиями.
Начнем со списка возможностей, который дает нам React UI-kit в React приложении:
- Можно передавать свойства в компонент
- Обновление свойств приведет к обновлению DOM
- В компонент можно вкладывать другие компоненты (композиция с помощью children и render props)
- Доступ к context приложения
- Поддержка SSR
Начать исследования я решил со знакомых, и на первый взгляд максимально близкий друг к другу технологий - React приложение и Preact UI-kit.
Другие потенциальные технологии - Web Components (плюс один из фреймворков с поддержкой SSR), Svelte.
Для интеграции Preact компонентов (да и любых других в будущем) в React приложение я решил использовать концепцию адаптеров - такой адаптер принимает Preact компонент из кита, и возвращает React компонент, готовый к использованию в приложении.
Код адаптера доступен тут - https://github.com/SuperOleg39/agnostik-kit/blob/master/adapters/preact-to-react/src/index.js
Для проверки возможностей, заранее создал адаптер-пустышку, обернул в него компоненты, и написал в приложении основные кейсы - с передачей пропсов, с коллбэками, пробросом в children текста, React компонентов, и других Preact компонентов, обернутых в адаптер.
Кейсы можно посмотреть тут - https://github.com/SuperOleg39/agnostik-kit/blob/master/apps/react-app/src/App.js
Рассмотрим подробнее реализацию адаптера.
На серверной стороне, мы должны отрендерить Preact компонент в строку c помощью
Вспомним требование про композицию - в children могут быть React компоненты, которые заранее надо отрендерить в строку, и так же вставить в китовый компонент.
React компоненты могут содержать другие PreactAdapter компоненты, так что по сути это рекурсивный процесс.
Оказывается, что механизм добавления произвольной разметки в React и Preact одинаковый -
Это дает нам возможность композиции, в несколько этапов:
1. Делаем ReactDOM.renderToString текущих children
2. Результат передаем в
3. Делаем Preact.renderToString этого компонента
4. Результат передаем в
В клиентском коде, в этот враппер необходимо отрендерить Preact компонент, а при наличии children, сделать их гидрацию (помним, что там будут React компоненты), используя ref на китовый компонент в качестве root.
Также, необходимо делать повторный рендеринг Preact компонента, при каждом изменении props.
Такой небольшой адаптер покрывает большую часть полноценной интеграции, и мы уже можем увидеть ряд недостатков:
- Теряем react context для вложенных React компонентов
- DOM дерево разрастается из-за врапперов для
- Нарушаем флоу работы React - вместо стандартного рендеринга дерева компонентов от самого вложенного, к родителю, и общего маунта, мы рендерим компоненты в адаптере сверху вниз, и маунтим по цепочке.
В теории, при больших поддеревьях компонентов, можно увидеть значительные задержки рендеринга.
- Нужно писать надежный механизм перерендеринга китового компонента
Хочу рассказать про одно не законченное исследование, и возможно таким образом дать себе инерцию завершить это дело)
Фронтенд экосистема Тинькофф состоит из двух мощных экосистем - Angular и React, примерно 100+ разработчиков на каждый фреймворк.
Для каждой экосистемы разработан и поддерживается свой ui-kit - один для React, и второй для Angular.
Целью исследования было проверить, можно ли создать функциональный фреймворк-агностик ui-kit на одной технологии, и эффективно переиспользовать его в каждом нашем фреймворке.
Спойлер - реализации прототипа только на одной технологии мне хватило для вывода, что реализовать такой kit, не потеряв при его использовании developer experience, сравнительно с нативным китом, невозможно, или крайне сложно, но мне по прежнему очень интересно, чего можно добиться с различными технологиями.
Начнем со списка возможностей, который дает нам React UI-kit в React приложении:
- Можно передавать свойства в компонент
- Обновление свойств приведет к обновлению DOM
- В компонент можно вкладывать другие компоненты (композиция с помощью children и render props)
- Доступ к context приложения
- Поддержка SSR
Начать исследования я решил со знакомых, и на первый взгляд максимально близкий друг к другу технологий - React приложение и Preact UI-kit.
Другие потенциальные технологии - Web Components (плюс один из фреймворков с поддержкой SSR), Svelte.
Для интеграции Preact компонентов (да и любых других в будущем) в React приложение я решил использовать концепцию адаптеров - такой адаптер принимает Preact компонент из кита, и возвращает React компонент, готовый к использованию в приложении.
Код адаптера доступен тут - https://github.com/SuperOleg39/agnostik-kit/blob/master/adapters/preact-to-react/src/index.js
Для проверки возможностей, заранее создал адаптер-пустышку, обернул в него компоненты, и написал в приложении основные кейсы - с передачей пропсов, с коллбэками, пробросом в children текста, React компонентов, и других Preact компонентов, обернутых в адаптер.
Кейсы можно посмотреть тут - https://github.com/SuperOleg39/agnostik-kit/blob/master/apps/react-app/src/App.js
Рассмотрим подробнее реализацию адаптера.
На серверной стороне, мы должны отрендерить Preact компонент в строку c помощью
preact-render-to-string
, до рендеринга в строку нашего React приложения.Вспомним требование про композицию - в children могут быть React компоненты, которые заранее надо отрендерить в строку, и так же вставить в китовый компонент.
React компоненты могут содержать другие PreactAdapter компоненты, так что по сути это рекурсивный процесс.
Оказывается, что механизм добавления произвольной разметки в React и Preact одинаковый -
dangerouslySetInnerHTML: { __html: string }
, правда нам потребуется создавать лишний тег - враппер, который и будет содержать эту разметку.Это дает нам возможность композиции, в несколько этапов:
1. Делаем ReactDOM.renderToString текущих children
2. Результат передаем в
dangerouslySetInnerHTML
Preact компонента3. Делаем Preact.renderToString этого компонента
4. Результат передаем в
dangerouslySetInnerHTML
враппераВ клиентском коде, в этот враппер необходимо отрендерить Preact компонент, а при наличии children, сделать их гидрацию (помним, что там будут React компоненты), используя ref на китовый компонент в качестве root.
Также, необходимо делать повторный рендеринг Preact компонента, при каждом изменении props.
Такой небольшой адаптер покрывает большую часть полноценной интеграции, и мы уже можем увидеть ряд недостатков:
- Теряем react context для вложенных React компонентов
- DOM дерево разрастается из-за врапперов для
dangerouslySetInnerHTML
- Нарушаем флоу работы React - вместо стандартного рендеринга дерева компонентов от самого вложенного, к родителю, и общего маунта, мы рендерим компоненты в адаптере сверху вниз, и маунтим по цепочке.
В теории, при больших поддеревьях компонентов, можно увидеть значительные задержки рендеринга.
- Нужно писать надежный механизм перерендеринга китового компонента
GitHub
agnostik-kit/adapters/preact-to-react/src/index.js at master · SuperOleg39/agnostik-kit
Contribute to SuperOleg39/agnostik-kit development by creating an account on GitHub.
Взвесив все эти минусы интеграции таких близких на первый взгляд технологий, решили не продолжать исследования, но идея попробовать другие технологии осталась.
Через несколько месяцев, мне попалась статья на habr про создание общего UI-kit для Vue и React с помощью Stencil - https://habr.com/ru/company/uchi_ru/blog/543308/
Веб-компоненты в связке со Stencil кажутся перспективными для фреймворк агностик кита, из-за флоу инициализации и работы на сервере и клиенте - рендеринг web components происходит поверх уже отрендеренного приложения, что в теории позволяет сохранить общее дерево нашего React приложения.
А внешняя схожесть React и Preact на самом деле никак не упрощает интеграцию, из-за завязки рендеринга фреймворка на конкретный DOM узел.
Также, у Stencil существует интеграция для React - https://stenciljs.com/docs/react
Про не завершенную, но интересную попытку интегрировать Stencil постараюсь рассказать в следующем посте.
Через несколько месяцев, мне попалась статья на habr про создание общего UI-kit для Vue и React с помощью Stencil - https://habr.com/ru/company/uchi_ru/blog/543308/
Веб-компоненты в связке со Stencil кажутся перспективными для фреймворк агностик кита, из-за флоу инициализации и работы на сервере и клиенте - рендеринг web components происходит поверх уже отрендеренного приложения, что в теории позволяет сохранить общее дерево нашего React приложения.
А внешняя схожесть React и Preact на самом деле никак не упрощает интеграцию, из-за завязки рендеринга фреймворка на конкретный DOM узел.
Также, у Stencil существует интеграция для React - https://stenciljs.com/docs/react
Про не завершенную, но интересную попытку интегрировать Stencil постараюсь рассказать в следующем посте.
Хабр
Единый UI-кит и синхронизация дизайна в Учи.ру. Часть 1
Пожалуй, все, кто имел дело с развитием семейства сайтов, сталкивались с проблемой поддержания единого вида компонентов. Когда счет сервисов идет на десятки и со...
Привет!
Продолжаю тему создания фреймворк-агностик UI-kit, с помощью веб-компонентов и Stencil - https://stenciljs.com/
Почему нам вообще нужно использовать какой-то фреймворк для веб-компонентов?
Для этого есть ряд причин, вот несколько самых важных:
- web components не поддерживают server-side rendering
- механизм обновления DOM должен реализовывать разработчик
- много шаблонного кода
Фреймворк
Отдельная боль - это не прозрачная конфигурация, и отсутствие рецептов в документации, как интегрировать Stencil, например новый UI-kit, в уже существующую монорепу.
Пока писал этот пост, обнаружил что во время предыдущего исследования сделал не корректную интеграцию, генерируя Stencil код не в библиотеку с компонентами, а в само приложение - https://github.com/SuperOleg39/agnostik-kit/pull/1
Нашел на гитхабе хороший пример интеграции Stencil и NextJS - https://github.com/jagreehal/nextjs-stenciljs-ssr-example, его и буду использовать для нового исследования.
Вот так выглядит рендеринг в строку на сервере:
И гидрация на клиенте:
Пример достаточно старый, и не использует официальные биндинги для React - https://stenciljs.com/docs/react, вместо этого используется хук
Сгенерировал биндинги в этом PR - https://github.com/SuperOleg39/nextjs-stenciljs-ssr-example/pull/1, большая часть кода - генерируется Stencil.
Интеграция React биндингов тоже крайне не привычна - мы должны создать пакет - "пустышку" для будущих React компонентов, но при этом компилировать его содержимое будет наш пакет с web компонентами, везде сложно понять, что надо сохранять в git, а что нужно только для публикации.
Еще заметил минус у биндингов - нет поддержки SSR при генерации React компонентов, и отсутствует нужный рецепт в документации, по итогу надо использовать оба пакета - с web-компонентами и с React биндингами.
Краткое резюме, после базовой интеграции Stencil с React и NextJS:
сама интеграция - достаточно проблемная, но мы получили типизированные React компоненты и возможность передавать в свойста веб-компонентов children, функции и объекты.
children можно передавать, если в веб-компоненте используется
Далее, я начал проверять, как ведут себя
Кстати, в React биндингах Stencil есть хак -
Затем, я провел ряд экспериментов, где в web-component вставлял React ноды, другие web-components, PR с тестовыми кейсами можно посмотреть тут (файл `a.tsx`) - https://github.com/SuperOleg39/nextjs-stenciljs-ssr-example/pull/2/files
Продолжаю тему создания фреймворк-агностик UI-kit, с помощью веб-компонентов и Stencil - https://stenciljs.com/
Почему нам вообще нужно использовать какой-то фреймворк для веб-компонентов?
Для этого есть ряд причин, вот несколько самых важных:
- web components не поддерживают server-side rendering
- механизм обновления DOM должен реализовывать разработчик
- много шаблонного кода
Stencil
решает эти проблемы, предоставляя поддержку SSR, vDOM + реактивные биндинги, и возможность использовать JSXФреймворк
Stencil
дает много возможностей, хотя оказался для меня достаточно не комфортным в интеграции - дело в том, что аналогично Svelte
, Stencil это компилируемый фреймворк, и например создавая библиотеку компонентов, перед публикацией мы должны генерировать код для рендеринга этих компонентов, который будет использовать наше приложение.Отдельная боль - это не прозрачная конфигурация, и отсутствие рецептов в документации, как интегрировать Stencil, например новый UI-kit, в уже существующую монорепу.
Пока писал этот пост, обнаружил что во время предыдущего исследования сделал не корректную интеграцию, генерируя Stencil код не в библиотеку с компонентами, а в само приложение - https://github.com/SuperOleg39/agnostik-kit/pull/1
Нашел на гитхабе хороший пример интеграции Stencil и NextJS - https://github.com/jagreehal/nextjs-stenciljs-ssr-example, его и буду использовать для нового исследования.
Вот так выглядит рендеринг в строку на сервере:
// под капотом у renderToHTML - ReactDOM.renderToString
const html = await app.renderToHTML(req, res, req.path, req.query);
// рендерим Stencil поверх результата рендеринга ReactDOM
const renderedHtml = await stencil.renderToString(html);
И гидрация на клиенте:
const {
applyPolyfills,
defineCustomElements
} = require("stencil-web-components/loader");
// все это происходит после ReactDOM.hydrate
applyPolyfills().then(() => {
defineCustomElements(window);
});
Пример достаточно старый, и не использует официальные биндинги для React - https://stenciljs.com/docs/react, вместо этого используется хук
useCustomElement
Сгенерировал биндинги в этом PR - https://github.com/SuperOleg39/nextjs-stenciljs-ssr-example/pull/1, большая часть кода - генерируется Stencil.
Интеграция React биндингов тоже крайне не привычна - мы должны создать пакет - "пустышку" для будущих React компонентов, но при этом компилировать его содержимое будет наш пакет с web компонентами, везде сложно понять, что надо сохранять в git, а что нужно только для публикации.
Еще заметил минус у биндингов - нет поддержки SSR при генерации React компонентов, и отсутствует нужный рецепт в документации, по итогу надо использовать оба пакета - с web-компонентами и с React биндингами.
Краткое резюме, после базовой интеграции Stencil с React и NextJS:
сама интеграция - достаточно проблемная, но мы получили типизированные React компоненты и возможность передавать в свойста веб-компонентов children, функции и объекты.
children можно передавать, если в веб-компоненте используется
<slot />
Далее, я начал проверять, как ведут себя
children
в веб-компоненте, и обнаружил, что Stencil
по умолчанию не использует shadow DOM
, и все веб-компоненты, используемые как React биндинги, сломаны на этапе ReactDOM.hydrate
- для исправления проблемы нужно включать shadow DOM вручную для каждого Stencil компонента, через shadow: true
, немного о проблеме в этой статье - https://leechy.dev/hide-stencil-childrenКстати, в React биндингах Stencil есть хак -
defineCustomElements
вызывается сразу, т.е. до ReactDOM.hydrate
- а React уже получает shadow DOM при гидрации, и не модифицирует его.Затем, я провел ряд экспериментов, где в web-component вставлял React ноды, другие web-components, PR с тестовыми кейсами можно посмотреть тут (файл `a.tsx`) - https://github.com/SuperOleg39/nextjs-stenciljs-ssr-example/pull/2/files
Stencil
Build. Customize. Distribute. Adopt.
Я был настроен скептически, но все тесты показали отличный результат:
- Сам механизм интеграции веб-компонентов устроен так, что React context не теряется
- Рендеринг на сервере и гидрация - работают корректно
- React прекрасно перерендеривает children, которые веб-компоненты рендерят через
- Веб-компоненты прекрасно обрабатывают изменение локального состояния и shadow DOM, не ломая управляемую React`ом разметку, которая приходит через
- Многократная вложенность React и веб-компонентов друг в друга ничего не ломают
Все работает корректно, если я все правильно понял, т.к. React управляет только разметкой в
Завершая исследование, могу сказать что web-components действительно отлично подходят для создания компонентов, которые могут быть использованы в любом фреймворке.
Мейнтейнеры
Для SPA приложений, интеграция веб-компонентов тривиальна, и может даже не требовать отдельных фреймворков вроде
Какие вопросы у меня остаются к
- Документация и интеграции - очень много открытых вопросов, интеграция с React рассмотрена поверхностно
- Размер генерируемого кода - для нескольких компонентов, Stencil сгенерировал 400кб исходного кода. Сделал анализа бандла, импорт и использование Stencil компонента из нашего UI-kit добавляет примерно 45кб gzip кода.
Определенно, такое маленькое приложение на Svelte сгенерировало бы гораздо меньше кода, это примерно размер react + react-dom
- Производительность рендеринга - сделал поверхностные замеры, похоже и на сервере и на клиенте все быстро - но приложение слишком небольшое
- Необходимость в React биндингах - очень уж не очевидный механизм для их генерации
- Сам механизм интеграции веб-компонентов устроен так, что React context не теряется
- Рендеринг на сервере и гидрация - работают корректно
- React прекрасно перерендеривает children, которые веб-компоненты рендерят через
<slot />
, не ломая разметку веб-компонента- Веб-компоненты прекрасно обрабатывают изменение локального состояния и shadow DOM, не ломая управляемую React`ом разметку, которая приходит через
children
- Многократная вложенность React и веб-компонентов друг в друга ничего не ломают
Все работает корректно, если я все правильно понял, т.к. React управляет только разметкой в
template
, а веб-компоненты сразу рендерят эти template в slot
, т.е. области ответственности фреймворков не пересекаются.Завершая исследование, могу сказать что web-components действительно отлично подходят для создания компонентов, которые могут быть использованы в любом фреймворке.
Мейнтейнеры
Stencil
проделали огромную работу, и фреймворк дает нам возможность использовать компоненты в SSR React приложении, без каких-то серьезных ограничений в developer experience.Для SPA приложений, интеграция веб-компонентов тривиальна, и может даже не требовать отдельных фреймворков вроде
Stencil
или lit-element
для простых случаев.Какие вопросы у меня остаются к
Stencil
:- Документация и интеграции - очень много открытых вопросов, интеграция с React рассмотрена поверхностно
- Размер генерируемого кода - для нескольких компонентов, Stencil сгенерировал 400кб исходного кода. Сделал анализа бандла, импорт и использование Stencil компонента из нашего UI-kit добавляет примерно 45кб gzip кода.
Определенно, такое маленькое приложение на Svelte сгенерировало бы гораздо меньше кода, это примерно размер react + react-dom
- Производительность рендеринга - сделал поверхностные замеры, похоже и на сервере и на клиенте все быстро - но приложение слишком небольшое
- Необходимость в React биндингах - очень уж не очевидный механизм для их генерации
Интересная статья про минусы
Но похоже это единственное хорошее решение с поддержкой SSR, в
Stencil
- https://www.abeautifulsite.net/posts/moving-from-stencil-to-lit-element/Но похоже это единственное хорошее решение с поддержкой SSR, в
lit
это только в экспериментах/планах - https://www.polymer-project.org/blog/2020-09-22-lit-element-and-lit-html-next-preview и https://github.com/lit/lit/tree/main/packages/labs/ssrwww.abeautifulsite.net
Moving from Stencil to LitElement
A blog about everything web. Est. 2007