前言 📝
每当我在学习一门技术的时候,我都会创建很多的项目。而且这些项目框架基本都是一样的依赖。每次当我想要找一个项目的时候很复杂,我需要用vscode挨个的打开项目,查看是不是我需要的项目。这么多的项目不仅浪费存储空间,项目管理也麻烦。于是就参考了很多优秀的开源项目 :vue、element-pluse、vite
等,都是使用pnpm+monorepo
管理项目,达到一个git仓库,管理多个项目或模块。我也是一边学习一边写的文章,有些理解不到位的还请大家在评论区发表一下自己的想法意见。接下来我们直接进入正题。
在进入正题之前,我们先参考一下ElementPlus项目目录结构:

我们看到 element-plus
是通过 monorepo
的方式,将整个 UI 组件库的多个依赖模块都整合到了同一个代码仓中,以其代码仓中的 packages
目录为例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 📦element-plus ┣ 📂... ┣ 📂packages ┃ ┣ 📂components # 各种 UI 组件 ┃ ┃ ┣ 📂button ┃ ┃ ┣ 📂input ┃ ┃ ┣ 📂... ┃ ┃ ┗ 📜package.json ┃ ┣ 📂utils # 公用方法包 ┃ ┃ ┗ 📜package.json ┃ ┣ 📂theme-chalk # 组件样式包 ┃ ┃ ┗ 📜package.json ┃ ┣ 📂element-plus # 组件统一出口 ┃ ┃ ┗ 📜package.json ┣ 📜...
|
看到了开源项目的目录结构之后,我的想法就是能不能把我的目录结构换一下呢,但还是换汤不换药,他是管理子模块,那我能不能管理项目呢??于是我就开始动手尝试了一下,发现是可以的。于是我得目录结构就换了个样子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 📦monorepo-demo ┣ 📂... ┣ 📂packages ┃ ┣ 📂components # 所有项目的公共组件 ┃ ┃ ┣ 📂button ┃ ┃ ┣ 📂input ┃ ┃ ┣ 📂... ┃ ┃ ┗ 📜package.json ┃ ┣ 📂utils # 所有项目公用方法包 ┃ ┃ ┗ 📜package.json ┃ ┣ 📂project1 # 项目一 ┃ ┃ ┗ 📜package.json ┃ ┣ 📂project2 # 项目二 ┃ ┃ ┗ 📜package.json ┣ 📜...
|
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 2 3 4
| "bugs": { "url" : "https://github.com/owner/project/issues", "email" : "project@hostname.com" }
|
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 2 3 4 5 6 7 8 9 10 11
| { "name": "a", "dependencies": { "b": "^1.0.0" }, "devDependencies": { "c": "^1.0.0" } }
|
1 2 3 4 5 6 7 8 9 10
| { "name": "b", "dependencies": { "d": "^1.0.0" }, "devDependencies": { "e": "^1.0.0" } }
|
1 2 3 4 5 6 7 8 9 10
| { "name": "c", "dependencies": { "f": "^1.0.0" }, "devDependencies": { "g": "^1.0.0" } }
|
在上面这种情况中, 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 2 3 4
| packages: - components - shared - project
|
1 2 3 4 5 6 7 8 9
| 📦my-project ┣ 📂components ┃ ┗ 📜package.json ┣ 📂shared ┃ ┗ 📜package.json ┣ 📂project ┃ ┗ 📜package.json ┣ 📜package.json ┣ 📜pnpm-workspace.yaml
|
4. 项目实战
因为我的项目都是基于vue3的,所以我先搭建一个vue3项目,然后基于vue3项目进行改造。
之后我们在项目下新建一个pnpm-workspace.yaml
文件,以及存放我们项目包的目录packages
,并创建一个项目目录project
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
| touch pnpm-workspace.yaml mkdir packages cd packages mkdir project
mkdir components cd ./components touch package.json vim package.json
{ "name": "@learn_vue3/components", "version": "0.0.1", "scripts": { "build": "vite build" }, "peerDependencies": { "vue": ">=3.4.0" } }
cd ../ mkdir shared cd ./shared touch package.json vim package.json
|
在然后我们手动整理一下目录, 将这些文件迁移到project目录下

