理解前端包管理器的基本原理。
包管理器
前端项目通常会依赖大量第三方包,比如框架、构建工具、请求库、组件库等。
包管理器的作用就是帮助我们管理这些依赖,包括:
- 安装依赖。
- 卸载依赖。
- 记录依赖版本。
- 管理依赖之间的关系。
- 保证不同环境下安装结果尽量一致。
package.json
package.json: 用来描述当前项目的基本信息、依赖信息和脚本命令。
常见字段如下:
| 字段 | 作用 |
|---|---|
name | 包名,通常使用英文、小写字母和连字符 |
version | 当前包的版本号 |
description | 包的描述信息 |
homepage | 项目主页地址 |
author | 包的作者信息 |
repository | 代码仓库地址 |
main | 包的入口文件 |
keywords | 搜索关键字 |
dependencies | 生产环境依赖 |
devDependencies | 开发环境依赖 |
版本号规则
依赖通常使用语义化版本号,格式是:
主版本号.次版本号.补丁版本号
比如:
1.2.3
三个数字分别表示:
| 位置 | 含义 | 说明 |
|---|---|---|
| 主版本号 | Major | 重大更新,可能包含不兼容变更 |
| 次版本号 | Minor | 新增功能,一般保持向下兼容 |
| 补丁版本号 | Patch | Bug 修复、性能优化、安全补丁 |
一般来说:
1.0.0 -> 2.0.0:主版本更新,可能有破坏性变更。1.2.0 -> 1.3.0:次版本更新,通常是新增功能。1.2.3 -> 1.2.4:补丁版本更新,通常是问题修复。
版本范围
在 package.json 中,依赖版本不一定只能写固定版本,也可以写版本范围。
常见写法如下:
| 写法 | 含义 |
|---|---|
1.0.0 | 锁定具体版本,只安装 1.0.0 |
>=1.0.0 | 安装大于等于 1.0.0 的版本 |
1.0.0 - 2.0.0 | 安装指定范围内的版本 |
1.2.x | 主版本是 1,次版本是 2,补丁版本任意 |
~1.2.3 | 允许补丁版本更新,范围是 >=1.2.3 <1.3.0 |
^1.2.3 | 允许次版本和补丁版本更新,范围是 >=1.2.3 <2.0.0 |
* | 使用最新发布版本 |
最常见的是 ^ 和 ~。
~更保守,只允许补丁版本变化。^更宽松,允许次版本和补丁版本变化。
比如:
{
"dependencies": {
"axios": "^1.6.0",
"lodash": "~4.17.21"
}
}
axios 可以安装 1.x.x 中符合条件的新版本,但不会自动升级到 2.0.0。
lodash 只能在 4.17.x 范围内更新。
dependencies 和 devDependencies
依赖通常分为两类:
| 类型 | 作用 | 示例 |
|---|---|---|
dependencies | 项目运行时需要的依赖 | vue、react、axios |
devDependencies | 开发、构建、测试阶段需要的依赖 | vite、webpack、eslint |
安装生产依赖:
npm install axios
npm install --save axios
npm install -S axios
安装开发依赖:
npm install vite -D
npm install vite --save-dev
现在的 npm install axios 默认就会写入 dependencies,所以通常不需要额外写 --save。
模块查找机制
在代码中引入模块时,如果路径不是以 /、./、../ 开头,Node.js 会把它当作一个包来查找。
例如:
import axios from 'axios'
查找流程大致是:
- 先在当前目录的
node_modules中查找axios。 - 如果没找到,就向上一级目录继续查找
node_modules。 - 一直查找到项目根目录或系统根目录。
- 如果始终找不到,就抛出模块不存在的错误。
包入口文件
当我们引入一个包时,包管理器和模块系统需要知道这个包的入口文件在哪里。
一般会先读取这个包自己的 package.json:
{
"main": "dist/index.js"
}
如果存在 main 字段,就使用 main 指定的文件作为入口。
如果没有 main 字段,通常会默认查找包目录下的 index.js。
npm 的历史问题
依赖嵌套过深
早期 npm 会按照依赖关系进行嵌套安装,目录结构可能会变得很深:
node_modules
└── a
└── node_modules
└── b
└── node_modules
└── c
这种结构会带来几个问题:
- 目录层级过深。
- 相同依赖可能被重复安装多次。
node_modules体积变大。
安装性能问题
早期 npm 的安装过程较慢,主要原因包括:
- 串行下载依赖。
- 重复下载相同包。
- 缓存机制不够完善。
- 安装日志较多,错误信息不够清晰。
版本一致性问题
如果没有锁文件,不同开发者在不同时间安装依赖,可能会得到不同版本。
例如 package.json 中写的是:
{
"dependencies": {
"demo": "^1.0.0"
}
}
今天安装可能得到 1.1.0,过几天安装可能得到 1.2.0。
如果新版本存在兼容性问题,项目就可能出现“我这里能跑,你那里不能跑”的情况。
后来的 npm 通过扁平化依赖、并行下载、缓存和 package-lock.json 等方式,已经改善了很多问题。
pnpm
pnpm 是一个更注重安装速度和磁盘空间利用率的包管理器。
它和 npm 最大的区别在于:pnpm 不会简单地把每个项目的依赖都完整复制一份到 node_modules 中。
pnpm 会把依赖统一存储在全局内容寻址仓库中,然后通过硬链接和符号链接组织项目中的 node_modules。
这样的优势是:
- 多个项目可以复用同一份依赖文件。
- 节省磁盘空间。
- 安装速度更快。
- 依赖结构更严格。
- 可以避免项目使用没有声明的依赖。
pnpm 的 node_modules 结构
使用 pnpm 安装依赖后,node_modules 中通常会看到:
node_modules
├── .bin
├── .pnpm
└── vue -> .pnpm/vue@3.x.x/node_modules/vue
其中:
.bin:存放命令行可执行文件。.pnpm:存放依赖的实际组织结构。- 普通依赖目录:通常是指向
.pnpm内部目录的符号链接。
这种结构让 pnpm 既能节省空间,又能保持依赖关系清晰。
硬链接和符号链接
理解 pnpm 的工作原理,需要先知道两个概念:硬链接和符号链接。
硬链接
硬链接可以理解为:给同一个文件内容创建多个文件名。
它的特点是:
- 只能用于文件,不能直接用于目录。
- 多个硬链接指向同一份文件内容。
- 删除其中一个文件名,不会影响其他硬链接访问内容。
- 只有所有硬链接都删除后,文件内容才会真正被释放。
pnpm 通过硬链接复用全局仓库中的依赖文件,避免重复复制文件内容。
符号链接
符号链接也叫软链接,可以理解为快捷方式。
它的特点是:
- 可以指向文件,也可以指向目录。
- 保存的是目标路径。
- 如果原始目标被删除,符号链接可能失效。
pnpm 通过符号链接把项目中的依赖目录连接到 .pnpm 中真实的依赖位置。