sportsbook-test
v0.4.43
Published
## Установка и использование
Readme
Sportsbook Integration Library
Установка и использование
Импорт библиотеки
Для использования библиотеки в вашем проекте:
// ES модули
import { IntegrationLayer } from 'sportsbook';
// Или по типам
import type { T4BTheme } from 'sportsbook/types';
// Создание экземпляра
const integration = new IntegrationLayer({
wrapperId: 'my-root-element'
backendUrl: 'backend-url',
signalrInstance: signalrInstance,
});
// Инжектирование стилей (опционально)
integration.injectCSS({
light: {
'primary-accent': '#ff0000',
'background': '#ffffff'
},
dark: {
'primary-accent': '#00ff00',
'background': '#000000'
}
});
// Запуск приложения
integration.build();TypeScript поддержка
При использовании TypeScript, убедитесь что у вас корректно настроен импорт типов:
import { IntegrationLayer } from 'sportsbook';
import type {
T4BTheme,
T4BMainColors,
T4BThemeColors,
IntegrationLayerConstructorInfo
} from 'sportsbook/types';Структура проекта после сборки
После выполнения pnpm build создается:
dist/sportsbook.es.js- основной файл библиотекиdist/sportsbook.css- стилиdist/types/index.d.ts- TypeScript типыdist/main-[hash].js- основной чанк с кодом
Что необходимо доработать:
- [ ] Тянуть все стили не из public, а как-то инкапуслировать эту логику чтоб при билде мы не отдавали клиентам все наши паблик ассеты
- [x] Добавить страницу 404 в роутинг
- [x] Добавить возможность собственного лоадера для страниц (скелетоны)
Роутинг приложения
Все страницы создаются в src/routes.
Создаем папку {ваше_название} -> создаем 2 файла: index.tsx и route.ts
В index.tsx лежит сама страница:
const Page = () => {
return <div>Page</div>;
};
export default Page;В route.ts регистрируем роут:
import { lazy } from 'react';
import { registrateRoute } from '$shared/router/utils';
export const PageRoute = registrateRoute({
component: lazy(() => import('./index')),
pathname: '/page',
});Далее в src/routes/index.tsx нужно подключить роут:
import { PageRoute } from './page/route'
export const PATHS: Route[] = [
// other routes
PageRoute
]
export const AppRouter = () => {
return (
<>
<Navigation />
<Router paths={PATHS} />
</>
);
};Если хотим добавить loading состояние пока прогружается страница - в registrateRoute нужно добавить поле loading передав туда или функцию или компонент
import { lazy } from 'react';
import { registrateRoute } from '$shared/router/utils';
import { Loader } from './loader';
export const PageRoute = registrateRoute({
component: lazy(() => import('./index')),
pathname: '/page',
loading: Loader, // или loading: () => {...}
});Еслил нужно подгрузить данные перед тем как отобразить страницу - в registrateRoute нужно добавить поле onBeforeLoad передав туда асинхронную функцию, которая возвращает промис
import { lazy } from 'react';
import { registrateRoute } from '$shared/router/utils';
import { Loader } from './loader';
export const SportPageRoute = registrateRoute({
pathname: '/',
onBeforeLoad: async () => {
return Promise.resolve(123);
},
component: lazy(() => import('./index')),
layout: SportLayout,
loading: Loader,
});
Для навигации по страницам нужно использовать компонент Link который лежит в $shared/components/link
Пример использования:
<nav>
<Link href="/">to /</Link>
<Link href="/cybersport">to /cybersport</Link>
<Link href={`/event/${Math.floor(Math.random() * 20)}`}>to /event/[random]</Link>
</nav>Интеграции с разными фреймворками:
Nuxt
onMounted(() => {
loadSript() {
scriptTag; // ...load bundle by this script
scriptTag.onload= () => {
new window.IntegraionLayer(/* params */).build();
}
document.getElementById('integration-layer')?.appendChild(scriptTag);
}
loadScript();
})
<template><div id="integration-layer"></div></template>с Nuxt все окей - проблем не возникло
Next.js
Аналогично с Nuxt, вместо onMounted используется useEffect или useLayoutEffect
Angular + Router
export class AppComponent implements AfterViewInit { // component
title = 'angular-test';
@ViewChild('main') mainElement!: ElementRef;
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}
ngAfterViewInit() {
if (isPlatformBrowser(this.platformId)) {
loadScript = () => {
// все та же логика интеграции
}
loadScript()
}
}
}Все окей, но особенность роутера у ангуляра - сериализует url для безопасности -> ?path=/sport превращается в ?path=%2Fsports. Но на стороне нашего спортбука (в роутере), когда нициализируется роутер (проставляются инит значения), мы десериализуем полученную url и все окей.
Как у нас работает дев/прод
Dev
С девом все просто, в main.tsx если среда DEV то это обычное SPA.
Prod
Так как в проде мы не должны аффектить клиента и он не должен нас, используем ShadowDOM для инкапсуляции.
if (import.meta.env.PROD) {
const tapgameStyles = new CSSStyleSheet();
const styles = 'TAPGAME_STYLES_PLACEHOLDER' + this.injectedStyles;
tapgameStyles.replaceSync(styles);
const shadowRoot = container.attachShadow({ mode: 'open' });
shadowRoot.adoptedStyleSheets = [tapgameStyles];
shadowRoot.appendChild(wrapper);
window.tapgameShadowRoot = shadowRoot;
}Здесь мы создаем ShadowDOM добавляя туда стили (о них ниже) и в качестве элемента внутрь добавляем наш созданный wrapper в который мы и будем рендерить приложение. Ничего сложного
Что там со стилями?
Какая есть проблема: когда мы работаем с shadow-root мы не можем брать стили извне -> мы должны стили каким-то образом положить в shadow-dom. Напрямую CSS-Modules класть у нас не получилось. Потому что собирая стили в .css файл его нужно как-то импортировать. Если попробовать его добавлять через
// это мы бы добавляли после билда скриптом, тк это должно быть внутри shadow-dom
<link href="./sportbook.css">то будет 404 потому что по сути запрос полетит от лица клиента, а у него такого файла нет.
Поэтому используется плагин vite-plugin-css-injected-by-js чтобы css modules переводить js чанки, от этого плагина мы используем метод injectCode который возвращает весь (если на билде выключена опция разбиения css на чанки) css приложения в виде строки.
import css from 'vite-plugin-css-injected-by-js';
defineConfig({
plugins: [
// ...
css({
injectCode: (css) => {
collectedCss = css;
return css;
},
}),
],
})Полученую строку мы должны как-то добавить в наш билд чтобы заинъектить.
Для этого создаем некоторый плейсхолдер в виде строки (после билда строки остаются прежними) и вместо этой строки мы будем подставлять полученую css строку и после добавлять в shadow-dom через конструкцию const tapgameStyles = new CSSStyleSheet(); + tapgameStyles.replaceSync(styles);
Вот как это выглядит в месте где у нас плейсхолдер
// создаем CSSStyleSheet, потому что добавляя стили в обычный тег style они добавялются с ковычками в начале и в конце, их можно обрезать но пока
// выбрали этот вариант
const tapgameStyles = new CSSStyleSheet();
// сам плейсхолжер который мы будем заменять | это стили которые мы сгенерили по переменным которые передал клиент.
const styles = 'TAPGAME_STYLES_PLACEHOLDER' + this.injectedStyles;
// добавляем наши стили в CSSStyleSheet. ! Важный момент, мы это делаем только на проде -> после билда вместо TAPGAME_STYLES_PLACEHOLDER у нас строка со всеми стилями приложения
tapgameStyles.replaceSync(styles);
const shadowRoot = container.attachShadow({ mode: 'open' });
// добалвяем все это дело в shadow-dom
shadowRoot.adoptedStyleSheets = [tapgameStyles];
shadowRoot.appendChild(wrapper);
window.tapgameShadowRoot = shadowRoot;А вот как мы подменяем плейсхолдер на стили:
let collectedCss = '';
defineConfig({
plugins: [
// ...other plugins
css({
injectCode: (css) => {
collectedCss = css;
return css;
},
}),
// наш плагин в котором производим подмену placeholder
insertCssToJsPlaceholderPlugin(() => collectedCss, {
targetFile: resolve(resolve(__dirname, 'dist'), 'integration.es.js'),
placeholder: '"TAPGAME_STYLES_PLACEHOLDER"',
}),
],
})Таким образом в бандл попадают все стили приложения и все хорошо работает в рамках shadow-dom.
Важно помнить:
Когда мы импортиурем какую-то сущность из файла, import { a } from './module' - начинает испольняться весь файл:
// module.js
console.log('it`s module a');
function innerFunction(value) {
console.log('inner function start work');
return value;
}
export const a = 10;
const innerVar = innerFunction(1);
(() => { console.log('im IIFE, an im running instantly!') })();в этом пример в консоли мы получим следующее:
it`s module a
inner function start work
im IIFE, an im running instantly!То есть при импорте всего одной переменной у нас отработала и IIFE функция, и произошла инициализия переменной innerVar которая не имеет экспорта нарушу, и так же innerVar инициировал вызов innerFunction
Нужно не забывать, что в reatom atom так же является функцией, следовательно когда модуль будет обрабатываться, но мы не трогали атом - он так же будет проинициализирован как и innerVar c innerFunction
