773 lines
22 KiB
Markdown
773 lines
22 KiB
Markdown
|
|
# 前端工程化与构建流水线
|
|||
|
|
|
|||
|
|
> 💡 **学习指南**:本章围绕一个问题展开:**如何把你写的一堆代码,变成用户浏览器里能跑、跑得快的网站?** 这就像是问:如何把原材料变成成品,还要保证质量、控制成本?
|
|||
|
|
|
|||
|
|
在开始之前,建议你先了解:
|
|||
|
|
|
|||
|
|
- **什么是模块化**:如果你还不熟悉 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' // 按需打包
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**坑 2:Tree 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 → JS,Sass → 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** | 映射压缩后的代码到源代码 | 调试 | 所有构建工具 |
|
|||
|
|
|
|||
|
|
**记住这张图**:
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
**简化的构建流程**:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
源代码 (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. **关注用户体验**:一切优化的最终目标都是让用户用得爽
|
|||
|
|
|
|||
|
|
希望这篇文章能帮助你建立起对前端工程化的整体认知。如果有任何疑问,欢迎交流!
|