↓ 跳过正文
  1. 文章/

前端工程化体系学习指南 - 从实践到原理

·18479 字·37 分钟·
hujiacheng
作者
hujiacheng
Front-end Developer / Strive To Become Better
目录
学习目标

本文档面向所有前端开发者,无论你是刚接触工程化,还是有一定使用经验但想系统学习的开发者。

适合人群:

  • 🔰 初学者:刚学完 HTML/CSS/JS,想了解如何搭建规范的项目
  • 🌱 工作驱动学习者:遇到问题才查资料,没有系统学习过
  • 📚 有经验但想深入:用过工具但不知道原理,想查漏补缺
  • 🎯 技术决策者:需要为团队选型和制定规范

本文特点:

  • ✅ 从零开始:假设你没有任何工程化基础
  • ✅ 问题导向:先讲为什么需要,再讲怎么做
  • ✅ 避坑指南:标注常见错误和容易忽略的点
  • ✅ 实战案例:每个知识点配合真实场景
  • ✅ 循序渐进:从简单到复杂,逐步深入

学习路径:

  1. 工程化基础 → 理解为什么需要工程化(20分钟)
  2. 代码规范 → 保证代码质量和团队协作(2小时)
  3. 构建系统 → 从开发到生产的完整流程(3小时)
  4. 包管理 → 依赖管理和多项目管理(2小时)
  5. 自动化 → 解放双手,提高效率(1小时)
  6. 监控运维 → 保证生产环境稳定(1小时)

阅读建议:

  • 📖 首次阅读:按顺序完整阅读,不跳过基础部分
  • 🔖 查漏补缺:查看每节的「常见误区」和「容易忽略的点」
  • 💻 边学边练:跟着示例代码动手实践
  • 🔄 反复回顾:工程化知识需要在实践中不断加深理解

一、前端工程化概述
#

1.1 什么是前端工程化
#

1.1.1 一个真实的场景
#

想象你刚入职一家公司,接手一个「没有工程化」的项目:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div id="app"></div>
  <script src="jquery.js"></script>
  <script src="utils.js"></script>
  <script src="app.js"></script>
</body>
</html>

你会遇到这些问题:

  1. 代码混乱

    // app.js - 1000 行代码都在一个文件里
    var userInfo = { name: 'Tom' }
    function login() { /* 100 行代码 */ }
    function logout() { /* 100 行代码 */ }
    // ... 还有 900 行
    
    • ❌ 找个函数要翻半天
    • ❌ 不敢随便改,怕影响其他功能
    • ❌ 多人协作容易冲突
  2. 代码风格不统一

    // 开发者 A 的代码
    function getUserInfo() {
        return userInfo
    }
    
    // 开发者 B 的代码
    const get_user_name = () => {
      return userInfo.name;
    };
    • ❌ 有的用 tab,有的用空格
    • ❌ 有的用单引号,有的用双引号
    • ❌ 命名风格各不相同
  3. 依赖管理混乱

    <!-- 不知道这些 CDN 是什么版本 -->
    <script src="https://cdn.../jquery-3.x.x.js"></script>
    <script src="https://cdn.../lodash.min.js"></script>
    • ❌ 不知道依赖了哪些库
    • ❌ 不知道库的版本
    • ❌ CDN 挂了网站就打不开
  4. 手动部署很痛苦

    # 每次上线的步骤
    1. 压缩 JS 文件
    2. 压缩 CSS 文件
    3. 上传到服务器
    4. 清理浏览器缓存
    5. 测试是否正常
    • ❌ 容易漏步骤
    • ❌ 每次都要重复劳动
    • ❌ 出错了不知道怎么回滚

1.1.2 工程化如何解决这些问题
#

使用工程化后的项目:

my-project/
├── src/                    # 源代码(开发时写的)
│   ├── components/         # 组件(代码复用)
│   ├── utils/              # 工具函数(模块化)
│   ├── api/                # 接口管理(统一管理)
│   └── main.ts             # 入口文件
├── .eslintrc.js           # 代码规范(自动检查)
├── .prettierrc.js         # 代码格式(自动格式化)
├── package.json           # 依赖管理(版本明确)
└── vite.config.ts         # 构建配置(自动化)

一条命令解决所有问题:

# 开发时
npm run dev
# ✅ 自动启动服务器
# ✅ 代码改动自动刷新
# ✅ 自动检查代码规范
# ✅ TypeScript 自动提示

# 部署时
npm run build
# ✅ 自动编译、压缩、优化
# ✅ 自动生成哈希文件名(解决缓存)
# ✅ 自动上传到服务器
# ✅ 自动发送通知

1.1.3 工程化的定义
#

前端工程化是将软件工程的方法和思想应用到前端开发中,以提高开发效率、保证代码质量、优化用户体验的系统化实践。

核心价值:

  • 📦 模块化:代码拆分、按需加载、依赖管理
  • 🔧 组件化:UI 组件复用、业务组件抽象
  • 📏 规范化:代码风格、提交规范、目录结构
  • ⚡ 自动化:构建、测试、部署、发布
  • 🎯 优化:性能优化、体积优化、加载优化

简单来说:

工程化就是让你写代码更轻松,让团队协作更顺畅,让项目质量更有保障。

1.2 工程化体系全景图
#

┌─────────────────────────────────────────────────────────────┐
│                      前端工程化体系                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐     │
│  │  开发规范    │  │  构建系统    │  │  质量保障    │     │
│  ├──────────────┤  ├──────────────┤  ├──────────────┤     │
│  │ • 代码规范   │  │ • 模块打包   │  │ • 单元测试   │     │
│  │ • 提交规范   │  │ • 资源处理   │  │ • E2E 测试   │     │
│  │ • 分支规范   │  │ • 代码转换   │  │ • 性能监控   │     │
│  │ • 文档规范   │  │ • 优化策略   │  │ • 错误追踪   │     │
│  └──────────────┘  └──────────────┘  └──────────────┘     │
│                                                              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐     │
│  │  包管理      │  │  自动化      │  │  部署运维    │     │
│  ├──────────────┤  ├──────────────┤  ├──────────────┤     │
│  │ • npm/pnpm   │  │ • Git Hooks  │  │ • CI/CD      │     │
│  │ • Monorepo   │  │ • 自动构建   │  │ • 容器化     │     │
│  │ • 依赖管理   │  │ • 自动测试   │  │ • 灰度发布   │     │
│  │ • 版本管理   │  │ • 自动发布   │  │ • 监控告警   │     │
│  └──────────────┘  └──────────────┘  └──────────────┘     │
│                                                              │
└─────────────────────────────────────────────────────────────┘

1.3 学习路径建议
#

阶段一:建立认知(第 1 天)
#

目标:理解为什么需要工程化

学习内容:

  • 对比有工程化和无工程化的项目
  • 理解工程化的核心价值
  • 了解工程化工具全景图

实践任务:

  • 用 Vite 创建第一个项目:npm create vite@latest
  • 运行 npm run dev,观察开发服务器
  • 运行 npm run build,查看构建产物

阶段二:代码规范(第 2-7 天)
#

目标:保证代码质量和团队协作

学习内容:

  • EditorConfig:统一编辑器配置
  • ESLint:代码质量检查
  • Prettier:代码格式化
  • Git Hooks:提交前自动检查

实践任务:

# Day 2-3:配置 ESLint
npm install -D eslint
npx eslint --init

# Day 4-5:配置 Prettier
npm install -D prettier
# 创建 .prettierrc.js

# Day 6-7:配置 Git Hooks
npm install -D husky lint-staged
npx husky init

常见问题:

  • ❓ ESLint 和 Prettier 冲突怎么办?
  • ❓ 为什么 Git 提交被拦截了?
  • ❓ 如何忽略某些文件的检查?

(这些问题在后面章节会详细解答)

阶段三:构建系统(第 8-14 天)
#

目标:理解从开发到生产的完整流程

学习内容:

  • 模块化:CommonJS vs ESM
  • Vite:开发服务器和热更新
  • Rollup:生产环境打包
  • 构建优化:代码分割、Tree Shaking

实践任务:

# Day 8-10:理解模块化
# 阅读并运行示例代码

# Day 11-12:配置 Vite
# 修改 vite.config.ts
# 添加插件、配置路径别名

# Day 13-14:构建优化
# 分析打包体积
# 实现代码分割

阶段四:包管理(第 15-21 天)
#

目标:管理依赖和多项目

学习内容:

  • npm/pnpm/yarn 对比
  • package.json 详解
  • Monorepo 实践
  • 版本管理

实践任务:

# Day 15-17:包管理器
# 理解 package.json
# 学习 pnpm 的优势

# Day 18-21:Monorepo
# 创建多包项目
# 配置 pnpm workspace
# 使用 Turborepo 加速构建

阶段五:自动化(第 22-28 天)
#

目标:自动化构建、测试、部署

学习内容:

  • GitHub Actions
  • 自动化测试
  • 自动化部署
  • 错误监控

实践任务:

# Day 22-24:CI/CD
# 配置 GitHub Actions
# 自动运行测试

# Day 25-28:部署上线
# 部署到 Vercel/Netlify
# 配置 Sentry 监控

阶段六:深入原理(持续学习)
#

目标:理解底层原理,能自己开发工具

学习内容:

  • 阅读 Vite 源码
  • 开发 Vite 插件
  • 开发 ESLint 规则
  • 开发 CLI 工具

学习方式:

  • 📖 阅读官方文档的 Advanced 部分
  • 🔍 查看优秀开源项目的源码
  • ✍️ 写技术博客总结
  • 🗣️ 在团队分享
  • 🌟 贡献开源项目

学习建议

不要一次性学完所有内容!

  1. 循序渐进:先会用,再懂原理
  2. 动手实践:每学一个工具就在项目中用起来
  3. 遇到问题:先看报错信息,再查文档,最后问 AI
  4. 定期回顾:隔一段时间重新看一遍,会有新的理解
  5. 不要焦虑:工程化是一个持续学习的过程,慢慢来

二、代码规范体系
#

2.1 EditorConfig - 编辑器配置统一
#

2.1.1 为什么需要 EditorConfig?
#

问题场景:

// 开发者 A 用 VS Code,tab 转 2 个空格
function hello() {
··console.log('A')
}

