1.3.2 • Published 1 year ago

@astral/editor-integration v1.3.2

Weekly downloads
-
License
-
Repository
-
Last release
1 year ago

@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.

На стороне сервера будет следующий код:

// Создание сервера (например в componentDidMount).
const iframe = window.frames[0];
const hostSendHandler = new HostSendHandler();
const frameServer = new FrameServer('server', [hostSendHandler], iframe);

// Закрытие открытых соединений (например в componentWillUnmount)
frameServer.closeConnection();

Закрытие соединения выполняет отписку от событий "message", хотя в случае с iframe вероятно новых сообщений не прилетит после unmount. Так или иначе вызов этой функции на усмотрение разработчика, например появилась необходимость прекратить любой обмен. На стороне клиента, который у нас обернут в iframe код может выглядеть следующим образом:

// Создание клиента (generic параметр нужен исключительно для типизации).
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,

  // Хуки для подключения обмена через iframe.
  useFrameClient,
  useFrameServer,

  // Общий уникальный ключ для интеграторов (можно вместо него использвоать любой свой).
  // Но он обязательно должен совпадать и для клиента (useFrameClient) и для сервера (useFrameServer), 
  // чтобы они не потребляли сообщения друг друга, так как обмен идет через общее окно.
  INTEGRATOR_FRAME_EXCHANGE_ID
} from "@astral/editor-integration";

import {
  // Собственные реализации предоставляемые второй стороне.
  ImplementedHandler1,
  ImplementedHandler2,
  ImplementedHandler3,
} from "editor/services/frame";

/**
 * Компонент обёртка для рендеринга отчётов редактора.
 * @param frameSrc - Адрес страницы, которая будет вставлена в iframe.
 */
export const ExampleWrapper = ({ frameSrc }: ExampleWrapperProps) => {
    // Инициализация ссылки на iframe через который будет производится обмен.
  const iframeRef = useRef<HTMLIFrameElement>(null);

  // Регистрация клиента.
  const [frameClient, forceClientInit] = useFrameClient(
    INTEGRATOR_FRAME_EXCHANGE_ID, // Уникальный id для стороны обмена (именно стороны обмена, а не сервиса).
    (createClient): HostFrameClient => ({ // Создание клиентов используя предоставленные контракты.
      externalClient1: createClient(ExternalContract1),
      externalClient2: createClient(ExternalContract2),
      externalClient3: createClient(ExternalContract3),
    }),
    () => iframeRef.current!.contentWindow!
  );

  const [frameServer, forceServerInit] = useFrameServer(
    INTEGRATOR_FRAME_EXCHANGE_ID, // Уникальный id для стороны обмена (именно стороны обмена, а не сервиса).
    (): HostFrameServer => { // Создание обработчиков сервера (которые реализуют предоставленные контракты).
      const implementedHandler1 = new ImplementedHandler1();
      const implementedHandler2 = new ImplementedHandler2();
      const implementedHandler3 = new ImplementedHandler3();

      return {
        implementedHandler1,
        implementedHandler2,
        implementedHandler3,
      };
    },
    () => iframeRef.current!.contentWindow!
  );

  // Метод для жесткой инициализации
  // при обновлении компонента и смены src у iframe.
  const forceInit = () => {
    forceClientInit();
    forceServerInit();
  };

  return <iframe src={frameSrc} iframeRef={iframeRef} onLoad={forceInit} />;
};
1.2.0

2 years ago

1.0.2-test.1

2 years ago

1.0.2

2 years ago

1.1.0

2 years ago

1.0.1

2 years ago

1.0.2-test.4

2 years ago

1.0.2-test.3

2 years ago

1.0.2-test.2

2 years ago

1.3.2

1 year ago

1.3.1

1 year ago

1.3.0

2 years ago

1.2.1

2 years ago

1.0.0

2 years ago