第 06 章:Nitro 宿主与 React Slots
1. 为什么需要 Surface + Host + Slot 三件套
React Native 的组件树和原生引擎的内部 view 树不天然相同。Home 需要同时满足:
- 一个占满页面的 native engine;
- 许多由 React 渲染的复杂内容;
- React slot 必须出现在原生 header/list/footer 的精确位置;
- slot 的交互和可访问性必须受当前 owner 控制。
因此 HomeContainer.native.tsx 的返回结构是:
<HomeContainerSurface>
<HomeContainerHost />
<HomeContainerSlot slotKey="header.account-row">...</HomeContainerSlot>
<HomeContainerSlot slotKey="content.header.portfolio">...</HomeContainerSlot>
</HomeContainerSurface>Surface 是 children 管理和 hit testing 的物理容器;Host 是 Nitro HybridView;Slot 是可被移动到原生 host 的 React 子树。
2. Nitro contract
HomeContainer.nitro.ts 把 native API 缩成:
export interface IHomeContainerNativeProps extends HybridViewProps {
initialStateJson: string;
backgroundColor: string;
debugOverlayEnabled: boolean;
onRenderError?: (code: string, message: string) => void;
onIntent?: (intentJson: string) => void;
}
export interface IHomeContainerNativeMethods extends HybridViewMethods {
setState(stateJson: string): void;
completeRefresh(requestId: string): void;
selectTab(tabId: string, animated: boolean): void;
}Nitrogen autolinking 把 HomeContainer 绑定到 Swift/Kotlin 的 HybridHomeContainer,而 codegenConfig 把 OneKeyHomeContainerSurface 和 OneKeyHomeContainerSlot 绑定到 Fabric component view manager。
3. JS wrapper 的三个关键技巧
稳定 host
function getHomeContainerHost() {
const runtime = globalThis as typeof globalThis & {
__onekeyHomeContainerHost?: IHomeContainerHost;
};
runtime.__onekeyHomeContainerHost ??= createHomeContainerHost();
return runtime.__onekeyHomeContainerHost;
}Host 在 runtime 内缓存,避免重复创建 Nitro host 对象。
稳定 callback
onRenderError 和 onIntent 使用 ref 保存最新闭包,再用 nitroCallback 暴露稳定函数。这样 native 不需要因为 React callback identity 改变而频繁重绑,但业务仍能拿到最新 state/handler。
首次 JSON 和增量 JSON 分离
首次通过 initialStateJson,后续通过 ref 的 setState。这与原生的“首帧同步、增量异步”策略对齐。
4. Slot flattening
slotBundle.slots 是按语义分组的:accountRow、balance、headerActionRow、contentHeaders、contentStates、contentFooters、tabAccessories。wrapper 把它 flatten 成稳定 key:
| 语义字段 | slot key |
|---|---|
accountRow | header.account-row |
balance | header.balance |
headerActionRow | header.action-row |
contentHeaders.portfolio | content.header.portfolio |
contentStates.history | content.state.history |
contentFooters.history.historyEnd | content.footer.history.historyEnd |
tabAccessories.nft | tab.accessory.nft |
native engine 只需要知道 key,不需要知道 React component 的类型。
5. Slot ownership 与“presence suppresses fallback”
Surface 会把当前出现的 slot key 集合传回 native:
“slot 是否出现”与“slot 是否有权交互”是两个条件:
- presence 表示 React 正在接管这个 presentation;native 不应在 owner 切换期间突然显示旧 fallback;
- owner match 决定它是否能响应触摸、是否进入 accessibility tree。
6. 为什么不直接把 slot 当普通 child
普通 child 只能跟随 Surface 的 bounds,无法自然出现在 header 内部的 UITableView/RecyclerView row、tab accessory 或 footer host。Surface/Slot 的自定义 mount、layout、hitTest 和 reparenting,实际上把 React 子树嵌入了原生页面内部的多个“投影点”。
这也是该系统最容易出 bug 的地方:布局变化、列表复用、owner 切换和 React unmount 必须同时触发重新挂载。因此 Android 和 iOS 都把 slotLayoutChange/onSlotLayoutChange 作为显式回调,并在列表 attach/detach、动画结束、header 高度改变时再次布局。