Skip to content

第 05 章:State / Intent 协议 ​

1. State 是跨语言协议,不是普通 props ​

在 HomeContainer.types.ts 中,public state 被刻意压成可序列化结构:

ts
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 是一个代数数据类型 ​

ts
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 都不能重复。
ts
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 用 JSONObject decode,并调用 hasValidTabInvariants();
  • 任一失败都不会继续 bind snapshot,而是通过 onRenderError 回报 state_decode_failed 或 invalid_state。

这是必要的防御:native engine 可能从旧 bundle、异常 bridge、开发调试输入或错误版本 JS 收到 JSON,不能假设上游永远正确。

4. Intent 是控制面 ​

ts
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 做四件事:

  1. JSON parse;
  2. 检查非空 intentId;
  3. 检查 owner 的 scopeKey/sessionId;
  4. 根据 kind 检查 command、tab、requestId 和 itemId 的形状。

它只回答“这个字符串符合协议吗”,不回答“这个动作现在允许吗”。Renderer 收到后还会比较 parsed owner 与当前 owner,再把 intent 包装成带 authority revision 的 Store intent。

5. Owner 是协议的一部分 ​

ts
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 只暴露三件事 ​

ts
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 不得影响新页面。

基于 app-monorepo 2026-07-31 源码快照整理 · 非官方文档