本次分享不会包含使用方式,如感兴趣可以自行查看
#简介
前端的包管理工具相信大家一定不会陌生,因为每天都需要跟他打交道,新项目或者刚拉下来的前端项目都需要去 install
依赖进行包的依赖安装,大家最熟悉的应该就是 npm
了,或者国内的 npm
镜像包 cnpm
,大家熟称为淘宝镜像
但是现在,npm
已经是前端家喻户晓的存在了,为什么还会出现诸如 cnpm
Yarn
pnpm
Yarn2
等等...今天就让我带大家一起一探究竟,为什么已经出现如此之久的 npm
还会有重复造轮子的包管理呢?
#包管理工具的功能
- 处理和编写元数据
- 批量安装或更新所有依赖项
- 添加、更新和删除依赖项
- 运行脚本
- 发布软件包
- 进行安全审查
#简史
第一个发布的软件包管理器是 npm
,早在 2010 年就已经存在了。它确立了如今包管理的核心,在前端包管理工具相当于是一种标准了。
如今 npm
已经存在 12 年了,为什么还有其他替代品?
- node_modules 不同的依赖解析算法,嵌套 VS 扁平化
- 不同的安全问题
- 不同的锁文件格式,有性能影响
- 在磁盘上存储包的不同方式,对磁盘空间有效率影响
- 对多包(单一代码库)项目的不同支持
- 不同程度的可配置性和灵活性
#主流包管理器
- npm
- Yarn
- pnpm
- Yarn Berry (还未发布)
#npm
npm 是包管理器的祖先。许多人错误地认为 npm 是 “Node 包管理器” 的首字母缩写,但事实并非如此。尽管如此,它与 Node.js 运行时捆绑在一起。
在 npm 之前,项目依赖都是手动下载和管理的。
npm 引入了一些概念:
- package.json 文件
- 元数据字段(例如,devDependencies)
- node_modules 中存储依赖项
- 自定义脚本
- 公共和私有包注册
在 node_modules 中存储依赖项、自定义脚本、公共和私有包注册等概念都是 npm 引入的
#Yarn (v1)
Yarn 是 Facebook 宣布与谷歌和其他一些公司开发新的软件包管理器,主要解决 npm 当时存在的一致性、安全性和性能问题,他们命名为 Yarn
Yarn 的架构设计建立在 npm 许多概念和流程之上,Yarn 在最初的发布中对包管理器产生了重大影响。Yarn 在安装依赖的过程中采用了并行安装,这是 npm 当时的一大痛点
Yarn 还发明了自己的许多概念,例如:
- 原生
monorpo
支持 - 缓存感知安装
- 离线缓存
- 锁文件
目前 Yarn 的热度在包管理器的热度上也是数一数二了。
#特点
- 速度快
Yarn
缓存了每个下载过的包,所以再次使用时无需重复下载- 同时利用并行下载以最大化资源利用率,因此安装速度更快
- 安全
- 在执行代码之前,
Yarn
会通过算法校验每个安装包的完整性
- 可靠
- 使用详细、简洁的锁文件格式和明确的安装算法,Yarn 能够保证在不同系统上无差异的工作
#创新性
- 离线模式
- 如果你以前安装过某个包,再次安装时可以在没有任何互联网连接的情况下进行。
- 确定性
- 不管安装顺序如何,相同的依赖关系将在每台机器上以相同的方式安装
- 网络性能
- Yarn 有效地对请求进行排队处理,避免发起的请求如瀑布般倾泻,以便最大限度地利用网络资源
- 相同的软件包
- 从 npm 安装软件包并保持相同的包管理流程
- 网络弹性
- 重试机制确保单个请求失败并不会导致整个安装失败
- 扁平模式
- 将依赖包的不同版本归结为单个版本,以避免创建多个副本
#pnpm
pnpm 是一个比较新颖的包管理工具。它和 Yarn 一样,是为了解决某些 npm 痛点的。可以说是 npm 的替换,如果你现在的项目是 npm 项目,那么可以直接使用 Pnpm
Pnpm 的出现是为了解决 Yarn 的问题,因为 Yarn 不解决例如磁盘占用的问题以及内部的发展不公开等原因,所以就自己去开发了一个,目前在使用体验上要比 Yarn 好一些而且解决了一些 Yarn 目前存在的问题以及痛点,感兴趣的同学可以看看原文。Why should we use pnpm?
虽然 Yarn 的速度优于 npm,但是它使用了相同的依赖解析方法
现在的前端项目越来越庞大,复杂。很多时候有成百上千的依赖包,每次安装都需要一定的时间,并且大量浪费磁盘空间。
pnpm 引入了一种替代的依赖解析策略:内容寻址存储。
这个方式导致你的 node_modules 文件夹里面的依赖包都将存储在 ~/.pnpm-store/
下。每个依赖包的版本在该文件夹中只存储一次,构成唯一来源,这样的话将会节省相当多的磁盘空间。
这是通过 node_modules
层实现的,使用符号链接创建一个嵌套的依赖关系结构,其中文件夹中的每个包都是到存储的硬链接。
这是为什么 pnpm
会在快速和磁盘效率上有大幅提升的原因。
#它是如何工作的?
官网介绍
- 如果依赖于依赖项的不同版本,则只有不同的文件才会添加到存储区。例如,如果它有100个文件,而一个新版本只在其中一个文件中有更改,pnpm update 将只向存储中添加一个新文件,而不是为了这个单一的更改而克隆整个依赖。
- 所有的文件都保存在磁盘上的一个地方。安装包时,它们的文件将从该位置硬链接,不消耗额外的磁盘空间。这允许您在项目之间共享相同版本的依赖项。
由于这种依赖关系的链接,它也比它的替代品快 2 倍。通过使用这项技术和一些真正高性能的缓存解决方案,您可以在眨眼之间安装包
#Yarn Berry
Yarn 2
也称为 Yarn Berry
,2020 年 1 月发布,据称是对 Yarn
的重大升级。它本质上是一个新的包管理器,新的代码基础和新原则,所以称为 Yarn Berry
。
Yarn Berry 太激进了,所以我们只简单讨论一些吧,感兴趣的同学可以自己去看看,目前最新版本已经到 3 了
Yarn Berry
的主要创新是 PnP(Plug'n'Play,即插即用 ),这种方法是修复 node_modules 的策略而产生的。相当于抛弃了 node_modules
原生 node 的查找依赖方式是向上级目录层层递归遍历 node_modules 文件夹,虽然,现有的包管理版本都已经做到了依赖提升,让依赖项尽量扁平化,但当碰到包依赖版本不匹配的时候,仍然会存在嵌套目录。 而 PnP,它记录了依赖的准群硬盘位置,可以在查找依赖时减少硬盘读写,同时,可以做到所有依赖项完全扁平化。
本质上,就是将你的依赖项通过下载并解析成 zip 的形式放到你的 .yarn/cache
目录下,通过提交源码将当前所有的 zip 文件上传,然后当其他团队成员在 down
代码的时候直接可以运行项目而不需要特意去安装。
#安装 Yarn Berry
因为 Yarn berry
比较特殊,需要通过当前目录进行安装,而不是作为一个全局管理,类似于只安装当前文件内
#升级 Yarn 2 或以上版本
代码语言:javascript复制// yarn 版本在 1.22
yarn set version berry
// 安装 react
yarn add react
依赖结构如下:
代码语言:javascript复制.
├── .pnp.cjs
├── .yarn
│ ├── .DS_Store
│ ├── cache
│ │ ├── .gitignore
│ │ ├── js-tokens-npm-4.0.0-0ac852e9e2-8a95213a5a.zip
│ │ ├── loose-envify-npm-1.4.0-6307b72ccf-6517e24e0c.zip
│ │ ├── object-assign-npm-4.1.1-1004ad6dec-fcc6e4ea8c.zip
│ │ └── react-npm-17.0.2-99ba37d931-b254cc17ce.zip
│ ├── install-state.gz
│ └── releases
│ └── yarn-berry.js
├── .yarnrc.yml
├── package.json
└── yarn.lock
依赖大小
使用 create-react-app
默认依赖进行对比
#Npm
#Yarn Berry
#Pnpm
可以发现依赖包的大小为 npm > Yarn Berry > Pnpm
Yarn Berry
主要是将依赖下载成 zip 形式存储,但是 Node 无法解析 zip 格式的依赖包,所以使用了 .pnp.js 来维护映射关系,我们将 Yarn Berry
生成的所有依赖可以直接上传到 git 上,其他成员拉下代码后,即可直接运行,实现 Zero Install
#对比- 玄学
- 依赖管理
- 安全
- 速度
#1. 玄学
不知道大家在使用过 npm
或者 Yarn
的时候有没有一种感受,就是在输入 Yarn install
或者 Yarn
的时候,要比 npm
更加舒适?
由于 n p m
三个字母都在键盘的右侧区域,所以正常打字的话可能需要一只手去输入。所以这就造成了 npm
的复杂度是 O(n),而 Yarn
的复杂度为 O(log n),虽然多了一个字母,但是分别在左右手区域各两个,所以在输入时更加顺畅,你自己在输入 np
的时候,你就已经可以把 yarn
输入完成了。
所以 pnpm 在输入上更略逊一筹,比 npm 还要难以输入...
#2. 依赖管理
#依赖结构
安装依赖时的原理:
- 将依赖包的版本区间解析为某个具体的版本号
- 下载对应版本依赖的 tar 包到本地离线镜像
- 将依赖从离线镜像解压到本地缓存
- 将依赖从缓存拷贝到当前目录的 node_modules 目录
#npm
- 在
npm v1
npm v2
版本中,依赖包的管理是树结构嵌套组成的
node_modules
└─ foo
├─ index.js
├─ package.json
└─ node_modules
└─ bar
├─ index.js
└─ package.json
如果出现大量重复的包,将重复安装多个,会导致 node_modules
非常巨大,形成嵌套地狱。与我们之前在 JS 内写回调地狱
类似
v3
版本之后使用扁平化管理
#Yarn
- 默认使用扁平化依赖管理
安装一个 React
,发现在 node_modules 目录内有其他包文件
.
└── node_modules
├── js-tokens
├── loose-envify
├── object-assign
└── react
├── cjs
├── node_modules
└── umd
将 React
需要的依赖都给打平到 node_modules
目录内,这个方案解决了嵌套地狱的问题,根据 node require
机制会不停的往上级 node_modules
去寻找,找到了就不会安装,解决了大量包重复安装的问题。
虽然解决了,但是扁平化的处理方式还存在一些问题。
- 模块可以访问他们并不依赖的包
- 平展依赖树的算法非常复杂
- 一些软件包在一个项目中被复制 node_modules 模块/文件夹
#pnpm
pnpm 会创建"奇怪"的 node_modules 结构
pnpm 解决的不是平铺目录所带来的问题,而是解决 npm v3
版本之前的树结构的依赖问题
我们先创建两个目录进行比较,先建立一个 npm 的包管理项目,然后在建立一个 pnpm 包管理项目
代码语言:javascript复制npm init
npm install react
然后看一下 npm/node_modules
里面的内容
.
├── js-tokens
├── loose-envify
├── object-assign
└── react
如上所述,建立了平铺的结构,其他我们不认识的依赖都是 React 本身的依赖,被打平在这儿
我们在继续在 pnpm 里进行操作
代码语言:javascript复制pnpm init
pnpm install
代码语言:javascript复制├── .pnpm
└── react -> .pnpm/react@17.0.2/node_modules/react
我们发现,除了一个我们不认识的 .pnpm
文件夹,只有一个 react
目录。 那么所有的次级依赖去哪里了呢?就在 .pnpm
的文件夹里面,我们打开后可以看到所有的依赖(包括依赖的依赖)都在 .pnpm
文件夹内,所以 react
是唯一一个你的应用必须拥有访问权限的包。
外面的 可以看到 react
是一个符号链接指向了它的真实位置
react
包的真实位置在 /node_modules/.pnpm/react@17.0.2/node_modules/react
所有你安装的依赖都存在 .pnpm/<name>@<version>/node_modules/<name>
,官方称它为虚拟存储目录
看一下 react
真实的位置内容
.pnpm/react@17.0.2/node_modules/react/node_modules
里面没有 node_modules
目录,那么 react
的依赖去哪里了?
其实 react
的所有依赖都被软链到了 node_modules/.pnpm/
中的对应目录了,这样将所有依赖放置同一级别可以避免循环的软链
#对比一下 Npm / Yarn / Pnpm 工作机制
画了一张略微有点乱的图
#3. 安全
npm / yarn
的扁平依赖结构,有一个非常严重的问题就是可以非法访问未声明的包
举个