第 07 章:Action、Tab、Refresh 如何回到业务层
1. Native 只发 command,不发副作用
原生 row、header button、tab 和 pull-to-refresh 最终都调用 emitIntent。以 Android action 为例,输出近似:
{
"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:
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 上点击:
- native 先把 ViewPager/scroll transition 做完;
- transition idle 时发
selectTabintent; - JS 通过
dispatchHomeIntent({ type: 'tabSelected', authority: ... })更新 Store; - Store 新 snapshot 回来;
- 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)。
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。它发:
{
"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 事件最好拆成三步:
- native 产生语法正确的 intent;
- JS 验证 owner 并选择业务 command;
- Store 用 revision/authority 决定是否接受。
任何一步缺失,都会把陈旧 UI 事件变成真实的业务副作用。