一文搞懂pnpm+monorepo的原理

pnpm是什么

pnpm是一个高性能的包管理工具。我们可以理解为performant npm(高性能的npm)的缩写;

可以看到官网给的定义,pnpm是一个包管理工具。

核心优势

  • 节省磁盘空间
  • 提高安装速度
  • 创建一个非扁平的 node_modules 目录

monorepo是什么

Monorepo (monolithic repository,单一代码仓库)指的是: 把多个项目/包放在同一个仓库里而不是每一个包一个独立的仓库

my-project/ 复制代码
├── packages/
│   ├── ui/         # UI 组件库
│   ├── utils/      # 工具库
│   └── app/        # 业务应用
├── package.json
└── tsconfig.json

这些包既能独立发布,又能共享同一个仓库的配置。

monorepo的核心特点

  • 单仓库多包: 所有包在同一个repo下的不同目录。
  • 统一依赖管理: 依赖通安装在根目录,子包通过workspace互相引用。
  • 包间相互引用: 本地直接import, 调试开发更高效。

对比 Polyrepo(每个项目单独一个仓库) ,Monorepo 有这些优势:

依赖一致性

  • 一个地方定义 React 版本,所有包都用同样的版本,不容易冲突。

开发效率高

  • 改了 utils 包,app 可以立刻用,无需发布再安装。

统一规范和工具链

  • 代码风格、测试、CI/CD 流程都能共享配置。

更容易重构和大规模协作

  • 所有包代码都在同一个仓库,跨包修改时更直观。

这里我们组件库+icon库+多个业务项目是非常契合这种架构的。

monorepo+pnpm使用

pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库)的支持。 一个workspace必须在根目录有一个,可能有一个.npmrc.

工作协议workspace

link-workspace-packages设置为true 时,则 pnpm 将在可用包与声明的范围匹配时链接工作区中的包。比如workspaceA项目依赖@B1.0.0包,并且@B1.0.0workspace 中时,则A会链接到@B1.0.0包,但是如果依赖@B2.0.0包,而工作区中没有,则会从远程仓库进行安装,如何远程仓库也没有,则会报错,这种行为会带来一定的不确定性。这时候可以使用pnpm支持的workspace:协议,如果使用workspace:协议,pnpm 将仅链接来自工作区的包。

使用

假设你在 workspace 中有一个名为 foo 的包, 通常,你会将其引用为 "foo":"workspace:*"

如果你想使用不同的别名,以下语法也将起作用: "bar": "workspace:foo@*"

在发布之前,别名被转换为常规名称。 上述示例将变成:"bar": "npm:foo@1.0.0"。 如果我们在工作空间中有 foobarqarzoo ,它们都是版本1.5.0,如下所示 :

json 复制代码
{  
    "dependencies": {  
    "foo": "workspace:*",  
    "bar": "workspace:~",  
    "qar": "workspace:^",  
    "zoo": "workspace:^1.5.0"  
    }  
}

打包发布时将转换为:

json 复制代码
{  
"dependencies": {  
    "foo": "1.5.0",  
    "bar": "~1.5.0",  
    "qar": "^1.5.0",  
    "zoo": "^1.5.0"  
    }  
}

pnpm原理

要了解pnpm的原理,我们必须先知道硬连接和软连接

硬连接和软连接的概念

  • 硬连接:计算机文件系统中的多个文件平等地共享同一个文件存储单元。 如果其中一个文件被修改,其他硬链接文件也会同步被修改。
  • 软连接: 是一类特殊的文件, 其包含有一条以绝对路径或者相对路径的形式指向其它文件或者目录的引用。如果原文件被删除,软链接就失效了。其本质是保存某一个文件的路径。

pnpm安装

要了解其关系,我们先来看下执行pnpm install的时候都发生了什么?

  1. npmyarn类似,读取package.jsonpackage-lock.yaml,从npm registry拉取最新匹配的版本并生成lock。最终得到一份完整的依赖树。
  2. 下载依赖到全局内容寻址区(store)或者复用,mac目前是在/pnpm/store目录下。pnpm store path进行查看,包的目录基于内容hash,所以相同的包只会存储一次,比如两个项目都依赖ant-design@1.0.0,只会下载一次和存储一次。
  3. 连接到项目的node_modules, 下载完成之后,pnpm不会复制文件,而是用硬链接或者软链接的形式生成到项目的node_modules下。(项目node_modules不是扁平化的,而是采用虚拟存储结构)。
  4. 更新lockfile。如果依赖版本有变化,pnpm会更新pnpm-lock.yaml文件。
  5. 校验完整性(通过文件hash值),保证下载没有被篡改。

