本文详述了去哪儿火车票团队将现有 React Native (RN) 项目迁移至基于 Taro 的企业级多端解决方案 QTaro 的实践经验。项目旨在通过一套代码实现 iOS、Android、HarmonyOS、支付宝小程序、微信小程序及 H5 等六端的复用,以降低开发和维护成本。文章深入探讨了技术选型、面临的组件和逻辑差异挑战,并详细介绍了转换工具的设计思路、RN 库映射多端库、组件属性差异抹平、业务代码多端适配等核心技术方案。特别针对微信小程序 Size 超限问题,团队采用了将 RN 页面转为 H5 并在 WebView 中加载的方案,并对比了 MPA 和 SPA 的优劣,最终选择了 SPA 实现。项目成果显著,包括代码复用率达到 87%、节约人力成本超过 50%,并成功突破小程序体积限制,且 H5 页面的 TTI-P90 性能表现优于小程序原生。文章最后也坦诚了当前在组件兼容性、转换工具覆盖度及样式边缘情况上存在的不足与未来展望。
文章概览

-
项目背景及目标
-
技术选型
-
项目筹备
-
迁移实战
-
项目复盘
-
结语
作者介绍:
刘亮亮,2019年加入去哪儿旅行火车票团队,火车票前端技术委员会成员,主要负责火车票/汽车/船票营销和辅营相关业务开发。
一、项目背景及目标
随着公司业务持续拓展,多端实现相同功能的需求日益凸显。与此同时,多端独立开发的弊端逐渐显现:
1. 人力成本上升,开发人员需掌握多种技术栈,学习成本高、负担重;
2. 维护成本增加,问题修复与迭代升级周期延长,效率降低。
为解决上述问题,火车票大前端团队启动“统一前端技术栈”项目,旨在通过一套代码实现多端复用。
项目目标:
1. 实现一套React Native(RN)代码在iOS、Android、Harmony、支付宝小程序、微信小程序及H5共6端复用;
2. 转换工具的代码复用度>80%
二、技术选型
1. 考量因素
-
当前火车票前端需求以RN开发为主,需将RN项目高效转化为多端应用,提升产品体验一致性。
-
QTaro是大前端中心基础框架团队基于Taro打造的多端解决方案,支持小程序、H5与RN项目同步开发。其在原生Taro生态基础上,提供了公司内部多端开发工具、业务扩展库及开发规范,能有效降低业务前端开发成本。QTaro还提供RN代码转换、运行时支持及项目发布能力,与火车票项目诉求高度契合。
-
尽可能保留原有RN开发习惯,降低团队学习成本。
整体方案:依托QTaro的跨平台能力,联合基础框架团队打通QTaro与QRN,基于QTaro实现业务跨端落地,最终将QRN运行于多端,从而降低学习成本、提升开发效率、减少人力投入。
2. 方案挑战
-
组件与属性差异大:涉及RN/QRN/第三方库组件与API约82处,CSS 属性差异30余处,如何抹平差异实现无缝衔接是关键。
-
业务逻辑层多端差异:页面路由、登录、支付等功能需各端独立实现。
-
多端性能优化:转换后代码在各端的性能表现需持续优化。
-
请求接口统一:如何实现各端网络请求接口的统一封装。
三、项目筹备
项目分两期推进:
-
一期目标:完成订单详情页从RN到QTaro的转换,并上线微信小程序版本。选取该页是因为其UI组件丰富、交互相对简单,适合作为技术验证切入点。
-
二期目标:完成火车票所有RN项目的转换并全端上线。
四、迁移实战
1.转换工具设计
1.1 整体流程


