/Monorepo——探秘源码管理新姿势!

Monorepo——探秘源码管理新姿势!

导语 | Monorepo是一个“单仓多包”的代码管理策略,由于众多大型厂商和开源项目在其上的实践,Monorepo受到了越来越多的关注,和其他已有的代码库管理方案相比,有着自身独特的优势。本文仅讨论Monorepo在前端开发场景中的应用及实践,里面提到的概念和示例都会有所局限,可依据实际情况自行扩展阅读其他资料。

代码(code)” 是程序员用开发工具所支持的语言写出来的源文件,用于实现或支持所有依托于计算机的程序及应用,因此,如何管理代码是开发人员在项目进程中非常重要的一环。

而“仓库(repository)”则是存储项目中所有代码文件和更改信息的重要载体和方式,它会帮助开发人员以“版本控制”的形式管理整个项目的生命周期。这里的仓库通常指代Git仓库,当然对其他比如Mercurial/Subversion的代码仓库也同样适用。而如何去设计仓库的代码管理策略将会直接影响项目的开发流程和使用体验。

而在近段时间,团队依据项目需求规划开发多个基于Vue3的组件集合,其中包括UI组件、Map组件、以及Chart组件,并采用Vitepress作为文档服务。但是在项目构建的过程中发现,如果采用通常的形式将其拆成三个仓库进行开发会使得管理变得复杂,极度相似的环境需要搭建三套。同时,文档的管理也变成了一个问题,每个仓库内建立一套文档服务和独立文档为一个仓库都将增加管理负担。在寻找技术解决方案的过程中,发现了Monorepo这样的代码管理策略,并进行了实际开发实践。

本文将会通过在项目中的实践经验进行总结和分享Monorepo在仓库代码管理上的策略理念,以及其在前端上面的技术实现。

一、Monorepo策略探索

(一)概念探索

  • 什么是Monorepo?


Monorepo可以理解为一种基于仓库的代码管理策略,它提出将多个代码工程“独立”的放在一个仓库里的管理模式,其中“独立”这个词非常重要,每个代码工程在逻辑上是可以独立运行开发以及维护管理的。Monorepo在实际场景中的运用可以非常宽泛,甚至有企业将它所有业务和不同方向语言的代码放在同一个仓库中管理,当然,这样的运用方式对企业的仓库底层能力要求相当高。因此,更多的Monorepo实践会根据业务和职能范围来进行组织。

  • 谁在用Monorepo?

Monorepo这个概念的提出已经有很长的历史,但直到最近几年,随着技术的更新迭代,以及各项工具链的完善,逐渐开始成为一个热门的话题,很多大型的互联网公司都在采取这样的代码管理策略,比如Google、Facebook、Uber、MicroSoft等。也有很多著名的前端开源库选择用这种方式来构建和管理自己的代码,比如Vue、React、Vite、Babel、Element-plus等。

  • 为何用Monorepo?

那么Monorepo解决了哪些项目代码管理上的问题,让这么多大型厂商和开源项目都纷纷尝试并投身其中?回答这个问题前,我们需要先引出另外两种较为普遍的管理策略:Single-repo Monolith和Multirepo,目前大部分项目所使用的都是这两种管理策略。接下来我们通过对比,来逐步探索对Monorepo的理解以及使用它的原因。

注意:后面所有的探索和实践都将基于前端开发的场景进行。

(二)对比探索

  • 架构模式发现

首先,我们先从最上层的架构入手,来看三种策略的分别是如何去划分和管理一个相对复杂,或者说拥有多个功能和业务模块的项目的。这里引用来自Guide to Monorepos for Front-end Code文章里的一张图,通过这张图可以很清晰的展示出三种策略各自的管理理念:

文章链接:https://www.toptal.com/front-end/guide-to-monorepos

从图中我们来分析三种策略在架构模式上核心的不同点:

  • Monorepo:只有一个仓库,并且把项目拆分多个独立的代码工程进行管理,而代码工程之间可以通过相应的工具简单的进行代码共享。

  • Single-repo Monolith:同样也只有一个仓库,而它并不会独立的分割每个代码工程,而是让他们成为一体来进行开发管理,模块的拆分取决于代码工程的设计。

  • Multi-repo:则是通过建立多个仓库,每个仓库包含拆分好的代码工程,而仓库间的调用共享则是通过NPM或者其他代码引用的方式进行。