1 2 3 4 5 6 7
| vim pnpm-workspace.yaml
packages: - packages/*
|
接下来我们统一一下,在每一个包中,都有一个src目录,src/index.ts作为当前包的统一导出

4.1 中枢(父级)管理
- 安装项目公共开发依赖,声明在根目录的 package.json - devDependencies 中。
-w
选项代表在 monorepo
模式下的根目录进行操作。
- 每个子包都能访问根目录的依赖,适合把
TypeScript
、Vite
、eslint
等公共开发依赖装在这里。
4.2 子包管理操作
1. 为指定模块安装外部依赖
1 2 3
| pnpm --filter a i -S lodash pnpm --filter a i -D lodash
|
2. 指定内部模块之间的互相依赖
配置好对应的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 51 52 53 54 55 56 57 58 59 60
| { "name": "learn_vue3", "version": "0.0.0", "private": "false", "scripts": { "install:all": "pnpm i -w", "build:all": "pnpm run ts:build && pnpm --filter ./packages/** run build && pnpm run mv:type", "dev:carshop": "pnpm --filter @learn_vue3/car_shop run dev", "build:carshop": "pnpm --filter @learn_vue3/car_shop run build-only", "build:components": "pnpm run ts:build && pnpm --filter @learn_vue3/components run build && pnpm run mv:type", "build:shared": "pnpm run ts:build && pnpm --filter @learn_vue3/shared run build && pnpm run mv:type", "clean:type": "rimraf ./dist", "mv:type": "tsx ./scripts/dtsMv.ts", "ts:check": "vue-tsc -p tsconfig.web.json --noEmit --composite false", "ts:build": "pnpm run clean:type && vue-tsc -p tsconfig.web.json --composite false --declaration --emitDeclarationOnly", "lint:script": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", "lint:style": "stylelint --fix ./**/*.{css,scss,vue,html}" }, "dependencies": { "vue": "^3.4.29" }, "devDependencies": { "@rushstack/eslint-patch": "^1.8.0", "@tsconfig/node20": "^20.1.4", "@types/node": "^20.16.1", "@vitejs/plugin-vue": "^5.0.5", "@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-typescript": "^13.0.0", "@vue/tsconfig": "^0.5.1", "eslint": "^8.57.0", "eslint-plugin-vue": "^9.23.0", "npm-run-all2": "^6.2.0", "prettier": "^3.2.5", "rimraf": "^6.0.1", "stylelint": "^16.8.2", "stylelint-config-recess-order": "^5.0.1", "stylelint-config-recommended-vue": "^1.5.0", "stylelint-config-standard-scss": "^13.1.0", "tsx": "^4.17.0", "typescript": "~5.4.0", "vite": "^5.3.1", "vite-plugin-vue-devtools": "^7.3.1", "vue-tsc": "^2.0.29" }
}
|
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
| { "name": "@learn_vue3/shared", "version": "0.0.1", "description": "第一次发布,公用方法组件", "repository": { "type": "git", "url": "git@github.com:chenjieya/monorepo_vue3.git" }, "keywords": [ "utils" ], "author": "alvis", "license": "MIN", "bugs": { "url": "https://github.com/chenjieya/monorepo_vue3/issues" }, "scripts": { "build": "vite build" }, "dependencies": { "@types/lodash": "^4.17.7", "lodash": "^4.17.21" }, "main": "./dist/learn_vue3-shared.umd.js", "module": "./dist/learn_vue3-shared.mjs", "exports": { ".": { "require": "./dist/learn_vue3-shared.umd.js", "module": "./dist/learn_vue3-shared.mjs", "types": "./dist/types/index.d.ts" } } }
|
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
| { "name": "@learn_vue3/components", "version": "0.0.1", "description": "第一次发布,vue组件", "repository": { "type": "git", "url": "git@github.com:chenjieya/monorepo_vue3.git" }, "keywords": [ "vue3", "components" ], "author": "alvis", "license": "MIT", "bugs": { "url": "https://github.com/chenjieya/monorepo_vue3/issues" }, "scripts": { "build": "vite build" }, "peerDependencies": { "vue": ">=3.4.0" }, "dependencies": { "@learn_vue3/shared": "workspace:^" }, "main": "./dist/learn_vue3-components.umd.js", "module": "./dist/learn_vue3-components.mjs", "exports": { ".": { "require": "./dist/learn_vue3-components.umd.js", "module": "./dist/learn_vue3-components.mjs", "types": "./dist/types/index.d.ts" } }, "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| { "name": "@learn_vue3/car_shop", "version": "0.0.0", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "build-only": "vite build", "type-check": "vue-tsc --build --force", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", "format": "prettier --write src/" }, "dependencies": { "@learn_vue3/components": "workspace:^" }, "peerDependencies": { "vue": ">=3.4.0" } }
|