Monorepo
什么是 Monorepo ?
Monorepo
是管理项目代码的方式之一,指在一个大的项目仓库(repo)中 管理多个模块/包(package),这种类型的项目大都在项目根目录下有一个 packages 文件夹,分多个项目管理。大概结构如下:
├── packages
| ├── pkg1
| | ├── package.json
| ├── pkg2
| | ├── package.json
├── package.json
目前很很多大型项目采用这样的结构,比如:Babel
、vue3
和 vite
等。
Monorepo 的好处在哪里嘞?
- 统一管理。比如微前端项目,多个子应用可以放在同一个
monorepo
中方便管理;后端用node.js
的项目放在monorepo
中也可以使用同一套技术栈管理。在 CI/CD 等流水线过程中,方便统一迭代或升级版本,也方便做通用化的配置,适用到多个子项目当中。 - 依赖提升。如果多个项目都依赖了诸如
react
、vue
或TypeScript
等常用库,那可以通过lerna
或者yarn workspace
将依赖提升到最外层,多个子模块/包复用同一个依赖,减小项目体积。
教程
包管理器使用 workspaces:www.cnblogs.com/wp-leonard/…
lerna 构建
.
├── lerna.json
├── package.json
└── packages
├── vue1
│ ├── __tests__
│ ├── lib
│ ├── README.md
│ └── package.json
└── vue2
├── __tests__
├── lib
├── README.md
└── package.json
-
lerna init
-
packages 目录下创建子项目模块
按照
mono-repo
的惯例,这几个子项目的名称最好命名为@<主项目名称>/<子项目名称>
,这样当别人引用你的时候,你的这几个项目都可以在node_modules
的同一个目录下面,目录名字就是@<主项目名称>
,所以我们手动改下三个子项目package.json
里面的name
这里我按照 vue 项目流程创建
-
lerna bootstrap
packages/下面的每个子项目有自己的 node_modules,如果将它打开,会发现很多重复的依赖包,这会占用我们大量的硬盘空间。lerna 提供了另一个强大的功能:将子项目的依赖包都提取到最顶层,我们只需要先删除子项目的 node_modules 再跑下面这行命令就行了:lerna bootstrap --hoist
lerna bootstrap --hoist 虽然可以将子项目的依赖提升到顶层,但是他的方式比较粗暴:先在每个子项目运行 npm install,等所有依赖都安装好后,将他们移动到顶层的 node_modules。这会导致一个问题,如果多个子项目依赖同一个第三方库,但是 需求的版本不同怎么办?比如我们三个子项目都依赖 antd,但是他们的版本不完全一样
vue1 和 vue2 项目需要的 element 版本都是 3.1.0,但是 common 需要的版本却是 4.9.4,如果使用 lerna bootstrap --hoist 来进行提升,lerna 会提升用的最多的版本,也就是 3.1.0 到顶层,然后把子项目的 node_modules 里面的 antd 都删了。也就是说 common 去访问 element 的话,也会拿到 3.1.0 的版本,这可能会导致 common 项目工作不正常
yarn workspace 可以解决前面说的版本不一致的问题。
lerna bootstrap --hoist
会把所有子项目用的最多的版本移动到顶层,而yarn workspace
则会检查每个子项目里面依赖及其版本,如果版本不一样则会留在子项目自己的node_modules
里面,只有完全一样的依赖才会提升到顶层。// 顶层package.json { "workspaces": [ "packages/*" ] }
// lerna.json { "npmClient": "yarn", "useWorkspaces": true }
使用了
yarn workspace
,我们就不用lerna bootstrap
来安装依赖了,而是像以前一样yarn install
就行了,他会自动帮我们提升依赖,这里的yarn install
无论在顶层运行还是在任意一个子项目运行效果都是一样的。 -
在顶层运行了
lerna run start
,这相当于去每个子项目下面都去执行yarn run start
或者npm run start
,具体是yarn
还是npm
,取决于你在lerna.json
里面的这个设置:"npmClient": "yarn"
只想在其中一个子项目运行命令
// 顶层package.json { "scripts": { "start:vue": "lerna --scope @mono-repo-demo/vue run start"//--scope来指定在管理员子项目下运行 } }
-
引入公共组件:lerna add @mono-repo-demo/common --scope @mono-repo-demo/vue1
common 组件写好了,我们就在 vue1 里面引用下他,要引用上面的组件,我们需要先在 vue1 的 package.json 里面将这个依赖加上,我们可以去手动修改他,也可以使用 lerna 命令:
//packages\vue\package
"dependencies": {
"@mono-repo-demo/common": "^0.0.0",
},
pnpm 构建
构建npm:lint 包
背景
某些工具方法或者组件的使用频率很高,好多项目都在用。如何做到这些工具方法或者组件的更优雅地复用而不是用到了就复制粘贴呢?封装为一个 npm 包是一个不错的选择。
lint 包配置
-
创建 npm/lint 文件夹
-
pnpm init
-
package.json
{ "name": "@lm/eslint-plugin-lint", "version": "1.0.0", "main": "index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "author": "", "license": "ISC", "description": "", "publishConfig": { "access": "public" }, "dependencies": { } }
-
-
根目录配置
Workspaces工作区
概念
workspaces
指定哪些目录或项目属于当前的工作空间,使得 pnpm 能够在这些项目之间有效地共享依赖和进行管理。(yarn 很早就支持了,npm
在 7.x
中开始支持,也就是 Node@15.0.0 新增的功能)。避免手动的去执行 npm link
命令,而是在 npm install
的时候,会自动把 workspaces
下面的合法包,自动创建符号链接到当前的 node_modules
文件夹里。
- 它会将子包中所有的依赖包都提升到根目录中进行安装,提升包安装的速度;
- 它初始化后会自动将子包之间的依赖进行关联(软链接);
- 因为同一个项目的关系,从而可以让各个子包共享一些流程,比如:eslint、stylelint、git hooks、publish flow 等;
配置
-
根目录
-
为了让各个项目之间能够互相引用,需要新建一个 pnpm-workspace.yaml 文件
packages: - 'npm/**'
-
运行
pnpm install
,./packages
内的所有包都将创建符号链接到当前根目录的node_modules
文件夹里
-
项目使用
pnpm add @lm/lint --workspace
--workspace`: 表示在工作区的所有包中添加该依赖。
假设你的项目结构如下:
my-monorepo/ ├── packages/ │ ├── pkg1/ │ │ └── package.json │ ├── pkg2/ │ │ └── package.json └── pnpm-workspace.yaml
执行
pnpm add @lm/lint --workspace
后,@lm/lint
将被安装到my-monorepo/node_modules
目录,并且pkg1
和pkg2
都可以使用@lm/lint
。
.eslint 使用
module.exports = {
'extends': [
'plugin:@lm/lint/vue',
],
}
构建npm:shared包
文件目录
package
{
"name": "@lm/shared",
"version": "1.0.0",
"main": "./lib",
"directories": {
"lib": "lib"
},
"files": [
"lib"
],
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "tsc"
},
"author": "liming",
"license": "MIT",
"description": "",
"publishConfig": {
"access": "public"
},
"dependencies": {
"lodash": "^4.17.21",
"typescript": "^5.1.6"
},
"devDependencies": {
"moment": "^2.29.1"
}
}
tsconfig
这里我在lib只导出js,并没有声明declaration文件。但是引用的时候,依然能智能提示
{
"baseUrl": "./",
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"outDir": "./lib",
"lib": ["es6"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
// "declaration": true,
// "declarationDir": "./lib/ts",
// "declarationMap": true // 生成类型声明映射文件(可选)
},
"include": ["**/*"],
"exclude": ["lib"]
}
打包
执行tsc/npm run build
但是TypeScript 编译器本身不处理样式文件,所以不会打包scss文件,这个后期再处理
根目录配置
已经配置了lint包就不需要了
使用
在项目中执行pnpm add @lm/lint --workspace
import $lm from '@lm/shared';
console.log($lm.lodash, 222);