现代前端工程化——探秘 Monorepo 仓库管理方式

本文主要是介绍现代前端工程化——探秘 Monorepo 仓库管理方式,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

monorepo 和 multirepo

一般大到分项目,小到写代码,都遵循着拆分的原则,尽量不在一个项目、方法里糅合太多的东西。对于这种管理仓库的风格,称之为 multirepo。这样做的好处是,按不同项目可以分为多个仓库,仓库之间保证独立,独立开发独立部署,保证了每个项目之间不受其他项目影响。

另一种风格就是 monorepo了,也就是把多个项目放到一个仓库中,虽然把所有东西放到一起听起来很糟糕,但不仅仅是这样。对于 monorepo 而言,你只是把多个项目按你所设的某种场景把它组织到了一起,它的粒度还是保持原有的划分,对于某个团队的某个开发人员而言,他的关注点还是在其中一个项目。可能听上去这种方式相对于 multirepo 而言有些多此一举,我已经把仓库拆分了,为何还要再组织回去呢?对于一些前端项目而言,它的确有组织回去的好处,接下来就具体分析一下。

monorepo 并不是一种工具,它只是对于项目的一种管理手段,一种思维方式。

monorepo 的优点

简化组织

很大一部分情况下,我们在一开始开发项目的时候并不能保证项目的规模只有这么大,随着不断的迭代业务会越来越多,逻辑越来越复杂,于是就要对项目进行拆分。又或者是几个不同的但都处于某一块业务的项目。用这时候使用 monorepo 的方式来管理这些项目,能够简化项目结构组织。用 Dan Luu(参考文档三)的观点来说,很多拆分都不是正确的拆分。可能只是因为代码量多,或其他非必要原因对仓库进行了拆分。那就需要再重新将他们组织回来。

减少重复依赖项

对于前端项目,npm installnpm 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 的话,我们可能要做几件事:

  1. 修改 module-b 的版本号

  2. 因为 module-a 依赖了 module-b,先修改 module-a 的依赖,再修改版本号

  3. 逐个发布包

这次只是个例子,如果实际开发中有几十个 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 仓库管理方式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/181636

相关文章

Vue3使用router,params传参为空问题

《Vue3使用router,params传参为空问题》:本文主要介绍Vue3使用router,params传参为空问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录vue3使用China编程router,params传参为空1.使用query方式传参2.使用 Histo

AJAX请求上传下载进度监控实现方式

《AJAX请求上传下载进度监控实现方式》在日常Web开发中,AJAX(AsynchronousJavaScriptandXML)被广泛用于异步请求数据,而无需刷新整个页面,:本文主要介绍AJAX请... 目录1. 前言2. 基于XMLHttpRequest的进度监控2.1 基础版文件上传监控2.2 增强版多

Docker镜像修改hosts及dockerfile修改hosts文件的实现方式

《Docker镜像修改hosts及dockerfile修改hosts文件的实现方式》:本文主要介绍Docker镜像修改hosts及dockerfile修改hosts文件的实现方式,具有很好的参考价... 目录docker镜像修改hosts及dockerfile修改hosts文件准备 dockerfile 文

Linux中的计划任务(crontab)使用方式

《Linux中的计划任务(crontab)使用方式》:本文主要介绍Linux中的计划任务(crontab)使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、前言1、linux的起源与发展2、什么是计划任务(crontab)二、crontab基础1、cro

CSS Padding 和 Margin 区别全解析

《CSSPadding和Margin区别全解析》CSS中的padding和margin是两个非常基础且重要的属性,它们用于控制元素周围的空白区域,本文将详细介绍padding和... 目录css Padding 和 Margin 全解析1. Padding: 内边距2. Margin: 外边距3. Padd

CSS will-change 属性示例详解

《CSSwill-change属性示例详解》will-change是一个CSS属性,用于告诉浏览器某个元素在未来可能会发生哪些变化,本文给大家介绍CSSwill-change属性详解,感... will-change 是一个 css 属性,用于告诉浏览器某个元素在未来可能会发生哪些变化。这可以帮助浏览器优化

CSS去除a标签的下划线的几种方法

《CSS去除a标签的下划线的几种方法》本文给大家分享在CSS中,去除a标签(超链接)的下划线的几种方法,本文给大家介绍的非常详细,感兴趣的朋友一起看看吧... 在 css 中,去除a标签(超链接)的下划线主要有以下几种方法:使用text-decoration属性通用选择器设置:使用a标签选择器,将tex

Win11安装PostgreSQL数据库的两种方式详细步骤

《Win11安装PostgreSQL数据库的两种方式详细步骤》PostgreSQL是备受业界青睐的关系型数据库,尤其是在地理空间和移动领域,:本文主要介绍Win11安装PostgreSQL数据库的... 目录一、exe文件安装 (推荐)下载安装包1. 选择操作系统2. 跳转到EDB(PostgreSQL 的

前端高级CSS用法示例详解

《前端高级CSS用法示例详解》在前端开发中,CSS(层叠样式表)不仅是用来控制网页的外观和布局,更是实现复杂交互和动态效果的关键技术之一,随着前端技术的不断发展,CSS的用法也日益丰富和高级,本文将深... 前端高级css用法在前端开发中,CSS(层叠样式表)不仅是用来控制网页的外观和布局,更是实现复杂交

Python将博客内容html导出为Markdown格式

《Python将博客内容html导出为Markdown格式》Python将博客内容html导出为Markdown格式,通过博客url地址抓取文章,分析并提取出文章标题和内容,将内容构建成html,再转... 目录一、为什么要搞?二、准备如何搞?三、说搞咱就搞!抓取文章提取内容构建html转存markdown