虽然这样可以简单的区别三种策略,而实际情况下,这三种策略其实是可以相互转换,相互包含的。一个Monorepo里可以包含多个以Single-repo Monolith形式组织的代码。Multi-repo中的每个repo都可以是一个Monorepo等等。所以,灵活的使用每种策略和组合策略才能更加高效的管理更为复杂多变的情形,这个在后文也会集中讨论Monorepo适合的场景。

  • 仓库组织对比

除了从架构概念上的区分外,这里再构造一个简单前端场景:假设现在有两个可以逻辑上被分割的项目Project1和Project2以及他们共用的一个公共库lib。在这个假设的场景中,我们通过实际的代码目录组织和相互引用方式来更加直观的展示三种策略的不同:

在Single-repo Monolith这个策略下,Project和lib都会被组织在一个仓库当中,并会将两个Projects中代码进行杂糅,放在同一个代码工程当中(当然这个组织形式可以有很多种,具体根据实际场景以及架构师对模块的设计理念)。而lib代码会放在该工程目录下,两个Projects可以简单的通过路径去引用,也可以通过工具设置绝对地址alias来方便引入。最终整个项目会共同构建并部署。

// Repository - monolith.├── package.json├── src/│   ├── views/|   |   ├── project1/|   |   ├── project2/│   ├── router/|   |   ├── project1/|   |   ├── project2/|   ├── ...│   └── lib/└── README.md
// 代码共享 package1/example.jsimport {method} from '../../lib';
// script引入共享<script src="@static/lib/index.js"></script>

在Multi-repo这个策略下,两个Project会单独成立代码工程放入两个仓库当中。而lib也会独立成库进行开发,并通过构建后进行NPM发包,两个Projects需要通过NPM的形式安装和更新lib。最终项目分别独立打包并进行部署。

// Repository - project1.├── node_modules/├── package.json├── src/│   ├── views/│   ├── router/|   ├── ...├── README.md
// Repository - project2.├── node_modules/├── package.json├── src/│ ├── views/│ ├── router/| ├── ...├── README.md

// Repository - lib.├── node_modules/├── package.json├── src| ├── ...├── README.md

// 代码共享- lib进行发包,比如包名为 @my-scope/lib- 进入Package1 或 Package2 进行npm install 或 npm update- 在代码中引入import {method} from '@my-scope/lib';

在Monorepo这个策略下,将会把两个Projects和一个lib统一放到packages目录下面,每个都会作为独立的包进行开发运行,公用依赖可以放在一级的node_module中,各个package也可以有自己独有的依赖。这里以Pnpm为例来展示,在pnpm-workspace.yaml里配置把packages下的所有包视为子项目,纳入包管理。在lib目录下的pacakge.json中为其添name,同时添加到一级目录的package.json当中,即可被两个Projects引用,而无需进行发包操作。

// Repository - monorepo.├── node_modules/├── package.json├── packages/│   ├── pacakge1/|   |   ├── src/|   |   ├── README.md|   |   ├── node_modules/|   |   ├── pacakge.json│   ├── package2/|   |   ├── src/|   |   ├── README.md|   |   ├── node_modules/|   |   ├── pacakge.json│   └── lib/|   |   ├── src/|   |   ├── README.md|   |   ├── node_modules/|   |   ├── pacakge.json├── README.md├── pnpm-workspace.yaml

// pnpm-workspace.yamlpackages: - 'packages/*'
// 代码共享- 假设 lib 的包名为 @my-scope/lib,无需发包至NPM- 在一级目录的 package.json 添加包名 @my-scope/lib: "workspace:*"- 在两个 projects 中的代码中引入import {method} from '@my-scope/lib';

  • 发展趋势分析

通过两组形式的对比,对Monorepo策略应该有了基础的了解,这里回过头来解答一下开头提出的问题,“为何用Monorepo”。再结合过去个人的一些前端开发经验和前端技术的发展,来谈谈我对其出现的一些看法。这里我把它简单的划分为三个时期,当然这并不是官方或某个机构对前端发展史上的定义,只是方便去对每种策略的出现做个人理解上的阐述:

“Monolith时期”

Monolith这种模式是最早开发人员所使用的仓库架构模式,当时的前端功能还很简单,还没有过多的框架出现,仅仅是HTML,CSS的编写以及加上简单的JS逻辑。在那个时期,项目的前后端还杂糅在一起,类似JSP的开发模式被广泛应用,因此前后端的代码会集中在一个仓库里,最终页面会由后端进行渲染,仓库代码也会整体进行构建和部署。

