主题
通信协议
挂件运行涉及三条通道:
| 数据 | 通道 | 方向 |
|---|---|---|
| 初始 init | iframe 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 消息
type | payload | 触发时机 |
|---|---|---|
config | Record<string, unknown>(完整 config 对象) | 用户在配置面板改动任意 config 字段 |
theme | Record<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 → 宿主 消息
type | payload | 用途 |
|---|---|---|
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 回写 |
log | unknown[] | ZBJ.log 转发;宿主以 [widget] <widgetId> … 输出到主进程日志 |
宿主收到 state / rpc 时只在 mode === "edit" 下处理,避免 preview / virtualcam 窗口的镜像组件重复处理同一调用。
5. WebSocket 协议(平台事件)
5.1 连接地址
ws://127.0.0.1:9855/wshost 与 wsUrl 都在 init 数据里下发,SDK 优先用 init 给的 wsUrl。
每个挂件 iframe 都自己开一条 WS 连接(多挂件 = 多连接,是被原生支持的)。
5.2 平台事件信封
jsonc
{
"event": "platform-message",
"data": {
"platform": "douyin",
"type": "message",
"content": { /* PlatformMessage 对象 */ }
}
}data.type | data.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) 时:
- SDK 生成单调递增的
rpcId(rpc_1、rpc_2),写入 rpc 映射; - 设置 10 秒定时器作为超时兜底;
- 通过
postMessage发送{ source:"zbj-widget", type:"rpc", payload: { rpcId, method, args } }; - 宿主执行后通过 postMessage 回发
{ type:"rpc-result", payload: { rpcId, ok, data?, error? } }; - 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。