// 开发者 B 用 WebStorm,tab 转 4 个空格
function hello() {
····console.log('B')
}

// Git 提交时产生大量无意义的 diff

EditorConfig 的作用:

让所有编辑器(VS Code、WebStorm、Sublime Text 等)都使用相同的代码格式配置。

2.1.2 快速上手(5分钟)
#

第一步:创建配置文件

在项目根目录创建 .editorconfig 文件:

# .editorconfig
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

第二步:安装编辑器插件

  • VS Code:搜索并安装 EditorConfig for VS Code
  • WebStorm:内置支持,无需安装

第三步:验证是否生效

  1. 新建一个 .js 文件
  2. 按 Tab 键
  3. 观察是否转换为 2 个空格

✅ 如果是 2 个空格,说明配置生效了!

2.1.3 配置项详解
#

# .editorconfig

# 表示这是最顶层的配置文件,停止向上查找
root = true

# 对所有文件生效
[*]
charset = utf-8                    # 文件编码
end_of_line = lf                   # 换行符(lf/cr/crlf)
indent_style = space               # 缩进风格(space/tab)
indent_size = 2                    # 缩进大小
trim_trailing_whitespace = true    # 删除行尾空格
insert_final_newline = true        # 文件末尾插入空行

# Markdown 文件特殊配置
[*.md]
trim_trailing_whitespace = false   # 保留行尾空格(Markdown 语法需要)
max_line_length = off              # 不限制行长度

# Makefile 必须使用 tab
[Makefile]
indent_style = tab

# JSON 文件使用 2 空格缩进
[*.json]
indent_size = 2

# Python 文件使用 4 空格缩进
[*.py]
indent_size = 4

2.1.4 深入理解
#

Q1:EditorConfig 和 Prettier 有什么区别?

工具作用时机作用范围优先级
EditorConfig编辑时实时生效编辑器行为(缩进、换行等)最高(编辑器层面)
Prettier保存时或手动格式化代码格式化(换行、空格、引号等)次之(工具层面)
ESLintLint 时代码质量检查最后(检查层面)

Q2:为什么我的配置没生效?

检查清单:

  1. ✅ 文件名是否正确:.editorconfig(注意开头的点)
  2. ✅ 文件位置是否正确:项目根目录
  3. ✅ 编辑器插件是否安装:VS Code 需要手动安装
  4. ✅ 重启编辑器:安装插件后需要重启

Q3:我已经有很多历史代码了,怎么办?

使用 Prettier 一次性格式化:

# 安装 Prettier
npm install -D prettier

# 格式化所有文件
npx prettier --write "**/*.{js,ts,vue,css,json}"

# 提交格式化代码
git add .
git commit -m "chore: format code with prettier"

Q4:EditorConfig 和 Prettier 有什么区别?

  • EditorConfig:编辑器级别,实时生效(你按 Tab 就转换为空格)
  • Prettier:工具级别,手动触发(保存文件或运行命令时格式化)

建议:两者都用,EditorConfig 保证基础一致,Prettier 做更细致的格式化。

容易忽略的点
  1. 换行符问题:Windows 是 CRLF,Mac/Linux 是 LF
  • 统一设置 end_of_line = lf
  • Git 也要配置:git config --global core.autocrlf false
  1. 文件末尾空行:insert_final_newline = true
  • 这是 POSIX 标准的要求
  • 避免 Git 提示 “No newline at end of file”
  1. 行尾空格:trim_trailing_whitespace = true
  • 自动删除行尾的空格
  • Markdown 除外(两个空格表示换行)

2.2 ESLint - 代码质量检查
#

2.2.1 为什么需要 ESLint?
#

问题场景:

// ❌ 声明了变量但没使用
const userName = 'Tom'

// ❌ 使用了未声明的变量
console.log(userNmae)  // 拼写错误

// ❌ 使用了 == 而不是 ===
if (value == null) { }

// ❌ 函数复杂度太高
function complex() {
  if (...) {
    if (...) {
      if (...) {
        // 100 行嵌套代码
      }
    }
  }
}

这些问题运行时才会发现,导致:

  • 🐛 Bug 多
  • ⏰ 调试时间长
  • 😱 上线后才发现

ESLint 的作用:

在写代码时就发现问题,编辑器会用红色波浪线提示:

// ✅ ESLint 会立即提示
const userName = 'Tom'  // ⚠️ 'userName' is assigned a value but never used

2.2.2 快速上手(10分钟)
#

第一步:安装 ESLint

npm install -D eslint @antfu/eslint-config

第二步:创建配置文件

// eslint.config.js
import antfu from '@antfu/eslint-config'

export default antfu()

第三步:安装 VS Code 插件

搜索并安装 ESLint 插件,然后重启 VS Code。

第四步:测试是否生效

创建 test.js:

const a = 1  // 应该显示警告:变量未使用
console.log(b)  // 应该显示错误:变量未定义

✅ 看到红色波浪线,说明 ESLint 生效了!

第五步:添加 npm 脚本

// package.json
{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
}
# 检查所有文件
npm run lint

# 自动修复可修复的问题
npm run lint:fix

2.2.3 核心概念
#

ESLint 是一个可配置的 JavaScript 代码检查工具,通过静态分析发现代码中的问题。

工作原理:

源代码 → 解析器(Parser) → 抽象语法树(AST) → 规则检查(Rules) → 报告问题

什么是 AST(抽象语法树)?

// 源代码
const a = 1

// 转换为 AST(简化版)
{
  type: 'VariableDeclaration',
  kind: 'const',
  declarations: [{
    type: 'VariableDeclarator',
    id: { type: 'Identifier', name: 'a' },
    init: { type: 'Literal', value: 1 }
  }]
}

ESLint 通过遍历 AST,检查每个节点是否符合规则。

2.2.4 配置系统演进
#

旧配置(ESLint < 9.0):

// .eslintrc.js
module.exports = {
  extends: ['eslint:recommended'],
  rules: {
    'semi': ['error', 'never'],
  }
}

新配置(ESLint >= 9.0):

// eslint.config.js
import antfu from '@antfu/eslint-config'

export default antfu({
  // 类型安全
  typescript: {
    tsconfigPath: 'tsconfig.json',
    overrides: {
      'ts/no-unsafe-assignment': 'off',
    }
  },

  // Vue 支持
  vue: true,

  // 格式化
  formatters: {
    css: true,
    html: true,
    markdown: 'prettier',
  },

  // 忽略文件
  ignores: [
    '**/dist',
    '**/node_modules',
    '**/.git',
  ],
})

2.2.5 常见问题和错误
#

问题 1:ESLint 不生效

症状:代码有问题但编辑器没有提示。

排查步骤:

# 1. 检查 ESLint 是否安装
npm list eslint

# 2. 检查配置文件是否存在
ls eslint.config.js

# 3. 检查 VS Code 插件是否安装
# 打开 VS Code 扩展面板,搜索 "ESLint"

# 4. 查看 VS Code 输出
# 打开输出面板(Ctrl+Shift+U),选择 "ESLint"

# 5. 手动运行 ESLint
npx eslint test.js

问题 2:规则太严格,到处都是错误

解决方案:

// eslint.config.js
import antfu from '@antfu/eslint-config'

export default antfu({
  // 关闭某些规则
  rules: {
    'no-console': 'off',  // 允许使用 console
    'no-debugger': 'warn',  // debugger 只警告不报错
    'vue/multi-word-component-names': 'off',  // 允许单词组件名
  },
})

问题 3:某些文件不想检查

解决方案:

// eslint.config.js
import antfu from '@antfu/eslint-config'

export default antfu({
  ignores: [
    '**/dist',
    '**/node_modules',
    '**/.git',
    '**/coverage',
    '**/*.min.js',
  ],
})

或创建 .eslintignore 文件:

# .eslintignore
dist
node_modules
*.min.js

问题 4:ESLint 和 Prettier 冲突

症状:ESLint 要求加分号,Prettier 自动删除分号。

解决方案:

# 方案 1:使用 @antfu/eslint-config(推荐)
# 它已经处理好了 ESLint 和 Prettier 的兼容

# 方案 2:手动配置
npm install -D eslint-config-prettier
// eslint.config.js
import prettier from 'eslint-config-prettier'

export default [
  // ... 其他配置
  prettier,  // 放在最后,关闭与 Prettier 冲突的规则
]

问题 5:某行代码想禁用 ESLint

// 禁用下一行的 ESLint 检查
// eslint-disable-next-line
console.log('debug')

// 禁用某个规则
// eslint-disable-next-line no-console
console.log('debug')

// 禁用整个文件
/* eslint-disable */

// 禁用文件中的某个规则
/* eslint-disable no-console */
注意

不要滥用 eslint-disable!

  • ✅ 偶尔用一两次:可以接受
  • ❌ 到处都是 eslint-disable:说明规则配置有问题

如果某个规则经常需要禁用,应该在配置文件中关闭它。

问题 6:如何找到合适的规则?

# 查看所有可用的规则
npx eslint --print-config eslint.config.js

# 查看某个文件应用的规则
npx eslint --print-config src/main.ts

# 在官网查找规则
# https://eslint.org/docs/latest/rules/

实用技巧:

// 错误级别:
// 'off' 或 0  - 关闭规则
// 'warn' 或 1 - 警告(不会导致程序退出)
// 'error' 或 2 - 错误(会导致程序退出)

export default antfu({
  rules: {
    'no-console': 'warn',  // 只警告,不报错
    'no-debugger': 'error',  // 报错
    'no-unused-vars': 'off',  // 关闭
  },
})
学习建议
  1. 先用默认配置:不要一开始就改配置
  2. 遇到问题再调整:看到报错后再决定是修代码还是改规则
  3. 理解规则意图:点击报错信息的链接,查看规则文档
  4. 逐步严格:一开始可以设置 warn,熟悉后改成 error

2.2.3 核心配置项深度解析
#

1. Parser(解析器)

export default {
  languageOptions: {
    parser: '@typescript-eslint/parser',
    parserOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      ecmaFeatures: {
        jsx: true,
        globalReturn: false,
      },
      // TypeScript 特有配置
      project: './tsconfig.json',
      extraFileExtensions: ['.vue'],
    }
  }
}

