作者:宋永杰
对于包管理器,不同语言其实都有自己的包管理器,比如:Python/Rust有自己的包管理器(pip/cargo),还有如rpm、maven等。
同样在现代前端开发中,bower、npm、yarn、cnpm、pnpm等各种包管理器,简化了资源引用的依赖关系,提升了我们的开发效率。本文将从包管理器的发展史和当下主流的三种工具:npm、yarn和pnpm来做一个全方位的分析和对比,探讨各自优点和适用场景。
nodejs诞生之前,我们想要引用一些三方资源库,比如jquery,经常使用以下方式:
随之就会出现版本管理混乱、项目文件过大、cdn资源失效、依赖升级等各种问题。
随着nodejs的爆火和模块化概念的诞生,npm出现了,最初npm只是用于服务端nodejs的包管理器,随着前端社区的不断发展,npm也使用在了客户端开发中。
那么当包管理工具出现后,是怎么逐步解决上述问题呢?这就得从它的发展史聊起了
2011年7月,npm发布了1.0版本。当时的node_modules文件夹是什么样子呢?
node_modules
└─ foo
├─ index.js
├─ package.json
└─ node_modules
└─ bar
├─ index.js
└─ package.json
为了解决上述问题,npm团队认真思考了node_modules的结构,并提出了扁平化的策略,就是把嵌套过深的层级,通过扁平化的方式,将依赖包进行提升,使嵌套层级尽可能的变少。在npm v3阶段,node_modules的结构如下:
node_modules
├─ foo
| ├─ index.js
| └─ package.json
└─ bar
├─ index.js
└─ package.json
虽然通过扁平化策略,确实减少了部分嵌套依赖太深和重复安装的问题,为什么说是解决部分问题呢?看下图:
yarn的出现,可以说是从根本上解决了npm存在的很多问题,比如资源一致性、安装速度慢等问题。
yarn的出现,我觉着最大的贡献就是推出了yarn.lock,来解决依赖版本错乱的问题,npm在一年后的npm@5也跟上yarn的脚步,推出了package-lock.json。
// 在一个项目中存在如下依赖:
node_modules
├─ htmlparser2@^3.10.1
| ├─ entities@^1.1.1
└─ dom-serializer@^0.2.2
| ├─ entities@^2.0.0
└─ entities@^2.1.0
通过npm install安装依赖后,生成的package-lock.json和node_modules结构如下:
通过yarn安装依赖后,生成的yarn.lock和node_modules结构如下:
对比可以看到:
SemVer 是指语义版本规范(Semantic Versioning),用来约定包版本格式。它由三部分组成:主版本号、次版本号和修订版本号。
实际上,yarn本质上还是在下载npm包,只是针对npm v3中的痛点,针对性的做了优化:
社区里有人针对yarn和npm的性能做了对比(来源于:github.com/appleboy/npm-vs-yarn):
npm install | npm ci | yarn | |
---|---|---|---|
install without cache (without node_modules) | 3m | 3m | 1m |
install with cache (without node_modules) | 1m | 18s | 30s |
install with cache (with node_modules) | 54s | 21s | 2s |
install without internet (with node_modules) | - | - | 2s |
pnpm项目建立的初衷:
在使用npm进行依赖安装的时候,不同项目有相同依赖的时候,都会被重复安装。在使用 pnpm 时,依赖会被存储在内容可寻址的存储仓库(store)中,采用store + hardLink的方式:
上面提到过,pnpm采用store + hardLink的方式进行依赖的管理和安装。
使用 npm 或 Yarn 安装依赖项时,所有的包都被提升到模块目录的根目录。这样就导致了一个问题,源码可以直接访问和修改依赖,而不是作为只读的项目依赖。
首先来看下pnpm是怎么解决嵌套依赖问题的:
-> - a symlink (or junction on Windows)
node_modules
├─ foo -> .registry.npmjs.org/foo/1.0.0/node_modules/foo
└─ .registry.npmjs.org
├─ foo/1.0.0/node_modules
| ├─ bar -> ../../bar/2.0.0/node_modules/bar
| └─ foo
| ├─ index.js
| └─ package.json
└─ bar/2.0.0/node_modules
└─ bar
├─ index.js
└─ package.json
在pnpm 创建的node_modules文件夹中,所有包都有自己的依赖项分组在一起,但目录树永远不会像 npm@2 那样深。pnpm 保持所有依赖关系平坦,但使用符号链接将它们分组在一起。
以下是来自官网的描述:
npm、yarn和pnpm都是当下十分优秀的包管理工具,具体选择哪个,还是要根据团队项目情况和个人喜好来决定。npm 是 Node.js 生态系统的一部分,yarn 提供了更快的依赖项安装和锁定文件功能,而 pnpm 则专注于减少磁盘空间的使用和安装时间。
附上三者在一些功能上的比较(https://pnpm.io/zh/feature-comparison):
功能 | pnpm | Yarn | npm |
---|---|---|---|
工作空间支持(monorepo) | ✔️ | ✔️ | ✔️ |
隔离的 node_modules | ✔️ - 默认 | ✔️ | ✔️ |
提升的 node_modules | ✔️ | ✔️ | ✔️ - 默认 |
自动安装 peers | ✔️ | ❌ | ✔️ |
Plug'n'Play | ✔️ | ✔️ - 默认 | ❌ |
零安装 | ❌ | ✔️ | ❌ |
修补依赖项 | ✔️ | ✔️ | ❌ |
管理 Node.js 版本 | ✔️ | ❌ | ❌ |
有锁文件 | ✔️ - pnpm-lock.yaml | ✔️ - yarn.lock | ✔️ - package-lock.json |
支持覆盖 | ✔️ | ✔️ - 通过 resolutions | ✔️ |
内容可寻址存储 | ✔️ | ❌ | ❌ |
动态包执行 | ✔️ - 通过 pnpm dlx | ✔️ - 通过 yarn dlx | ✔️ - 通过 npx |
辅助缓存 | ✔️ | ❌ | ❌ |
列出许可证 | ✔️ - 通过 pnpm licenses list | ✔️ - 通过插件 | ❌ |