iOS + RN 混编实战总结:桥接、映射、Tab 栏、生命周期、数据处理
iOS + RN 混编实战总结:桥接、映射、Tab 栏、生命周期、数据处理
这篇记录我们在业务型 App 做 RN 增量迁移时的一些实战经验,重点是可落地,而不是炫技架构。
一、项目背景与目标
我们是存量 iOS(OC/Swift)项目,业务持续迭代。目标不是一次性重写,而是:
- 增量接入 RN,加快页面迭代
- 保留原生在鉴权、网络、路由、关键业务流程上的稳定性
- 保证线上主流程不中断,做到可回滚、可兼容、可观测
二、架构原则:谁负责什么
2.1 职责边界(推荐)
- 原生负责:鉴权、网络请求、路由/导航、支付、分享、关键流程
- RN 负责:页面渲染、轻交互、状态编排、模型映射
2.2 这样划分的收益
- 安全与协议口径统一
- 问题定位更快(原生链路 vs RN 渲染)
- 兼容旧模块成本更低
三、桥接设计:统一出口 + 兼容回退
3.1 不要让业务直接调用NativeModules
建议加一层统一出口,业务只调用出口函数,不直接依赖具体桥接模块。
// weproNativeBridge.tsimport{NativeModules}from'react-native';constauthBridge=NativeModules.WPAuthBridge;constlegacyBridge=NativeModules.WPOrderTrackBridge;// 示例:统一消费订单上下文,优先新桥接,失败回退旧桥接exportasyncfunctionconsumePendingTrackContext(){if(authBridge?.consumePendingTrackContext){try{return(awaitauthBridge.consumePendingTrackContext())||null;}catch{}}if(legacyBridge?.consumePendingTrackContext){try{return(awaitlegacyBridge.consumePendingTrackContext())||null;}catch{}}returnnull;}3.2 原生桥接返回值要统一语义
建议统一:
- 页面类:
Promise<boolean> - 网络类:
{ success, body, msg } - 异常类:
reject(code, message)
RCT_EXPORT_METHOD(requestPost:(NSString*)path params:(NSDictionary*)params resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){// 成功统一 resolve({ success, body, msg })// 失败统一 reject("native_post_error", xxx, nil)}四、数据映射:把脏数据处理留在边界层
RN 业务层尽量不要到处写a?.b?.c ?? '',集中在 mapping 层做转换。
typeOrderDTO={id?:string|number;status?:string;amount?:string|number;};typeOrderVM={id:string;statusText:string;amountText:string;};exportfunctionmapOrder(dto:OrderDTO):OrderVM{conststatusMap:Record<string,string>={pending:'待处理',paid:'已支付',failed:'失败',};conststatus=String(dto.status||'pending');constamount=Number(dto.amount||0);return{id:String(dto.id||''),statusText:statusMap[status]||'未知状态',amountText:`¥${amount.toFixed(2)}`,};}建议:DTO(后端)-> Domain(业务)-> VM(展示)三段式,排障和重构更稳。
五、Tab 栏与双导航栈:最容易踩坑的点
混编常见问题:
- RN 页面 push 原生页后,TabBar 状态错乱
- 返回时 TabBar 异常显示/隐藏
- push 到了错误的导航栈
5.1 处理思路
- 维护一个 TabBar 目标状态
- 优先拿真实业务导航栈,不盲目用当前
navigationController - 在转场时做主线程二次兜底设置
// 示例:统一设置 tabBar 显隐(简化版)-(void)wp_setTabBarHidden:(BOOL)hidden{UITabBarController*tabController=[selfresolveTabController];if(!tabController)return;tabController.tabBar.hidden=hidden;dispatch_async(dispatch_get_main_queue(),^{tabController.tabBar.hidden=hidden;// 转场兜底});}六、生命周期:避免看起来已登录,实际 token 未就绪
跨端常见时序问题:RN 首屏请求发起时,原生 token 还没准备好。
6.1 处理策略
- token 多来源解析(
UserModule / UserDefaults / 旧字段) - 请求前短轮询重试(如最多 6 次,间隔 80ms)
- 暴露登录快照用于排障(只读)
-(void)wp_resolveRequestMetaWithPath:(NSString*)path nonce:(NSString*)nonce timestamp:(NSString*)timestamp maxRetry:(NSInteger)maxRetry delay:(NSTimeInterval)delay resolver:(RCTPromiseResolveBlock)resolve{NSString*token=[selfresolvedToken];if(token.length>0){resolve(@{@"commonParams":...,@"authorization":...});return;}if(maxRetry<=0){resolve(@{@"commonParams":...,@"authorization":[NSNull null]});return;}dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(delay*NSEC_PER_SEC)),dispatch_get_main_queue(),^{[selfwp_resolveRequestMetaWithPath:path nonce:nonce timestamp:timestamp maxRetry:maxRetry-1delay:delay resolver:resolve];});}七、数据处理与容错:让链路可恢复
7.1 一次性上下文消费(防重复)
例如订单轨迹上下文:读取后立刻清空,避免重复消费。
staticNSDictionary*pendingContext=nil;RCT_EXPORT_METHOD(consumePendingTrackContext:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){@synchronized([MyBridge class]){NSDictionary*ctx=pendingContext;pendingContext=nil;// 一次性消费resolve(ctx?:[NSNull null]);}}7.2 关键流程先落盘再跳页
对于复效/补件等长流程,先做本地归档再进入页面,避免中途退出导致状态丢失。
八、排障与观测:没有这层,混编会很痛
建议至少做三件事:
- 关键桥接调用日志(方法名、参数摘要、耗时、结果)
- 登录态快照(token 长度、来源、
checkLogin状态) - 链路 traceId/callId(RN -> Native -> API 串联)
九、我们踩过的坑(简版)
- 业务直接调用
NativeModules,后续桥接升级改动面太大 - 页面 push 到 RN 容器导航栈,导致 TabBar 和回退行为异常
- token 读取只有单来源,首进页面偶发鉴权失败
- 没有兼容回退机制,新桥接异常会直接影响主流程
十、落地建议(给想做增量迁移的团队)
- 先定职责边界,再写桥接
- 桥接统一出口,避免业务散落调用
- 新旧能力并存期必须有 fallback
- 做最小可用观测:日志 + 快照 + 错误码归一
- 混编治理目标是稳定交付,不是追求架构名词
适用场景
- 存量 iOS 项目需要增量接入 RN
- 业务高频迭代,且不能接受一次性重写风险
- 团队对稳定性、回滚能力有明确要求
不适用场景
- 纯新项目且团队 RN/原生边界不清
- 缺少日志与发布治理能力,无法支撑混编复杂度
总结
RN 混编不是简单上 RN 页面,核心是治理跨端边界、时序一致性、导航状态和容错可观测。
业务型 App 的目标不是炫技,而是:可迭代、可回滚、可维护、线上稳。
