yarn、npm和pnpm会根据彼此的lock文件下包吗?

如果项目的包是用npm下载的,然后别人下载下来用yarn或者pnpm导包,会根据npm的lock文件导还package.json文件导?如果不按loc…
关注者
5
被浏览
5,529
登录后你可以
不限量看优质回答 私信答主深度交流 精彩内容一键收藏

概述

pnpm - 速度快、节省磁盘空间的软件包管理器

perfomance npm,即pnpm(高性能npm)

优势

  • 快速
    • pnpm 是同类工具速度的将近 2 倍
  • 高效
    • node_modules 中的所有文件均链接自单一存储位置
  • 支持monorepos
注:这个东西这么读monorepos = Monolithic repository /ˌmänəˈliTHik/ /rəˈpäzəˌtôrē/
    • pnpm 内置了对单个源码仓库中包含多个软件包的支持
  • 严格
    • pnpm 创建的 node_modules 默认并非扁平结构,因此代码无法对任意软件包进行访问


以上是4条优势是官网的说明和宣传,后面我们会针对npm的发展历史中存在的问题,

来对比说明, pnpm的提出动机,pnpm 的优势在哪里,为什么具备这些优势。

npm发展历程中存在的问题

npm 全称, Node Package Manager node包管理工具

执行npm install 之后。npm 帮我们下载对应的依赖包并解压到本地缓存,然后构造node_modules目录结构,写入依赖文件,对应的node_modules内部结构也经历了几个版本的变化。

npm v1/v2 嵌套依赖

最开始其实没有注重npm包的管理,只是简单的嵌套依赖,这种方式层级依赖结构清晰

但是随着npm包的增多,项目的迭代扩展,重复包越下载越多,造成了空间浪费,导致前端本地项目node_modules 动辄上百M

在业务开发中,安装几个项目,项目体积好几G,对使用者们极其不友好。


入下图所示,依赖包C 在AB 中都被引用了, 被重复下载了两次,其实是两个完全相同的东西。

从我们现在的角度看,完全没有必要。



npm v3 扁平化

node_modules体积过大,嵌套过深

npm 团队也意识到这个问题,通过 扁平化 的方式,将子依赖安装到了主依赖所在项目中,以减少依赖嵌套太深,和重复下载安装的问题。


如下图所示,A 的依赖项C 被提升到了顶层,如果后续有安装包,也依赖C,会去上一级的node_modules查找,如果有相同版本的包,则不会再去重复下载,直接从上一层拿到需要的依赖包C


说明:为什么自己的node_modules没有C,也能在上层访问到C呢?
require 寻找第三方包,会每层级依次去寻找node_modules,所以即便本层级没有node_moudles,上层有,也能找到




扁平化方式解决了相同包重复安装的问题,也 一定程度上 解决了依赖层级太深的问题。


为什么说是一定程度上?

因为如上图所示,B 依赖的C v2.0.0,并没有提升,依然是嵌套依赖。


因为在两个依赖包 C 的版本号不一致,只能保证一个在顶层,上图所示C v1.0.0 被提升了,v2.0.0 没有被提升,后续v2.0.0 还是会被重复下载,所 以当出现多重依赖时,依然会出现重复安装的问题。

而且这个提升的顺序,也不是根据使用量优先提升,而是根据先来先服务原则,先安装的先提升。 这会导致不确定性问题 ,随着项目迭代,npm i 之后得到的node_modules目录结构,有可能不一样。


与此同时,我们把C,提升到了顶层,即使项目package.json,没有声明过C,但是也可以在项目中引用到C, 这就是幽灵依赖问题。


可以说 npm v3 在解决嵌套依赖,重复安装问题的同时,又带来了新的问题。

npm v5 lock

npm v5 借鉴yarn的思想, 新增了package-lock.json

该文件里面记录了package.json依赖的模块,以及模块的子依赖。并且给每个依赖标明了版本、获取地址和验证模块完整性哈希值。

通过package-lock.json,保障了依赖包安装的确定性与兼容性,使得每次安装都会出现相同的结果。

这个就解决了不确定性的问题

多重依赖和幽灵依赖 并没有好的解决方式。


pnpm

综上,基于 npm扁平化node_modules 的结构下,虽然解决了依赖嵌套、重复安装的问题,但 多重依赖和幽灵依赖 并没有好的解决方式。

