Skip to content

iframe 优雅通讯

Posted on:2023年6月5日 at 18:54

最近开发了个项目,基座是 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 => {
    // 也可以收听到基座发来的消息辣
  });
});