小白笔记-pnpm

performant npm,高性能的 npm,由 npm、yarn 衍生而来,解决了 npm/yarn 内部潜在的 bug,极大的优化了性能。

特点

  • 速度快,安装高效

  • 节省磁盘空间

    当使用 npm 或 yarn 时,如果有 100 个项目使用到某个依赖(dependency),就会有 100 份改依赖的副本保存在硬盘上。使用 pnpm 时,依赖会被存储在内容可寻址的存储中(the denpendency wil be sotred in a content-addressable store),所有

    • 用到某个依赖的不同版本,只会将不同版本有差异的文件添加到仓库
    • 所有的文件都会存储在硬盘的某一个位置。当软件包被安装时,包里的文件会硬链接到该位置,不会再占用额外的磁盘空间。可允许跨项目的共享同一版本的依赖
  • 优化了依赖的 node_modules 结构,创建非扁平化的 node_modules 文件夹

    • 使用 npm 或 yarn 安装依赖项时,所有的包都被提升到模块目录的根目录,因此,项目可以访问到未被添加进当前项目的依赖
    • 默认情况下,pnpm 使用软链的方式将项目的直接依赖添加进模块文件夹的根目录
  • 内置了对 Monorepo 的支持,无需在引入 lerna

npm、yarn 面临的问题

Npm 安装包的过程

  • 先检查 .npmrc文件,包含以下 npm 配置信息,如 registry,全局缓存目录等
  • 读取 package.json 文件中的依赖信息,根据semver语义化版本信息生成完整的版本依赖树
  • 先查询本地缓存目录,如果有缓存就直接使用,如果不存在,再去 npm 仓库下载到的缓存目录,此过程,会校验包的哈希,以保证安全性

上述过程中依赖树可能存在大量重复的模块,因为它按依赖树的结构进行安装,比如 A,B 模块都依赖了 C 模块,那么 C 模块会在 A、B 模块的 node_modules 目录内重复安装,造成大量的重复和冗余。

image-20220622162106602

npm3 进行了优化,加入了dedupe模块扁平化,尽可能的将所有依赖都发到最顶层node_modules 目录下,如果有重复的模块,且版本相互兼容,就会只保留一个,丢弃其它的。如果版本不兼容,那么只有一个被提升到顶层,其它的会放在其父依赖的 node_modules 目录下,而哪一个被提升到顶层可能不固定,所有在 npm5+版本中新增了package-lock.json用于锁定依赖结构,确保每一次安装出来的目录结构和依赖版本相同。

image-20220622162714637

phantom dependencies (幽灵依赖)

由于扁平化的处理方式, 用户可以引用 package.json 中没有声明的依赖。 比如项目 1 使用 依赖 A,其中 A 有一个 A1 包,由于扁平化 A1 包会别提升到项目 node_modules 下,此时在项目 1 中可直接使用 A1 包,将来 A 升级或不在使用 A1 包,那么项目 1 就会出现异常或报错

npm dopplelgangers (npm 包分身)

npm 扁平化的处理机制,可能导致心魔中应用多个版本的包,从而导致

  • 需要安装多个版本包
  • 打包出来的文件可能包含多个包
  • 如果该包需要用到单例,会出现异常

pnpm 原理

pnpm 安装依赖后 node_modules 目录大概如下:

image-20220622165200478

其中 node_modules 包含 .bin,.pnpm目录以及其它的 npm 包,这些 npm 包与 package.json 中声明的保持一致,因此只有 package.json 中声明过的依赖才能在项目中使用,从而避免了幽灵依赖的问题。

不同于 npm,这些 npm 包都是 symbolic link 符号链接,指向了.pnpm 目录下的包

.pnpm 下的包名规则是

1
2
3
<organization-name> + <package-name>@<version>/node_modules/<name>
组织名+包名@版本号/node_modules/名称(项目名)
typescript@4.7.4/node_modules/typescript

image-20220622170327166

因为.pnpm 里面的包路径中加入了版本号信息,因此可以在同一目录下相对扁平化的存储所有的包

由于每个包又有自己的依赖,为了迎合 node.js 的包查找规则,因此上面的包命名规则中有添加了一层 node_modules 目录,本包的依赖也会在这个目录 内创建 Symbolic link 符号链接,链接到.pnmp 顶层上的包,通过此法,使得每一个包都能正常的使用自己的依赖,但不会污染到顶层的 node_modules,从而避免幽灵依赖的问题

image-20220622171758946

注: node_modules 目录下还存在一部分没有在 package.json 中声明的依赖,比如@jest/type,@vitejs,这是因为一部分包需要在顶层才能使用,比如 eslint、typescript 声明,prettier 插件等,pnpm 会默认将包名含 types,eslint、prettier 等关键词的包提升到顶层。可通过.npmrc中设置public-hoist-pattern[]=来关闭这些提升

