Monorepo与pnpm:解构幽灵依赖与符号链接机制
2026/9/12 22:36:13 网站建设 项目流程

前段时间在整理前端工程化面试题时,我遇到一个很有意思的连环追问:

“项目为什么用 Monorepo?”

“pnpm 的优势是什么?”

“那 pnpm 怎么做到‘没声明就不能用’的?”

前两问,大多数候选人能聊上几句,比如代码复用、安装速度快、依赖隔离。但问到第三问,很多人会卡住。大家用着 pnpm,却不清楚它背后那段“非扁平 node_modules + 符号链接”的依赖解析机制。这篇文章就把 Monorepo、pnpm、幽灵依赖三者串成一条线,从原理讲到实验,从安装讲到排错,帮你在面试和实际项目里都能把这一整块问题说清楚。

1. 从一个面试场景说起:Monorepo、pnpm 与幽灵依赖

先还原一下面试中的完整场景。面试官想看候选人是否真的理解工程化选型,所以不会只问“你用没用过 pnpm”,而是会沿着一条逻辑链往下挖:

  1. 项目为什么选 Monorepo?
  2. 在 Monorepo 场景下,为什么选 pnpm 而不是 npm 或 yarn?
  3. pnpm 声称能解决幽灵依赖,它是用什么机制解决的?
  4. 追问:pnpm 怎么做到“package.json 里没声明,代码里就不能用”?

你会发现,这三个问题其实是层层递进的。Monorepo 是一个仓库组织策略,pnpm 是为这个策略提供工程能力的包管理器,而幽灵依赖是传统包管理器在 Monorepo 场景下最容易暴露的问题。搞懂了最后一条,你才算真正理解了 pnpm 的设计核心。

为了便于理解,先给三个概念做一句话定位:

  • Monorepo:把多个项目放进同一个 Git 仓库统一管理。
  • pnpm:一个速度快、省磁盘、依赖隔离严格的包管理器。
  • 幽灵依赖:代码里直接使用了一个没有在 package.json 中声明的包,却因为 node_modules 的扁平化结构“侥幸”能运行。

下面逐个展开。

2. pnpm 安装与环境准备

正式开始动手之前,先把 pnpm 装好。pnpm 是一个 Node.js 生态的包管理器,所以安装 pnpm 之前,本机要先有 Node.js 环境。

2.1 检查 Node.js 环境

在终端执行:

node -v npm -v

如果你还没有安装 Node.js,建议先安装一个较新的 LTS 版本。不同项目对 Node 版本要求不同,建议使用 nvm(macOS/Linux)或 nvm-windows(Windows)来管理 Node 版本,方便随时切换。

2.2 安装 pnpm 的三种方式

pnpm 官方提供了多种安装方式,这里列出三种最常见的。

方式一:使用 Corepack 安装(Node.js 自带,推荐)

Node.js 16.9 及以上版本自带 Corepack,可以用它来管理和启用 pnpm:

corepack enable corepack prepare pnpm@latest --activate

这种方式的好处是无需手动安装全局工具,也不容易出现 PATH 找不到命令的问题。

方式二:通过 npm 安装

npm install -g pnpm

安装后可以用pnpm -v验证版本。如果你在 Windows 上遇到权限问题,可以尝试用管理员身份打开终端再执行。

方式三:macOS / Linux 使用官方安装脚本

curl -fsSL https://get.pnpm.io/install.sh | sh -

安装完成后,重新打开终端,执行:

pnpm -v

如果能够输出版本号,说明 pnpm 已经安装成功。

2.3 配置国内镜像

由于网络原因,直接安装依赖可能会很慢。如果安装速度不理想,可以把 registry 切换到国内镜像:

pnpm config set registry https://registry.npmmirror.com

设置后,建议查看一下当前配置:

pnpm config get registry

如果输出的是https://registry.npmmirror.com,说明镜像已生效。

2.4 Windows 常见安装问题

很多 Windows 用户在安装 pnpm 后,会遇到类似这样的报错:

pnpm : 无法将“pnpm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。

或者:

'pnpm' 不是内部或外部命令,也不是可运行的程序或批处理文件。

这通常说明 pnpm 已经安装,但它所在的全局 bin 目录没有加入系统 PATH。解决办法如下:

  1. 执行pnpm setup,让 pnpm 自动配置 PATH。
  2. 重新打开终端窗口。
  3. 如果问题依然存在,手动将全局安装目录加入 PATH。
  4. 如果你使用 nvm-windows 管理 Node,全局安装目录通常在C:\Users\你的用户名\AppData\Roaming\npm下。

