npm、yarn到pnpm的发展历程

news2024/11/18 4:25:28

npm、yarn到pnpm的发展历程

  • 背景
  • 价值点
  • npm发展及存在的问题
      • npm v1(树状结构)
          • 安装原则
          • 优点
          • 不足:
      • npm v3(扁平化结构)
          • 安装原则
          • 优点:
          • 不足:
      • 目录结构不确定
          • 依赖A先安装
          • 依赖A后安装
      • npm v5
          • 优点
      • npm包分身
          • 定义
          • 隐患
      • 幻影依赖
          • 定义
          • 隐患
  • yarn
      • 特点
  • pnpm
      • 特点
      • 原理
      • 三层结构
          • 第一层
          • 第二层
          • 第三层
      • 次级依赖
      • 依赖提升
          • 默认配置
          • 严格模式
          • 结论
  • 总结
  • 不足
  • 参考文章

背景

团队要将各个项目的代码迁移到大仓,前端基于 pnpm 搭建 monorepo,本文记录前端迁移大仓为什么要选 pnpm 来管理依赖,以及这个过程中遇到的一些问题以及一些原理的思考和理解。

价值点

  • 在日常前端开发中node包管理工具(npm) 可谓无处不在,必不可少。
  • pnpm本质上也是node的一种包管理工具,所以理解这些原理后有助于我们平时在开发的时候能够更精准的定位和解决问题。
  • 因此读完本文你可以大致了解 npm 和 yarn 的发展史,以及 pnpm 的原理。

npm发展及存在的问题

一种新技术的出现,往往是为了解决现有技术存在的不足。 正所谓知其然,还要知其所以然。那pnpm到底解决了npm/yarn存在的什么问题?下面将详细介绍npm/yarn的发展及其存在的问题。

我们知道,在执行npm install 后,依赖模块被安装到了 node_modules 目录下,那node_modules目录下的依赖管理机制又是如何的呢?

npm v1(树状结构)

安装原则

npm在版本3之前处理依赖的方式简单粗暴,以递归的形式,严格按照 package.json 结构以及子依赖包的 package.json 结构将依赖安装到他们各自的 node_modules 中。直到有子依赖包不在依赖其他模块。 举个例子,我们的项目 myProject 现在依赖了两个模块:A、B:

{
  "name": "myProject",
  "dependencies": {
    "A": "^1.0.0",
    "B": "^1.0.0",
  }
  "devDependencies": {
    "C": "^1.0.0",
  }
}

而模块A又依赖 D@1.0.0,模块B又依赖 D@2.0.0,模块C又依赖 D@2.0.0

那么,执行 npm install 后,node_modules 目录中的模块结构如下

image-20221123103624445

优点
  • node_modules 的结构和 package.json 结构一一对应,层级结构明显直观,并且保证了每次安装目录结构都是相同的。
不足:
  • node_modules会十分庞大:需要安装的模块非常非常多,在不同层级的依赖中,就算是依赖同一个模块,都需要重新安装一次,因此会有大量的模块重复安装。(比如上图的模块 D@2.0.0)
  • 层级嵌套很深:系统对文件路径都会有一个最大长度的限制,嵌套层级过深可能导致不可预知的问题(比如无法直接删除)。

npm v3(扁平化结构)

为了解决以上问题,NPM 在版本3 做了一次较大更新。其将之前的嵌套结构改为扁平结构。

安装原则
  • 安装模块时,不管其是直接依赖还是子依赖的依赖(间接依赖),优先将其安装在 node_modules 根目录。
  • 当安装到相同模块时,判断在 node_modules 根目录已安装的模块版本是否符合新模块的版本范围,如果符合则跳过,不用再次安装;不符合则在当前模块的 node_modules 下安装该版本的模块。

还是上面的例子,如果使用npm版本3安装依赖,则最后的依赖结构应该如下图:

image-20221123103703944

