React Native构建踩坑记录

1. 背景

我们团队负责的RN项目分为common和 SP plugin两个仓库,都使用yarn classic作为包管理工具。 其中 common 是基础bundle,包括react、react-native等基础包,SP plugin是我们团队的业务bundle,内部不包含common bundle的内容。SP App可以看做是一个RN plugin 容器,会集成多个团队的业务RN plugin 。SP App 会先将common bundle内置到app中,当用户进入不同的业务场景时,App只需要加载每个业务plugin即可。RN架构详情请参考:Shopee SZ去中心化的 React Native 架构探索

最近需要将我们团队负责的两个仓库使用pnpm改造为monorepo

2. 基础知识

在改造之前我们需要了解一些基础知识

React Native使用 metro 作为打包工具

metro打包过程分为以下三个阶段

  1. Resolution : 从入口模块出发分析模块依赖关系并构建模块依赖图,内部使用jest-haste-map 来进行依赖分析与文件监听。这个阶段与Transformation阶段并行。
  2. Transformation:此阶段主要将模块代码转换为目标平台可识别的格式(通常通过babel进行转换)
  3. Serialization :此阶段主要是将Transform后的模块进行序列化,组合所有模块生成一个或多个bundle。一个bundle就是一个JavaScript文件 react-native官方提供了一些基础命令
  • bundle: 根据传入的 JavaScript entry file 构建 bundle
  • start: 启动本地开发服务器

这些命令都是 @react-native-community/cli 这个包实现的,bundle和start命令内部通过调用metro来构建bundle。 题外话:在打包业务bundle时怎么排除common bundle的内容?

在打包common bundle和业务bundle时配置相同的 serializer.createModuleIdFactory 来生成 module id,打包common bundle后记录所有common bundle中的module id。随后在打包业务bundle时配置 serializer.processModuleFilter,如果发现 module id存在于common bundle module id中,则将此模块排除

3. 工程改造

接下来就是工程改造,将两个工程放到一个工程中,并在 pnpm-workspace.yaml 文件中配置workspace目录

yaml 复制代码
# pnpm-workspace.yaml
packages:
  - 'bundles/**'

工程目录如下

lua 复制代码
.
├── bundles/
│   ├── common/
│   │   ├── deploy/
│   │   ├── babel.config.js
│   │   ├── index.js
│   │   ├── metro.config.js
│   │   ├── package.json
│   │   └── README.md
│   └── business-plugin/
│       ├── deploy/
│       ├── src/
│       │   └── index.ts
│       ├── babel.config.js
│       ├── metro.config.js
│       ├── metro.config.dev.js
│       ├── index.dev.js
│       ├── package.json
│       └── README.md
├── .eslintrc.js
├── .gitignore
├── .gitlab-ci.yml
├── .prettierrc
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
└── tsconfig.json

src/index.ts

ts 复制代码
import 'react'

// import other pages
// ...

其中bundles/common 是我们的common包,bundles/business-plugin是我们的业务包。

在scripts中新增两个命令

business-plugin/package.json

json 复制代码
{
    "scripts": {
        "dev": "react-native start --confg metro.config.dev.js",
        "build-android": "react-native bundle --config metro.config.js --entry-file src/index.ts --bundle-output dist/android/sp.android.js --assets-dest dist/android --dev false --reset-cache --platform android"
        "build-ios": "react-native bundle --config metro.config.js --entry-file src/index.ts --bundle-output dist/android/sp.android.js --assets-dest dist/android --dev false --reset-cache --platform android"
    }
}

3.1 验证 build 命令

至此工程基本已经配置好了,我们先来测试bundle是否能构建成功。运行 pnpm build-android 命令,果然不出所料报错了

这种情况显然是resolve react的过程报错了,奇怪的是明明已经安装了react,但为什么会找不到呢?于是我开始debug resolve的逻辑 可以metro-resolve 最终查找到react对应的package.json路径,这个路径肯定是存在的,因为我们已经安装了react,确实存在当前的node_modules中,但是这个if 判断结果却是false(当然下面会继续尝试resolve index文件,但仍然会失败),让我们继续 debug 进入 context.doesFileExist

可以看到内部会通过 jest-haste-map 构建的 hasteFS 查找文件,那么hasteFS._files 里面为什么没有包含react/package.json呢?

其实在一开始,metro 会先根据配置创建一个Jest-haste-map 实例,内部根据metro.config.js中的watcherFolders 配置来指定将watcherFolders目录下所有的文件加载到内存中(并监听变化的文件目录),即hasteFS._files属性(这是一个map数据结构,key为file的相对路径,value为文件信息),后续当resolve file时可以直接通过此数据判断文件是否存在)

可以看到,如果我们不传watcherFolders配置时,默认为当前执行脚本的目录(即process.cwd()),那么内部是如果查找watcherFolders目录下所有文件的呢?接着往下看