解析器选择:

  • espree:ESLint 默认,支持标准 JavaScript
  • @typescript-eslint/parser:TypeScript 专用
  • @babel/eslint-parser:需要 Babel 特性时
  • vue-eslint-parser:Vue SFC 文件

2. Plugins(插件)

import pluginVue from 'eslint-plugin-vue'
import pluginImport from 'eslint-plugin-import'
import pluginUnusedImports from 'eslint-plugin-unused-imports'

export default [
  {
    plugins: {
      vue: pluginVue,
      import: pluginImport,
      'unused-imports': pluginUnusedImports,
    },
    rules: {
      // 使用插件规则
      'vue/multi-word-component-names': 'off',
      'import/order': ['error', {
        'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
        'newlines-between': 'always',
      }],
      'unused-imports/no-unused-imports': 'error',
    }
  }
]

常用插件推荐:

插件作用推荐场景
@typescript-eslintTypeScript 支持TypeScript 项目必备
eslint-plugin-vueVue.js 支持Vue 项目必备
eslint-plugin-reactReact 支持React 项目必备
eslint-plugin-import导入语句检查大型项目推荐
eslint-plugin-unused-imports清理未使用导入提高代码质量
eslint-plugin-unicorn更多最佳实践追求代码质量
eslint-plugin-promisePromise 规范异步代码较多
eslint-plugin-nNode.js 规范Node.js 项目

3. Rules(规则)

export default [
  {
    rules: {
      // 错误级别:'off' | 0, 'warn' | 1, 'error' | 2
      
      // === 代码质量规则 ===
      'no-console': ['warn', { allow: ['warn', 'error'] }],
      'no-debugger': 'error',
      'no-unused-vars': ['error', { 
        vars: 'all',
        args: 'after-used',
        ignoreRestSiblings: true,
      }],
      'no-empty': ['error', { allowEmptyCatch: true }],
      'prefer-const': ['error', { destructuring: 'all' }],
      
      // === 代码风格规则 ===
      'semi': ['error', 'never'],
      'quotes': ['error', 'single', { avoidEscape: true }],
      'comma-dangle': ['error', 'always-multiline'],
      'indent': ['error', 2, { SwitchCase: 1 }],
      
      // === TypeScript 规则 ===
      'ts/no-explicit-any': 'warn',
      'ts/no-non-null-assertion': 'off',
      'ts/consistent-type-imports': ['error', {
        prefer: 'type-imports',
        disallowTypeAnnotations: false,
      }],
      
      // === Vue 规则 ===
      'vue/multi-word-component-names': 'off',
      'vue/no-v-html': 'off',
      'vue/require-default-prop': 'off',
      'vue/component-tags-order': ['error', {
        order: ['script', 'template', 'style'],
      }],
    }
  }
]

2.2.4 实现自定义规则
#

场景:禁止在代码中使用 console.time,团队统一使用性能监控 SDK。

// eslint-plugin-custom/rules/no-console-time.js
module.exports = {
  meta: {
    type: 'problem',
    docs: {
      description: '禁止使用 console.time,请使用性能监控 SDK',
      category: 'Best Practices',
    },
    messages: {
      noConsoleTime: '禁止使用 {{method}},请使用 Performance.measure()',
    },
    schema: [],
  },
  
  create(context) {
    return {
      // 访问成员表达式节点
      MemberExpression(node) {
        // 检查是否是 console.time 或 console.timeEnd
        if (
          node.object.name === 'console' &&
          (node.property.name === 'time' || node.property.name === 'timeEnd')
        ) {
          context.report({
            node,
            messageId: 'noConsoleTime',
            data: {
              method: `console.${node.property.name}`,
            },
          })
        }
      },
    }
  },
}

使用自定义规则:

// eslint.config.js
import customPlugin from './eslint-plugin-custom/index.js'

export default [
  {
    plugins: {
      custom: customPlugin,
    },
    rules: {
      'custom/no-console-time': 'error',
    },
  },
]

2.2.5 性能优化
#

问题:大型项目 ESLint 检查慢(> 30s)。

优化策略:

1. 缓存机制

# 启用缓存
eslint --cache --cache-location node_modules/.cache/eslint/

# package.json
{
  "scripts": {
    "lint": "eslint --cache ."
  }
}

2. 并行处理

# 使用多核并行检查
pnpm add -D eslint-parallel

# package.json
{
  "scripts": {
    "lint": "eslint-parallel 'src/**/*.{js,ts,vue}'"
  }
}

3. 增量检查

# 只检查变更的文件(配合 lint-staged)
pnpm add -D lint-staged

# .lintstagedrc
{
  "*.{js,ts,vue}": "eslint --fix"
}

4. 规则优化

// 关闭耗时的类型检查规则
export default [
  {
    rules: {
      // 这些规则需要完整的类型信息,很慢
      'ts/no-unsafe-assignment': 'off',
      'ts/no-unsafe-member-access': 'off',
      'ts/no-unsafe-call': 'off',
    }
  }
]

2.3 Prettier - 代码格式化
#

2.3.1 为什么需要 Prettier?
#

问题场景:

// 开发者 A 的代码
const user = {name: "Tom",age: 18,city: "Beijing"};
if(user.age>18){console.log("Adult")}

// 开发者 B 的代码
const user = {
  name: 'Tom',
  age: 18,
  city: 'Beijing',
}

if (user.age > 18) {
  console.log('Adult')
}

即使用了 ESLint,还是会遇到:

  • ❌ 代码风格不统一(换行、空格、引号)
  • ❌ 手动格式化很麻烦
  • ❌ Code Review 时浪费时间讨论格式问题

Prettier 的作用:

一个固执己见的代码格式化工具,保存文件时自动格式化,让团队不再纠结代码风格。

核心理念:

“You press save and code is formatted.”

保存即格式化,无需思考,无需讨论。

Prettier 解决的问题:

  1. 彻底统一代码风格

    • 所有人的代码看起来都一样
    • 不再有 tab vs space 的争论
    • 不再有单引号双引号的纠结
  2. 提高开发效率

    • 不需要手动调整格式
    • 保存时自动格式化
    • 可以随便写,反正会自动整理
  3. 减少 Code Review 时间

    • 不再讨论代码格式问题
    • 专注于代码逻辑和业务
  4. 降低心智负担

    • 写代码时不用考虑格式
    • 新人上手更容易

为什么有人不用 Prettier?

参考 Anthony Fu 的文章《为什么我不使用 Prettier》:

反对观点:

  1. ESLint 已经可以处理格式化
  2. Prettier 会强制一些不合理的格式
  3. 配置冲突问题
  4. 性能开销

支持观点:

  1. ESLint 格式化能力有限
  2. Prettier 的"零配置"哲学值得
  3. 团队统一比个人喜好重要
  4. 节省大量时间

我的建议:

  • 👥 团队项目:建议用 Prettier(统一比个性重要)
  • 👤 个人项目:可以不用(用 ESLint 的 --fix 也行)
  • 🆕 新项目:建议用(一开始就规范)
  • 🏛️ 老项目:慎用(格式化会产生大量 Git diff)

2.3.2 快速上手(5分钟)
#

第一步:安装 Prettier

npm install -D prettier

第二步:创建配置文件

// .prettierrc.js
export default {
  semi: false,              // 不要分号
  singleQuote: true,        // 单引号
  printWidth: 100,          // 每行最多 100 字符
  trailingComma: 'all',     // 尾随逗号
  arrowParens: 'avoid',     // 箭头函数参数只有一个时省略括号
  endOfLine: 'lf',          // 换行符
}

第三步:安装 VS Code 插件

搜索并安装 Prettier - Code formatter。

第四步:配置保存时自动格式化

打开 VS Code 设置,添加:

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true
}

第五步:测试是否生效

创建 test.js:

const user={name:"Tom",age:18};if(user.age>18){console.log("Adult")}

按 Ctrl+S 保存,应该自动格式化为:

const user = { name: 'Tom', age: 18 }
if (user.age > 18) {
  console.log('Adult')
}

✅ 自动格式化了,说明 Prettier 生效了!

2.3.3 ESLint + Prettier 集成方案
#

如果要用 Prettier:

pnpm add -D prettier eslint-config-prettier eslint-plugin-prettier
// .prettierrc.js
export default {
  // 基础配置
  semi: false,
  singleQuote: true,
  printWidth: 100,
  trailingComma: 'all',
  arrowParens: 'avoid',
  
  // 换行符
  endOfLine: 'lf',
  
  // Vue 特定
  vueIndentScriptAndStyle: false,
  
  // 覆盖特定文件
  overrides: [
    {
      files: '*.json',
      options: {
        printWidth: 80,
      }
    },
    {
      files: '*.md',
      options: {
        proseWrap: 'never',
      }
    }
  ]
}
// eslint.config.js
import prettier from 'eslint-plugin-prettier'
import configPrettier from 'eslint-config-prettier'

export default [
  configPrettier, // 关闭 ESLint 中与 Prettier 冲突的规则
  {
    plugins: {
      prettier,
    },
    rules: {
      'prettier/prettier': 'error', // 将 Prettier 错误显示为 ESLint 错误
    }
  }
]

2.4 Stylelint - 样式代码检查
#

2.4.1 为什么需要 Stylelint?
#

问题场景:

/* 不规范的 CSS 代码 */
.button{
  color:#ff0000;
  FONT-SIZE:16px;
  margin:0px 10px 0px 10px;
  background-color: #ff0000;  /* 和 color 重复 */
  disp1ay:flex;  /* 拼写错误 */
}

