npm 和 pnpm 的一些理解

npmnode_modules 目录结构分析

  1. 安装直接依赖(你通过 npm i 安装的包):

    • 当你运行 npm i A 时,A总是 会作为一级依赖被安装到 node_modules根目录下。这是没有疑问的。
  2. 安装间接依赖(A 所依赖的 C 包): C 包最终安装在哪里,取决于当前 node_modules 根目录的状态:

    • 情况 1:根目录 node_modules 下没有 C

      • 那么 npm 会把 C 提升 (hoist) 安装到根目录下的 node_modules 里(即 node_modules/C)。
      • 结果: C 存在于根目录。
    • 情况 2:根目录 node_modules 下已经存在一个 C 包(可能是其他直接或间接依赖已安装的)

      • npm 会检查已存在的 C 的版本是否满足 AC 的版本要求(查看 Apackage.jsondependenciesC 的版本范围)。

        • 如果满足(兼容):npm 就不会再为 A 单独安装 CA 将直接使用根目录下已存在的这个 C包。

          • 结果: C 只存在于根目录(一份),A 的文件夹下不会 创建 node_modules/C
        • 如果不满足(不兼容):npm 就会在 A 自己的文件夹下创建一个 node_modules 子目录,并将 A 所需的特定版本的 C 安装在那里(即 node_modules/A/node_modules/C)。

          • 结果C 有两份(或不限于两份):

            • 一份是根目录下的版本(其他包在用)。
            • 另一份是 A 专有的、特定版本的 C(位于 A 的本地 node_modules 里)。
  3. 在项目代码中 import/require C

    • 当你在项目根目录的源代码(例如 src/index.js)中写 const c = require('C')import c from 'C' 时:

      • Node.js 的模块解析器会首先 在项目根目录的 node_modules(即 node_modules 根目录)中查找 C

      • 如果根目录 node_modules 下有 C

        • 无论这个 C 是直接安装的还是被提升上来的,你的代码都会加载到它。
      • 如果根目录 node_modules 下没有 C(比如因为版本冲突,所有 C 都只存在于某个包的嵌套 node_modules 里):

        • 你的代码将找不到 C(除非你有其他机制)。这就是为什么你不能在项目代码中直接使用那些因为版本冲突而被嵌套安装的包。
    • A 包的代码中 import/require C

      • 如果 A 自己文件夹下有 node_modules/C(因为版本冲突),那么 A 内部的代码加载到的就是它自己专有的 C
      • 如果 A 自己没有 node_modules/C(因为根目录的 C 兼容可用),那么 A 内部的代码加载到的就是根目录的 C
    • 注意 :这里其实有个 bug,因为如果你项目里没有安装 C 包,安装了 A 包, A 包依赖 C 包,此时 C 包会被提升到项目根目录的 node_modules,然后你在项目里导入 C 包没有问题,但是如果你哪天把 A 包删掉了, C 包也会跟着删掉,就出现了幽灵依赖的问题,项目里导入 C 包直接报错。


pnpm 核心机制梳理

📂 文件结构

perl 复制代码
node_modules/
  ├── .pnpm/                  # 所有依赖的「唯一真实存储」
  │   ├── esbuild@0.25.8/     # 包文件夹
  │   ├── vite@7.0.6/         # 包文件夹
  │   │   └── node_modules/   # 隔离层:仅存自身 + 其直接依赖的软链接
  │   │       ├── esbuild     # vite 的直接依赖(软链接指向 .pnpm/esbuild@0.25.8 硬链接)
  │   │       └── vite        # 自身(硬链接)
  │   └── node_modules/       # 内部 Hoist 区:存放共享包(软链接)
  └── vite                    # 软链接指向 .pnpm/vite@7.0.6/node_modules/vite 硬链接

ヽ(ー_ー)ノ pnpm 官方原理图


🔗 核心机制

  1. 硬链接存储

    → 所有依赖包物理文件​​只存一份​ ​在 .pnpm store,通过硬链接复用,节省磁盘。

  2. 软链接隔离

    → 项目直接依赖在根 node_modules软链接 指向 .pnpm 中的真实文件,不是项目的直接依赖不会放在根 node_modules 里面。

  3. .pnpm 文件的结构

    → 每个依赖包的 node_modules/.pnpm/包@版本/node_modules),只包含自身硬链接和其直接依赖的软链接。

    → 直接依赖和间接依赖都会平铺在 .pnpm 文件根目录。

  4. 幽灵依赖防御

    → 项目代码 require(X) 只能访问​​项目显式依赖​ ​(根 node_modules 里的软链接)。

    → 无法访问任何间接依赖!✅


🧩 Hoist 机制(兼容第三方包问题)

  • 内部共享区.pnpm/node_modules → 存放被多个包依赖的公共包的软链接。
  • 作用 :当包 A 未声明依赖 C 时,内部代码 require('C') 可向上回溯到此区找到 C