另外,如果你安装的 pnpm 版本较新,而本机 Node.js 版本偏低,可能会出现类似这样的报错:

error: this version of pnpm requires at least node.js v22.13

意思是当前 pnpm 版本要求更高版本的 Node.js。解决方式有两种:升级本机 Node.js 到报错要求的版本,或者安装一个兼容当前 Node 版本的 pnpm 版本。

3. Monorepo 的核心价值与适用场景

现在进入第一个面试题:项目为什么用 Monorepo?

3.1 什么是 Monorepo

Monorepo 并不是一种具体的软件,而是一种仓库管理策略。它的核心思想是:把多个应用、多个包、多个服务的源码放到同一个 Git 仓库中,使用同一套依赖管理、构建流程和发布流程。

与 Monorepo 相对的是 MultiRepo(多仓库),也就是每个项目单独建一个 Git 仓库,项目之间通过发布 npm 包或服务接口来协作。

在 Monorepo 出现之前,很多团队的做法是:

  • 公共组件库单独一个仓库。
  • 业务项目 A 单独一个仓库。
  • 业务项目 B 单独一个仓库。

当公共组件库需要改一个接口时,流程是:改组件库 → 发布新版本 → 去项目 A 升级依赖 → 去项目 B 升级依赖。如果项目 A 和项目 B 都忘了升级,就会出现多个版本并存的问题。

Monorepo 的做法是:所有项目放在同一个仓库里,公共代码通过 workspace 协议直接引用,改完公共代码,所有引用它的项目立刻感知。

3.2 Monorepo 解决了什么

Monorepo 带来的收益,主要体现在四个方面。

第一,代码复用更自然。公共包不需要先发布到 npm registry,再在业务项目里安装。通过 workspace,你可以在仓库内直接引用另一个包的源码。本地开发时,改完公共包,业务项目马上生效。

第二,原子提交成为可能。一个功能如果同时涉及前端项目、公共组件和后端 API 定义,在 Monorepo 中可以一次提交全部搞定。代码评审时,也能看到完整的变更上下文。

第三,依赖版本更容易统一。多个项目共享同一套依赖解析结果,可以减少“环境不一致”带来的问题。尤其当多个微前端子应用需要共用同一版本的 React 或 Vue 时,Monorepo 优势非常明显。

第四,统一构建和发布流程。只需要维护一套 CI 配置、一套代码规范、一套测试策略,而不是为每个仓库各配一套。

下面用表格对比多仓库和 Monorepo 的差异:

维度MultiRepo(多仓库)Monorepo(单仓库)
代码共享通过发布 npm 包通过 workspace 直接引用
跨项目改动多次提交、多次发布一次提交完成
版本一致性依赖版本容易漂移统一依赖解析、统一版本
构建与 CI每个仓库独立配置统一配置,可增量构建
仓库权限管理仓库维度隔离需要目录/业务维度约束
仓库体积每个仓库较小仓库会持续变大

3.3 Monorepo 的缺点与适用场景

Monorepo 并不是银弹。它的缺点也很明显:

  • 仓库体积会越来越大,克隆和下载成本变高。
  • 权限控制粒度变粗,很难限制某个团队只能访问某个子目录。
  • CI 如果配置不当,容易变成“每次提交全量构建”,导致构建时间变长。
  • 团队规范要求更高,否则容易变成“大乱炖”。

所以,Monorepo 更适合以下场景:

  • 多个前端应用共享大量 UI 组件或工具库。
  • 多个 Node.js 服务共享公共 SDK。
  • 微前端架构下,多个子应用需要统一依赖管理。
  • 团队希望用一套工具链统一管理所有前端项目。

如果你的项目是独立的单页应用,并且没有和其他项目共享代码的需求,用普通单仓库就够了,没有必要强行上 Monorepo。

4. pnpm 相比 npm/yarn 的核心优势

第二个面试题是:pnpm 的优势是什么?这一节我们展开讲。

4.1 速度快

pnpm 的安装速度比 npm 和 yarn classic 更快,原因有三个:

  1. 并行下载依赖,而不是串行逐一安装。
  2. 使用全局内容寻址存储(Content-Addressable Store),同一个版本的包在机器上只存一份。
  3. 通过硬链接直接链接到项目目录,不需要重复解压和复制。

真实项目里,如果你用 npm 安装一个依赖很大的项目,耗时可能需要好几分钟。而 pnpm 在首次安装后,第二次安装几乎就是秒级完成,因为它可以复用全局 store 中的缓存。