而往后发展,Web能力开始变得越来越强大,前端能做到的事情逐渐变多,对应的工程也越来越复杂,AJAX的应用催生了前后端分离的概念。顺应趋势,前端开始将整体模块单独抽离形成仓库进行管理,到这里我们开始看到了Multi-repo的影子,但前端的代码依旧会统一在一个仓库里进行开发和管理。

随着前端功能还在不断的增多,更多的依赖被引入到仓库中,使得仓库越来越臃肿,特别在当时NPM还没被应用时,前端往往会去各个开源库官网或GitHub上下载文件并放入自己的项目当中。因此,在Single-repo Monolith架构下,对开发人员代码架构组织能力要求越来越高,同时扩展性和维护性也变得越来越低。

“Multi-repo时期”

Multi-repo的流行很大程度上是为了解决这种模块高度耦合,代码臃肿的情况,开发者们开始更加倾向将整个业务项目进行拆分,独立进行管理。


伴随着当时Web环境质的飞跃,SPA框架的流行,对应Bundle工具的产生,npm的流行以及ES,Commonjs模块化的代码引入方式天然的为分割代码模块提供了良好的时机。这使得更多开发人员将工具代码单独成库并发布成包,并将庞大的业务进行拆分,每个业务模块建立单独的库由各自团队负责开发以及维护,各种包都通过npm来进行共享。

然而随着模块拆分不断的增多,开发者们又发现过多的仓库加大了维护的成本,新的项目环境搭建,和涉及整体业务的重构和依赖同步都将变得繁琐,此时回归单repo的概念又开始兴起。因此,已经被提出很久的Monorepo开始浮出水面,应运而生的工具也开始占据了一席之地。

“Monorepo时期?”

Monorepo的出现开始解决环境及依赖统一的问题,代码之间的共享也不再强依赖于NPM来进行。既保留了Monolith单仓环境维护的便利性,同时满足Multi-repo对于项目解耦的独立开发管理。

而后类似lerna+yarn的包管理方案的出现让Monorepo拥有了较为完整的解决方案,并伴随着新兴的技术Pnpm,Changesets,Turborepo的不断推出,Monorepo的整个管理流程变得越来越完善和简单,也逐渐被很多开发者所采用。

以上是个人对于这三种策略发展趋势的理解,三种管理模式并不是依照时间递进取代的关系,更多的是随着技术发展的趋势和实际项目情况做出更合适的选择。并且随着未来技术的迭代更新,譬如Http import/Module Federation等技术的成熟,也许会再次改变这三者的使用选择。

(三)场景探索

  • 优劣分析总结


结合上面的讨论和实践以及对一些文献的阅读,这里分条来总结一下Monorepo在各方面的优劣。

使用Monorepo的优势

  • 统一管理:由于只有一个仓库,所有的配置都可以统一进行管理,而无需为不同项目重复构建环境,包括通用的代码规范检测,相同的测试框架,以及统一的CI/CD构建流程等。

  • 原子提交:这一点也是建立在统一管理的基础之上,使用原子提交轻松重构全局特性,而无需为每个repo执行拉取请求找出构建更改的顺序。这样可以简单的保持所有项目的全局特性是统一的,并且交由专人进行维护升级,而各个代码工程的开发者无需过度关注。

  • 简单依赖:多个代码工程的相同依赖可以提升至根目录进行管理,大大减少重复安装所带来的空间浪费。同时,代码工程之间也可以在保持隔离的同时相互引用,而无需在构建时依次构建相关依赖包并重新发布。


  • 文化开放:由于使用相同的代码库,所有开发者都能够浏览以及提交代码,在一定程度上也会激励团队成员共建可复用的组件及工具方法。

使用Monorepo带来的问题


  • 权限问题:由于单仓的管理模式,使用Monorepo将无法简单的控制各个模块代码的访问限制,任何有权限访问该仓库的人员将有权限访问所有的代码工程,这可能会导致部分安全问题。


  • 性能问题:当仓库的代码规模非常的巨大,达到GB/TB的级别,会增大开发环境的代码下载成本,以及本地硬盘的压力,执行git status也可能需要花费数秒甚至数分钟的时间。并且,当代码工程很多且活跃数量也很多的情况,会加大分支管理策略和各个代码工程版本管理的压力。


