前端基于 postMessage 的跨标签页通信
需求:
- 页面1打开页面2,页面2打开页面3,页面2和页面3通信。
- 页面2和页面3只要有1个关闭那么也关闭另一个
- 在页面2支持激活页面3窗口
- 由于页面2和页面3是在同域下前端基于 postMessage 的跨标签页通信,所以不用考虑跨域情况
选型:
- postMessage:用于一对一的窗口通信,优点:可实现父页面激活子页面窗口
- Broadcast API:用于一对多的窗口通信
/**
* 打开与被打开标签页之间的通信封装,使用 window.onmessage、window.postMessage
*/
import { isDev } from '@utils/index';
import { Subscription, fromEvent } from 'rxjs';
import { EChanelMessageType } from '.';
import { isWindowReloaded } from '@utils/window';
enum EInternalMessageType {
RequestSource = 'RequestSource',
Source = 'Source'
}
type ITypeToData = Record<ISafeAny, ISafeAny>;
export class ChanelPoint<TypeToData extends ITypeToData> {
public readonly target: ChanelPoint<TypeToData> | null = null;
private readonly _typeToListeners = new Map<keyof TypeToData, Set<(data: ISafeAny, e: MessageEvent) => void>>();
private _subs: Subscription;
protected _targetWindow: Window | null = null;
protected onWindowRefresh?: Function;
protected onWindowClose?: Function;
private dataStackWaitingPost: ISafeAny[] = [];
constructor(targetWindow: Window | null) {
this._subs = fromEvent(window, 'message').subscribe((ev) => {
const {
data: { type, data }
} = ev as MessageEvent;
if (isDev()) {
console.log(`收到消息,类型:${EChanelMessageType[type] ?? type}, 数据:`, data);
}
this._typeToListeners.get(type)?.forEach((x) => x(data, ev as MessageEvent));
});
let beforeTime = 0;
window.addEventListener('beforeunload', () => {
beforeTime = new Date().getTime();
});
window.addEventListener('unload', () => {
const interval = new Date().getTime() - beforeTime;
if (interval < 5) {
// close
this.onWindowClose?.();
} else {
this.onWindowRefresh?.();
}
});
this.addMessageListener(EInternalMessageType.RequestSource, () => {
setTimeout(() => {
this.postMessage(EInternalMessageType.Source, null as ISafeAny);
}, 200);
});
this.addMessageListener(EInternalMessageType.Source, (data: ISafeAny, ev) => {
if (isDev()) {
console.log('收到 Source', ev.source);
}
this._targetWindow = ev.source as Window;
this.dataStackWaitingPost.forEach((x) => this._targetWindow!.postMessage(x));
this.dataStackWaitingPost.length = 0;
});
if (targetWindow) {
this._targetWindow = targetWindow;
} else {
this.postMessage(EInternalMessageType.RequestSource, null as ISafeAny);
}
}
/**
* 向对方窗口发送消息
* @param type
* @param data
*/
public postMessage<T extends keyof TypeToData>(type: T | EInternalMessageType, data: TypeToData[T]) {
if (!this._targetWindow) {
// 进来的时候有可能还没获取对方 window 的引用,先将要发送的数据存放起来
this.dataStackWaitingPost.push({ type, data });
} else {
this._targetWindow.postMessage({ type, data });
}
}
public addMessageListener<Type extends keyof TypeToData>(type: Type, listener: (data: TypeToData[Type], e: MessageEvent) => void) {
let listeners = this._typeToListeners.get(type);
if (!listeners) {
this._typeToListeners.set(type, (listeners = new Set()));
}
listeners.add(listener);
return () => {
this._typeToListeners.get(type)?.delete(listener);
};
}
public destroy() {
return this._subs.unsubscribe();
}
/**
* 激活对方窗口(有些浏览器不行,有的浏览器父窗口能激活子窗口),不能保证成功
*/
public focusTarget() {
this._targetWindow?.focus();
return this;
}
}
export class ChanelOpener<TypeToData extends ITypeToData> extends ChanelPoint<TypeToData> {
constructor(childUrl: string) {
let childWindow: Window | null = null;
if (!isWindowReloaded()) {
// 如果不是刷新后的页面那么直接打开就可以了,如果是刷新的话需要发送一段请求子页面的引用消息
childWindow = window.open(childUrl);
if (childWindow === null) {
throw new Error('打开子窗口失败,可能是没有赋予浏览器权限');
}
}
super(childWindow);
this.onWindowClose = () => {
if (!this._targetWindow?.closed) {
this._targetWindow?.close();
}
};
this.onWindowRefresh = () => {
this.postMessage(EInternalMessageType.RequestSource, null as ISafeAny);
};
}
}
export class ChanelChild<TypeToData extends ITypeToData> extends ChanelPoint<TypeToData> {
static instance: ChanelChild<ITypeToData> | null = null;
static get<T extends ITypeToData>() {
if (!ChanelChild.instance) {
ChanelChild.instance = new ChanelChild<T>();
}
return ChanelChild.instance as ChanelChild<T>;
}
private constructor() {
const openerWindow = window.opener;
if (!openerWindow) {
throw new Error('实例化 ChanelChild 失败,找不到父窗口引用');
}
super(openerWindow);
this.onWindowClose = () => {
if (!openerWindow.closed) {
openerWindow.close();
}
};
}
}
/**
* 获取页面导航类型,可以用于判断页面是否是刷新的
* @returns
*/
export function getNavigationType() {
const entries = performance.getEntriesByType('navigation');
if (entries.length > 0) {
const navigationEntry = entries[0] as PerformanceNavigationTiming;
return navigationEntry.type;
}
console.log('无法获取页面导航信息');
return null;
}
/**
* 判断页面是否是刷新的
* @returns
*/
export function isWindowReloaded() {
return getNavigationType() === 'reload';
}