Iframe RPC 调用

背景

有个比较老的项目, 需要跨iframe通讯. 主要原因是后端不想对所有接口开跨域, 而有部分通用信息需要保存在后端接口的统一域名下的localStoragecookie中, 在无法去掉iframe的情况下, 只能优化iframe的调用过程了. 旧的基本都是直接使用document.createElement('iframe')注入, 然后postMessage返回结果, 对于ts来说开发十分不友好, 基本全是any类型

开发

page

这里实现接受iframe传回的调用请求, 并将本地方法执行完成之后通过postMessage发送回iframe, 并提供一个调用iframe里方法的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Page {
private init() {
window.addEventListener('message', this.handleMessageEvent, false);
}
private async handleMessageEvent(event) {
const handler = {
callPageFn: async (data) => {
const iframeId = data.iframeId;
if (!iframeId) return;
const iframe = this.iframeData[iframeId];
if (!iframe) return;
if (data.fnId && localPageFunction.includes(data.fn) && iframe.$iframe && iframe.$iframe.contentWindow) {
const result = await this.localFn[data.fn](data.params);
iframe.$iframe.contentWindow.postMessage(
JSON.stringify({ type: 'callPageFnCallback', fnId: data.fnId, result: result }),
iframe.$iframe.getAttribute('src'),
);
}
},
};
try {
const data: { type: typeof MessageType[number]; iframeId: string; [key: string]: any } = JSON.parse(event.data);
handler[data.type](data);
} catch (error) {
return;
}
}
private async transformCorsData<T = { [key: string]: string }>(fn: TransformCorsDataPageOptions['fn'], options: any = {}) {
const $iframe = document.createElement('iframe');
const iframeId = this.generateIframeId();
$iframe.src = ``;
$iframe.onload = () => {
this.iframeData[iframeId].loaded = true;
};
const $page = this.addIframe($iframe, '', 0, 0, false, iframeId, null, true);
return await new Promise<T>((resolve) => {
this.iframeData[iframeId] = {
$dom: $page,
type: 'transformCorsData',
$iframe,
options: { ...options, closeCb: (data: T) => resolve(data) },
loaded: false,
};
});
}
}

iframe

这里实现一次iframe调用的接口, 调用完后去掉这个iframe并将结果通过postMessage传回页面, 同时可以通过Page提供的callPageFn调用页面提供的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class TransformCorsDataPage {
options: TransformCorsDataPageOptions;
private callPageFn: callPageFn;
async init(options: TransformCorsDataPageOptions) {
this.options = { ...options };
this.callPageFn = options.callPageFn;
const result: { [key: string]: any } = await this[options.fn]?.(JSON.parse(decodeURIComponent(options.options)));
postMessageToParent('close', this.options.referrer, result);
}
private async getIframeData(): Promise<IframeData> {
return localStorage.getItem('iframeData');
}
private async getPageData(): Promise<PageData> {
const result = await this.callPageFn<PageData>({ method: 'get-page-data', data: {} });
return result;
}
}

const callPageFn: callPageFn = async <T>(fn, params) => {
const currentFnId = (fnId++).toString();
const fnPromise = new Promise<T>((resolve) => {
fnHandlerMap[currentFnId] = { resolve };
});
postMessageToParent('callPageFn', referrer, { fn, params, fnId: currentFnId });
return await fnPromise;
};

export default new TransformCorsDataPage();

小结

这里只列出了核心的实现, 完成这部分通用逻辑后再想互相调用就可以使用比较简单的async/await调用方法了, 省去了很多开发时间, 而且对ts的支持比较好, 可以根据类型检测避免比较多的低级错误

作者

Mosby

发布于

2020-03-24

许可协议

评论