背景如果你一直从事前端工作和开发,相信你对npm、yarn等工具已经很熟悉了。作为包管理工具,npm有着悠久的历史[1]。一个项目初始化过程需要通过npminstall命令将相关依赖安装到node_modules目录下。对于较大的前端项目,node_modules的大小往往超出我们的想象。而npm在版本更新迭代的过程中一直在优化这个问题。下面简单说一下npm的发展历程。npmdevelopmentnpm2在npm2的开发阶段,依赖的安装比较简单。它会根据配置文件package.json中的dependencies直接下载相关的依赖包,依赖包的组织形式是树状结构。.由于不同包的依赖版本差异很大,依赖也比较复杂,npm2直接根据配置下载和组织依赖是一种简单明了的方式,保证了各个依赖的独立性。当依赖发生变化时,互不影响,关系可以用下图来描述:从上图中我们可以看出A、B、C包是相互独立的。A、B、C包可能依赖同一个包,比如D@1.0A,B、C包可能有很深的依赖级别,比如C包,其中2和3的负面影响会随着项目的复杂性,这可能会导致几个问题:更大的冗余。同一个依赖包D@1.0下载多次不能共享。更深的依赖树。依赖太多破坏了Windows文件系统[2]最大路径长度限制[3]为什么Windows有260个字符的路径长度限制?[4]node_modules依赖包路径太长,超过了运行的最大路径限制system(windows:260characters,macos:1024characters),参见:层级太深导致文件查找的复杂度增加,严重影响性能,增加耗时。注意:使用npmls--depth=n查看项目相关依赖层次结构的深度。npm3为了解决npm2中的冗余和依赖树问题,npm3以扁平化的方式讨论和处理依赖关系[5]。扁平化具体是指不按照树型安装依赖,而是在安装时将依赖安装在同级目录下。npminstall安装依赖时,会按照配置文件package.json中的依赖顺序进行解析,遇到新的包。放在一级目录下即可(如D@1.0、E@1.0、F@1.0)。如果后面遇到一级目录下已有包,会先判断依赖版本,如果版本相同则忽略,否则按照npm2的方式挂在依赖包目录下。这个处理的原理遵循`Nodejs`[6]的依赖解析规则:如果当前目录下没有找到node_modules,就会递归解析父目录下的node_modules。使用npm3安装依赖后如下图所示:这种扁平化的处理方式在一定程度上缓解了冗余和依赖树的问题。同时npm3还支持动态安装更新包。如果依赖有更新,可以使用npmdedupe命令优化依赖树。但是npm3也存在一些问题,比如:phantom_deps(幻影依赖)[7]。npm3不会以确定的方式安装依赖项。比如:我们在NodeJS中的require()函数不需要考虑配置文件package.json中是否存在依赖。这可能会导致依赖版本不兼容,开发者不容易发现;另外,由于`Nodejs`[8]的依赖解析规则,这也会导致幻影node_modules,即查找依赖,可能会超出代码目录本身的node_modules。如下:-my-monorepo/-package.json-node_modules/-semver/-...-my-monorepo/my-library/-package.json-lib/-index.js-node_modules/-brace-expansion-minimatch-...my-monorepo/my-library/lib/index.js可以使用my-monorepo/node_modules中的依赖项,而不是它自己的目录my-monorepo/my-library/node_modules。npm分身(npm克隆)[9]。npmavatar简单来说就是在node_modules中会出现同一个依赖的不同版本。比如项目同时依赖A@1.0.0和A@2.0.0,不管是压扁A@1.0.0还是A@2.0.0,都会重复另一个依赖。如果这样的克隆很多,会造成一些潜在的问题,比如扩展包体积变大,相关类型验证交叉等。npm5npm5通过添加锁文件记录依赖树信息,并进行依赖锁来唯一确定结构节点模块。这个过程可以确保团队成员使用相同的node_modules依赖结构。但是,我们前面提到的分块算法的复杂度、幻象依赖和头像问题仍然没有解决。pnpm介绍在上一篇文章中,我们大致梳理了npm的发展和遗留问题。而pnpm则巧妙的解决了它们,大大提高了依赖包管理的效率。pnpm指的是performantnpm(高性能npm)。正如pnpm官网所述[10],它是一个节省磁盘空间的快速包管理工具。同时也很好地支持workspace和monorepos。pnpm效果与npm、yarn、yarnpnp工具链效果对比,来自pnpmbenchmarks[11]actioncachelockfilenode_modulesnpmpnpmYarnYarnPnPinstall1m9.5s15.3s16.6s23.6sinstall???2.4s1.3s2.3sn/ainstall??14.8s4s6.8sinstall??14.8s4s6.8sinstall???1.3s2.3sn/ainstall??14.8s4s6.8s1。21.8s8.9s11.2s6.2sinstall?35.4s13.4s12s17.9sinstall??3.1s1.9s7sn/ainstall??2.4s1.3s7.6sn/ainstall?3s6.1s11.8sn/aupdaten/an/an/a2.3s11。8s15.5s28.3s从上表的数据可以看出pnpm的性能相比其他包管理工具有优势。那么你可能想知道为什么pnpm有如此优越的性能。接下来说说pnpm的主要特点。pnpm的原理pnpm的原理有两个主要区别于它的包管理工具的特点:基于硬链接的node_modulesspnpm从全局存储创建硬链接到项目中的node_modules文件夹[12],硬链接指向磁盘上原始文件所在的相同位置。位置,具体来说,node_modules中每个包的每个文件都是来自内容可寻址存储[13]的硬链接。简而言之,全球只有一个具有特定版本和名称的包副本。例如:node_modules└──.pnpm├──bar@1.0.0│└──node_modules│└──bar->/bar│├──index.js│└──package.json└──foo@1.0.0└──node_modules└──foo->/foo├──index.js└──package.jsonnode_modules下唯一的文件夹叫做.pnpm,.pnpm下面是一个文件夹,它下面的文件夹是基于内容可寻址存储的硬链接。同时我们也可以使用pnpmroot命令打印出当前项目存放模块的有效目录。基于依赖分析的软链接符号链接遵循以下依赖包结构:node_modules├──foo->./.pnpm/foo@1.0.0/node_modules/foo└──.pnpm├──bar@1.0.0│└──node_modules│└──bar->/bar└──foo@1.0.0└──node_modules├──foo->/foo└──bar->../../bar@1.0.0/node_modules/bar我们可以看到foo@1.0.0/node_modules/bar中引用了软链接../../bar@1.0.0/node_modules/bar,在项目中引用了foo./.pnpm/foo@1.0.0/node_modules/foo的软链接,如果项目中新增了一个依赖包qar@2.0.0,其引用结构如下:node_modules├──foo->。/.pnpm/foo@1.0.0/node_modules/foo└──.pnpm├──bar@1.0.0│└──node_modules│├──bar->/bar│└──qar->../../qar@2.0.0/node_modules/qar├──foo@1.0.0│└──node_modules│├──foo->/foo│├──bar->../../bar@1.0.0/node_modules/bar│└──qar->../../qar@2.0.0/node_modules/qar└──qar@2.0.0└──node_modules└──qar->/qar根据我们前面介绍的`Nodejs`依赖解析规则[14],实际使用的是foo@1.0.0/node_modules/foo/index.js中需要的依赖包bar是内容在bar@1.0.0/node_modules/bar中。因此,只能访问依赖项中的包。不同peer依赖的依赖解析原理,可以参考这里的Howpeersareresolved[15]。基于硬链接node_modules和基于依赖解析的软链接原理,我们了解到,当我们在同一个操作系统下第二次安装同一个依赖包时,我们只需要创建一个对应的硬链接即可依赖包,对于同一个依赖包的不同版本,只会再次保存不同的部分,哪里判断是否有pnpm?全局pnpm索引文件在~/.pnpm-store/v3/files中。基于此,硬链接的使用使得依赖包的安装速度非常快,同时也去除了冗余,节省了大量的磁盘空间。symlinkssymboliclink[16]pnpmpnpm的具体使用这里就不介绍了,可以在官网[17]和CLI命令[18]上查看使用方法。这里只是介绍几个比较有意思的点CI集成在GitHubActions上,可以像这样使用pnpm来安装和缓存依赖,配置文件目录:.github/workflows/NAME.yml。名称:pnpm示例工作流程:推送:工作:构建:运行:ubuntu-20.04策略:矩阵:节点版本:[15]步骤:-使用:actions/checkout@v2-使用:pnpm/action-setup@v2.0.1with:version:6.20.3-name:UseNode.js${{matrix.node-version}}使用:actions/setup-node@v2with:node-version:${{matrix.node-version}}cache:'pnpm'-name:Installdependenciesrun:pnpminstallpnpm除了在开发体验上的优越性能外,在项目集成方面也不逊色。较大的项目从npm或yarn迁移到pnpm后,也得到了很大的优化,结果如下:WithoutcacheWithcacheyarn2(withoutdedupe)6min31s1min11syarn3(withoutdedupe)4min50s57syarn34min1s50syarn3(optimized)1min1045spnpm58s24s通过以上数据可以看出pnpm在CI应用中的良好表现。详情请参考这个最佳实践Astoryofwehowmigratingtopnpm[19]在pnpmpre-project中使用pnpm时,如果不希望项目中的其他人使用npmi或yarn等包管理器,你可以在package中将preinstall配置项添加到.json配置文件中,以规范统一包管理器的使用。{"scripts":{"preinstall":"npxonly-allowpnpm"}}管理NodeJS版本以前如果同时支持多个项目,需要切换,可能需要切换到不同的NodeJS版本,也许你会使用像nvm或Volta[20]这样的NodeJS版本管理器,而pnpm从v6.12.0开始支持pnpmenv[21]命令,你可以使用它来安装和指定要使用的NodeJS版本,是不是方便多了。monorepo支持由于pnpm对monorepos的强大支持,Vue、Vite等开源项目也纷纷转而使用它。使用pnpmrun结合--filter、--recursive和--parallel选项,您可以指定特定的包并高速执行相关命令。这样做的好处是,之前额外安装了lerna等monorepo管理工具的场景,现在可以用pnpm来覆盖了。详细的文章可以参考pnpmvsLerna:filteringinamulti-packagerepository[22]。小结本文从pnpm出现的背景入手,简要介绍了npm的发展历程和存在的问题,然后简要介绍了pnpm及其作用,重点介绍了pnpm的实现原理,并从应用端选取四点展开.作为新一代的包管理器,pnpm有很多优秀的表现。通过硬链接和软链接解决了npm幻象依赖和头像问题,更好的解决了依赖包的复用问题,从而实现了依赖包的高效快速安装。需要特别注意的是,pnpm严格遵循Nodejs的依赖解析规则,避免了之前任何依赖包的访问修改问题。当然在使用pnpm的过程中也存在一些问题,包括Vue官方在迁移过程中处理过的一些问题。另外,有些包还存在兼容性问题,因为包本身实现了模块解析,没有遵循相关规范。但是pnpm也提供了相关的解决方案。详见pnpmFAQ[23]。参考资料:[1]历史:https://github.com/npm/cli/blob/latest/changelogs/CHANGELOG-1.md[2]过多的依赖破坏了Windows文件系统:https://github.com/npm/npm/issues/3697[3]最大路径长度限制:https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd[4]为什么Windows中是否存在260个字符的路径长度限制?:https://stackoverflow.com/questions/1880321/why-does-the-260-character-path-length-limit-exist-in-windows[5]DependsFlat讨论与处理:https://github.com/npm/cli/blob/latest/changelogs/CHANGELOG-3.md[6]Nodejs依赖解析规则:https://nodejs.org/api/modules。html#all-together[7]phantom_deps(幻影依赖):https://rushjs.io/pages/advanced/phantom_deps/[8]npmdoppelgangers(npm克隆):https://rushjs.io/pages/advanced/npm_doppelgangers/[9]pnpm官网:https://pnpm.io/[10]pnpmbenchmarks:https://pnpm.io/zh/benchmarks[11]硬链接:https://zh.wikipedia.org/wiki/%E7%A1%AC%E9%93%BE%E6%8E%A5[12]内容可寻址存储:https://en.wikipedia.org/wiki/Content-addressable_storage[13]peer如何解析:https://pnpm.io/zh/how-peers-are-resolved[14]symlinks符号链接:https://zh.wikipedia.org/wiki/%E7%AC%A6%E5%8F%B7%E9%93%BE%E6%8E%A5[15]使用方法:https://pnpm.io/zh/pnpm-cli[16]CLI命令:https://pnpm.io/zh/cli/add[17]我们如何迁移到pnpm的故事:https://divriots.com/blog/switching-to-pnpm[18]Volta:https://volta.sh/[19]pnpmenv:https://pnpm.io/zh/cli/env[20]pnpmvsLerna:在多包存储库中进行过滤:https://medium.com/pnpm/pnpm-vs-lerna-filtering-in-a-multi-package-repository-1f68bc644d6a[23]pnpm常见问题解答:https://pnpm.io/faq#pnpm-does-not-work-with-your-project-here