Pnpm 前端最佳实践

Tags
Web Dev
Pnpm
Published
November 27, 2021
Author
HJS
Pnpm 是社区近几年最受关注的的包管理工具。它用符号链接组织 node_modules 结构;用硬链接共享依赖包。如果你不明白这两点代表什么,建议先阅读官方文档和实践后回过头来看这篇文章
 

理解 hardlink 和 symlink

  • inode: 存储文件元信息,Unix/Linux 系统内部使用 inode 号码来识别文件
  • hardlink: 是一个链接文件,和源文件指向同一个 inode;因此删除其中一个文件不影响另一个文件的访问,文件内容的修改会同步到所有文件
  • symlink: 软链接(AKA 符号链接),是一个链接文件,指向源文件的地址;因此修改源文件内容,软链接内容也会改变。当删除源文件时,访问软链接会报错 No such file or directory
 

Node 依赖查找规则

  • hardlink: 从硬链接文件位置开始查找以来,与源文件位置无关
  • symlink: 从源文件位置开始查找依赖。用 -preserve-symlinks 可修改查询规则为从软链接文件位置
其它工具也有类似配置,例如WebpacksymlinksTypeScriptpreserveSymlinks 配置等
 

Pnpm 的 package-import-method

  • auto: 默认值,尝试 clone 包,如果不支持则按 hardlink / copy 降级
  • clone: 从 store 中克隆 packages。(AKA 写时复制或引用链接)
  • hardlink: 从 store 中硬链接 packages。缺点是修改 node_modules 中的 packages 会影响 store
  • copy: 从 store 中复制 packages
  • clone-or-copy: 尝试 clone,如果不支持降级为 copy
最理想且性能最好的情况是 clone,但目前写时复制在各个操作系统的兼容性方面仍是一个问题。
 

Pnpm 的 hosit 相关属性

  • hoist: 默认 true,依赖项提升到 node_modules/.pnpm;未列出的依赖项将在 .pnpm/node_modules
    • semi-strict 模式,应用仅能访问到依赖项,但依赖之间能够互相访问;node_modules/.modules.yamlhoistedDependencies 字段可以看到所有被提升的依赖。
  • hoist-pattern: 指示 pnpm 将匹配模式的依赖提升到 node_modules/.pnpm;即放入 .pnpm/node_modules 目录下
  • public-hoist-pattern: 默认为['*eslint*', '*prettier*'],指示 pnpm 将匹配模式的依赖提升到根模块目录
  • shamefully-hoist: 默认为 false,为 true 时相当于 public-hoist-pattern*
 

Pnpm 的 node_modules 配置

  • shamefully-hoist=true: 最松散的模式。类似 npm 将所有依赖提升到根目录;应用和依赖,依赖和依赖都存在幻影依赖
  • hoist=true: semi-strict 模式。应用仅能访问到依赖项,依赖间仍能互相访问;但范围仅限于当前项目,如果在当前项目外仍有 node_modules 应用代码仍能访问幻影依赖(例如 monorepo 架构时根目录存在 node_modules
  • host=false: strict 模式。应用和依赖都无法访问幻影依赖,但范围同样仅限于当前项目。
但因为社区中存在幻影依赖的 Library 非常多,因此 Pnpm 的默认策略是 semi-strict 模式。
 

Pnpm 的 node_modules 设计

资料:
要点:
  • hardlink 指向依赖项,symlink 构建嵌套的依赖关系图结构
  • 合理的结构避免循环 symlink
  • 兼容现有的 Nodenpm 特性:允许包导入自己
  • 包与它们的依赖关系很好地分组
 

Pnpm 如何控制包的版本

  • .pnpmfile.cjs: 通过 .pnpmfile.cjs 中的 hook 直接侵入到 pnpm 的包安装过程
 

Pnpm 中 peerDependencies 不同时的包分身问题

项目中对于同一个包的同一个版本,pnpm 最终都指向同一个 hardlink 文件。但如果:
  • 项目包含 2 次依赖 foo@1.0.0
  • 2 次依赖的 foo 虽然版本一致,但其 peerDependencies 存在差异
Pnpm 则会创建多个依赖集,此时会有两个 foohardlink,构建工具打包时如果没有特殊处理就会打包进多个实例。
Ps. 目前该行为是符合预期的,npm / yarn / pnpm 都是该行为
解决方案:
  • Pnpm 控制包的安装版本或过程
  • 开启 dedupe-peer-dependents
  • 构建工具用 alias 指定包为同一个实例
 

Pnpm 的 .npmrc 配置最佳实践

Peer Dependency Settings
  • dedupe-peer-dependents: 指示 Pnpm 项目依赖中的相同版本的 peer deps 指向同一个实例
  • resolve-peers-from-workspace-root:
    • monorepo 下子项目需要在声明 peer deps 的同时在 dev deps 再写一遍,否则子项目开发时就会缺乏该依赖
    • 假如此时 B 项目依赖该 A 项目,B 项目中就需要安装 Apeer deps,整个 workspace 中就存在两份 Apeer deps
    • 这会造成 2 个小问题:
    • 如果 Apeer deps 版本改了,Bpeer deps 并不随之改变,除非重新 pnpm install
    • 重复写 peer depsdev deps 比较麻烦
    • 解决方案:开启 resolve-peers-from-workspace-root 指示 Pnpmworkspace 中所有项目的 peer deps 安装到根目录 .pnpm 目录下,确保所有项目共享 peer deps,子包依赖能互相访问且版本一致。
      Ps. 造成一定程度的幻影依赖,但瑕不掩瑜