Skip to content

前端工程化

概述

前端工程化是将前端开发流程规范化、自动化的过程,包括构建工具代码规范自动化测试CI/CD等。

核心内容

  • 📦 构建工具 - Webpack、Vite、Rollup
  • 🔧 编译工具 - Babel、TypeScript、SWC
  • 📝 代码规范 - ESLint、Prettier、Stylelint
  • 🧪 自动化测试 - Jest、Vitest、Playwright
  • 🚀 CI/CD - GitHub Actions、GitLab CI
  • 📚 Monorepo - pnpm、Turborepo、Nx

为什么需要工程化?

javascript
// 没有工程化的问题:
/*
1. 手动压缩代码
2. 手动处理兼容性
3. 手动合并文件
4. 没有统一的代码规范
5. 测试依赖人工
6. 部署流程复杂易错
*/

// 工程化解决:
/*
1. 自动构建打包
2. 自动处理兼容性(Babel)
3. 自动优化代码
4. 强制代码规范(ESLint)
5. 自动化测试
6. 自动化部署(CI/CD)
*/

包管理器对比:npm vs yarn vs pnpm

发展历史

  • npm(2010年):Node.js 的默认包管理器,最早也是使用最广泛的
  • yarn(2016年):Facebook 联合 Google、Exponent 开发,解决 npm 早期的性能和一致性问题
  • pnpm(2017年):performant npm,通过创新的存储机制提供更好的性能和磁盘效率

核心区别对比表

特性npmyarnpnpm
安装速度中等快(并行安装)最快(硬链接)
磁盘占用大(重复安装)大(重复安装)小(全局存储)
依赖管理方式扁平化 node_modules扁平化 node_modules硬链接 + 软链接
lock文件package-lock.jsonyarn.lockpnpm-lock.yaml
幽灵依赖问题存在存在不存在(严格隔离)
Monorepo支持workspaces(v7+)workspaces原生支持,性能最好
NodeLinkerhoistedhoistedisolated(可配置)
兼容性最好需要注意某些场景

npm 的问题

1. 嵌套地狱(npm v2 时代)

bash
# npm v2 的 node_modules 结构
node_modules/
├── A
   └── node_modules
       └── B@1.0
           └── node_modules
               └── C@1.0
└── D
    └── node_modules
        └── B@2.0
            └── node_modules
                └── C@2.0

# 问题:
# - 路径过长(Windows 限制)
# - 重复安装(B 和 C 都装了多次)
# - 安装速度慢

2. 幽灵依赖(Phantom Dependencies)

javascript
// package.json 中只声明了 A
{
  "dependencies": {
    "A": "^1.0.0"
  }
}

// 但因为 A 依赖 B,npm v3+ 扁平化后
// 你的代码可以直接引用 B(未声明的依赖)
import B from 'B'; // 能用,但很危险!

// 问题:
// 1. A 升级后可能移除 B,导致你的代码崩溃
// 2. 不同环境 node_modules 结构可能不同
// 3. 依赖关系不透明

3. 重复安装问题

bash
# 即使是扁平化,仍可能重复安装
node_modules/
├── A (依赖 C@1.0)
├── B (依赖 C@2.0)
├── C@1.0  # 被提升
└── B/
    └── node_modules/
        └── C@2.0  # 重复安装

# 问题:
# - 浪费磁盘空间
# - 安装时间长

pnpm 为什么更快更省空间?

1. 硬链接和软链接机制

bash
# pnpm 的 node_modules 结构
node_modules/
├── .pnpm/
   ├── lodash@4.17.21/
   └── node_modules/
       └── lodash/          # 真实文件(硬链接到全局存储)
   └── axios@1.0.0/
       └── node_modules/
           ├── axios/
           └── follow-redirects/ # axios 的依赖
├── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash  # 软链接
└── axios -> .pnpm/axios@1.0.0/node_modules/axios       # 软链接

关键概念:

  • 硬链接(Hard Link):指向磁盘上相同的 inode,多个硬链接共享同一份文件内容
  • 软链接(Symbolic Link):类似快捷方式,指向另一个文件路径
javascript
// 硬链接示意
// 全局存储:~/.pnpm-store/v3/files/00/abc123...
// 项目A:   项目/node_modules/.pnpm/lodash@4.17.21/...  ← 硬链接
// 项目B:   项目/node_modules/.pnpm/lodash@4.17.21/...  ← 硬链接
// 实际只占用一份磁盘空间!

2. 内容寻址存储(Content-addressable Store)

bash
# pnpm 全局存储结构
~/.pnpm-store/
└── v3/
    └── files/
        ├── 00/
   └── a1b2c3...  # 文件内容的 hash
        ├── 01/
   └── d4e5f6...
        └── ...

# 特点:
# 1. 相同文件只存一次(基于内容 hash)
# 2. 所有项目共享同一个全局存储
# 3. 安装时只需创建硬链接,不需要复制文件

3. 严格的依赖隔离

javascript
// package.json
{
  "dependencies": {
    "express": "^4.18.0"
  }
}

// 使用 npm/yarn
import lodash from 'lodash'; // 能用!express 依赖了 lodash

// 使用 pnpm
import lodash from 'lodash'; // 报错!必须显式声明依赖

// pnpm 的 node_modules 只包含你声明的依赖
node_modules/
├── express -> .pnpm/express@4.18.0/node_modules/express
└── .pnpm/
    ├── express@4.18.0/
    │   └── node_modules/
    │       ├── express/
    │       └── lodash/  # lodash 在这里,但你访问不到

4. 性能对比实测

bash
# 安装 React 项目依赖(~1200个包)

# npm (v9)
time npm install
# 耗时: ~45s
# 磁盘: ~200MB

# yarn (v1)
time yarn install
# 耗时: ~35s
# 磁盘: ~200MB

# pnpm (v8)
time pnpm install
# 耗时: ~15s(首次)、~5s(有缓存)
# 磁盘: ~50MB(硬链接)

# 结论:pnpm 快 2-3 倍,省空间 70%+

常用命令对比

bash
# ============ 安装依赖 ============
npm install              # 安装 package.json 的所有依赖
yarn                     # 同上(yarn install 的简写)
pnpm install             # 同上

# ============ 添加依赖 ============
npm install lodash       # 添加到 dependencies
yarn add lodash          # 同上
pnpm add lodash          # 同上

npm install -D webpack   # 添加到 devDependencies
yarn add -D webpack      # 同上
pnpm add -D webpack      # 同上

npm install -g pm2       # 全局安装
yarn global add pm2      # 同上
pnpm add -g pm2          # 同上

# ============ 移除依赖 ============
npm uninstall lodash
yarn remove lodash
pnpm remove lodash

# ============ 运行脚本 ============
npm run dev
yarn dev                 # 可以省略 run
pnpm dev                 # 同上

# ============ 更新依赖 ============
npm update
yarn upgrade
pnpm update

# ============ Monorepo ============
# 在根目录执行某个包的脚本
npm run build -w packageA
yarn workspace packageA build
pnpm -F packageA build   # -F 是 --filter 的简写

# 给特定包添加依赖
npm install lodash -w packageA
yarn workspace packageA add lodash
pnpm add lodash --filter packageA

# ============ pnpm 特有命令 ============
pnpm store status        # 查看全局存储状态
pnpm store prune         # 清理未使用的包
pnpm ls --depth 0        # 查看直接依赖
pnpm why lodash          # 为什么安装了这个包

配置文件对比

yaml
# .npmrc
registry=https://registry.npmjs.org/
save-exact=true
package-lock=true

# .yarnrc
registry "https://registry.npmjs.org/"
save-prefix ""

# .npmrc (pnpm 也用这个文件)
registry=https://registry.npmjs.org/
shamefully-hoist=false    # 不提升依赖(严格模式)
strict-peer-dependencies=false

Monorepo 支持对比

json
// package.json - npm/yarn/pnpm 通用
{
  "name": "monorepo-root",
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}
yaml
# pnpm-workspace.yaml - pnpm 专用
packages:
  - 'packages/*'
  - 'apps/*'
  - '!**/test/**'
bash
# 性能对比(大型 monorepo,30个包,5000+依赖)
npm workspaces install   # ~180s
yarn workspaces install  # ~120s
pnpm install            # ~25s (快7倍!)

# pnpm 的优势:
# 1. 所有包共享 .pnpm 存储,没有重复
# 2. 硬链接机制,安装速度极快
# 3. 原生支持 workspace 协议

如何选择?

选择 npm

bash
# 适合场景:
 小型项目,依赖不多
 需要最大兼容性(某些老旧工具只支持 npm)
 团队不愿引入新工具
 CI/CD 环境默认使用 npm

# 优势:
- Node.js 自带,无需额外安装
- 生态最成熟,兼容性最好
- 文档资源最丰富

