本文主要是介绍现代前端工程化——探秘 Monorepo 仓库管理方式,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
monorepo 和 multirepo
一般大到分项目,小到写代码,都遵循着拆分的原则,尽量不在一个项目、方法里糅合太多的东西。对于这种管理仓库的风格,称之为 multirepo
。这样做的好处是,按不同项目可以分为多个仓库,仓库之间保证独立,独立开发独立部署,保证了每个项目之间不受其他项目影响。
另一种风格就是 monorepo
了,也就是把多个项目放到一个仓库中,虽然把所有东西放到一起听起来很糟糕,但不仅仅是这样。对于 monorepo
而言,你只是把多个项目按你所设的某种场景把它组织到了一起,它的粒度还是保持原有的划分,对于某个团队的某个开发人员而言,他的关注点还是在其中一个项目。可能听上去这种方式相对于 multirepo
而言有些多此一举,我已经把仓库拆分了,为何还要再组织回去呢?对于一些前端项目而言,它的确有组织回去的好处,接下来就具体分析一下。
monorepo
并不是一种工具,它只是对于项目的一种管理手段,一种思维方式。
monorepo 的优点
简化组织
很大一部分情况下,我们在一开始开发项目的时候并不能保证项目的规模只有这么大,随着不断的迭代业务会越来越多,逻辑越来越复杂,于是就要对项目进行拆分。又或者是几个不同的但都处于某一块业务的项目。用这时候使用 monorepo
的方式来管理这些项目,能够简化项目结构组织。用 Dan Luu(参考文档三)的观点来说,很多拆分都不是正确的拆分。可能只是因为代码量多,或其他非必要原因对仓库进行了拆分。那就需要再重新将他们组织回来。
减少重复依赖项
对于前端项目,npm install
,npm run dev
这些步骤已经刻在我们的基因里了。但是 install 安装了庞大的 node_modules
后,其实大部分项目所安装的库都有重复。那我们应该把这些依赖项提取出来,而都引用了这些依赖的子项目只需要通过软链接这样的方式引入依赖项即可。
跨项目开发
如果我们同时在给几个 npm 模块进行功能迭代开发,在多个仓库下调试起来可能不是那么方便,还需要手动维护 npm link
,通过 monorepo
可以直接在本地进行夸项目间的联调,提高开发的效率。
更方便的仓库管理
对于一些大型、开源的项目来说,多个仓库意味着你要在多个地方进行 issue 之类的处理,你会更倾向于统一这些类似的 issue,管理很多 PR 和 git 钩子等等。
实践
Lerna
前面有讲过,既然 monorepo
不是工具,那我们就需要一个工具来实现它,在参考了一些文章后,我决定使用 Lerna(参考文档二)。
A tool for managing JavaScript projects with multiple packages.
这是 Lerna 官网对于它作用的描述,用来管理多个 JS 项目。不仅可以用 Lerna 来实现对多个项目仓库的管理,也可以用来管理多个 npm 包的维护发布。vue-cli(参考文档五),Babel(参考文档六)等知名项目也使用了 Lerna,大佬们都用了,那我们跟着梭哈就完事儿了。
然后我们只要通过命令 npm install --global lerna
将 Lerna 安装到全局。然后就可以开始了。
通过命令 lerna init
来初始化 Lerna 的目录,初始化完可以看到目录特别简单:
- packages
- package.json
- lerna.json
我们只要把项目放到 packages 文件夹下面即可。
在 lerna.json
中就是对 Lerna 的一些配置了,package.json
是用来管理主仓库的依赖之类的。
lerna.json
{"npmClient": "npm","packages": ["packages/*"],"version": "0.0.0"
}
npmClient
指定了安装使用的方式,可以用 npm 也可以用 yarn。packages
里面定义了子项目的路径,可以根据你想要的逻辑自由定义路径。
那么接下来就要实现一下刚才提到的关于依赖项的优化,Lerna 提供了一个命令:lerna bootstrap
来安装所有 package 的依赖项。运行这个命令相当于给每个项目都运行了 npm install
。可我们想要的是把所有 package 的共同依赖提取到一个公共的地方,所以 Lerna 也提供了参数 --hoist
来提取共同的依赖项到主项目的 node_modules
中。
运行命令:lerna bootstrap --hoist
这时候主项目中会有一个 node_modules
里面放了一些公共的依赖,而用到了该依赖的项目,会在 node_modules/.bin
这个目录下生成一些符号链接来连接到公共的 node_modules
(软链接)。这样就无需修改每一个项目的 package.json
了。
说到这里,有一个问题。如果项目中依赖的包版本不同怎么办?比如 A 项目使用了 Vue3,B 还在使用 Vue2.6。
Lerna 对于版本问题的解决方式是在公共的 node_modules
中安装版本最高的,再在低版本项目的node_modules
中独立为期安装依赖,并且在命令行中抛出警告。比如我在两个子项目中一个安装了 Vue2.6,另一个安装了 Vue3.0。
同时这样我们也可以清楚的发现不同项目中依赖的包版本有差异的问题,有时候我们最先做的 A 项目安装某一个包时,安装了 2.6.1 版本,之后在 B 项目中安装该依赖时已经有了 2.8.1 版本。这两个版本或许可以直接升级,并不会造成影响。但是手动去维护不同项目的依赖版本效率又特别低,通过 Lerna 可以很清楚的发现这样的版本差异问题。
如果需要开发多个 npm 包也能够使用 Lerna,就像 vue-cli 那样。简单举个例子,我们先创建两个项目:
lerna create module-a
lerna create module-b
根据命令行提示配置好一些 npm 的基本信息后,就能在目录下看到项目了:
- packages- module-a- __tests__ - lib- module-a.js- package.json- README.md- module-b- __tests__ - lib- module-b.js- package.json- README.md
在 module-a.js
中引入模块 b,并把模块 b 添加到模块 a 的依赖项中:
module-a.js
const moduleB = request('module-b');
package.json
"dependencies": {"module-b": "0.0.1"
},
然后运行命令 lerna link
后就能看到模块a中的 node_modules
引入了模块b的软链接。
开发完成后,就要把 npm 包发布到对应的仓库了,可以通过命令 lerna publish
发布。
到这里维护各个 npm 模块的流程就结束了,但是按照 multirepo
的话,我们可能要做几件事:
修改 module-b 的版本号
因为 module-a 依赖了 module-b,先修改 module-a 的依赖,再修改版本号
逐个发布包
这次只是个例子,如果实际开发中有几十个 npm 互相依赖就会造成不小的困扰了。那么对于其他的命令和详细的用法可以参考 Lerna 的文档(参考文档七)。
submodule
还有一种思路,在 Lerna 项目中,我们可以通过 git submodule
来引入不同的子项目仓库(Lerna 项目本身也是一个仓库,称之为主仓库)。
可以在 Lerna 项目根目录运行命令 git submodule add git@github.com:CUndH/mono-subA.git packages/
,把一个 git 仓库放到 packages 下面。添加后,就可以在根目录下看到一个名为 .gitmodules
的文件,大致如下:
[submodule "packages/mono-subB"]path = packages/mono-subBurl = git@github.com:CUndH/mono-subB.git
[submodule "packages/mono-subA"]path = packages/mono-subAurl = git@github.com:CUndH/mono-subA.git
很清晰,不用多解释,就是一些子仓库的配置信息。
这时候,如果我是负责 mono-subA
项目的开发人员,我不需要去了解主仓库相关的信息。我的关注点还是在 mono-subA
项目上,而主项目不参与具体业务开发,只用于管理项目的版本、发布,便于导航、查阅代码提交日志等,可以交由负责人或者 CI 去进行一些拉取合并代码、发布生产环境等操作。
可以通过 git diff
命令清楚的看到不同子仓库的修改记录:
-dirty
后缀代表这一次修改还没被 commit。
在主项目中添加子项目的时候,git submodule
所添加的子仓库会对应到具体的某一次 commit 上,具体对应的 commit 是当前主项目中对于子项目最近的一次修改。所以在仓库里会这样显示:
所以如果子仓库开发完毕提交了代码后,主仓库所指向的子仓库还是前一次 commit 的仓库,我查了一些文档说可以通过 git submodule update --remote
来获取子仓库的最新代码,这里有个小问题。如果直接运行该命令可能会报错,因为 git 会默认拉取子仓库的 origin/master
分支代码,而现在在 github 创建新仓库默认只有 main 分支了。如果你想要指定子仓库的拉取分支,你可以在 .gitmodules
文件中配置 branch:
[submodule "packages/mono-subA"]path = packages/mono-subAurl = git@github.com:CUndH/mono-subA.gitbranch = main // 加上这行
对于子仓库的修改,当你 commit 和 push 之后,在主仓库也会有一个待提交的 commit,需要再在主仓库中再提交一遍更新才行,这里有点多此一举的味道,但是我认为这可以更好的控制多个子项目的版本的一致性。假设我们每周会更新一次主仓库来进行发布生产环境这样的操作,那么这一步就能确保主仓库可以准确的控制某一个子仓库的代码更新,而不会因为不可控引起一颗老鼠屎坏一锅粥的情形。
monorepo 的缺点
无论是对于代码层面的设计,亦或是仓库管理层面的设计思想,都像踩跷跷板一样,没有最好,只有最适合。所以 monorepo
也有一些不足的地方:
无法管理某个、某些项目对于指定人员的权限
不同分支下的版本控制会显得较为混乱
对于发布构建的挑战,难度会比单个项目构建要大
不适用于业务相对零散、项目之间关系不大的场景
结尾
monorepo
和 multirepo
并没有哪个更胜一筹,只是不同的管理方式而已,本文也并不是说 monorepo
更好,我查阅的一些文章中也看到了不少反对意见,只要能从中学习到自己本不了解的东西就好,况且在现实中我们对于类似的技术的学习和使用本就不应该做选择题。
参考
文档一:Git 命令 - submodule:https://git-scm.com/docs/git-submodule
文档二:Lerna 官网:https://lerna.js.org/
文档三:dan luu 的博客:https://danluu.com/monorepo/
文档四:Monorepo 可能没你想象的那么香:https://baijiahao.baidu.com/s?id=1663844045880515134&wfr=spider&for=pc
文档五:vue-cli:https://github.com/vuejs/vue-cli
文档六:Babel:https://github.com/babel/babel
文档七:lerna 文档:https://github.com/lerna/lerna
全文完
以下文章您可能也会感兴趣:
Linux 的 IO 通信 以及 Reactor 线程模型浅析
浅谈 BI 与数据分析的可视化
iOS 照片涂鸦功能的实现
捋一捋 App 性能测试中的几个重要概念
所谓 Serverless,你理解对了吗?
如何写好产品中的提示文案
Web 开发打印总结
JVM 揭秘: 一个 class 文件的前世今生
Android 图片编辑的原理与实现 -- 涂鸦与马赛克
微服务环境下的集成测试探索(二)—— 契约式测试
微服务环境下的集成测试探索(一) —— 服务 Stub & Mock
Objective-C 中的语法糖
Facebook、Google、Amazon 是如何高效开会的
谈谈到底什么是抽象,以及软件设计的抽象原则
后端的缓存系统浅谈
从 React 到 Preact 迁移指南
如何成为一名数据分析师:数据的初步认知
复杂业务状态的处理:从状态模式到 FSM
聊聊移动端跨平台数据库 Realm
我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。
这篇关于现代前端工程化——探秘 Monorepo 仓库管理方式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!