CSS 的问题:

  • ❌ 没有代码提示和检查
  • ❌ 拼写错误只有运行时才发现
  • ❌ 属性重复、顺序混乱
  • ❌ 颜色值不统一(#f00 vs #ff0000)
  • ❌ 简写属性不规范

Stylelint 的作用:

CSS/SCSS/Less 的代码检查工具,就像 ESLint 之于 JavaScript。

Stylelint 解决的问题:

  1. 语法错误检查

    • 拼写错误(disp1ay → display)
    • 属性值错误(color: 123px)
    • 选择器错误
  2. 代码质量检查

    • 重复属性检查
    • 无效属性检查
    • 颜色值统一(都用小写)
  3. 代码风格统一

    • 属性顺序统一
    • 缩进和空格统一
    • 引号统一
  4. 最佳实践

    • 禁止使用 ID 选择器
    • 限制选择器嵌套深度
    • 建议使用简写属性

2.4.2 配置指南
#

pnpm add -D stylelint stylelint-config-standard stylelint-config-recommended-vue
// stylelint.config.js
export default {
  extends: [
    'stylelint-config-standard',
    'stylelint-config-recommended-vue',
  ],
  
  rules: {
    // 颜色格式
    'color-hex-length': 'short',
    'color-no-invalid-hex': true,
    
    // 选择器
    'selector-class-pattern': '^[a-z][a-zA-Z0-9]*$', // camelCase
    'selector-max-id': 0, // 禁止 ID 选择器
    
    // 属性顺序
    'order/properties-order': [
      'position',
      'top',
      'right',
      'bottom',
      'left',
      'display',
      'flex',
      // ... 更多属性
    ],
    
    // Vue 特定
    'vue/no-deprecated-slot-attribute': true,
  },
  
  // 忽略文件
  ignoreFiles: [
    '**/*.js',
    '**/*.jsx',
    '**/*.tsx',
    '**/*.ts',
    'dist/**',
  ],
}

2.5 Git Hooks - 提交规范
#

2.5.1 为什么需要 Git Hooks?
#

问题场景:

# 团队成员的提交记录
git log --oneline

abc123f 修复bug
def456g update
ghi789h 。。。
jkl012m 111
mno345n 紧急修复!!!
pqr678s 改了点东西

这样的提交记录:

  • ❌ 看不出修改了什么
  • ❌ 无法追溯问题来源
  • ❌ 无法生成 CHANGELOG
  • ❌ 代码提交前没检查,提交了有问题的代码

更严重的问题:

// 开发者直接提交了这样的代码
const a=1
console.log(b)  // 未定义的变量
debugger  // 调试代码忘记删除

然后直接 git commit 推送到了远程仓库,导致:

  • 🔥 线上代码报错
  • ⏰ 影响其他人开发
  • 😰 紧急回滚和修复

Git Hooks 的作用:

在 Git 操作的关键节点自动执行检查,提交前拦截问题代码。

Git Hooks 解决的问题:

  1. 代码质量保障

    • 提交前自动运行 ESLint
    • 提交前自动运行测试
    • 确保有问题的代码无法提交
  2. 提交信息规范

    • 强制要求规范的提交信息
    • 自动生成 CHANGELOG
    • 方便代码回溯
  3. 团队协作

    • 统一的质量标准
    • 降低 Code Review 成本
    • 减少低级错误
  4. 自动化流程

    • 无需手动运行检查
    • 降低遗忘风险
    • 提高开发效率

常见的 Git Hooks:

Hook触发时机常用场景
pre-commit提交前运行 Lint、格式化、测试
commit-msg提交信息录入后验证提交信息格式
pre-push推送前运行完整测试
post-merge合并后自动安装依赖

2.5.2 Husky - Git Hooks 管理
#

安装配置:

pnpm add -D husky
pnpm exec husky init

目录结构:

.husky/
├── pre-commit          # 提交前执行
├── commit-msg          # 提交信息验证
├── pre-push            # 推送前执行
└── _/
    └── husky.sh

pre-commit:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# 运行 lint-staged
pnpm lint-staged

# 运行类型检查
pnpm type-check

# 运行测试
pnpm test:unit --run

commit-msg:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# 验证提交信息格式
pnpm commitlint --edit $1

2.5.3 lint-staged - 增量检查
#

为什么需要 lint-staged?

问题场景:

# 项目有 1000 个文件,你只修改了 2 个文件
# 但 pre-commit 检查所有 1000 个文件
npm run lint  # 耗时 30 秒 😰

结果:

  • ❌ 每次提交都很慢
  • ❌ 检查了大量无关文件
  • ❌ 开发体验很差

lint-staged 的作用:

只检查 Git 暂存区(staged)的文件,大幅提升速度。

# 只检查修改的 2 个文件
lint-staged  # 耗时 1 秒 ✅

配置:

// .lintstagedrc.js
export default {
  // JavaScript/TypeScript 文件
  '*.{js,jsx,ts,tsx}': [
    'eslint --fix',
    'vitest related --run', // 运行相关测试
  ],
  
  // Vue 文件
  '*.vue': [
    'eslint --fix',
    'stylelint --fix',
  ],
  
  // 样式文件
  '*.{css,scss,less}': [
    'stylelint --fix',
  ],
  
  // Markdown 文件
  '*.md': [
    'markdownlint --fix',
  ],
  
  // 图片压缩
  '*.{png,jpg,jpeg}': [
    'imagemin',
  ],
}

优化技巧:

// 防止 lint-staged 运行时间过长
export default {
  '*.{js,ts,vue}': (filenames) => {
    // 限制最多处理 50 个文件
    const files = filenames.length > 50 
      ? filenames.slice(0, 50) 
      : filenames
    
    return [
      `eslint --fix ${files.join(' ')}`,
      `vitest related ${files.join(' ')} --run`,
    ]
  },
}

2.5.3 Commitlint - 提交信息规范
#

安装:

pnpm add -D @commitlint/cli @commitlint/config-conventional

配置:

// commitlint.config.js
export default {
  extends: ['@commitlint/config-conventional'],
  
  rules: {
    // 类型枚举
    'type-enum': [
      2,
      'always',
      [
        'feat',      // 新功能
        'fix',       // 修复 bug
        'docs',      // 文档更新
        'style',     // 代码格式(不影响代码运行)
        'refactor',  // 重构
        'perf',      // 性能优化
        'test',      // 测试相关
        'build',     // 构建系统或外部依赖
        'ci',        // CI 配置
        'chore',     // 其他不修改 src 或 test 的更改
        'revert',    // 回退
      ],
    ],
    
    // 主题不能为空
    'subject-empty': [2, 'never'],
    
    // 主题最大长度
    'subject-max-length': [2, 'always', 100],
    
    // 类型必须小写
    'type-case': [2, 'always', 'lower-case'],
    
    // scope 必须小写
    'scope-case': [2, 'always', 'lower-case'],
  },
}

提交信息格式:

<type>(<scope>): <subject>

<body>

<footer>

示例:

feat(auth): 添加用户登录功能

- 实现用户名密码登录
- 添加登录状态管理
- 集成 JWT token 认证

Closes #123

2.5.4 Commitlint - 提交信息规范
#

为什么需要 Commitlint?

即使配置了 Husky,开发者仍然可以:

git commit -m "修复bug"        # 不规范
git commit -m "update"          # 看不出修改了什么
git commit -m "。。。"          # 完全没有信息

Commitlint 的作用:

强制要求规范的提交信息格式,不符合规范的提交会被拒绝。

# ❌ 会被拒绝
git commit -m "修复bug"
# Error: 提交信息不符合规范

# ✅ 会通过
git commit -m "fix(auth): 修复登录失败的问题"

安装:

pnpm add -D @commitlint/cli @commitlint/config-conventional

配置:

// commitlint.config.js
export default {
  extends: ['@commitlint/config-conventional'],
  
  rules: {
    // 类型枚举
    'type-enum': [
      2,
      'always',
      [
        'feat',      // 新功能
        'fix',       // 修复 bug
        'docs',      // 文档更新
        'style',     // 代码格式(不影响代码运行)
        'refactor',  // 重构
        'perf',      // 性能优化
        'test',      // 测试相关
        'build',     // 构建系统或外部依赖
        'ci',        // CI 配置
        'chore',     // 其他不修改 src 或 test 的更改
        'revert',    // 回退
      ],
    ],
    
    // 主题不能为空
    'subject-empty': [2, 'never'],
    
    // 主题最大长度
    'subject-max-length': [2, 'always', 100],
    
    // 类型必须小写
    'type-case': [2, 'always', 'lower-case'],
    
    // scope 必须小写
    'scope-case': [2, 'always', 'lower-case'],
  },
}

提交信息格式:

<type>(<scope>): <subject>

<body>

<footer>

示例:

feat(auth): 添加用户登录功能

- 实现用户名密码登录
- 添加登录状态管理
- 集成 JWT token 认证

Closes #123

为什么要这样做?

  1. 可追溯性:知道每次提交做了什么
  2. 自动化:可以自动生成 CHANGELOG
  3. 团队协作:统一的提交风格
  4. 问题定位:快速找到引入 bug 的提交

2.5.5 Commitizen - 交互式提交
#

为什么需要 Commitizen?

即使有了 Commitlint,手写规范的提交信息仍然:

  • ❌ 记不住格式
  • ❌ 容易写错 type
  • ❌ 不知道该写什么 scope

Commitizen 的作用:

提供交互式命令行界面,引导你写出规范的提交信息。

# 不用记格式,一步步选择
pnpm cz

? 选择提交类型: feat
? 选择改动范围: auth
? 输入提交信息: 添加用户登录功能
? 输入详细描述: (可选)
? 是否有破坏性更新: No
? 关联的 issue: #123

# 自动生成规范的提交信息
feat(auth): 添加用户登录功能

Closes #123

安装:

pnpm add -D commitizen cz-git

配置:

// .czrc.js
export default {
  alias: {
    fd: 'docs: fix typos',
  },
  
  messages: {
    type: '选择你要提交的类型 :',
    scope: '选择一个提交范围(可选):',
    customScope: '请输入自定义的提交范围 :',
    subject: '填写简短精炼的变更描述 :\n',
    body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
    breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
    footerPrefixSelect: '选择关联 issue 前缀(可选):',
    customFooterPrefix: '输入自定义 issue 前缀 :',
    footer: '列举关联 issue (可选) 例如: #31, #I3244 :\n',
    confirmCommit: '是否提交或修改 commit ?',
  },
  
  types: [
    { value: 'feat', name: 'feat:     ✨ 新功能', emoji: ':sparkles:' },
    { value: 'fix', name: 'fix:      🐛 Bug 修复', emoji: ':bug:' },
    { value: 'docs', name: 'docs:     📝 文档更新', emoji: ':memo:' },
    { value: 'style', name: 'style:    💄 代码格式', emoji: ':lipstick:' },
    { value: 'refactor', name: 'refactor: ♻️  代码重构', emoji: ':recycle:' },
    { value: 'perf', name: 'perf:     ⚡️ 性能优化', emoji: ':zap:' },
    { value: 'test', name: 'test:     ✅ 测试相关', emoji: ':white_check_mark:' },
    { value: 'build', name: 'build:    📦️ 构建相关', emoji: ':package:' },
    { value: 'ci', name: 'ci:       🎡 CI 配置', emoji: ':ferris_wheel:' },
    { value: 'chore', name: 'chore:    🔨 其他修改', emoji: ':hammer:' },
    { value: 'revert', name: 'revert:   ⏪️ 回退代码', emoji: ':rewind:' },
  ],
  
  scopes: [
    { value: 'auth', name: 'auth:       认证模块' },
    { value: 'user', name: 'user:       用户模块' },
    { value: 'order', name: 'order:      订单模块' },
    { value: 'components', name: 'components: 组件相关' },
    { value: 'utils', name: 'utils:      工具函数' },
    { value: 'deps', name: 'deps:       依赖更新' },
    { value: 'config', name: 'config:     配置文件' },
  ],
}

使用:

# 代替 git commit
pnpm cz

# 或者添加 npm script
{
  "scripts": {
    "commit": "cz"
  }
}

三、构建系统深度解析
#

3.1 模块化演进史
#

3.1.1 模块化发展历程
#

// === 1. 全局函数时代(史前时代)===
function sayHello() {
  console.log('Hello')
}

// 问题:全局污染、命名冲突、依赖关系不明确


// === 2. 命名空间模式 ===
var MyApp = {
  utils: {
    sayHello: function() {
      console.log('Hello')
    }
  }
}

// 问题:仍然是全局变量,本质上没解决污染


// === 3. IIFE 模式 ===
var module = (function() {
  var privateVar = 'private'
  
  return {
    publicMethod: function() {
      return privateVar
    }
  }
})()

// 优点:真正的私有作用域
// 问题:依赖关系仍不明确


// === 4. CommonJS(Node.js)===
// math.js
module.exports = {
  add: (a, b) => a + b
}

// main.js
const math = require('./math')
math.add(1, 2)

// 特点:同步加载,适合服务端


// === 5. AMD(RequireJS)===
define(['jquery', './math'], function($, math) {
  return {
    calculate: function() {
      return math.add(1, 2)
    }
  }
})

// 特点:异步加载,适合浏览器


// === 6. UMD(通用模块)===
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['jquery'], factory)
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS
    module.exports = factory(require('jquery'))
  } else {
    // 全局变量
    root.myModule = factory(root.jQuery)
  }
}(this, function ($) {
  return {
    method: function() {}
  }
}))


