2

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

 2 years ago
source link: https://xiaomi-info.github.io/2020/01/02/react-native-version-check/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

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

mismatch.png

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

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

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

版本检测机制

报错的位置

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

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 中声明:

exports.version = {
major: 0,
minor: 0,
patch: 0,
prerelease: null,
};

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

Platform.constants.reactNativeVersion

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

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 中,如下:

import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';

...

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

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

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,关键代码:

@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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK