Bakkac's Blog
本文目录

现代前端工具链里,最核心的两个角色通常就是包管理器和构建工具。

  • 包管理器负责把“项目需要哪些依赖”变成一套可安装、可复现、可解析的文件结构
  • 构建工具负责把“面向开发者的源码”变成浏览器或运行时能直接消费的产物

它们经常一起出现,但解决的并不是同一个问题。

为什么这两个要放在一起看

一个现代前端项目,至少要先回答两类问题:

  1. 依赖怎么安装、锁定、复现
  2. 源码怎么转译、组织、提供给浏览器执行

第一类问题对应包管理器,第二类问题对应构建工具。

如果把现代前端项目看成一条工作流,那么可以粗略理解为:

声明依赖
  -> 安装依赖
  -> 解析模块
  -> 转译源码
  -> 开发时按需提供
  -> 生产时打包优化

其中前两步主要是包管理器,后几步主要是构建工具。

包管理器在做什么

依赖管理的本质是:把“我需要哪些包”变成一套稳定的本地依赖结构,让运行时和工具都能找到它们。

一个项目里和依赖管理最相关的几个东西通常是:

package.json = 依赖需求
包管理器 = 解析算法和安装执行者
lock file = 精确解析结果快照
node_modules = 安装后的文件结构

package.json

package.json 描述的是项目的依赖需求和元信息,比如:

  • 这个项目叫什么
  • 需要哪些第三方包
  • 可以执行哪些 scripts
  • 某些工具应该如何理解这个项目

它声明的是“范围”或“意图”,不是最终安装结果。

lockfile

锁文件记录的是最终精确解析结果。

例如:

{
  "dependencies": {
    "react": "^19.0.0"
  }
}

这里的 ^19.0.0 并不是固定版本,它允许安装多个兼容版本;但锁文件会把最终结果固定下来,这样团队成员在不同时间安装依赖时,不会得到不同的依赖树。

node_modules

node_modules 是安装后的事实源。

  • 对包管理器来说,它是依赖落地后的目录结构
  • 对运行时和构建工具来说,它是解析第三方包的重要输入

也就是说,包管理器把依赖“放进来”,而后续工具会继续“消费它”。

pnpm install 在做什么

以 pnpm 为例,安装流程可以粗略理解为:

读取 package.json
  -> 读取 pnpm-lock.yaml(如果存在)
  -> 检查 lock 是否满足 package.json
  -> 如果满足,按 lock 安装
  -> 如果不满足,重新解析
  -> 写回新的 pnpm-lock.yaml
  -> 生成 node_modules 的链接结构

这也是为什么 pnpm 解决的核心问题不是“下载文件”这么简单,而是依赖解析、版本锁定和目录拓扑构建。

构建工具在做什么

构建工具负责的是另一件事:把开发者友好的源码,变成浏览器或运行时能直接执行的结果。

现代前端源码通常包含这些内容:

  • TypeScript、JSX、TSX
  • CSS 预处理、静态资源导入
  • 裸模块导入,例如 import React from "react"
  • 别名路径、动态导入、环境变量注入

这些内容浏览器原生并不能完整理解,因此需要构建工具来处理。

模块化能力增强

原生 ESM 只理解比较朴素的模块 URL,比如 ./foo.js 或 /bar.js。

但现代前端项目常见的是:

import React from "react";
import Button from "@/components/Button";
import "./style.css";
import logoUrl from "./logo.png";

这里已经超出了浏览器原生模块解析的能力边界。构建工具需要帮你完成:

  • 裸模块导入解析
  • 路径别名解析
  • 资源文件导入转换
  • TypeScript / JSX 转译
  • 动态导入拆分

这部分可以配合 模块化与打包增强 一起看。

模块图分析

包管理器处理的是 package graph,构建工具处理的是 module graph。

两者的区别在于:

对象包管理器构建工具
分析单位包模块/文件
依赖信息来源package.json 这类显式元数据源码里的 import / export / import()
主要目标安装、锁定、复现依赖转译、组织、拆分、优化模块

package graph 的依赖信息是显式的,而 module graph 的依赖信息藏在代码语法里,所以构建工具通常要解析 AST,才能准确知道谁依赖谁。

开发阶段在做什么

以 Vite 为代表的现代构建工具,在开发阶段的核心思路不是“整项目先打成一个 bundle”,而是:

浏览器请求入口模块
  -> 构建工具解析 import
  -> 按需转译当前模块
  -> 返回浏览器可执行的 ESM

同时,它还会:

  • 预构建第三方依赖
  • 维护模块图
  • 监听文件变更
  • 基于模块边界做 HMR

这部分可以继续看 [Vite 的构建流程](./Vite/Vite 的构建流程.md) 和 [HMR 如何工作](./Vite/HMR 如何工作.md)。

生产构建在做什么

到生产阶段,构建工具会完整分析依赖图,并做真正的打包优化:

解析 import
  -> 构建完整依赖图
  -> 转译与资源处理
  -> tree-shaking
  -> code splitting
  -> 压缩与产物输出

也就是说,开发阶段更偏向“按需提供模块”,而生产阶段更偏向“重组和优化整个模块图”。

包管理器和构建工具怎么衔接

两者的关系可以理解成:

  • 包管理器先把第三方依赖解析并落到本地
  • 构建工具再基于这些依赖和你的源码,建立模块图并完成转译/打包

最直观的例子就是第三方包导入:

import React from "react";

包管理器负责把 react 安装进本地依赖结构里,构建工具负责把 "react" 这条导入解析到具体文件,并转换成浏览器可消费的形式。

所以它们不是替代关系,而是上下游关系:

  • 包管理器提供依赖基础设施
  • 构建工具消费依赖基础设施并组织运行产物

放在一起理解时,一个很重要的认识是:

pnpm 解决“依赖怎么落地”,Vite 解决“模块怎么被解析、转译和交付”。

因此,一个现代前端项目的主工作流通常可以概括为:

package.json / lockfile
  -> pnpm 安装依赖
  -> 生成 node_modules 结构
  -> Vite 基于源码和 node_modules 建立模块图
  -> 开发时按需提供模块
  -> 生产时打包并输出优化产物
  • 包管理器解决“我要什么依赖,以及怎样稳定拿到它们”
  • 构建工具解决“这些源码和依赖,最终怎样变成可运行的应用”

前者关注依赖树,后者关注模块图。前者是基础设施,后者是交付流水线。