选择 yarn

bash
# 适合场景:
 老项目已经在用 yarn
 需要 yarn 的特定功能(如 yarn dlx)
 Yarn 2+ (Berry) 的 PnP 模式

# 优势:
- npm
- yarn.lock 更稳定
- 命令更简洁(yarn add vs npm install)

# 注意:
- Yarn 1 (Classic) 已停止维护
- Yarn 2+ (Berry) 改动很大,需要学习成本

选择 pnpm(推荐)

bash
# 适合场景:
 新项目(强烈推荐)
 Monorepo 项目(最佳选择)
 磁盘空间有限
 需要严格的依赖管理
 追求极致性能

# 优势:
- 最快的安装速度
- 最小的磁盘占用
- 解决幽灵依赖问题
- Monorepo 原生支持且性能最好

# 注意:
- 某些老旧工具可能不兼容
- 需要团队学习成本(但很低)

迁移到 pnpm

bash
# 1. 安装 pnpm
npm install -g pnpm

# 2. 删除旧的依赖
rm -rf node_modules
rm package-lock.json  # 或 yarn.lock

# 3. 使用 pnpm 安装
pnpm install

# 4. 如果遇到兼容性问题,配置 .npmrc
shamefully-hoist=true    # 提升依赖(临时方案)
public-hoist-pattern[]=*eslint*  # 只提升特定包

# 5. 更新 CI/CD 配置
# .github/workflows/ci.yml
- uses: pnpm/action-setup@v2
  with:
    version: 8

面试题详解

1. 为什么 pnpm 比 npm/yarn 快?

答案:

javascript
// 核心原因:硬链接 + 内容寻址存储

// npm/yarn 的安装流程:
1. 解析依赖树
2. 下载所有包到缓存
3. 从缓存复制文件到 node_modules  // ⏱️ 慢在这里!
4. 扁平化处理

// pnpm 的安装流程:
1. 解析依赖树
2. 下载所有包到全局存储(~/.pnpm-store)
3. 创建硬链接到 .pnpm/                // ⚡ 快!只是链接,不复制
4. 创建软链接到 node_modules/         // ⚡ 快!

// 速度优势:
- 创建硬链接比复制文件快 10-100
- 相同版本的包只下载一次,永久缓存
- 并行安装 + COW (Copy-on-Write) 优化

图示对比:

npm/yarn:
下载 lodash@4.17.21 (500KB)
  ↓ 复制 500KB
项目A/node_modules/lodash
  ↓ 复制 500KB
项目B/node_modules/lodash
  ↓ 复制 500KB
项目C/node_modules/lodash
总磁盘占用:1.5MB

pnpm:
下载 lodash@4.17.21 (500KB)
  ↓ 存储到全局
~/.pnpm-store/v3/files/00/abc123
  ↓ 硬链接(0 字节)
项目A/.pnpm/lodash@4.17.21
  ↓ 硬链接(0 字节)
项目B/.pnpm/lodash@4.17.21
  ↓ 硬链接(0 字节)
项目C/.pnpm/lodash@4.17.21
总磁盘占用:500KB

2. 什么是幽灵依赖?pnpm 如何解决?

答案:

javascript
// 幽灵依赖(Phantom Dependencies):
// 能够 require/import 一个没有在 package.json 中声明的包

// 例子:
// package.json
{
  "dependencies": {
    "express": "^4.18.0"  // 只声明了 express
  }
}

// npm/yarn 扁平化后的 node_modules:
node_modules/
├── express/
├── body-parser/      // express 的依赖,被提升了
├── cookie/           // express 的依赖,被提升了
└── ...

// 你的代码可以直接用(幽灵依赖):
import cookie from 'cookie'; // ⚠️ 能用,但很危险!

// 问题:
// 1. express@5 可能移除 cookie 依赖
// 2. 你的代码会突然崩溃
// 3. 不同机器 node_modules 结构可能不同(安装顺序影响)

pnpm 的解决方案:

bash
# pnpm 的 node_modules 结构:
node_modules/
├── express -> .pnpm/express@4.18.0/node_modules/express  # 软链接
└── .pnpm/
    └── express@4.18.0/
        └── node_modules/
            ├── express/          # 真实文件
            ├── body-parser/      # express 的依赖
            └── cookie/           # express 的依赖

# 你的代码:
import cookie from 'cookie'; // 报错!找不到模块

