Files
test-repo/docs/zh-cn/appendix/frontend-engineering.md
T

773 lines
22 KiB
Markdown
Raw Normal View History

# 前端工程化与构建流水线
> 💡 **学习指南**:本章围绕一个问题展开:**如何把你写的一堆代码,变成用户浏览器里能跑、跑得快的网站?** 这就像是问:如何把原材料变成成品,还要保证质量、控制成本?
在开始之前,建议你先了解:
- **什么是模块化**:如果你还不熟悉 ES6 的 `import`/`export`,可以先了解一下基础概念。
- **命令行基础**:会用 `cd``npm` 等基础命令会帮助你更好地理解构建流程。
---
## 0. 引言:为什么前端越来越"重"了?
<BuildPipelineDemo />
还记得十年前的前端开发吗?那时候的我们:
- 写几个 HTML 页面,内嵌一些 CSS 和 JavaScript
- 直接把文件拖到浏览器里就能看效果
- 部署的时候,直接把文件夹上传到服务器
- 一个网站的总代码量可能也就几十 KB
但现在的前端开发,完全变了样:
- 我们用 TypeScript 代替 JavaScript,需要编译
- 我们用 Vue/React 的 JSX/SFC,需要转换
- 我们用 Sass/Less 写 CSS,需要预处理
- 我们用各种 npm 包,需要打包
- 一个中大型项目的依赖可能上千个,总大小几百 MB
**这就是"前端工程化"要解决的问题。**
<BundlerComparisonDemo />
### 1.1 前端工程的"三座大山"
现代前端工程主要面临三大挑战:
| 挑战 | 十年前 | 现在 | 解决方案 |
|------|--------|------|----------|
| **开发体验** | 刷新页面即可 | 热更新、类型检查、代码规范 | Vite、ESLint、TypeScript |
| **产物优化** | 无需优化 | Tree Shaking、代码分割、压缩 | Webpack、Rollup、Terser |
| **部署策略** | 直接上传 | CDN、缓存策略、版本控制 | CI/CD、Hash 文件名 |
### 1.2 为什么你需要了解构建流程?
你可能会说:"我用 Vite 或者 Create React App,开箱即用,为什么还需要了解这些?"
让我讲一个真实的故事:
> **小明的踩坑记**
>
> 小明是一个前端新人,公司用 Vite 搭建的项目。有一天,产品经理说首页加载太慢了,要优化。
>
> 小明一顿操作:图片压缩、路由懒加载、启用 Gzip... 但首页依然慢。
>
> 后来他请教师傅,师傅一看:`vendor.js` 有 2MB
>
> 原来小明为了用某个日期格式化函数,引入了 `moment.js` 整个库,而 `moment.js` 包含了 100 多种语言的 locale 文件。
>
> 解决方案:换成 `dayjs` 或者按需引入 `date-fns`。2MB 变成了 2KB。
>
> 小明从此明白:**不了解构建和打包原理,你连问题出在哪都不知道。**
---
## 2. 核心概念:构建、打包、转译都是啥?
在深入工具之前,让我们先搞清楚几个经常被混淆的概念。
### 2.1 转译(Transpile
**是什么?** 把一种编程语言(或其新版本)转换成另一种(或其旧版本)的过程。
**为什么需要?**
- 浏览器不支持最新的 ES2022 语法
- 要把 TypeScript 转成 JavaScript
- 要把 JSX/Vue SFC 转成纯 JS
**常见工具:**
- **Babel**:最老牌、生态最丰富的转译器
- **SWC**:用 Rust 写的,速度极快(比 Babel 快 20 倍)
- **esbuild**:Go 写的,也很快,但功能相对简单
**举个例子:**
```javascript
// 你写的 (ES2020+)
const result = data?.items?.map(item => item.name) ?? []
// Babel 转译后 (ES5)
var _data$items, _data$items$map
var result =
(_data$items$map =
(_data$items = data == null ? void 0 : data.items) == null
? void 0
: _data$items.map(function (item) {
return item.name
})) != null
? _data$items$map
: []
```
### 2.2 打包(Bundle
**是什么?** 把多个分散的模块文件合并成一个(或几个)文件的过程。
**为什么需要?**
- 浏览器原生不支持 ES 模块(虽然有 `type="module"`,但生产环境还是需要考虑兼容性)
- 减少 HTTP 请求数(HTTP/1.1 时代很重要,HTTP/2 有所改善但仍有限度)
- 可以做更多的优化(Tree Shaking、代码分割等)
**举个例子:**
```
源代码结构:
src/
├── index.js (import a, b)
├── utils/
│ ├── a.js (import c)
│ ├── b.js
│ └── c.js
└── components/
└── Button.vue
打包后:
dist/
└── bundle.js (包含所有代码,按正确顺序组织)
```
### 2.3 构建(Build
**是什么?** 这是一个更广义的词,通常包含**转译 + 打包 + 各种优化**的完整流程。
**一个完整的构建流程通常包括:**
1. **预编译**TypeScript → JavaScript、Sass → CSS
2. **代码检查**ESLint、类型检查
3. **依赖解析**:分析模块依赖关系
4. **转译**Babel 转换语法
5. **打包**:合并模块
6. **优化**:压缩、Tree Shaking、代码分割
7. **资源处理**:图片压缩、生成雪碧图
8. **产物生成**:输出到 dist 目录
### 2.4 三者的关系
用一个餐厅比喻来理解:
| 概念 | 餐厅比喻 | 实际作用 |
|------|----------|----------|
| **转译** | 把中文菜谱翻译成英文给外国厨师看 | 把新语法转成浏览器能懂的旧语法 |
| **打包** | 把各桌点的菜装成一个个外卖盒 | 把分散的模块文件合并成 bundle |
| **构建** | 从接单、做菜、打包到上菜的完整流程 | 从源代码到生产代码的完整转换过程 |
---
## 3. 实战案例:从0到1搭建工程化流程
讲了这么多概念,让我们看一个真实的案例:某创业公司如何从"直接写 HTML"进化到"现代化工程化流程"。
### 3.1 第一阶段:原始时代(痛点初现)
**背景**:小团队,3个前端,做一个管理后台
**当时的工作方式**
```
项目结构:
project/
├── index.html
├── login.html
├── css/
│ ├── bootstrap.css
│ └── custom.css
├── js/
│ ├── jquery.js
│ ├── bootstrap.js
│ └── app.js
└── images/
```
**遇到的问题**
1. **全局变量污染**:所有变量都在全局命名空间,经常冲突
2. **依赖管理混乱**jQuery 插件要先加载 jQuery,顺序错了就报错
3. **代码难以复用**:想复用某个功能,只能复制粘贴
4. **没有代码检查**:低级错误(如变量未定义)要运行后才发现
**当时的解决方案**(临时的、不优雅的):
```javascript
// 用自执行函数模拟模块化
var ModuleA = (function () {
var privateVar = 'private'
function privateFn() {
console.log(privateVar)
}
return {
publicMethod: function () {
privateFn()
}
}
})()
// 依赖管理靠注释说明
/**
* @requires jquery.js (must load first)
* @requires bootstrap.js
*/
```
### 3.2 第二阶段:引入模块化(初见曙光)
**转折点**:团队扩充到 8 人,项目变复杂,原生模块化(ES6)开始普及
**引入的工具**
1. **Webpack**:模块打包
2. **Babel**ES6+ 转译
3. **ESLint**:代码检查
4. **npm/yarn**:依赖管理
**新的项目结构**
```
src/
├── components/ # Vue/React 组件
│ ├── Button/
│ │ ├── index.js
│ │ ├── Button.vue
│ │ └── Button.test.js
│ └── Modal/
├── utils/ # 工具函数
│ ├── index.js
│ ├── date.js
│ └── http.js
├── services/ # API 服务
│ ├── user.js
│ └── order.js
├── assets/ # 静态资源
│ ├── images/
│ └── styles/
├── App.vue # 根组件
└── main.js # 入口文件
```
**Webpack 配置(简化版)**
```javascript
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js',
clean: true
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|svg|jpg|gif)$/,
type: 'asset/resource'
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html'
})
]
}
```
**带来的改善**
1. **模块化开发**:每个文件就是一个模块,通过 import/export 清晰管理依赖
2. **代码复用**:组件和工具函数可以在不同项目中复用
3. **代码质量**:ESLint 在保存时自动检查,类型检查(后来引入 TypeScript)在编译时发现问题
4. **性能优化**:Webpack 的代码分割、懒加载让首屏加载变快
### 3.3 第三阶段:现代化工具链(当前实践)
**新的痛点**Webpack 时代):
1. **构建速度慢**:项目大了以后,Webpack 冷启动要 30 秒以上
2. **配置复杂**:Webpack 配置往往几百行,新人难以上手
3. **HMR 慢**:修改代码后,热更新要等好几秒
**引入 Vite**:2021 年后,团队开始用 Vite 替代 Webpack
**Vite 的优势**
| 对比项 | Webpack | Vite |
|--------|---------|------|
| 冷启动 | 30s+ | <1s |
| HMR 更新 | 3-5s | <100ms |
| 配置复杂度 | 高(需大量配置) | 低(约定优于配置) |
| 构建产物 | 成熟稳定 | 现代浏览器优化 |
**现在的开发体验**
```bash
# 以前(Webpack
npm run dev
# 等待 30 秒...
# [INFO] Compiled successfully in 30123ms
# 修改代码 -> 保存 -> 等待 5 秒看到效果
# 现在(Vite
npm run dev
# 等待 300 毫秒...
# [INFO] ready in 312ms
# 修改代码 -> 保存 -> 瞬间看到效果
```
### 3.4 团队踩过的坑(真实教训)
**坑 1:依赖地狱**
```javascript
// 不要这样做:
import moment from 'moment' // 2.5MB
// 推荐做法:
import dayjs from 'dayjs' // 2KB
// 或者按需导入
import { format } from 'date-fns' // 按需打包
```
**坑 2Tree Shaking 失效**
```javascript
// 问题:这会引入整个 lodash
import _ from 'lodash'
_.debounce(fn, 200)
// 正确:只导入需要的函数
import debounce from 'lodash/debounce'
// 或者使用 lodash-es
import { debounce } from 'lodash-es'
```
**坑 3:缓存策略不当**
```javascript
// 错误:没有 hash,更新后用户可能还在用旧代码
import './utils.js'
// 正确:使用 content hash
import './utils.a3f7b2c.js'
// 现代工具会自动处理:
// Vite/Webpack 会自动添加 [contenthash]
```
---
## 4. 原理深入:构建工具的工作机制
了解了实际案例,让我们深入看看这些工具到底是怎么工作的。
### 4.1 Vite 为什么这么快?
Vite 的核心理念是:**利用浏览器原生的 ES 模块支持,让开发时无需打包**。
**传统打包工具的工作方式(如 Webpack)**
```
源代码 (100+ 文件)
[构建时打包]
Bundle (单个/几个大文件)
浏览器
```
问题:无论改多小的代码,都要重新打包整个项目。
**Vite 的工作方式**
```
源代码 (100+ 文件)
[不打包!直接按需编译]
浏览器 ← 按需加载每个模块
```
具体流程:
1. **冷启动**:Vite 启动时只做一些轻量级的预处理,不需要打包,所以秒开
2. **浏览器请求**:浏览器请求 `index.html`
3. **模块转换**:当浏览器通过 `<script type="module">` 请求 JS 文件时,Vite 拦截请求,实时转换代码:
-`import { a } from './a.js'` 中的路径解析正确
- 把 TypeScript/Vue/Sass 等实时编译成浏览器能懂的 JS/CSS
4. **HMR**:当你保存文件,Vite 通过 WebSocket 通知浏览器只更新那个模块,页面无需刷新
**为什么生产构建还是打包?**
开发时不打包是为了速度,但生产环境还是要打包的,因为:
- 减少 HTTP 请求数(即使是 HTTP/2,太多小文件也有开销)
- 可以进行更激进的优化(Tree Shaking、代码分割等)
- 兼容旧浏览器
Vite 生产构建使用 Rollup,生成高度优化的静态资源。
### 4.2 Webpack 的 loader 和 plugin 机制
Webpack 的核心设计理念是:**一切皆模块**。
Webpack 通过两个核心概念来扩展功能:
**Loader(加载器)**
- 职责:将非 JavaScript 模块转换为 Webpack 能处理的模块
- 执行时机:在模块加载时(构建阶段)
- 链式调用:一个文件可以被多个 loader 顺序处理
```javascript
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.vue$/, // 匹配 .vue 文件
loader: 'vue-loader' // 用 vue-loader 处理
},
{
test: /\.ts$/, // 匹配 .ts 文件
use: [
{
loader: 'ts-loader', // 先交给 ts-loader
options: {
transpileOnly: true // 只做转译,不做类型检查
}
}
]
},
{
test: /\.css$/, // 匹配 .css 文件
use: [
'style-loader', // 把 CSS 注入到 DOM
'css-loader', // 解析 CSS 中的 import
'postcss-loader' // 用 PostCSS 处理(加前缀等)
]
},
{
test: /\.(png|jpg|gif)$/, // 匹配图片
type: 'asset', // Webpack 5 内置的资源模块
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 小于 8KB 转成 base64
}
}
}
]
}
}
```
**Plugin(插件)**
- 职责:扩展 Webpack 的功能,可以干预构建过程的各个生命周期
- 执行时机:贯穿整个构建流程
- 能力更强:可以访问和修改 Webpack 的内部数据
```javascript
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [
// 1. 清理旧的构建产物
new CleanWebpackPlugin(),
// 2. 自动生成 HTML,并自动注入打包后的资源
new HtmlWebpackPlugin({
template: './public/index.html', // 以这个为模板
title: 'My App',
minify: {
removeComments: true,
collapseWhitespace: true
}
}),
// 3. 把 CSS 提取成独立文件(而不是内嵌在 JS 里)
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css'
}),
// 4. 分析包体积(只在分析模式启用)
...(process.env.ANALYZE
? [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: true
})
]
: [])
]
}
```
**Loader vs Plugin 总结**
| 对比项 | Loader | Plugin |
|--------|--------|--------|
| **职责** | 文件转换(TypeScript → JSSass → CSS | 功能扩展(生成 HTML、提取 CSS、清理文件) |
| **执行时机** | 模块加载时(对单个文件) | 贯穿整个构建生命周期 |
| **配置方式** | `module.rules` 数组 | `plugins` 数组 |
| **链式调用** | 支持多个 loader 串联 | 每个插件独立工作 |
| **举例** | `babel-loader``vue-loader``sass-loader` | `HtmlWebpackPlugin``MiniCssExtractPlugin` |
---
## 5. 实战模板:vite.config.js 完整配置
理论讲得差不多了,给你一个可直接使用的 Vite 配置模板:
```javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import AutoImport from 'unplugin-auto-import/vite'
import { visualizer } from 'rollup-plugin-visualizer'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
// 1. 基础配置
base: './', // 部署时的基础路径
publicDir: 'public', // 静态资源目录
// 2. 路径别名
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils'),
'@api': resolve(__dirname, 'src/api')
}
},
// 3. CSS 配置
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/vars.scss" as *;`
}
},
postcss: {
plugins: [
require('autoprefixer'),
require('postcss-nested')
]
}
},
// 4. 开发服务器配置
server: {
port: 3000,
open: true, // 自动打开浏览器
cors: true, // 允许跨域
proxy: {
// 代理 API 请求到后端
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
// 5. 构建配置
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: mode !== 'production', // 生产环境不生成 sourcemap
// Rollup 打包配置
rollupOptions: {
output: {
// 代码分割策略
manualChunks: {
// 把 vue 相关库打包到一起
'vue-vendor': ['vue', 'vue-router', 'pinia'],
// UI 组件库
'ui-vendor': ['element-plus'],
// 工具库
'utils-vendor': ['lodash-es', 'axios', 'dayjs']
},
// 入口文件命名
entryFileNames: 'js/[name]-[hash].js',
// 代码分割后的 chunk 命名
chunkFileNames: 'js/[name]-[hash].js',
// 资源文件命名
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.')
const ext = info[info.length - 1]
if (/\.(png|jpe?g|gif|svg|webp|ico)$/i.test(assetInfo.name)) {
return 'img/[name]-[hash][extname]'
}
if (/\.(woff2?|eot|ttf|otf)$/i.test(assetInfo.name)) {
return 'fonts/[name]-[hash][extname]'
}
return '[ext]/[name]-[hash][extname]'
}
}
},
// 压缩配置
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // 移除 console
drop_debugger: true // 移除 debugger
}
},
// 大于这个阈值的资源会被单独打包
chunkSizeWarningLimit: 500
},
// 6. 插件配置
plugins: [
// Vue 支持
vue(),
// 自动导入组件
Components({
resolvers: [ElementPlusResolver()],
dirs: ['src/components'],
deep: true
}),
// 自动导入 API
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
resolvers: [ElementPlusResolver()],
eslintrc: { enabled: true }
}),
// 打包分析(只在 ANALYZE 环境变量时启用)
mode === 'analyze' &&
visualizer({
open: true,
gzipSize: true,
brotliSize: true
})
].filter(Boolean)
}))
```
这个配置涵盖了:
- **开发体验**:路径别名、热更新、代理
- **代码质量**ESLint、TypeScript、自动导入
- **性能优化**:代码分割、Tree Shaking、压缩
- **部署友好**Hash 文件名、Source Map 控制
---
## 6. 总结对照表
最后,用一张表来总结前端工程化的核心概念:
| 概念 | 通俗解释 | 解决的问题 | 代表工具 |
|------|----------|------------|----------|
| **转译** | 把新语法翻译成旧语法 | 浏览器不支持新特性 | Babel、SWC、esbuild |
| **打包** | 把多个文件合并成一个 | 模块化、减少 HTTP 请求 | Webpack、Rollup、Vite |
| **构建** | 从源代码到生产代码的完整流程 | 自动化、优化、部署 | 上述所有 |
| **Tree Shaking** | 删除未使用的代码 | 减少包体积 | Webpack、Rollup |
| **Code Splitting** | 代码分割,按需加载 | 首屏加载优化 | Webpack、Vite |
| **HMR** | 热模块替换,不刷新更新 | 提升开发体验 | Webpack、Vite |
| **Source Map** | 映射压缩后的代码到源代码 | 调试 | 所有构建工具 |
**记住这张图**
![前端工程化流程](https://example.com/frontend-engineering-flow.png)
**简化的构建流程**
```
源代码 (TypeScript/Vue/Sass)
[转译] Babel/SWC (新语法 → 旧语法)
[打包] Webpack/Rollup/Vite (模块 → Bundle)
[优化] Terser/ESBuild (压缩、Tree Shaking)
[输出] dist/ (index.html + assets/)
[部署] CDN/服务器
```
---
## 名词对照表
| 英文术语 | 中文对照 | 解释 |
|----------|----------|------|
| **Bundle** | 包/打包产物 | 将多个模块合并后的输出文件 |
| **Chunk** | 代码块 | 代码分割后产生的独立文件 |
| **Transpile** | 转译 | 将高级语言/新语法转换为低级/旧语法 |
| **Minify** | 压缩 | 移除空白、缩短变量名等减小体积 |
| **Source Map** | 源映射 | 压缩代码与源代码的映射关系 |
| **HMR** | 热模块替换 | 不刷新页面实时更新修改 |
| **Tree Shaking** | 摇树优化 | 删除未使用的代码 |
| **Code Splitting** | 代码分割 | 将代码拆分成多个 chunk 按需加载 |
| **Lazy Loading** | 懒加载 | 需要时才加载资源 |
| **Prefetch** | 预获取 | 浏览器空闲时提前加载 |
| **Preload** | 预加载 | 高优先级提前加载当前页面需要的资源 |
| **Hash** | 哈希值 | 根据内容生成的唯一标识,用于缓存 |
| **Content Hash** | 内容哈希 | 根据文件内容计算的 hash,内容变则 hash 变 |
| **Module** | 模块 | 独立的代码单元,可导入导出 |
| **ESM** | ES 模块 | ES6 标准的模块化方案 (import/export) |
| **CJS** | CommonJS | Node.js 的模块规范 (require/module.exports) |
| **UMD** | 通用模块定义 | 兼容多种模块规范的格式 |
| **Polyfill** | 垫片/补丁 | 为旧环境添加新特性的兼容性代码 |
| **Babel** | 转译工具 | 将新语法转为旧语法的工具 |
| **SWC** | 快速转译器 | 基于 Rust 的超快转译工具 |
| **esbuild** | 快速打包器 | 基于 Go 的超快打包工具 |
| **Terser** | 压缩工具 | JavaScript 代码压缩器 |
---
## 写在最后
前端工程化是一个很大的话题,本文只能覆盖最核心的部分。记住以下几点:
1. **不要迷恋工具**Webpack、Vite、Rollup 都是工具,了解原理比会配置更重要
2. **从问题出发**:不要盲目上新技术,先找到痛点
3. **关注用户体验**:一切优化的最终目标都是让用户用得爽
希望这篇文章能帮助你建立起对前端工程化的整体认知。如果有任何疑问,欢迎交流!