haste-jest-map会先检查当前环境是否已经安装[watchman](GitHub - facebook/watchman: Watches files and records, or triggers actions, when they change.) 且是否配置了useWatchman(如果没有配置,默认为true),如果两个条件都满足就使用watchman查找文件,否则使用node模块查找文件。为了方便理解,我们直接debug进入node模块

如果当前系统支持 find 原生命令则使用 find命令查找,否则使用nodejs fs模块查找,同样为了方便理解,我们直接debug到node fs查找文件的逻辑

可以看到,haste-jest-map 利用 fs.readdir api依次读取 watchFolders 目录下的文件,这里需要重点关注,hast-jest-map 排除了 symbolic link的文件(软连接) ,也就是说最终的hasteFS._files 中并没有包含软链接的文件。

debug到这里,我们应该可以知道metro resolve 失败的原因了。

3.1.1 resolve file失败原因总结

  • 使用了pnpm改造工程,默认pnpm会将所有的包提升至顶层目录的node_moduels/.pnpm中,在workspace node_module中只会保留一份软链接文件,并指向node_modeuls/.pnpm下对应的文件

  • 我们使用的metro版本为0.59.0,并不支持追踪软链接到真实路径(0.72.0版本开始支持resolver.unstable_enableSymlinks 配置),所以 react 解析到的路径仍然为plugin workspace下的node_modules/react,这是一个软链接路径

  • jest-haste-map会根据传入的watchFolder 配置查找所有文件并存入hasteFS._files,hasteFS._files会忽略软链接文件。metro resolve会通过hasteFS._files 查找文件是否存在。

所以 metro 尝试使用 plugin/node_modules/react 路径去hasteFS._file中查找,自然是会失败的。

3.1.2 resolve error解决

知道了resolve失败的原因,我们应该让metro能通过软链接追踪到真实的文件路径,同时配置 watchFolder 监听真正的文件目录

有以下几种方案

  • 配置 pnpm node-linkerhoisted , 这样就能保持node_modules与yarn classic同样的目录结构且不会有软链接

  • 升级metro版本到0.72.0,并配置resolver.unstable_enableSymlinks 和 watchFolders

  • 不升级metro,使用社区提供的 @rnx-kit/metro-resolver-symlinks 包来解决追踪软链接的问题,同时配置 resover.resolverRequest和watchFolder

因为我们使用 pnpm 来管理工程就是为了使用pnpm的优点,所以不考虑方案一。由于种种原因我们目前还不能升级metro版本,所以当前采用方案三。最终看到bundle可以成功构建! 🎉🎉🎉

3.2 验证start命令

build成功之后,我们还要继续测试start 命令,理论上start命令仅仅多了一个启动本地服务器的步骤,其余步骤与build一致,应该不会出现错误。

但是当我运行start 命令,并在浏览器输入 localhost:8080/index.dev.bundle (入口文件bundle) 访问并构建bundle时,熟悉的错误又出现了

通过debug,发现 在 hasteFS._files中确实找不到react对应的文件,这是为什么呢?按照上面的分析,我们已经配置了resover.resolverRequest和watchFolder,理论上不会出现这个错误才对呀?

那么我想问题应该出现在 @rnx-kit/metro-resolver-symlinks 中,于是我开始debug @rnx-kit/metro-resolver-symlinks源码

最终发现,resolverRequest接收到的platfom为null,导致了@rnx-kit/metro-resolver-symlinks 走了原生的 metro-resolver 逻辑,自然就无法追踪软链接了。

那么这个platform是怎么来的呢?继续debug

最终发现在 start 命令中,platform是从 url 的query获取的,然而为了方便测试,我没用真机,而是直接在浏览器输入 localhost:8080/index.dev.bundle , 并没有传入 platform参数,所以导致了后续resolve失败,🤡🤡🤡🤡 !!!在浏览器输入localhost:8080/index.dev.bundle?platform=android(事实上,native就是传入这样的URL来支持本地调试的)就成功了~~~

那为什么build命令不报错呢?

因为我们在构建命令中传入了 --platform android 参数~~~🙈🙈🙈

相关推荐
超级白的小白1 天前
Taro崩溃排查
前端·react native
朝阳391 天前
React Native【实战范例】登录页(含密码显示隐藏)
react native
朝阳392 天前
React Native【实战范例】水平滚动分类 FlatList
react native
EndingCoder2 天前
React Native 项目实战 —— 记账本应用开发指南
javascript·react native·react.js
程序员小张丶2 天前
基于React Native的HarmonyOS 5.0房产与装修应用开发
javascript·react native·react.js·房产·harmonyos5.0
程序员小刘2 天前
HarmonyOS 5对React Native有哪些新特性?
react native·华为·harmonyos
朝阳392 天前
React Native【实战范例】银行卡(含素材)
react native
EndingCoder2 天前
React Native 构建与打包发布(iOS + Android)
android·react native·ios
William Dawson2 天前
【React Native 性能优化:虚拟列表嵌套 ScrollView 问题全解析】
react native·react.js·性能优化
EndingCoder2 天前
React Native 性能优化实践
react native·react.js·性能优化