当前位置: 首页 > Web前端 > HTML

ReactNative项目Monorepo改造实践

时间:2023-03-28 11:26:03 HTML

图片来自:https://unsplash.com本文作者:ddx后台目前云音乐中有多个RN收银台场景分布在不同的项目中,如页面收银台、浮层收银台、个性化收银台等,以后可能还会有其他收银台场景。开发过程中的问题在于,每台收银机的核心逻辑,如商品展示、支付方式展示、订单购买等,大致相同,每次有修改或新增需求,都需要多次开发。重复的代码效率更低。虽然通过npm包的形式可以复用代码,但是有些组件和代码块不容易打包,会造成调试麻烦和发布问题。所以,为了提高代码复用,提高开发效率,我们希望能够在一个仓库中包含多个项目,这就是Monorepo的形式。Monorepo什么是MonorepoMonorepo是一种将多个项目的代码集中在同一个仓库中的软件开发策略,相对于传统的MultiRepo策略,即每个项目都在一个单独的仓库中进行管理。目前社区中一些知名的开源项目如Babel、React、Vue等都是采用这种策略来管理代码。Monorepo解决的问题要知道Monorepo解决了哪些问题和优势,我们先来看看MultiRepo存在的问题。当我们需要复用MultiRepo中两个项目之前的一些代码时,我们往往会采用将它们解压到npm包中的形式。但是当npm包发生变化时,我们需要做如下操作:修改npm包代码,通过npmlink调试调试两个项目后发布新版本比较麻烦,如果是在Monorepo下,我们可以解压公共部分到工作区。我们两个项目也是工作区,可以直接引用公共工作区的代码。该工具将帮助我们在开发过程中管理这些依赖关系并进行调试。也很方便,不涉及签约、版本依赖等,修改完代码公共部分后即可部署两个项目。从上面可以看出,Monorepo主要有代码复用容易、调试方便、依赖管理简单等优点,这也是我们选择这个方案的原因。当然,Monorepo也有一些缺点,比如:仓库体积大,工程权限控制不好等。所以无论是Monorepo还是MultiRepo都不是完美的解决方案,只要能解决当前的问题,就是很好的解决方案。Monorepo工具目前业界最常见的Monorepo工具和解决方案包括lerna、yarnworkspace和pnpm。Lernalerna是一个使用git和npm优化多包仓库管理工作流程的工具。多用于多个npm包相互依赖的大型前端项目。它提供了许多CLI命令来帮助开发者简化从npm开发、调试到发布的整个过程。但已经正式宣布停止维护。pnpmpnpm是一种新型的依赖包管理工具,支持工作区功能。它的优点主要是通过全局存储和硬链接来节省磁盘空间和提高安装速度,通过软链接解决幻象依赖问题。但是RN的构建工具metro在解析符号链接方面还是有问题,需要修改,代价很大。Yarn工作空间Yarn工作空间是yarn提供的Menorepo依赖管理机制。它是一个底层工具,用于管理仓库根目录下多个包的依赖关系。它自然支持提升功能。安装依赖时,包中相同的依赖会被提升到根目录,减少依赖的重复安装。工作空间之间的引用在依赖安装时通过yarnlink建立软链接。修改代码时,可以在依赖的工作空间实时生效,方便调试。通常业界主流的方案是lerna+yarnworksapce,lerna负责发布和版本升级,yarnworkspace负责依赖管理。因为我们的RN项目是页面项目,不涉及发送npm包,需要依赖改进功能(这个后面会讲到),所以最终采用了yarnworspace的方案。在进行Metro工程化改造之前,我们先来了解一下ReactNative构建工具Metro。Metro在构建过程中主要会经历三个阶段:Resolution:这个阶段Metro会从入口文件中分析依赖的模块,生成所有模块的依赖图,主要使用jest-haste-map包进行依赖分析。这个阶段与转型阶段平行;Transformation:这个阶段主要是将模块代码转换成目标平台可以识别的格式;Serialization:这个阶段主要是对转换后的模块进行序列化,然后将这些模块组合起来生成一个或多个Bundlejest-haste-map是单元测试框架Jest的包之一,主要用于获取所有监控文件和他们的依赖。工程改造的下一步是项目的改造。首先,我们将两个RN项目放在一个项目下,按照yarnworkspace的方式进行配置,然后通过脚手架(这里使用的是公司开发的脚手架)分别创建app。-a和app-b两个RN项目,如下图rn-mono|--apps|--app-a|--app-b|--package.json//package.json{..."workspaces":{"packages":["apps/*"]},"private":true}然后我们运行yarninstall,发现packages中相同的依赖会安装到根目录的node_modules中,然后我们启动app如下-a或者app-byarnworkspaceapp-arundev这时候如果你的app-a项目中的dev启动命令使用的是相对路径,可能会找不到该命令,比如//app-a/package.json{//这里的react-native安装在根目录下,所以找不到命令,需要修改路径"script":{"dev":"node./node_modules/react-native/local-cli/cli.jsstart"}}如果调用./node_modules/.bin中的命令,则不需要,因为安装依赖包时.bin中的命令会有软链接指向根目录./node_modules/.bin中的命令。启动成功后,打开页面会报如下错误:这是因为jest-haste-map在做依赖分析时,通过metro.config.js中的watcherFolders配置项指定了需要监听变化的文件目录.watcherFolders默认值是项目的根目录,也就是此时app-a中的目录,但是我们的模块是安装在根目录下的,所以不会被发现。我们需要修改metro.config.js中的watcherFolders//app-a/metro.config.jsconstpath=require('path');module.exports={watchFolders:[path.resolve(__dirname,'../../node_modules')],};修改完成后,我们重启,再打开页面发现可以正常打开,同理app-b也可以正常运行。但是我们对项目进行monorepo改造的目的是抽取通用组件,复用代码。所以我们在根目录下创建一个common文件夹,用来存放常用的部分。此时需要在commonrn-mono|--common|--package.json中添加根目录下package.json中的package和apps中各个app的metro.config.js中的watchFolder配置|--apps|--app-a|--app-b|--package.json//package.json{..."workspaces":{"packages":["apps/*","common"],},"private":true}//apps/app-a/metro.config.jsconstpath=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",},}然后重新安装yarninstall。此时在根目录的node_modules下,可以看到common模块软链接到common目录,所以在app-a中引入common时,直接像npm包一样导入即可。同样,app-b也可以。从“通用”导入通用;到这里我们的RN项目的monorepo改造就基本完成了。依赖提升在这里解释了为什么需要依赖提升。先来看看取消依赖提升的问题。可以在根目录的package.json中配置nohoist,指定不需要升级安装的模块到根目录{..."workspaces":{"packages":["apps/*","common"],"nohoist":["**react**"],},"private":true}然后重新yarninstall,启动app-a后会报如下错误。这是因为一些模块jest-haste-map在做依赖分析生成依赖图的时候,发现两个不同的目录会存在命名冲突,导致报错。所以我们需要依赖提升,将我们使用的相同依赖安装到根目录,这样它们只会安装一次。同一个依赖的版本一致虽然有依赖升级,但是如果每个包中同一个依赖的版本不一致,也会导致同一个依赖被多次安装,无论是在根目录下还是在对应的目录下包裹。除了上述问题,这种情况还可能导致其他潜在的问题,比如依赖客户端的第三方模块。如果有多个版本,bundle执行过程中会多次注册组件,导致组件注册失败。这将在调用Componentnotfound错误时发生。虽然可以在metro中配置blacklistRE和extraNodeModules来指明从哪个位置读取依赖,但是这种方法并不通用,每次引入新的依赖都要配置,比较麻烦。所以我们需要保持每个包中的依赖版本一致。人为地约定这个规则肯定是不安全的。可以开发依赖版本的lint检测工具,在提交代码时进行强制检测。我们最终的解决方案是结合gitlab-ci开发一个检测脚本来检测何时推送了分支代码。如果失败,则不允许推送代码以规避风险。//.gitlab-ci.ymltest-dev-version:stage:testbefore_script:-npminstall--registryhttp://rnpm.hz.netease.comscript:-npmrundepVerLintonly:changes:-"package.json"-"packages/**/package.json"项目迁移过渡如果将多个快速迭代的项目迁移到一个Monorepo仓库,肯定会遇到存量开发分支代码同步的问题。比如我们要将项目A迁移到新仓库,如果我们只是基于master分支将代码复制到新项目中,而在改造开发过程中,组内其他同学也在拉基于master的分支在master上进行开发,当你transform完成之前,开发完成并合并到master中。这时,你的新项目的代码已经过时了。如果要同步,只能手动复制改过的代码,容易出错。为了解决这个问题,我们可以使用gitsubtree。Git子树允许一个仓库嵌套在另一个仓库中作为子仓库,所以这里我们可以将项目A作为子项目添加到新建的Monorepo项目对应的packages目录下,如果有更新可以直接使用拉同步。#添加gitsubtreeadd--prefix=apps/app-ahttps://github.com/xxxx/app-a.gitmaster--squash#更新gitsubtreepull--prefix=apps/app-ahttps://github.com/xxxx/app-a.gitmaster--squash新项目或新开发分支,直接在该项目下开发即可。构建由于我们的构建机器还不支持yarn,所以直接使用yarnworkspace命令是有问题的。目前的做法是使用yarn作为一个devDependency,然后在根目录下创建一个脚本文件来收敛各个包的构建命令。结合yarnworkspace的命令,构建时只需要传入不同的包名即可。##scripts/build.shPLATFORM=$1PROJECT=$2EXEC_PARAMS=${@:2}YARN="${PWD}/node_modules/.bin/yarn"...echo"startyarninstall"${YARN}缓存清理${YARN}installecho"startbuild"echo"${YARN}workspace${PROJECT}runbuild:${PLATFORM}${EXEC_PARAMS}"${YARN}workspace${PROJECT}runbuild:${PLATFORM}${EXEC_PARAMS}//package.json{..."workspaces":{"packages":["apps/*"],},"private":true,"scripts":{"build":"./script/build.sh"},}比如构建app-a,可以npmrunbuildiosapp-a##其实就是执行yarnworkspaceapp-arunbuild:ios。至此,ReactNative项目的menorepo改造已经基本完成了,对多个功能相似的项目使用Monorepo管理方式,确实会方便代码复用和调试,提高我们的开发效率。如果公司内部的其他场景也有类似的需求,未来的规划可以将它们沉淀成脚手架。目前针对h5项目的Monorepo方案已经比较成熟,但是由于构建机制不同,无法完全应用到RN项目中,参考资料也很少。本文也通过实践记录了一些踩坑的经验。如果大家有更好的做法,欢迎留言一起讨论。本文由网易云音乐技术团队发布。未经授权禁止任何形式的转载。我们常年招聘各种技术岗位。如果你要跳槽,又恰好喜欢云音乐,那就加入我们吧grp.music-fe(at)corp.netease.com!

猜你喜欢