当前位置: 首页 > 科技观察

深入思考现代包管理器——为什么我现在推荐pnpm而不是npm-yarn?

时间:2023-03-21 20:25:49 科技观察

好久没有更新原文章了,还在思考和沉淀中,以及公众号以后会更频繁的输出一些前端工程相关的干货,希望能有所启发帮助您在实际工作中提高效率。本文将为大家分享一位业界优秀的包管理器——pnpm。目前GitHub已经有9.8k的star,比较成熟稳定。它源自npm/yarn,但解决了npm/yarn内部潜在的bug,极大地优化了性能和扩展了使用场景。以下是本文的思维导图:1.什么是pnpm?pnpm的官方文档(https://pnpm.js.org/en/)是这样说的:Fast,diskspaceefficientpackagemanager因此,pnpm本质上是一个包管理器,它与npm/yarn没有区别,但它有两个优点作为杀手级特色:包安装速度极快;磁盘空间利用率非常高。安装也非常简单。能有多简单?npmi-gpnpm二、特性概述1、速度快pnpm安装包有多快?先以React包为例进行对比:可以看到pnpm黄色部分在大多数场景下,包安装速度明显优于npm/yarn,速度会快2-3倍比npm/yarn更快。对yarn比较熟悉的同学可能会说,yarn不是有PnP安装方式吗(https://classic.yarnpkg.com/en/docs/pnp/)?直接去掉node_modules,将依赖包的内容写入磁盘,节省node文件I/O开销,也可以提高安装速度。(具体原理可以看这篇文章(https://loveky.github.io/2019/02/11/yarn-pnp/))接下来我们使用这样一个仓库(https://github.com/pnpm/benchmarks-of-javascript-package-managers)为例,我们来看一下benchmark数据,主要是pnpm和yarnPnP的对比:由此可见,总体来说,pnpm的包安装速度还是明显优于纱线即插即用。2.磁盘空间的高效利用pnpm内部使用内容可寻址的文件系统将所有文件存储在磁盘上。该文件系统的突出特点是不会重复安装同一个包。在使用npm/yarn的时候,如果有100个项目依赖lodash,那么lodash大概安装了100次,这部分代码写在了磁盘的100个地方。但是使用pnpm的时候只会安装一次,而且在磁盘里面只有一个地方可以写,以后再用的时候直接使用hardlink(hardlink,不清楚的可以参考本文(https://www.cnblogs.com/itech/archive/2009/04/10/1433052.html))。即使一个包有不同的版本,pnpm也会极大的复用之前版本的代码。比如lodash有100个文件,更新版本后又增加了一个文件,那么磁盘就不会重写101个文件,而是保留原来100个文件的hardlink,只写入新的一个文件。3.支持monorepo随着前端工程越来越复杂,越来越多的项目开始使用monorepo。以前,对于多个项目的管理,我们一般使用多个git仓库,而monorepo的目的是用一个git仓库来管理多个子项目。所有的子项目都存放在根目录的packages目录下。那么一个子项目A项目代表一个包。如果之前没有接触过monorepo的概念,建议仔细看看这篇文章(https://www.perforce.com/blog/vcs/what-monorepo)和开源的monorepo管理工具lerna(https://github.com/lerna/lerna#readme),项目目录结构可以参考babel仓库(https://github.com/babel/babel)。pnpm与npm/yarn的另一大区别是它支持monorepo,这体现在每个子命令的功能上。比如在根目录下pnpmaddA-r,当然所有的包都会加上A依赖。支持--filter字段过滤包。4、安全性高之前使用npm/yarn时,由于node_module的扁平结构,如果A依赖B,B依赖C,那么A可以直接使用C,但是问题是A没有声明C的依赖.因此,会发生这种非法访问。但是pnpm脑洞很大,自创了一套依赖管理的方法,很好的解决了这个问题,也保证了安全。如何体现安全性,避免非法访问依赖的风险,后面会详细讨论。3.依赖管理npm/yarninstall的原理主要分为两部分。首先,执行npm/yarninstall后,包是如何到达项目node_modules的。其次,node_modules内部是如何管理依赖关系的。命令执行后,会先构建依赖树,然后每个节点下的包会经过以下四个步骤:-1.将依赖包的版本范围解析成具体的版本号-2.下载对应版本的依赖tar包到本地离线镜像-3.将离线镜像的依赖解压到本地缓存-4.将缓存中的依赖复制到当前目录的node_modules目录下,对应的包就会到达项目的节点模块。那么,node_modules里面这些依赖的目录结构是怎样的呢?也就是说,项目的依赖树是什么?在npm1和npm2中,显示了一个嵌套结构,例如:node_modules└─foo├─index.js├─package.json└─node_modules└─bar├─index.js└─package.json如果有依赖在酒吧,它会继续筑巢。试想这样的设计存在什么问题:依赖层次太深会导致文件路径太长,尤其是在window系统下。安装了大量重复包,文件体积非常大。比如和foo在同一个目录下有一个baz,两者都依赖同一个版本的lodash,那么lodash会安装到两者的node_modules中,也就是重复安装。不能共享模块实例。例如,React有一些内部变量。React在两个不同的包中引入的不是同一个模块实例,所以内部变量无法共享,导致一些不可预知的bug。然后,从npm3开始,包括yarn,都着手通过扁平化依赖来解决这个问题。我相信每个人都有这样的经历。我安装了express,为什么node_modules里有那么多东西?是的,这是扁平依赖管理的结果。与之前的嵌套结构相比,当前目录结构类似如下:到node_modules目录下,不再有深层嵌套关系。这样,在安装新包时,根据noderequire机制,会继续寻找上层的node_modules。如果找到相同版本的包,则不会重新安装,解决了重复安装大量包的问题,??依赖级别也不一样。太深了。前面的问题是解决了,但是你想想这个压扁的方法,是不是真的刀枪不入?一点也不。它还有很多问题,我们来梳理一下:依赖结构的不确定性。展平算法本身非常复杂,需要很长时间。仍然有可能非法访问未在项目中声明依赖项的包。后两者很容易理解。第一点的不确定性是什么意思?这里有详细的解释。如果现在项目依赖了两个包foo和bar,这两个包的依赖关系是这样的:那么npm/yarninstall扁平化的时候,是这个还是这个?答案是:两者皆有可能。根据foo和bar在package.json中的位置,如果foo早声明,则为前一个结构,否则为后一个结构。这就是依赖结构存在不确定性问题的原因,也是锁文件诞生的原因。不管是package-lock.json(npm5.x才出现)还是yarn.lock,都是为了保证在install结构之后生成一定的node_modules。尽管如此,npm/yarn本身仍然存在扁平化算法复杂、非法包访问等问题,影响性能和安全性。pnpm依赖管理pnpm的作者ZoltanKochan发现yarn并不打算解决上述问题,于是他从头开始,编写了一个新的包管理器,并创建了一个新的依赖管理机制。现在让我们一探究竟。还是以安装express为例,我们新建一个目录,执行:pnpminit-y然后执行:pnpminstallexpress我们看下node_modules:.pnpm.modules.yamlexpress我们可以直接看到express,但是值得注意的是这里只是A软链接,不信你打开一看,里面没有node_modules目录。如果是真实文件位置,那么根据node的包加载机制,是找不到依赖的。那么它的真实位置在哪里呢?我们继续在.pnpm中查找:?node_modules?.pnpm?accepts@1.3.7?array-flatten@1.1.1...?express@4.17.1?node_modules?accepts?array-flatten?body-parser?content-处置...?etag?express?libHistory.mdindex.jsLICENSEpackage.jsonReadme.md好家伙!我是在.pnpm/express@4.17.1/node_modules/express下找到的!Whatever再打开一个包:好像是一样的规则,都是@version/node_modules/这样的目录结构。而express的依赖都在.pnpm/express@4.17.1/node_modules下,这些依赖都是软链接。再看看.pnpm,虽然.pnpm目录呈现的是扁平的目录结构,但仔细想想,沿着软链接慢慢展开,其实是一个嵌套结构!?node_modules?.pnpm?accepts@1.3.7?array-flatten@1.1.1...?express@4.17.1?node_modules?accepts->../accepts@1.3.7/node_modules/accepts?array-flatten->../array-flatten@1.1。1/node_modules/array-flatten...?express?libHistory.mdindex.jsLICENSEpackage.jsonReadme.md将包本身和它的依赖放在同一个node_module下,完全兼容原生Node,可以非常好地组合包和相关依赖井井有条,设计精美。现在回过头来看,根目录下的node_modules不再是眼花缭乱的依赖,而是和package.json中声明的依赖基本一致。虽然pnpm里面有些package会设置依赖提升,提升到根目录node_modules,但是总体来说,根目录node_modules比之前更加清晰和规范了。4、再说说安全。不知道大家有没有发现,pnpm的依赖管理方式也巧妙的避免了非法获取依赖的问题,即只要一个包没有在package.json中声明依赖,就不能在项目中使用.访问过。但是在npm/yarn中是做不到的,那么你可能会问,如果A依赖B,B依赖C,那么即使A没有声明C的依赖,由于依赖提升的存在,安装了C在A中的node_modules,然后我在A中使用C,运行没有问题。我上线后,也能正常运行。不是很安全吗?并不真地。首先,你要知道B的版本随时可能变化。如果之前依赖C@1.0.1,现在出新版本,新版本B依赖C@2.0.1,那么在项目A中,npm/yarninstall后,2.0.1版本安装了C的,A中仍然使用C中的老版本API,所以可能会直接报错。第二,如果B更新了,C可能就不需要了,那么在安装依赖时,node_modules中不会安装C,A中引用C的代码会直接报错。还有一种情况,在一个monorepo项目中,如果A依赖X,B依赖X,还有一个C,它不依赖X,但是在它的代码中使用了X。由于依赖提升的存在,npm/yarn会将X放到根目录的node_modules中,这样C就可以在本地运行了,因为根据node的包加载机制,可以加载到monorepo项目的node_modules中根目录下的X。但是试想一下,一旦C单独打包出来,用户单独安装C,那么X就找不到了,执行引用X的代码就会直接报错。这些都依赖于提高潜在的错误。如果是自己的业务代码,那还好,但是试想一下,如果是很多开发者的工具包,危害就很严重了。npm过去也想解决这个问题,指定--global-style参数可以禁止变量提升,但这样做相当于回到嵌套依赖的时代,一夜之间回到解放前,前面提到的缺点嵌套的依赖关系仍然暴露。npm/yarn本身似乎很难解决依赖提升的问题,但是社区有针对这个问题的具体解决方案:dependency-check,地址:https://github.com/dependency-check-team/dependency-check但不可否认的是pnpm更彻底。独创的一套依赖管理方式,不仅解决了依赖提升的安全问题,还在时间和空间上极大地优化了性能。5.日常使用说了这么多,你可能觉得pnpm挺复杂的,用起来贵吗?相反,pnpm使用起来非常简单。如果您以前有使用npm/yarn的经验,您甚至可以在不将Seam迁移到pnpm的情况下使用它。不信,我们举几个日常使用的例子。pnpminstall类似于npminstall,安装项目下的所有依赖。但是对于monorepo项目,会安装工作空间下所有包的所有依赖项。但是可以通过--filter参数指定包,只安装满足条件的依赖包。当然你也可以用这个安装单个包://安装axiospnpminstallaxios//安装axios并添加axios到devDependenciespnpminstallaxios-D//安装axios并添加axios到依赖pnpmintalaxios-S当然也可以通过--filter指定包。pnpmupdate根据指定范围更新包到最新版本,可以在monorepo项目中通过--filter指定包。pnpmuninstall删除node_modules和package.json中指定的依赖项。monorepo项目也是如此。示例如下://removeaxiospnpmuninstallaxios--filterpackage-apnpmlink将本地项目连接到另一个项目。请注意,使用的是硬链接,而不是软链接。如:pnpmlink../../axios另外,对于我们经常使用的npmrun/start/test/publish,直接将这些替换成pnpm也是一样的,这里不再赘述。更多的使用姿势可以参考官方文档:https://pnpm.js.org/en/可以看到,虽然pnpm内部做了很多复杂的设计,但实际上用户是察觉不到的。使用看起来很友好。而且,作者现在还在维护。目前npm上周下载量已经达到100000+,经过了大规模用户的考验,稳定性也能得到保障。因此,从整体上看,pnpm是比npm/yarn更好的解决方案,期待未来pnpm的更多实现。