🧄 Monorepo管理多个项目
在进入正题之前,我们先参考一下ElementPlus项目目录结构:
我们看到 element-plus
是通过 monorepo
的方式,将整个 UI 组件库的多个依赖模块都整合到了同一个代码仓中,以其代码仓中的 packages
目录为例子。
1 | 📦element-plus |
看到了开源项目的目录结构之后,我的想法就是能不能把我的目录结构换一下呢,但还是换汤不换药,他是管理子模块,那我能不能管理项目呢??于是我就开始动手尝试了一下,发现是可以的。于是我得目录结构就换了个样子。
1 | 📦monorepo-demo |
1. Monorepo 介绍
Monorepo 是一种项目代码管理方式,指单个仓库中管理多个项目,有助于简化代码共享、版本控制、构建和部署等方面的复杂性,并提供更好的可重用性和协作性。Monorepo 提倡了开放、透明、共享的组织文化,这种方法已经被很多大型公司广泛使用,如 Google、Facebook 和 Microsoft 等。
monorepo
可以解决我的一些痛点:
- 任意一个模块发生修改,另一个项目能够立即反馈而不用走繁琐的发布和依赖更新流程;
- 各个模块之间也能够充分复用配;
- 各个包的版本和互相之间的依赖关系得到集中管理;
- 我可以将多个项目在一起管理,并将公共组件、公共方法提取出来,当作一个模块。这样长久累计下去,我可以发布一个npm包供所有人使用。
总之能了解到这个技术非常的高兴,确实解决了我很大的痛点。
2. 包管理基础 package.json
在进行下一步之前,我们先简单了解一下包的配置文件package.json
,一个包或者子模块不一定发布到 npm
仓库,但一定有 package.json
文件。package.json
所在的目录就代表了一个模块/包,这个 json
文件定义了模块的各种配置,例如基本信息、依赖关系、构建配置等等。所有包管理器(npm
/yarn
/pnpm
)以及绝大多数构建工具都会依赖于这个配置文件的信息。
从我们接触前端开始,每个项目的根目录下一般都会有一个package.json
文件,这个文件定义了当前项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等)。但是你真正的了解package.json
文件吗?
下面我找了一个完整的配置文件,详细解释一下主要字段的真实含义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50{
"name": "alvis",
"version":"0.0.1",
"description": "alvis-project",
"keywords":["node.js","alvis", "project"],
"homepage": "https://alvis.org.cn",
"bugs":{"url":"http://path/to/bug","email":"alvis@xxxx.com"},
"license": "ISC",
"author": "alvis",
"contributors":[{"name":"alvis","email":"alvis@xxxx.com"}],
"files": "",
"main": "./dist/index.ts",
"bin": "",
"man": "",
"directories": "",
"repository": {
"type": "git",
"url": "https://path/to/url"
},
"scripts": {
"start": "webpack serve --config webpack.config.dev.js --progress"
},
"config": { "port" : "8080" },
"dependencies": {},
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/preset-env": "^7.14.4",
"@babel/preset-react": "^7.13.13",
"babel-loader": "^8.2.2",
"babel-plugin-import": "^1.13.3",
"glob": "^7.1.7",
"less": "^3.9.0",
"less-loader": "^9.0.0",
"style-loader": "^2.0.0",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.11.2"
},
"peerDependencies": {
"tea": "2.x"
},
"bundledDependencies": [
"renderized", "super-streams"
],
"engines": {"node": "0.10.x"},
"os" : [ "win32", "darwin", "linux" ],
"cpu" : [ "x64", "ia32" ],
"private": false,
"publishConfig": {}
}
2.1 name字段
在配置文件种,最重要的就是name
和version
字段,他们俩个一起就是该配置文件唯一标识。该名称会作为参数传递给require
,因此它应该是简短的,但也需要具有合理的描述性。
2.2 version字段
version
一般的格式是x.x.x
, 并且需要遵循该规则。
2.3 description字段
description
是一个字符串,用于编写描述信息。有助于人们在npm
库中搜索的时候发现你的模块
2.4 keywords字段
keywords
是一个字符串组成的数组,有助于人们在npm
库中搜索的时候发现你的模块。
2.5 homepage字段
homepage
项目的主页地址。
2.6 bugs字段
bugs
用于项目问题的反馈issue地址或者一个邮箱。
1 | "bugs": { |
2.7 license字段
license
是当前项目的协议,让用户知道他们有何权限来使用你的模块,以及使用该模块有哪些限制。更多开源协议
2.8 author字段和contributors字段
author
是具体一个人,contributors
表示一群人,他们都表示当前项目的共享者。同时每个人都是一个对象。具有name
字段和可选的url
及email
字段。
2.9 files字段
files
属性的值是一个数组,指定了发布为 npm
包时,哪些文件或目录需要被提交到 npm
服务器中。内容是模块下文件名或者文件夹名,如果是文件夹名,则文件夹下所有的文件也会被包含进来(除非文件被另一些配置排除了)
可以在模块根目录下创建一个.npmignore
文件,写在这个文件里边的文件即便被写在files
属性里边也会被排除在外,这个文件的写法与.gitignore
类似。
2.10 main字段
main
字段指定了加载的入口文件,require
导入的时候就会加载这个文件。这个字段的默认值是模块根目录下面的index.js
。
2.11 repository字段
源码仓库地址
2.12 scripts字段
scripts
指定了运行脚本命令的npm命令行缩写
2.13 dependencies字段和devDependencies字段
在我们的项目开发中,dependencies字段和devDependencies字段并没有任何区别,只是存在语义化的区别。我们常说的开发环境、生产环境是构件行为。构件并不是包管理器的职责,而是webpack
、vite
等工具的工作,此时包管理器起的作用仅仅是执行脚本而已。 各种包管理器处理 dependencies
和 devDependencies
差异的行为都发生在依赖安装时期,即 npm install
的过程中。
我们的web应用中
,web应用的的产物往往是部署到服务器,不会发布到npm仓库,作为包的使用。而包管理器对于一级依赖,无论 dependencies
还是 devDependencies
都会悉数安装。但是对于这些一级依赖项具有的更深层级依赖,在深度遍历的过程中,只会安装 dependencies
中的依赖,忽略 devDependencies
中的依赖。 因为包管理器认为:作为包的使用者,我们当然不用再去关心它们开发构建时的依赖,所以会为我们忽略 devDependencies
。 而 dependencies
是包产物正常工作所依赖的内容,当然有必要安装。
举个例子:
1 | // 项目a |
1 | // node_modules/b/package.json |
1 | // node_modules/c/package.json |
在上面这种情况中, b和c作为npm包来使用,e,g是npm包的开发依赖,所以他们不会被下载安装。
2.14 peerDependencies字段
peerDependencies
声明包的同步依赖。但是包管理器不会像 dependencies
一样,自动为用户安装好依赖,当用户使用包时,必须遵照该包的 peerDependencies
同步安装对应的依赖,否则包管理器会提示错误。
2.15 private字段
private
用于指定项目是否为私有包。当我们的项目不想被意外发布到公共 npm
仓库时,就设置 private: true
。
2.16 publishConfig字段
当我们的项目需要发布到私有的 npm
仓库时(比如公司内网的仓库),需要设置 publishConfig
对象.
入口信息
入口信息主要被
Node.js
、各路构建工具(Vite
/Rollup
/Webpack
/TypeScript
)所识别。未正确设置会导致npm
包无法被加载或者实际加载了预料之外的文件。入口文件的加载机制是比较复杂的,在不同的构建工具中有着不同的加载逻辑,对此给大家分享一篇文章:package.json 导入模块入口文件优先级详解。
3. workspace 模式
pnpm
支持 monorepo
模式的工作机制叫做 workspace(工作空间)。
它要求在代码仓的根目录下存有 pnpm-workspace.yaml
文件指定哪些目录作为独立的工作空间,这个工作空间可以理解为一个子模块或者 npm
包。例如以下的 pnpm-workspace.yaml
文件定义:components
目录、shared
目录、project
目录,都会各自被视为独立的模块。
1 | packages: |
1 | 📦my-project |
4. 项目实战
因为我的项目都是基于vue3的,所以我先搭建一个vue3项目,然后基于vue3项目进行改造。
1 | pnpm create vue@latest |
之后我们在项目下新建一个pnpm-workspace.yaml
文件,以及存放我们项目包的目录packages
,并创建一个项目目录project
1 | touch pnpm-workspace.yaml |
在然后我们手动整理一下目录, 将这些文件迁移到project目录下
1 | vim pnpm-workspace.yaml |
接下来我们统一一下,在每一个包中,都有一个src目录,src/index.ts作为当前包的统一导出
4.1 中枢(父级)管理
- 安装项目公共开发依赖,声明在根目录的 package.json - devDependencies 中。
-w
选项代表在monorepo
模式下的根目录进行操作。 - 每个子包都能访问根目录的依赖,适合把
TypeScript
、Vite
、eslint
等公共开发依赖装在这里。
1 | pnpm install -wD xxx |
4.2 子包管理操作
1. 为指定模块安装外部依赖
1 | # 为 a 包安装 lodash |
2. 指定内部模块之间的互相依赖
1 | # 指定 a 模块依赖于 b 模块 |
配置好对应的package.json之后,直接安装对应的依赖,然后启动对应的项目或者打包对应的项目即可。
由于讲的不太好,我把我的package.json
文件都列出来给大家看一下:
1 | // 项目根目录下package.json |
1 | // /packages/shared/packages.json 所有项目公用的方法 |
1 | // /packages/components/packages.json 所有项目公用的vue组件 |
1 | // /packages/carshop/packages.json 这是一个项目模块 |