ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

精读 pnpm:高性能 Node 包管理器的三层寻址、软硬链接与幻影依赖治理

精读 pnpm:高性能 Node 包管理器的三层寻址、软硬链接与幻影依赖治理 文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载pnpmPerformant NPM通过软硬链接与全新的依赖组织方式将 Node 包管理的安装速度、磁盘占用与依赖规范化做到了极致它既遵循node_modules就近寻址的默认规则又用「三层寻址」同时解决了重复文件安装与幻影依赖两大历史顽疾。本文以精读周刊第 253 期前沿技术/253.精读《pnpm》.md为主体系统拆解 pnpm 的架构设计、peer-dependencies 安装规则、软硬链接的操作系统原理与 pnpm-store 的内容寻址机制读完后你将能解释清楚「为什么是三层而不是两层或四层」并能在自己的项目中评估与落地 pnpm。pnpm 是什么从 npm 一键迁移的高性能包管理器pnpm 全称是Performant NPM即「高性能的 npm」。它结合软硬链接与新的依赖组织方式大大提升了包管理的效率也同时解决了「幻影依赖」的问题让包管理更加规范减少潜在风险发生的可能性。使用 pnpm 非常容易可以直接用 npm 安装npm i pnpm -g安装完成后便可用pnpm代替npm命令了。比如最重要的安装包步骤可以使用pnpm i代替npm ipnpm i这样就算把 pnpm 使用起来了。pnpm i是pnpm install的简写执行后会在项目下生成树状结构的node_modules并在其中创建.pnpm目录统一管理每个版本的包——这正是下文要展开的核心设计。以本仓库前端精读周刊为例仓库根目录的 package.json 中声明了esm、husky、lint-md-cli三个运行时依赖并配置了 husky 的pre-commit钩子执行npx lint-md ./。若用 pnpm 安装该仓库其node_modules顶层只会出现这三个被直接声明的包任何未被package.json声明的传递依赖都无法在顶层被寻址到——这就是 pnpm 与 npm 打平式node_modules最直观的差异。pnpm 的三大优势快、准、狠pnpm 的优势可以用一个比较好记的词概括——「快、准、狠」快安装速度快。得益于全局内容寻址存储与硬链接复用无需在每次安装时重复下载与解压相同的内容。准安装过的依赖会准确复用缓存甚至包版本升级带来的变化都只 diff绝不浪费一点空间逻辑上也严丝合缝。这一点的底层支撑是内容寻址content-addressable存储详见后文。狠直接废掉了幻影依赖在逻辑合理性与含糊的便捷性之间毫不留情地选择了逻辑合理性。npm 打平结构带来的「顺带可用」的方便被彻底取消未声明即不可用。而带来这些优势的点子核心全在于一张三层寻址的架构图。所有设计都围绕「依赖文件如何被找到」展开所有 npm 包都安装在全局目录~/.pnpm-store/v3/files下同一版本的包仅存储一份内容甚至不同版本的包也仅存储 diff 内容。每个项目的node_modules下有.pnpm目录以打平结构管理每个版本包的源码内容以硬链接方式指向 pnpm-store 中的文件地址。每个项目node_modules下安装的包结构为树状符合 node 就近查找规则以软链接方式将内容指向node_modules/.pnpm中的包。所以每个包的寻找都要经过三层结构node_modules/package-a → 软链接 node_modules/.pnpm/package-a1.0.0/node_modules/package-a → 硬链接 ~/.pnpm-store/v3/files/00/xxxxxx经过这三层寻址带来了什么好处为什么是三层而不是两层或者四层下面逐层拆解。依赖文件三层寻址pnpm 的核心设计骨架第一层还原 package.json 的语义沿用 npm2 的设计第一层寻找依赖是由nodejs或webpack等运行环境/打包工具执行的。它们在node_modules文件夹中寻找依赖并遵循就近原则Node 的模块解析会从当前目录逐级向上查找node_modules。因此第一层依赖文件势必要写在node_modules/package-a下这一层设计有两个目的遵循依赖寻找路径没有将依赖都拎到上级目录也没有将依赖打平目的就是还原最语义化的package.json定义——即定义了什么包就能依赖什么包反之则不行。子依赖就近解析每个包的子依赖也从该包内寻找解决了多版本管理的问题。同时这也使node_modules拥有一个稳定的结构该目录的组织算法仅与package.json定义有关而与包安装顺序无关。这一点非常关键——npm 打平结构下包的最终层级往往取决于安装顺序而 pnpm 的第一层彻底消除了这种不确定性。如果止步于此这就是npm2.x的包管理方案。正因npm2.x的包管理方案最没有歧义每个包的依赖都被嵌套在自己的目录里绝不外泄第一层沿用了该方案的设计。第二层软链接解决项目内代码复用从第二层开始要解决npm2.x设计带来的主要问题——包复用。npm2.x的嵌套结构会让同一个包在不同层级被重复安装多份浪费大量磁盘空间。第二层的寻址路径是node_modules/package-a → 软链接 node_modules/.pnpm/package-a1.0.0/node_modules/package-a这一层利用软链接解决了代码重复引用的问题node_modules下的package-a只是一个轻量的链接文件真正的内容统一放在.pnpm目录下按版本打平管理。对比npm3将包打平的设计把传递依赖全部提升到顶层node_modules软链接方案有两个优势保持包结构的稳定.pnpm下每个版本的包独立成目录版本互不干扰结构只与package.json有关用文件指针解决重复占用硬盘空间的问题链接本身几乎不占空间同一版本内容只存一份。若止步于此也已经解决了一个项目内的包管理问题。但项目不止一个多个项目对于同一个包的多份拷贝还是太浪费因此要进行第三步映射。第三层硬链接指向全局统一存储第三层映射是node_modules/.pnpm/package-a1.0.0/node_modules/package-a → 硬链接 ~/.pnpm-store/v3/files/00/xxxxxx这一步已经脱离当前项目路径指向一个全局统一管理路径这正是跨项目复用的必然选择。所有项目的.pnpm目录内容都通过硬链接指向同一个全局存储~/.pnpm-store不同项目安装同一个包时磁盘上只有一份真实数据。然而 pnpm 更进一步没有将包的源码直接存储在 pnpm-store而是将其拆分为一个个文件块block并以内容哈希命名。这一设计的收益会在「pnpm-store 的内容寻址」一节详细展开。为什么恰好是三层把三层放在一起看每一层都有明确且不可替代的职责层级路径技术解决的核心问题第一层node_modules/package-a真实目录树状遵循 Node 就近寻址规则还原package.json语义第二层node_modules/.pnpm/package-a1.0.0/node_modules/package-a软链接项目内代码复用避免多版本重复安装第三层~/.pnpm-store/v3/files/00/xxxxxx硬链接跨项目复用全局只存一份内容两层不够只保留第一层就是npm2重复安装问题依旧只保留前两层只能解决单项目内的复用跨项目的多份拷贝依然浪费。四层则没必要全局存储之上再加一层映射只会增加寻址复杂度没有任何收益。所以三层是「满足寻址规则 复用 跨项目共享」的最小完备设计。幻影依赖pnpm 如何从根源上解决它幻影依赖phantom dependency / ghost dependency是指项目代码引用的某个包没有直接定义在package.json中而是作为子依赖被某个包顺带安装了。代码里依赖幻影依赖的最大隐患是对包的语义化控制不能穿透到其子包。也就是说包apatch级别的改动可能意味着其子依赖包bmajor级别的 Break Change——a的维护者没有义务保证b的 API 稳定而你却直接import了b等于把版本兼容性风险建立在一个不受自己控制的依赖之上。为什么 npm 时代幻影依赖屡见不鲜因为npm3的打平结构会把所有传递依赖提升到顶层node_modules代码里可以随意引用任何被顺带装进来的包。而正因为 pnpm 三层寻址的设计使得第一层可以仅包含package.json定义的包顶层node_modules中只存在直接声明的依赖通过软链接指向.pnpm未定义在package.json中的包根本不会出现在第一层寻址范围内因此node_modules不可能寻址到未定义在package.json中的包自然就解决了幻影依赖的问题。但还有一种更难以解决的幻影依赖问题用户在 Monorepo 项目根目录安装了某个包这个包可能被某个子 Package 内的代码寻址到。因为 Node 的就近寻址规则会一路向上查找子 Package 内的代码完全可能「顺路」命中根目录node_modules里的包而这些包并不在子 Package 的package.json中。要彻底解决这个问题需要配合使用Rush等 Monorepo 管理工具在工程上通过依赖问题检测来彻底解决——这正对应精读周刊中对 Monorepo 治理的讨论可参见 前沿技术/102.精读《Monorepo 的优势》.md 中的背景。peer-dependencies 的严格安装规则不同环境多份拷贝pnpm 对peer-dependencies有一套严格的安装规则。对于定义了peer-dependencies的包来说意味着它对peer-dependencies内容是敏感的潜台词是对于不同的peer-dependencies这个包可能拥有不同的表现。因此 pnpm 针对不同的peer-dependencies环境可能对同一个包创建多份拷贝。举例说明引用 pnpm 官方文档中的例子包bar的peer-dependencies依赖了baz^1.0.0与foo^1.0.0。在 Monorepo 环境中两个 Package 分别安装不同版本的包- foo-parent-1 - bar1.0.0 - baz1.0.0 - foo1.0.0 - foo-parent-2 - bar1.0.0 - baz1.1.0 - foo1.0.0安装结果如下node_modules └── .pnpm ├── foo1.0.0_bar1.0.0baz1.0.0 │ └── node_modules │ ├── foo │ ├── bar - ../../bar1.0.0/node_modules/bar │ ├── baz - ../../baz1.0.0/node_modules/baz │ ├── qux - ../../qux1.0.0/node_modules/qux │ └── plugh - ../../plugh1.0.0/node_modules/plugh ├── foo1.0.0_bar1.0.0baz1.1.0 │ └── node_modules │ ├── foo │ ├── bar - ../../bar1.0.0/node_modules/bar │ ├── baz - ../../baz1.1.0/node_modules/baz │ ├── qux - ../../qux1.0.0/node_modules/qux │ └── plugh - ../../plugh1.0.0/node_modules/plugh ├── bar1.0.0 ├── baz1.0.0 ├── baz1.1.0 ├── qux1.0.0 ├── plugh1.0.0可以看到这里安装了两个相同版本的foo虽然内容完全一样但却分别拥有不同的名称foo1.0.0_bar1.0.0baz1.0.0与foo1.0.0_bar1.0.0baz1.1.0。命名中后缀_bar1.0.0baz1.0.0记录的就是 peer-dependencies 环境的差异。这也是 pnpm 规则严格的体现任何包都不应该有全局副作用或者必须考虑好单例实现否则可能会被 pnpm 装多次。比如依赖了react的 UI 组件库如果项目里存在两套不同的reactpeer 环境或版本产生分化该组件库就可能被安装为多个实例——这也为下文总结部分提到的 Monorepo 版本一致性治理埋下了伏笔。硬链接与软链接的操作系统原理要理解 pnpm 软硬链接的设计首先要复习一下操作系统文件子系统对软硬链接的实现。这两者是第二层、第三层寻址的技术底座。硬链接多个文件名指向同一个 inode硬链接通过ln命令创建ln ./my.txt ./hard.txt这样创建出来的hard.txt与my.txt都指向同一个文件存储地址因此无论修改哪个文件都因为直接修改了原始地址的内容导致这两个文件内容同时变化。进一步说通过硬链接创建的 N 个文件都是等效的。通过ls -li ./查看文件属性时可以看到两个文件拥有相同的 inode 索引$ ls -li ./ 84976912 -rw-r--r-- 2 author staff 489 Jun 9 15:41 my.txt 84976912 -rw-r--r-- 2 author staff 489 Jun 9 15:41 hard.txt其中第三个参数2表示该文件指向的存储地址有两个硬链接引用inode 引用计数为 2。硬链接如果指向目录就麻烦多了第一个问题是这样会导致文件的父目录有歧义同时还要将所有子文件都创建硬链接实现复杂度较高因此 Linux 并没有提供这种能力——这也是 pnpm 为什么不能用硬链接直接覆盖「目录」层级的原因之一。软链接指向路径的指针软链接通过ln -s创建ln -s ./my.txt ./soft.txt软链接可以认为是指向文件地址指针的指针它本身拥有一个新的 inode 索引但文件内容仅包含指向的文件路径。用ls -li查看会看到箭头标记$ ls -li ./ 84976913 -rw-r--r-- 2 author staff 489 Jun 9 15:41 soft.txt - my.txt软链接与硬链接的关键差异源文件被删除时软链接会失效变成悬空链接硬链接不会失效因为硬链接直接共享数据块作用对象软链接可以对文件夹生效硬链接不行占用空间软链接本身是一个小文件占用极少空间。因此 pnpm 虽然采用了软硬结合的方式实现代码复用但软链接本身也几乎不会占用多少额外的存储空间硬链接模式更是零额外内存空间占用。所以对于相同的包pnpm 额外占用的存储空间可以约等于零。软硬链接在 pnpm 中的分工小结链接类型用于哪一层作用软链接第二层node_modules/package-a→.pnpm/.../package-a以指针方式组织树状结构可指向「目录」保持结构稳定硬链接第三层.pnpm/.../package-a→~/.pnpm-store/v3/files/...零额外空间的内容共享实现跨项目复用pnpm-store基于内容寻址的全局存储第三层寻址时 pnpm 采用了硬链接但硬链接的目标文件并不是普通的 NPM 包源码而是一个哈希文件。这种文件组织方式叫做content-addressable基于内容的寻址。简单来说基于内容的寻址比基于文件名寻址的好处是即便包版本升级了也仅需存储改动 Diff而不需要存储新版本的完整文件内容在版本管理上进一步节约了存储空间。pnpm-store 的组织方式大概是这样的~/.pnpm-store - v3 - files - 00 - e4e13870602ad2922bfc7.. - e99f6ffa679b846dfcbb1.. .. - 01 .. - .. .. - ff ..也就是采用文件内容寻址而非文件位置寻址的存储方式文件路径由内容哈希决定00到ff是哈希前缀分桶哈希值相同的文件必然内容相同。之所以能采用这种存储方式是因为一个关键的行业事实NPM 包一经发布内容就不会再改变。内容不变内容寻址就是绝对可靠的——同样的哈希必然对应同样的内容不会产生歧义因此适合内容寻址这种内容固定的场景。同时内容寻址也忽略了包的结构关系当一个新包下载下来解压后遇到相同文件 Hash 值时就可以直接抛弃仅存储 Hash 值不存在的文件。这样就自然实现了文章开头说的能力pnpm 对于同一个包的不同版本也仅存储其增量改动相同内容的文件块被全局去重。总结三层寻址的收益与 Monorepo 中的注意事项pnpm 通过三层寻址同时达成了三件事贴合node_modules默认寻址方式第一层的树状结构完全符合 Node 的就近查找规则运行环境与打包工具无需任何改动解决重复文件安装的问题第二层软链接 第三层硬链接从项目内到跨项目每个文件内容在磁盘上只保留一份顺便解决幻影依赖问题第一层只暴露package.json声明的包未声明即不可寻址。在包管理的「安装速度、磁盘占用、依赖规范性」三个维度上pnpm 的设计逻辑严丝合缝堪称包管理领域的里程碑式创新。但其苛刻的包管理逻辑也带来一个需要正视的边界单独使用 pnpm 管理大型 Monorepo 时容易遇到一些「符合逻辑但又觉得别扭」的地方。比如如果每个 Package 对于同一个包的引用版本产生了分化可能会导致 Peer Deps 了这些包的包产生多份实例对应上文 peer-dependencies 严格规则一节中的现象而这些包版本的分化可能是不小心导致的。此时我们可能需要使用Rush等 Monorepo 管理工具来保证版本的一致性在工程层面通过统一版本策略与依赖检测把「逻辑上允许」的分化收敛为「工程上受控」的单一版本。本文在 weekly 仓库中的位置本文源自前端精读周刊「前沿技术」系列的第 253 期完整原文见 前沿技术/253.精读《pnpm》.md。周刊的全部文章索引维护在仓库根目录的 readme.md其中的目录结构由发布辅助脚本 helper.js 自动生成按目录读取、按编号排序、输出 Markdown 链接列表。仓库自身的依赖声明与 husky 校验配置见 package.json读者可以用pnpm i复现本文描述的组织方式后再对照node_modules实际结构理解三层寻址的每一层。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐pnpm 技术指南基于内容寻址存储的高效 Node.js 包管理器原理与实践pnpm 技术指南基于内容寻址存储的高效 Node.js 包管理器原理与实践 本篇技术指南以当前仓库 pnpm11/pnpm/README.md 为核心系统包管理器开发工具CLIHuLa依赖管理pnpm workspace多包管理策略HuLa依赖管理pnpm workspace多包管理策略 痛点跨平台即时通讯应用的依赖管理挑战 开发跨平台即时通讯桌面应用时你可能会遇到这些困扰 多个平即时通讯桌面应用前端pnpm 依赖树构建器 pnpm/deps.inspection.tree-builder 深度解析为符号链接 node_modules 生成依赖层级pnpm 依赖树构建器 pnpm/deps.inspection.tree builder 深度解析为符号链接 node_modules 生成依赖层级 p包管理器开发工具CLI上一篇多厂商 AI 水印情报与去标记策略映射watermarks-remover vendor-notes 深度解读下一篇如何高效备份微信聊天记录专业的PyWxDump完整解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表