# Node.js 查找模块的路径:
# 1. 当前目录 node_modules/cookie        ← 没有
# 2. 父目录 node_modules/cookie           ← 没有
# 3. ... 一直到根目录                      ← 都没有

# 解决:必须显式声明依赖
pnpm add cookie  # 在 package.json 中声明

真实案例:

javascript
// 某项目使用了 @types/node,但没声明依赖
// npm/yarn 能用(因为某个依赖引入了它)
import { EventEmitter } from 'events'; // ✅ 能用

// 迁移到 pnpm 后报错
// Error: Cannot find module 'events'

// 正确做法:显式声明
pnpm add @types/node

// 好处:
// 1. 依赖关系清晰透明
// 2. 升级依赖不会破坏代码
// 3. 团队成员环境一致

3. pnpm 的硬链接机制是什么?

答案:

javascript
// 文件系统基础知识:

// inode(索引节点):
// - 存储文件的元数据(大小、权限、时间戳)
// - 指向文件实际数据的指针

// 硬链接(Hard Link):
// - 多个文件名指向同一个 inode
// - 共享同一份数据
// - 删除一个不影响其他
// - 必须在同一文件系统

// 软链接(Symbolic Link):
// - 类似 Windows 快捷方式
// - 存储目标文件的路径
// - 可以跨文件系统

pnpm 的实现:

bash
# 全局存储(内容寻址)
~/.pnpm-store/v3/files/
└── 00/
    └── a1b2c3d4e5f6...  # lodash 的某个文件(inode: 12345)

# 项目 node_modules
项目/.pnpm/lodash@4.17.21/node_modules/lodash/index.js
# ↑ 硬链接到全局存储(inode: 12345)

# 验证是否是硬链接
ls -li ~/.pnpm-store/v3/files/00/a1b2c3d4e5f6...
# 12345 -rw-r--r-- 3 user group 1024 Jan 1 12:00 a1b2c3d4e5f6...
#  ↑ inode        ↑ 链接数(3个硬链接)

# 特点:
# 1. 修改任何一个硬链接,所有都会变(同一份数据)
# 2. 删除一个硬链接,其他不受影响
# 3. 只有所有硬链接都删除,数据才会被删除
# 4. 不占用额外磁盘空间

完整示例:

javascript
// 1. 安装包
pnpm add lodash

// 内部流程:
// Step 1: 下载到全局存储
下载 lodash@4.17.21.tgz
解压到 ~/.pnpm-store/v3/tmp/
计算每个文件的 hash: sha512(file content)
移动到 ~/.pnpm-store/v3/files/{hash前2位}/{hash}

// Step 2: 创建虚拟存储(.pnpm)
创建目录 .pnpm/lodash@4.17.21/node_modules/lodash/
对每个文件创建硬链接:
  .pnpm/lodash@4.17.21/node_modules/lodash/index.js
~/.pnpm-store/v3/files/00/abc123...

// Step 3: 创建软链接
node_modules/lodash
  → .pnpm/lodash@4.17.21/node_modules/lodash

// 2. 再安装一个依赖 lodash 的包
pnpm add express

// express 依赖 lodash@4.17.21
// 创建硬链接(复用全局存储):
.pnpm/express@4.18.0/node_modules/lodash/index.js
~/.pnpm-store/v3/files/00/abc123...  # 同一个 inode!

// 结果:
// - lodash 只下载了一次
// - 磁盘只占用一份空间
// - 两个项目都能正常使用

优势总结:

javascript
// 1. 极致的磁盘效率
// 传统方式:100个项目 × 200MB = 20GB
// pnpm:     全局存储 2GB + 硬链接开销 < 3GB

// 2. 安装速度快
// 复制文件:O(n) 时间复杂度,取决于文件大小
// 硬链接:  O(1) 时间复杂度,只创建索引

// 3. 缓存永久有效
// npm/yarn: 每个项目都要从缓存复制到 node_modules
// pnpm:     安装过的包,所有项目直接硬链接,秒装

// 4. 依赖严格隔离
// npm/yarn: 扁平化,有幽灵依赖风险
// pnpm:     软链接 + 硬链接,每个包只能访问声明的依赖

学习路线

  1. Webpack - 最流行的打包工具
  2. Vite - 新一代构建工具
  3. Babel - JavaScript编译器
  4. 代码规范 - 保证代码质量
  5. CI/CD - 自动化部署
  6. Monorepo - 大型项目管理

开始学习具体内容 👉