图片来自:https://unsplash.com
本文作者: ddx
背景
目前云音乐内有多个RN收银台场景散布在不同的工程,比方页面收银台,浮层收银台,共性收银台等,后续可能还会有别的收银台场景。
那在开发过程中存在的问题就是每个收银台的外围逻辑如商品展现、领取形式展现、下单购买等逻辑都大致相同,而每次有批改或者新增需要的时候都须要开发屡次,反复代码较多效率低下。
尽管能够通过发npm包的模式复用代码,然而有些组件和代码块不太好抽成包,还会带来调试麻烦,发版等问题。所以为了进步代码复用,进步开发效率,咱们心愿可能在一个仓库内蕴含多个工程,也就是Monorepo模式。
Monorepo
什么是Monorepo
Monorepo是一种将多个我的项目的代码集中在同一个仓库中的软件开发策略,与之对抗的是传统的MultiRepo策略,即每个我的项目在一个独自的仓库进行治理。
目前像社区内一些驰名的开源我的项目Babel、React和Vue等都是用这种策略来治理代码。
Monorepo解决的问题
要想晓得Monorepo解决了哪些问题与其劣势,咱们先来看下MultiRepo存在的问题。
当咱们在MultiRepo下两个工程之前须要复用一些代码时,往往会采纳抽成npm包的模式。但当npm包有改变时咱们须要做以下事件:
- 批改npm包代码,通过npm link与两个工程调试
- 调试实现后公布新版本
- 两个工程降级npm包新版本,再进行公布
整个流程能够看出还是比拟繁琐的,那如果是在Monorepo下咱们能够将公共局部抽成一个workspace,咱们的两个工程别离也是workspace能够间接援用公共workspace的代码,工具会帮咱们治理这些依赖关系,开发过程中调试起来也十分不便,而且不波及到发包,版本依赖等,公共局部代码改变实现后两个工程部署即可。
从上述能够看出Monorepo次要有代码复用容易、调试不便和简化依赖治理等长处,这也是咱们抉择这个计划的起因。
当然Monorepo也有一些毛病,比方:仓库体积大、工程权限不好管制等。所以不论是Monorepo还是MultiRepo都不是完满的计划,只有能解决当下的问题就是好计划。
Monorepo的工具
目前业界最常见的实现monorepo工具和计划有lerna、yarn workspace和pnpm等。
Lerna
lerna是一个通过应用git和npm来优化多包仓库管理工作流的工具,多用于多个npm包相互依赖的大型前端工程,提供了许多CLI命令帮忙开发者简化从npm开发,调试到发版的整个流程。然而目前已官宣进行保护。
Pnpm
pnpm是一个新型的依赖包管理工具,并反对workspace性能,它的劣势次要是通过全局存储和硬链接来来磁盘空间并晋升装置速度,通过软链接来解决幻影依赖问题。然而RN的构建工具metro对于符号链接的解析还存在问题须要革新,老本较大。
Yarn workspace
yarn workspace是yarn提供的Menorepo依赖管理机制,是一个底层的工具,用于在仓库根目录下治理多个package的依赖,人造反对hoist性能,装置依赖时会将packages中雷同的依赖晋升到根目录,缩小反复依赖装置。workspace之间的援用在依赖装置时通过yarn link建设软链,代码批改时能够在依赖其的workspace中实时失效,调试不便。
通常业界支流计划是lerna + yarn worksapce,lerna负责公布和版本升级,yarn workspace负责依赖治理。因为咱们的RN工程是页面工程,不波及到发npm包,而且须要依赖晋升的性能(这个前面会说到),所以最终采纳yarn worspace计划。
Metro
在工程革新之前,咱们先理解下ReactNative的构建工具Metro。
Metro在构建过程中次要会经验三个阶段:
- Resolution:此阶段Metro会从入口文件登程剖析所依赖的模块生成一个所有模块的依赖图,次要是应用jest-haste-map这个包做依赖剖析。这个阶段和Transformation阶段是并行的;
- Transformation:此阶段次要是将模块代码转换成指标平台可辨认的格局;
- Serialization:此阶段次要是将Transform后的模块进行序列化,而后组合这些模块生成一个或多个Bundle
jest-haste-map是单元测试框架Jest的其中一个包,次要用来获取监听的所有文件及其依赖关系。
工程革新
接下来就是对工程的革新,首先咱们将两个RN工程放在一个工程下,并依照yarn workspace的形式进行配置,而后通过脚手架(这里应用的是公司外部自研的脚手架)别离创立app-a和app-b两个RN工程,如下所示
rn-mono|-- apps |-- app-a |-- app-b|-- package.json
// package.json{ ... "workspaces": { "packages": [ "apps/*" ] }, "private": true}
接着咱们运行
yarn install
发现packages中雷同的依赖都会装置在根目录下的node_modules中,接着咱们用如下启动app-a或app-b
yarn workspace app-a run dev
这时如果你的app-a工程中的dev启动命令是用相对路径的形式可能会呈现命令找不到的状况,比方
// app-a/package.json{ // 这里的react-native是装置在了根目录,所以会找不到命令,须要批改下门路 "script": { "dev": "node ./node_modules/react-native/local-cli/cli.js start" }}
那如果是调用./node_modules/.bin
中的命令则不须要,因为在装置依赖的时候packages中.bin
中的命令会有个软链指向根目录下./node_modules/.bin
中的命令。启动胜利后,这时关上页面会报如下谬误:
这是因为jest-haste-map在做依赖剖析时通过metro.config.js中的watcherFolders配置项来指定须要监听变动的文件目录。
watcherFolders默认值为工程根目录,此时也就是app-a中目录,然而咱们的模块都是装置在根目录下,所以会找不到。咱们须要批改下metro.config.js中watcherFolders
// app-a/metro.config.jsconst path = require('path');module.exports = { watchFolders: [path.resolve(__dirname, '../../node_modules')],};
批改实现后咱们重新启动,再关上页面后发现曾经能够失常关上了,同样的形式app-b也能够失常运行。
然而咱们对工程进行monorepo革新的目标是为了抽离公共组件,复用代码。所以咱们在根目录下建设个common的文件夹来寄存公共局部,此时根目录下的pacage.json中的packages和apps里每个app的metro.config.js中watchFolder配置都须要退出common
rn-mono|-- common |-- package.json|-- apps |-- app-a |-- app-b|-- package.json
// package.json{ ... "workspaces": { "packages": [ "apps/*", "common" ], }, "private": true}// apps/app-a/metro.config.jsconst path = require('path');module.exports = { watchFolders: [path.resolve(__dirname, '../../node_modules'), path.resolve(__dirname, '../../common')],};
接着在common中增加个Button组件,package.json中增加相应的依赖,版本要和apps中对应依赖的版本保持一致
{ ... "dependencies": { "react": "16.8.6", "react-native": "0.60.5", },}
而后yarn install重新安装下,这时在根目录的node_modules下就能够看到common模块软链到了common目录,所以在app-a中引入common时就能够像npm包一样间接引入,同样app-b也能够。
import common from 'common';
到这里咱们RN工程的monorepo革新也根本实现了。
依赖晋升
这里解释下为什么须要依赖晋升。
咱们先来看下勾销依赖晋升会有什么问题,能够在根目录中的package.json中nohoist配置来指定不须要晋升装置到根目录的模块
{ ... "workspaces": { "packages": [ "apps/*", "common" ], "nohoist": ["**react**"], }, "private": true}
而后从新yarn install,启动app-a后会发现报如下谬误
这是因为有些模块jest-haste-map在做依赖剖析生成dependency graph时发现在两个不同的目录下会产生命名抵触,导致报错。所以咱们须要依赖晋升,将所用到的雷同依赖装置到根目录,这样只会装置一次。
雷同依赖的版本保持一致
尽管有了依赖晋升但如果每个packages中雷同依赖的版本不统一,同样会导致雷同的依赖会装置屡次的状况呈现,根目录和对应的package中都会有。这种状况除了会产生以上问题外还有可能产生其余潜在的问题,比方依赖客户端的第三方模块,如果存在多个版本在bundle执行时会屡次注册组件导致组件注册失败,在调用时会产生找不到组件的报错。
尽管能够在metro中配置blacklistRE和extraNodeModules来表明要读取哪个地位的依赖,然而这种形式并不通用,每次在引入新的依赖时都要去配置下较为繁琐。所以咱们须要将每个packages中的依赖版本保持一致。
人为的去约定这个规定必定是不平安的,能够开发一个依赖版本的lint检测工具,在提交代码的时候做强制性的检测。
咱们最终的计划是开发一个检测脚本联合gitlab-ci在分支代码push的时候检测,未通过则不容许push代码来防止危险。
// .gitlab-ci.ymltest-dev-version: stage: test before_script: - npm install --registry http://rnpm.hz.netease.com script: - npm run depVerLint only: changes: - "package.json" - "packages/**/package.json"
工程迁徙过渡
如果是将多个正在疾速迭代的工程迁徙到一个Monorepo仓库时,必定会遇到存量开发分支代码同步问题。比方咱们要将工程A迁徙到新仓库,如果咱们只是基于master分支将代码copy到新工程,并在革新开发过程中还有组内其他同学也在基于master拉取分支做开发,并在你革新实现前开发实现合并到了master,此时你新工程的代码是落后的,要想同步只能手动copy改变的代码,很容易出错。为了解决这个问题咱们能够应用git subtree。
git subtree容许将一个仓库作为子仓库嵌套在另一个仓库里,所以这里咱们能够将工程A作为一个子工程增加到Monorepo新工程对应的packages目录下,如果有更新能够间接应用pull进行同步。
# 增加git subtree add --prefix=apps/app-a https://github.com/xxxx/app-a.git master --squash# 更新git subtree pull --prefix=apps/app-a https://github.com/xxxx/app-a.git master --squash
对于新工程或者新的开发分支就能够间接此工程下进行开发了。
构建
因为咱们的构建机还不反对yarn,所以间接应用yarn workspace的命令是有问题的。目前的做法是将yarn作为devDependency,而后在根目录下创立个脚本文件,将每个package的构建命令收敛在一起。联合yarn workspace的命令,这样只须要在构建时传入不同的package name即可。
## scripts/build.shPLATFORM=$1PROJECT=$2EXEC_PARAMS=${@:2}YARN="${PWD}/node_modules/.bin/yarn"...echo "start yarn install"${YARN} cache clean${YARN} installecho "start build"echo "${YARN} workspace ${PROJECT} run build:${PLATFORM} ${EXEC_PARAMS}"${YARN} workspace ${PROJECT} run build:${PLATFORM} ${EXEC_PARAMS}
// package.json{ ... "workspaces": { "packages": [ "apps/*" ], }, "private": true, "scripts": { "build": "./script/build.sh" },}
比方对app-a进行构建,就能够
npm run build ios app-a## 实际上执行的是yarn workspace app-a run build:ios
总结
至此对React Native工程的menorepo革新根本实现了,对于多个性能相似的工程采纳Monorepo的治理形式的确会不便代码复用和调试,进步咱们的开发效率。如果公司外部其余场景有相似的需要,将来布局能够将其积淀出一个脚手架。
目前对于h5工程的Monorepo计划曾经较为成熟了,然而对RN工程来说因为构建机制不同无奈齐全实用,可参考的材料也较少。本文也是通过实际记录了一些踩坑教训,如果你有更好的实际,欢送留言一起探讨。
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!