4.2 省磁盘空间

npm 的做法是:每个项目都会把依赖完整地复制到自己的node_modules目录里。如果你在十个项目里都用了lodash,磁盘上就有十份几乎一样的lodash

pnpm 的做法是:所有依赖的真实文件都存放在全局 store 中,项目里的node_modules通过硬链接从 store 指向这些文件。如果你的项目很多,并且共享了大量依赖,pnpm 能节省非常可观的磁盘空间。

你可以使用pnpm store path查看 store 的路径,使用pnpm store prune清理不再被引用的缓存包。

4.3 依赖隔离严格

这是 pnpm 最核心的设计亮点。

npm 会把所有依赖扁平化安装到顶层node_modules中,导致你的代码可以访问那些并没有在 package.json 中声明的包,也就是“幽灵依赖”。

pnpm 默认不会这样做。它采用非扁平结构,项目顶层node_modules里只会有你直接声明过的依赖,其他传递依赖会被放到一个隐藏的.pnpm目录中,并通过符号链接精确连接到对应位置。这样,代码里只能用 package.json 中声明过的包,没声明就找不到。

4.4 对 Monorepo 的原生支持

pnpm 原生支持 workspace,只需要一个pnpm-workspace.yaml文件,就能管理一个包含多个包的 Monorepo 仓库。它提供了pnpm --filter等命令,可以精确地只安装、构建某个子包,这在大型 Monorepo 中非常实用。

4.5 命令迁移成本低

pnpm 的命令和 npm 非常接近,比如:

npm 命令pnpm 命令
npm installpnpm install
npm run devpnpm dev
npm run buildpnpm build
npm add lodashpnpm add lodash
npm remove lodashpnpm remove lodash

大多数情况下,把 npm 换成 pnpm 就能直接使用,团队迁移成本比较低。

5. 幽灵依赖:问题从哪来,危险在哪

第三个面试题是:如何解决幽灵依赖?要理解 pnpm 的解法,先要知道幽灵依赖是什么。

5.1 幽灵依赖是什么

幽灵依赖(Ghost Dependency / Phantom Dependency)指的是:你的代码里直接引入了一个包,但这个包并没有在你的 package.json 中声明。

举个例子:

{ "name": "my-app", "version": "1.0.0", "dependencies": { "eslint": "^8.0.0" } }

你的项目只声明了eslint,但 eslint 内部依赖了lodash。在 npm 的扁平化 node_modules 结构下,lodash会被提升到顶层node_modules/lodash。于是,你的源码里可以直接写:

const _ = require('lodash');

而且程序能正常运行。

这个lodash就是一个幽灵依赖。你明明没有在 package.json 里声明它,它却因为“别人依赖它”而间接存在于你的项目里。

5.2 为什么会产生幽灵依赖

问题出在 npm 的依赖提升策略上。

为了兼容 Node.js 的模块解析机制并避免非常深的目录层级,npm 从 v3 开始把依赖树扁平化。当一个包被安装时,npm 会把它的全部传递依赖尽可能提升到顶层 node_modules。这样做的结果是:所有被打包进项目的依赖,无论是否被声明,都在顶层目录里“可见”。

视觉上,你打开node_modules会发现里面躺着一堆你根本不认识的包。这堆包不是你安装的,而是“别人”的依赖被提升上来了。

5.3 幽灵依赖为什么危险

幽灵依赖最大的危险在于“侥幸运行”。

你写了一行require('lodash'),开发时一切正常。某天 eslint 升级了,它的内部依赖变了,lodash不再被 eslint 依赖,npm 安装后就不会把lodash提升到顶层。于是你的项目突然报错:

Cannot find module 'lodash'

更隐蔽的是,即使lodash还在,但它可能被升级到了一个大版本,某个 API 被移除,你的代码就会在运行时崩溃。

这类问题很难定位,因为报错信息不会提示“你少声明了一个依赖”,只会提示某个模块找不到。尤其在大型 Monorepo 里,依赖关系复杂,排查成本非常高。

5.4 pnpm 如何解决幽灵依赖

pnpm 的解决思路,是彻底放弃 npm 式的扁平化 node_modules,改为“非扁平化 + 符号链接”的结构。我们把这种结构放到下一节详细拆解,因为这也对应面试官最后那个追问:

“pnpm 怎么做到 package.json 里没声明就不能用?”

6. pnpm 如何做到“没声明就不能用”:原理拆解与动手验证