实操

下面实操一下,初始化一个pnpm-demo仓库 ,安装一个axios依赖。我们可以看到,download数量为0 ,reused数量23,那么这个就是在全局store中找到了对应依赖,通过硬连接的方式到当前项目中,所以速度会非常快。而且硬连接,和全局store中对应的包指向同一个存储地址,并不会copy一份开辟新的存储空间,所以也节省了很大的磁盘空间,而且项目越多,相比于节省的磁盘空间也越多。

node_modules目录如下图所示。

axios依赖右方箭头代表该文件是一个软连接,然后有一个.pnpm目录。展开(下图)可以看到没有软链接箭头,代表是硬连接,里面除了axios以外还有很多包,为axios包的所有依赖和依赖的依赖...在.pnpm目录中平铺展示。

再看下图npm安装一个axios是什么样的,和pnpm项目下的.pnpm目录类似。

为什么要将实际包都放在.pnpm文件下呢?

我们项目中引入三方包,比如import react from 'react' , 1. 判断是否是核心模块fs,path这种,不是的话找当前目录下的node_modules,没找到的话向上层继续寻找,类似于作用域链 的概念。那么这时候我们找axios,就会在当前目录的node_modules中查找,找到之后导入。这时候,我们如果导入一个axios的依赖呢?npm在就会在当前目录下找到并且导入,而pnpm就会因为没有找到并且报错,这里pnpm的行为才是符合我们预期的,因为我们的package.json没有这个依赖。

pnpm是如何通过workspace:协议实现monorepo的?

首先创建demo2作为依赖包(比如组件库,Hooks库等)。

然后创建demo作为使用依赖包的项目。这里添加依赖包demo2dependencies中,执行pnpm install

然后可以看到demo下的node_modules下生成了一个软链接的demo2依赖包。

我们再看一下lock文件,demo中的依赖包demo2通过node_modules软链接到了实际的demo2目录中。 所以我们本地通过import 'demo2' 这种形式去安装并引入本地依赖包,实际上是通过查找到noode_modulesdemo2依赖包的软链接转到实际的demo2项目里面,从而实现了丝滑的monorepo

pnpm用法

基础用法

arduino 复制代码
pnpm add pkg // 安装软件包及其依赖的任何软件包。 默认情况下,任何新软件包都被安装为生产依赖项。
pnpm install // 用于安装项目的所有依赖项。
pnpm update // 基于指定的范围更新包到它们的最新版本。
pnpm remove // 从 node_modules 和 package.json 中删除软件包。
pnpm link // 引用本地项目以外的包,本质就是通过软链接,原理和前面workspace类似,但是没workspace可靠
pnpm unlink // 取消链接一个系统范围的 package
pnpm run // 执行script脚本

进阶一点

csharp 复制代码
 pnpm store add pkg // 直接将包加在全局store
 pnpm store path // 查看全局store的路径
 pnpm store prune // 清理全局包未被应用的包,不建议频繁使用,可以在磁盘空间不够的时候的一个优化手段。

总结一下

  • pnpm通过全局store存储依赖,同构软件链接➕硬连接的方式实现包的多个仓库复用,从而节省磁盘空间 ,提高安装效率
  • 通过node_modules.pnpm存放实际的依赖包(硬链接),将项目直接安装的包通过软链接方式直接安装在node_modules目录下,从而避免了幽灵依赖 的发生,提高项目的可靠性
  • workspace:协议通过软链接的方式将本地的依赖包安装到了项目中,从而实现了高效的modorepo

相关资料

相关推荐
艾小码14 小时前
告别无效加班!这4个表单操作技巧,让你效率翻倍
前端·javascript·html
TimelessHaze14 小时前
面试必备:深入理解 Toast 组件的事件通信与优化实现
前端·trae
zayyo14 小时前
从 Promise 到 Generator,再到 Co 与 Async/Await 的演进
前端·javascript
我的写法有点潮14 小时前
这么全的正则,还不收藏?
前端·javascript
XiaoSong14 小时前
React 表单组件深度解析
前端·react.js
薛定谔的算法14 小时前
标准盒模型与怪异盒模型:前端布局中的“快递盒子”公摊问题
前端·css·trae
stroller_1214 小时前
React 事件监听踩坑:点一次按钮触发两次请求?原因竟然是这个…
前端
文艺理科生14 小时前
Nuxt 应用安全与认证:构建企业级登录系统
前端·javascript·后端
彭于晏爱编程14 小时前
🌍 丝滑前端国际化:React + i18next 六语言实战
前端