去哪儿网 QRN 兼容升级方案

1,753 阅读15分钟

作者简介: 汲国兴,2018 年 7 月加入去哪儿网。曾致力于小程序工程化建设,完成小程序发布上线全自动化。现专注于 qrn 框架开发。

1 前言

React Native 0.63 已经发布,为我们带来了一些非常令人兴奋的新功能的同时,也让人头疼的,因为升级它并不是那么容易。

尤其是遇到大版本更新,Java、iOS 和 Android 三端的配置构建文件都有非常大的变动,有时候三者的配置文件又互相耦合在一起,往往牵一发而动全身。

如果还想做完美兼容旧版升级,那更是难上加难啊。去那儿刚刚结束 0.63 的升级, 这里就来分享下升级的过程吧。

2 QRN 是什么?

react-native 目前是去哪儿网跨端最佳方案,已是去哪儿网客户端的主要技术栈. qrn-js 是在 react-native 基础上开发的一个专属于去哪儿的 rn 框架。

qrn 的核心功能包括但不限于对 rn 原生的代码修改加工,兼容双端的组件和 api,自定义的组件和 api 等。

同时也支持 Redux,webx 等功能。QRN 现可支持全业务线开发,已成为去哪儿大前端的关键一环。

在业务线如此繁多的情况下,为了业务之间解耦,我们将 JS 端划分成 N 个业务包和一个框架包,业务包和框架包有各自的打包脚本,每个业务包也都可以独立发布。

一个完整的 js 环境都是由各个业务线的业务包加框架包组成,互不干扰。但是如果每一个业务包都依赖不同版本的 qrn-js,这就会产生多份重复的 qrn-js 代码。

为了防止这种情况的发生,我们将 qrn-js 内置的到客户端中,跟随客户端版本发布。当启动一个项目时,客户端会先加载内置的 qrn-js,然后再加载业务 js。

这样业务包内就可以没有任何 qrn-js 代码了。

也就是说,无论业务线依赖的哪个版本的 qrn-js,实际上运行的都是当前客户端内置的 qrn-js 版本。虽然 qrn-js 是跟随客户端版本内置的,但同样也具备热发能力。

除非很紧急的情况,不然都会内置客户端发布,这样就需要做好版本兼容性。兼容性在 qrn-js 迭代的过程中倒是比较好办,但是再升级 rn 版本后就要难办的多了。

比如说 rn 有一个小改动,影响了全部的业务代码,就需要所有业务线去改代码,这样会消耗很多人力成本。但是如果可以在 qrn-js 中进行兼容适配,那么就会省去很多业务线的工作量。

所以说兼容升级很重要,每次升级我们都会优先考虑兼容升级。这次升级是从 0.61.3 升级到 0.63.2,那么接下来就介绍下本次升级的主要内容以及兼容升级是如何实现的。

3 为什么这么麻烦还有更新?

为什么要定期更新 React Native?主要如下:

  • 可以获得所有最新的性能改进
  • 提高健壮性和稳定性
  • 可以使用新的 API 和功能
  • 紧跟最新的重大变化

考虑到这些,还是有必要定期更新的。下面看下这次我们兼容升级的主要过程吧:

4 更新内容

我们先是通过翻阅 61 到 63 所有版本的更新日志和 commit,将其收集归类,再经过评估后,整理出本次升级需要着重关注的更新。以此来确定更新影响范围和制定兼容方案。我们将内容整理到 wiki 上,也可让各业务线开发快速了解更新内容。

这里就挑了几个更新拆解说明一下。

  • 提高开发调试效率

全新的错误提示框 LogBox 这个新的 Logbox 的体验要比之前的好上很多,之前的调用栈很难读,基本上都是靠错误信息来分析错是什么。特别是组件内报错,因为没有页面栈,经常要花上好一会时间来定位。

在新的 Logbox 中得到了很好的改善,如果是组件内报错 logbox 是可以定位到组件内的某一行,调用栈完全可读,点击之后还可以让你的 VSCode 打开那个组件的那一行,比之前要方便太多了。

这个功能只需要打开 dev 模式就可以体验了。rn 分为 dev 和 release 两种模式,他们各自有一套执行代码, dev 模式下 rn 会开启代码规范校验,和废弃的属性和方法提示,升级的过程过程中可以提示更新代码,有些业务线在 dev 模式下开发少,所以本次升级时会报出部分写法错误。以后的开发我们也推荐开启 dev 模式。

  • 核心包瘦身:将部分组件从核心包抽出到社区库

这个 61 版本的时候 rn 已经开始做了,这次抽出的数量比之前的要多。它的好处除了减少核心包的大小外,这些组件还可以独立更新,不再需要跟着 rn 版本更新。对于我们来说,以后还可以减少一些升级的压力。

  • 提高运行速度:持续移除 PropsTypes

rn 计划在编译阶段去做属性类型合法性和正确性的检测,而不是在运行时。开始移除 PropsTypes 的使用,将全面使用 flow。移除了运行时检测,运行速度会有一定的提升。

  • 新的交互组件:Pressable