这一节是整篇文章的重点,也是面试官最想听的部分。

6.1 Node.js 的模块查找机制

要理解 pnpm 的严格隔离,先要理解 Node.js 是如何找到模块的。

当你在某个文件里写:

const lodash = require('lodash');

Node.js 会从当前文件所在的目录开始,逐级向上查找node_modules目录:

  1. 查找/项目目录/src/node_modules/lodash
  2. 查找/项目目录/node_modules/lodash
  3. 查找/上级目录/node_modules/lodash
  4. 一直查到系统根目录

只要在某个层级找到了lodash,就停止查找并加载。如果一直找不到,就会抛出:

Error: Cannot find module 'lodash'

所以,“某个包能不能被使用”只取决于一件事:它是否存在于当前文件向上查找的任意一个node_modules路径中。

pnpm 正是通过控制这些路径里的内容,来实现“声明了才能用”。

6.2 pnpm 的非扁平 node_modules 结构

一个典型 pnpm 项目安装后的 node_modules 结构大概是这样的:

node_modules/ ├── .pnpm/ │ ├── lodash@4.17.21/ │ │ └── node_modules/ │ │ └── lodash/ │ └── pkg-a@1.0.0/ │ └── node_modules/ │ ├── lodash -> ../../lodash@4.17.21/node_modules/lodash │ └── pkg-a └── pkg-a -> .pnpm/pkg-a@1.0.0/node_modules/pkg-a

注意几个关键点:

  • 项目顶层的node_modules中,只有你直接声明的依赖被创建了符号链接,比如这里的pkg-a
  • 真实依赖文件存放在node_modules/.pnpm/<name>@<version>/node_modules/下。
  • 每个包内部依赖的其他包,都以符号链接的形式放在自己的node_modules中。

简单来说,pnpm 把 node_modules 从“一个全公司共享的大平层”改成了“每个包独立的小房间”。

6.3 “没声明就不能用”的完整链路

现在把 Node.js 解析机制和 pnpm 目录结构结合起来,你就能回答面试官了。

假设你的项目根目录package.json是这样:

{ "name": "my-app", "dependencies": { "pkg-a": "1.0.0" } }

你在根目录的index.js里写:

require('lodash');

Node.js 会从index.js所在目录开始向上查找node_modules/lodash。但 pnpm 安装后,根目录node_modules下只有pkg-a这个符号链接,根本没有lodash。所以 Node.js 一直向上找,也找不到lodash,最终抛出:

Cannot find module 'lodash'

这就是 pnpm 的“没声明就不能用”:直接依赖被符号链接暴露在顶层,而不在顶层暴露传递依赖。

反过来,pkg-a内部为什么可以用 lodash?因为 pnpm 会在pkg-a自己的node_modules目录里放置指向lodash的符号链接。也就是说,每个包只能看到自己 package.json 中声明的依赖。

6.4 硬链接与符号链接各司其职

这里有个常见的理解误区:pnpm 省磁盘用的是硬链接,还是符号链接?

答案:两者都在用,但作用不同。

  • 符号链接(symlink)负责“让模块能被找到”,解决的是依赖解析问题。
  • 硬链接(hard link)负责“让磁盘上只存一份内容”,解决的是空间占用问题。

具体安装时,pnpm 会把依赖的真实内容存放在全局 store 中,然后通过硬链接把文件映射到.pnpm目录中,再通过符号链接让包能找到对应的依赖。硬链接让多个项目共享同一份文件数据,符号链接让 Node.js 的模块解析能找到正确的依赖位置。

6.5 动手验证:一个最小 Demo

为了让你更直观地理解,下面做一个最简单的本地实验。

先创建项目结构:

mkdir pnpm-ghost-demo cd pnpm-ghost-demo mkdir -p packages/pkg-a

创建pnpm-workspace.yaml

packages: - 'packages/*'

创建根目录package.json

{ "name": "pnpm-ghost-demo", "private": true, "dependencies": { "pkg-a": "workspace:*" } }

创建packages/pkg-a/package.json

{ "name": "pkg-a", "version": "1.0.0", "main": "index.js", "dependencies": { "lodash": "^4.17.21" } }

创建packages/pkg-a/index.js

const _ = require('lodash'); module.exports = _.camelCase('hello world');

创建根目录index.js

// 根项目没有直接声明 lodash,只有依赖的 pkg-a 内部声明了 lodash const _ = require('lodash'); console.log(_.camelCase('pnpm ghost dependency'));

然后安装依赖:

pnpm install

安装完成后,先运行根目录的index.js

node index.js

你会看到类似这样的报错:

Error: Cannot find module 'lodash'

这是因为根项目的 package.json 中没有声明 lodash,pnpm 也没有把 lodash 提升到根目录的 node_modules 中,所以 Node.js 找不到它。

然后到pkg-a目录中运行:

node packages/pkg-a/index.js

这次可以正常输出:

helloWorld

因为 pkg-a 自己声明了 lodash,pnpm 通过符号链接让 pkg-a 可以访问到它。

这个实验很直观地说明了一件事:pnpm 的依赖隔离不是靠“规则约束”,而是靠“物理上不让你找到”。

6.6 如果你非要把依赖提升到顶层

有些开源项目在迁移到 pnpm 时,可能会因为依赖提升策略不同而出现构建脚本找不到命令的问题。针对这种情况,pnpm 也提供了提升配置,可以在项目根目录创建.npmrc文件:

shamefully-hoist=true

或者更克制地指定某些包提升到顶层:

public-hoist-pattern[]=*eslint* public-hoist-pattern[]=*vitest*

但这里要提醒:提升配置等于重新引入了幽灵依赖的空间,不到万不得已不建议全局开启。更好的方式是修复代码,让每一个被使用的依赖都显式声明。

7. 常见问题与排查思路

pnpm 在安装和使用过程中,有一些频率很高的问题。这里整理成表格,方便快速检索。

问题现象常见原因解决思路
pnpm 无法被识别为 cmdlet 或内部命令pnpm 全局 bin 目录不在 PATH 中执行 pnpm setup,并将全局目录加入 PATH
报错 require at least node.js v22.13pnpm 版本要求更高版本的 Node.js升级 Node.js,或降低 pnpm 版本
pnpm install 速度很慢默认 registry 网络不稳定设置国内镜像后重试
安装过程中提示 pnpm approve-builds某些依赖的 postinstall 脚本默认被 pnpm 阻止执行 pnpm approve-builds 按需允许
迁移到 pnpm 后,原本能运行的代码报 Cannot find module代码中存在幽灵依赖,npm 时代靠扁平化侥幸运行将代码中用到但未声明的包显式加入 package.json
某些开源项目在 pnpm 下构建失败依赖提升策略不同,构建脚本找不到命令检查项目文档,或使用 .npmrc 中的 hoist 配置
重复安装相同依赖但磁盘占用增长多版本依赖并存,或 store 中存在过期缓存使用 pnpm store prune 清理

7.1 关于 pnpm approve-builds 的补充说明

近期的 pnpm 版本出于安全考虑,默认不会执行依赖包里的 postinstall 等构建脚本。如果你安装esbuildnode-sasssqlite3这类需要编译的包,可能会看到提示:

Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.

解决方式有两种。

第一种,执行交互式命令:

pnpm approve-builds

然后按提示选择允许执行的依赖。

第二种,在根目录 package.json 中配置构建白名单:

{ "pnpm": { "onlyBuiltDependencies": [ "esbuild" ] } }

第二种方式更可控,也更容易在团队中统一执行。

7.2 关于从 npm 迁移到 pnpm

如果项目原本使用 npm,并且存在package-lock.json,可以通过 pnpm 自动生成体验更好的锁文件:

pnpm import

这个命令会根据已有的 npm lockfile 生成pnpm-lock.yaml。不过要注意,迁移后最好执行一次pnpm install,并重点检查是否存在幽灵依赖问题。如果之前代码里“偷偷”使用了未声明的包,迁移到 pnpm 后会立刻暴露出来。换个角度想,这其实是帮你排掉了一个隐患。

8. 最佳实践与工程建议

理解了原理之后,再看 pnpm 在 Monorepo 中的工程实践,思路会清晰很多。

8.1 目录结构建议

一个 pnpm Monorepo 常见的目录结构如下:

my-monorepo/ ├── apps/ │ ├── web/ │ └── admin/ ├── packages/ │ ├── ui/ │ ├── utils/ │ └── request/ ├── package.json ├── pnpm-workspace.yaml ├── pnpm-lock.yaml └── .npmrc

建议用apps放应用入口,用packages放公共库和业务模块。这样pnpm --filter指令一眼就能看清楚作用范围。

8.2 根依赖与子包依赖的划分

根目录的package.json尽量只放公共开发依赖,比如:

  • TypeScript
  • ESLint
  • Prettier
  • Vitest
  • Husky