当然,上面的问题在Google,Facebook等实践中都有相应的技术团队提供解决方案和支持,但不是每个公司或者团队都能拥有这些大厂的支撑能力,因此,未来还需要更多通用的解决方案来帮助完善整个Monorepo的工具链。

  • 适用场景思考

说到适用场景,是很难用一刀切的方式来决定合适或者不合适,实际情况需要考虑的问题往往是非常复杂的。这里,总结比较常见的问题,并结合Monorepo的优劣势来做出决策,选择是否采用Monorepo来管理团队的代码:

  • 项目的代码规模如何?

  • 项目的开发人员规模如何?


  • 项目是否依赖权限管理?

  • 项目的技术栈是否统一?

  • 公司文化是否鼓励团队进行合作?


当逐条去回答上面提出的问题后,我们可以得出一个粗略的Monorepo使用场景的总结:

  • 代码规模相对来说不会太大,包括代码数据量和packages量。

  • 代码并不涉及机密性核心技术,无需做较强的权限管理。

  • 代码的技术选择相对统一,可复用性强。

  • 公司文化鼓励协同合作,共建代码。

二、Monorepo技术探讨

(一)核心技术

  • 包管理方案

Monorepo只是一个概念,它并不代表某项具体的技术,开发人员需要使用相应的技术手段或者工具来达到或者完善它的整个流程,从而达到更好的开发和管理体验。而实现Monorepo最重要的一个环节就是如何管理包依赖。这里简单的介绍目前两种较成熟的支持Monorepo的包管理工具。

Yarn

Yarn是一个包管理工具,它提供安全,稳定的管理机制,它在很早的时候就提出了workspace的概念来支持Monorepo的解决方案,用户也仅仅需要在package.json中配置workspace的目录即可将其纳入Yarn的包管理当中。Yarn本身的设计是为了弥补npm的一些缺陷而出现的,包括安装速度、lockfile等。当然目前为止npm也解决了部分这些方面的问题。

而随着yarn2.x,yarn3.x的版本更迭,相较以前添加了诸多特性譬如 pnpm的linker机制,git workspaces等,吸收了竞争对手的优点,并开辟了许多有趣的功能特性,这使得它长期成为开发者们的选择。

Pnpm

Pnpm是一个快速的,节省磁盘空间的包管理工具,并天然支持Monorepo的解决方案。Pnpm在官网上摆出了他的核心优势:

  • 快速:pnpm比替代方案快2倍,当然也有Benchmark数据作为参考依据。

  • 高效:Node_modules中的文件是从一个单一的可内容寻址的存储中链接过来的。

  • 支持Monorepos:pnpm内置支持了单仓多包,使用起来相当简单。

  • 严格:pnpm创建了一个非平铺的node_modules,因此代码无法访问任意包。

pnpm在包依赖管理的机制上有着独特的成果,包括symlink和hard link机制,既极大的缩小了安装包的体积,同时也解决了幽灵依赖的问题,这里就不展开描述,有机会再单独聊一聊。


pnpm在使用习惯上保留了npm的所有命令,开发者可以无痛的进行切换。而使用monorepo的功能,仅仅需要在根目录创建一个pnpm-workspace.yaml文件,并填写需要管理的目录,而之后这些目录将自动纳入pnpm的工作空间,并由它进行管理。

  • 包版本方案

除了包依赖的管理以外,如何去管理众多项目的版本也是完善Monorepo工具链的重要部分,而其中最为出名的就是Lerna,它通常会配合Yarn一起使用,另外一个则是新兴的版本管理工具Changesets,它拥有自己的一套工作流程来契合Monorepo的场景。

Lerna

Lerna是一个管理工具,用于管理包含多个软件包(package)的JavaScript项目,其功能非常复杂和完善,它拥有包管理的功能,同时还兼顾版本管理,并支持全量发布和单独发布等功能。在业界实践中通常采用Yarn来处理依赖安装和用workspace来管理项目中各个包,用Lerna来处理依赖的更新和发布问题。这套技术组合完整的实现了Monorepo中项目的包管理,更新到发布的全流程。