新的交互组件在未来将替代目前可以进行交互的组件:

Button, TouchableWithoutFeedback, TouchableHighlight, TouchableOpacity, TouchableNativeFeedback, TouchableBounce。

新核心组件 Pressable,可用于检测各种类型的交互。提供的 API 可以直接访问当前的交互状态,而不必在父组件中手动维护状态。它还可以使用各平台的所有功能,包括悬停,模糊,聚焦等。rn 希望开发者利用 Pressable 去设计组件,而不是使用带有默认效果的组件 如: TouchableOpacity。

  • Native Colors (PlatformColor, DynamicColorIOS)

可以通过字符串直接访问 native color。PlatformColor 允许用户使用 iOS 或 Android 设备的本机颜色。DynamicColor 允许用户根据手机的外观模式(亮模式 / 暗模式)配置颜色。

  • 其他

其他的更新就是废弃组件,属性,方法及已知 bug 修复等等。

这个阶段 RN 再不断的优化开发调试的体验,react-devtools 也得到了很好的升级。接下来就要开始制定兼容升级方案了,那么兼容升级都会带来哪些问题呢?

5 兼容方案

5.1 适配核心包瘦身

rn 在 0.61 版本的时候就已经开始将一些组件从核心包中抽离了,抽离的组件和原来的引用方式完全变了,原来是从 react-native 中导出的,现在是从 react-native-community 中导出的。

如果这块不兼容的话,业务线需要把引用代码全部改成从 react-native-community 引入,这样显然是不行的,我们先是分别在 js 端和客户端做个升级前后的对比,把只是从 rn 核心包移出且没有较大改动(使用方法,属性,api 没有变化)的组件整理出来,

将这些组件做了一个重定向,可以像以前一样从 react-native 引入,qrn-js 会重定向到 react-native-community 中,在 5.0 之后的版本,业务线使用的包都是 react-native-community。如果业务线需要更新某一个库的版本时,需要跟我们说下。

我们会同时更新 js 端和客户端的版本,保证同步。这其中有一些已经从 RN 中删除了引用入口,我们也是要在修改源码把他们加回来。这样业务线就不需要修改代码的情况下也做到兼容了。

代码示例:

这里还有个组件更新不好处理。

就是 RN 将 TimePickerAndroid, DatePickerIOS, DatePickerAndroid 整合成了一个组件 @react-native-community/datetimepicker,改变了写法,用 mode 参数区分是 date 类型还是 time 类型。

这样就不能直接将这三个都指向到新的 datetimepicker 了,开始我们想的是做一个中间组件,在这个组件里面去做一些适配处理,后来花了些时间仔细看了新 datetimepicker 的代码,除了可以不用在区分平台引用外,也就只是做了结合,而且最终的组件的效果也没什么差别。

所以就决定不使用新 datetimepicker,还是使用原来的组件,这样业务线也不需要再对此做代码适配,我们也把这个建议放到了文档中,如果哪个业务线对新 datetimepicker 感兴趣,可以直接使用新的就好。这样影响也可以控制到最小。

5.2 解决调用没变,但是实现变了的组件

比如 Image 组件,原 image 组件使用 native 的 ImageViewManager,现新版换成了 NativeImageLoaderIOS,这种也需要修改源码解决。

因为这个只有在新版客户端才能引用 NativeImageLoaderIOS,优先去引用新版本的模块,如果找不到新版模块, 默认就使用旧版本的。

像类似这种的更新,可能都会用这种方式来做兼容。

5.3 解决官方留下的问题

1、升级后发现所有的 Touchable 组件点击后的效果都不正常了,点了之后会一直灰,过了一会才会恢复正常,之前觉得可能是开启 dev 模式后无动画效果的问题,但是关了 dev 模式也是这样。

我先是 diff 了一下升级前后的代码,没发现什么问题,然后又读了下新的代码,在 Pressability 里找到了问题,升级后常量 DEFAULT PRESSDELAY_MS 设置成了 130ms,所以才会有点击后的效果延迟的问题,将这个参数改回了 0ms,也就恢复到了正常效果。

后来我也在 0.64 版本发现官方修复了这个问题。

2、iOS 弹出 LogBox 后导致页面无法交互,这个问题的现象是当 Logbox 弹出后收起 Logbox 页面仍无法点击,这个我们从 js 端到 native 端查了很久,后来 iOS 同学查看 native 页面层级发现这种情况是出现在同时加载两个或多个 hybirdid 的项目,且同时触发弹出 Logbox 后,就会出现这个问题。这个 Logbox 在 iOS 中是 window 层级的,是在 viewController 之上的存在,我们不同的 hybirdid 项目之间的环境又是相互独立的。当多个 hybirdid 的项目一起触发 Logbox 时,客户端就会接收到多次弹出 Logbox 的消息,这样客户端就多个的 Logbox Window,但是此时 js 端只能渲染当前项目的 LogBox 中的内容,无法渲染其他项目的 Logbox 内容,也没有入口去通知客户端关闭,所以就导致其他项目的 Logbox window 存在且是透明的,页面也就无法交互了。

