React Native 启动版本检查机制探究
[作者简介] 陈久林,信息部前端组,主要负责服务体系前端开发。
引子
有同学反馈 React Native(简称 RN) 项目启动报错,提示版本不匹配,错误截图如下:
经过一番排 (xia) 查 (gao),最后发现是本地打包了老版本 js 文件,和项目本身依赖的版本不同导致,删除本地的老版本文件即可。
通过这个错误,我们可以发现 RN 在启动时是有版本检查的,具体机制如何呢,下面我们一起跟着源码走一遍。
至于为何本地会打包一个老的 js 文件,以及为何这么多年过去了今天才出问题,这是另一个话题,暂且忽略
版本检测机制
报错的位置
通过搜索关键字 React Native version mismatch
可以发现检测的最终代码在 Libraries/Core/ReactNativeVersionCheck.js
中:
1 | import Platform from '../Utilities/Platform'; |
该方法对比了 ReactNativeVersion.version
和 Platform.constants.reactNativeVersion
两个的 major 和 minor,当这两个值不匹配时即会抛出该异常。
如果版本号是 0.59.9,那么 major 就是 59,minor 就是 9。感觉 RN 就没打算把最前面的 0 去掉(手动捂脸
同时,
checkVersion
是在启动时候加载,这部分代码大家自行搜索即可看到,不做分析
ReactNativeVersion.version
那么这两个值分别代表的什么呢,首先查看 ReactNativeVersion.version
,它在同目录下的 Libraries/Core/ReactNativeVersion.js 中声明:
1 | exports.version = { |
嗯,非常的清晰明了。简直写了跟没写一样嘛,不急,反正我们知道了,这个值是在 js 文件中,会随着最终的打包进入 bundle.js 中。
Platform.constants.reactNativeVersion
根据引用,我们找到 Libraries/Utilities/Platform.android.js 这个文件,关键内容如下:
1 | import NativePlatformConstantsAndroid from './NativePlatformConstantsAndroid'; |
又导向了 NativePlatformConstantsAndroid.getConstants()
,在 Libraries/Utilities/NativePlatformConstantsAndroid.js 中,如下:
1 | import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; |
犹抱琵琶半遮面,通过 TurboModuleRegistry.getEnforcing('PlatformConstants')
获取到,继续往下 Libraries/TurboModule/TurboModuleRegistry.js:
1 | const NativeModules = require('../BatchedBridge/NativeModules'); |
一大坨东西,就一个目标,获取一个原生模块,名字叫 PlatformConstants
,那找到这个原生模块就能揭秘了,通过搜索 PlatformConstants
,可以找到它的原生实现在 ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java,关键代码:
1 |
|
一步之遥了,继续看同目录下的 ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java :
1 | public class ReactNativeVersion { |
和 js 那边的一样,都是 0,这个待会再论。可以看出, Platform.constants.reactNativeVersion
是在 java 侧定义的,最终在原生代码中,我们在 build.gradle 文件中引用的 com.facebook.react:react-native:0.59.9
则包含了这部分代码。
阶段性总结
可以看出,在 js 侧有个版本号,同时在 java 侧也有个版本号,两者会在启动的时候进行判断,如果不相同就会抛出错误。
js 和 java 是两个依赖,js 部分在 package.json
中进行依赖,java 部分在 android/app/build.gradle
中依赖,两者必须匹配才能很好的工作,所以有了上述的检查工作。
通过对启动源码分析,发现其实仅在开发环境才进行检查,生产环境则没有这段
一般而言,开发环境都会执行比生产环境更为严格的检测,确保开发阶段错误及时暴露,而在生产环境则会去掉与主功能无关的代码,保证运行时的最大效率。这可以说是大部分库的一个处理手段,严开发宽发布,值得我们学习借鉴
版本号如何设置
前面源码查看,发现版本号都是 0,那么具体版本号是如何设置上去的呢,大家可以查看下这个目录 scripts/versiontemplates/,其下则是版本号设置的模版,真正的操作则是在 scripts/bump-oss-version.js#L60 中进行的,这个脚本接受一个版本号,然后填充前面的模版,并覆盖项目中对应的文件。这个脚本是在发版的时候执行的,详情见 step-2-cut-a-release-branch-and-push-to-github,至此一切就都清楚了。
所以版本号是在发布的时候通过脚本设置上去的,通过模版的方式进行统一设置,避免人工修改遗漏
模版部分就是简单替换,并未引用额外的模版引擎,能简单处理就绝不搞复杂,这点值得我们学习
脚本很多都是 js 写的,这样非常容易阅读和修改,我们也可以多用 js 来处理脚本,不能提到脚本就 bash、python 的,其实 js 也很流行
什么情况下会发生这个错误
我遇到的这例是因为该同学使用 RN 0.55.4 进行了手动打包,并将打包后的 js 文件上传了仓库,后来升级 RN 到 0.59.9,开发环境下,设备因为某些原因没有连接到对应的 packager,然后直接使用了本地的 js 文件,从而产生了该问题。
从前面源码分析来看,如果开发时 packager 启动了错误版本,也是可能产生该问题的。可以理解该机制就是确保当前运行的 App 从 packager 下载到的 js 文件版本是一致的,避免大家在错误的版本上继续开发,导致问题蔓延,不便于最后问题的排查。
当我们遇到这个问题时,一般都是 packager 启动了错误版本导致的。其次,除非你知道你在干什么,否则是严禁手动生成 js 的包,这部分都应该交由 RN 的打包脚本来执行和维护,并且是不能提交仓库的。
为什么有这个检查
并没有找到相关的说明,但可以推测下。个人认为是 RN 的开发模式导致的,在开发阶段,电脑上会启动一个 server,也就是上面提到的 packager,用来分发最新的 js 文件,这也是 RN 开发阶段可以快速更新代码的基础,因为分发是独立的,所以这部分是有可能发生版本不一致的的问题,而版本不一致是不影响大部分开发,因为 API 大部分是兼容性设计,如果放任这种行为,到了开发后期出现问题,排查将会非常艰难,所以这也是提前暴露问题。而在发布阶段,因为都是脚本自动执行,这部分相对安全很多。
很多时候,一些疑难问题都是由低级错误导致的,只是问题在初期隐藏,到了中后期才爆发,这时再去排查就非常耗时了。特别对于 RN 这种开源项目,如果 issues 中有很多是低级错误导致的 “疑难杂症”,这是对资源的巨大浪费。从这点来看,这些基本检测还是很有必要的
总结
在开发环境,RN 启动阶段,会对 js 和 java 两边的版本号进行校验,匹配后才开始真正的系统启动流程。增加这一步检查,是确保开发基础环境的一致,保证开发顺利进行。
同时在追踪源码的过程中,也能学到很多知识,包括库的设计,开发环境与生产环境的差异化,模版设计等等。对于开源项目的错误,很多时候我们可以通过源码来了解问题的本质,这对于我们的开发和学习有很大的帮助。
作者
陈久林,信息部前端组
招聘
信息部是小米公司整体系统规划建设的核心部门,支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作,服务小米内部所有的业务部门以及 40 家生态链公司。
同时部门承担大数据基础平台研发和微服务体系建设落,语言涉及 Java、Go,长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。
欢迎投递简历:jin.zhang(a)xiaomi.com(武汉)
扫描二维码,分享此文章