@astral/editor-integration
FrameExchange
Общая информация.
Набор сервисов предназначен для физической развязки общения двух сервисов по схеме клиент-сервер.
Самый главный кейс использования - это обмен данными с сервисом другого приложения через iframe.
В основе работы сервиса лежит метод window.postMessage,
и соответственно используются стандартные механизмы браузера для доставки сообщений из одного приложения в другое.
Далее мы подробно рассмотрим варианты использования сервиса. Библиотека предназначена для использования с TypeScript.
Основные термины.
В данном разделе рассматриваются основные используемые термины данного модуля.
Сервисы обеспечивающие связь.
- Сервер (FrameServer) - сервис обеспечивающий логику вызова методов в серверных обработчиках.
- Клиент (FrameClient) - клиент выполняющий вызов методов на стороне сервера, поставляет клиентские сервисы основываясь на каком либо классе/шаблоне.
Рабочие сервисы.
- Шаблон (Template) - базовый абстрактный класс, который используется в качестве шаблона для сервера и клиента (по сути это общие контракты).
- Серверный обработчик (ServerService) - любой сервис реализующий логику обработки запросов/событий (может базироваться как на шаблоне, так и не иметь базы).
- Клиентский обработчик (ClientService) - сервис созданный с помощью FrameClient и обеспечивающий вызовы на стороне сервера (в качестве базы может быть как шаблон, так и сервис с реализацией).
Рабочие элементы.
- Запрос (Request) - метод, который всегда возвращает Promise (то есть является асинхронным). Основное назначение - выполнять запросы с ожиданием результата.
- Событие (Event) - метод, который может быть как синхронным, так и асинхронным. Основное назначение - выполнять вызовы сервиса по принципу "пули вылетели".
Декораторы:
- @frameExchangeService - декоратор предназначен для метки сервисов, которые будут использоваться для обмена (обязательно требуется указать имя обменника, оно должно совпадать с обменником на противоположной стороне).
- @frameExchangeRequest - декоратор предназначен для метки методов с ожиданием результата, данный метод будет вызывать такой же метод на противоположной стороне и дожидаться ответа от него (название можно менять, названия должны совпадать на стороне клиента и сервера).
- @frameExchangeEvent - декоратор предназначен для метки методов без ожидания результата, данный метод будет вызывать такой же метод на противоположной стороне, но ждать ответа не будет (название можно менять, названия должны совпадать на стороне клиента и сервера).
Правила именования
Стоит придерживаться строго правила именования сервисов.
Шаблон для именования выглядит следующим образом СторонаДействиеТип где:
- Сторона - Editor для реализации на стороне редактора или Host соответсвенно для стороны интегратора.
- Действие - Print, Send, SideBar и т.д. (зона ответственности)
- Тип - один из трех возможных типов класса.
- Contract - для базовых классов, которые являются основной.
- Client - для классов с реализацией запросов.
- Handler - для классов с реализацией ответов/обработки.
Пример, нам надо реализовать сервис печати, реализацию которого пишет интегратор:
- HostPrintContract - базовый абстрактный класс с контрактами.
- HostPrintHandler - класс с реализацией на стороне хоста.
- HostPrintClient - класс клиент на стороне редактора (фактически он создается автоматом).
Еще один пример, получение метаинформации на стороне редактора:
- EditorDocumentsContract - базовый абстрактный класс с контрактами.
- EditorDocumentsHandler - класс с реализацией на стороне редактора.
- EditorDocumentsClient - класс клиент на строне хоста (фактически он создается автоматом).
Использование в проекте.
В данном разделе рассматриваются различные варианты использования.
Использование с шаблонами (типовой сценарий).
Как правило, для более удобного использования библиотеки в нескольких проектах стоит выделить шаблоны (Template).
Шаблоны представляют собой обычные абстрактные классы и нужны главным образом для распространения общих контрактов.
Контракт может выглядеть следующим образом:
@frameExchangeService('HostSendV1')
export abstract class HostSendContract {
@frameExchangeRequest()
public abstract checkAllowSendRequest: (name: string) => Promise<boolean>;
@frameExchangeEvent()
public abstract sendReportSuccessEvent: (name: string) => void;
}
Обратите внимание, что шаблон содержит все необходимые декораторы, в такой ситуации устанавливать их повторно в сервисах обработки нет необходимости.
Далее необходимо создать сервис, который будет заниматься реальной обработкой входящих запросов.
Выглядеть он может например вот так:
export class HostSendHandler extends HostSendContract {
checkAllowSendRequest = (name: string): Promise<boolean> => {
console.log(`Проверили сервис ${name} и все хорошо.`);
return Promise.resolve(true);
};
sendReportSuccessEvent = (name: string): void => {
console.log(`Оповестили пользователя, что ${name} успешно отправлен.`);
};
}
Обратите внимание, что сервис может использовать любые механизмы и вспомогательные методы, вызываться будут только те методы, что помечены атрибутами @frameExchangeRequest и @frameExchangeEvent.
Теперь нам необходимо всего лишь подключить все это дело. Сразу стоит оговориться, что подписываться надо на один и тот же элемент, со стороны iframe это window, а со стороны хоста iframe.
Предположим, что в нашей ситуации сервер находится на стороне хоста, а клиент на стороне приложения в iframe.
На стороне сервера будет следующий код:
const iframe = window.frames[0];
const hostSendHandler = new HostSendHandler();
const frameServer = new FrameServer('server', [hostSendHandler], iframe);
frameServer.closeConnection();
Закрытие соединения выполняет отписку от событий "message", хотя в случае с iframe вероятно новых сообщений не прилетит после unmount.
Так или иначе вызов этой функции на усмотрение разработчика, например появилась необходимость прекратить любой обмен.
На стороне клиента, который у нас обернут в iframe код может выглядеть следующим образом:
const frameClient = new FrameClient('client', window);
const hostSendClient =
frameClient.createClient<HostSendContract>(HostSendContract);
const isAllow = await hostSendClient.checkAllowSendRequest('someName');
if (isAllow) {
hostSendClient.sendReportSuccessEvent('someName');
}
frameClient.closeConnection();
В принципе это все, что необходимо для использования данных сервисов.
Использование без шаблона.
В принципе особой разницы нет, все сценарии использования одинаковы, но если требуется физическая развязка в рамках одной системы (без импортов шаблонов),
то можно просто создать два отдельных класса, а после просто создать соответствующие клиент и сервер.
Код может выглядеть так:
@frameExchangeService('HostSendV1')
export abstract class HostSendClient {
@frameExchangeRequest()
public abstract checkAllowSendRequest: (name: string) => Promise<boolean>;
@frameExchangeEvent()
public abstract sendReportSuccessEvent: (name: string) => void;
}
@frameExchangeService('HostSendV1')
export class HostSendHandler {
@frameExchangeRequest()
checkAllowSendRequest = (name: string): Promise<boolean> => {
console.log(`Проверили сервис ${name} и все хорошо.`);
return Promise.resolve(true);
};
@frameExchangeEvent()
sendReportSuccessEvent = (name: string): void => {
console.log(`Оповестили пользователя, что ${name} успешно отправлен.`);
};
}
const hostSendHandler = new HostSendHandler();
const frameServer = new FrameServer('server', [hostSendHandler], window);
const frameClient = new FrameClient('frameClient', window);
const hostSendClient = frameClient.createClient<HostSendClient>(HostSendClient);
Также при желании можно использовать различные названия для методов, декоратор позволяет дать другое имя, которое будет использоваться при обмене.
В конечном счете важно только, чтобы совпадали контракты (названия методов и сервиса в декораторах, а также наборы аргументов).
В случае, если не совпадут названия сервисов или методов, то вызов просто не будет произведен, а методы request типа, будут падать по таймауту (который по умолчанию 30 секунд).
В случае, если не совпадут аргументы, то на вход в метод просто прилетят не те аргументы, которые ожидаются, проверки соответствия контрактов нет, так JS подобное не поддерживает.
Специальных декораторов для проверки контрактов также нет, так как это по сути далеко от типового использования данной библиотеки.
Работа через хуки в функциональных компонентах.
Пример работы через хуки:
import {
ExternalContract1,
ExternalContract2,
ExternalContract3,
useFrameClient,
useFrameServer,
INTEGRATOR_FRAME_EXCHANGE_ID
} from "@astral/editor-integration";
import {
ImplementedHandler1,
ImplementedHandler2,
ImplementedHandler3,
} from "editor/services/frame";
export const ExampleWrapper = ({ frameSrc }: ExampleWrapperProps) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [frameClient, forceClientInit] = useFrameClient(
INTEGRATOR_FRAME_EXCHANGE_ID,
(createClient): HostFrameClient => ({
externalClient1: createClient(ExternalContract1),
externalClient2: createClient(ExternalContract2),
externalClient3: createClient(ExternalContract3),
}),
() => iframeRef.current!.contentWindow!
);
const [frameServer, forceServerInit] = useFrameServer(
INTEGRATOR_FRAME_EXCHANGE_ID,
(): HostFrameServer => {
const implementedHandler1 = new ImplementedHandler1();
const implementedHandler2 = new ImplementedHandler2();
const implementedHandler3 = new ImplementedHandler3();
return {
implementedHandler1,
implementedHandler2,
implementedHandler3,
};
},
() => iframeRef.current!.contentWindow!
);
const forceInit = () => {
forceClientInit();
forceServerInit();
};
return <iframe src={frameSrc} iframeRef={iframeRef} onLoad={forceInit} />;
};