跳转至

40 · postMessage 协议 TS 类型定义

📌 本文目标:把架构文档 v1.2.2-impl §12.3 的 9 类 postMessage 协议落地为 TypeScript 严格类型,前端智能体可直接复制到 src/types/stage-bridge.ts。Stage HTML 端虽然是 vanilla JS,但字段约定必须与本文一致(任何变更需新建 ADR)。


1. 协议总览

方向 类型 Stage→Shell 8 类 Shell→Stage 2 类
1 stage-ready
2 toolbar-inject
3 stage-status
4 stage-hint
5 select-node
6 tuning-dialog
7 show-toast
8 inspector-inject
9 module-properties-inject
10 subtab
11 toolbar-click

注:v1.2.2-impl §12.3 列为"9 类"是因为把 8 类 Stage→Shell + 2 类 Shell→Stage 合并表述(去重 toolbar 一对:toolbar-inject + toolbar-click 算一对联动),实际类型定义是 11 个。


2. 完整 TS 类型定义

// src/types/stage-bridge.ts

/**
 * Stage 标识
 */
export type StageKey = 'xilink' | 'xitune' | 'xiforge' | 'xitest'

// =============================================================
// Stage → Shell 消息(8 类)
// =============================================================

/** 1. Stage 启动就绪 */
export interface StageReadyMsg {
  type: 'stage-ready'
  stage: StageKey
  version: string             // Stage 实现版本号(如 '1.2.2-impl')
}

/** 2. 注入 StageBar 工具组 */
export interface ToolbarInjectMsg {
  type: 'toolbar-inject'
  stage: StageKey
  buttons: ToolbarButton[]    // 替换式注入(不是追加)
}
export interface ToolbarButton {
  id: string                  // 唯一 id(按 id 反查回调)
  icon: string                // 图标名(如 'play' 'stop' 'save')
  label: string               // 显示文字
  tip?: string                // hover 提示
  group?: string              // 工具组名('文件'/'构建'/'编辑'/'视图')
  disabled?: boolean
}

/** 3. 注入 StatusBar Stage 附加状态槽 */
export interface StageStatusMsg {
  type: 'stage-status'
  stage: StageKey
  html: string                // 直接 v-html 渲染(Stage 责任:避免注入恶意脚本)
}

/** 4. 注入 BottomBar 右侧快捷键提示槽 */
export interface StageHintMsg {
  type: 'stage-hint'
  stage: StageKey
  html: string
}

/** 5. 节点选中事件(Shell 据此更新右 Drawer 标题)*/
export interface SelectNodeMsg {
  type: 'select-node'
  stage: StageKey
  node: string                // 节点 id
  label: string               // Drawer 标题(如 "PEQ.gain.lowShelf")
}

/** 6. 拉起跨 Stage Tuning Dialog 浮窗 */
export interface TuningDialogMsg {
  type: 'tuning-dialog'
  stage: StageKey
  module: string              // 模块名(PEQ / Mixer / GEQ / Limiter ...)
  label: string               // 浮窗标题
}

/** 7. Stage 请求 Shell 显示 toast */
export interface ShowToastMsg {
  type: 'show-toast'
  stage: StageKey
  message: string
  level?: 'info' | 'warn' | 'error' | 'success'
}

/** 8. 注入右 Drawer Inspector 子内容(v3.4 边界修正)*/
export interface InspectorInjectMsg {
  type: 'inspector-inject'
  html: string                // Inspector 内容 HTML
  module: string              // 当前模块名(用于 Drawer 标题副标题)
}

/** 9. 注入 Bottom 属性 tab 子内容(v3.4 边界修正)*/
export interface ModulePropertiesInjectMsg {
  type: 'module-properties-inject'
  html: string                // 属性 tab 内容 HTML
  module: string
}

/** Stage→Shell 消息联合类型 */
export type StageMessage =
  | StageReadyMsg
  | ToolbarInjectMsg
  | StageStatusMsg
  | StageHintMsg
  | SelectNodeMsg
  | TuningDialogMsg
  | ShowToastMsg
  | InspectorInjectMsg
  | ModulePropertiesInjectMsg

// =============================================================
// Shell → Stage 消息(2 类)
// =============================================================

/** 10. Shell 通知 Stage 切 SubTab */
export interface SubtabMsg {
  type: 'subtab'
  subtab: string              // SubTab key(按 Stage 不同含义不同)
}

/** 11. Shell 通知 Stage 顶栏工具按钮被点击 */
export interface ToolbarClickMsg {
  type: 'toolbar-click'
  stage: StageKey
  id: string                  // ToolbarButton.id
}

