返回片段
发布于 2024 年 10 月 21 日,星期一
Monorepo和Pnpm共同使用
Monorepo架构允许将多个相关项目放在同一个代码仓库中,而Pnpm则通过其独特的依赖管理机制,解决了传统包管理器在Monorepo环境中可能遇到的依赖冗余和性能问题。博客内容可能涵盖了如何配置和使用Pnpm在Monorepo中进行依赖管理,以及这种组合如何帮助开发者更高效地组织和维护大型前端项目。
Monorepo 和 Pnpm
Monorepo 简介
- Monorepo 是一种项目开发与策略管理的策略模式,它代表着 "单一代码仓库(Monolithic Repository)"
为什么项目需要使用 Monorepo?
- 如果有多个项目,并且在多个项目中有着复用组件和复用逻辑。
使用 Monorepo 的好处
- 代码复用:多个仓库都会用到的组件、工具函数、类型声明、样式等,可以放到 common 子包中,
- 独立构建和部署:每个子包都是一个独立的项目,有自己的 package.json 文件,独立安装依赖、独立端口和本地启动、独立测试、独立构建和部署,互不影响
- 降低切换成本:由于只有单一仓库,clone 代码、切换分支、安装依赖比较方便,不用在不同文件夹之间切换。
- 节约磁盘空间:pnpm 天然具备 monorepo 能力,支持全局依赖管理,所有子包之间共享依赖,节约磁盘空间。
- 方便提交 PR:由于是单仓库,增加新组件或给组件增加新特性,只需要提交一个 MR、编写一次 MR 描述、关联一次需求/缺陷单。
- 方便代码检视:一个完整的特性只需要统一在一个 MR 中检视,不用在多个仓库/多个 MR 之间切换。
- 灵活便于扩展:后续增加新的工程只需要在 packages 下增加一个子包,不需要申请新的代码仓库,也降低后续仓库维护成本,比如:配置保护分支 / GitHub Actions / 仓库标签等。
Monorepo (单仓多模块) 开发模式
-
回归单体管理:它允许在一个代码仓库中管理多个项目,组件或服务,提供更好的代码共享和重用性。
-
优点:
- 保留了多仓库的主要优势
- 管理所有项目,版本控制更容易一致,降低了不同项目之间的版本冲突
- 可统一项目的构建和部署流程,降低了配置和维护多个项目需要的工作量
- 代码复用
- 模块独立管理
- 分工明确,业务场景独立
- 代码耦合度降低
-
缺点
- 随着时间推移,导致构建时间较长,管理起来较麻烦
- 项目颗粒度的权限管理较为困难
- 如何解决?
- 使用代码所有权文件:使用如 CODEOWNERS 文件(Github 等平台支持)来指定某个目录和文件的所有者。当这些文件或目录被修改时,需要指定的所有者批准更改。
- 分支策略:通过分支严格管理不同级别的开发人员可以访问和修改的分支,合并的权限。
- 使用 web hooks:在对应的文件发生修改时,执行脚本来给所有者通知并检查。
- 第三方工具:Gitlab 和 Bitbucket 提供了更细颗粒度的权限控制设置。
- 如何解决?
-
为什么组件库项目会选用 Monorepo 模式?
- components: 作为组件库的主要代码,实现各个 UI 组件的核心逻辑。
- shared: 主要存放各种杂七杂八的工具方法。
- theme: 实现组件库的主题样式定制方案。
- cli: 实现组件库模板脚手架的命令行工具。
- docs: 组件库的示例 demo 与使用文档。
- playground: 组件库的在线编辑、演示应用。
- S:模块划分的越清晰,复用时的灵活性、可操作性就越强,每个独立模块产物的体积也会越轻量。
Pnpm 在 Monorepo 中使用
- 我们项目中有一个 main 应用,在 web 文件夹下还有一个 react 应用和 vue 应用,我们可以用 pnpm 对依赖进行统一管理
- 安装
# 安装npm i pnpm -g# 检验是否安装成功pnpm -v
- 在根目录 pnpm 初始化生成 package.json
pnpm init
- 配置工作空间, 新建 pnpm-workspace.yaml 文件
- 配置 pnpm-workspace.yaml 文件
- 安装项目依赖,在根目录运行如下命令,一键为所有项目安装依赖
pnpm i
7. 暴露公用方法,创建 common 文件夹及 index.ts
8. 在 common 文件夹中运行 pnpm init 初始化
pnpm init
- 在 pnpm-workspace.yaml 文件中添加 common 文件夹
- 在 common 下编写 index.ts 文件暴露方法
export const hello = () => { console.log("hello");};
- 根目录运行 pnpm -F main-project add common 将 common 里的方法暴露给 main-project
- 这里的-F 是--filter 的简写,用于过滤指定的 package,用法 pnpm --filter
pnpm -F main-project add common
- 在 vue 和 react 项目页面中引入公共方法
- 启动项目
pnpm -F main-project dev
Pnpm 常用命令
#安装软件包及其依赖的任何软件包 如果workspace有配置会优先从workspace安装pnpm add <pkg>#安装项目所有依赖pnpm install#更新软件包的最新版本pnpm update#移除项目依赖pnpm remove#运行脚本pnpm run#创建一个 package.json 文件pnpm init#以一个树形结构输出所有的已安装package的版本及其依赖pnpm list
- 为指定模块安装外部依赖
- 例如:为 A 包安装 lodash
// 为 a 包安装 lodashpnpm --filter a i -S lodash // 生产依赖pnpm --filter a i -D lodash // 开发依赖
- 指定内部模块之间的相互依赖
- 例如:为 a 包安装内部依赖 b。
// 指定 a 模块依赖于 b 模块pnpm --filter a i -S b
- pnpm workspace 对内部依赖关系的表示不同于外部,它自己约定了一套 Workspace 协议 (workspace:)https://pnpm.io/zh/workspaces#workspace-%E5%8D%8F%E8%AE%AE-workspace。
{ "name": "a", // ... "dependencies": { "b": "workspace:^" }}
- 在实际发布 npm 包时,workspace:^ 会被替换成内部模块 b 的对应版本号(对应 package.json 中的 version 字段)。替换规律如下所示:
{ "dependencies": { "a": "workspace:*", // 固定版本依赖,被转换成 x.x.x "b": "workspace:~", // minor 版本依赖,将被转换成 ~x.x.x "c": "workspace:^" // major 版本依赖,将被转换成 ^x.x.x }}
现有项目改造成 Monorepo 的步骤
- 创建子包
- 第一步就是在根目录创建 packages 目录,增加项目子包,比如项目叫:portal
root├── packages| └── portal| ├── ... // 项目文件和目录
- 现有项目文件放进子包里
- 把现有工程的 src / public / package.json / vite.config.ts / tsconfig.xx.json / index.html / README.md 等项目启动和构建相关的目录和文件全部剪切到 packages/portal 目录中
root├── packages| └── portal| ├── index.html| ├── package-lock.json| ├── package.json| ├── public| ├── README.md| ├── src| ├── tsconfig.app.json| ├── tsconfig.json| ├── tsconfig.node.json| └── vite.config.ts
- 配置 pnpm-workspace.yaml
- 根目录创建 pnpm-workspace.yaml 文件。
packages: - packages/**
- 配置 package.json
- 项目原来的 package.json 属于子包,需要放到 portal 子包中。
- 项目根目录需要创建一个新的 package.json 文件。
{ "name": "root", "private": true}
-
改造前后目录结构对比
-
验证本地启动和构建命令
执行 pnpm i 安装依赖执行 pnpm -F portal dev 本地启动执行 pnpm -F portal build 项目构建如果以上命令都正常,说明本次 Monorepo 改造成功!
- 增加便捷命令
- 将本地启动命令放到根目录的 packages.json scripts 中,方便启动。
{ "name": "root", "private": true,+ "scripts": {+ "dev": "pnpm -F portal dev",+ "build": "pnpm -F portal build",+ "preview": "pnpm -F portal preview"+ }}
- 后续启动项目:pnpm dev
- 构建项目:pnpm build
- 增加一个新项目 admin - 在 packages 目录下执行 npm create vite admin,选择 React 框架。
执行 pnpm i 安装依赖执行 pnpm -F admin dev 本地启动 admin 新项目执行 pnpm -F admin build 构建 admin 新项目
-
两个项目同时启动了。
-
实现了 portal / admin 两个项目分开启动、分开构建、分开管理依赖、分开测试,互不影响,而且 portal 是 Vue 技术栈,admin 是 React 技术栈。
-
可以在根目录的 package.json scripts 增加对应的便捷命令。
{ "name": "root", "private": true, "scripts": { "dev": "pnpm -F portal dev", "build": "pnpm -F portal build", "preview": "pnpm -F portal preview",+ "dev:admin": "pnpm -F portal dev",+ "build:admin": "pnpm -F portal build",+ "preview:admin": "pnpm -F portal preview" }}
root├── package.json├── packages| ├── admin| | ├── eslint.config.js| | ├── index.html| | ├── package.json| | ├── public| | ├── README.md| | ├── src| | ├── tsconfig.app.json| | ├── tsconfig.json| | ├── tsconfig.node.json| | └── vite.config.ts| └── portal| ├── index.html| ├── package-lock.json| ├── package.json| ├── public| ├── README.md| ├── src| ├── tsconfig.app.json| ├── tsconfig.json| ├── tsconfig.node.json| └── vite.config.ts├── pnpm-lock.yaml└── pnpm-workspace.yaml
- 如果有些逻辑 portal / admin 都用到了,我们可以新加一个子包:common
- 然后在 portal / admin 中引入 common。
pnpm -F portal i commonpnpm -F admin i common
# 构建工具# 状态管理# 代码质量# 版本控制# DevOps