1.1.1 方案选型
-
方案一: RN转多端。以RN项目为开发起点,隐藏中间转换项目,保持RN开发习惯,实现RN→多端单向转换,学习与开发成本最低。
-
RN转换成QTaro中间项目
-
中间项目转多端(WX小程序/H5/ALI小程序)的方式,是RN单向转换,最小的开发成本就可实现RN代码一拖六
-
方案二: QTaro转多端。以QTaro中间项目为起点,实现QTaro→6端转换,转换工具实现复杂,且需遵循RN样式约束,开发成本较高。
综合评估后,选择方案一:RN转多端。兼顾开发效率、开发成本、学习成本,以及减少对当前RN项目的影响。
-
从QTaro中间项目作为开发起点,转RN。必须按照规范《 React Native 端开发前注意》开发,涉及6端的转换, 转换工具实现复杂;
-
按照QTaro的RN规范开发,本质是以RN的样式管理约束。在样式上统一多端,木桶效应,短板对齐(向最弱的RN对齐);
-
在灵活的QTaro项目下开发,还要施加RN的样式约束,不如直接以RN项目开发更顺畅;
-
QTaro->RN可能存在诸多问题,对当前RN项目影响大(线上出故障的概率变高)而且增加维护成本。
1.1.2 中间项目说明
-
中间项目是临时&隐藏项目, 位置在当前项目.taroCache/project目录下。开发者不需要关心.taroCache,可以认为它不存在。

-
中间项目符合QTaro框架规范的要求。
-
开发者使用rn2QTaro cli命令行进行操作,内部调用Taro命令对中间项目进行构建。
-
经过Taro build的处理之后,中间项目将会生成目标平台产物。
1.1.3 一套代码6端复用的思路
-
RN项目->转换工具->QTaro中间项目->ALI小程序/WX小程序/H5 3端
-
RN项目->Metro->IOS/Adr/Harmony 3端
1.2 转换工具流程图

转换流程说明:
1.2.1 初始化中间项目
1. rn2QTaro拉取模板项目。模板项目是最小化的QTaro Js项目,承载转换后的JS代码。
2. 根据当前项目中rn2QTaro.config.js配置项,安装中间项目的依赖文件以及RN多端库。
1.2.2 转换过程
1. 转换工具遍历项目所有JS文件,根据配置项,将RN/QRN等相关库映射为替换库。
2. 删除RN项目中所有propTypes。
1.3 RN库映射多端库

1.3.1 架构图说明
1. 转换工具根据配置文件,把项目中使用的RN库映射为对应的多端库。
2. 多端库通过QTaro的MultiPlatformPlugin引用对应平台实现的组件和API。
1.3.2 架构亮点
1. 多端库的代码结构设计,利用QTaro已有的多端能力(MultiPlatformPlugin)进行多平台切换,简单高效。
2. 通过封装QTaro抽象层的组件和API构建的RN等效多端库,可以随QTaro的扩展而动态支持更多平台。
1.4 抹平组件属性差异
1.4.1 方案选择

方案分析:
-
RN和其他端相同功能的组件,例如: View, Text等, 默认属性差异大, 且style属性多为动态赋值,用babel处理非常复杂。例如: 为CSS样式width属性添加'PX'单位。期望将width: 20转义为width: "20PX",但实际项目多是动态赋值:

