小米信息部技术团队

React Native 启动版本检查机制探究

2020-01-02

React Native 启动版本检查机制探究

[作者简介] 陈久林,信息部前端组,主要负责服务体系前端开发。

引子

有同学反馈 React Native(简称 RN) 项目启动报错,提示版本不匹配,错误截图如下:

经过一番排 (xia) 查 (gao),最后发现是本地打包了老版本 js 文件,和项目本身依赖的版本不同导致,删除本地的老版本文件即可。

通过这个错误,我们可以发现 RN 在启动时是有版本检查的,具体机制如何呢,下面我们一起跟着源码走一遍。

至于为何本地会打包一个老的 js 文件,以及为何这么多年过去了今天才出问题,这是另一个话题,暂且忽略

版本检测机制

报错的位置

通过搜索关键字 React Native version mismatch 可以发现检测的最终代码在 Libraries/Core/ReactNativeVersionCheck.js 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Platform from '../Utilities/Platform';
const ReactNativeVersion = require('./ReactNativeVersion');

exports.checkVersions = function checkVersions(): void {
const nativeVersion = Platform.constants.reactNativeVersion;
if (
ReactNativeVersion.version.major !== nativeVersion.major ||
ReactNativeVersion.version.minor !== nativeVersion.minor
) {
console.error(
`React Native version mismatch.\n\nJavaScript version: ${_formatVersion(
ReactNativeVersion.version,
)}\n` +
`Native version: ${_formatVersion(nativeVersion)}\n\n` +
'Make sure that you have rebuilt the native code. If the problem ' +
'persists try clearing the Watchman and packager caches with ' +
' `watchman watch-del-all && react-native start --reset-cache` .',
);
}
};

该方法对比了 ReactNativeVersion.versionPlatform.constants.reactNativeVersion 两个的 major 和 minor,当这两个值不匹配时即会抛出该异常。

如果版本号是 0.59.9,那么 major 就是 59,minor 就是 9。感觉 RN 就没打算把最前面的 0 去掉(手动捂脸

同时, checkVersion 是在启动时候加载,这部分代码大家自行搜索即可看到,不做分析

ReactNativeVersion.version

那么这两个值分别代表的什么呢,首先查看 ReactNativeVersion.version ,它在同目录下的 Libraries/Core/ReactNativeVersion.js 中声明:

1
2
3
4
5
6
exports.version = {
major: 0,
minor: 0,
patch: 0,
prerelease: null,
};

嗯,非常的清晰明了。简直写了跟没写一样嘛,不急,反正我们知道了,这个值是在 js 文件中,会随着最终的打包进入 bundle.js 中。

Platform.constants.reactNativeVersion

根据引用,我们找到 Libraries/Utilities/Platform.android.js 这个文件,关键内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import NativePlatformConstantsAndroid from './NativePlatformConstantsAndroid';

const Platform = {

...

get constants() {
if (this.__constants == null) {
this.__constants = NativePlatformConstantsAndroid.getConstants();
}
return this.__constants;
}

...

};

module.exports = Platform;

又导向了 NativePlatformConstantsAndroid.getConstants() ,在 Libraries/Utilities/NativePlatformConstantsAndroid.js 中,如下:

1
2
3
4
5
6
7
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';

...

export default (TurboModuleRegistry.getEnforcing < Spec > (
'PlatformConstants',
): Spec);

犹抱琵琶半遮面,通过 TurboModuleRegistry.getEnforcing('PlatformConstants') 获取到,继续往下 Libraries/TurboModule/TurboModuleRegistry.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const NativeModules = require('../BatchedBridge/NativeModules');
const turboModuleProxy = global.__turboModuleProxy;

export function get < T: TurboModule > (name: string): ? T {
if (!global.RN$Bridgeless) {
// Backward compatibility layer during migration.
const legacyModule = NativeModules[name];
if (legacyModule != null) {
return ((legacyModule: any): T);
}
}
if (turboModuleProxy != null) {
const module: ? T = turboModuleProxy(name);
return module;
}
return null;
}

export function getEnforcing < T: TurboModule > (name: string): T {
const module = get(name);
return module;
}

一大坨东西,就一个目标,获取一个原生模块,名字叫 PlatformConstants ,那找到这个原生模块就能揭秘了,通过搜索 PlatformConstants ,可以找到它的原生实现在 ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java,关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public @Nullable Map<String, Object> getConstants() {
HashMap<String, Object> constants = new HashMap<>();

...

constants.put("reactNativeVersion", ReactNativeVersion.VERSION);

...

return constants;
}

一步之遥了,继续看同目录下的 ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java

1
2
3
4
5
6
7
public class ReactNativeVersion {
public static final Map<String, Object> VERSION = MapBuilder.<String, Object>of(
"major", 0,
"minor", 0,
"patch", 0,
"prerelease", null);
}

和 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(武汉)

扫描二维码,分享此文章