@avv/ucd-ibus
v1.1.0
Published
Post messaging processor.
Readme
@avv/ucd-ibus
Установка
npm i @avv/ucd-ibusОписание postMessage обработчика (v1.0.0)
Модуль служит задаче обеспечения коммуникации через postMessage web API. Модуль реализует некоторый протокол обмена сообщениями, поэтому предполагается, что он будет установлен на обеих сторонах коммуникации. Однако, это не обязательно, если следовать описанному ниже протоколу.
Помимо этого, модуль реализует некоторую политику безопасности, согласно которой можно настраивать доступ другой стороны к функционалу текущей.
Краткое описание работы с модулем
Для начала работы, необходимо инциализировать message обработку. Для этого необходимо импортировать initialize функцию из пакета и вызвать её с коллекцией функций и опциональным объектом дополнительных параметров:
import { initialize } from 'ucd_post-messaging'
const postMessageActions = {
someMethod: () => {},
groupMethods: {
someMethod2: (payload, sender, additionalParams) => {},
someMethod3: async (payload, sender, additionalParams) => {}
}
}
const additionalParams = {
// некоторые дополнительные данные
}
const {
open,
close,
allow,
forbid,
setPermissions,
getSender
} = initialize(postMessageActions, additionalParams)Это необходимо сделать при запуске всего приложения в самом начале. Возможно, до монитирования в DOM дерево.
Коллекция функций postMessageActions может быть древовидной, таким образом функции можно группировать по смыслу. Функция принимает:
payload- данные из объекта сообщения;sender- имя отправителя;additionalParams- объект дополнительных параметров, переданных при инициализациии.
Функция может быть синхронной и асинхронной. Её результат ложится в payload объект ответного сообщения.
На этапе инициализации приложения, в корневом компоненте, необходимо вызвать метод open, а при размонтировании - метод close:
const RootAppComponent = () => {
// некоторая логика...
useEffect(() => {
// ...
open();
return () => {
// ...
close();
// ...
};
}, []);
// дальнейшая логика
}Методы open и close создают и навешивают/уничтожают соответственно callback обработчик message события на объекте Window.
Для того, чтобы обмен сообщениями стал возможен, необходимо на обеих сторонах вызвать allow метод с параметрами соединения. Для закрытия соединения, можно вызвать forbid метод с именем соединения.
allow('Pparent', 'http://dashboard.host.com/', { 'someMethod': false, 'anotherMethod': true }, null, window.parent);
// ...
forbid('Parent');Пример компонента, создающего iframe:
const Iframe = ({
name,
origin,
url,
permissions = true,
height = 100,
onSourceChange = null,
source = null
}) => {
useEffect(() => {
allow(name, origin, permissions, onSourceChange, source);
return () => {
forbid(name);
}
}, []);
// render <iframe .../> элемента...
};Интерфейс метода allow:
name- имя соединения;origin-protocol://host:portсоединения (желательно знать заранее);permissions- настройки доступа к методам текущего окна (подробно рассмотрено ниже);onSourceChange- callback, с помощью которого можно подписаться на событие смены ссылки на объектWindowсоседнего фрейма.source- ссылка на объектWindowсоседнего фрейма (может быть равным null, в таком случае будет проинициализирован при обработке@INITсообщении).
По объекту permissions стоит заметить, что если нет необходимости регулировать доступ к функциям, и нужно разрешить полный доступ - то можно просто передать true значение вместо объекта. Принцип объекта permissions описан ниже.
Здесь есть рекомендация: инициировать коммуникацию удобнее всего из дочернего фрейма, так как в нём точно можно поймать момент, когда он готов к коммуникации. Поэтому обычно именно дочерний фрейм инициирует коммуникацию и первым запрашивает нужные данные. Однако, если нужно, чтобы первым данные отправил родительский фрейм - это можно осуществить через onSourceChange callback. Он будет вызван, как только прилетит @INIT сообщение и будет получена ссылка на объект Window дочернего фрейма. В этот момент можно начинать отправлять сообщения в дочерний фрейм.
Чтобы отправить сообщение, нужно получить callback sender путём вызова getSender метода:
const sender = getSender('Parent');
// ...
const answer = await sender(new ActionMessage('someMethod', { someParam: 'someValue' }))Callback sender возвращает Promise объект. Он резолвится с ответом, и реджектится с ошибкой. Конструктор объекта сообщения первым параметром принимает имя метода, вторым - объект данных в произвольном формате. Второй параметр опциональный.
Если нам не нужно получать ответа на наш запрос, можно в sender вторым параметром передать false:
sender(new ActionMessage('groupMethods/someMethod2'), false); // ответ не ожидается к обработке.Здесь важно заметить, что для обращения к методу, содержащемуся в глубине коллекции функций принимающей системы, разделителем служит косая черта.
Протокол и общая логика работы
Структура сообщения
Общем случае, структура сообщений реализует интерфейс:
interface IMessage<T> {
messageId: number;
provider: string;
receiver: string;
sender?: string;
message: IMessageBody<T>;
}Поля имеют следующие значения:
messageId- идентификатор сообщения, обязательное поле; идентификатор сообщение генерируется автоматически внутри модуля при отправке сообщения;provider- строковый идентификатор модуля, обязательное поле; поле нужно для того, чтобы была возможность отфильтровывать сообщения от других подсистем, отправляющих и получающих сообщения через postMessage API;receiver- имя получателя, имя получатея назначается отправителем; поле обрабатывается автоматически;sender- имя отправителя, поля может не быть в init сообщении, имя назначается получателем; поле обрабатывается автоматически;message- тело сообщения, его интерфейс разбирается ниже.
interface IMessageBody<T> {
type: MESSAGE_TYPE,
action?: string,
status?: RESPONSE_STATUS,
payload?: IMessagePayload<T>
}
type IMessagePayload<T> = T | IErrorPayload | undefined
interface IErrorPayload {
code: ERROR_CODES,
error: string
}
enum MESSAGE_TYPE {
INIT = '@INIT',
REQUEST = '@REQUEST',
RESPONSE = '@RESPONSE',
END = '@END'
}
enum RESPONSE_STATUS {
SUCCESS = 'SUCCESS',
ERROR = 'ERROR'
}Поля тела сообщения имеют следующие значения:
type- тип сообщения, обязательное поле, поле может содержать одно из четырёх значений изMESSAGE_TYPEсписка:@INIT- начальное инициирующее сообщение, обрабатывается полностью автоматически модулем;@REQUEST- запрос, данных или каких-либо действий;@RESPONSE- ответ, возвращает результат работы метода, к которому обращались;@END- завершающее сообщение, закрывает соединение;
action- поле обязательно для сообщения-запроса (type = @REQUEST), содержит имя метода, который нужно вызвать и вернуть его результат;status- поле обязательно для сообщения-ответа (type = @RESPONSE), может содержать одно из двух значений изRESPONSE_TYPE:SUCCESS- запрос успешно обработан, и возможно содержит ответ вpayload;ERROR- обработка запроса завершилась с ошибкой, вpayloadсодержится объект ошибки;
payload- полезная нагрузка сообщения; может передаваться в запросе и в ответном сообщении; содержит данные при успешной обработке запроса и при возникновении ошибок (интерфейсIErrorPayloadвыше); може быть пустым; в запросе именно объектpayloadпередаётся на вход вызываемого метода в системе-получателе; имеет произвольную структуру; логика обработки содержимого этого поля определяется разработчиком.
Установление соединения
Для того, чтобы два экземпляра модуля могли установить друг с другом связь, необходимо эту коммуникацию разрешить. Разрешение на коммуникацию с выдаётся путём вызова allow метода с передачей:
- имени "собеседника",
origin"собеседника",- настроек прав доступа "собеседника",
- обработчика смены ссылки на объект
Window"собеседника" (если необходимо), - ссылки на объект
Window"собеседника" (если есть).
// в дочернем фрейме разрешена коммуникация с родительским
allow('Parent', '*', true, null, window.parent)// в родительском фрейме разрешена коммуникация с дочерним
allow('Porter', 'https://porter.com/', true, null, null)Интерфейс метода allow будет рассмотрен подробнее ниже.
Имя "собеседника" необходимо передать в обязательном порядке, и оно должно быть уникальным. Крайне желательно, на момент разрешения, точно знать origin "собеседника" - это гарантирует безопасность коммуникации. Однако, если origin неизвестен, то можно передать * - при обработке ответа на @INIT сообщение будет проставлен реальный origin отправителя ответа.
Для отправки @INIT сообщения, один из участников коммуникации должен иметь ссылку на объект Window получателя. Чаще всего, это дочерний iframe. Соответственно, именно дочернему iframe удобнее всего первым отправить @INIT сообщение. Родительский фрейм может изначально не иметь ссылки на объект Window дочернего, но он получит эту ссылку при обработке @INIT запроса и сохранит у себя.
Начальное сообщение:
{
"messageId": 1,
"sender": null,
"receiver": "Parent",
"provider": "POST-MESSAGING-PACKAGE-V1",
"message": {
"type": "@INIT"
}
}Ответ:
{
"messageId": 1,
"sender": "Parent",
"receiver": "Porter",
"provider": "POST-MESSAGING-PACKAGE-V1",
"message": {
"type": "@RESPONSE",
"payload": {
"inited": true
}
}
}С момента получения ответа на @INIT сообщение, возможна полноценная коммуникация в обе стороны.
Обмен сообщениями
Запрос-ответ связываются друг с другом внутри модуля через идентификатор запроса: ответное сообщение имеет тот же идентификатор.
Сообщение-запрос имеет тип type = @REQUEST, содержит имя вызываемого метода в поле action = method/name, опционально может содержать payload с произвольным содержимым. Содержимо поля payload будет передано в функцию method/name в принимающей системе.
Сообщение-ответ имеет тип type = @RESPONSE, содержит статус ответа в поле status, и в поле payload (так же опциональном) произвольные данные. С этими данными резолвится промис, возвращаемый при отправке сообщения. Состояние промиса можно обработать произвольным образом. Содержимое payload рвно тому, что вернется в результате вызова method/name метода.
Пример сообщения-запроса:
{
"messageId": 2,
"sender": "Porter",
"receiver": "Parent",
"provider": "POST-MESSAGING-PACKAGE-V1",
"message": {
"type": "@REQUEST",
"action": "dataRequest/getClient"
}
}Пример сообщения-ответа:
{
"messageId": 2,
"sender": "Parent",
"receiver": "Porter",
"provider": "POST-MESSAGING-PACKAGE-V1",
"message": {
"type": "@RESPONSE",
"status": "SUCCESS",
"payload": {
"base": {
"guid": "sdglk234234j2l34jk234l2j"
}
}
}
}Для отправки сообщения, необходимо вызвать getSender с именем получателя (зарегистрированном при вызове allow) получить ссылку на callback, и вызвать этот его с экземпляром объекта сообщения:
const sender = getSender('Parent');
sender(new ActionMessage('dataRequest/getSomeData', { param: 'value' }))
.then((answer) => {
// логика обработки ответа
});
// или
const answer = await sender(new ActionMessage('dataRequest/getSomeData', { param: 'value' }));Экземпляр объекта сообщения первым параметром получает имя вызываемого метода, вторым - данные для этого метода в произвольном формате. Второй параметр не обязательный.
Callback sender возвращает Promise объект.
Закрытие коммуникации
Коммуникация между двумя окнами закрывается при отправке @END сообщения. При этом на обеих сторонах уничтожается информация друг о друге. Ответ на это сообщение не отправляется.
Пример завершающего сообщения:
{
"messageId": 1224,
"sender": "Porter",
"receiver": "Parent",
"provider": "POST-MESSAGING-PACKAGE-V1",
"message": {
"type": "@END"
}
}Отправить завершающее сообщение можно через sender callback. Вызов forbid метода не отправляет никаких сообщений, но закрывает коммуникацию с другим фреймом молча.
const sender = getSender('Parent');
sender(new EndMessage());
// или
forbid('Parent');Callback sender, как обычно вернет Promise объект. Он зарезолвится как только сообщение физически будет отправлено, а данные соединения будут уничтожены. Происходит это синхронно, поэтому обычно нет надобности подписываться на событие закрытия соединения. Метод forbid ничего не возвращает.
API модуля
Функцияinitialize
Модуль экспортирует одну функцию initialize. Она при вызове принимает объект коллекции функций и объект произвольных дополнительных параметров (опциональный), который впоследствии будет передан в вызываемые при обработке запросов методы из коллекции функций.
Вызов функции инициализирует модуль, но ещё не создаёт обработчик message события.
const {
open,
close,
allow,
forbid,
setPermissions,
getSender
} = initialize(postMessageActions, additionalParams)Возвращает объект со вспомогательными методами.
Метод open
Создает обработчик события message и инициализирует все внутренние механизмы модуля. Повторный вызов метода от одного экземпляра initialize проблем не создаст: повторной регистрации обработчика message и инициализации модуля - не случится. Никаких параметров не принимает. Ничего не возвращает.
Метод close
Уничтожает обработчик message события для данного экземпляра модуля и останавливает все внутренние процессы в модуле, уничтожает все данные. Никаких параметров не принимает и ничего не возвращает.
Метод allow
Метод разрешающий коммуникацию с фреймом с указанными параметрами. Принимает параметры (уже было описано выше):
name- имя соединения;origin-protocol://host:portсоединения;permissions- настройки доступа к методам текущего окна;onSourceChange- callback, с помощью которого можно подписаться на событие смены ссылки на объектWindowсоседнего фрейма.source- ссылка на объектWindowсоседнего фрейма.
Метод ничего не возвращает.
Важно отметить, что желательно чтобы origin и source были известны на момент вызова. Однако не всегда это доступно или удобно. Поэтому здесь допустимы следующие ситуации:
originнеизвестен,sourceизвестенВ таком случае в качестве
originможно передать*- то есть он может быть любым. Фрейм, который находится в таком положении должен первым отправить сообщение@INIT. В ответном сообщении будетoriginотправителя, и именно он будет в итоге записан вместо*.originизвестен,sourceнеизвестенФрейм, находящийся в таком состоянии вынужден ждать
@INITсообщения от фрейма с указаннымorigin. Именно указанныйoriginстановится идентификатором, позволяющий точно понять к какому соединению относится данный запрос. Внутри модуля из запроса извлекаетсяsourceи записывается в параметры данного соединения.оба параметры известны
Формально, они могут друг-другу одновременно отправить
@INITсообщение. В таком случае финальным состоянием соединения будет состояние по итогам обработки второго@INITсообщения. Но лучше принять решение, о том какая сторона инициирует коммуникацию первой.
Ситуация, когда оба параметра неизвестны - недопустима. В таком случае, объективно, нет возможности однозначно и точно понять источник запроса @INIT и корректно проинициализировать параметры соединения. Не говоря уже о том, что неизвестный origin создаёт потенциальные проблемы безопасности.
Метод forbid
Закрывает соединение с указанным именем, принимает только строку. Имя должно совпадать с именем переданным в allow первым параметром. Ничего не возвращает. Соедиение будет закрыто "молча", сообщение @END отправлено не будет.
Метод setPermissions
Метод принимает имя соединения (или origin) и объект (или boolean значение) permissions. Принцип работы настроек доступа описан ниже. Метод ничего не возвращает.
Метод getSender
Метод принимает имя или origin соединения и возвращает функцию отправки сообщения.
Возвращённый callback принимает экземляр объекта сообщения, созданного конструкторами ActionMessage, InitMessage, EndMessage. Вторым параметром принимает boolean значение, по-умолчанию равно true: ожидается ли ответ на запрос. Если ожидается ответ, то будет возвращён Promise объект, который резолвится с ответом на запрос. Если ответ не ожидается, что callback ничего не вернёт.
Конструкторы сообщений
Модуль экспортирует три конструктора сообщений и один конструктор параметров ошибки в payload ответного сообщения:
InitMessageEndMessageActionMessageErrorPayload
Для передачи @INIT или @END сообщений, достаточно в sender callback передать соответствующие экземпляры:
sender(new InitMessage());
sender(new EndMessage());Конструктор ActionMessage принимает два параметра: имя вызываемого метода и payload сообщения. Второй параметр опциональный. Объект payload может быть произвольным (может быть и скалярным значением).
Политика безопасности
Параметр permissions в методе allow может принимать либо булево значение, либо передаваться в виде объекта (плоский HashMap). Во втором случае ключами являются имена методов, значениями статус доступа true|false.
Если передаётся permissions = true, это значает что данному источнику сообщений предоставляется полный доступ ко всем имеющимся методам. Передача permissions = false рвносильно полному запрету всякого доступа. Может использоваться для временной полной блокировки.
Передача объекта позволяет гибко настроить доступ к разным методам. Например, мы имеем коллкцию функций:
const functions = {
client: {
getClient: async () => {},
setClientName: async (payload, sender, additionalParams) => {},
getMoney: async (payload, sender, additionalParams) => {}
},
settings: {
getSettings: () => {},
setParam: (payload, sender, additionalParams) => {}
}
}В какой-то момент мы вынуждены работать с фреймом, которому доступ к данным клиента предоставлять нельзя, но можно выполнять разные операции с настройками системы. Мы можем передать в permissions следующие настройки:
const permissions = {
'client': false,
'settings': true
}Или, фрейм может обращаться ко всем методам, кроме getMoney:
const permissions = {
'client': true,
'client/getMoney': false,
'settings': true
}Или, фрейм может обращаться ко всем методам настроек системы, и может запрашивать данные клиента, но ему запрещено модифицировать данные клиента и выполнять манипуляции с деньгами:
const permissions = {
'client/getClient': true,
'settings': true
}Объект настроек доступа работает по следующим правилам:
- если
permissions = trueразрешается доступ ко всему; - если
permissions = falseко всему доступ запрещён; - если передан объект, следует принципу "все запрещено, если не указано иное":
- если в объекте не указано имя конкретного метода или группы методов - доступ к методу и группе методов запрещён (если объект пустой - доступ ко всему запрещён);
- если в объекте указано имя конкретного метода или группы методов, и в нём содержится
true, то доступ к этому методу или группе методов разрешен, ко всему остальному доступ запрещён; - если в объекте указано имя конкретного метода или группы методов, и в нём содержится
false, то доступ к этому методу или группе методов запрещён, даже если есть имя надгруппы и там содержится значениеtrue;
Дальнейшие доработки и развитие модуля
- адаптировать модуль к postMessage взаимодействию с Worker объектом (один на один);
- предоставить возможность разработчку самому создавать обработчик входящих сообщений (передавать генератор в стиле саги) - может быть это не нужно;
- ввиду схожести механизма WebSocket коммуникации, возможно есть смысл его адаптировать и под такую возможность.
