写点什么

近万字长文详述携程大规模应用 RN 的工程化实践

2020 年 2 月 15 日

近万字长文详述携程大规模应用RN的工程化实践

一、RN 在携程的使用情况

2015 年 3 月 React Native iOS 开源,半年之后 Android 开源。携程于 2016 年 6 月份投入资源在 React Native 框架的预研,并于 8 月份正式上线,至今已有 2 年多。


随着业务使用的复杂度增加,各种问题随之而来,我们就这些问题一一提供解决方案,并建设相关配套系统来支撑业务开发团队使用。本文将从携程内部对 RN(ReactNative 简称,下同)的性能稳定性优化以及相关基础设施的建设来做分享。


截止 2018 年 9 月底,使用情况大致如下:



广泛使用: 生产环境总共有 104 个 RN 业务 Bundle,其中携程旅行 App 中运行的有 83 个,其它 21 个运行在公司内其它独立 App 中,比如 Trip.com、铁友智行等。从 2016 年 8 月份上线至今,PV 以同比 300%的增速增长,其日 PV 量已是传统 H5 Hybrid 技术的近 2 倍。



深度使用: 全流程使用,比如特价机票、特价酒店、国际机票、租车、旅拍等,已是全流程使用 RN 开发。复杂度高,火车票模块,5.8MB 的 js 代码(uglify 压缩后),超过 100 个页面,都打包在一个业务 Bundle 中。


总的来说,RN 在携程已经广泛使用于生产环境,并得到业务和用户的认可。


二、CRN 框架

我们基于 React Native 框架优化,定制成适合携程业务的跨平台开发框架 - CRN,提供从开发、发布、运维的全生命周期支持。



  • 开发框架,主要是提供在开发阶段的支持。包括工具 &文档、组件和解决方案、跨平台打通和代码托管功能。 工具主要包括 CLI 和 Packer,文档包括 API 文档和设计文档,跨平台主要是抹平平台差异组件间的 API,代码托管是为了方便业务团队,特别是新加入 CRN 开发的团队,可以参考已有业务代码快速上手。

  • 性能优化,主要是为了解决首屏渲染的性能问题和 RN 框架的稳定性问题。为了解决首屏渲染性能问题,我们先后开发了框架拆分和预加载、业务按需加载、业务预加载和渐进式渲染方案,稍后会就这些方案做详细介绍。

  • 发布运维,主要是提供发布系统和性能、错误监控平台,让业务开发同事能够有完备的系统去发现和解决线上问题。


下面会从这几个方面详细介绍。


2.1 开发框架

以下是我们的 crn-cli 脚手架,对 RN 原始的 CLI 进行二次包装,提供从工程创建,服务启动,在已集成框架的 App 运行 RN 代码等常用功能,方便开发人员快速上手。


Commands:   init                   建立并初始化CRN工程,可指定appId,默认为携程App   start                  启动CRN服务,默认端口5389   run-ios                运行指定appId的IOS App   run-android            运行指定appId的Android App   run-patch              执行patch,替换CRN修改过的lib文件   cli-update             更新cli版本   example                创建CRN组件和API调用Demo工程   aux                    增强型功能入口:log Server、本地打包、上传开发包等Options:   -h, --help             显示命令帮助   -v, --version          显示版本
复制代码


文档方面,我们提供 API 文档和设计文档



API 文档采用 YUI doc 根据代码注释自动生成,该文档中主要记录新增组件以及使用示例。



设计文档,主要包含一些组件/API 的设计文档,常见问题解决方案,业务开发常见问题都可以再该文档站点找到对应的解决方案。


2.2 组件和解决方案

提供 100 多个业务和公共组件支持,并保证跨平台提供一致 API。



三、CRN 性能优化

在具体介绍性能优化方案之前,先看 2 段 Demo 视频,两段视频是同一份代码的运行效果,一份使用原始版本 RN 打包运行,另一份使用 CRN 打包运行。选择同一台测试机(2015 年老款 SumSung S6 Edge+,Android7.0 系统),为确保环境尽可能一直,在每次运行 Demo 前,均清空所有后台程序。


https://v.qq.com/x/page/z081264pygu.html


原始版本 RN 运行 Demo