Lerna的工作流可以非常完善,它包含了包管理的流程以及各项参数配置,这里仅使用它的版本管理功能来描述它的一个发布过程:

  • 使用lerna version命令,lerna会动找出上一个版本发布依赖有过变更的 packages,并提示开发者确定发布的版本号。

  • 开发者依照提示确认后,lerna会自动将有更新的对应的package.json中的version字段。

  • 如果配置了–conventional-commit参数,lerna会依据先前规范的 commit message生成当前版本的CHANGELOG。

  • 提交修改,并打上版本的tag,推送到git上。

  • 使用lerna publish命令,依据提示选择要发布的包完成发布。

更加简单的工作流程如下图:

Changesets

Changesets是一个用于Monorepo项目下版本以及Changelog文件管理的工具,它也是Pnpm官方所推荐使用的版本管理工具,它所做的工作相较于Lerna而言更加专一。

Changesets的工作流会将开发者分为两类人,一类是项目的维护者,还有一类为项目的开发者,开发者在Monorepo项目下进行开发,开发完成后,给对应的子项目添加一个changeset文件。项目的维护者后面会通过changeset来消耗掉这些文件并自动修改掉对应包的版本以及生成CHANGELOG文件,最后将对应的包发布出去。更直观的工作流程如下时序图:

  • 包构建方案

在某些场景下,Monorepo的规模较大,包之间拥有拓扑式的依赖结构,而此时进行项目构建往往会需要依照依赖的链式逐步进行,过程将会耗费大量时间。而为了解决这样的构建痛点,也有相应的技术浮出水面,比如Turborepo。

Turborepo

Turborepo是一个用于JavaScript/TypeScript monorepos的快速构建系统。目的是为了解决大型monorepo项目构建速度缓慢的一大痛点。turbo的核心是永远不会重新构建已经构建过的内容。turbo会把每次构建的产物与日志缓存起来,下次构建时只有文件发生变动的部分才会重新构建,没有变动的直接命中缓存并重现日志。turbo拥有更智能的任务调度程序,充分利用空闲CPU,使得整体构建速度更快。另外,turbo还具有远程缓存功能,可以与团队和CI/CD共享构建缓存。

这里也给出一个简单的示例来辅助理解Turborepo在构建中的优势,假设有如下的包依赖结构,其中我们要对app E进行构建,它依赖4个lib包的构建,而lib包之间也有相互的依赖,特别lib B还同时依赖lib A和lib C:

(lib A -> lib B) -> lib D -> app E(lib C -> lib B)

正常的构建过程将会依照顺序依次执行,而Turborepo会在这个基础上通过它的缓存机制来确定哪些包需要进行构建,同时通过它的任务调度机制来进行并行构建,从而加快整个构建流程。

(二)辅助技术

  • 代码规范工具

Monorepo的多包管理既方便了做项目管理,同时对团队规范的统一有了一定的要求,那么适当的工具便成了必要,这里主要介绍两个非常常用的代码规范校验工具,Eslint和Prettier。


Eslint

Eslint是目前最受欢迎的Javascript代码质量校验工具,它可以通过静态分析代码来发现语言规范问题,多数问题可以被自动修复,Eslint修复程序具有语法意识,因此不用担心修复后而引入错误。同时,它内置自定义解析器,使用者可以编写自己的规则来使得Eslint能够更加适合所开发的项目。

Prettier

Prettier可以理解为一个代码格式化工具,它提供一套完整的代码风格方案,它通过解析代码并使用自己的规则强制重新打印代码,从而使得代码能够保证一致性。使用它可能不会让项目的代码完全符合开发者想要的格式规范,但却在一定程度上是最通用和便捷的团队规范方案。并且,通过plugin的形式prettier可以被集成到Eslint当中,使得两者的结合使用会更加便利。

同时,由于Monorepo的规模性,每次修改代码都进行全量的规范校验带来时间上的消耗,此时可以利用husky和lint-staged两个工具在进行git提交时进行增量代码的校验。

  • 提交规范工具


Monorepo下大概率会有多个团队进行开发,而在git仓库模式下,每次提交的Commit信息会成为重要的功能管理依据,同时在一些场景中,会依据它自动生成CHANGELOG文件,这里也介绍两种常用的工具commitlint和commitizen。

Commitlint

Commitlint是一个提交规范校验工具,它将帮助团队遵守一定的提交信息格式约定,默认采用Convenional提交规范,它也提供一定的配置允许使用者更改校验规则。同时,通过Husky添加相应的git hook,来达到提交自动校验的提示的功能。

Commitizen