/** Shell→Stage 消息联合类型 */
export type ShellMessage = SubtabMsg | ToolbarClickMsg

// =============================================================
// 协议版本(用于将来扩展)
// =============================================================

export const PROTOCOL_VERSION = '1.0.0'

/** 类型判别助手 */
export function isStageMessage(msg: any): msg is StageMessage {
  return msg && typeof msg.type === 'string' && [
    'stage-ready', 'toolbar-inject', 'stage-status', 'stage-hint',
    'select-node', 'tuning-dialog', 'show-toast',
    'inspector-inject', 'module-properties-inject',
  ].includes(msg.type)
}

export function isShellMessage(msg: any): msg is ShellMessage {
  return msg && typeof msg.type === 'string' && ['subtab', 'toolbar-click'].includes(msg.type)
}

3. useStageBridge composable(完整实现)

// src/composables/useStageBridge.ts
import { onMounted, onUnmounted, type Ref } from 'vue'
import type {
  StageMessage,
  ShellMessage,
  StageKey,
} from '@/types/stage-bridge'
import { isStageMessage } from '@/types/stage-bridge'

type Handler<T extends StageMessage['type']> = (
  msg: Extract<StageMessage, { type: T }>
) => void

export function useStageBridge(stageFrameRef: Ref<HTMLIFrameElement | null>) {
  const handlers = new Map<string, Handler<any>>()

  /** 注册某类消息的处理器(同 type 多次注册会覆盖)*/
  function on<T extends StageMessage['type']>(type: T, handler: Handler<T>) {
    handlers.set(type, handler)
    return () => handlers.delete(type)  // 反注册函数
  }

  /** 向 iframe 发 Shell→Stage 消息 */
  function send<T extends ShellMessage['type']>(
    type: T,
    data: Omit<Extract<ShellMessage, { type: T }>, 'type'>
  ) {
    const win = stageFrameRef.value?.contentWindow
    if (!win) {
      console.warn('[useStageBridge] iframe contentWindow not ready, skip send', type)
      return
    }
    win.postMessage({ type, ...data }, '*')
  }

  /** 监听 iframe → Vue 的消息 */
  function onMessage(e: MessageEvent) {
    // 来源校验:只接受来自 stageFrame 的消息
    if (e.source !== stageFrameRef.value?.contentWindow) return
    const msg = e.data
    if (!isStageMessage(msg)) {
      console.warn('[useStageBridge] reject malformed message', msg)
      return
    }
    handlers.get(msg.type)?.(msg as any)
  }

  onMounted(() => window.addEventListener('message', onMessage))
  onUnmounted(() => window.removeEventListener('message', onMessage))

  return { on, send }
}

4. Stage 端调用示例(vanilla JS)

// public/stages/stage-xilink.html 内的 <script>
;(() => {
  const STAGE = 'xilink'

  // 启动就绪
  window.parent.postMessage({
    type: 'stage-ready',
    stage: STAGE,
    version: '1.2.2-impl',
  }, '*')

  // 注入工具组
  window.parent.postMessage({
    type: 'toolbar-inject',
    stage: STAGE,
    buttons: [
      { id: 'file.new', icon: 'plus', label: '新建', tip: '新建链路' },
      { id: 'file.open', icon: 'folder', label: '打开' },
      { id: 'build.compile', icon: 'play', label: '编译', group: '构建' },
      { id: 'build.deploy', icon: 'upload', label: '烧录', group: '构建' },
    ],
  }, '*')

  // 节点选中
  function onNodeClick(node) {
    window.parent.postMessage({
      type: 'select-node',
      stage: STAGE,
      node: node.id,
      label: `${node.type}: ${node.name}`,
    }, '*')

    // 同时注入 Inspector 内容(v3.4)
    window.parent.postMessage({
      type: 'inspector-inject',
      module: node.type,
      html: `<div class="inspector-content"><h3>${node.name}</h3>...</div>`,
    }, '*')
  }

  // 双击节点 → 拉起 Tuning 浮窗
  function onNodeDblClick(node) {
    window.parent.postMessage({
      type: 'tuning-dialog',
      stage: STAGE,
      module: node.type,
      label: `${node.type} 调参 - ${node.name}`,
    }, '*')
  }

  // 监听 Shell → Stage 消息
  window.addEventListener('message', (e) => {
    if (e.data.type === 'subtab') {
      switchInternalSubtab(e.data.subtab)
    } else if (e.data.type === 'toolbar-click' && e.data.stage === STAGE) {
      handleToolbarClick(e.data.id)
    }
  })
})()

5. Stage → Shell 协议字段速查表