pnpm出现就是为了解决现在npm 存在的问题,正如官网pnpm 所形容自己的是一款 速度快,节省磁盘空间 的软件包管理器。


前置知识 软链接&硬链接

简单理解

硬链接就是多个文件名指向了同一个文件,这多个文件互为硬链接。

像是JS 中的两个相同的对象,a 和b 的真实内容指向堆中同一个地址,修改一个,同时改变,一荣俱荣,一损俱损。删除一个,并不影响另一个。

let a = {test:1} 
let b = a
a.test = 2
console.log(b) // {test:2}

软链接就是快捷方式,是一个单独文件。

就像我们电脑桌面上的快捷方式,大小只有几字节,指向源文件,点击快捷方式,其实执行的就是源文件。

专业理解

在 Linux 的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。A 是 B 的硬链接(A 和 B 都是文件名)则 A 文件中的 inode 节点号与 B 文件的 inode 节点号相同,即一个 inode 节点对应两个不同的文件名,两个文件名指向同一个文件。

软链接可以认为是指向文件地址指针的指针,即它本身拥有一个新的 inode 索引,但文件内容仅包含指向的文件路径。

源文件被删除时,软链接也会失效,但硬链接不会,软链接可以对文件夹生效。

软硬链接 是linux 中 解决文件的共享使用问题的两个方式,目的也是为了节省磁盘空间。


node_modules的层级结构

比如某项目中,package.json里声明了A和B,

A的package.json 里声明了C v1.0.0,B的package.json 里声明了C v2.0.0



进行pnpm i 之后,node_modules的层级结构如下

双键头代表硬链接

单箭头代表软链接

node_modules
|_ A -> .pnpm/A@1.0.0/node_modules/A
|_ B -> .pnpm/B@1.0.0/node_modules/B
|_ .pnpm
  |_ A@1.0.0
    |_ node_modules
      |_ A => pnpm/store/A 
      |_ C -> ../../C@1.0.0/node_modules/C
  |_ B@1.0.0
    |_ node_modules
      |_ B => pnpm/store/B 
      |_ C -> ../../C@2.0.0/node_modules/C
  |_ C@1.0.0
    |_ node_modules
      |_ C => pnpm/store/C 
  |_ C@2.0.0
    |_ node_modules
      |_ C => pnpm/store/C

以A 包为例,A的目录下并没有node_modules,是一个软链接,真正的文件位于 .pnpm/A@1.0.0/node_modules/A 并硬链接到全局store 中。


A 和 B 是我们在项目package.json中声明的依赖包,node_modules除了A,B 没有其他包,说明不是扁平化结构。也就不存在 幽灵依赖的问题


.pnpm 中存放着所有的包。最终硬链接指向指向全局pnpm 仓库里的store目录下。

也就是说,我们所有的包,最终都以硬链接的形式,最终都在全局 pnpm/store 中,可以使得不同的项目从全局 store 寻找到同一个依赖, 大大节省了磁盘空间


如果上面这个文件列表不够直观,大家也可以看我参考官网画的结构图



pnpm 验证

全局安装 brew install pnpm

以我自己基于vue-cli封装的一个移动端项目vue-template为例

github 地址如下
基于vue-cli二次封装的移动端框架,vue3 +vue-cli4 + webpack5 + 多入口打包 + 自动生成项目模版 + pinia + 数据持久化 + 路由动画 + axios二次封装

npm i 之后

查看node_modules 体积 293M

du -h -s node_modules
293M node_modules

查看package-lcok.json中重复文件,以postcss为例,一眼就看到了两个版本的postcss版本,

查看node_modules只有一个版本的postcss 包会被提升,其他版本的就会被重复下载


pnpm i 之后

查看node_modules 体积 251M

du -h -s node_modules
251M node_modules

切换到node_modules目录下,查看所有文件信息

cd node_modules
ls -alh

以axios库为例,只有37B,只是一个快捷方式,axios 软链接指向

这里的37字节其实是axios@0.26.1文件名的大小。

.pnpm/axios@0.26.1/node_modules/axios



切换到.pnpm 目录下,查看所有文件信息

cd .pnpm
ls -alh

我们看到postcss三个版本文件夹,说明现在项目里依赖三个版本的postcss



切换到postcss@7.0.39目录,查看文件信息

