包管理器: 包管理器或包管理系统是一系列软件工具的集合, 这些软件工具用和电脑操作系统一致的方式, 使应用的安装, 升级, 配置和删除软件包的过程自动化, 它通常维护一个数据库软件的依赖和版本信息, 防止软件不匹配和无法跟踪 --维基百科
当我们知道上面👆所说的包管理器的含义,那么我们聚焦于前端领域,看看前端的包管理器以及工具:
1、常用包管理器
目前,常用的包管理工具有 npm
/yarn
/pnpm
三种:
- npm
npm
是由 node.js 官方推出的包管理器,于 2010 年首次发布,旨在解决 node.js 项目中的依赖管理问题,后续也被前端项目所广泛使用 - yarn
yarn
是由于 2016 年推出的另一个包管理工具,为了解决当时 npm 的一些性能以及稳定性问题,使用一种全新的算法来优化依赖关系的解析和安装流程。即将当时 npm 的node_modules
嵌套改为平铺(目前 npm 也是平铺策略) - pnpm
pnpm
采用了一种全新的依赖解决方案,它使用硬链接和符号链接 结合方式显著减小了硬盘空间的占用。最简单的一个示例是:当你拥有 100 个依赖lodash
的项目,使用 pnpm 磁盘将仅仅占用一份lodash
多的体积大小
三者功能对比:链接
Feature Comparison
Feature | pnpm | Yarn | npm |
---|---|---|---|
Workspace support | ✔️ | ✔️ | ✔️ |
Isolated node_modules |
✔️ - The default | ✔️ | ✔️ |
Hoisted node_modules |
✔️ | ✔️ | ✔️ - The default |
Autoinstalling peers | ✔️ | ❌ | ✔️ |
Plug'n'Play | ✔️ | ✔️ - The default | ❌ |
Zero-Installs | ❌ | ✔️ | ❌ |
Patching dependencies | ✔️ | ✔️ | ❌ |
Managing Node.js versions | ✔️ | ❌ | ❌ |
Has a lockfile | ✔️ - pnpm-lock.yaml |
✔️ - yarn.lock |
✔️ - package-lock.json |
Overrides support | ✔️ | ✔️ - Via resolutions | ✔️ |
Content-addressable storage | ✔️ | ❌ | ❌ |
Dynamic package execution | ✔️ - Via pnpm dlx |
✔️ - Via yarn dlx |
✔️ - Via npx |
Side-effects cache | ✔️ | ❌ | ❌ |
Listing licenses | ✔️ - Via pnpm licenses list |
✔️ - Via a plugin | ❌ |
性能对比 链接
2、模块解析时resolve算法
为了保证模块的正确加载,实现了额外的依赖查找算法resolve算法。比如:当我们引入一个模块时,我们将会在当前路径 的 node_modules
中寻找该 package,如果找不到则递归上级目录 的 node_modules
寻找,直至根路径。
ini
const lodash = require('lodash')
正是因为有了 resolve
算法,不管我们使用的 npm/yarn 目前的 node_modules 扁平安装方式,还是 pnpm 的软硬链接结合方式,装包所需要做到的就是在符合 resolve
算法的同时,使得安装速度加快,安装体积减小。
除此之外monorepo 也正是基于 resolve
算法,使其解决如何依赖其它其它子包,以及如何减少整体安装体积。
3、项目中存在多个lockfile,怎么办?
如果一个项目中同时存在多个 lockfile,则表明该项目包管理器管理混乱,此时应该只保留正确包管理器的 lockfile
,并将其它包管理器对应的 lockfile
置于 .gitignore
中
那此时如何确定正确的包管理器呢?此时可以通过以下操作来确定:
- 比较多个 lockfile 的上次修改时间(lastModified),以最后修改为准
- 查看是否有 CI/CD,如果有跟着 CI/CD 中的包管理工具确认
- 查看是否有 Dockerfile,如果有跟着 Dockerfile 确认
- 查看是否有文档,如果有跟着文档走
除此可以借助antfu开源的 @antfu/ni:
shell
npm i -g @antfu/ni
如果您的项目中同时存在多个锁文件,@antfu/ni
将按照以下优先级进行选择:Yarn > pnpm > npm > bun。也就是说,如果同时存在 yarn.lock
和 package-lock.json
,@antfu/ni
将优先选择 Yarn
4、npm link 原理解析
npm link
可以帮助我们模拟包安装后的状态,它会在系统中做一个快捷方式映射,让本地的包就好像install过一样,可以直接使用,这样就便于我们本地开发时调试未发布的包!
由于 yarn/npm link 的原理相同,此处使用 yarn link
软连接rollup 为例说明使用方式以及原理。
- 在
rollup
源码目录,通过npm run watch
进行构建,此时会生成带有source-map
的构建文件。 - 在
rollup
源码目录,执行yarn link
,它会自动寻找当前目录的package.json
中的name
字段,并创建全局目录(~/.config/yarn/link
)软链接至该项目 - 在自己项目,执行
yarn link rollup
,将会替换node_modules/rollup
,其软链接至全局目录
简单来说yarn/npm link
的原理:
1、yarn link
:将当前 package 软链接至 ~/.config/yarn/link
,其名为 package 的名称,即 package.json
中的 name
字段
2、yarn link rollup
:将当前项目,即需调试项目目录中的 node_modulels/rollup
软链接到 ~/.config/yarn/link/rollup
5、node_modules 拓扑结构
1、 npm v2: 嵌套结构
直接依赖会平铺在 node_modules 下,子依赖嵌套在直接依赖的 node_modules 中。
比如项目依赖了A 和 C,而 A 和 C 依赖了不同版本的 B@1.0 和 B@2.0,node_modules 结构如下:
kotlin
node_modules
├── A@1.0.0
│ └── node_modules
│ └── B@1.0.0
└── C@1.0.0
└── node_modules
└── B@2.0.0
如果 D 也依赖 B@1.0,会生成如下的嵌套结构:
kotlin
node_modules
├── A@1.0.0
│ └── node_modules
│ └── B@1.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── D@1.0.0
└── node_modules
└── B@1.0.0
可以看到同版本的 B 分别被 A 和 D 安装了两次。
2、npm v3: 平铺结构
为了将嵌套的依赖尽量打平,避免过深的依赖树和包冗余,npm v3 将子依赖「提升」(hoist),采用扁平的 node_modules 结构,子依赖会尽量平铺安装在主依赖项所在的目录中。
kotlin
node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0
└── node_modules
└── B@2.0.0
可以看到 A 的子依赖的 B@1.0 不再放在 A 的 node_modules 下了,而是与 A 同层级。
而 C 依赖的 B@2.0 因为版本号原因还是嵌套在 C 的 node_modules 下。
这样不会造成大量包的重复安装,依赖的层级也不会太深,解决了依赖地狱问题,但也形成了新的问题(幽灵依赖)。
比如上方的示例其实我们只安装了 A 和 C:
css
{
"dependencies": {
"A": "^1.0.0",
"C": "^1.0.0"
}
}
由于 B 在安装时被提升到了和 A 同样的层级,所以在项目中引用 B 还是能正常工作的
6、'臭名昭著'的幽灵依赖
如果一个npm包没有在package.json
中声明而直接被项目所依赖,那么这个 npm 包就是幽灵依赖(Phantom Denendency)
比如下面这个场景:
如果使用 npm/yarn 来作为包管理器时,那我们可以在项目中直接使用 loose-envify
而无需手动 npm install
。假设此时项目中需要使用到该 npm 包,但没有手动安装。
ini
const envify = require('loose-envify')
因为对于 npm/yarn 而言,它会将 loose-envify
该依赖提升至 node_modules/loose-envify
目录下,根据 npm 包进行 resolve 的算法而言,此时可以直接使用它。
但是随着时间的推移,假设 React
发布了 20.0.0
版本,并在内部实现中废弃掉了 loose-envify
,但此时我们的项目因为依赖 loose-envify
而又无法找到,因此报错。
注:React 在 17 版本时依赖 object-assign 与 loose-envify 两个直接依赖,但是在 React 18 版本废弃掉了 object-assign 依赖
1、它会导致什么问题
简单来说:
- 依赖缺失
- 兼容性问题
- 增加项目的体积
2、解决措施
1、 pnpm
解决了该问题,它的 node_modules
目录中仅包含 package.json
声明的 npm 包
pnpm三层寻址的设计:
第一层处理直接依赖关系。第一层寻找依赖是 nodejs 或 webpack 等运行环境/打包工具进行的,他们在 node_modules 文件夹寻找依赖,并遵循就近原则。所以第一层依赖文件势必写在node_modules/package-a下,一方面遵循依赖寻找路径,一方面没有将依赖都拎到上级目录,也没有将依赖打平,目的就是还原最语义化的 package.json。同时每个包的子依赖也从该包内寻找,解决了多版本管理的问题,同时也使 node_modules 拥有一个稳定的结构,即该目录组织算法仅与 package.json 定义有关,而与包安装顺序无关。
第二层处理符号链接依赖项。解决 npm@2.x 设计带来的问题,主要是包复用的问题。利用软链接解决了代码重复引用的问题。相比 npm@3 将包打平的设计,软链接可以保持包结构的稳定,同时用文件指针解决重复占用硬盘空间的问题。
bash
(bar 将被符号链接到 foo@1.0.0/node_modules 文件夹)
node_modules
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
└── foo@1.0.0
└── node_modules
├── foo -> <store>/foo
└── bar -> ../../bar@1.0.0/node_modules/bar
第三层是硬链接寻址,脱离当前项目路径指向一个全局统一管理路径,这正是跨项目复用的必然选择,解决了多个项目对于同一个包的多份拷贝过于浪费问题。
2、但还有一种更难以解决的幻影依赖问题,即用户在 Monorepo 项目根目录安装了某个包,这npx/dlx个包可能被某个子 Package 内的代码寻址到,要彻底解决这个问题,需要配合使用 Rush,在工程上通过依赖问题检测来彻底解决。
原文链接:blog.csdn.net/qq_45225759...
7、npx/dlx
简而言之:
npx
是node package execute
的意思,dlx
是download and execute
- npx: 可以通过
npx
直接在命令行执行项目中命令,若不存在则安装它
除了 npm
,在 pnpm/yarn
上也有对应的工具,如下所示:
1、 npx
,实际上是 node package execute
的简写 2、 yarn dlx
3、 pnpx
,实际上是 pnpm dlx
的简写
基本功能也是一样:
1、直接执行 node_modules/.bin
递归目录(如果当前 node_modules/.bin 目录下无法找到,则去上一级 node_modules/.bin 目录下寻找)下的可执行文件 2、直接执行全局 npm 命令行工具,不存在则下载
那当它们不存在则下载时,下载了哪里,又是如何管理全局命令行工具的?
- 对于
npx
而言,如果该 npm 包不存在,则会下载到 npx 全局目录~/.npm/``._npx
,而不会直接下载到全局可执行目录污染$PATH
。
或者这样理解:
npx 首先会执行 ./node_packages/.bin
下面的命令,如果没有找到才会去下载一个 npm package 并执行。
这会造成一个问题,如果你本地有这个 package,那么执行的命令可能不是最新的版本
所以 yarn 和 pnpm 将这个命令拆分成两个,一个是执行本地,另一个是下载最新的package并执行:
yarn exec
,yarn dlx
pnpm exec
,pnpm dlx