-
babel转义难以完成。
-
选用运行时方案,仅需封装各端的组件和API,修改接口或属性向RN对齐,即可完成。
-
在RN项目中,多采用import进行引用,而import语句本身是静态的,在转义阶段就已确定,因此import路径的替换必须放在转义时进行。
方案确认:
1. 通过babel AST完成RN库映射为多端库。
2. 在运行时抹平各组件CSS样式差异,抹平样式模块封装在基类组件中。
1.4.2 行内样式 & 样式文件
RN使用JS对象来定义样式,并通过StyleSheet.create方法来创建样式表。
//styles.jsconst styles = StyleSheet.create({container: {flex: 1,backgroundColor: '#F5FCFF',}});//App.js<Viewstyle={styles.container}></View>
当RN转QTaro,使用react开发时,同样支持使用JS对象定义样式。保持RN原有的CSS-in-JS样式是比较简单的处理方式。
另一个问题是react中没有StyleSheet.create方法。
解决方法2种:
1. babel AST转为
//styles.js//babel转义: 把StyleSheet.create的入参,直接赋值给stylesconst styles = {container: {flex: 1,backgroundColor: '#F5FCFF',}};//App.js<Viewstyle={styles.container}></View>
2. 自己实现StyleSheet.create
const StyleSheet = {create(styles) {const result = {};Object.keys(styles).forEach(key => {result[key] = styles[key]});return result;}};
1.4.3 行内样式style兼容
//RN中style支持样式数组,QTaro中不支持<Viewstyle={[styles.container,extraStyle]}></View>
解决方法:
1. 自己实现StyleSheet.flatten,递归地展平数组中的每个样式对象,并将它们合并成一个单一的样式对象。
2. 样式处理
3. 方式1: babel 转义
//RN中style支持样式数组,QTaro中不支持<Viewstyle={StyleSheet.flatten([styles.container,extraStyle])}></View>
方式2: 在兼容组件的入口,调用StyleSheet.flatten转为单一样式对象
1.4.4 更多样式问题
1. 尺寸单位
RN中许多样式属性是不带单位的。因为RN的样式系统是基于弹性布局(Flexbox)设计的,它在不同的平台(iOS 和 Android)上会自动进行适配。
但是在QTaro中,建议使用px、%作为尺寸单位,其默认会根据平台对单位进行转换,不希望做默认转换需要设置为"PX"。
我们评估RN代码转QTaro,大多数需求场景设置"PX"较为合理,实际项目测试的结果也符合预期。
常见的width, height, margin, bottom等,约有60多个。
可参考文档: 《Layout Props · React Native》
2. 阴影
//RN代码 shadow样式StyleSheet.create({myView: {shadowRadius: 5,shadowOpacity: 0.5,shadowOffset: {x: 2, y: 2},shadowColor: 'gray'}})//转为 react boxShadow样式StyleSheet.create({myView: {boxShadow: `2PX 2PX 5PX 0PX gray`}})
3. 动画
将RN的transform属性转换为Web的transform属性。
rn支持的转换:
-
rotate系列:rotate, rotateX, rotateY, rotateZ
-
scale系列:scale, scaleX, scaleY,scaleZ
-
translate系列:translate, translateX, translateY, translateZ
-
skew系列:skew, skewX, skewY
//RN的 transform{transform: [{ rotateZ: '-60deg' },{ translateY: -8 },{ translateY: 4 }]}//react的 transform{transform: 'rotateZ(-60deg) translateY(-8PX) translateY(4PX)'}
4. 样式替换
例如: RN的marginStart需要替换为marginLeft, marginHorizontal需要替换为marginLeft, marginRight。
//RN属性{marginHorizontal: 10}//转换后代码{marginLeft: 10PX,marginRight: 10PX}
5. 默认样式差异
-
display:
-
RN: 组件的display属性是flex,所有的布局都是基于Flexbox。
-
CSS: 组件的display属性是block,类似于HTML中的div元素。
-
flex和flexDirection:
-
RN: 在View上默认display: flex和 column。子组件垂直排列
-
CSS: 如果设置为flex布局,默认值是row。子元素水平排列
-
box-sizing:
-
RN: 默认值为border-box且只支持这种
-
CSS: 默认值是content-box
-
更多默认样式差异可参考:
-
React Native端开发前注意
-
reactnative的css布局和web中css布局区别及介绍
2. 实现RN多端库
实现UI组件及API库,替换RN/QRN/第三方RN库中对应的UI组件及API。RN多端库是抹平多端差异的核心库, 具有通用性,适用于各RN项目。
2.1 已实现多端库