// === 7. ES Modules(现代标准)===
// math.js
export const add = (a, b) => a + b
export const subtract = (a, b) => a - b
export default { add, subtract }

// main.js
import math, { add } from './math.js'

// 特点:
// - 静态结构,利于 Tree Shaking
// - 异步加载
// - 浏览器和 Node.js 都支持

3.1.2 ESM vs CommonJS 深度对比
#

特性ESMCommonJS
加载时机编译时(静态)运行时(动态)
加载方式异步同步
输出值的引用(live binding)值的拷贝
循环依赖支持(更好)支持(有限制)
Tree Shaking✅ 支持❌ 不支持
顶层 thisundefinedmodule.exports
动态导入import()require()
浏览器支持✅ 原生支持❌ 需要打包

live binding 示例:

// === CommonJS ===
// counter.js
let count = 0
module.exports = {
  count,
  increment: () => count++
}

// main.js
const counter = require('./counter')
console.log(counter.count) // 0
counter.increment()
console.log(counter.count) // 0(值的拷贝,不会变)


// === ES Modules ===
// counter.js
export let count = 0
export const increment = () => count++

// main.js
import { count, increment } from './counter.js'
console.log(count) // 0
increment()
console.log(count) // 1(值的引用,会变)

3.2 Vite 深度解析
#

3.2.1 为什么需要 Vite?
#

传统开发的痛点(Webpack):

# 启动开发服务器
npm run dev

# 等待...(喝杯咖啡 ☕)
⏰ 启动耗时: 45 秒
⏰ 热更新耗时: 5 秒

# 修改一行代码
console.log('hello')

# 等待热更新...(又要等 🙄)
⏰ 热更新耗时: 5 秒

大型项目的体验:

  • ❌ 启动服务器要等几分钟
  • ❌ 每次改代码要等几秒才能看到效果
  • ❌ 开发体验极差,浪费时间

为什么 Webpack 慢?

启动时:分析所有依赖 → 编译所有模块 → 打包合并 → 启动服务器
        ▲______________慢(几分钟)______________▲

更新时:找到修改的文件 → 重新编译相关模块 → 重新打包 → 推送更新
        ▲______________慢(几秒)______________▲

3.2.2 Vite 如何解决这些问题?
#

Vite 的革新思路:

# 启动开发服务器
npm run dev

# 几乎秒启动 🚀
⚡ 启动耗时: 0.3 秒

# 修改代码
console.log('hello')

# 立即看到效果 ⚡
⚡ 热更新耗时: < 50 毫秒

核心原理:

1. 开发时:利用浏览器原生 ESM

启动 → 启动服务器(瞬间,不编译任何代码)
       ↓
     浏览器请求某个文件
       ↓
     编译这一个文件(esbuild,毫秒级)
       ↓
     返回给浏览器

传统方式 vs Vite:

// 传统方式(Webpack)
// 启动时就编译所有文件(1000+ 个文件)
app.js (500KB) ← 打包了所有模块

// Vite 方式
// 启动时不编译,浏览器请求哪个就编译哪个
main.js     ← 只编译入口文件
├─ App.vue  ← 浏览器请求时才编译
├─ Home.vue ← 浏览器请求时才编译
└─ About.vue ← 没请求,不编译

2. 使用 esbuild 预构建依赖

// 问题:lodash-es 有 600+ 个小文件
import { debounce } from 'lodash-es'

// 不预构建:浏览器要发 600+ 个请求 😰
GET /node_modules/lodash-es/debounce.js
GET /node_modules/lodash-es/delay.js
GET /node_modules/lodash-es/...
// 600+ 个请求

// Vite 预构建:合并成一个文件 ✅
GET /.vite/deps/lodash-es.js  // 只需 1 个请求

esbuild 为什么快?

  • 用 Go 语言编写(比 JavaScript 快 10-100 倍)
  • 多线程并行处理
  • 高效的算法和数据结构

3.2.3 Vite 解决的问题
#

问题传统工具Vite提升
启动速度30-60 秒< 1 秒30-60 倍
热更新速度3-10 秒< 50 毫秒60-200 倍
首次访问需要等待即时无等待
开发体验😡 难受😄 舒适质的飞跃

为什么 Vite 快?

传统构建工具(Webpack):

启动 → 分析依赖 → 编译所有模块 → 打包 → 启动服务器
        ▲________________慢(几分钟)________________▲

Vite 的方式:

启动 → 启动服务器(瞬间)
       ↓
     浏览器请求
       ↓
     按需编译单个模块(esbuild,毫秒级)

核心原理:

  1. Dev 开发时:

    • 利用浏览器原生 ESM 支持
    • esbuild 预构建依赖(Go 语言,比 JS 快 10-100 倍)
    • 按需编译,只编译当前路由用到的模块
  2. Build 生产时:

    • 使用 Rollup 打包(更成熟,产物更优)
    • 代码分割、Tree Shaking、压缩

3.2.2 依赖预构建
#

为什么需要预构建?

// lodash-es 有 600+ 个小模块
import { debounce } from 'lodash-es'

// 不预构建:浏览器会发起 600+ 个请求
// 预构建后:合并成一个文件,只需 1 个请求

预构建过程:

1. 扫描入口文件 → 找到所有依赖
2. esbuild 打包依赖 → node_modules/.vite/deps/
3. 添加强缓存 → 依赖不变,缓存永久有效

配置预构建:

// vite.config.ts
export default {
  optimizeDeps: {
    // 手动指定需要预构建的依赖
    include: [
      'vue',
      'vue-router',
      'pinia',
      'axios',
      'lodash-es',
    ],
    
    // 排除不需要预构建的依赖
    exclude: [
      'your-local-package',
    ],
    
    // esbuild 配置
    esbuildOptions: {
      target: 'es2020',
      plugins: [],
    },
    
    // 强制重新预构建
    force: true,
  },
}

3.2.3 HMR(热模块替换)
#

实现原理:

文件变更 → Vite 监听 → 编译模块 → WebSocket 推送 → 浏览器接收 → 更新模块

HMR API:

// src/utils/math.ts
export const add = (a: number, b: number) => a + b

// 接受自身模块的更新
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    console.log('math 模块已更新', newModule)
  })
}

Vue/React 的 HMR:

// Vite 插件自动处理
import { defineComponent } from 'vue'

export default defineComponent({
  setup() {
    // 组件内部状态会保留
    const count = ref(0)
    return { count }
  }
})

3.2.4 环境变量
#

# .env
VITE_APP_TITLE=My App

# .env.development
VITE_API_URL=http://localhost:3000

# .env.production
VITE_API_URL=https://api.example.com
// vite-env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string
  readonly VITE_API_URL: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}
// 使用
console.log(import.meta.env.VITE_APP_TITLE)
console.log(import.meta.env.MODE) // 'development' | 'production'
console.log(import.meta.env.DEV)  // boolean
console.log(import.meta.env.PROD) // boolean

3.2.5 插件开发
#

Vite 插件是 Rollup 插件的超集。

// vite-plugin-example.ts
import type { Plugin } from 'vite'

