前言
Vue3 采用 monorepo
的方式进行项目代码管理。在本文中,可以学习到
monorepo
是什么,有哪些优缺点?了解其技术应用背景、解决技术痛点- 探讨几种实现
monorepo
策略的具体方案?根据项目场景选择适合的技术方案 Vue3
源代码 monrepo 实现,了解其实现细节和目录规范设计。monorepo
在大型项目有哪些新的技术方案
monorepo 是什么?
Monorepo(单一代码仓库)是一种代码管理策略,用于将多个相关项目存储在同一个代码仓库中。相比于传统的多个独立代码仓库,Monorepo 的目标是提高代码的可共享性、可重用性和协作效率
那么,采取一种新的策略,肯定是因为该策略具备一些优点。从下面这张图中,可以看出,项目代码的管理策略是在实践中不断发展变化的。
- 第一阶段 monolith:一开始不管多少代码都放在一个项目中进行管理,随着时间推移,代码量越来越多,每一次构建都会花费很长时间,代码耦合度强,可维护性差,代码冲突频繁等各种问题逐渐显现且愈加严重。
- 第二阶段 multi repo:将业务相对独立的功能拆分不同的项目进行维护,这样确实解决了一些问题,比如项目自治,可维护性变强。不过也存在一些问题,例如代码不能共享,联调困难,每个项目都要重复安装,版本管理等问题
- 第三阶段 monorepo:由于存在以上种种问题,聪明的工程师想出的一种代码管理策略,接下来就分析 monorepo 有什么优势和劣势。
monorepo 优劣势?
通过 monorepo 策略管理的代码,目录结构看起来会是下面这样,将不同项目的目录汇集到一个目录之下
.
├── package.json
└── packages/ # 这里将存放所有子 repo 目录
├── project_1/
│ ├── index.js
│ ├── node_modules/
│ └── package.json
├── project_2/
│ ├── index.js
│ ├── node_module/
│ └── package.json
...
monorepo 优势
monorepo 在代码管理上优势:
- 代码重用将变得容易:由于所有的项目代码都集中于一个代码仓库,很容易抽离出各个项目共用的业务组件或工具,在代码内引用;
- 依赖管理将变得简单:由于项目之间的引用路径内化在同一个仓库之中,容易追踪当某个项目的代码修改后,会影响到其他哪些项目。通过使用 lerna 一些工具,可以做到版本依赖管理和版本号自动升级;
- 统一构建和测试:使用统一的构建配置和流程,减少配置和维护的工作量。此外,可以在整个 Monorepo 中执行统一的测试流程,确保所有项目质量和稳定性。
- 便于协作和开发:在一个代码仓库中,更容易地浏览、搜索和理解整个项目的代码,便于团队成员之间的协作。Monorepo 还可以促进跨项目的合作和知识共享,提高团队的整体效率和协同能力。
- 更少的内存:多个项目引用相同的依赖,只需要安装一份依赖即可,减少重复安装节省内存空间
monorepo 劣势
其实,优势和劣势都是相对的,在一定程度上,如果不遵循约束和规范,优势也会转换为劣势,所以在设计上要更加严谨,这也是学习源码优秀设计的原因之一。
- 新员工的学习成本变高:不同于一个项目一个代码仓库这种模式下,在 monorepo 策略下,新人可能不得不花更多精力来理清各个代码仓库之间的相互逻辑,当然这个成本可以通过新人文档的方式来解决,但编写和维护文档也需要精力成本;
- 团队协作和权限管理变复杂:在 Monorepo 中,团队成员需要共享同一个代码仓库,并且对所有模块都具有相同的权限级别。这可能会导致一些团队成员对整个项目的代码和资源具有过多的访问权限,增加了潜在的安全风险。
- 代码耦合和影响范围:在 Monorepo 中,一个模块的更改可能会对其他模块产生意外的影响,增加了代码耦合性,并可能导致意外的副作用。
如何取舍?
看了之后是不是在犹豫要不要使用 monorepo 管理代码了,别这么早下结论,软件开发领域从来没有完美一说,需要根据组织和特定的项目来选择。可以把 monorepo 策略实践在「项目」这个级别,即从逻辑上确定项目与项目之间的关联性,然后把相关联的项目整合在同一个仓库下。
通常情况下,我们不会有太多相互关联的项目,使用 monorepo 技术可以管理多而乱的项目,实现项目复用,好好利用放大它的优点,同时通过制定规范、项目文档管理规范补齐它的短板。
monorepo 实现方案
重新强调一下,monorepo
它是一个策略,是一种思想,而不是一个具体的工具,不要将它和 lenrn
、 yarn workspace
划上等号,实现这个策略可以有多种方案,那么介绍以下3种方案。
lerna
Lerna 是为 monorepo 而生的工具,在项目中配置 lerna
1、项目根目录安装 lenrna
包 npm install lerna -D
,使用 npx lerna init
初始化,于是根目录新增一个 lerna.json
文件,默认内容为:
{
packages:[
"packages/*"
],
// 默认 npm 省略
"npmClient": "npm"
}
lerna 默认工作目录是 packages
,lerna 默认使用的是 npm
,省略了配置项 "npmClient": "npm"
2、在 packages 目录创建多个独立的子包,分别初始化 package.json
文件,如下
.
├── package.json
└── packages/ # 这里将存放所有子 repo 目录
├── project_1/
│ ├── index.js
│ ├── node_modules/
│ └── package.json
├── project_2/
│ ├── index.js
│ ├── node_module/
│ └── package.json
...
3、在根项目 package.json
配置 scripts
运行命令,然后执行 npm run bootstrap
,安装 packages 各个子包依赖
{
scripts:{
'bootstrap':'lerna bootstrap'
}
}
lerna 如何工作
1、lerna
有两种工作模式:模式固定模式(Fixed)、独立模式(Independent),使用 version
关键字表示
{
"packages": ["packages/*"],
"npmClient": "npm",
"version": "independent"
}
-
independent
独立模式:将每个子项目的版本号看作是相互独立的。当某个子项目代码更新后,运行lerna publish
时,关联的子项目版本号不会自动升级 -
Fixed
固定模式:相反,使用固定模式时,任一子项目的代码变动,都会导致所有子项目的版本号基于当前指定的版本号升级。
lerna 常用命令
Lerna 提供了很多 CLI 命令以满足我们的各种需求,但根据 2/8 法则,应该首先关注以下这些命令
lerna run
:会像执行一个 for 循环一样,在所有子项目中执行 npm script 脚本,并且,它会非常智能的识别依赖关系,并从根依赖开始执行命令;lerna exec
:像 lerna run 一样,会按照依赖顺序执行命令,不同的是,它可以执行任何命令,例如 shell 脚本;lerna publish
:发布代码有变动的 package,因此首先需要在使用 Lerna 前使用 git commit 命令提交代码,好让 Lerna 有一个 baseline;lerna add
:将本地或远程的包作为依赖添加至当前的 monorepo 仓库中,该命令让 Lerna 可以识别并追踪包之间的依赖关系,因此非常重要;--concurrency <number>
:参数可以使 Lerna 利用计算机上的多个核心,并发运行,从而提升构建速度;--scope '@mono/{pkg1,pkg2}'
:–scope 参数可以指定 Lerna 命令的运行环境,通过使用该参数,Lerna 将不再是一把梭的在所有仓库中执行命令,而是可以精准地在我们所指定的仓库中执行命令,并且还支持示例中的模版语法;--stream
:该参数可使我们查看 Lerna 运行时的命令执行信息
yarn workspaces
yarn workspaces
天然自带 monorepo
能力。虽然没有专用的配置文件,但需要在项目根路径下 package.json
文件中做些配置,例如
{
workspaces:[
"packages/*"
]
}
执行 yarn install
,各个子项目会安装各自的依赖项,配置相对简单
将 lerna + yarn workspace 结合实现 monorepo
在这里 lerna
和 yarn workspace
角色分明,依赖管理的工作交给 yarn worksapces
,利用 lerna
提供的一些工具命令来优化对 monorepo 类型项目的管理,比如启动不同的项目,利用 lerna 选择性的执行某些命令。同时lerna还提供了诸如版本发布等可以优化开发体验的工具
pnpm workspace
pnpm
作为一个比较新的工具,相比于 yarn
安装速度更快,占用内存更少,它也和 yarn 一样,提供了工作空间实现 monorepo
pnpm
配置 monorepo,在项目根目录下新建 pnpm-workspace.yaml
文件
packages:
- 'packages/*'
通过上面简单的配置,pnpm 就搭建了 monorepo
环境,实现起来相当简单。总的来说,小项目不需要 monorepo,在大项目中也许需要将业务和组件库代码抽离,需要考虑利用这种手段,实现多个项目的代码和配置共享
跟 yarn
一样,pnpm
可以和 lerna
一起工作
。接下来介绍 Vue3 中 pnpm 实现 monorpo
Vue3 实现 monorepo
在 Vue3 项目 package.json
写明包管理器使用 pnpm
,node 版本 18+
,推荐全局安装 npm i -g pnpm
{
"packageManager": "pnpm@8.15.0",
"engines": {
"node": ">=18.12.0"
},
}
pnpm-workspace.yaml
配置文件告诉 pnpm 包管理目录是 packages
packages:
- 'packages/*'
安装项目依赖 pnpm install
,在根目录和 packages 子目录下分别安装依赖包,运行 pnpm dev
打包 vue 代码
可以看到 在 packages
目录有十几个项目,Vue3 将内部实现的部分抽象成了一个个模块,每个模块都有自己的类型声明、单元测试、构建测试流程, 打包独立 npm
发布,这样设计便于维护、发版和扩展。
同时,独立的子项目模块不仅仅可以在 vue3 中使用,例如 reactivity
响应式这个模块,安装 npm i @vue/reactivity
可以在 js、react 其他项目中使用
介绍下源码的目录设计
core
├── packages // vue 源码核心包,使用 pnpm workspace 工作区管理
│ ├── compiler-core
│ ├── compiler-dom
│ ├── compiler-sfc
│ ├── compiler-ssr
│ ├── reactivity
│ ├── reactivity-transform
│ ├── runtime-core
│ ├── runtime-dom
│ ├── runtime-test
│ ├── server-renderer
│ ├── sfc-playground
│ ├── shared
│ ├── size-check
│ ├── template-explorer
│ └── vue
│ └── vue-compat
包功能模块介绍:
compiler-core
: 编译器(平台无关),例如基础的baseCompile
编译模版文件,baseParse
生成ASTcompiler-dom
: 基于compiler-core
,专为浏览器的编译模块,可以看到它基于baseCompile
,baseParse
,重写了complie、parsecompiler-sfc
: 编译vue单文件组件compiler-ssr
: 服务端渲染相关的reactivity
: vue独立的响应式模块runtime-core
: 也是与平台无关的基础模块,有vue的各类API,虚拟dom的渲染器runtime-dom
: 针对浏览器的runtime。包含处理原生DOM APIruntime-test
:一个专门为了测试而写的轻量级 runtime。由于这个 rumtime 「渲染」出的 DOM 树其实是一个 JS 对象,所以这个 runtime 可以用在所有 JS 环境里。你可以用它来测试渲染是否正确。shared
:内部工具库,不暴露APIsize-check
:简单应用,用来测试代码体积template-explorer
:用于调试编译器输出的开发工具vue
:面向公众的完整版本, 包含运行时和编译器api-extractor.json
—— 所有包共享的配置文件。当我们 src 下有多个文件时,打包后会生成多个声明文件。使用@microsoft/api-extractor
这个库是为了把所有的.d.ts
合成一个,并且,还是可以根据写的注释自动生成文档。template-explorer
: 用于调试编译器输出的开发工具。您可以运行npm run dev dev template-explorer并打开它index.html
以获取基于当前源代码的模板编译的副本。在线编译网址:vue-next-template-explorer.netlify.app/#
大型应用构建 Monorepo 方案
Turborepo
Turborepo 是 Vercel 团队开源的高性能构建代码仓库系统,允许开发者使用不同的构建系统。
构建加速思路:
- Multiple Running Task:构建任务并行进行,构建顺序交给开发者配置
- Cache、Remote Cache:通过缓存 及 远程缓存,减少构建时间
Rush
-
解决了幽灵依赖:将项目所有依赖都安装到 Repo根目录的
common/temp
下,通过软链接到各项目,保证了node_modules
下依赖与package.json
一致 -
并行构建:Rush 支持并行构建多个项目,提高了构建效率
-
插件系统:Rush 提供了丰富的插件系统,可以扩展其功能,满足不同的需求,具体参考
-
项目发布,ChangeLog 支持友好:自动修改项目版本号,自动生成 ChangeLog
Nx
Nx 是 Nrwl 团队开发的,同时在维护 Lerna,目前 Nx 可以与 Learn 5.1及以上集成使用
构建加速思路(比 Turborepo 更丰富)
- 缓存: 通过缓存 及 远程缓存,减少构建时间(远程缓存:Nx 公开了一个公共 API,它允许您提供自己的远程缓存实现,Turborepo 必须使用内置的远程缓存)
- 增量构建: 最小范围构建,非全量构建
- 并行构建: Nx 自动分析项目的关联关系,对这些任务进行排序以最大化并行性
- 分布式构建: 结合 Nx Cloud,您的任务将自动分布在 CI 代理中(多台远程构建机器),同时考虑构建顺序、最大化并行化和代理利用率
用 Nx 强大的任务调度器加速 Lerna:Lerna 擅长管理依赖关系和发布,但扩展基于 Lerna 的 Monorepos 很快就会变得很痛苦,因为 Lerna 很慢。这就是 Nx 的闪光点,也是它可以真正加速你的 monorepo 的地方。
扩展阅读
原文链接:https://juejin.cn/post/7351656743187185691 作者:网页建筑师