当前位置: 首页 > Web前端 > JavaScript

精读《pnpm》

时间:2023-03-27 02:07:43 JavaScript

pnpm全称是“PerformantNPM”,即高性能npm。它将软硬链接与新的依赖组织方式相结合,大大提高了包管理的效率,同时解决了“幻影依赖”问题,使包管理更加规范,降低了潜在风险的可能性。pnpm的使用很简单,可以使用npm安装:npmipnpm-g之后就可以使用pnpm来代替npm命令了。比如最重要的安装包的步骤,可以用pnpmi代替npmi,这样就可以使用pnpm了。pnpm的优点用一个比较好记的词来形容pnpm的优点,那就是“快、准、狠”:快:安装速度快。准确:安装的依赖会准确复用缓存,即使是包版本升级带来的变化也只会是diff,绝不会浪费任何空间,逻辑上无缝衔接。无情:直接废掉了幻影依赖,在逻辑合理性和模棱两可的便利性上毫不留情地选择了逻辑合理性。带来这些优势的思路都在官网这张图里:所有的npm包都安装在全局目录~/.pnpm-store/v3/files下,同一个版本的包只存放一个内容,甚至不同版本的包也只存储差异内容。每个项目的node_modules下都有一个.pnpm目录,以扁平化结构管理各个版本包的源代码内容,并通过硬链接指向pnpm-store中的文件地址。每个项目的node_modules下安装的包结构是一个树形结构,符合节点最近搜索规则,内容以软链接的形式指向node_modules/.pnpm中的包。所以对于每个包的搜索都要经过三层结构:node_modules/package-a>softlinknode_modules/.pnpm/package-a@1.0.0/node_modules/package-a>hardlink~/.pnpm-存储/v3/文件/00/xxxxxx。通过这三层寻址有什么好处?为什么是三层而不是两层或四层?依赖文件三层寻址的目的第一层沿用上面的例子。第一层查找依赖是通过运行环境/打包工具来完成的,比如nodejs或者webpack。他们在node_modules文件夹下寻找依赖,遵循就近原则,所以第一层依赖文件一定要写在node_modules/package-a下。一方面遵循依赖查找路径,另一方面也不会把所有的依赖都转移到上层目录,也不会把依赖压平。目的是还原最具语义的包。json定义:即定义任何包都可以依赖,反之亦然。同时,每个包的子依赖也是从包中查找,解决了多版本管理的问题,也使得node_modules有一个稳定的结构。也就是说,目录组织算法只与package.json定义有关,与包安装顺序无关。如果就此打住,这就是npm@2.x包管理方案,但是因为npm@2.x包管理方案是最没有歧义的,所以第一层遵循这个方案的设计。第二层从第二层开始,要解决npm@2.x设计带来的问题,主要是包复用的问题。所以node_modules/package-a>softlinknode_modules/.pnpm/package-a@1.0.0/node_modules/package-a寻址的第二层使用软链接来解决代码重复引用的问题。相比npm@3的扁平化包设计,软链接可以保持包结构稳定,同时利用文件指针解决重复占用硬盘空间的问题。到此为止,解决了一个项目中包管理的问题,但是项目不止一个,多个项目同一个包多份太浪费了,所以需要第三步映射.第三层第三层映射node_modules/.pnpm/package-a@1.0.0/node_modules/package-a>硬链接~/.pnpm-store/v3/files/00/xxxxxx已经离开了当前项目路径和指向一个全局统一的管理路径是跨项目复用的必然选择。然而,pnpm更进一步。不是直接将包的源代码存放在pnpm-store中,而是将其拆分成文件块,后面会详细讲解。PhantomdependencyPhantomdependency是指项目代码引用的包不是直接在package.json中定义的,而是被一个包作为子依赖附带安装的。依赖幻象依赖在代码中最大的隐患是包的语义控制无法渗透到其子包,即包a@patch的变化可能意味着其子级别的BreakChange依赖包b@major.因为这三层寻址的设计,第一层只能包含package.json中定义的包,使得node_modules无法寻址package.json中没有定义的包,自然就解决了幻象依赖的问题。但是还有一个比较难解决的幻影依赖问题,就是用户在Monorepo项目的根目录下安装了某个包,而这个包可能是通过某个子包中的代码来解决的。要彻底解决这个问题,需要配合使用Rush。在工程上,完全是靠问题检测来解决的。peer-dependences安装规则pnpm对peer-dependences有一套严格的安装规则。对于定义了对等依赖的包,意味着对等依赖的内容是敏感的。潜台词意味着对于不同的对等依赖,这个包可能有不同的性能,所以pnpm针对不同的对等依赖环境,可能会创建同一个包的多个副本。比如barpeer-dependences这个包依赖baz^1.0.0和foo^1.0.0,那么当我们在Monorepo环境下,在这两个Packages下安装不同版本的包会怎么样呢?-foo-parent-1-bar@1.0.0-baz@1.0.0-foo@1.0.0-foo-parent-2-bar@1.0.0-baz@1.1.0-foo@1.0.0结果是这样的(引用官网文档例子):node_modules└──.pnpm├──foo@1.0.0_bar@1.0.0+baz@1.0.0│└──node_modules│├──foo│├──栏->。./../bar@1.0.0/node_modules/bar│├──baz->../../baz@1.0.0/node_modules/baz│├──qux->../../qux@1.0.0/node_modules/qux│└──plugh->../../plugh@1.0.0/node_modules/plugh├──foo@1.0.0_bar@1.0.0+baz@1.1.0│└──node_modules│├──foo│├──bar->../../bar@1.0.0/node_modules/bar│├──baz->../../baz@1.1.0/node_modules/baz│├──qux->../../qux@1.0.0/node_modules/qux│└──plugh->../../plugh@1.0.0/node_modules/plugh├──bar@1.0.0├──baz@1.0.0├──baz@1.1.0├──qux@1.0.0├───plugh@1.0.0可以看到安装了两个相同版本的foo,虽然内容完全一样,但是名字不同:foo@1.0.0_bar@1.0.0+baz@1.0.0,foo@1.0.0_bar@1.0.0+baz@1.1.0这也是pnpm规则严格的体现。任何包都不要有全局副作用,或者考虑单例实现,否则可能会被pnpm安装多次。硬链接和软链接的原理要理解pnpm软硬链接的设计,首先要回顾一下操作系统文件子系统对软硬链接的实现。硬链接是通过lnoriginFilePathnewFilePath创建的,比如ln./my.txt./hard.txt,创建的hard.txt文件和my.txt都指向同一个文件存储地址,所以无论修改哪个文件,就是直接修改了原地址的内容,导致两个文件的内容同时发生变化。此外,通过硬链接创建的N个文件是等价的。通过ls-li./查看文件属性,可以看到通过硬链接创建的两个文件的inode索引相同:ls-li./84976912-rw-r--r--2authorstaff489Jun915:41my.txt84976912-rw-r--r--2authorstaff489Jun915:41hard.txt其中第三个参数2表示这个文件指向的存储地址有两个硬链接引用。如果硬链接指向目录就麻烦多了。第一个问题是,这会导致文件的父目录出现歧义。同时所有的子文件都必须创建硬链接,实现起来比较复杂。因此,Linux不提供这种能力。软链接是通过ln-soriginFilePathnewFilePath创建的,可以认为是一个指向文件地址的指针,即它本身有一个新的inode索引,但是文件内容只包含指向的文件路径,如:84976913-rw-r--r--2authorstaff489Jun915:41soft.txt->my.txt删除源文件后,软链接也会失效,但硬链接不会,而且软链接可以对文件夹生效。因此,虽然pnpm采用了软硬件结合的方式来实现代码复用,但是软链接本身几乎不占用多少额外的存储空间,而硬链接方式则不占用额外的内存空间。因此,对于同一个包,pnpm额外占用的存储空间可以近似为零。全局安装目录pnpm-storepnpm的组织方式在三级寻址中采用了硬链接的方式,但是还有一个问题没有提到,就是硬链接的目标文件不是普通的npm包源码,而是一个散列文件,这种文件组织方式称为内容寻址(content-basedaddressing)。简单来说,基于内容寻址相对于基于文件名寻址的优势在于,即使包版本升级,只需要存储变化的Diff,而不是新版本的完整文件内容,进一步节省了关于版本管理。贮存。pnpm-store的组织方式大概是这样的:~/.pnpm-store-v3-files-00-e4e13870602ad2922bfc7..-e99f6ffa679b846dfcbb1....-01..-....-ff..Content-地址,而不是文件位置寻址存储。之所以可以采用这种存储方式,是因为npm包的内容一旦发布,就不会再发生变化,适合内容寻址,内容固定。同时,内容寻址也忽略了包的结构关系。当一个新包下载解压后,遇到文件Hash值相同的时候,可以丢弃,只存储Hash值不存在的文件。这自然会体会到开头说的,pnpm只存储同一个包能力的不同版本的增量变化。综上所述,pnpm采用了三层寻址,既契合了node_modules默认的寻址方式,又解决了文件重复安装的问题,顺便解决了幻象依赖的问题。可以说是目前包管理方面最好的创新。但是其苛刻的包管理逻辑使得我们在单独使用pnpm管理大型Monorepos时很容易遇到一些合乎逻辑却很尴尬的地方。例如,如果每个Package与同一个包的参考版本发生分歧,则可能会导致PeerDeps这些包的包生成多个实例,而这些包版本的差异可能是粗心造成的。我们可能需要使用Rush等Monorepo管理工具来保证版本一致性。讨论地址为:Jingdu《pnpm》·Issue#435·dt-fe/weekly想参与讨论的请点这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号版权声明:免费转载-非商业-非衍生保留属性(CreativeCommons3.0License)