理解前端包管理器的基本原理。

2022-09-15 03:35:27

包管理器

前端项目通常会依赖大量第三方包,比如框架、构建工具、请求库、组件库等。

包管理器的作用就是帮助我们管理这些依赖,包括:

  • 安装依赖。
  • 卸载依赖。
  • 记录依赖版本。
  • 管理依赖之间的关系。
  • 保证不同环境下安装结果尽量一致。

package.json

package.json: 用来描述当前项目的基本信息、依赖信息和脚本命令。

常见字段如下:

字段作用
name包名,通常使用英文、小写字母和连字符
version当前包的版本号
description包的描述信息
homepage项目主页地址
author包的作者信息
repository代码仓库地址
main包的入口文件
keywords搜索关键字
dependencies生产环境依赖
devDependencies开发环境依赖

版本号规则

依赖通常使用语义化版本号,格式是:

主版本号.次版本号.补丁版本号

比如:

1.2.3

三个数字分别表示:

位置含义说明
主版本号Major重大更新,可能包含不兼容变更
次版本号Minor新增功能,一般保持向下兼容
补丁版本号PatchBug 修复、性能优化、安全补丁

一般来说:

  • 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项目运行时需要的依赖vuereactaxios
devDependencies开发、构建、测试阶段需要的依赖vitewebpackeslint

安装生产依赖:

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'

查找流程大致是:

  1. 先在当前目录的 node_modules 中查找 axios
  2. 如果没找到,就向上一级目录继续查找 node_modules
  3. 一直查找到项目根目录或系统根目录。
  4. 如果始终找不到,就抛出模块不存在的错误。

包入口文件

当我们引入一个包时,包管理器和模块系统需要知道这个包的入口文件在哪里。

一般会先读取这个包自己的 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 中真实的依赖位置。