cd postcss@7.0.39/node_modules/postcss
stat -x package.json

值得关注的属性有两个,一个是Links,表示硬链接个数,一个是Inode



我们可以通过Inode 去查询所有的硬链接

find . -inum 8177610

可以看到,在全局Library/pnpm/store/下对应的文件目录
4条记录 也对应了 links:4



对比

当只一个项目时,node_modules体积两者差距不大。

我们使用两个相同项目,npm 的node_modules 将达到近600M,

pnpm 依旧能保持 第二个相同项目node_modules的体积只有54M,因为软链接和硬链接也会占用一部分体积。所以两个项目总体积在 54 + 253 = 300M。

此时优势已经很明显了。

举一个极端的例子,当有10个相同项目时,npm 的node_modules 将达到2930M,将近3个G,而pnpm 依旧能保持 住一个相对不大的体积。

我们在业务开发时,其实一般都通用的模版,所以项目的依赖基本上一致,我觉得pnpm还是非常好的。

全局安装目录 pnpm-store的目录结构

pnpm
└── store
    └── v3
        └── files
            ├── 00
              - cd3e571524c095736
              - 02a74db92f0368580
            ├── 01
            ├── 02

上图是我们全局目录下pnpm 的目录结构。

我们在全局目录里存放的不是npm 包的源码,而是hash值,这里采用了基于文件内容寻址方案。

简单来说就是文件内容被加密成了64位hash值,hash值都是唯一的,如果文件内容不变,hash 值也不会变。

这个非常适合npm的安装包,一般来说,依赖包的更新都是向下兼容的,两个版本的包差别只是部分,而我们使用hash存储,会根据文件内容变化,只会存储变化的部分,相同的部分,生成的hash不会变,只存储一份就够了, 一定程度上,也节省了磁盘空间。


pnpm 常见问题

为什么使用硬链接? 为什么不直接创建到全局存储的软链接?

这个问题非常复杂,说来话长,我一点点分析,花了很多功夫在这个问题上,和大家分享一下调研结果。

首先,pnpm 官网如此解释

直接软链至全局存储与 Node 的 --preserve-symlinks 标志一起使用是可行的,但是,该方法附带了个自己的问题,因此我们决定使用硬链接。


大意就是可以做,但我们不想,因为会引发新的问题。

require 直接引入软链接

软链接的文件中,使用require 直接引用的包会报错,软链接会从文件原始位置开始查找依赖。

我们希望的是软链可以将其他地方的目录增加到依赖查找路径中。


有兴趣可以去看github 关于软链接引用报错的讨论,这时已经有人提出使用硬链接 github.com/nodejs/node/


我们实验一下

如下图,建立两个文件夹a,b



a/index.js中写入,b中安装qs库

const test = require('qs')
console.log(test)

b 中建立index.js的软链接index-s.js

执行node index-s.js 发现找不到模块

因为软链接中的require软链接会从文件原始位置开始查找依赖,a中没有node_modules,直接报错了,但是如果是硬链接则不存在这样的问题



--preserve-symlinks

最后node官方,增加了--preserve-symlinks来专门处理软链接的引用路径问题。

Node.js 有这样一个选项:–preserve-symlinks,可以设置成按照软链所在的位置查找依赖。

新的问题

–preserve-symlinks 会引发新的问题,但是我查阅了github 的issues,有好几百条的讨论,没有看到有详细解释清楚这个问题的,我现在大概的理解就是node官方对软链接支持的不够好,即使提出了–preserve-symlinks,也有问题,所以pnpm团队不用了。


有兴趣可以看看老外们的讨论
github.com/npm/npm/issu


后来,我在node.js 中文文档里找到着这么一句,但是自己没有验证

使用 --preserve-symlinks 会有其他方面的影响。 比如,如果符号连接的原生模块在依赖树里来自超过一个位置,它们会加载失败。 (Node.js 会将它们视为两个独立的模块,且会试图多次加载模块,造成抛出异常。)

nodeapp.cn/cli.html#



最终作者抛弃了这个方案



通过和大佬们讨论分析后总结出两点原因

  • 如果全部使用软链接,删除一个软链接,导致就找不到解析目录了
  • 如果全部使用软链接,resolve解析文件路径过长,不利于路径解析和文件管理

pnpm 弊端

调试问题