export function myPlugin(): Plugin {
  return {
    name: 'vite-plugin-example',
    
    // 应用模式:'serve' | 'build' | 'all'
    apply: 'build',
    
    // 执行顺序:'pre' | 'post' | undefined
    enforce: 'pre',
    
    // Vite 特有钩子:服务器启动
    configureServer(server) {
      server.middlewares.use((req, res, next) => {
        // 自定义中间件
        next()
      })
    },
    
    // Vite 特有钩子:开发时模块转换
    transform(code, id) {
      if (id.endsWith('.svg')) {
        // 将 SVG 转换为 Vue 组件
        return {
          code: `export default ${JSON.stringify(code)}`,
          map: null,
        }
      }
    },
    
    // Rollup 钩子:解析模块
    resolveId(id) {
      if (id === 'virtual-module') {
        return id
      }
    },
    
    // Rollup 钩子:加载模块
    load(id) {
      if (id === 'virtual-module') {
        return 'export default "虚拟模块"'
      }
    },
    
    // Vite 特有钩子:HMR
    handleHotUpdate({ file, server }) {
      if (file.endsWith('.custom')) {
        server.ws.send({
          type: 'custom',
          event: 'special-update',
          data: {}
        })
        
        return [] // 返回空数组表示自己处理了
      }
    },
  }
}

实战:自动导入图标组件:

// vite-plugin-icon-auto-import.ts
import type { Plugin } from 'vite'
import fs from 'node:fs'
import path from 'node:path'

export function iconAutoImport(): Plugin {
  const virtualModuleId = 'virtual:icons'
  const resolvedVirtualModuleId = '\0' + virtualModuleId

  return {
    name: 'vite-plugin-icon-auto-import',
    
    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId
      }
    },
    
    load(id) {
      if (id === resolvedVirtualModuleId) {
        // 读取 icons 目录下的所有 SVG
        const iconsDir = path.resolve(__dirname, 'src/assets/icons')
        const files = fs.readdirSync(iconsDir)
        const icons = files
          .filter(f => f.endsWith('.svg'))
          .map(f => f.replace('.svg', ''))
        
        // 生成导入代码
        const imports = icons.map(icon => 
          `import ${icon} from './assets/icons/${icon}.svg?component'`
        ).join('\n')
        
        const exports = `export default { ${icons.join(', ')} }`
        
        return `${imports}\n${exports}`
      }
    },
  }
}
// vite.config.ts
import { iconAutoImport } from './vite-plugin-icon-auto-import'

export default {
  plugins: [
    iconAutoImport(),
  ],
}
<!-- App.vue -->


<template>
  <component :is="icons.Home" />
  <component :is="icons.User" />
</template>

3.3 Rollup 深度解析
#

3.3.1 Rollup vs Webpack
#

特性RollupWebpack
目标场景库打包应用打包
Tree Shaking✅ 原生支持,效果更好✅ 支持,需配置
代码分割✅ 支持✅ 支持(更强大)
HMR❌ 不内置✅ 内置支持
插件生态精简丰富
产物体积更小较大
配置复杂度简单复杂

3.3.2 Rollup 配置详解
#

// rollup.config.js
import { defineConfig } from 'rollup'
import typescript from '@rollup/plugin-typescript'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import terser from '@rollup/plugin-terser'
import dts from 'rollup-plugin-dts'

export default defineConfig([
  // 主构建配置
  {
    // 入口文件
    input: 'src/index.ts',
    
    // 输出配置
    output: [
      // ESM 格式
      {
        file: 'dist/index.esm.js',
        format: 'esm',
        sourcemap: true,
      },
      // CommonJS 格式
      {
        file: 'dist/index.cjs.js',
        format: 'cjs',
        sourcemap: true,
        exports: 'named', // 导出模式
      },
      // UMD 格式(浏览器)
      {
        file: 'dist/index.umd.js',
        format: 'umd',
        name: 'MyLibrary', // 全局变量名
        sourcemap: true,
        globals: {
          // 外部依赖的全局变量映射
          vue: 'Vue',
          lodash: '_',
        },
      },
    ],
    
    // 外部依赖(不打包进产物)
    external: [
      'vue',
      'lodash',
      /^@vue\//,  // 正则匹配
    ],
    
    // 插件
    plugins: [
      // 解析 node_modules 中的模块
      resolve({
        extensions: ['.ts', '.js'],
        preferBuiltins: true,
      }),
      
      // 转换 CommonJS 模块为 ESM
      commonjs(),
      
      // TypeScript 支持
      typescript({
        tsconfig: './tsconfig.json',
        declaration: false, // 类型文件单独生成
      }),
      
      // 压缩
      terser({
        compress: {
          drop_console: true,
        },
      }),
    ],
    
    // Tree Shaking 配置
    treeshake: {
      moduleSideEffects: false,
      propertyReadSideEffects: false,
      unknownGlobalSideEffects: false,
    },
  },
  
  // 类型文件构建
  {
    input: 'src/index.ts',
    output: {
      file: 'dist/index.d.ts',
      format: 'esm',
    },
    plugins: [dts()],
  },
])

3.3.3 Tree Shaking 原理
#

什么是 Tree Shaking?

移除未使用的代码(Dead Code Elimination)。

前提条件:

  1. 使用 ESM(静态结构)
  2. 无副作用(side effects)

示例:

// utils.js
export const add = (a, b) => a + b
export const subtract = (a, b) => a - b
export const multiply = (a, b) => a * b

// main.js
import { add } from './utils.js'
console.log(add(1, 2))

// 打包后:multiply 和 subtract 会被删除

副作用问题:

// utils.js
export const add = (a, b) => a + b

// 副作用:修改全局变量
window.__APP_VERSION__ = '1.0.0'

// 即使没用到 add,这段代码也会保留

package.json 声明无副作用:

{
  "name": "my-library",
  "sideEffects": false,
  
  // 或指定有副作用的文件
  "sideEffects": [
    "*.css",
    "*.scss",
    "src/polyfills.ts"
  ]
}

3.3.4 实现一个简易 Rollup 插件
#

场景:自动为导出的函数添加性能监控。

// rollup-plugin-performance.js
export function performancePlugin() {
  return {
    name: 'rollup-plugin-performance',
    
    transform(code, id) {
      // 只处理 .js 和 .ts 文件
      if (!/\.(js|ts)$/.test(id)) return null
      
      // 正则匹配导出的函数
      const regex = /export (function|const|let|var) (\w+)/g
      let match
      const functions = []
      
      while ((match = regex.exec(code)) !== null) {
        functions.push(match[2])
      }
      
      if (functions.length === 0) return null
      
      // 生成包装代码
      const wrapperCode = functions.map(fnName => `
        const original_${fnName} = ${fnName}
        ${fnName} = function(...args) {
          const start = performance.now()
          const result = original_${fnName}.apply(this, args)
          const end = performance.now()
          console.log(\`[Performance] ${fnName} took \${end - start}ms\`)
          return result
        }
      `).join('\n')
      
      return {
        code: code + '\n' + wrapperCode,
        map: null,
      }
    },
  }
}

3.4 构建优化策略
#

3.4.1 代码分割(Code Splitting)
#

为什么需要代码分割?

不分割:main.js (2MB) → 加载慢,首屏白屏时间长
分割:main.js (100KB) + vendor.js (500KB) + page-a.js (200KB)
     → 首屏快,按需加载

Vite 自动分割:

// vite.config.ts
export default {
  build: {
    rollupOptions: {
      output: {
        // 手动分包
        manualChunks(id) {
          // node_modules 中的包分到 vendor
          if (id.includes('node_modules')) {
            // 大型库单独分包
            if (id.includes('lodash')) return 'lodash'
            if (id.includes('echarts')) return 'echarts'
            if (id.includes('@vue')) return 'vue-vendor'
            
            return 'vendor'
          }
          
          // 工具函数单独分包
          if (id.includes('/src/utils/')) {
            return 'utils'
          }
        },
        
        // 文件命名
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: '[ext]/[name]-[hash].[ext]',
      },
    },
    
    // chunk 大小警告阈值
    chunkSizeWarningLimit: 500,
  },
}

路由懒加载:

// router/index.ts
const router = createRouter({
  routes: [
    {
      path: '/about',
      // 懒加载,会生成独立的 chunk
      component: () => import('../views/About.vue'),
    },
    {
      path: '/user',
      // 使用魔法注释指定 chunk 名称
      component: () => import(
        /* webpackChunkName: "user" */
        '../views/User.vue'
      ),
    },
  ],
})

3.4.2 压缩优化
#

// vite.config.ts
import viteCompression from 'vite-plugin-compression'

export default {
  plugins: [
    // Gzip 压缩
    viteCompression({
      verbose: true,
      disable: false,
      threshold: 10240, // 10KB 以上才压缩
      algorithm: 'gzip',
      ext: '.gz',
    }),
    
    // Brotli 压缩(压缩率更高)
    viteCompression({
      algorithm: 'brotliCompress',
      ext: '.br',
    }),
  ],
  
  build: {
    // 生产环境移除 console
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
    
    // CSS 代码分割
    cssCodeSplit: true,
    
    // 禁用 CSS 内联
    assetsInlineLimit: 4096, // 4KB 以下内联为 base64
  },
}

3.4.3 构建分析
#

pnpm add -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'

export default {
  plugins: [
    visualizer({
      open: true, // 构建后自动打开
      gzipSize: true,
      brotliSize: true,
      filename: 'dist/stats.html',
    }),
  ],
}

四、包管理与 Monorepo
#

4.1 为什么需要包管理器?
#

问题场景:没有包管理器的时代

<!-- 2010 年的前端开发 -->
<!DOCTYPE html>
<html>
<head>
  <!-- 手动下载 jQuery,放到项目里 -->
  <script src="js/jquery-1.8.3.min.js"></script>
  <!-- 又要用 lodash,再手动下载 -->
  <script src="js/lodash-2.4.1.min.js"></script>
  <!-- 不知道用的是什么版本 -->
  <script src="js/some-plugin.js"></script>
</head>
</html>

遇到的问题:

  • ❌ 手动下载很麻烦
  • ❌ 版本管理混乱
  • ❌ 升级依赖很痛苦
  • ❌ 依赖关系不明确
  • ❌ 团队协作困难

包管理器解决的问题:

# 安装依赖
npm install jquery lodash

# package.json 记录版本
{
  "dependencies": {
    "jquery": "^3.7.1",
    "lodash": "^4.17.21"
  }
}

# 其他人只需要
npm install  # 自动安装相同版本的依赖

4.2 npm/pnpm/yarn 对比
#

4.2.1 为什么有这么多包管理器?
#