在 linux 文件系统中,保存在磁盘分区中文件都会分配一个编号,称为索引编号(inode index),文件名和 inode 通常是一一对应的,且允许多个文件名指向同一个 inode, 删除任何一个文件名并不会对 inode 或其它文件有影响。只有当最后一个硬链接删除后才回收 inode 编号并标记对应的 block 为可用,等待其它数据存储后抹去其内容。如此,同一个文件可以有多个文件路径和文件名,但在磁盘中仅仅只有一份内容,避免了重复占用。

项目的 node_modules 目录下的所有文件都是通过硬链接的方式链接到全局 pnpm store 内的文件,以.pnpm 目录下的 vue 为例, 查看 README.md 文件的 inode 编号:64817529

image-20220622185716656

进入 pnpm 全局 stroe 路径,查找 inode 编号

1
2
Users/liuzhenjiang948/.pnpm-store  // 全局目录
find . -inum 64817529 // 查找对应 inode

image-20220622190031040

所有 npm 包里面的所有文件都会在全局进行存储,存储是根据文件的哈希信息进行散列,这样可以扁平化的存储,而不用按原始的 npm 包目录结构进行存储,节省大量磁盘空间。

pnpm install 安装包的大致过程

  • 判断是否有 pnpm-lock.yaml文件,
    • 如果没有,则根据package.json中声明的版本计算依赖树以及各版本 npm 包的 integrity 值
    • 如果有 且版本跟 package.json 中声明的匹配,就根据pnpm-lock.yaml中各个依赖包的 integrity 信息,并计算对应的 -index.json 文件的完整哈希和路径
    • 如果 stroe 里面有对应的包的-index.json 文件,即有该包的缓存
    • 如果没有的话需联网下载对应的 tar 包,并生成对应的-index.json 文件,并且将包内的文件计算 integrityz 值和哈希
    • 对整个依赖树进行完以上操作后再项目内的 node_modules 目录创建个依赖的符号链接和文件的硬链接完成安装

CLI 命令

  • pnpm add , -D 安装到 devDependencies, -O 安装到 optionalDependencies

  • pnpm install (i), 安装项目所有依赖

    1
    2
    3
    Packages are copied from the content-addressable store to the virtual store.
    Content-addressable store is at: /Users/liuzhenjiang948/Library/pnpm/store/v3
    Virtual store is at: node_modules/.pnpm
    配置项 默认值/类型 说明
    –offline false 为 true,仅使用在store中已有的包,本地找不到,安装失败
    –ignore-scripts false 不执行任何项目中package.json和它依赖项中定义的任何脚本
    –ockfile-only fasle 只更新pnpm-lock.yamlpackage.json,不写入node_modules目录
    –fix-lockfile 自动修复损坏的 lock 文件入口
    —reporter= default, silent, append-only, ndjson silent: 除致命 errors,不输出记录信息
    ndjson: 最详细记录信息
  • pnpm update (up), 更新软件包的最新版本

    命令,配置项 说明
    pnpm up package.json指定的范围更新所有的依赖项
    pnpm up –latest 更新所有依赖项,忽略package.json指定的范围
    pnpm up –recursive 递归更新子目录中的依赖包
    pnpm up –global 更新全局安装的依赖包
  • pnpm remove (rm,un,uninstall), 删除指定的包

  • pnpm link (ln), 使当前本地包 可在系统范围内 或 其他位置 访问

    在项目开发时,需要将一些公用的代码抽离发布成 npm 包,作为项目的依赖去安装使用。但在开发调试中需要频繁的打包发布,再安装依赖,很不方便。为解决此问题,可以使用 link 命令将模块链接到项目中。

    • 假设 项目名 project-jiang,和一个公用组件模块 common,现在需要在项目中使用 common,且 common 是作为项目的 npm 包依赖。
    • 在 common 目录下使用 pnpm link ,将 common 模块创建成本地依赖包
    • 在 project-jiang 项目中,使用 pnpm link common 和本地 common 模块建立链接。此时该项目中的 node_modules 里就会添加一个 common 模块的软连接
  • pnpm unlink, 取消链接一个系统访问的 package

  • pnpm import, 从另一个软件包管理器的 lock 文件生产 pnpm-lock.yaml, 支持的源文件 package-lock.json,yarn.lock,npm-shrinkwrap.json

查看依赖

  • pnpm audit, 检查已安装包的已知安全问题,如果发现问题,尝试使用pnpm updatepnpm audit --fix
  • pnpm list, 以树形结构输出所有的已安装package的版本及其依赖
  • pnpm outdated, 检查过期的 packages

运行脚本

  • pnpm run , 运行一个在 package 文件定义的脚本

  • pnpm test, 运行在 package scripts 对象中test 属性指定的任意的命令

  • pnpm exec, 在项目访问内执行 shell

  • pnpm dlx, 从源中获取包而不将其安装为依赖,热加载,并运行它公开的任何默认命令的二级制文件

    例如 pnpm dlx create-react-app my-app, 使用 create-react-app来初始化一个 react 应用

  • pnpm create, 从create-*@foo/create-*启动套件创建项目, 例如pnpm create react-app my-app

管理 Node 环境

pnpm env

  • 安装并使用指定版本 node.js

    pnpm env use --global lts, pnpm env use --global 16, pnpm env use --global latest