pnpm 如何区分直接/间接依赖

从图中左侧的 package.json 内容可看出判断逻辑:

go 复制代码
项目根的 package.json → 
  "dependencies": { 
      "bar": "^1.0.0"   // 👉 直接依赖
  }
​
bar/package.json → 
  "dependencies": { 
      "foo": "^1.0.0"   // 👉 对项目来说是间接依赖
  }
pnpm 的具体操作:
  1. 递归解析依赖树 读取项目根 package.json → 找到直接依赖 bar → 再解析 barpackage.json 找到 foo

  2. 标记依赖类型

    • 直接依赖 = 项目 package.json 中显式声明的包(如图中的 bar
    • 间接依赖 = 由直接依赖引入的包(如图中的 foo,是 bar 的依赖)

那这其实就可以说明,为什么项目根里的 node_modules 只存放直接依赖了。


全局安装机制(npmpnpm

1. 全局安装的存储位置:确实有「全局 node_modules」

无论是 npm 还是 pnpm,全局安装的包都会被存储在一个特定的「全局目录」中,但具体路径不同。

  • npm :可通过 npm config get prefix 查看「全局目录」路径。
  • pnpm :可通过 pnpm config get global-dir 查看「全局目录」路径。

简单说:全局安装的包确实放在了全局相关目录中 ,但这个目录是工具统一管理的,和项目内的 node_modules 是完全分离的。

2. 全局安装的核心作用:注册「全局命令」,而非供项目引入

全局安装的核心目的是让包提供的「命令行工具」能在系统任何位置被直接调用,而不是让项目代码直接引入使用。

举个例子:

  • 安装 npm install -g @vue/cli 后,你可以在任何目录下运行 vue create my-project 命令(这是全局命令的作用)。
  • 但如果你的项目代码里写 import VueCli from '@vue/cli' 会报错 ------ 因为 Node.js 在解析依赖时,只会查找项目本地的 node_modules(和 Node 核心模块),不会去扫描全局目录。

3. 为什么项目里不能直接引入全局包?

Node.js 的模块查找机制是「局部优先」的:当你在代码中写 require('xxx')import 'xxx' 时,它只会按以下顺序查找:

  1. 项目当前目录的 node_modules
  2. 上级目录的 node_modules(递归直到根目录)
  3. Node.js 内置的核心模块(如 fspath

全局安装的包不在这个查找链中,所以即使全局装了,项目里不本地安装就无法引入。


pnpm 的目录配置与多磁盘设计

三个关键目录配置

  • storeDir: 全局存储目录,存放所有包的实际文件
  • virtualStoreDir: 项目虚拟存储目录,通常在 node_modules/.pnpm
  • virtualStoreDirMaxLength: 60:限制虚拟存储目录中单个依赖路径最大长度为 60 字符。若路径超长(如 73 字符的依赖路径),pnpm 会自动缩短处理

多磁盘设计原理

1. 硬链接限制
  • 硬链接只能在同一文件系统(磁盘)内创建
  • 跨磁盘无法使用硬链接
2. 每磁盘都会有一个单独的 store
  • C盘项目使用C盘存储目录(硬链接)
  • D盘项目使用D盘存储目录(硬链接)
  • 实现最优性能和空间效率

全局强制统一 store 的后果

1. 跨磁盘项目降级
  • 无法使用硬链接
  • 自动降级为文件复制
2. 性能影响
  • ✅ 不会出错,正常工作
  • ❌ 安装速度变慢
  • ❌ 占用更多磁盘空间
  • ❌ 无法享受硬链接的高效共享
相关推荐
李明卫杭州5 分钟前
正则表达式前瞻操作符详解
前端·javascript
TTATTC9 分钟前
Linux网络流量带宽问题排查
前端
用户3406335587710 分钟前
Fork工作流
前端
掘金一周14 分钟前
只有 7 KB!前端圈疯传的 Vue3 转场动效神库!效果炸裂! | 掘金一周 8.7
前端·后端·ai编程
YGY_Webgis糕手之路19 分钟前
OpenLayers 综合案例-切片坐标与经纬网调试
前端·gis
白白白鲤鱼21 分钟前
Vue2项目—基于路由守卫实现钉钉小程序动态更新标题
服务器·前端·spring boot·后端·职场和发展·小程序·钉钉
xianxin_36 分钟前
HTML5 地理定位
前端
Running_C43 分钟前
Vue组件化开发:从基础到实践的全面解析
前端·vue.js·面试
Clain44 分钟前
如何搭建一台属于自己的服务器并部署网站,超详细小白教程
linux·运维·前端
胡清波1 小时前
小程序中使用字体图标的最佳实践
前端