20 KiB
Архитектура Widget Системы
Оглавление
- Обзор архитектуры
- SSR совместимость
- Компоненты системы
- Жизненный цикл виджета
- Коммуникация PostMessage
- Безопасность
- Layout переключение
- Event система
- Примеры использования
- Troubleshooting
Обзор архитектуры
Widget система представляет собой комплексное решение для встраивания Quasar приложения в виде iframe на внешние сайты с полноценной двусторонней коммуникацией.
Основные принципы:
- SSR-совместимость - система работает как на клиенте, так и в SSR режиме
- Автоматическое определение - iframe режим определяется автоматически
- Безопасная коммуникация - PostMessage с валидацией origin
- Динамический layout - автоматическое переключение между обычным и widget layout
- Event-driven архитектура - система событий для взаимодействия
Диаграмма архитектуры:
┌─────────────────┐ PostMessage ┌─────────────────┐
│ Parent Site │ ◄──────────────► │ Widget App │
│ │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ Widget SDK │ │ │ │Widget Bridge│ │
│ │ (External) │ │ │ │ (Internal) │ │
│ └─────────────┘ │ │ └─────────────┘ │
│ │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ iframe │ │ │ │Widget Layout│ │
│ │ │ │ │ │ │ │
│ └─────────────┘ │ │ └─────────────┘ │
└─────────────────┘ └─────────────────┘
SSR совместимость
Проблема
В SSR режиме объект window не существует, что приводило к ошибкам при инициализации widget системы.
Решение
Система использует lazy инициализацию и client-only checks:
// Проверка на существование window (для SSR)
const isClient = typeof window !== 'undefined';
// Все операции с DOM выполняются только на клиенте
if (isClient) {
// код работы с window, document, etc.
}
Стратегия инициализации:
- На сервере: Создаются только реактивные состояния без обращений к browser API
- На клиенте: Выполняется полная инициализация через
forceInitialize() - Lazy singleton: Widget экземпляры создаются только при первом обращении
Компоненты системы
1. WidgetMode (widget-mode.ts)
Назначение: Определение и управление widget режимом
Ключевые особенности:
- Singleton pattern с lazy инициализацией
- Автоматическое определение iframe режима
- Управление CSS стилями и темами
- Конфигурация widget поведения
Методы:
class WidgetMode {
static getInstance(): WidgetMode;
getState(): IWidgetState;
updateConfig(config: Partial<IWidgetConfig>): void;
forceInitialize(): void; // Принудительная инициализация на клиенте
static createWidgetUrl(baseUrl: string, options: object): string;
}
Определение widget режима:
private detectWidgetMode(): void {
if (!isClient) return; // SSR защита
const urlParams = new URLSearchParams(window.location.search);
const isInIframe = window.parent !== window;
const widgetModeParam = urlParams.get(WIDGET_URL_PARAMS.WIDGET_MODE);
// Widget режим определяется по:
// 1. Наличию iframe (window.parent !== window)
// 2. URL параметру ?widget=true
widgetState.isWidget = isInIframe || widgetModeParam === 'true';
}
2. PostMessageBridge (postmessage-bridge.ts)
Назначение: Безопасная коммуникация между iframe и родительским окном
Функциональность:
- Валидация origin для безопасности
- Очередь сообщений до готовности
- Система подписок на события
- Автоматическая сериализация данных
Типы сообщений:
const WIDGET_MESSAGE_TYPES = {
// Исходящие (из виджета)
WIDGET_READY: 'widget:ready',
WIDGET_DATA_CHANGE: 'widget:data-change',
WIDGET_ERROR: 'widget:error',
WIDGET_NAVIGATE: 'widget:navigate',
WIDGET_RESIZE: 'widget:resize',
// Входящие (в виджет)
WIDGET_INIT: 'widget:init',
WIDGET_SET_DATA: 'widget:set-data',
WIDGET_GET_DATA: 'widget:get-data',
WIDGET_NAVIGATE_TO: 'widget:navigate-to',
WIDGET_UPDATE_CONFIG: 'widget:update-config',
WIDGET_DESTROY: 'widget:destroy',
};
Безопасность коммуникации:
private handleMessage(event: MessageEvent) {
// 1. Проверка origin
if (!isOriginAllowed(event.origin, this.config.allowedOrigins)) {
console.warn(`Blocked message from unauthorized origin: ${event.origin}`);
return;
}
// 2. Валидация структуры сообщения
if (!isValidWidgetMessage(event.data)) {
console.warn('Invalid message format:', event.data);
return;
}
// 3. Обработка сообщения
this.processMessage(event.data);
}
3. useWidgetBridge (use-widget-bridge.ts)
Назначение: Vue композабл для работы с widget bridge
Возможности:
- Реактивное состояние widget
- Методы отправки данных
- Система событий
- Автоматическое отслеживание размеров
Пример использования:
const {
isReady,
isWidget,
widgetData,
config,
sendData,
sendError,
sendNavigation,
sendResize,
on,
off,
} = useWidgetBridge();
// Отправка данных
if (isWidget.value && isReady.value) {
sendData({ user: userData });
}
// Подписка на события
on('data-change', (data) => {
console.log('Received data:', data);
});
4. DynamicLayoutWrapper (routes/index.ts)
Назначение: Динамическое переключение layout в зависимости от режима
Реализация:
const DynamicLayoutWrapper = defineComponent({
name: 'DynamicLayoutWrapper',
setup() {
// Определяем widget режим только на клиенте
const isWidgetMode =
typeof window !== 'undefined' &&
(window.parent !== window ||
new URLSearchParams(window.location.search).get('widget') === 'true');
return () => {
const LayoutComponent = isWidgetMode ? widgetLayout : layout;
return h(LayoutComponent);
};
},
});
Преимущества:
- Автоматическое определение layout
- SSR совместимость
- Нет необходимости в ручном переключении
Жизненный цикл виджета
1. Инициализация (Server-Side)
// На сервере создаются только реактивные состояния
const widgetState = reactive({
isWidget: false,
isReady: false,
parentOrigin: '',
config: { ...DEFAULT_WIDGET_CONFIG },
});
2. Hydration (Client-Side)
// В boot файле на клиенте
if (typeof window !== 'undefined') {
// Принудительная инициализация
widgetModeInstance.forceInitialize();
const widgetState = widgetModeInstance.getState();
if (widgetState.isWidget.value) {
// Инициализация widget режима
widgetEventManager.initialize();
}
}
3. Widget Detection
private detectWidgetMode(): void {
const urlParams = new URLSearchParams(window.location.search);
const isInIframe = window.parent !== window;
const widgetModeParam = urlParams.get('widget');
widgetState.isWidget = isInIframe || widgetModeParam === 'true';
if (widgetState.isWidget) {
this.initializeWidgetMode(urlParams);
}
}
4. Bridge Setup
if (checkIfWidget()) {
isWidget.value = true;
bridge = new PostMessageBridge(config);
// Подписка на события
bridge.on('ready', (data) => {
isReady.value = true;
widgetData.value = data;
});
}
5. Layout Application
// В DynamicLayoutWrapper
const isWidgetMode =
typeof window !== 'undefined' &&
(window.parent !== window ||
new URLSearchParams(window.location.search).get('widget') === 'true');
const LayoutComponent = isWidgetMode ? widgetLayout : layout;
6. Communication Ready
// Отправка сообщения о готовности
this.sendMessage(WIDGET_MESSAGE_TYPES.WIDGET_READY, {
config: this.config,
timestamp: Date.now(),
});
Коммуникация PostMessage
Отправка сообщений
public sendMessage(type: WidgetMessageType, data?: any, id?: string) {
if (!isClient) return; // SSR защита
const message = createWidgetMessage(type, data, id);
if (this.isReady) {
this.sendMessageToParent(message);
} else {
// Сообщения в очереди до готовности
this.messageQueue.push(message);
}
}
Получение сообщений
private handleMessage(event: MessageEvent) {
// Origin validation
if (!isOriginAllowed(event.origin, this.config.allowedOrigins)) {
return;
}
// Message validation
if (!isValidWidgetMessage(event.data)) {
return;
}
// Processing
this.processMessage(event.data);
}
Очередь сообщений
private processMessageQueue() {
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
if (message) {
this.sendMessageToParent(message);
}
}
}
Безопасность
1. Origin Validation
export function isOriginAllowed(
origin: string,
allowedOrigins: string[],
): boolean {
if (allowedOrigins.includes('*')) return true;
return allowedOrigins.some((allowed) => {
if (allowed.startsWith('*.')) {
const domain = allowed.substring(2);
return origin.endsWith(domain);
}
return origin === allowed;
});
}
2. Message Validation
export function isValidWidgetMessage(message: any): message is IWidgetMessage {
return (
typeof message === 'object' &&
message !== null &&
typeof message.type === 'string' &&
typeof message.timestamp === 'number' &&
Object.values(WIDGET_MESSAGE_TYPES).includes(message.type)
);
}
3. Sandbox Attributes
iframe.setAttribute(
'sandbox',
'allow-scripts allow-same-origin allow-forms allow-popups',
);
Layout переключение
Widget Layout Features
.widget-layout {
// Убираем все лишние отступы
&.q-layout {
min-height: auto;
}
// Компактный header
.widget-header {
min-height: 40px;
}
// Компактный footer
.widget-footer {
min-height: 30px;
}
// Основная страница виджета
.widget-page {
padding: 8px;
min-height: auto;
}
// Темы
&.widget-layout--theme-light {
background: white;
color: #1a1a1a;
}
&.widget-layout--theme-dark {
background: #1a1a1a;
color: white;
}
}
Динамическое скрытие элементов
private setupWidgetEnvironment(): void {
if (!isClient) return;
document.body.classList.add('widget-mode');
if (widgetState.config.hideHeader) {
document.body.classList.add('widget-hide-header');
}
if (widgetState.config.hideFooter) {
document.body.classList.add('widget-hide-footer');
}
}
Event система
Widget Events
export interface WidgetEventHandlers {
onReady?: (data?: any) => void;
onDataChange?: (data: any) => void;
onError?: (error: any) => void;
onNavigate?: (path: string) => void;
onResize?: (dimensions: { width: number; height: number }) => void;
onDestroy?: () => void;
}
Использование событий
const { emitEvent, eventHistory } = useWidgetEvents({
onReady: (data) => {
console.log('Widget ready:', data);
},
onDataChange: (data) => {
console.log('Data changed:', data);
},
onError: (error) => {
console.error('Widget error:', error);
},
});
// Отправка пользовательского события
emitEvent('custom-event', { payload: 'data' });
Global Error Handling
function setupGlobalErrorHandlers() {
if (!isClient) return;
window.addEventListener('error', handleJSError);
window.addEventListener('unhandledrejection', handleUnhandledRejection);
}
Примеры использования
1. Создание iframe виджета
// На внешнем сайте
const iframe = document.createElement('iframe');
iframe.src =
'https://your-app.com?widget=true&widget_config=' +
encodeURIComponent(
JSON.stringify({
theme: 'light',
hideHeader: true,
allowedOrigins: [window.location.origin],
}),
);
iframe.style.width = '100%';
iframe.style.height = '600px';
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms');
document.getElementById('widget-container').appendChild(iframe);
2. Использование в Vue компоненте
<template>
<div v-if="isWidget" class="widget-content">
<p>Widget режим активен</p>
<button @click="sendDataToParent">Отправить данные</button>
</div>
</template>
<script setup>
import { useWidgetBridge } from 'src/shared/lib/widget';
const { isWidget, isReady, sendData } = useWidgetBridge();
function sendDataToParent() {
if (isReady.value) {
sendData({ message: 'Hello from widget!' });
}
}
</script>
3. Навигация в widget
// В router guard
router.beforeEach((to, from, next) => {
if (widgetState.isWidget.value) {
// Отправляем информацию о навигации в родительское окно
sendNavigation(to.fullPath);
}
next();
});
Troubleshooting
Распространенные проблемы
1. "window is not defined" в SSR
Причина: Обращение к browser API на сервере
Решение: Добавить проверку isClient
if (typeof window !== 'undefined') {
// код для браузера
}
2. Widget не определяется
Причина: Неправильные URL параметры или iframe настройки
Диагностика:
console.log('Parent window:', window.parent !== window);
console.log('URL params:', new URLSearchParams(window.location.search));
console.log('Widget state:', widgetModeInstance.getState());
3. PostMessage не работает
Причина: Проблемы с origin или структурой сообщения
Диагностика:
// В PostMessageBridge
private handleMessage(event: MessageEvent) {
console.log('Message received:', {
origin: event.origin,
data: event.data,
allowed: isOriginAllowed(event.origin, this.config.allowedOrigins)
});
}
4. Layout не переключается
Причина: Проблемы с DynamicLayoutWrapper
Проверка:
// В browser console
console.log(
'Is widget mode:',
typeof window !== 'undefined' &&
(window.parent !== window ||
new URLSearchParams(window.location.search).get('widget') === 'true'),
);
5. События не доходят
Причина: Неправильная инициализация event системы
Решение: Проверить последовательность инициализации
// В boot файле
if (widgetState.isWidget.value) {
widgetEventManager.initialize(); // Должно быть до router setup
}
Debug режим
Для отладки можно включить расширенное логирование:
// В widget конфигурации
const config = {
...DEFAULT_WIDGET_CONFIG,
debug: true,
};
// В PostMessageBridge
if (config.debug) {
console.log('[Widget Debug]', message);
}
Performance мониторинг
// Отслеживание производительности
const performanceObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.name.includes('widget')) {
console.log('Widget performance:', entry);
}
});
});
performanceObserver.observe({ entryTypes: ['measure', 'navigation'] });
Заключение
Widget система представляет собой надежное и масштабируемое решение для встраивания Quasar приложений. Ключевые преимущества:
- SSR совместимость - работает в любом режиме рендеринга
- Автоматическое определение - не требует ручной настройки
- Безопасность - валидация origin и сообщений
- Производительность - lazy инициализация и оптимизация
- Гибкость - настраиваемые темы и поведение
- Надежность - обработка ошибок и fallback сценарии
Система готова к использованию в production окружении и может быть легко интегрирована в любые веб-сайты.