Skip to content

第 07 章:Action、Tab、Refresh 如何回到业务层 ​

1. Native 只发 command,不发副作用 ​

原生 row、header button、tab 和 pull-to-refresh 最终都调用 emitIntent。以 Android action 为例,输出近似:

json
{
  "intentId": "...",
  "owner": { "scopeKey": "...", "sessionId": "..." },
  "intent": {
    "kind": "action",
    "commandId": "home.portfolio.openAsset",
    "itemId": "token:ethereum:usdc"
  }
}

Native 不知道这个 command 是打开详情、管理 token、切换市场分类还是弹窗。它只负责把用户操作变成结构化事件。

2. Renderer 的 intent 分流 ​

handleIntent 的顺序很重要:

3. Action authority:解析成功仍然不代表可执行 ​

对于 header command,Renderer 使用 shell.shellCommandRevision;对于 section command,使用每个 section 自己的 sectionCommandRevision:

ts
storeIntent = {
  type: 'sectionActionInvoked',
  actionId: intent.intent.commandId,
  authority: {
    kind: 'sectionCommands',
    revision: targetRevision,
    sectionId: targetSectionId,
  },
  execution: 'controller',
  intentId: intent.intentId,
  itemId,
  owner: facts.owner,
  sectionId: targetSectionId,
  sessionId: facts.ownerToken.sessionId,
};

Store 仍可以拒绝:

  • command 已从当前 presentation 消失;
  • revision 已过期;
  • owner/session 不一致;
  • 当前 section 不再适用。

这是“显示层可乐观、执行层必须保守”的典型设计。

4. Tab 选择是双向同步 ​

用户在 native tabs 上点击:

  1. native 先把 ViewPager/scroll transition 做完;
  2. transition idle 时发 selectTab intent;
  3. JS 通过 dispatchHomeIntent({ type: 'tabSelected', authority: ... }) 更新 Store;
  4. Store 新 snapshot 回来;
  5. native 以新的 selectedTabId 复核当前页。

如果 Store 拒绝 tab selection,acceptTabSelection 会用 ref 把 native 选回当前 authoritative tab,避免 UI 留在一个业务不承认的页面。

5. Refresh 是 request/ack 协议 ​

native pull-to-refresh 生成 requestId,JS 对当前 tabs 发起 section refresh;只要至少一个 Store effect 被接受,就在 1.2 秒后调用 completeRefresh(requestId)。

ts
if (!didStartRefresh) {
  nativeRef.current?.completeRefresh(requestId);
  return;
}
const timeoutId = setTimeout(() => {
  nativeRef.current?.completeRefresh(requestId);
}, HOME_REFRESH_FEEDBACK_DURATION_MS);

这里的完成语义是“结束用户反馈动画”,不是“所有网络请求已经完成”。真实数据更新仍通过 Store 回流;若请求被拒绝或 owner 已不存在,必须立即 ack,不能让 UI 一直转圈。

6. Handoff tab 的特殊性 ​

handoff tab 的 native sections 为空,所以点击不能走 section action。它发:

json
{
  "kind": "handoff",
  "tabId": "perps",
  "commandId": "home.perps.openWeb"
}

JS 通过 tabHandoffInvoked 做 authority check,成功后 navigation.switchTab(ETabRoutes.WebviewPerpTrade)。这样 native Home 可以保留统一 tab chrome,而 Perps 的真正内容仍由 Web/另一个 route 承载。

7. 一个值得复用的原则 ​

跨层 UI 事件最好拆成三步:

  1. native 产生语法正确的 intent;
  2. JS 验证 owner 并选择业务 command;
  3. Store 用 revision/authority 决定是否接受。

任何一步缺失,都会把陈旧 UI 事件变成真实的业务副作用。

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