NX 重构 RN 项目
背景
所负责的一个 rn 项目,分为用户端和医生端,这两个端有很多可复用的地方,比如相似的界面,相似的组件,相似的逻辑。
在此前的架构设计可能是前期工期比较紧张的原因,简单粗暴地将将两个端放置在同一个仓库当中,仅通过文件夹作为区分
├── src
│ ├── assets
│ ├── components
│ ├── hooks
│ ├── client
│ ├── doctor
│ └── index.ts
├── package.json
└── README.md
大概就是类似这样的一个结构。
这带来了什么问题呢?
- 统一入口为 index.ts, 所有的 client 和 doctor 的页面都在这里注册,导致最终 bundle 出来的 包非常大,对用户端来说多了很多医生端的代码,同理对医生端来说,多了很多用户端的代码。
- 无法评估影响点,例如仅修改医生端的代码,构建的时候会把用户端的部分也重新构建。并且作为开发者而言,我们也不好评估修改某些代码时会影响到哪一端。
- 一旦构建就是两端同时构建,这导致构建时间变长。
解决方案
其实上面的问题本质上都是因为入口单一,都通过 index.ts 作为注册页面的入口。比较容易想到的是,把入口拆分成 client-index.ts 与 doctor-index.ts 类似这样的。
但这仍然存在问题:
项目是通过 gitlab ci 持续集成构建的,当有代码变更的时候,就会执行构建脚本。但 gitlab 显然无法得知需要构建的是 client 还是 doctor,如果不管三七二十一,我都构建,那么显然上述 2 和 3 的问题是无法解决的。
为了解决上面的问题,即如何能获知代码变更造成的影响点,我逐渐把目光放到了 monorepo 上。
monorepo
提到 monorepo,第一个反应应当是 lerna,但当时的 lerna 还是不能满足上面的需求。然后我又看了一些目前比较热门的 monorepo 框架,如 nx / rush 等。此时我发现一个 issue https://github.com/lerna/lerna/issues/3121 没错,在这个 issue 里提到,lerna 将转给 nx 来维护,由此我打算仔细研究一下 nx。
nx
翻阅文档可以看到 nx 对比 lerna
- Parallelization and task dependencies - Nx automatically knows how your projects relate to each other. As a result, if
project-adepends onproject-band you run the build command forproject-a, Nx first runs the builds for all ofproject-a‘s dependencies and then the invoked project itself. Nx sorts these tasks to maximize parallelism.- Only run what changed - Using Nx affected commands you only really execute tasks on the projects that changed, compared to a given baseline (usually the main branch).
- Caching - You get Nx’s computation caching for free. All operations, including artifacts and terminal output are restored from the cache (if present) in a completely transparent way without disrupting your DX. No configuration needed. Obviously this results in an incredible speed improvement.
nx 对比 lerna 强在于任务编排
- nx 可以自动分析项目依赖,在执行任务时可以将前置依赖的package 构建
- 通过 nx affected 命令,nx 可以仅运行有变更的项目
- nx 可以缓存构建结果
从上述看出,nx 完美符合我的需求,因此打算将项目通过 nx 进行重构。
重构的过程及踩坑
重构的步骤比较简单:
- 由于结构变动比较大,因此直接初始化一个 react native 项目
npx create-nx-workspace your-workspace-name \ --preset=react-native \ --appName=your-app-name npm install @nrwl/react-native --save-dev nx g @nrwl/react-native:app doctor nx g @nrwl/react-native:app client - 将公共的代码抽成 libs
nx g @nrwl/react-native:library shared-hooks nx g @nrwl/react-native:library shared-components nx g @nrwl/react-native:library shared-assets - 修改引用,之前引用公共代码类似
import SomeComponent from '@components',现在则更新为import SomeComponent from 'your-workspace-name/shared-component'。注意这里还需要修改一下 tsconfig 里的别名,以方便import SomeImage from 'your-workspace-name/shared-assets'"your-workspace-name/shared-assets/*": ["libs/shared-assets/src/*"] - 重点在 package 的脚本,构建脚本调整为
当 gitlab ci 运行时,执行"release": "nx affected --target=release --base=origin/main~1 --head=origin/main",npm run release这个脚本,此时nx affected会检测到 origin/main~1 与 origin/main 不同的地方,找到受影响的项目,并且执行这些项目的 release 命令 - 上面提到的
--target=release中的 release 是自定义的 excutor,即 nx 的执行脚本,关于这部分可以参考 https://nx.dev/executors/run-commands-builder 实际上最主要的功能就是执行 react-native-cli 的 bundle(当然 nx 也内置了 bundle 的 excutor)
踩坑
以上都比较顺利,唯一的坑点在于,导入 shared-assets 的图片在 app 中并不会加载出来。一开始我以为是 nx 的问题,后来经过探索发现实际上是 metro 的问题(react-native-cli 底层打包工具)。具体可以查看这个 issue https://github.com/facebook/metro/issues/290#issuecomment-543746458 大概的原因是因为 metro 在打包资源的时候,会将资源的相对路径也编译进去,引用外层的资源时,路径中就会包含 ../ 然而最终生成的代码目录显然找不到这样的路径,由此加载会失败。怎么办呢,这个 issue 里有人提出用类似 dir 这样的特殊字符来替换 ../
我在尝试了上述 https://github.com/facebook/metro/issues/290#issuecomment-917695784 的方案后,开发时正常,但是在最终构建之后依然无法显示出来(时间有点久了,忘记具体原因了,大概原因还是打包后的路径依然不正确导致的)。
最终结合了 issue 里的方案,调整了个目前运行起来没问题的方案(不得不吐槽下 metro 的文档,有一些 api 就一笔带过,只能翻 issue 看用法)
// metro.config.js
const { withNxMetro } = require('@nrwl/react-native');
const { getDefaultConfig } = require('metro-config');
const exclusionList = require('metro-config/src/defaults/exclusionList');
module.exports = (async () => {
const {
resolver: { sourceExts, assetExts },
} = await getDefaultConfig();
return withNxMetro(
{
// ...
// 将请求资源的路径 ../ 替换成 dir/
assetPlugins: [require.resolve('./metro-asset-plugin')],
},
server: {
// 开发环境替换图片资源引用路径,否则无法找到正确的路径
enhanceMiddleware: (middleware) => {
return (req, res, next) => {
if (req.url.indexOf('/dir') !== -1) {
req.url = req.url.replace(/dir\//g, '../');
}
return middleware(req, res, next);
};
},
},
// ...
},
// ...
);
})();
// metro-asset-plugin.js
module.exports = function (assetData) {
if (assetData.httpServerLocation.indexOf('../') !== -1) {
assetData.httpServerLocation = assetData.httpServerLocation.replace(
/\.\.\//g,
'dir/'
);
}
return assetData;
};
核心就在于 server.enhanceMiddleware 与 transformer.assetPlugins 替换图片资源引用路径。
总结
上述仅简单概述了一下通过 monorepo 重构 RN 项目的过程,隐去了不少细节,但通过阅读文档应当也能摸索出具体操作,这里就不再赘述。
通过上述的重构之后,构建后用户端体积减少至原来的 30%,而医生端则降低到原来的 80%,效果还是比较显著的。
当然 nx 的作用不仅于此,因为不是重点,因此也不打算展开。