因为在安装依赖 A@1.0.0 时,把 D@1.0.0 优先安装在了根node_modules 下,接着再安装依赖 B@1.0.0时,其依赖的 D@2.0.0也会优先考虑安装在根node_modules 下,但是根node_modules 下已经存在 D@1.0.0,所以只能将 D@2.0.0 安装在当前依赖 B@1.0.0 的node_modules下。最后再安装依赖C@1.0.0,也是同样的原则。

优点:

在满足 Node.js 的模块查找规则的同时降低了依赖层级,一定程度上缓解了占用磁盘空间(其实并没有完全解决,比如上图的依赖D@2.0.0还是会被安装多次)和路径过长的问题

不足:
  • 依赖的目录结构不确定
  • npm包分身 (npm一直都存在这个问题)
  • 幻影依赖

下面将一一介绍存在的这些不足点。

目录结构不确定

在执行 npm install 的时候,按照 package.json 里依赖的顺序依次解析,则依赖在 package.json 的放置顺序则决定了 node_modules 的依赖结构

思考一个问题:依赖 A 先安装和后安装,其目录结构是否一样?

依赖A先安装

image-20221123103719061

依赖A后安装

image-20221123103740302

由于依赖 B@1.0.0 和依赖 C@1.0.0 都依赖了 D@2.0.0,所以依赖D@2.0.0 优先安装在根node_modules 目录下。当最后安装依赖A时,其间接依赖 D@1.0.0 只能安装在其自身的node_modules 目录下了。

依赖结构的不确定性可能会给程序带来不可预知的问题。

npm v5

为了解决 npm install 时依赖结构不确定的问题,在 npm版本5新增了 package-lock.json文件