Commitizen是一个提交日志工具,辅助开发者使用提交规则,再使用它进行git提交操作时,将自动提示填写Commit Messsage所必须的字段,并获取有关提交信息格式的及时反馈,使用者只需按提示输入相关内容信息即可。

通常,上面两者技术也可以配合进行使用,既提供相应脚手架工具来辅助提交信息填写,同时保证提交时规范的校验。

三、Monorepo项目实践


(一)实践概述

在述说了这么多Monorepo的理念和相关技术后,我们脚踏实地的从实践出发,基于Pnpm从零开始构建一个基础的Monorepo的前端项目架构,里面所使用的技术都是经验所得,可依据团队实际情况进行取舍。技术选择如下:

  • pnpm:包依赖管理工具。

  • changesets:包版本管理工具。

  • eslint,pretter: 代码规范工具。

  • commitizen,commitlint:提交规范工具。

  • husky,lint-staged:git hook相关工具。

  • vitepress:文档服务工具。

接下来,我们逐步应用上述技术,并添加至项目架构当中。

(二)实践流程

  • 基础架构搭建

  • 初始化目录

首先,初始化一个最基本的项目目录结构,包括package.json,README.md,以及packages下的各个项目模块,其中shared为公共方法模块,其中每个模块的命名都以@my-scope/xx (xx为项目模块名称)。

├── package.json├── packages/│   ├── pkg1/├── |    ├──package.json├── |    ├──src/│   ├── pkg2/├── |    ├──package.json├── |    ├──src/│   └── shared/├── |    ├──package.json├── |    ├──src/├── README.md

  • 添加Pnpm包管理

然后,添加pnpm作为项目的包管理工具,这里将全局安装pnpm:

npm install pnpm -g

并在根目录的package.json中添加如下脚本来限制包的安装:

// 此行命令将限制使用 pnpm 来进行 install 操作"scripts": {  "preinstall": "npx only-allow pnpm"}

创建pnpm-workspace.yaml文件,并添加如下配置,将packages下所有子目录纳入工作空间进行管理:

packages:  # 所有在 packages/ 子目录下的 package  - 'packages/**'

在根目录package.json中添加shared作为依赖模块,后续安装后即可在其他模块使用:

"dependencies": {  "@my-scope/shared": "workspace:*"}

  • 添加Changesets版本管理工具

接下来,添加changesets作为项目的版本管理工具,安装其脚手架工具:

pnpm install @changesets/cli -DW

安装成功后,通过changeset命令操作初始化配置文件:

pnpx changeset init

后续,只需安装以下命令进行开发,版本生成以及版本发布即可:

// 开发人员在完成开发后添加 changeset 文件信息pnpx changeset add
// 管理人员在发布版本前消费 changeset 进行 changelog 生成pnpx changeset version
// 确认 changelog 和各项流程后发布包pnpx changeset publish

  • 规范工具配置

  • 添加ESlint、Prettier代码规范工具

首先,根据各个项目的需求安装对应的依赖包,本模板将使用下面所有的依赖:

pnpm install eslint -DW
// 如果使用 Typescriptpnpm install typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser -DW
// 如果使用 Prettierpnpm install prettier eslint-plugin-prettier -DW

创建.eslintrc.js,并添加如下配置来使得Eslint正常工作:

module.exports = {  extends: [    'plugin:@typescript-eslint/recommended',    'prettier',    'plugin:prettier/recommended'  ],  parser: '@typescript-eslint/parser',  parserOptions: {    sourceType: 'module',    tsconfigRootDir: __dirname,    project: ['./tsconfig.json']  },  plugins: ['@typescript-eslint', 'prettier']}

如果使用prettier则创建.prettierrc文件,并添加如下配置:

semi: falsesingleQuote: trueprintWidth: 80trailingComma: 'none'arrowParens: 'avoid'

如果使用Typescript则创建tsconfig.json文件,并添加如下配置:

{  "compilerOptions": {    "allowJs": true,    "strict": true,    "module": "ES6",    "target": "ES2018",    "noImplicitAny": false,    "declaration": true,    "moduleResolution": "Node",    "esModuleInterop": true,    "jsx": "preserve",    "sourceMap": true,    "lib": ["ES2018", "DOM"],    "allowSyntheticDefaultImports": true,    "forceConsistentCasingInFileNames": true,    "resolveJsonModule": true  },  "include": ["packages", "commitlint.config.js", ".eslintrc.js"],  "exclude": ["node_modules", "**/dist", "docs"]}