npm 的问题(2015 年前):

  • 🐢 安装慢(串行安装)
  • 💾 占用空间大(重复安装)
  • 🐛 依赖地狱(版本冲突)

Yarn 的诞生(2016 年):

  • ✅ 并行安装,速度快
  • ✅ 离线缓存
  • ✅ 锁定版本(yarn.lock)

Yarn 的诞生(2016 年):

  • ✅ 并行安装,速度快
  • ✅ 离线缓存
  • ✅ 锁定版本(yarn.lock)

pnpm 的革新(2017 年):

  • ✅ 硬链接,节省空间
  • ✅ 严格的依赖管理
  • ✅ 速度最快

4.2.2 三者对比
#

特性npmyarnpnpm
安装速度慢快最快
磁盘占用大(重复安装)大小(硬链接)
node_modules 结构扁平化扁平化嵌套 + 软链接
幽灵依赖❌ 存在❌ 存在✅ 不存在
Monorepo 支持WorkspacesWorkspacesWorkspaces(更好)

什么是幽灵依赖?

// package.json - 你只声明了 express
{
  "dependencies": {
    "express": "^4.18.0"
  }
}
// express 依赖了 body-parser
// npm/yarn 会将 body-parser 提升到顶层

// 你可以直接使用,但这是错的!
import bodyParser from 'body-parser' 
// ❌ package.json 中没有声明
// ❌ 如果 express 不再依赖 body-parser,你的代码就会报错

pnpm 如何解决?

// pnpm 不会提升依赖
import bodyParser from 'body-parser'
// ✅ 直接报错:找不到模块
// ✅ 强制你在 package.json 中声明

4.3 pnpm 原理
#

硬链接 + 符号链接:

node_modules/
├── .pnpm/
│   ├── express@4.18.0/
│   │   └── node_modules/
│   │       └── express/  (硬链接到全局 store)
│   └── body-parser@1.20.0/
│       └── node_modules/
│           └── body-parser/
└── express -> .pnpm/express@4.18.0/node_modules/express

优势:

  1. 节省磁盘空间(所有项目共享同一个 store)
  2. 安装速度快(不需要复制文件)
  3. 严格的依赖管理(避免幽灵依赖)

4.4 Monorepo 实战
#

4.4.1 为什么需要 Monorepo?
#

问题场景:传统多仓库(Multi-repo)

公司项目/
├── project-web/          (独立仓库)
├── project-admin/        (独立仓库)
├── project-mobile/       (独立仓库)
├── shared-components/    (独立仓库)
└── shared-utils/         (独立仓库)

遇到的问题:

1. 代码复用困难

# 修改了 shared-utils
cd shared-utils
git commit
npm version patch
npm publish

# 每个项目都要更新依赖
cd ../project-web
npm update shared-utils

cd ../project-admin
npm update shared-utils

cd ../project-mobile
npm update shared-utils

# 😰 要操作 3 次,容易漏!

2. 调试困难

# 调试 shared-components 的问题
cd project-web
npm link ../shared-components  # 手动链接
# 改代码 → 构建 → 刷新 → 重复...

# 😰 流程复杂,效率低

3. 版本管理混乱

// project-web/package.json
{
  "dependencies": {
    "shared-utils": "^1.2.0"
  }
}

// project-admin/package.json
{
  "dependencies": {
    "shared-utils": "^1.1.0"  // 😰 版本不一致!
  }
}

4. 团队协作困难

# 开发一个跨项目的功能
# 需要在 5 个仓库中分别创建 PR
# 需要 5 个仓库的维护者分别 Review
# 😰 流程繁琐,效率低下

4.4.2 Monorepo 如何解决?
#

Monorepo:单一仓库管理多个项目

my-monorepo/               (一个仓库)
├── packages/
│   ├── shared-components/
│   └── shared-utils/
└── apps/
    ├── web/
    ├── admin/
    └── mobile/

优势:

1. 代码复用简单

// apps/web/package.json
{
  "dependencies": {
    "@my-app/utils": "workspace:*"  // 自动使用本地版本
  }
}
# 修改 shared-utils
cd packages/shared-utils
# 改代码,保存

# 所有引用的项目立即生效 ✅
# 不需要发布,不需要更新依赖

2. 统一的工具链

// 根目录的 package.json
{
  "scripts": {
    "dev": "turbo run dev",      // 所有项目一起启动
    "build": "turbo run build",  // 所有项目一起构建
    "test": "turbo run test",    // 所有项目一起测试
    "lint": "turbo run lint"     // 所有项目一起检查
  }
}

3. 统一的版本管理

# 使用 Changesets 统一管理版本
pnpm changeset
pnpm changeset version  # 自动更新所有包的版本
pnpm changeset publish  # 一键发布所有包

4. 团队协作高效

# 跨项目功能开发
# 只需要一个 PR
# 只需要一次 Code Review
# 所有改动一起合并 ✅

5. 依赖管理统一

# 所有项目共享依赖
# React 只需要安装一次
# Vue 只需要安装一次
# 节省磁盘空间,加快安装速度

4.4.3 Monorepo 的适用场景
#

✅ 适合使用 Monorepo:

  • 多个项目有大量共享代码
  • 团队需要频繁跨项目协作
  • 需要统一的工具链和规范
  • 组件库 + 多个应用
  • 前端 + 后端 + 移动端

❌ 不适合使用 Monorepo:

  • 项目完全独立,没有共享代码
  • 团队规模小(< 3 人)
  • 不同项目的技术栈完全不同
  • 对构建性能要求极高

4.4.4 项目结构
#

my-monorepo/
├── package.json
├── pnpm-workspace.yaml
├── packages/
│   ├── ui/                    # 组件库
│   │   ├── package.json
│   │   └── src/
│   ├── utils/                 # 工具函数
│   │   ├── package.json
│   │   └── src/
│   └── cli/                   # CLI 工具
│       ├── package.json
│       └── src/
└── apps/
    ├── web/                   # Web 应用
    │   ├── package.json
    │   └── src/
    └── admin/                 # 管理后台
        ├── package.json
        └── src/

4.3.2 pnpm Workspace 配置
#

# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'apps/*'
  - '!**/__tests__/**'
// package.json(根目录)
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "dev": "pnpm --filter \"./apps/*\" dev",
    "build": "pnpm --recursive build",
    "test": "pnpm --recursive test",
    "lint": "pnpm --recursive lint"
  },
  "devDependencies": {
    "typescript": "^5.3.0",
    "vite": "^5.0.0"
  }
}

4.3.3 包之间的依赖
#

// packages/ui/package.json
{
  "name": "@my-app/ui",
  "version": "1.0.0",
  "dependencies": {
    "vue": "^3.4.0",
    "@my-app/utils": "workspace:*"  // 使用 workspace 协议
  }
}
// apps/web/package.json
{
  "name": "web",
  "dependencies": {
    "@my-app/ui": "workspace:*",
    "@my-app/utils": "workspace:*"
  }
}

安装依赖:

# 为特定包安装依赖
pnpm --filter @my-app/ui add lodash

# 为所有包安装依赖
pnpm add -D vitest -w  # -w 表示安装到根目录

# 运行特定包的脚本
pnpm --filter web dev

# 运行所有包的脚本
pnpm -r build  # -r 表示 recursive

4.3.4 Turborepo 加速构建
#

pnpm add -D turbo
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],  // 依赖包先构建
      "outputs": ["dist/**"],
      "cache": true
    },
    "dev": {
      "cache": false,
      "persistent": true  // 持续运行的任务
    },
    "lint": {
      "outputs": [],
      "cache": true
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"],
      "cache": true
    }
  }
}
// package.json
{
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev --parallel",
    "test": "turbo run test"
  }
}

Turborepo 特性:

  1. 远程缓存:团队共享构建缓存
  2. 增量构建:只构建变更的包
  3. 并行执行:多核并行构建
  4. 依赖感知:自动处理包之间的依赖关系

4.3.5 Changsets 版本管理
#

pnpm add -D @changesets/cli
pnpm changeset init
# .changeset/config.json
{
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "linked": [],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}

工作流程:

# 1. 开发新功能
git checkout -b feat/new-feature

# 2. 添加 changeset
pnpm changeset
# 选择变更的包
# 选择版本类型(major/minor/patch)
# 填写变更描述

# 3. 提交代码
git add .
git commit -m "feat: add new feature"

# 4. 发布前生成版本和 CHANGELOG
pnpm changeset version

# 5. 构建和发布
pnpm build
pnpm changeset publish

五、自动化与 CI/CD
#

5.1 为什么需要 CI/CD?
#

5.1.1 传统开发流程的痛点
#

没有 CI/CD 的开发流程:

# 开发完成,准备上线
# 第一步:手动运行检查
npm run lint       # ❌ 忘记运行了
npm run test       # ❌ 测试失败了,但没注意
npm run build      # ✅ 构建成功

