即上一次分享的前端基于 postMessage 跨标签页通信以后业务又有了变化,现在业务要求弹窗到副屏,但是搜了很多资料以目前的前端水平可能还无法实现。于是就提出弹窗只弹一个,然后由客户手动拉弹窗到副屏,当点开不同的详情页以后,弹窗内容跟着发生变化。技术上原本的基于 window.postMessage 无法实现这样的效果(postMessage适用于1对1,这次是多对1),于是借助 localstorage 重新封装了一个,实现接口与之前的差不多:
/* eslint-disable @typescript-eslint/no-explicit-any */
export type ITypeToData = Record<any, any>;
export interface IChanelPoint<TypeToData extends ITypeToData> {
postMessage<T extends keyof TypeToData>(type: T, data: TypeToData[T]): void;
addMessageListener<Type extends keyof TypeToData>(type: Type, listener: (data: TypeToData[Type]) => void): () => void;
destroy(): void;
// focusTarget(): void;
}
export enum EChanelType {
Id = 'Id',
List = 'List'
}
export interface IChanelTypeToData {
[EChanelType.Id]: number;
[EChanelType.List]: {
id: number;
data: number[];
};
}
import { isDev } from '@utils/index';
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { fromEvent } from 'rxjs';
/* eslint-disable @typescript-eslint/no-unused-vars */
import { IChanelPoint, ITypeToData } from './types';
interface IChanelPointOptions {
/**
* @default false
*/
rePostOnWindowFocus?: boolean;
}
export class ChanelPoint<TypeToData extends ITypeToData> implements IChanelPoint<TypeToData> {
private typeToListeners = new Map<keyof TypeToData, Set<(data: any) => void>>();
destroy: () => void;
private history: ITypeToData;
constructor(options: IChanelPointOptions = {}) {
const { rePostOnWindowFocus = false } = options;
const subs = fromEvent<StorageEvent>(window, 'storage').subscribe((ev) => {
const { key: normalizedType, newValue: normalizedData } = ev;
const type = this.deNormalizeType(String(normalizedType));
const data = this.deNormalizeData(String(normalizedData));
if (isDev()) {
console.log('<--- 接收', type, data);
}
const listeners = this.typeToListeners.get(type);
if (listeners) {
listeners.forEach((x) => x(data));
}
if (!listeners) {
if (isDev()) {
console.log('没有处理函数', type);
}
}
});
this.destroy = () => subs.unsubscribe();
this.history = {};
if (rePostOnWindowFocus) {
window.addEventListener('focus', this.rePost);
this.destroy = compose(this.destroy, () => {
window.removeEventListener('focus', this.rePost);
});
}
}
private rePost = () => {
Object.entries(this.history).forEach(([type, data]) => {
this.postMessage(type, data);
});
};
postMessage<T extends keyof TypeToData>(type: T, data: TypeToData[T]): void {
if (isDev()) {
console.log('---> 发送', type, data);
}
this.history[type] = data;
localStorage.setItem(this.normalizeType(type), this.normalizeData(data));
}
addMessageListener<Type extends keyof TypeToData>(type: Type, listener: (data: TypeToData[Type]) => void): () => void {
let listeners = this.typeToListeners.get(type);
if (!listeners) {
this.typeToListeners.set(type, (listeners = new Set()));
}
listeners.add(listener);
// 如果已经存有数据,那么读取已经存在的数据
// const existingJsonString = localStorage.getItem(this.#normalizeType(type));
// if (existingJsonString) {
// Promise.resolve().then(() =>
// listener(this.#deNormalizeData(existingJsonString))
// );
// }
return () => {
this.typeToListeners.get(type)?.delete(listener);
};
}
private normalizeType<T extends keyof TypeToData>(type: T) {
return `${ChanelPoint.name}_${String(type)}`;
}
private deNormalizeType(normalizedType: string) {
return normalizedType.replace(`${ChanelPoint.name}_`, '');
}
private normalizeData<T>(data: T): string {
return JSON.stringify({
key: Math.random().toString(36).slice(0, 10),
data
});
}
private deNormalizeData(data: string) {
return JSON.parse(data)?.data ?? null;
}
}
enum InternalType {
HelloChild = 'HelloChild',
HelloOpener = 'HelloOpener'
}
export class OpenerChanel<TypeToData extends ITypeToData> extends ChanelPoint<TypeToData> {
constructor() {
super({
rePostOnWindowFocus: true
});
}
async connect(timeout: number) {
return new Promise((resolve, reject) => {
// @ts-ignore
this.postMessage(InternalType.HelloChild, null);
const unListen = this.addMessageListener(InternalType.HelloOpener, () => {
unListen();
resolve(null);
});
setTimeout(() => reject('连接超时'), timeout);
});
}
}
export class ChildChanel<TypeToData extends ITypeToData> extends ChanelPoint<TypeToData> {
constructor() {
super();
// @ts-ignore
this.postMessage(InternalType.HelloOpener, null);
this.addMessageListener(InternalType.HelloChild, () => {
// @ts-ignore
this.postMessage(InternalType.HelloOpener, null);
});
}
}
function compose(...fns: ((...args: any[]) => any)[]) {
if (fns.length <= 1) {
return fns[0];
}
return fns.reduce(
(a, b) =>
(...args: any[]) =>
a(b(...args))
);
}