优点
  • 固定依赖结构:package-lock.json 的作用是锁定依赖结构,即只要目录下有 该文件,那么每次执行 npm install 后生成的 node_modules 目录结构是相同的。同时版本也会锁定在指定的范围内
  • 提高安装速度:`package-lock.json`` 中已经缓存了每个包的具体版本和下载链接,不需要再去远程仓库进行查询,减少了大量网络请求,提高了安装速度。

虽然npm v5解决了安装依赖时结构不确定的问题,但其安装方式还沿用了 npm v3 的扁平化的方式,所以npm还是还是会遗留刚刚说的两个问题

  • npm包分身
  • 幻影依赖

npm包分身

定义

相同版本的子依赖包被不同的项目依赖所依赖时会安装两次,比如还是上面的例子(依赖D@2.0.0被安装了两次)

隐患
  • 相同的包安装了两次,占用磁盘空间,相对的安装的速度也会变慢
  • 破坏单例,如果是单例的库会使得不同的使用方拿不到相同的实例(代码都不是同一份)

幻影依赖

定义

一个库使用了没有在package.json 中声明的依赖。 还是一开始的例子,myProject的依赖可以直接访问到D依赖,虽然它们没有在package.json中声明。

隐患
  • 不兼容的版本:依赖可能需要的D版本是2.0.0,但是引入的是却是版本1.0.0(这个通过package-lock.json文件可以解决)
  • 缺少依赖:当我们项目里面没有安装到它所依赖的包时,此时就会报错。

其实幻影依赖这种隐患在 npm 树状结构的情况下是能够尽量避免的,因为树状结构严格按照 package.json中声明的依赖安装。但是扁平化这种结构为了减少依赖重复安装,却使幻影依赖出现的可能性更大了。


针对这两个大问题,另外一个node包管理工具yarn是否解决了呢,其实并没有。

yarn

yarn 是在 2016 年发布的,那时 npm 还处于 V3 时期,还没有 package-lock.json 文件,就像上面我们提到的:不稳定性、安装速度慢等缺点经常会受到广大开发者吐槽。此时,yarn 就诞生了。 后来 npm 也意识到了自己的问题,进行了很多次优化,在后面的优化(lock文件、缓存)中,我们多多少少能看到 yarn 的影子,可见 yarn 的设计还是非常优秀的。

特点

yarn 也是采用的是 npm v3 的扁平结构来管理依赖,安装依赖后默认会生成一个 yarn.lock 文件

所以发展到现阶段来看,yarn最大的优点就是比npm安装速度快。

  • npm:串行安装,按照队列安装每个 package,必须要等到当前 package 安装完成之后,才能继续后面的安装
  • yarn:并行安装:同步安装所有包

所以yarn也会有npm同样的问题(依赖管理方式都一样了),那pnpm是怎么处理这些问题的呢,下面就来具体介绍pnpm。

pnpm

pnpm即Performant NPM,高性能npm。

特点

  • 提高安装速度:不用安装那么多重复的包,
  • 节约磁盘空间,安装过的依赖会复用缓存,甚至包版本升级带来的变化都只 diff
  • 非常优雅的解决 npm和yarn的问题而不带入新的问题

想知道他为什么会有这些特点,以及是怎么解决npm和yarn存在的问题的,就得知道他的工作原理具体是怎样的。

原理

还是一开始的例子,当使用pnpm来管理依赖时,其目录结构如下图所示:

image-20221123103811822

下面就逐步介绍pnpm是怎么管理依赖的。

三层结构

第一层
  • 一定程度上沿用了 npm 版本3之前的树状结构,但又不完全是。取其精华,弃其糟粕。
  • 第一层寻找依赖是 nodejs 或 webpack 等运行环境/打包工具进行的,他们在 node_modules 文件夹寻找依赖,并遵循就近原则,所以第一层依赖文件势必要写在 node_modules下,一方面遵循依赖寻找路径,一方面没有将依赖都拎到上级目录,也没有将依赖打平。
  • 项目的根 node_modules 里面依赖和 package.json 里声明的一一对应,简而言之,就是我们在项目的package.json里定义了什么包就只能依赖什么包,即只会有 package.json下声明的包,不会有次级依赖的包。

比如上面的例子,在根node_modules目录下的依赖只有三个 A@1.0.0、B@1.0.0、C@1.0.0,不会有他们的次级依赖。因为在我们项目myProject的package.json中声明的也就只有这三个依赖,没有其他的了。

但是在根node_modules目录下的依赖又不是他们真正的物理位置,接下来涉及到第二层结构。

第二层
  • 项目的 node_modules 下有 .pnpm 文件夹,以平铺的形式储存着所有的包(包括次级依赖)
  • 每个项目根 node_modules 下安装的包以软链接(符号链接)方式将内容指向 node_modules/.pnpm 中的包(如上图的虚线代表的是软连接)。node解析到软链时,会解析这些依赖的真实位置。

这一层主要是解决npm/yarn包重复安装的问题(npm包分身)。每个包的位置:.pnpm/<name>@<version>/node_modules/<name>,当我们的项目依赖里面使用多个重复的包时,只需要安装一次,使用软链指向同一个包即可。

所以到第二层时每个包寻址过程为:node_modules/<name> --> 软链接 node_modules/.pnpm/<name>@<version>/node_modules/<name>

经过两层结构,解决了在一个项目内的包重复安装的问题,但项目不止一个,多个项目对于同一个包的多份拷贝还是冗余了,因此还要来到第三层结构。

第三层
  • 所有包都安装在磁盘全局目录 ~/.pnpm-store/v3/files 下,.pnpm目录下的所有依赖都是以硬链接方式指向这个位置。
  • 全局统一管理路径,跨项目复用,同一版本的包仅存储一份内容,甚至不同版本的包也仅存储 diff 内容。
  • 文件管理方式:基于内容寻址的文件系统CAS(content-addressable):通过文件内容生成内容地址(通常是hash算法),优势是单一实例存储,提高安装速度,节省磁盘空间。

所以,pnpm中每个包都要经过三层寻址路:node_modules/<name> --> 软链接 node_modules/.pnpm/<name>@<version>/node_modules/<name> --> 硬链接 ~/.pnpm-store/v3/files

那 pnpm 中是如何次级依赖的呢?

次级依赖

依赖和依赖的依赖(次级依赖)的实际位置位于同一目录级别,次级依赖再软链到.pnpm目录下。 还是上面的例子,模块A@1.0.0会依赖D@1.0.0,pnpm会把它们放在同一目录层级,D@1.0.0还是会通过软链指向node_modules/.pnpm/D@1.0.0/node_modules/D。同理,B@1.0.0 和 C@1.0.0 所依赖的 D@2.0.0 也会指向同一个位置node_modules/.pnpm/D@2.0.0/node_modules/D


大费周章,终于解决了 npm包分身的问题。但是还有一个问题–幻影依赖。那pnpm到底是怎么解决这个问题的呢?这个涉及到了 pnpm的依赖提升。

依赖提升

注意到,pnpm默认情况下会存在这么一个目录node_modules/.pnpm/node_modules,用来存储提升的所有依赖,当某个依赖有多个版本时,pnpm 也只是遍历依赖关系图并提升找到依赖的第一个版本,所以并不是将某个依赖的所有版本都提升。

默认配置
  • 半严格
; 提升所有包到 node_modules/.pnpm/node_modules
hoist-pattern[]=*

; 提升所有名称包含types的包至根,以便Typescript能找到
public-hoist-pattern[]=*types*

; 提升所有ESLint相关的包至根
public-hoist-pattern[]=*eslint*
  • 我们的项目将只能引用 package.json 中已声明的依赖,但项目所依赖的那些包将能访问任何其他的包

举个例子 我们的项目demo依赖 A@1.0.0 和 D@1.0.0,依赖A@1.0.0 又依赖模块B@1.0.0和C@1.0.0

{
  "name": "demo",
  "dependencies": {
    "A": "^1.0.0",
    "D": "^1.0.0",
  }
}

使用 pnpm 安装依赖后的结构如下

image-20221123103840868

注意到node_modules/.pnpm/node_modules目录下有四个包A、B、C、D,这就是在半严格(默认)配置下将项目中的所有包都提升到这里了。

这时候,按照node/webpack模块加载机制,我们的项目demo也只是能引用到模块A@1.0.0和D@1.0.0,但是模块 A,B,C,D可以被我们项目中所有的依赖引用,即使他们自己的package.json中并没有声明(比如模块A就可以访问到模块D,但模块A中并没有声明D)。

介绍到这,大家是否有这个疑问:不是说pnpm也解决了幻影依赖的问题吗,那按照依赖提升这种情况,不也是会出现幻影依赖这种现象吗?

没错,如果直接使用pnpm的默认配置(半严格模式),确实会有这个问题。但既然说pnpm能够解决这个问题,自然有其他办法,那就是严格模式配置。

严格模式
  • 设置hoist=false
  • 我们的项目以及项目所依赖的包都只能访问他们自己声明的依赖

设置为严格模式后,node_modules/.pnpm/node_modules 目录不存在,根据 node 模块加载机制,模块A就无法访问到模块D了。

但是,目前有不少的包都会直接使用没有在package.json声明的包,比如vue-template-compiler就默认我们项目会安装vue,它自己内部没有声明就使用了。

所以解决这种问题,则可在项目根目录创建文件 .pnpmfile.cjs ,并使用一个 hook 将缺少的依赖项添加到包的清单中

module.exports = {
  hooks: {
    readPackage: (pkg) => {
      if (pkg.name === "vue-template-compiler") {
        pkg.dependencies['vue'] = pkg.version;
      }
      return pkg;
    }
  }
};
结论

所以,pnpm 可以解决幻影依赖的问题。但是,默认情况下 pnpm 安装的依赖也是会被提升的,也会有幻影依赖的问题。我们可以通过设置hoist=false禁止依赖提升,结合.pnpmfile.cjs ,使用hooks将缺少的依赖安装。如此一来,幻影依赖的问题就彻底解决了。

总结

pnpm通过软链+硬链的方式管理依赖,同时严格按照 node 的模块加载机制,解决了npm/yarn存在的包分身和幻影依赖问题。通过创建全局的存储空间~/.pnpm-store/v3/files保证了每个版本的包只会安装一次,还可以跨项目共享,可谓又快(安装快),又小(占用磁盘空间少)。

特点npm v1npm v3npm v5yarnpnpm
依赖结构树状:严格按照package.json中声明的结构扁平化 ,优先安装在根node_modules下扁平化扁平化平铺,全部安装在.pnpm目录下
重复安装依赖有且很严重有,比v1一定程度上少了有,比v1一定程度上少了有,和npn v3 一样,但安装快了无,全局空间~/.pnpm-store/v3/files 统一管理
幻影依赖有,但尽可能可以避免有,相比v1,出现几率更高默认配置(半严格模式)有,严格模式无
有锁文件package-lock.jsonyarn.lockpnpm-lock.yaml
安装速度🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟

不足

世上没有一种东西时十全十美的,pnpm也不例外。在具体的实践过程中,个人觉得比较费劲的一个点是:设置hoist=false开启严格模式后,有不少的包都会出现找不到的情况,这时候就得手动找出哪些包被谁引用,然后再通过hooks手动把这些缺少的包安装到对应的模块中。

参考文章

  • Phantom dependencies
  • NPM doppelgangers
  • [npm 存在的问题以及 pnpm 是怎么处理的](

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/29962.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mysql全文索引

引用&#xff1a;https://www.cnblogs.com/miracle-luna/p/11147859.html MySQL 5.7.6之前只支持英文全文索引&#xff0c;不支持中文全文索引&#xff0c;&#xff0c;需要利用分词器把中文段落预处理 拆分成单词&#xff0c;&#xff0c;然后存入数据库 MySQL 5.7.6 开始&am…

FSK/OOK 调制单发射芯片CMT2119A-ESR/CMT2119B-EQR

FSK/OOK 调制单发射芯片CMT2119A-ESR/CMT2119B-EQR CMT2119A 是 HopeRF 旗下CMOSTEK 无线产品线 FSK/OOK 调制单发射芯片&#xff0c;CMT2219A 则是与之对应的 FSK/OOK 调制单接收芯片&#xff0c;均支持 Sub-1G 应用无线发射芯片CMT2119A主要应用&#xff1a; 无线遥控开关、…

接口设计与优化

文章目录接口的注意事项获取对象的属性或方法&#xff0c;先 判断对象是否为空&#xff01;修改老接口&#xff0c;思考接口的兼容性重点接口&#xff0c;考虑线程池隔离调用第三方接口考虑超时、重试接口的熔断、降级接口&#xff0c;需要考虑限流接口要打印好日志接口考虑热点…

ETF动量轮动+RSRS择时,RSRS修正标准分,回撤降至16%

原创文章第113篇&#xff0c;专注“个人成长与财富自由、世界运作的逻辑&#xff0c; AI量化投资”。 昨天的策略我是比较满意的&#xff0c;沿着进化的方向在迭代我们的策略。ETF轮动RSRS择时&#xff0c;加上卡曼滤波&#xff1a;年化48.41%&#xff0c;夏普比1.89 我们会持…

Linux从入门到精通(十)——进程管理

文章篇幅较长&#xff0c;建议先收藏&#xff0c;防止迷路 文章跳转Linux从入门到精通&#xff08;八&#xff09;——Linux磁盘管理goLinux从入门到精通&#xff08;九&#xff09;——Linux编程goLinux从入门到精通&#xff08;十&#xff09;——进程管理goLinux从入门到精…

Go Web项目 接口开发全流程

风离不摆烂学习日志 Day5 — Go Web项目 接口开发全流程 接上篇地址 Web项目学习之项目结构 routes包分析 InitRoutes package routesimport ("fmt""github.com/gin-gonic/gin""go-web-mini/common""go-web-mini/config""go-we…

Charles断点

1、断点测试的含义 1.1、断点&#xff08;英语&#xff1a;Breakpoint&#xff09;是程序中为了调试而故意停止或者暂停的地方。 调试设置断点&#xff1a;可以让程序运行到该行程序时停住&#xff0c;借此观察程序到断点位置时&#xff0c;其变量、寄存器、I/O等相关的变量内…

mysql InnoDB 索引结构

目录 前言 1. InnoDB常见的索引 2. B树索引 2.1 二分查找法 2.2 二叉查找树 2.3 平衡二叉树 2.4 B树索引 2.5 B树索引 2.5.1 聚集索引 2.5.2 非聚集索引 2.5.3 聚集索引与非聚集索引区别 前言 索引的本质是让mysql以最高效、扫描行数最少的方式找到需要的数据。索引…

Paper写作怎么了解题目方面的重要性?

我们常听到&#xff1a;Paper的好坏&#xff0c;从选择题目开始&#xff01;可见选择Paper题目的重要性。这正是我们今天要探讨的内容。 We often hear that the quality of paper starts from choosing the topic!This shows the importance of selecting paper topics.This i…

数字信号处理-9-离散余弦变换

1 波形合成 假定给一系列振幅和一系列频率&#xff0c;要求构建一个信号&#xff0c;此信号是这些频率元素的和。这样的操作就是合成 def synthesize(amps, fs, ts):"""amps 振幅数组fs 频率数组ts 采样时间点"""# ts 和 fs 的外积&#xff0c…

Spring Cloud(十三):Spring 扩展

Spring扩展点 Bean的生命周期中的BeanPostProcessor扩展点Spring扩展点梳理 Spring扩展点应用场景 整合Nacos 服务注册 ApplicationListener扩展场景——监听容器中发布的事件Lifecycle Nacos 发布订阅 & Eureka 服务启动、同步、剔除Lifecycle扩展场景——管理具有启动、停…

JSP快速入门

目录 1、jsp简述 2、JSP快速入门 2.1、搭建环境 2.2、导入JSP页面 2.3、编写代码 2.4、启动测试 3、JSP原理 4、JSP脚本 4.1、JSP脚本的分类 4.2、案例 4.2.1、需求 4.2.2、实现 ​编辑 4.2.3、测试 ​编辑4.3、JSP缺点 5、JSP语法 5.1、JSP页面的基本结构…

【每日一题Day36】LC1742盒子中小球的最大数量 | 哈希表 找规律

盒子中小球的最大数量【LC1742】 You are working in a ball factory where you have n balls numbered from lowLimit up to highLimit inclusive (i.e., n highLimit - lowLimit 1), and an infinite number of boxes numbered from 1 to infinity. Your job at this facto…

[附源码]java毕业设计养老院管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

C++ 语言学习 day10 复习(2)

1.友元 的三种形式 /*********** 友元 ************ * ①全局函数做友元 * ②类做友元 * ③类成员函数做友元 * **************************/ 代码&#xff1a; #include <iostream> #include <string> using namespace std;/* ③类函数友元 : 程序规则{ 自上…

Charles下载抓包基本流程

一、Charles官网下载链接&#xff1a; https://www.charlesproxy.com/download/ 二、抓包步骤&#xff1a; 1、安装Charles&#xff0c;并打开 2、电脑设置代理端口&#xff1a; 打开charles->Proxy->Proxy Settings,设置代理端口&#xff0c;如图所示 3、手机设置代…

Day10--配置uni-app的开发环境

1.啥子是ui-app呢&#xff1f; 1》官方介绍&#xff1a; 2》博主介绍 ************************************************************************************************************** 2.开发工具建议使用HBuilder X *************************************************…

BSA/HSA表面修饰二甘醇酐,人血清白蛋白HSA、牛血清白蛋白BSA偶联二甘醇酐

BSA作用&#xff1a; BSA一般做为稳定剂被用于限制酶或者修饰酶的保存溶液和反应液中&#xff0c;因为有些酶在低浓度下不稳定或活性低。加入BSA后&#xff0c;它可能起到“保护”或“载体”作用&#xff0c;不少酶类添加 BSA后能使其活性大幅度提高。不需要加BSA的酶加入BSA一…

【时序预测-SVM】基于鲸鱼算法优化支持向量机SVM实现时序数据预测附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

【数据结构】二叉树详解(上篇)

&#x1f9d1;‍&#x1f4bb;作者&#xff1a; 情话0.0 &#x1f4dd;专栏&#xff1a;《数据结构》 &#x1f466;个人简介&#xff1a;一名双非编程菜鸟&#xff0c;在这里分享自己的编程学习笔记&#xff0c;欢迎大家的指正与点赞&#xff0c;谢谢&#xff01; 二叉树&…