https://v.qq.com/x/page/n08125f8cs1.html


CRN 优化后运行 Demo


分享具体性能优化措施前,先来解释几个基本概念。


  • React Native 打包是符合 commonjs 规范的,参考下面的代码:


// moduleA.jsmodule.exports = function( value ){return value * 2;}
// moduleB.jsvar multiplyBy2 = require('./moduleA');var result = multiplyBy2(4);
复制代码


简单地说,模块必须通过 module.exports 导出对外的接口或者变量,通过 require()导入其他模块,并同步加载该导入的模块。


  • define


简化版 define 实现如下


function define(moduleId, factory) {    if (moduleId in modules) {        return;    }    modules[moduleId] = {        factory:factory,        hasError:false,        isInitialized:false,        exports: undefined    }}
复制代码


可以看到 define 仅仅是将模块代码嵌入到 factory 中,cache 到 modules 对象内部,并未真正执行。


  • require


简化版 require 实现如下:


function require(moduleId) {var module = modules[moduleId];return (module && module.isInitialized)?module.exports:    guardedLoadModule(moduleId, module);//代码执行,并赋值给module.exports}
复制代码


可以看到,require 是真正模块代码执行的点,JS 模块数越多,耗时越长。guardedLoadModule 内部会使用 try/catch 包裹去执行模块代码,此处可以捕获所有模块的代码异常,RN 内部的 js 错误,都是从此处抛出。


  • import


import 在 bundle 编译前后的示例代码如下:


/*源码*/import page1 from './src/Page1.js'import page2 from './src/Page2.js'
/*编译后*/var _Page = require(662); //662=./src/Page1.jsvar _Page2 = _interopRequireDefault(_Page);var _Page3 = require(663); //662=./src/Page2.jsvar _Page4 = _interopRequireDefault(_Page3);
复制代码


简单地说,编译后 import 等价于 require。


  • 页面加载流程



以上是一个 RN 页面加载的全流程,首选是 Native 容器的创建,接着是下载安装最新包(如果有的话),之后开始 CRN 框架(包含 Native 和 JS 组件)加载,框架加载完成之后,加载业务代码,计算页面虚拟 dom,通知 Native 进行页面首次渲染,如果有网络请求,请求完成之后,再次渲染。


灰色部分是可选的,真实 RN 页面的渲染性能包含 4、5、6 三部分,针对这三部分,我们提供了不同的性能优化方案。


  • CRN 框架加载:框架和业务代码拆分、框架代码预加载、JSC 执行引擎缓存

  • 业务代码加载:业务代码按需加载、业务代码预加载

  • 业务页面渲染:渐进式渲染、骨架图预渲染


接下来我们一一介绍。


3.1 CRN 框架加载的优化

先看下 react-native bundle 命令打包之后的 bundle 文件结构


//头部 - 全局变量定义(function(global) {global.require = _require;global.__d = define; /*...code... */})(typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self:this);
//中间 -- 各模块定义部分__d(/* demo/index.ios.js */function(global, require, module, exports) { var _React Native = require(12); // 12 = react-native var theCompnent = require(524); // 524 = ./main _React Native.AppRegistry.registerComponent('Demo', function () {return theCompnent;}); module.exports = theCompnent; }, 0, null, "crn-demo/index.ios.js");
__d(/* react-native-implementation */function(global, require, module, exports) { var React Native = {/*...code... */} module.exports = React Native; }, 12, null, "react-native-implementation");
// 尾部 -- 引擎初始化和入口模块执行;require(50); //50为InitializeJavaScriptAppEngine模块;require(0); //0为入口Component模块
复制代码


结构可以简化为三部分:


  • 为头部全局变量定义;

  • 中间框架/业务模块定义;

  • 尾部引擎初始化/入口函数调用;


3.1.1 框架和业务代码拆分

先来看看我们打包之后的文件目录结构


//框架包rn_common目录结构rn_common       ├── common_android.js  //Android CRN框架代码,包括RN+CRN扩展JS组件+常用第三方组件    ├── common_ios.js      //iOS CRN框架代码,包括RN+CRN扩展JS组件+常用第三方组件    └── pack.config        //打包日志文件,记录打包时间,RN版本,App版本等信息
//业务包rn_flight_booking目录结构rn_flight_booking ├── _crn_config_v2 //配置文件,记录业务代码所在文件夹,默认是js-modules,同时记录业务代码入口模块文件名 ├── _crn_unbundle //CRN打包格式标识文件,该文件存在时候,才当做CRN包格式加载 ├── assets/ //图片资源目录,定制过资源打包/加载流程,iOS/Android目录一致 ├── fonts/ //字体文件目录,每个js模块一个文件,文件名为模块ID.js ├── js-diffs/ //Android和iOS平台差异代码,Android优先加载该文件夹中的业务代码 ├── js-modules/ //业务js代码目录 └── pack.config //打包日志文件,记录打包时间,RN版本,App版本等信息
复制代码


rn_common 为框架包,可以再后台线程加载,业务包在进入业务的时候才开始加载。


打包部分:


  • 生成框架 jsbundle


业务代码拆分主要是把中间框架/业务模块定义给拆分开来,拆分的思路很简单,用一个空白页面作为入口点,AppRegistry.registerComponent 加载这个入口点。进入业务时,通过这个入口点页面去加载真实的业务代码。把这个空白的入口点页面作为框架的一部分,通过 react-native bundle 命令打包成框架 jsbundle。


  • 抽取业务 js 代码


对 React Native unbundle 的打包过程进行定制,首先让 iOS 支持 unbundle 打包(默认是不支持的), 将生成的业务 js 模块代码单独保存,每个 js 模块一个文件,文件名即为模块 ID.js;


  • js 模块加载优化


空白页面入口组件,要能加载(require)真实的业务代码,我们需要改造 RN 的 require 方法,简单修改 Native SDK 中的 JSCExecutor(RCTJSCExecutor.mm/JSCExecutor.cpp)文件,调整 nativeRequire 实现即可。


3.1.2 框架代码预加载

RN 框架 instance

RCTBridge/ReactInstanceManager(后文统称为 instance)是 RN 框架中核心的 2 个类,这个类分别控制不同平台的 JavaScriptCore 的执行,同时又都是各自平台 ReactView 的属性,View 的显示于事件靠它来驱动。


所以为了能做到后台预加载 js 代码,首先要做的就是解开台 ReactView 和 instance 之间的耦合解开,能让 instance 在后台独立加载。处理起来不复杂,只需要对 ReactRootView/RCTRootView 接口做简单调整即可。



上图是我们定义的 CRN 框架 instance 的生命周期状态:


  • 框架加载过程,标记为 Loading 状态

  • 框架加载完成,标记为 Ready 状态

  • 框架引擎被业务使用,标记为 Dirty 状态

  • 框架在加载或者业务使用过程中出了异常,会被标记为 Error 状态


App 启动,我们就会预创建一个框架引擎的 instance,创建完成,状态标记为 Ready 并缓存起来,进入业务时候,会优先使用这个缓存的 instance 去加载业务代码,这个时候进入业务页面,只有业务代码的加载执行时间。当这个缓存的 Ready 状态的 instance 被使用之后,后台立即再创建一个,以备后续业务使用。


根据线上数据统计,我们发现 95%的场景,都能直接使用到后台预创建好的框架 instance,或者是已经加载过业务的 instance。也就是说,进入业务页面,只有 5%的用户,需要耗时间加载 RN 框架代码。


3.1.3 业务 instance 缓存

对于加载过业务代码的框架 instance,在用户离开业务时候,会暂时缓存住,这样如果重复进入页面,少了业务代码的加载执行,打开速度提升明显。当暂存的加载过业务的 instance 数量超过 2 个时,会按照创建时间顺序,回收掉最早创建的 instance。根据线上数据统计,有 15%的场景,都会使用到的加载了业务代码的 instance。


框架代码的加载优化已基本完成,来看我们当时测试的一组数据。


3.1.4 一组数据


上图是 2016 年 10 月,基于 RN 0.30 版本,在 iPhone 6 和 Sony Xperia Z5 机型上,多次测试的平均数据。可以看到,优化后,首屏时间比原来都减少 45%左右。后续我们升级 0.41,0.51 版本,该优化都一直在做,方案和思路都是一样的。


3.2 业务代码加载优化

业务代码加载优化我们主要从 2 个方面考虑,业务代码按需加载和预加载,先简单解释两者的差别


按需加载:是进入业务模块时候,只加载对应页面的代码


预加载: 是尚未进入业务模块前,即把需要进入业务页面的代码在后台加载执行掉


3.2.1 业务代码按需加载

LazyRequire 按需加载方案


先来看一段我们初始化页面路由表的代码


import PageA from ("pages/PageA");import PageB from ("pages/PageB");import PageC from ("pages/PageC");import PageD from ("pages/PageD");
//设置页面路由表let pageList = [PageA, PageB, PageC, PageD];App.startApp(pageList);
复制代码


早期业务简单,页面数量少,上面的优化方案已经可以是 RN 基本达到 native 的体验,但是随着业务越来越复杂(当时有业务 bundle,包含 70 多个 Page js 代码 uglify 之后达到 3MB),首屏加载慢的问题又出来,为此我们实现一种懒加载的方案,进入业务时候,只加载当前需要显示的 Page 的代码, 对业务的使用非常简单,下面是我们懒加载的页面路由代码写法。


const PageA = lazyRequire("pages/PageA");const PageB = lazyRequire("pages/PageB");const PageC = lazyRequire("pages/PageC");const PageD = lazyRequire("pages/PageD");//设置页面路由表let pageList = [PageA, PageB, PageC, PageD];App.startApp(pageList);
复制代码


对业务开发来说,切换成本非常低,只需要使用 lazyRequire 函数替代 import 指令。怎么做到的呢,其实也很简单。


//LazyRequire函数定义,返回lazyModule对象LazyModule lazyRequire(path)
LazyModule = { load(); //代码真正执行的点,返回执行结果}
复制代码


细心的同学可能发现这里有个问题,lazyRequire 函数传入的文件相对路径,打包之后,还是相对路径,而打包完成之后,每个业务 js 模块都被打成模块 ID.js 文件,这会导致运行时查找不到这些业务页面的模块。是的,在打包过程中,需要开发一个 babel 插件,将 lazyRequire 函数例的文件路径,转换成模块 ID,实现方式和 import 的 babel 插件基本一致。


随着业务代码增加,进入首屏需要加载(require)的代码会增加,前面分析过,require 会导致 JS 代码的执行,是耗时的操作,最终导致首屏变慢。所以,我们就想,进入业务的时候,只加载第一个 Page 相关的代码,其他页面的,路由跳转过去的时候再加载。


Getter API 导出模块

我们先来看看 React Native 模块内的组件导出方式:


//原始代码如下//Module1.jsconsole.log("Start load module1");module.exports = {doJob:()=> {console.log("doJob called in module1");    }}
//Module2.jsimport Module1 from "./Module1";//执行结果:Start load module1
复制代码


这是最常见的模块导出和引用方式,和我们前面说的一样,import 的时候,实际上会去执行对应的代码。接下来,我们创建一个 common.js(文件名无限制),修改下模块的导出方式,参考下面的代码。


//common.jsmodule.exports = {get Module1() {return require('./Module1');    }}
//回到Module2.js的引用import Module1 from "./common";//执行结果:没打印任何日志
Module1.doJob();//执行结果: 打印以下两条日志//Start load module1//doJob called in module1
复制代码


可以看到,通过 ES5 的 getter API 来导出模块,在引用时,代码不会立即执行,直到导出对象真正使用时候,才开始执行。所以如果我们有自己的公共组件,多个业务都需要用到,那么使用 getter API 导出模块是一种不错的选择。其实 RN 里面的 ReactNative 模块导出方式也是这样,参考下面的代码。


const ReactNative = {get ActivityIndicator() { return require('ActivityIndicator'); },get ART() { return require('React NativeART'); },get DatePickerIOS() { return require('DatePickerIOS'); },get DrawerLayoutAndroid() { return require('DrawerLayoutAndroid'); },get Image() { return require('Image'); },get ListView() { return require('ListView'); },//...}module.exports = React Native;
复制代码


通过 getter API 导出模块实现按需加载是 ES5 默认支持的,对原始 RN 没有任何侵入性修改,是比较推荐的一种方案。


那我们为何需要 LazyRequire 呢?很明显,使用 getter API 导出替换 LazyRequire 是可行的,只是达到不了按需加载的功效了,因为在赋值页面路由表的时候,需要用到所有的 Page 对象,用到这些对象的时候,会直接触发所有 Page 的代码加载执行。


inlineRequire 方案

方案很简单,预先定义模块对象,赋值为 null,在使用时候判断对象是否为 null,null 时候则做真正的 require,进行模块加载。看一段简单示例代码。


let VeryExpensiveModule = null;
export default class Optimized extends Component { someEvent = ()=>{ if (VeryExpensiveModule == null) { //require('path').default, 动态加载模块代码 VeryExpensiveModule = require('./VeryExpensive').default; } }}
复制代码


3.2.2 工具和数据

为了能方便业务开发同事快速定位到具体是哪个 js 模块加载耗时长,以及具体的调用链是怎样的,我们开发了 CRN Require Profile Tool



如上图所示,业务开发同学,很容易就发现是哪个模块加载耗时长,需要使用按需加载。


按需加载方案是 2017 年,基于 RN 0.41 版本开发的,当时上线前我们也做过首屏性能测试, 数据是 iOS 模拟器上跑出来的,由于首次进入业务加载的页面数量猛降,所以首屏时间减少了 2/3。由于这个优化是在 JS 层做的优化,iOS、Android 性能提示基本一致。



3.3 业务代码预加载

经常有这样的业务场景,A 流程订单完成之后,有 B 产品推荐,A、B 业务代码在不同的 RN bundle 里面,A 业务开发完,希望能把 B 业务在后台加载掉,这样用户打开 B 业务首屏速度会更快。为此,我们提供了业务预加载方案。主要两个点,预加载和缓存。


预加载有前面框架代码拆分和预加载的基础,实现起来非常简单,基本没有改造成本。为了能让尽可能多的代码实现预加载,我们在 LazyRequire 里面添加逻辑,让在预加载状态模式下,LazyRequire 等价于 Require,强制加载。


缓存,先前业务代码的缓存是按照业务的 URL 作为 key 来存储的,预加载模式下为了尽可能提高缓存的命中率,我们将缓存的 key 统一成业务 bundle 名,同一业务,同一缓存,这么操作需要业务开发代码也要注意,避免全局变量的使用。


缓存的另外一个问题就是内存占用,我们在提供业务预加载的时候,用一个全局数组来缓存业务 instance,超过限制,或者内存警告时候,会按照 LRU 策略清理没有使用的 instance。实际测试下来,Android 平台,预加载一个业务,会增加 2MB 左右内存(包括框架和业务代码都加载完),而渲染一个正常页面,占用约 20MB 内存,其中最主要的内存被图片占用。


先前同事在开发这个方案的时候我没在意性能数据,简单测试了下,发现效果非常不错,对于一般页面,业务代码提前预加载后,性能可以达到和 native 基本一致。我们使用了荣耀 7X(千元机,性能偏中低端)进行测试,已经基本感知不到首屏加载和 native 有什么差别了。


3.4 业务页面渲染

我们发现,随着页面复杂度增加,渲染耗时逐渐增加,这也可以理解,要完成页面渲染,需要计算 vitrual dom 的 diff,传输数据给 native,如果数据传输有延迟,就会出现掉帧,为了让页面尽可能快的显示,我们需要简化首次渲染。


渐进式渲染


策略很简单,先渲染 header 部分,setTimeout 去渲染其余部分,如果是 listview/scrollview,先渲染屏幕可视区域,在滑动时候,再渲染其他区域。下面一个 demo 视频,我们看下。


https://v.qq.com/x/page/u08125tcr7w.html


骨架图


先渲染骨架图,由于骨架图相对简单,渲染很快,待请求数据返回后重新渲染界面。骨架图目前没有好的自动渲染框架,需要页面开发同学,根据页面样式,自行开发。


四、发布与运维

一个成熟完善的开发框架,是需要各种配套系统支撑的。我们也为 CRN 开发框架提供了多个内部系统,下面来介绍其中的主要几个。


4.1 发布系统


上图是我们的发布系统页面截图,除了常规的按照版本/平台/环境发布、灰度、回滚支持,我们还增加了发布结果和实时到达率的的报表,方便发布之后,对发布效果评估。


几点说明


1、不同环境,按照顺便发布,首先发布 FAT(开发环境)、测试通过再发 UAT(跨业务测试环境)、测试通过再发 PRD(生产)。真正的打包只在第一个环境打包,后续的环境都是直接发布前一环境的打包产物,避免重复打包导致的不一致问题,同时也提高发布效率;


2、跨 RN 版本,不支持同时发布,避免新版本 RN 代码发布到老的 RN 版本上,直接在发布系统选择版本的时候做了控制,不能选择 2 个不同 RN 版本的 App;


3、控制发布版本数量,创建发布单时候,可以选择多个版本,经常有发布的同学为了简单,一键勾选所有版本,实际上老版本可能用户量非常小,而回归测试却覆盖不到所有版本,为了避免老版本因为测试不重复导致的问题,我们将版本选择功能做了优化,按照 UV 数量排序,并在版本后面显示 UV 比例,同时默认只能选择 Top5 版本,如果要发布更多版本,需要点击更多,展开其他版本。


监控指标


1、发布结果:发布之后,分平台、App 版本展示下载到这个包的成功、失败次数,以及失败的原因分布。


2、实时到达率:这个是业务最应该关注的数据,数据直观的展示,发布之后,实时的有多少比例的用户已经用到最新包。



为了提高实时到达率,我们在打包过程中记录业务模块 ID 和文件名之间的映射,这样可以避免新增文件出现的的大量 JS 文件的文件名(即为模块 ID)变化,从而导致的差分包过大问题。做到只下发真实变更和新增的文件内容。通过线上数据分析,所有首页入口的 RN 模块,新版本发布之后,有 85%的实时到达率,二级及以上入口,实时到达率可以达到 97%。


4.2 性能报表

统计线上业务首屏加载的耗时趋势、分布和使用量,可以支持按照 App/版本/系统过滤查看。



首屏首次渲染完成的时间点,可以在在下面 2 个点添加事件,抛给外层统计。


//基于RN 0.51版本//Android ReactRootView.java 添加dispatchDraw方法protected void dispatchDraw (Canvas canvas){//相对准确,可能会调用多次,内部要做好判断}
//iOS RCTRootContentView.java- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex{//准确,RN自带的profile工具里面的TT时间,也是以此处为结束点}
复制代码


4.3 错误报表

用于收集客户端上报的 RN 错误,包括 JS 执行异常,或者是 native runtime 的一些异常,在业务模块发布之后,必须要到此平台确认自己的发布稳定性是否正常。


除了常规的版本、业务、平台功率,我们在错误堆栈详情页面,还将当前出错的业务包版本和打包记录关联起来,方便开发人员排查问题。



五、其他实践经验

5.1 版本升级

从 2016 年 8 月至今,总共更新 0.28-0.30-0.41-0.51 四个官方 RN 版本,除 0.28 是调研阶段仅使用两个月,其他都使用半年以上。整体升级相对可控,除 0.41 升级 0.51,因为有 PopertyType 组件的移除,需要业务做些适配,其他版本升级对业务都是基本透明的,仅需常规回归测试。


升级流程上,首先是框架团队前期验证(包括打包,SDK 定制,发布,监控全流程确认)、制定升级方案和时间点,接下来是业务团队配合升级和新版本发布,最后是框架团队确认所有业务都在新的 RN 版本重新打包发布过。


升级成本来说,框架团队大约需要 3 名工程师(iOS/Android/前端各 1 人),2-3 周时间,业务升级和回归测试,一般可在一周内完成。


升级频率上,由于使用的业务团队太多,频繁的升级会对业务造成影响,为了尽可能对业务开发友好,大约 8-12 个月会升级一个 RN 重要版本。当然,如果是有重大的性能升级,比如 RN frabic 的重构版本,我们也会第一时间跟进升级。


5.2 第三方组件版本管理

先看看 npm 模块的版本规则:major.minor.patch, package.json 支持模糊版本,比如>,>=,<,<=,~,^,.x,*, 其他都比较好理解,~,^简单解释下(完整本的版本说明参考 semver )


//举例说明~0.2.0 匹配 [0.2.0, 0.3.0), 有minor, 最大版本为minor+1, major不变,patch为0~0     匹配 [0.0.0, 1.0.0), 无minor, 最大版本为major+1, minor,patch为0^0.2.0 匹配 [0.2.0, 0.3.0), 最大版本为左侧第一个不为0的版本号+1^1.2.0 匹配 [1.2.0, 2.0.0),
复制代码


我们再看下 react-native-recyclerview-list 这个组件, 组件版本和依赖的 RN 版本关系如下。



如果我们使用的 RN 是 0.47 版本,对这个库的依赖方式写成^0.2.0, 当组件版本发布到 0.2.2 时候,都使用的很正常,一旦 0.2.3 版本发布,如果再打包发布,则会出现不兼容问题,线上会出大量 JS 报错。


我们就在生产环境出现过类似问题。为了避免类似问题,我们在打包之前做了 preBuildCheck,检测第三方组件的依赖版本,凡是不使用固定版本的,直接报错。


5.3 分平台打包

目的是抹平组件的平台差异,解决资源加载路径不一致的问题。很长一段时间,我们 iOS/Android 的业务代码,只打一次包,以 iOS 平台打包。因为涉及到 Native 代码的新组建的引入,都是由框架团队控制,所以一直以来都没出什么问题。直到公司内部独立 App,他们引入的第三方组件 iOS/Android 有差异,导致发布之后在 Android 上运行有问题。


分平台打包之后,先打包 iOS,再打包 Android,将差异代码存储在 js-diff 目录,加载时,Andorid 先在 js-diff 中查找模块,查找得到直接使用,如果查找不到,再在默认的 js-modules 文件夹中查找。iOS 则只在 js-modules 文件夹中进行模块查找。


5.4 稳定性优化

iOS 平台相对简单,注意解决以下两个 API 相关问题后,绝大部分问题都好处理。


//自己注册错误handler,在此处去进行日志上报,并持续优化void RCTSetFatalHandler(RCTFatalHandler fatalHandler);
//iOS所有错误都是通过此次抛出void RCTFatal(NSError *error);
复制代码


iOS 所有错误都是通过此次抛出 void RCTFatal(NSError *error); ``` iOS RN 注意事项:


  • 必须要自己注册错误处理 handler,否则一旦有 RCTFatal 抛出错误,生产环境会有 Crash

  • 所有的错误都是 RCTFatal 抛出,为了方便排查问题,需要记录 error 的来源


Android RN 相对复杂,主要注意事项:


  • so 加载失败。简单处理可以在原有的 LoadLibrary 加上 try/catch,并在 catch 中再 load 一次,能大幅度降低该问题导致的 Crash;

  • ReactInstanceManager 创建过程中的 Native 异常,是通过 DevSupportManager 传递出去,需要处理 DefaultNativeModuleCallExceptionHandler 的 handleException 方法

  • JS 执行出错,都是通过 ExceptionsManagerModule 模块抛出,所以需要将该错误和 ReactInstanceManager 相关联,并抛给上层;

  • libjsc.so Crash,如果有做 Native Crash 收集,会在后台系统看到不少 libjsc 相关报错,这是由于 RN 自带的 JavaScriptCore 版本为 2014 年的版本,兼容性和稳定性较差,建议参考开源的 jsc-android-buildscrips 项目,将 JavaScriptCore 升级到 2017 年 11 月的版本(WebkitGTK Revision 225067),我们升级到该版本后,发现该错误降低了 90%;


六、总结

CRN 框架对原生 RN 的大量底层改造优化,解决了性能和稳定性两大核心问题,从落地效果来看,其性能可以做到和 Naitve 基本一致水平,而开发成本却大幅降低。


CRN 框架已在业务团队中广泛使用,为业务的快速迭代提供了强有力支持。对于规模化业务开发团队,使用 RN 作为跨平台开发的解决方案,是切实可行的选择。


2019 年,我们计划根据开发资源情况,适时开源 CRN 框架的部分模块。


作者介绍


赵辛贵,携程无线平台研发部开发总监。2013 年加入携程,主要负责 App 基础框架研发相关工作,目前重点关注 React Native 技术在公司的推广和研发支持、无线框架和工程架构升级。


本文转载自公众号携程技术(ID:ctriptech)。


原文链接


https://mp.weixin.qq.com/s/Z1GUJW3qBqDGH1jnGt5qAg


2020 年 2 月 15 日 17:28374

评论

发布
暂无评论
发现更多内容

推荐5个4K视频下载网站 (百万优质资源)

科技猫

网站 分享 视频 经验 资源分享

跳出CRUD!阿里大牛熬夜整理71W字性能优化全解究竟有什么魅力?

程序员小毕

Java 程序员 架构 面试 性能优化

怎样才能拿到高薪?JDK掌握的集合类库知识梳理,网友大喊666

java专业爱好者

Java

ShutdownHook原理

捉虫大师

Java

开发环境上云,打造五星级开发体验

CODING DevOps

Kubernetes 云原生 CODING Nocalhost

2021年处置非法集资部际联席会议:密切关注打着区块链、虚拟货币等旗号的新型风险

CECBC区块链专委会

阿里“秘密团队”整理出来的一份Java面试复盘手册!全面复盘在望

Java架构之路

Java 程序员 架构 面试 编程语言

真的香!Github一夜爆火被各大厂要求直接下架的面试题库也太全了

Java架构之路

Java 程序员 架构 面试 编程语言

LiteOS内核源码分析:动态内存之Bestfit分配算法

华为云开发者社区

LiteOS Huawei LiteOS 动态内存 Bestfit 分配算法

网络协议学习笔记 Day2

穿过生命散发芬芳

网络协议 4月日更

阿里P9大佬,首次分享SpringBoot整合MybatisPlus笔记,我跪了

java专业爱好者

Java

理性看待区块链+大宗商品

CECBC区块链专委会

区块链

如何从零开始学Python:(7)如何解决发布和上传代码过程中遇到的问题?

广之巅

Python 四月日更

全网最全 ECMAScript 攻略

清秋

JavaScript ecmascript 前端 ES6 Ecma

自动源代码质量度量(ISO/IEC 5055)

Tom(⊙o⊙)

软件质量 静态分析

欢迎 ProForma 的制造商 ThinkTilt 加入 Atlassian 的大家庭!

Atlassian

敏捷 esm ITSM Atlssian JiraServiceManagement

这份阿里P8技术专家整理的《一面到底》Java岗,GitHub已标星79k

Java架构之路

Java 程序员 架构 面试 编程语言

世界读书日,爱奇艺ers的技术产品书单

爱奇艺技术产品团队

读书

当区块链遇到工业互联网,浪潮云洲链正在那里

浪潮云

云计算

4种语义分割数据集Cityscapes上SOTA方法总结

华为云开发者社区

语义分割 OCR 数据集Cityscapes HRNet SegFix

算法速成!字节强推2021最新Leetcode刷题笔记Github全面开源,卷就完了!

Java王路飞

Java 程序员 架构 面试 算法

大学四年,学了这些SQL攻击与防御,成为了别人眼中的大神

Machine Gun

sql 网络安全 渗透测试

马丁策略倍投软件开发,量化倍投系统

13823153121

千人万面奇妙自见:爱奇艺短视频推荐技术中多兴趣召回技术的演变

爱奇艺技术产品团队

推荐 短视频 模型 召回

控制事务结束后的行为

在即

四月日更

暴涨暴跌的牛市,普通人怎么和平发育?

CECBC区块链专委会

区块链

总是记不住java的IO流用法?用N个问题教你掌握java IO流

华为云开发者社区

Java 字符串 IO流 字节输入流 字符流

Flink的状态一致性

大数据技术指南

flink 4月日更

【得物技术】得物分布式UI自动化实践

得物技术

测试 UI 质量 自动化测试 得物技术

苹果(Apple Watch)手表使用必知必会19条

Flychen

苹果手表 IWatch Apple Watch

插件化库VirtualAPK详解

寻找生命中的美好

android 插件化 VirtualAPK

2021年全国大学生计算机系统能力大赛操作系统设计赛 技术报告会

2021年全国大学生计算机系统能力大赛操作系统设计赛 技术报告会

近万字长文详述携程大规模应用RN的工程化实践-InfoQ