同时在package.json中添加校验命令,后续即可输入命令进行全量校验和自动修复:

"scripts": {  "lint": "eslint --ext .js,.ts",  "lint:fix": "eslint --ext .js,.ts --fix",}

  • 添加Commitizen,Commitlint提交规范校验工具


接下来,先安装commitizen全局工具并安装cz-conventional-changelog适配器:

// 全局安装npm install commitizen -g// 项目中安装pnpm install cz-conventional-changelog -DW

在package.json中添加相关配置使得commitzen可以读取相对应的适配器:

"config": {    "commitizen": {      "path": "cz-conventional-changelog"    }  }

后续开发完成提交可以使用cz命令进行,并按照提示进行填写即可:

git cz

完成了commit规范脚手架工具配置后,我们再安装commitlint相关依赖进行提交校验:

pnpm install @commitlint/cli @commitlint/config-conventional -DW

创建commitlint.config.js文件,并添加如下代码:

module.exports = {extends: ['@commitlint/config-conventional']}

  • 添加husky、lint-staged


接下来,再来安装git hook相关的工具来确保在提交时触发相应的校验工作:

pnpm install husky lint-staged -DW

这里使用的husky版本>6,因此需要进行初始化相关配置:

pnpm set-script prepare "husky install"pnpm run prepare

生成.husky/pre-commit hook并添加lint-staged执行命令:

npx husky add .husky/pre-commit 'npx lint-staged'

在package.json中添加相应lint-staged规则来触发eslint进行增量代码校验:

"lint-staged": {  "*.{js,ts}": [    "eslint --fix",    "git add"  ]}

生成.husky/commit-msg hook并添加commilint执行命令:

npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'

  • 文档服务配置

  • 添加Vitepress文档服务

安装vitepress来生成文档服务:

pnpm install vitepress -DW

在package.json中添加如下脚本

"scripts": {  "docs:dev": "vitepress dev docs", // 启动本地文档服务  "docs:build": "vitepress build docs" // 构建打包文档}

配置vitepress目录结构如下,可前往官网查看相关配置字段:

.├─ docs│  ├─ .vitepress│  │  └─ config.js│  └─ index.md

具体配置参考官网:Vitepress配置

(https://vitepress.vuejs.org/config/basics.html)

(三)实践小结

至此,我们完成了一个简单的Monorepo项目的搭建,真正的项目还需要添加相应的开发及打包工具比如Vite,Webpack,Rollup等,和代码测试工具,比如Jest。如果项目复杂,也可以选择Turborepo作为构建流工具,可以根据实际情况进行选择。

四、总结


本文对Monorepo进行了不同形式的探索对比以及实践讨论,可以发现Monorepo在前端项目的很多场景中确实是一个非常好的策略选择,也值得我们在更多的实际的项目中去尝试以及使用它。当然,每种策略都不是一个完美的代码管理方式,它们都有着自己的优势和适合的使用场景。技术和策略上的选择从来都不是非此即彼,而是依据团队以及业务的发展去组合和拓展,在已有的基础上提出想法,并在实践中不断的完善各阶段的流程和技术选型,从失败中总结经验,并将经验转化为下一次的进步。

最后,希望本文能够帮助你选择合适的代码管理策略。

参考资料:

1.Guide to Monorepos for Front-end Code

2.为什么前端工程越来越爱使用Monorepo架构

3.Monorepos in Git

4.What is a Monorepo

5.What is monorepo? (and should you use it?)

6.Monorepo vs Multi-Repo: Pros and Cons of Code Repository Strategies

7.Monorepo Wikipedia

8.Pnpm官网


 作者简介



张杰

腾讯前端高级开发工程师


腾讯前端高级开发工程师,毕业于北京交通大学。目前负责腾讯云大数据平台Wedata的前端开发工作,有丰富的前端架构搭建,大数据应用产品开发经验。



 推荐阅读


Golang高质量单测之Table-Driven:从入门到真香!

10分钟搞懂!消息队列选型全方位对比

在线教程!C++如何在云应用中快速实现编译优化?

CGO让Go与C手牵手,打破双方“壁垒”!



本文来自微信公众号“腾讯云开发者”(ID:QcloudCommunity)。大作社经授权转载,该文观点仅代表作者本人,大作社平台仅提供信息存储空间服务。