type stage 字段 必带其他字段 Shell 处理后果
stage-ready version 标记 ready,可发后续消息
toolbar-inject buttons[] 替换 StageBar 右半区工具组
stage-status html 注入 StatusBar #stageStatusSlot
stage-hint html 注入 BottomBar #stageHintSlot
select-node node, label 更新右 Drawer 标题为 label
tuning-dialog module, label 拉起 TuningDialog 浮窗
show-toast message, level? 显示 toast
inspector-inject html, module 注入右 Drawer Inspector
module-properties-inject html, module 注入 Bottom 属性 tab

📝 inspector-inject / module-properties-inject 不带 stage 字段是历史决策(v3.4 直接以 module 为标识)。不要添加 stage 字段(避免 break Stage 端 vanilla 代码)。


6. 协议变更流程(ADR 强制)

任何对 9 类协议的变更(新增/删除/字段调整)都必须:

Step 1 · 新建 ADR

docs/02-products/P1-xistudio/adr/ 下新建 ADR-XX-protocol-change-{name}.md

# ADR-06 · protocol-change · theme 消息(Shell→Stage)

## 背景
Vue Shell 主题切换需通知 iframe 内 Stage 同步切换。

## 变更
新增 Shell→Stage 消息类型 `theme`- `{ type: 'theme', theme: ThemeKey }`

## 影响
- 4 个 Stage HTML 需添加 message handler
- TS 类型 ShellMessage 联合类型新增 ThemeMsg

## 时间
2026-MM-DD

Step 2 · 更新本文件 §2 TS 类型

Step 3 · 更新架构 v1.2-ide-architecture.md §12.3

Step 4 · 更新 4 个 Stage HTML(同步代码)

Step 5 · 更新 useStageBridge handler 注册

Step 6 · 更新 §5 速查表

⚠️ 跳过任何一步都会导致协议契约破坏。CI 中应加 schema 校验脚本兜底。


7. 错误处理

错误场景 useStageBridge 行为
iframe contentWindow 为 null send 静默 noop + console.warn
收到 e.data 不带 type 字段 isStageMessage 返回 false → 忽略
收到未知 type handlers.get 返回 undefined → 忽略
来源不是 stageFrame iframe 直接 return(不打 warn 避免噪音)
收到的 stage 字段与当前 stageStore.current 不符 当前不做处理(信任来源),未来可加严格模式拒绝

8. 测试用例(Vitest)

// tests/stage-bridge.spec.ts
import { describe, it, expect, vi } from 'vitest'
import { ref } from 'vue'
import { mount } from '@vue/test-utils'
import { useStageBridge } from '@/composables/useStageBridge'

describe('useStageBridge', () => {
  it('注册的 handler 在收到对应 type 消息时被调用', async () => {
    const frame = document.createElement('iframe')
    document.body.appendChild(frame)
    const ref$ = ref(frame)
    const handler = vi.fn()

    // 模拟组件 setup
    const wrapper = mount({
      setup() {
        const bridge = useStageBridge(ref$)
        bridge.on('stage-ready', handler)
        return {}
      },
      template: '<div />',
    })

    // 模拟 iframe 发消息
    const msg = { type: 'stage-ready', stage: 'xilink', version: '1.0.0' }
    Object.defineProperty(window, 'event', { value: { source: frame.contentWindow } })
    window.dispatchEvent(new MessageEvent('message', { data: msg, source: frame.contentWindow as any }))

    expect(handler).toHaveBeenCalledWith(msg)
    wrapper.unmount()
  })

  it('来源校验拒绝外部 iframe 消息', async () => {
    // ... 类似测试
  })

  it('isStageMessage 类型判别正确', () => {
    expect(isStageMessage({ type: 'stage-ready', stage: 'xilink', version: '1' })).toBe(true)
    expect(isStageMessage({ type: 'unknown' })).toBe(false)
    expect(isStageMessage(null)).toBe(false)
  })
})

9. 给 Stage HTML 端的兼容提示

虽然 Stage 是 vanilla JS(无 TS),但为了与 Vue Shell 类型契约一致,建议:

  1. 复制 §2 类型注释到 stage-*.html 顶部(作为参考)
  2. field 名严格对齐stage version buttons html module label node message
  3. 向后兼容:Stage 端可以不发某些可选字段,但必带字段绝不能省略
  4. JSDoc 注解(可选):在 vanilla JS 中用 JSDoc 加类型提示
/**
 * @param {{ id: string, icon: string, label: string }[]} buttons
 */
function injectToolbar(buttons) {
  window.parent.postMessage({ type: 'toolbar-inject', stage: 'xilink', buttons }, '*')
}

v1.0 · 2026-05-17 · D4 前端改造手册 · postMessage 协议 TS 类型 · 配套 v1.2.2-impl §12.3