35 自动化:升级0.72新架构的步骤和关键
你好,我是蒋宏伟,今天我们来聊聊架构升级。
今年 2 月,超过 2000 人的调查报告——《State of React Native 2022》指出:有 6.3% 的开发者认为,React Native 版本升级是他们的痛点,位列所有问题之首。
是的,升级确实很痛。
正如上一讲中提到的,升级能够获得客观的性能收益,但成本太高了。当成本抵消收益时,理性的选择是放弃升级,即使无法享受性能收益,无法拥抱最新的生态,还可能会留下技术负担。
React Native 升级涉及许多方面,这些都是成本,包括 iOS、Android、JavaScript 三种语言的升级,多个团队的协同,所有项目的回归测试,以及因升级而放弃的业务研发的机会成本。只有在时机合适且收益能够覆盖业务的机会成本时,升级才是我们需要考虑的事项。
然而,一旦你确定要升级后,必须做好两方面的工作:一方面是规划好升级方案,减少升级对业务节奏的影响;另一方面是善用升级工具,加速项目升级。
那么这节课,我就来介绍下我对升级的整体思考。然后,我会重点介绍一个好用的升级工具——JSCodeshift,它可以帮助你自动批量升级项目。
升级方案
大概有以下3种升级方案:
- Expo 升级方案
- React Native Upgrade
- 将业务代码复制新版本上
下面,我会逐一介绍它们。
第一种方案是 Expo。Expo 提供了 eas-cli 和 expo 命令来帮助开发者进行升级。并且,每当发布一个新的 SDK 版本, Expo 都会提供相应的升级文档和建议。但是,由于 Expo 升级工具只能在 Expo 生态应用中使用,而我们主要使用原始的 React Native 项目,因此这里不做详细介绍。
第二种方案是React Native Upgrade。React Native 官方提供的升级工具只需要执行一行命令即可进行升级。
但当我尝试使用官方的 Upgrade 工具进行升级时,却失败了。就在上周,0.72 版本正式发布,我想着我们专栏的项目 react-native-classroom 也停留在 0.68 版本好久了,就准备将它从 0.68 升级到 0.72。但运行命令后,却发现报错了。
报错内容为 upgrade failed
,并且还提示我有若干个 Android 文件升级冲突了。
于是,我查了一下 Upgrade 工具的原理,发现它无非是将 0.68 版本的模板代码和 0.72 版本的模板代码进行 diff,然后把 diff 后的结果应用到 react-native-classroom 项目上。其本质是在老项目中,应用 0.72 版本的模板代码。
于是,我想到直接拷贝 0.72 模板代码的方案,这就是第三种方案。我放弃了 Upgrade 工具,直接将 react-native-classroom 项目的所有 JavaScript 业务代码,包括 package.json 等文件,拷贝到新的 0.72 工程上,经过多次代码修改和安装构建后,我算是把 0.72 新架构版本的 react-native-classroom 给跑起来了。
依赖升级与代码修改
接下来,我说一下升级的细节和所踩的坑。
第一类坑是第三方包的版本不匹配。例如,在升级 React Navigation 后,我遇到了报错。在将 0.68 版本的依赖项拷贝到 0.72 版本的依赖项时,我遇到了几个类似的报错,报错信息不够明确,很难确定问题所在。
ERROR Invariant Violation: requireNativeComponent: "RNSScreenStackHeaderConfig" was not found in the UIManager.
针对这类问题,我在升级过程中采取了重装的方案,也就是将几乎所有依赖于 Native 的依赖项进行了版本升级和重新安装。通过这种方式,我成功解决了这类报错的问题。下面是重新安装的参考命令:
// 先删除相关的依赖性,然后重新安装
$ yarn add @react-navigation/bottom-tabs @react-navigation/devtools @react-navigation/drawer ...
第二类坑是重装依赖后提示已经找不到本地依赖模块的问题。例如,在我使用 Yarn 重装依赖重启应用时,Reanimated 报错没有正常初始化(doesn’t seem to be initialized)。
ERROR Error: [Reanimated] The native part of Reanimated doesn't seem to be initialized. This could be caused by - not rebuilding the app after installing or upgrading Reanimated - trying to run Reanimated on an unsupported platform - running in a brownfield app without manually initializing the native library, js engine: hermes LOG Running "RN72" with {"rootTag":341}
这类问题通常是由缓存导致的。只需清除 JavaScript 缓存并重新安装本地依赖和安装包,即可解决。以 Android 为例,步骤如下:
// 重新安装 Android 依赖
$ cd android
$ /gradlew build --refresh-dependencies
// 清除 JavaScript 构建缓存
$ cd ../
$ npx react-native start --reset-cache
// 按键盘 a 键,重新安装 Android 包
第三个坑是JavaScript代码的变动。例如,Gesture 手势库在升级后要求必须在你的Root App组件外部包装一个 <GestureHandlerRootView>
(类似于Redux中的 <Provider>
)。这类改动相对容易,我把我遇到的相关报错和改动贴了出来,如下:
ERROR Error: GestureDetector must be used as a descendant of GestureHandlerRootView. Otherwise the gestures will not be recognized
开启新架构
上述三个坑踩完后,接着我开启了 React Native 的新架构。
要在Android上启用新架构,需先将 android/gradle.properties
文件中的 newArchEnabled
设置为true。然后重新安装依赖和构建Android包,你可以运行以下命令:./gradlew build --refresh-dependencies
。
// 修改 android/gradle.properties 文件中的变量
newArchEnabled=true
// 运行命令依赖安装和构建的命令
$ ./gradlew build --refresh-dependencies
iOS 开启新架构也类似,重新安装依赖和 iOS 包,安装新架构依赖命令如下:
至此,咱们课程中用到的 react-native-classroom 升级 0.72 新架构算是完成了。
配置升级自动化
前面我们已经完成了单个项目的升级,但如果有二三十个项目需要升级,手动逐个升级的效率肯定很低。
那么,有哪些适合自动化的步骤呢?适合自动化的步骤包括:
- 依赖于Native的依赖项的配置
- JavaScript代码的升级
接下来,我们借助 Node.js 对配置实现自动升级,借助 JSCodeshift 对 JavaScript 代码实现自动化升级。
React Native 项目升级,主要涉及三个配置文件:
- package.json
- babel.config.js
- metro.config.js
自动化升级的思路是:先整理好需要升级的新配置,然后通过脚本对比升级需要更新的配置项,最后将更新后的配置项写入。
以 package.json 为例。假设你在同一个 App 中有 10 个项目要升级,只要你把其中 1 个项目升级成功,那么剩下的 9 个项目只要照葫芦画瓢即可。也就是说,我们只需将剩余 9 个项目的 package.json 中依赖 Native 的 dependencies 的相关配置和第 1 个项目对齐即可。
现在,我们已经将 react-native-classroom 项目升级完成了,我摘录了一部分的 dependencies 依赖的升级规则,如下:
const navigationDeps = {
"@react-navigation/bottom-tabs": "^6.5.8",
"@react-navigation/devtools": "^6.0.19",
"@react-navigation/drawer": "^6.6.3",
"@react-navigation/material-bottom-tabs": "^6.2.16",
"@react-navigation/material-top-tabs": "^6.6.3",
"@react-navigation/native": "^6.1.7",
"@react-navigation/native-stack": "^6.9.13",
"@react-navigation/stack": "^6.3.17",
"react-native-safe-area-context": "^4.6.3",
"react-native-screens": "^3.22.0",
}
要自动化地升级这部分配置,需要借助 Node.js 的力量。我们可以先读取 package.json 文件,再将最新的 dependencies(navigationDeps)插入到 package.json 文件中,最后将旧的 dependencies 删除即可。伪代码如下:
// 伪代码
if (includes('package.json', 'react-navigation')) {
addDeps('package.json', navigationDeps)
removeDeps('package.json', ['react-navigation'])
}
function mergePackage (rootPath) {
// 1. 读取项目package.json
const toPkgPath = resolveRootPath(rootPath, 'package.json');
const toPkg = require(toPkgPath);
// 2. 合并更新配置
const newPkg = mergePkg(toPkg, fromPkg);
// 3. 将新配置写入package.json
fs.writeFileSync(toPkgPath, JSON.stringify(newPkg, null, 2));
}
简单讲,自动升级配置的方案就是:先列举配置文件的升级规则,然后借助 Node.js 实现该升级规则。
代码升级自动化
完成自动化配置升级,接着我们继续完成代码的自动化升级。
JavaScript 代码本身不像配置文件,不借助一些工具读取和修改起来是非常麻烦的。
这里,我们使用正则修改和升级代码,但更好的方式是借助 AST 来升级它们。JSCodeshift 就是一款基于 AST 修改代码的工具。
与升级配置项类似,包括两步:
- 手动升级一个项目,列举升级规则
- 借助 JSCodeshift,将升级规则自动化
通过手动升级项目和官方文档,比如 React Native 各个版本的 Changelog,尽可能地找出需要改动的代码部分。
例如,React Native 在 0.60 版本移除了框架自带的 WebView 组件,开发者需要迁移到社区的 react-native-webview 组件。这就涉及到 WebView Import 规则的升级,升级规则示例如下:
明确规则后,具体的实现咱们可以借助 ChatGPT 偷偷懒。我写的 Promot 如下:
我正在对 JavaScript 项目的代码进行升级,请你使用 JSCodeshift 写一段代码,对该项目下 case 目录的所有文件应用升级规则,直接进行代码替换。升级规则如下:
// 升级前
import { WebView } from 'react-native'
// 升级后
import { WebView } from 'react-native-webview'
经过多次沟通后,ChatGPT 给了我正确的答案。具体代码比较长,而且涉及到 AST 的概念,解释起来也比较麻烦。如果你感兴趣可以看看它给出的实现,但更重要的是掌握灵活使用 ChatGPT 的思路。
const fs = require('fs');
const path = require('path');
const j = require('jscodeshift');
// 更新导入声明的函数
function updateImportDeclaration(j, root) {
root
.find(j.ImportDeclaration) // 查找所有的导入声明
.filter((path) => path.node.source.value === 'react-native') // 筛选出源代码值为 'react-native' 的导入声明
.forEach((path) => {
const webViewSpecifier = path.node.specifiers.find(
(specifier) => specifier.imported.name === 'WebView'
); // 找到导入的 WebView
if (webViewSpecifier) {
webViewSpecifier.imported.name = 'WebView'; // 将导入的 WebView 改为新的名字
path.node.source.value = 'react-native-webview'; // 将导入的源代码值改为新的模块名
}
});
}
// 更新代码的函数
function updateCode(code) {
const ast = j(code); // 将代码解析为抽象语法树(AST)
const root = ast.find(j.Program); // 找到 AST 的根节点
updateImportDeclaration(j, root); // 调用函数更新导入声明
return ast.toSource(); // 将 AST 转换回代码
}
// 处理单个文件的函数
function processFile(file) {
const filePath = path.resolve(file); // 获取文件的绝对路径
const code = fs.readFileSync(filePath, 'utf-8'); // 读取文件内容
const upgradedCode = updateCode(code); // 调用函数更新代码
fs.writeFileSync(filePath, upgradedCode, 'utf-8'); // 将更新后的代码写回文件
}
// 递归处理目录下的所有文件
function processDirectory(directory) {
fs.readdirSync(directory).forEach((file) => {
const filePath = path.join(directory, file); // 获取文件的完整路径
const stats = fs.statSync(filePath); // 获取文件的状态信息
if (stats.isFile() && file.endsWith('.js')) {
processFile(filePath); // 如果是文件且以 .js 结尾,调用处理单个文件的函数
} else if (stats.isDirectory()) {
processDirectory(filePath); // 如果是目录,递归处理子目录
}
});
}
const srcDirectory = path.resolve('case'); // 目标文件夹的路径
processDirectory(srcDirectory); // 调用处理目录的函数,开始处理所有文件
代码升级自动化的关键是定义升级规则,然后借助 ChatGPT、JSCodeshift 这些工具,对其进行实现。
总结
升级 React Native 版本和新架构难免会遇到一些阻碍,而我们可以通过三个步骤逐步跨越这些障碍。
- 升级试点项目,并在试点过程中积累经验,包括收集所需升级的第三方依赖包和代码升级规则。
- 提前研发升级工具和 Native SDK,将第一步收集的升级规则,编程为 JSCodeshift 等自动化工具的代码,并提前准备好对应的新架构 Native SDK。
- 基建团队和业务团队协同升级。由于第二步的积累,业务团队能较快地将业务代码完成升级,最后统一发版上线。
完整代码我放在了GitHub上,供你参考,代码地址:0.72 新架构版本的 react-native-classroom
思考题
你在升级 React Native 版本过程中遇到过哪些困难,你又是怎么解决的?
期待你的分享,欢迎你把这节课分享给有需要的朋友,我们下节课再见!
- 听说昵称太长了躲在树后面会被别人看见的 👍(7) 💬(0)
我是先拉一个 git 分支出来自动升级,升级失败了可以通过自己本地 git 比较看哪些改动,自动升级失败就放弃这个分支另外拉一个分支,然后用 rn 最新版创建一个空项目,把老项目中代码慢慢复制过来,但是如果老项目原生代码写了很多,还要迁移原生代码也很麻烦,后来我把项目改成了原生为主体,拿出 activity 承载 rn,开始把 rn 端依赖的第三方原生库尽量干掉,把第三方库的原生代码抄写到自己的原生工程,rn 只剩下纯 js 的外部依赖会少很多升级问题,并且第三方依赖约少升级坑越少。最后,我觉得 rn 还是只能作为原生的一个补充,去实现一些简单功能,以及作为新需求的敏捷迭代,稳定下来之后如果遇到性能瓶颈果断用原生改写,有的场景例如视频处理 Android 在原生端还要使用 Java 的原生开发(ndk) ,rn 完全不够看,比如 rn 在安卓上调转动画的持续时间得改框架依赖的原生代码,最后还是回到原生,所以 rn 遇到刺手问题时别想着去优化 rn 性能,优化来优化去最后一地鸡毛。rn 在安卓上就起了一个 activity 而已,对于大项型目不可能就一个 activity 搞定,搞到后面都是多模块多人协作,站在一般原生开发者视角,一个 activity 做出来大型项目就是一个大号 demo,不然业务规模扩大人员扩充后再往 rn 身上甩甩锅,大概率要用原生重构
2023-07-07 - 陈彦祖 👍(0) 💬(1)
依赖包本地缓存这种问题真是新手梦魇。没有多少经验遇到这种问题真的难顶。
2023-07-11