还包括qunar-react-native, react-native-keyboard-aware-scroll-view, react-native-render-html, react-native-WebView等共14个RN多端库, 不再详细列举。
RN标准库中组件和API实现已覆盖80%(因平台差异, 无法实现组件占位留空)。
火车票项目中组件和API覆盖86%,和平台关联较大的部分组件(截屏、QP包管理、日历、通知等)已被降级处理。
2.2 组件实例接口差异
RN中组件节点的实例方法与QTaro组件库存在差异
常见的有:
-
this.viewRef.current.measureLayout测量视图布局
-
this.qVideoRef.current.play/pause/seek视频播放等操作
-
this.textInputRef.current.blur/isFocused判断焦点操作
通过自定义组件实例的外部接口,确保多端库组件实例方法与RN组件接口一致。
下面以View组件为例,添加可供外部调用的方法measureLayout,和RN View接口保持一致。

3. 业务代码多端适配方案
RN转QTaro框架, 不仅要适配RN/QRN/三方RN组件,还需处理平台相关的业务逻辑差异。
深入分析每个差异功能后,发现业务逻辑差异是因使用RN原生插件引起,以NativeModules为切入点可以做到隐藏差异。
主要差异功能:登录、支付、页面路由、网络请求
以Network为例,说明业务代码的多端适配图

3.1 架构说明
以NativeModules为切入点,业务开发同学不需要关心平台差异,同时可以对NativeModules扩展,后续支持更多平台。
3.2 NativeModules多平台切换
@taro/NativeModules实现了一套和火车业务RN原生插件接口一致的类。
-
RN平台通过@taro/NativeModules/index.js引用react-native的 NativeModules
-
WX平台通过
@taro/NativeModules/index.weapp.js引入weapp多端库实现。
以接口请求Network.js为例说明:
-
多端适配前
//RN插件 NativeModules.TRNQunarNetwork 发送请求import { NativeModules } from 'react-native';
-
多端适配后
//路径替换,引用多端文件import NativeModules from '@taro/NativeModules';
-
RN平台@taro/NativeModules/index.js
//RN平台直接导出 react-native 的 NativeModules。外部通过 NativeModules.TRNQunarNetwork 获取原生请求插件import { NativeModules } from 'react-native';export default NativeModules;
-
WX平台
@taro/NativeModules/index.weapp.js
//微信小程序平台导出自定义 NativeModules, 对外接口和RN平台保持一致。外部通过 NativeModules.TRNQunarNetwork 获取WX平台请求接口const NativeModules = {TRNQunarNetwork,//封装了WX端实现类}export default NativeModules;
3.3 方案总结
该方案进一步封装了业务逻辑的多端差异,把NativeModules作为多平台切换的入口点,在各个平台封装一组与原生RN插件等效的功能类(例如 TRNQunarNetwork, TRNNavigator),并确保功能类对外接口在各平台完全一致。
可以完美和react-native的NativeModules数据结构契合,做到各端平滑切换。涉及业务代码的多平台差异被隐藏在NativeModules中,后续产品需求开发时,开发同学仅需聚焦到业务逻辑即可。
4. 项目的难点
4.1 RN转微信小程序严重超Size问题
4.1.1 背景
小程序总包剩余空间不足, 产品需求开发已严重受限。
RN转QTaro项目面临的最大难题是要解决Size问题,如果不能上线微信小程序,多端复用也就失去意义。
4.1.2 解决方案
把所有的RN页面利用QTaro框架转为H5,通过WebView方式加载。
WebView加载方式选择:

4.1.2.1 MPA面临的问题
1. 不能共享内存实例对象: global上注册了很多功能类和数据类,包括共享的ABTest数据,QConfig等。这些数据在各H5页面间不同步。
2. 事件订阅和通知无效: 页面通过EventCenter注册事件订阅,监听某个页面的数据变化。页面无法监听其他页面触发的事件。
解决方法 : 难以修复。火车项目页面>100个,MPA下修复共享实例问题,需要改动的代码量巨大,将会严重阻塞项目进度。
4.1.2.2 SPA面临的问题
1. 多个RN项目是单独打包出多个H5产物。同一个H5产物,页面之间可以共享对象实例,但不同的H5产物之间仍无法共享数据。
解决方法:多个RN项目合并打包,打出一个H5产物。
4.1.2.3 MPA和SPA共同面临的问题
1. 修改WebView的hash方式传参,H5页面被重新reLaunch,页面栈丢失。
解决方法:同域名可以通过local storage传参,跨域下通过后端接口共享数据。
2. 小程序和H5登录态不同步问题
解决方法:外部登录态通过URL传入WebView,保存在WebView的cookie中。
3. 首次资源下载问题过大
解决方法:通过webpack文件分割优化后,首次下载资源1.7M,后续页面下载平均200K。
综合考虑以上因素,选择SPA方案能够更好地解决项目中的问题。SPA在内存对象共享、页面流畅度以及资源利用效率等方面具有优势,且能解决项目中面临的关键问题。
五、项目复盘
5.1 项目的成果
5.1.1 转换工具代码复用度87%
针对所有涉及的RN项目,分别获取起始和结束时的git commit hash(版本提交哈希值),然后进行git diff操作,计算得出两次提交之间代码行数的变化量与总代码行数的比值为13%。
粗略估计转换工具可以节省RN转换整体工作量的 75%。
5.1.2 一套代码多端复用
UI样式效果微信小程序H5对比Adr-RN:


UI样式和功能向RN端看齐, 减少用户多端切换时的学习成本,保证用户各端功能体验一致。
5.1.3 节约人力成本>50%

以3端为例:
-
接入QTaro方案,节省(300%-160%)/300% =47%
-
业务差异封装后, 节省(300%-100%)/300% =67%
-
覆盖的端越多,每端成本越小
以业务线投入4人到非RN端开发计算,统一多端后,年化节约人力 4*235pd( 每人每年pd数 ) = 940pd。
5.1.4 突破小程序Size限制
通过加载单页面H5,解决了RN转WX小程序严重超Size的问题。
转H5后,车次列表页TTI-P90对比如下:

安卓手机,微信原生和微信H5对比


IOS手机,微信原生和微信H5对比


结论:
在IOS/Adr系统上,H5的TTI-P90基本优于微信小程序原生代码。
在解决微信小程序Size问题上,目前取得了阶段性的成功。火车票前端正在深入分析原因,尝试通过加载流程优化,资源预加载等策略进一步提升用户体验。
5.2 不足与展望
5.2.1 组件兼容性
例如: RN部分组件/API在平台兼容性方面存在问题,其中较为棘手的是Animated、PanResponder等与动画手势相关的组件。
H5多端库动画问题, 可以参考开源库 https://github.com/necolas/react-native-web进一步完善。
WX多端库动画问题:
-
WX动画是通过API createAnimation实现, 而RN的Animated由3-4个API配合使,考虑到调用方式以及动画与组件的关联,完全模拟较为复杂。
-
wx.createAnimation依赖于小程序的JS运行环境,受限于小程序的性能和资源分配,尤其是在低端设备上,容易出现卡顿。
解决思路:可以使用CSS动画实现。但CSS不能动态调整动画参数,不适合作为通用的多端库组件,因此需要在平衡动画流畅性和通行性上做取舍
5.2.2 转换工具覆盖度
多端库组件和API,目前主要适配火车RN的项目,未来可扩展支持更多RN生态组件。
5.2.3 样式边缘情况
组件适配的边缘情况的处理不够完善。在抹平组件属性差异的过程中,个别属性差异可能导致特定设备或浏览器上显示异常,需持续测试与完善。
六、结语
本项目提供了一套将RN项目转换为QTaro框架的完整解决方案,涵盖转换工具、多端兼容库与业务差异封装,显著提升开发效率与多端一致性。在迁移过程中,团队积累了丰富的跨端适配经验,也深刻认识到协作与沟通在复杂项目中的关键作用。
未来,火车票前端将持续跟进QTaro框架演进,进一步优化性能与体验,为业务赋能提供更强支撑。
