Skip to content

通信协议

挂件运行涉及三条通道:

数据通道方向
初始 initiframe URL #zbjInit=<encoded JSON> hash宿主 → iframe(同步)
配置 / 主题 / 尺寸 / RPC / 测试事件 / 实例持久化window.postMessage双向
实时平台事件WebSocket,SDK 直连 /ws服务端 → 挂件

挂件作者使用 Widget SDK API 即可,无需直接处理本协议。本文用于协议级描述,便于排查与建立心智模型。

1. 初始化

宿主构造 iframe URL 时把完整 init 数据编码进 URL hash:

http://127.0.0.1:9855/widgets/<id>/<entry>?zbjHost=...#zbjInit=<URI-encoded JSON>

SDK 启动时同步从 location.hash 读出 zbjInit 参数,立即写入运行时上下文、应用 CSS 变量、连接 WS、触发 ZBJ.ready 回调。iframe 加载即 ready,无握手消息。

1.1 zbjInit payload

ts
{
  sdkVersion: 1,
  mode: "edit" | "preview" | "virtualcam",
  surface: "canvas",
  platform: "bilibili" | "douyin" | "kuaishou" | "shipinhao" | "xiaohongshu",
  widgetId: string,
  instanceId: string,
  config: Record<string, unknown>,
  theme: Record<string, unknown>,
  widgetState: Record<string, unknown>,
  size: { width: number; height: number },
  host: string,    // e.g. "http://127.0.0.1:9855"
  wsUrl: string    // e.g. "ws://127.0.0.1:9855/ws"
}

2. postMessage 信封

ts
// 宿主 → iframe
interface HostMessage {
  source: "zbj-host";
  type: string;
  payload: unknown;
}

// iframe → 宿主
interface WidgetMessage {
  source: "zbj-widget";
  type: string;
  payload: unknown;
}

payload 必须是 structured clone 兼容的对象:纯 JSON 可序列化的数据。挂件作者通过 SDK API 使用,SDK 会保证传出的内容符合要求。

3. 宿主 → iframe 消息

typepayload触发时机
configRecord<string, unknown>(完整 config 对象)用户在配置面板改动任意 config 字段
themeRecord<string, unknown>(完整 theme 对象)用户改动任意 theme 字段
resize{ width: number; height: number }元素尺寸变化(拖拽缩放 / 面板改宽高)
rpc-result{ rpcId: string; ok: boolean; data?: unknown; error?: string }RPC 调用完成
test-event一个归一化事件对象(ZBJAnyEvent用户点击配置面板「发送测试事件」按钮

config / theme / resize 的推送做 RAF 节流:同一帧内的多次变更合并为一次 postMessage,确保拖动滑块 / 缩放控制柄时挂件 UI 帧率稳定。

4. iframe → 宿主 消息

typepayload用途
rpc{ rpcId: string; method: string; args: unknown[] }调用 invoke / getGifts / getRoomInfo
request-resize{ width: number; height: number }对应 ZBJ.requestResize;宿主在 min/maxSize 内可接受,否则丢弃
state{ op: "set" | "remove"; key: string; value?: unknown }ZBJ.storage.set / remove 回写
logunknown[]ZBJ.log 转发;宿主以 [widget] <widgetId> … 输出到主进程日志

宿主收到 state / rpc 时只在 mode === "edit" 下处理,避免 preview / virtualcam 窗口的镜像组件重复处理同一调用。

5. WebSocket 协议(平台事件)

5.1 连接地址

ws://127.0.0.1:9855/ws

hostwsUrl 都在 init 数据里下发,SDK 优先用 init 给的 wsUrl

每个挂件 iframe 都自己开一条 WS 连接(多挂件 = 多连接,是被原生支持的)。

5.2 平台事件信封

jsonc
{
  "event": "platform-message",
  "data": {
    "platform": "douyin",
    "type": "message",
    "content": { /* PlatformMessage 对象 */ }
  }
}
data.typedata.content 形态
"message"PlatformMessage 对象(参见 平台事件
"status""connect-success" | "connect-error" | "connect-close"

SDK 只把 event === "platform-message" && data.type === "message" 的帧通过归一化分发给挂件;其它帧被忽略。

5.3 断连与重试

SDK 在 ws.onclose 后做指数退避重连(base 1s,cap 30s)。

6. 时序

iframe 创建

   │── URL 形如 ".../entry?zbjHost=...#zbjInit=<JSON>"
   │── HTML 解析,<script src="…zbj-widget-sdk.js"> 加载执行
   │── SDK 同步读 hash → 填充 ctx → ready=true
   │── SDK 异步 new WebSocket(ctx.wsUrl)
   │── microtask 触发所有 ZBJ.ready 回调

   │── 挂件内 ZBJ.invoke(method) ──postMessage→ 宿主
   │                                          │── handleRpc 执行
   │ ◀── postMessage rpc-result ──────────────│

   │── 用户改面板 → 宿主 RAF 节流
   │ ◀── postMessage config / theme / resize ─

   │── ZBJ.storage.set ──postMessage→ 宿主
   │                                          │── 写回元素 widgetState

   │── /ws 流 ──ws.onmessage→ SDK normalize → ZBJ.on('gift', cb) 等回调

6.1 RPC 关联

挂件调用 ZBJ.invoke(method, ...args) 时:

  1. SDK 生成单调递增的 rpcIdrpc_1rpc_2),写入 rpc 映射;
  2. 设置 10 秒定时器作为超时兜底;
  3. 通过 postMessage 发送 { source:"zbj-widget", type:"rpc", payload: { rpcId, method, args } }
  4. 宿主执行后通过 postMessage 回发 { type:"rpc-result", payload: { rpcId, ok, data?, error? } }
  5. SDK 据 rpcId 解出对应 promise 并清除定时器。

6.2 RPC 白名单

宿主端的 RPC 处理器按 method 路由。当前白名单:

method返回
"getGifts"ZBJGift[],当前平台礼物列表
"getRoomInfo"ZBJRoomInfo,当前房间信息

未在白名单的 method:宿主回发 { ok: false, error: 'RPC method "<method>" not allowed' },SDK 端 invoke Promise reject。