# 第二步:手动部署
scp -r dist/* user@server:/var/www/  # 😰 需要记住命令
ssh user@server "pm2 restart app"     # 😰 需要手动重启

# 第三步:检查是否成功
# 打开浏览器,手动测试
# 😱 发现页面白屏!原来刚才测试失败了
# 😰 紧急回滚...

遇到的问题:

  • ❌ 容易忘记运行测试
  • ❌ 部署步骤复杂,容易出错
  • ❌ 不同人部署结果可能不同
  • ❌ 出问题后回滚困难
  • ❌ 没有部署记录

真实场景:

# 周五下午 5:30,准备下班
# 小明:我改了个小功能,直接发布了
git push

# 周五下午 5:35
# 客服:网站打不开了!!!
# 小明:😱 我忘记运行测试了
# 小明:😱 我也不知道怎么回滚

# 结果:加班到晚上 10 点才修好

5.1.2 CI/CD 如何解决?
#

CI(Continuous Integration)持续集成:

# 代码推送到 GitHub
git push

# 自动触发 CI 流程
✅ 自动安装依赖
✅ 自动运行 ESLint
✅ 自动运行测试
✅ 自动构建
❌ 如果任何步骤失败,拒绝合并

# 保证主分支代码永远是可用的

CD(Continuous Deployment)持续部署:

# 代码合并到主分支
git merge feature-branch

# 自动触发 CD 流程
✅ 自动构建生产版本
✅ 自动部署到服务器
✅ 自动发送通知
✅ 自动记录部署历史

# 一键回滚
git revert <commit>
# 自动回滚到上一个版本

CI/CD 的价值:

方面手动部署CI/CD
部署时间30-60 分钟3-5 分钟
出错概率高(人为失误)低(自动化)
回滚速度慢(手动操作)快(一键回滚)
可追溯性差(无记录)好(完整日志)
团队协作难(需要权限)易(自动化)

5.2 GitHub Actions 实战
#

5.2.1 基础配置
#

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, dev]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      
      - name: Run lint
        run: pnpm lint
  
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      
      - run: pnpm install --frozen-lockfile
      
      - name: Run tests
        run: pnpm test:coverage
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json
  
  build:
    runs-on: ubuntu-latest
    needs: [lint, test]  # 依赖前面的任务
    steps:
      - uses: actions/checkout@v4
      
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      
      - run: pnpm install --frozen-lockfile
      
      - name: Build
        run: pnpm build
      
      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: dist
          path: dist/

5.2.2 自动发布
#

# .github/workflows/publish.yml
name: Publish

on:
  push:
    tags:
      - 'v*'

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write
    steps:
      - uses: actions/checkout@v4
      
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
          registry-url: 'https://registry.npmjs.org'
      
      - run: pnpm install --frozen-lockfile
      
      - run: pnpm build
      
      - name: Publish to npm
        run: pnpm publish --no-git-checks
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
      
      - name: Create GitHub Release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          draft: false
          prerelease: false

5.2 部署自动化
#

5.2.1 Vercel
#

// vercel.json
{
  "buildCommand": "pnpm build",
  "outputDirectory": "dist",
  "devCommand": "pnpm dev",
  "installCommand": "pnpm install",
  "framework": "vite",
  "rewrites": [
    { "source": "/(.*)", "destination": "/" }
  ],
  "headers": [
    {
      "source": "/assets/(.*)",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    }
  ]
}

5.2.2 Netlify
#

# netlify.toml
[build]
  command = "pnpm build"
  publish = "dist"

[build.environment]
  NODE_VERSION = "20"
  NPM_FLAGS = "--prefix=/dev/null"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

[[headers]]
  for = "/assets/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

六、监控与调试
#

6.1 为什么需要 SourceMap?
#

6.1.1 生产环境的困境
#

问题场景:

// 你写的代码(src/index.ts)
function calculateTotal(items: Item[]) {
  const total = items.reduce((sum, item) => {
    return sum + item.price * item.quantity
  }, 0)
  return total
}

生产环境的代码(压缩后):

// dist/index.js
function c(a){return a.reduce((b,c)=>b+c.p*c.q,0)}

用户反馈:页面报错了!

# 浏览器控制台
Uncaught TypeError: Cannot read property 'p' of undefined
  at c (index.js:1:234)

你:😱 c (index.js:1:234) 是哪里?

  • ❌ 看不懂压缩后的代码
  • ❌ 不知道是哪个文件的哪一行
  • ❌ 无法调试和定位问题

为什么要压缩代码?

// 原始代码:15KB
function calculateTotal(items: Item[]) {
  const total = items.reduce((sum, item) => {
    return sum + item.price * item.quantity
  }, 0)
  return total
}

// 压缩后:2KB
function c(a){return a.reduce((b,c)=>b+c.p*c.q,0)}

// 体积减少 87%,加载更快!

但是:

  • ✅ 用户加载更快
  • ❌ 开发者无法调试

6.1.2 SourceMap 如何解决?
#

SourceMap 的作用:

建立压缩代码和源代码之间的映射关系。

压缩代码:c (index.js:1:234)
   ↓ (通过 SourceMap 映射)
源代码:calculateTotal (src/index.ts:3:15)

有了 SourceMap:

# 浏览器控制台
Uncaught TypeError: Cannot read property 'price' of undefined
  at calculateTotal (src/index.ts:3:15)
  #                   ↑ 清楚地看到原始文件和行号

你:✅ 是 src/index.ts 第 3 行的 item.price 你:✅ 原来是 item 可能为 undefined 你:✅ 快速定位和修复问题

6.1.3 SourceMap 类型选择
#

类型构建速度产物体积调试体验生产建议
eval最快无增加较差❌
eval-source-map较快大好❌
cheap-source-map快较大一般❌
source-map慢大最好✅
hidden-source-map慢大需上传✅ 推荐
nosources-source-map慢较小有限✅

生产环境推荐:hidden-source-map

// vite.config.ts
export default {
  build: {
    sourcemap: 'hidden', 
    // ✅ 生成 .map 文件
    // ✅ 但不在 JS 中引用(用户看不到)
    // ✅ 上传到监控平台(Sentry)
    // ✅ 只有开发者能用来调试
  },
}

为什么不直接暴露 SourceMap?

// ❌ sourcemap: true
// 用户可以下载 .map 文件
// 用户可以看到你的源代码
// 相当于开源了你的项目!

// ✅ sourcemap: 'hidden'
// 只生成 .map 文件,不公开
// 上传到 Sentry 等监控平台
// 只有开发者通过监控平台看到源码

6.1.4 生产环境 SourceMap 方案
#

// vite.config.ts
export default {
  build: {
    sourcemap: 'hidden', // 生成 .map 文件但不在 JS 中引用
  },
}
// 上传 SourceMap 到监控平台
import { uploadSourceMap } from './utils/sentry'

// vite.config.ts
export default {
  plugins: [
    {
      name: 'upload-sourcemap',
      closeBundle() {
        if (process.env.NODE_ENV === 'production') {
          uploadSourceMap({
            apiKey: process.env.SENTRY_API_KEY,
            org: 'my-org',
            project: 'my-project',
            dist: './dist',
          })
        }
      },
    },
  ],
}

6.2 为什么需要错误监控(Sentry)?
#

6.2.1 没有监控的困境
#

问题场景:

# 周一早上 9:00
# 老板:为什么周末有 100 个用户投诉网站打不开?
# 你:😱 我不知道啊,我周末没看

# 没有监控,你不知道:
# ❌ 什么时候出错的?
# ❌ 影响了多少用户?
# ❌ 是什么错误?
# ❌ 在哪个页面出错的?
# ❌ 用户用的什么浏览器?

用户投诉的问题:

用户A:网站打不开了
  → 什么时候?哪个页面?什么浏览器?
  用户:忘了...

用户B:点击按钮没反应
  → 能重现吗?控制台有报错吗?
  用户:我不懂技术...

用户C:页面白屏
  → 能截图吗?能看下控制台吗?
  用户:已经刷新了,现在正常了...

结果:

  • ❌ 问题无法重现
  • ❌ 不知道问题根源
  • ❌ 只能瞎猜

6.2.2 Sentry 如何解决?
#

Sentry 的作用:

自动收集和上报生产环境的错误,提供完整的错误信息和上下文。

有了 Sentry 之后:

# Sentry 自动发送告警邮件
🚨 新错误:Cannot read property 'price' of undefined
  • 发生时间:2026-01-25 14:32:15
  • 影响用户:23 人
  • 发生次数:156 次
  • 位置:src/pages/Cart.vue:45
  • 浏览器:Chrome 120
  • 操作系统:Windows 11
  • 用户操作:点击"结算"按钮
  • 用户信息:userId: 12345

Sentry 提供的信息:

  1. 完整的错误堆栈(通过 SourceMap)
  2. 用户操作记录(breadcrumbs)
  3. 浏览器和系统信息
  4. 错误发生的频率
  5. 受影响的用户数量
  6. 错误趋势图

价值:

  • ✅ 主动发现问题(不等用户投诉)
  • ✅ 快速定位问题(完整上下文)
  • ✅ 评估影响范围(多少用户受影响)
  • ✅ 追踪修复效果(错误是否减少)

6.3 Sentry 错误监控实战
#

pnpm add @sentry/vue
// main.ts
import * as Sentry from '@sentry/vue'

Sentry.init({
  app,
  dsn: 'YOUR_DSN',
  integrations: [
    new Sentry.BrowserTracing({
      routingInstrumentation: Sentry.vueRouterInstrumentation(router),
    }),
    new Sentry.Replay(),
  ],
  
  // 性能监控采样率
  tracesSampleRate: 1.0,
  
  // 录屏采样率
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  
  // 环境
  environment: import.meta.env.MODE,
  
  // 版本
  release: `my-app@${__APP_VERSION__}`,
  
  // 过滤错误
  beforeSend(event, hint) {
    // 忽略特定错误
    if (event.exception?.values?.[0]?.value?.includes('ResizeObserver')) {
      return null
    }
    return event
  },
})

七、学习资源
#

7.1 官方文档
#

7.2 开源项目
#

学习优秀项目的工程化实践:

7.3 推荐文章
#

7.4 实践建议
#

第一阶段:基础掌握(1个月)

  • 配置一个完整的项目脚手架
  • 实现 ESLint + Prettier + Husky
  • 理解 Vite 的基本原理
  • 学习 Git 工作流

第二阶段:深入理解(2-3个月)

  • 阅读 Vite 核心源码
  • 实现一个简易的 Vite 插件
  • 实现一个 ESLint 自定义规则
  • 搭建 Monorepo 项目

第三阶段:工程实践(持续)

  • 在团队中推广工程化最佳实践
  • 建立团队代码规范文档
  • 搭建 CI/CD 流水线
  • 建立性能监控体系

八、总结
#

前端工程化是一个系统工程,需要从工具使用 → 原理理解 → 实践优化逐步深入。

核心要点:

  1. 代码规范:建立自动化的代码质量保障体系
  2. 构建优化:理解构建原理,针对性优化
  3. 包管理:合理使用 Monorepo,提高代码复用
  4. 自动化:建立完整的 CI/CD 流程
  5. 监控运维:确保生产环境稳定性

学习建议:

  • 不要只停留在工具使用层面
  • 多阅读优秀开源项目的源码
  • 在实际项目中应用和验证
  • 持续关注前端技术发展

记住:工程化的目标是提高效率、保证质量,而不是为了工程化而工程化。选择合适的工具和方案,而不是追求"最新最全"。


持续更新

本文档会持续更新,欢迎反馈和补充!

相关文章