所有项目引用的包都在全局一个地方,如果想对某个包进行调试,其他项目正好引用了,本地运行也会收到影响。

兼容问题

symlink 即软连接的方式可能会在 windows 存在一些兼容的问题,但是针对这个问题,pnpm 也提供了对应的解决方案:在 win 系统上使用一个叫做 junctions 的特性来替代软连接,这个方案在 window 上的兼容性要好于 symlink


我没有windows电脑,没有实验过,这条是从官网挪过来了。

我理解的是window下也是可以使用的,pnpm 已经帮我们做了兼容,只是没有使用软链接的方案。

总结

最后我们再翻译翻译,pnpm 官网的这些话


节省磁盘空间

pnpm通过 hard link(硬连接) 机制,把包都存储在全局的pnpm/store/目录下。当安装软件包时,其包含的所有文件都会硬链接自此位置,而不会占用额外的硬盘空间。pnpm 对于同一个包不同的版本也仅存储其增量改动的部分。


快速

安装包之前,如果已经在全局安装过,就不会被再次下载了,节省了安装时间。随着项目增多,效果会越来越明显。


支持单体仓库

pnpm 提供工作空间workspace能力,就是保证一个仓库内多个项目的package.json有自己生效的范围。这个yarn npm 也支持,不算pnpm的突出点。我对monorepos 也没有研究过,这块等后续有时间了,可以对比三个工具的workspace专题讨论。


严格

pnpm 默认创建了一个非扁平化的 node_modules,因此代码无法访问未声明的包,解决了npm 存在的幽灵依赖问题。


使用 pnpm 替代 yarn/npm,了解 pnpm 原理,降低了 node_modules 体积,并提升了装包以及部署速度,部署时间从 3分钟优化到2.5分钟

待研究的问题

  • pnpm-lock.yaml 文件里的属性和生成过程
  • pnpm 对peerDependencies 的处理
  • 老项目使用yarn 或者npm 如何迁移至pnpm
  • pnpm npm yarn 工作空间workspace的研究
  • Java的meavns是怎么管理依赖包的,和前端有什么区别

Maven是什么? 读音是 ['meɪv(ə)n]或['mevn] ,不要读作“妈文”。

Maven是一款自动化构建工具,专注服务于Java平台的项目构建和依赖管理,相当于前端的npm。

java启动项目,需要在编译器里先配置本地的一个公共路径,公共路径存放全局所有项目依赖的jar 包。

jar包和前端的npm 包原理一样,对于嵌套依赖的问题,maven的处理方式是对所有的jar包扁平化处理,因为存放到了全局一个自定义的目录下, 所以不存在重复下载的问题。但是存在幽灵依赖的问题。 java开发们习以为常,如果严格控制,配置文件会有一堆

java引用包的时候是严格的 ,需要声明包所属公司,包名,包版本,依赖是明确的,可以全部扁平化处理。

但是前端包管理是松散 ,只有一行引用require/import,并不知道具体要引用哪个版本的包,node_modues里有哪个版本引用哪个版本。

所以npm 在扁平化处理的时候,做不到把两个相同包,不同版本全部扁平化,因为会代码导致引用出现紊乱。

npm



2010年的产物,至今存在已经10多年

模块数量和依赖关系日趋复杂化:



  • 模块关系错综复杂,存在重复依赖,小文件很多,浪费磁盘空间并拖慢写入速度。 文件 IO,尤其是海量小文件的读写是非常耗时的。

npm install 过程

总结一下npm install的全过程

首先检查config,获取npm配置,项目级的.nmprc > 用户级的 .npmrcc > 全局.npmrc > npm 内置的.npmrc



npm install先检测是有package-lock.json文件:

  • 没有package-lock.json文件
    • 根据pacakage.json 递归构建依赖树,分析依赖关系,这是因为我们可能包会依赖其他的包,并且多个包之间会产生相同依赖的情况;
    • 下载时会检查是否有相关缓存
      • 有,将完整的内容解压到nodee_modules
      • 没有,从registry仓库中下载压缩包(如果我们设置了镜像,那么会从镜像服务器下载压缩包);
        • 检查包的完整性
        • 获取到压缩包后会对压缩包进行缓存( 从npm5开始有的, npm config get cache 可以查看地址 )
        • 将压缩包解压到项目的node_modules文件夹中
    • 最后生成package-lock.json文件
  • 有package-lock.json文件
    • 检测lock中包的版本是否和package.json中一致
      • 不一致,那么会重新构建依赖关系,直接会走上面的流程;
    • 一致的情况下,会去优先查找缓存
      • 缓存没有找到,从registry仓库下载,直接走上面流程;
      • 命中缓存会获取缓存中的压缩文件
    • 将压缩文件解压到node_modules文件夹中;




