最近开发了个项目,基座是 VsCode 插件,通过 iframe 集成了一个 Vue3 的子应用,子应用需要很频繁的与基座通讯。
我们可以通过 parent.postMessage
来向基座传递消息,通过 window.addEventListener('message', () => {})
来监听来自基座的消息。
但是在 vue3 的代码中写大量这样的代码就很不美观,我们可以将其封装成Promise
风格, 使用mitt
库来完成发布订阅。
我们在 src 下新建 message 文件夹, 并pnpm add mitt
开始吧
先定义 ts 类型
// ***********
// src/message/types.ts
// ***********
// 定义枚举 来存放与基座约定的eventId
export enum ON_MESSAGE_MAP {
API = "TransformApis",
CONFIG = "GetConfigToJson",
}
export enum EMIT_MESSAGE_MAP {
API = "TransformApis",
CODE = "GeneratorCode",
}
export type EMIT_MESSAGE_MAP_KEYS = keyof typeof EMIT_MESSAGE_MAP;
export type ON_MESSAGE_MAP_KEYS = keyof typeof ON_MESSAGE_MAP;
type MapTo<T extends string, U extends Record<T, unknown>> = {
[K in T]: U[K];
};
// 使用自定义Ts工具函数 将枚举key转为mitt需要使用到的类型
export type Emitter = MapTo<
ON_MESSAGE_MAP_KEYS,
{
API: VarTemplate.ContentOriginal[];
CONFIG: VarOneCodeConfig.OneCodeConfig;
}
>;
封装 window message 方法
// ***********
// src/message/on.ts
// ***********
import mitt from "mitt";
import { ON_MESSAGE_MAP_KEYS, ON_MESSAGE_MAP } from "./types";
import type { Emitter } from "./types";
export const windowMessage = mitt<Emitter>();
const getMessageKey = (key: string) => {
let temp: ON_MESSAGE_MAP_KEYS | null = null;
for (const [k, v] of Object.entries(ON_MESSAGE_MAP)) {
if (v === key) {
temp = <ON_MESSAGE_MAP_KEYS>k;
break;
}
}
return temp;
};
// 格式化基座传递过来的数据 可以在这里做错误处理
const payloadParser: {
[P in ON_MESSAGE_MAP_KEYS]: (data: any) => Emitter[P];
} = {
API: data => {
if (!Array.isArray(data)) {
throw new Error("无效的 API message");
}
return data.map<VarTemplate.ContentOriginal>((v: any) => {
return {
content:
typeof v.content === "string" ? v.content : JSON.stringify(v.content),
type: v.type as VarTemplate.FromType,
};
});
},
CONFIG: () => {
return {
dataSource: [],
apiPackageResolver: {},
};
},
};
export const setupWindowMessage = () => {
window.addEventListener("message", (event: MessageEvent) => {
if (!event.data) return;
const { data, eventId } = event.data;
if (!eventId) return;
const emitterKey = getMessageKey(eventId);
if (!emitterKey) return;
windowMessage.emit(emitterKey, payloadParser[emitterKey](data));
});
};
封装向基座发消息的方法
// ***********
// src/message/emit.ts
// ***********
import {
EMIT_MESSAGE_MAP,
EMIT_MESSAGE_MAP_KEYS,
Emitter,
ON_MESSAGE_MAP_KEYS,
} from "./types";
import { windowMessage } from "./on";
// 封装 postMessage
export const postMessage = <K extends EMIT_MESSAGE_MAP_KEYS, V = any>(
key: K,
data?: V
) => {
parent.postMessage(
{
eventId: EMIT_MESSAGE_MAP[key],
data: data || {},
},
"*"
);
};
// 我们将 emit与on封装在一起 并利用setTimeout做伪超时
const awaitMessage = <
K extends EMIT_MESSAGE_MAP_KEYS,
X extends ON_MESSAGE_MAP_KEYS,
V = any
>(
key: K,
onKey: X,
data?: V,
delay = 3000
) => {
return new Promise<Emitter[X]>((resolve, reject) => {
postMessage(key, data);
const timer = setTimeout(() => {
reject(new Error("timeout"));
windowMessage.off(onKey);
}, delay);
windowMessage.on(onKey, data => {
resolve(data);
clearTimeout(timer);
windowMessage.off(onKey);
});
});
};
// 子 -> 父组件 然后父 立刻响应 子
export const pushApiMessage = async () => {
return await awaitMessage("API", "API", undefined, 1000 * 60 * 3);
};
// 子 -> 父 单向通讯
export const pushCodeMessage = (data: unknown) => {
postMessage("CODE", data);
};
最后在 index.ts 中导出
// ***********
// src/message/index.ts
// ***********
export * from "./types";
export * from "./on";
export * from "./emit";
Usage
在 main.ts 中开始全局监听
// ***********
// src/main.ts
// ***********
import { createApp } from "vue";
import { setupWindowMessage } from "./message";
// ...
const app = createApp(App);
setupWindowMessage();
// ...
app.mount("#app");
在需要的地方
import { onMounted } from "vue";
import { pushApiMessage, windowMessage } from "@/message";
onMounted(async () => {
const data = await pushApiMessage();
console.log(data);
// 就可以立即发送立即拿到基座返回的消息辣
// 或者
windowMessage.on("API", event => {
// 也可以收听到基座发来的消息辣
});
});