刚刚结束的 QRN 升级, 有些同学可能会有些小疑惑.
qrn-js 是干什么的呢?
为什么要升级呢?
升级都做了什么呢?
那希望本篇可以消除一些小疑惑, 对 qrn-js 有一步的了解.
React-native 目前是去哪儿网跨端最佳方案, 有十几个业务线参与其中.RN 已是去哪儿网客户端的主要技术栈.
业务线如此繁多的情况下, 我们需要再 RN 的基础上开发一个属于去哪儿的 RN 框架: QRN.
QRN 分为 JS 和 Native 两部, 本篇只我们介绍下 qrn-js 框架的作用, qrn-js 的核心功能包括但不限于对 rn 原生的代码修改加工, 兼容双端的组件和 api, 自定义的组件和 api等.
同时也支持 Redux, webx 等功能. 随着不断的实践与积累, QRN 现可支持全业务线开发, 已成为去哪儿大前端的关键一环.
我参与过两次 qrn 升级, 至于
时间推移, 技术再不断进步, RN 每个阶段都会有不同的想法和方向. 为了更好的性能和体验, QRN 也就需要紧紧跟好步伐了.
我参与过两次 qrn 升级, 上一次升级是从 RN 的 0.59 升级的 0.61. 上一次升级改动优化了生命周期, qrn 也重构了打包脚本,
应用了拆包方案, 来减少业务包体积和优化启动速度.
与上一次升级是不同的时, 这次升级做到了兼容升级, 业务线无需设置版控和分水岭, 相较上次升级成本要小.
这次升级是从 0.61.3 升级到 0.63.2, 那来看看这次我们
软 patch 是只修改组件 api 的预处理, 好处就是在运行时处理, 可以不修改原本的内部逻辑,
这种方式在 qrn 中主要用来加日志上报和参数预处理. 因为这种方式不会修改原执行逻辑, 所以 rn 升级
对其影响不大, 通常会优先选择软 patch 的方式进行修改, 如果满足不了, 就只能使用 硬 patch 的方式了.
代码会在引入 qrn-js 时自动执行, 代码示例:
硬 patch 也是 file patch, 就是完全覆盖 RN 原生文件, 这种一般软 patch 满足不了的情况会采用这种方式,
比如修改执行逻辑或者大面积改变写法等. 这种方式在 qrnjs 中还是比较多的, 我们有很多修改源码来满足我的业务需求.
每处修改我们都会加些特殊的注释, 代表此次是 qrnjs 修改的, 也方便以后升级, 不然不知道改了哪里, 就要多花很多时间.
patch代码在 qrn-js 的目录和在 RN 的目录相同, 可以很清晰的知道都修改了哪些文件.
file patch 代码示例:
上面介绍了 qrn-js 修改 RN 源码的两种方式, 那么其实每次升级我们都需要去确认修改的代码在新版中还是否生效,
像用file patch修改的文件, 如果这次升级改变了内部实现, 那么我们就需要对新的代码重新做file patch. 那么除了这部分我还需要知道官方的所有改动,
我们先是通过翻阅61到63所有版本的更新日志和commit, 将其收集归类, 再经过评估后, 确定更新影响范围并整理出本次升级需要着重关注的更新.
这里就挑了几个更新拆解说明一下.
-
提高开发调试效率: 全新的错误提示框 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修复等等.
更多的升级亮点:升级RN63亮点
根据整理完的内容, 我就开始考虑可以不可以做兼容升级, 兼容升级对业务的接入成本要比不兼容会小一些,我们就开始了兼容方案的调研.首先我们要解决核心包瘦身的更新所带来兼容问题
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感兴趣, 可以直接使用新的就好. 这样影响也可以控制到最小.
比如 Image 组件, 原 image
组件使用 native 的 ImageViewManager
, 现新版换成了 NativeImageLoaderIOS
, 这种也需要修改源码解决.
因为这个只有在新版客户端才能引用 NativeImageLoaderIOS, 优先去引用新版本的模块, 如果找不到新版模块, 默认就使用旧版本的.
像类似这种的更新, 可能都会用这种方式来做兼容
1.升级后发现所有的Touchable组件点击后的效果都不正常了, 点了之后会一直灰, 过了一会才会恢复正常, 之前觉得可能是开启 dev 模式后无动画效果的问题, 但是关了 dev 模式也是这样.
我先是 diff 了一下升级前后的代码, 没发现什么问题, 然后又读了下新的代码, 在Pressability里找到了问题, 升级后常量 DEFAULT_PRESS_DELAY_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.
最后就是业务线再升级过程中遇到的适配问题了, 业务线对组件使用的程度更大, 也就会暴露出比较深层的问题.
这里面有些写法官方再比较早的时候就提示废弃了,业务线平时开发可能也很少开 dev 模式, dev 模式会有很多代码校验, 所以有些业务线在这次升级的过程中这类的问题还是挺多的,
因为现在 logbox 的出现, 我们也建议业务线都开启 dev 模式开发. 我们也将业务线暴露出的问题收集整理了一个文档, 也会提出解决方案和建议, 如果有其他业务线也有相同问题
可以帮助快速理解并解决. 还有一个想说的是
因为去哪儿的业务很多, 为了更快的加载速度和更小的体积, 需要将业务线包和框架包解耦. qrn 采用了官方 metro 拆包方案, 分为业务包和框架包. 业务包和框架包可以独立打包.
拆包之后, 可能会出现不同业务包之间或者业务包和框架包之间依赖了同一模块情况, 为了防止这种重复依赖的情况,
打包的时候会根据当前的依赖关系生成依赖的映射文件, 里面的内容是编号及对应的引用路径, 编号是安装引用顺序去递增的.
为了防止业务和框架的编号冲突, 框架的依赖编号是从 1 开始递增, 业务的依赖编号是从 200001 开始递增.当业务包再打包时, 如果有一个模块同时也在框架包的依赖映射文件中存在, 则会优先使用框架包的编号
举个例子, 业务包依赖了一个模块 A, 那么假想成它在业务依赖映射表的值为: "/node_modules/A/index.js": 200005
, 在框架包依赖映射表的值为: "/node_modules/A/index.js": 5
这个时候在打业务包的时候会直接使用框架的编号 5, 这样 A 模块就不会被打进业务的 qp 包了, 可以做到减少业务包的体积.
我们现在回来看这个问题, 升级了RN版本之后, 它的依赖也随之更新了版本, 这很正常. 但是也就造成子依赖模块和 qrn-js 的子依赖模块的版本冲突, 导致了 node_modules 的结构改变了, 按照之前的映射就会找不到对应的模块.
npm 现在的逻辑也是会优先放在顶层, 如果有版本不兼容就会存在多份. 模块引用的查找逻辑是先找当前目录下的 node_modules, 如果没有, 会依次向外层找, 直到找到根目录的node_modules.
举个刚才的例子, 业务依赖编号为 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 依赖的位置.
这样就可以一直保证使用的是顶层的模块, 也和之前的正式版映射表匹配, 就不会出现升级后找不到模块的问题啦.
以上基本就是本次升级的典型了, 其他细节就不一一列举了.
qrn 后面的计划是要对启动速度的优化, 目前已有两种方案再调研中, 如果有任何问题或者想法, 欢迎与我们联系
感谢大家的阅读