iframe 优雅通讯

发布时间 2023-06-05 19:03:29作者: demo_you

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