由于开启了多个 jsc 环境,没有办法在 js 端解决,最后是在客户端解决的。加了个类似单例模式,来阻止多次弹出 Logbox。

5.4 解决业务线再升级过程中的问题

最后就是业务线再升级过程中遇到的适配问题了,业务线对组件使用的程度更大,也就会暴露出比较深层的问题。

这里面有些写法官方再比较早的时候就提示废弃了,业务线平时开发可能也很少开 dev 模式,dev 模式会有很多代码校验,所以有些业务线在这次升级的过程中这类的问题还是挺多的。

因为现在 logbox 的出现,我们也建议业务线都开启 dev 模式开发。我们也将业务线暴露出的问题收集整理了一个文档,也会提出解决方案和建议,如果有其他业务线也有相同问题,可以帮助快速理解并解决。

5.5 红屏错误: Not found module 8

去哪儿的业务众多,为了更快的加载速度和更小的体积,需要将业务线包和框架包解耦。qrn 采用了官方 metro 拆包方案,拆包之后,可能会出现不同业务包之间或者业务包和框架包之间依赖了同一模块情况,为了防止这种重复依赖的情况,打包的时候会根据当前的依赖关系生成依赖的映射文件,里面的内容是编号及对应的引用路径,编号是安装引用顺序去递增的。

举个例子,业务包依赖了一个模块 A,那么假想成它在业务依赖映射表的值为:

在框架包依赖映射表的值为:

"/node_modules/A/index.js": 5

这个时候在打业务包的时候会直接使用框架的编号 5,这样 A 模块就不会被打进业务的 qp 包了,可以做到减少业务包的体积。

我们现在回来看这个问题,升级了 RN 版本之后,它的依赖也随之更新了版本,这很正常。但是也就造成子依赖模块和 qrn-js 的子依赖模块的版本冲突,导致了 node_modules 的结构改变了,按照之前的映射就会找不到对应的模块。

npm 现在的逻辑也是会优先放在顶层,如果有版本不兼容就会存在多份。模块引用的查找逻辑是先找当前目录下的 node modules,如果没有,会依次向外层找,直到找到根目录的 nodemodules。

举个刚才的例子,业务依赖编号为 5 模块路径是:

"/node_modules/A/index.js"

现在变成了:

"/node_modules/react-native/node_modules/A/index.js" : 5,

这样新打出来的包跑在旧的客户端上用新的 path 那肯定就会找不到这个模块。那么其实让版本不冲突不就好了么,但 qrn-js 为了防止安装模块时自动升级版本而造成差异问题,依赖的版本都是指定的版本,没有加匹配规则。所以即使现在不冲突了,也不能保证之后不会冲突。

一旦出现刚才说的情况,那么影响是很大的。我就在 qrn-js 的 postInstall 中添加脚本,脚本会删除 RN 内部的 node_modules,让 rn 内部使用的模块安装 qrn 依赖的位置。

这样就可以一直保证使用的是顶层的模块,也和之前的正式版映射表匹配,就不会出现升级后找不到模块的问题啦。

5.6 JSBundle 体积变大了

升级后,有业务线发现 JSBundle 的体积比升级前大了,开始我们怀疑是某一个模块升级后突然增大,前面有说过业务包中不会包含框架的代码,所以很可能是业务中的某一个文件,对比了升级前后的代码,业务代码几乎没有变动。

那能从打包的产物找找原因了,由于打包会把项目打成一个文件,所以根本无法对比出来是哪个模块出现的问题。于是我们将 bundle 的每一行内容拆成了一个一个的 js 文件,并以它的编号命名。每一个 js 文件的大小便是这个模块在 bundle 中的大小。

这样就可以单独对每一个模块进行升级前后对比了,经过对比后,没有发现增长特别明显的模块,最多的也就比之前大了 1k 左右。随后就发现很多模块都有少量增大的情况,整个项目累加起来就变的很明显了。

那基本就可以确定是编译过程中增加的了。查了下 babel 的版本已经升到了最新版,而我们的业务代码和 rn 的代码都不需要将 babel 升级的最新版本,回退了 babel 版本并重新做了版本锁定之后,恢复到升级前的大小了。

所以如果可以确定不需要升级的模块 (不仅仅是 babel),请一定做好版本锁定,防止自动升级。

6 总结

以上基本就是本次升级的典型了,也顺利的完成了兼容升级。总体来看兼容升级要考虑的地方很多,难度也要比不兼容升级大。兼容升级后,业务线可以直接进入到功能回归阶段,也缩短了升级过程。

升级后代码可直接运行到旧版客户端上,业务也不再是截断式的。

希望文本对计划要做升级的你有些帮助,文中有任何错误还请指正。如果有任何问题或者想法,也欢迎与我们联系。