第 05 章:State / Intent 协议
1. State 是跨语言协议,不是普通 props
在 HomeContainer.types.ts 中,public state 被刻意压成可序列化结构:
export interface IHomeContainerState {
owner: IHomeContainerOwner;
payload: IHomeContainerSnapshotPayload;
}
export interface IHomeContainerSnapshotPayload {
selectedTabId: IHomeContainerTabId;
header: IHomeContainerHeader;
tabs: IHomeContainerTab[];
theme: IHomeContainerTheme;
}它不允许 ReactNode、函数或 JS class 穿过 native boundary。复杂 UI 通过 slotBundle 单独传递,native state 只描述 native engine 能理解的可序列化内容。
2. Tab destination 是一个代数数据类型
type IHomeContainerTab =
| {
id: IHomeContainerTabId;
title: string;
destination: 'inline';
sections: IHomeContainerSection[];
handoffCommandId?: never;
}
| {
id: IHomeContainerTabId;
title: string;
destination: 'handoff';
sections: [];
handoffCommandId: string;
};这比 isWeb?: boolean 更强,因为它把非法组合直接写进类型和运行时 invariant:
- inline tab 不能带 handoff command;
- handoff tab 必须带非空 command;
- handoff tab 不能带 sections;
- selected tab 必须存在且必须是 inline;
- tab id 和 section id 都不能重复。
export function isHomeContainerSnapshotInvariantValid(snapshot) {
const tabIds = new Set<IHomeContainerTabId>();
for (const tab of snapshot.tabs) {
if (tabIds.has(tab.id)) return false;
tabIds.add(tab.id);
if (tab.destination === 'handoff') {
if (!tab.handoffCommandId || tab.sections.length > 0) return false;
} else if (tab.handoffCommandId !== undefined) {
return false;
}
}
return snapshot.tabs.some(
(tab) => tab.id === snapshot.selectedTabId && tab.destination === 'inline',
);
}3. Native 侧必须再次验证
TS validator 只保护 JS;Swift 和 Kotlin 都实现自己的模型与 invariant:
- Swift
HomeContainerTab.init(from:)对 destination 进行条件 decode; - Swift
homeContainerValidatesBusinessInvariants检查 owner、tab、section; - Kotlin
HomeContainerJson.parseState用JSONObjectdecode,并调用hasValidTabInvariants(); - 任一失败都不会继续 bind snapshot,而是通过
onRenderError回报state_decode_failed或invalid_state。
这是必要的防御:native engine 可能从旧 bundle、异常 bridge、开发调试输入或错误版本 JS 收到 JSON,不能假设上游永远正确。
4. Intent 是控制面
type IHomeContainerIntentPayload =
| { kind: 'action'; commandId: string; itemId?: string }
| { kind: 'handoff'; tabId: IHomeContainerTabId; commandId: string }
| { kind: 'refresh'; tabId: IHomeContainerTabId; requestId: string }
| { kind: 'selectTab'; tabId: IHomeContainerTabId };parseHomeContainerIntent 做四件事:
- JSON parse;
- 检查非空
intentId; - 检查 owner 的
scopeKey/sessionId; - 根据 kind 检查 command、tab、requestId 和 itemId 的形状。
它只回答“这个字符串符合协议吗”,不回答“这个动作现在允许吗”。Renderer 收到后还会比较 parsed owner 与当前 owner,再把 intent 包装成带 authority revision 的 Store intent。
5. Owner 是协议的一部分
export interface IHomeContainerOwner {
scopeKey: string;
sessionId: string;
}两者用途不同:
| 字段 | 作用 |
|---|---|
scopeKey | 描述业务展示作用域,例如 enabled/disabled、wallet、account、network、all/single 的组合 |
sessionId | 描述本次 Home renderer session,区分同一个 scope 的不同挂载/生命周期 |
Native Surface 的每个 slot 都携带这两个值,native 只有在 state.owner == slot.owner 时才让 slot 参与交互和可访问性。
6. Imperative ref 只暴露三件事
export interface IHomeContainerRef {
setState(state: IHomeContainerState): void;
completeRefresh(requestId: string): void;
selectTab(tabId: IHomeContainerTabId, animated?: boolean): void;
}没有 scrollToRow、renderItem、runCommand 等 escape hatch。ref 只处理 native engine 自己拥有的状态:同步 state、结束 refresh、切 tab。业务动作仍然必须经过 intent 回到 Store。
7. 协议的真正价值
这套协议让 native engine 可以被当成一个可测试的“状态机”:给定合法 state 和一串用户手势,应该只产生合法 intent;给定非法 state,应该稳定拒绝;给定不同 owner,旧 slot/旧 refresh 不得影响新页面。