Comunicación eficiente entre iframes en aplicaciones Vue 3 integradas con VSCode

En el desarrollo de una extensión para VSCode, se integra una sub-aplicación Vue 3 mediante iframes. Para gestionar una comunicación frecuente y limpia entre ambos, se encapsula la lógica de mensajes utilizando el patrón Promise y la biblioteca mitt para el sistema de publicación-suscripción.

La comunicación se basa en parent.postMessage para enviar mensajes a la base y window.addEventListener('message', ...) para recibirlos. Para mejorar la estructura en el código Vue 3, se crea una capa de abstracción.

Definición de tipos TypeScript

// src/messaging/types.ts

export enum INCOMING_EVENTS {
  APIS = 'FetchApis',
  SETTINGS = 'LoadSettings'
}

export enum OUTGOING_EVENTS {
  APIS = 'FetchApis',
  SNIPPET = 'GenerateSnippet'
}

export type INCOMING_EVENT_KEYS = keyof typeof INCOMING_EVENTS;
export type OUTGOING_EVENT_KEYS = keyof typeof OUTGOING_EVENTS;

type EventMap<K extends string, V extends Record<K, unknown>> = {
    [P in K]: V[P]
};

export type EventBusType = EventMap<
  INCOMING_EVENT_KEYS,
  {
    APIS: DataModel.Content[]
    SETTINGS: ConfigModel.Configuration
  }
>;

Configuración del listener de mensajes

// src/messaging/receiver.ts

import mitt from 'mitt'
import { INCOMING_EVENT_KEYS, INCOMING_EVENTS } from './types'
import type { EventBusType } from './types'

export const messageBus = mitt<EventBusType>()

const resolveEventKey = (identifier: string): INCOMING_EVENT_KEYS | null => {
  for (const [key, value] of Object.entries(INCOMING_EVENTS)) {
    if (value === identifier) return key as INCOMING_EVENT_KEYS
  }
  return null
}

const payloadProcessors: {
  [K in INCOMING_EVENT_KEYS]: (raw: any) => EventBusType[K]
} = {
  APIS: (raw) => {
    if (!Array.isArray(raw)) throw new Error('Formato de APIs inválido')
    return raw.map<DataModel.Content>((item: any) => ({
      body: typeof item.body === 'string' ? item.body : JSON.stringify(item.body),
      kind: item.kind as DataModel.EntryType
    }))
  },
  SETTINGS: () => ({
    sources: [],
    resolver: {}
  })
}

export const initializeMessageReceiver = () => {
  window.addEventListener('message', (event: MessageEvent) => {
    if (!event.data) return
    const { payload, eventId } = event.data
    if (!eventId) return
    const busKey = resolveEventKey(eventId)
    if (!busKey) return
    messageBus.emit(busKey, payloadProcessors[busKey](payload))
  })
}

Métodos para enviar mensajes

// src/messaging/sender.ts

import {
  OUTGOING_EVENTS,
  OUTGOING_EVENT_KEYS,
  EventBusType,
  INCOMING_EVENT_KEYS
} from './types'
import { messageBus } from './receiver'

export const sendToParent = <K extends OUTGOING_EVENT_KEYS, T = any>(
  eventKey: K,
  payload?: T
) => {
  parent.postMessage(
    { eventId: OUTGOING_EVENTS[eventKey], payload: payload || {} },
    '*'
  )
}

const awaitResponse = <
  SendKey extends OUTGOING_EVENT_KEYS,
  ListenKey extends INCOMING_EVENT_KEYS,
  T = any
>(
  sendEvent: SendKey,
  listenEvent: ListenKey,
  data?: T,
  timeoutMs = 3000
): Promise<EventBusType[ListenKey]> => {
  return new Promise((resolve, reject) => {
    sendToParent(sendEvent, data)
    const timer = setTimeout(() => {
      reject(new Error('Tiempo de espera excedido'))
      messageBus.off(listenEvent)
    }, timeoutMs)
    messageBus.on(listenEvent, (responseData) => {
      resolve(responseData)
      clearTimeout(timer)
      messageBus.off(listenEvent)
    })
  })
}

export const fetchApisWithResponse = async () => {
  return awaitResponse('APIS', 'APIS', undefined, 180000)
}

export const emitCodeSnippet = (data: unknown) => {
  sendToParent('SNIPPET', data)
}

Exportación centralizada

// src/messaging/index.ts

export * from './types'
export * from './receiver'
export * from './sender'

Entegración en la aplicación

// src/main.ts

import { createApp } from 'vue'
import { initializeMessageReceiver } from './messaging'

const app = createApp(App)
initializeMessageReceiver()
app.mount('#app')
// Ejemplo de uso en un componente

import { onMounted } from 'vue'
import { fetchApisWithResponse, messageBus } from '@/messaging'

onMounted(async () => {
  try {
    const apiData = await fetchApisWithResponse()
    console.log('Datos de APIs recibidos:', apiData)
  } catch (error) {
    console.error('Error al obtener APIs:', error)
  }

  // Alternativamente, suscribirse a eventos específicos
  messageBus.on('APIS', (event) => {
    // Manejar evento de APIs entrante
  })
})

Etiquetas: iframe Vue 3 VSCode postMessage mitt

Publicado el 6-15 04:07