使用 npm i --timing=true --loglevel=verbose 命令可以看到 npm install 的完整过程

package-lock.json

package-lock.json 的作用是锁定依赖结构,即只要你目录下有 package-lock.json 文件,那么你每次执行 npm install 后生成的 node_modules 目录结构一定是完全相同的。



  • name:项目的名称;
  • version:项目的版本;
  • lockfileVersion:lock文件的版本;
  • requires:使用requires来跟踪模块的依赖关系;
  • dependencies:项目的依赖
    • version表示实际安装的版本;
    • resolved用来记录下载的地址,registry仓库中的位置;
    • requires记录当前模块的依赖;
    • integrity用来从缓存中获取索引,再通过索引去获取压缩包文件

npm 缓存机制

npm config get cache 

打开 /Users/yinzhuo/.npm/_cacache

  • content-v2

存放二进制文件,其实是npm tar包资源

  • index-v5

是content-v2的文件索引信息


npm 在执行安装时,可以根据 package-lock.json 中存储的 integrity、version、name 生成一个唯一的 key 对应到 index-v5 目录下的缓存记录,从而找到 tar包的 hash,然后根据 hash 再去找缓存的 tar包直接使用。


"lodash": {
  "version": "4.17.11",
  "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
  "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
}

包的完整性



例如我们执行 npm info 命令,shasum值就是hash值,用户下载依赖包到本地后,需要确定在下载过程中没有出现错误,所以在下载完成之后需要在本地在计算一次文件的 hash 值,如果两个 hash 值是相同的,则确保下载的依赖是完整的,如果不同,则进行重新下载。

yarn



概述

yarn 是在 2016 年发布的,那时 npm 还处于 V3 时期,那时候还没有 package-lock.json 文件,就像上面我们提到的:不稳定性、安装速度慢等缺点经常会受到广大开发者吐槽。此时,yarn 诞生:

Yarn 被定义为快速、安全、可靠的依赖管理。

  • Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。
  • 在执行代码之前,Yarn 会通过算法校验每个安装包的完整性。
  • 使用详细、简洁的锁文件格式和明确的安装算法,Yarn 能够保证在不同系统上无差异的工作。

优势

  • 离线模式

如果你以前安装过某个包,再次安装时可以在没有任何互联网连接的情况下进行。

(本地有缓存,优先取缓存)

  • 确定性

不管安装顺序如何,相同的依赖关系将在每台机器上以相同的方式安装。

(lock.file控制)

  • 网络性能

Yarn 有效地对请求进行排队处理,避免发起的请求如瀑布般倾泻,以便最大限度地利用网络资源。

(可并行请求,可通过--network-concurrency 自定义上限,维护了一个请求队列)

  • 相同的软件包

从 npm 安装软件包并保持相同的包管理流程。

(迁移简单)

  • 网络弹性

重试机制确保单个请求失败并不会导致整个安装失败。

(自动重发策略(请求失败尝试重发,单请求重发上限为5次),实现思路为维护一个离线队列存放失败未重发请求,每 3s 出队一个进行重发。当超过重发上限,输出“info There appears to be trouble with your network connection. Retrying...”提示,表示放弃该请求。)

  • 扁平模式

将依赖包的不同版本归结为单个版本,以避免创建多个副本。

(扁平化处理)

lockfile

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

yarn.lock锁文件把所有的依赖包都扁平化的展示了出来,对于同名包但是semver不兼容的作为不同的字段放在了yarn.lock的同一级结构中。

package-1@^1.0.0:
  version "1.0.3"
  resolved "https://registry.npmjs.org/package-1/-/package-1-1.0.3.tgz#a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
package-2@^2.0.0:
  version "2.0.1"
  resolved "https://registry.npmjs.org/package-2/-/package-2-2.0.1.tgz#a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
  dependencies:
    package-4 "^4.0.0"
package-3@^3.0.0:
  version "3.1.9"