本节摘要:地图、视频播放器、富文本编辑器——这些能力以「视图」的形态存在于原生世界,纯模块桥接装不下它们,需要的是原生组件封装:把原生视图包成 JSX 里可用的 React 组件,属性从 JavaScript 流向视图、事件从视图流回 JavaScript。本节以一个原生横幅视图为例走通双向通道,并给出「两端一致性」与「生命周期对齐」两大封装纪律。
先划清适用边界。如果能力可以「调用后拿结果」(查电量、扫个码),7.1 的模块够了;如果能力必须「持续可见、可交互」(地图要拖动缩放、播放器要进度手势、相机要实时预览),就必须把原生视图本体嵌进界面树。此时 JavaScript 拥有的不再是数据,而是视图的「遥控器」:发属性让它变,收事件被它通知。
封装的核心是双向通道。属性方向:React 组件的 props 变化被映射为原生视图的属性更新,框架负责在渲染管线里同步这条通道——新架构下这条通道也走了直调,属性刷新不再排队。事件方向:原生视图把用户交互(点击、拖动、播放完成)包装成带载荷的事件发给 JavaScript。以一个营销横幅视图为例,声明合同:
// NativeBanner.tsx:封装后的 React 组件外壳 import { requireNativeComponent, processColor } from 'react-native'; // 原生组件引用:名称与双端注册名一致 interface NativeBannerProps { title: string; highlightColor: string; // 颜色需经转换成平台格式 onPressBanner?: (e: { position: number }) => void; } const NativeBannerView = requireNativeComponent<NativeBannerProps>('BannerView'); export function NativeBanner(props: NativeBannerProps) { return ( <NativeBannerView {...props} // 颜色属性在 JS 侧先转整数格式,避免双端解析差异 highlightColor={processColor(props.highlightColor)} /> ); }
// Android 侧:视图管理器负责创建视图、映射属性、派发事件 class BannerViewManager : SimpleViewManager<TextView>() { override fun getName() = "BannerView" override fun createViewInstance(ctx: ThemedReactContext) = TextView(ctx) // 属性映射:JS 侧的每个 prop 对应一个标注方法 @ReactProp(name = "title") fun setTitle(view: TextView, title: String) { view.text = title } @ReactProp(name = "highlightColor", customType = "Color") fun setHighlight(view: TextView, color: Int) { view.setTextColor(color) } }
// iOS 侧:视图管理器创建原生视图并暴露属性与事件 @objc(BannerViewManager) class BannerViewManager: RCTViewManager { override func view() -> UIView { BannerView() } override static func requiresMainQueueSetup() -> Bool { true } // UI 构建回主线程 } class BannerView: UIView { @objc var title: String = "" { didSet { label.text = title } } @objc var highlightColor: UIColor = .blue { didSet { label.textColor = highlightColor } } // 点击时经桥发事件给 JS @objc func tapped() { onTap?(["position": 0]) } }
纪律一:两端行为对齐要靠测试,不能靠相信。 同一个组件名背后是两份独立实现,属性解析、事件载荷、边界行为都可能悄悄分叉。做法是给封装层配一份双端行为清单:每个属性的默认值、每个事件的载荷字段、空值与极值的处理,逐项在两端真机验证。声明式合同解决了「签名对齐」,行为对齐依然要靠清单——代码生成器管不了 didSet 里写没写错。
纪律二:生命周期对齐。 原生视图有自己的资源(播放器内核、地图引擎实例),React 组件的挂载卸载必须与原生资源起停严格对应:组件卸载时释放播放器、停掉地图定位,否则每进出一次页面漏一份内核资源,正是 6.3 节惯犯手法的原生版。另外注意框架的回收机制——列表中的原生组件在滚出窗口时会被回收重建,重资源组件要实现「快照降级」(离场留一张封面图,回场再复活内核),这也是视频信息流的标准做法。
背景:短视频信息流最初把播放器做成「永不回收」的单例悬浮播放,滚出屏幕的条目视频仍在解码,低端机两屏之后必然卡顿。操作:第一步把播放器封装成原生组件并接入列表虚拟化体系,离场自动暂停释放;第二步实现快照降级——离场瞬间抓当前帧存为封面,回场先显封面再无缝续播;第三步对事件通道节流,播放进度事件从每帧上报改为每秒两次,进度条本身用原生动画补间,避免事件洪峰冲击 JS 线程(2.1 与 3.4 的知识在桥接层再次会师)。结果:同机型连刷上百条不卡顿,内存曲线锯齿平稳。解读:封装的功夫在门外——组件本体不难,难的是与列表虚拟化、线程模型、内存纪律的对接。变式:如果产品要求「画中画」,播放器要脱离 React 树挂到系统窗口层,此时属性通道仍在、生命周期改为由系统接管,封装层需要额外的「脱离模式」状态机——原生组件的复杂度永远来自与系统机制的对齐,而不是视图本身。
能力与界面都会桥接了,下一节补上规矩:权限怎么合规地要,存量模块怎么走向新架构。