各业务包需要的运行时依赖,放进各自包的package.json。这样每个包的职责边界清晰,也方便单独发布。

8.3 使用 workspace 协议

在 Monorepo 中,子包之间互相引用时,推荐使用workspace:*协议:

{ "dependencies": { "@my-org/ui": "workspace:*" } }

这样 pnpm 会把包直接链接到本地源码,开发时改动即时生效。发布时,pnpm 会根据发布配置将协议转换为实际版本号。

8.4 使用 --filter 做增量操作

在 Monorepo 中,不要总是对整个仓库执行构建。使用pnpm --filter可以精确控制作用范围:

# 只构建 web 应用 pnpm --filter web build # 构建 web,并构建它依赖的所有本地包 pnpm --filter web... build # 向 utils 包添加依赖 pnpm --filter @my-org/utils add lodash

如果希望只运行某个包及其依赖包的脚本,可以这样写:

pnpm --filter web... run build

三个点表示“包含该包的所有依赖链”。

8.5 在 CI 中优化安装效率

CI 中建议固定 lockfile,使用以下命令:

pnpm install --frozen-lockfile

这意味着安装过程严格以pnpm-lock.yaml为准,不会解析新的依赖版本,保证 CI 和本地环境一致。

同时可以缓存 pnpm 的全局 store 目录:

pnpm store path

把输出路径加入 CI 的缓存配置,能显著减少重复下载。

8.6 安全与生产环境注意事项

在生产环境执行任何依赖变更前,建议先确认以下几点:

  • 当前分支代码已经提交,或者已备份现有 lockfile。
  • 删除或升级依赖后,在测试环境完整验证一次构建和部署流程。
  • 保持最小权限原则,CI 中的pnpm publish权限只授予必要的账号。

另外,对于构建后部署的问题,很多前端项目关心pnpm run build之后的产物如何部署到 Nginx。这其实和单仓项目没有本质区别:构建产物一般在对应子包的dist目录,把 Nginx 的root指向该目录即可。如果使用前端路由的 history 模式,还需要配置try_files回退到index.html,避免刷新页面时出现 404。

9. 面试时如何组织回答

最后,把前面所有内容浓缩成一套面试回答思路。

如果面试官问:“项目为什么用 Monorepo?”

你可以回答:

“Monorepo 的核心价值是代码复用和原子提交。多个项目共享公共代码时,可以直接通过 workspace 引用,不需要先发包再升级依赖。一次改动可以跨多个项目同时提交,代码评审的上下文也更完整。同时它可以统一依赖版本、统一构建流程,减少环境差异。当然它也有成本,比如仓库体积变大、权限管理需要额外设计,所以适合多项目有较强共享关系的团队。”

如果面试官接着问:“pnpm 的优势是什么?”

你可以回答:

“站在 Monorepo 的视角,pnpm 有三个核心优势。第一是速度快,它的全局内容寻址 store 会让同一个版本的依赖只存一份,后续安装通过硬链接复用,二次安装几乎是秒级。第二是省磁盘,多项目共享依赖时效果非常明显。第三是依赖隔离,pnpm 不会把传递依赖扁平化提升到顶层 node_modules,所以代码里使用任何没有在 package.json 中声明的包都会直接报 Cannot find module。此外,pnpm 原生支持 workspace 和 --filter,这让 Monorepo 的包管理非常顺手。”

如果面试官继续追问:“那 pnpm 是怎么做到‘没声明就不能用’的?”

这个时候,你已经可以通过前面第六节的原理来回答了:

“pnpm 会把项目的真实依赖放在 node_modules/.pnpm 目录下,每个依赖都有自己的子目录。项目顶层 node_modules 里只有 package.json 中直接声明的依赖符号链接。每个包的内部依赖,会被符号链接到 .pnpm 下对应版本的真实目录中。Node.js 解析模块时会从当前文件目录向上查找 node_modules,而没有被声明、没有被链接到解析路径里的包,根本不在任何可见的 node_modules 路径中,所以 Node.js 永远无法找到它。这就从物理层面实现了‘没声明就不能用’。”

如果面试官还愿意深挖,你还可以补充硬链接和符号链接的区别:符号链接解决依赖解析路径,硬链接解决磁盘空间复用。

这一串回答下来,Monorepo、pnpm、幽灵依赖这条技术线就很完整了。技术概念只有在实践中被亲手验证过,面试时才不会被追问卡住。建议你找个空闲时间,把第六节的 demo 完整跑一遍,体会一下依赖报错从“意外”变成“预期”的感觉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询