feat(docs): restructure appendix content into organized directories
- Move standalone AI-related files into 8-artificial-intelligence directory - Move development tools content into 2-development-tools directory - Move server/backend content into 4-server-and-backend directory - Create new index files for each section - Update .gitignore to exclude old backup directories - Update theme imports for new component locations
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# 无障碍与国际化
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,971 @@
|
||||
# 浏览器渲染管道
|
||||
::: tip 🎯 核心问题
|
||||
**为什么有些网页流畅如丝,有些却卡成PPT?** 浏览器是怎么把一堆HTML、CSS、JavaScript代码变成你眼前看到的网页的?本章将带你深入浏览器的"车间",理解它的工作流程,从而写出性能更好的网页。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要理解"渲染管线"?
|
||||
|
||||
### 1.1 从"能跑"到"跑得快":前端开发的进阶之路
|
||||
|
||||
刚开始学前端时,我们只关心代码"能不能跑"——页面能显示出来,按钮能点击,就算成功了。但随着项目变大,用户变多,你很快会发现一个残酷的现实:**同样的功能,有人写的页面丝般顺滑,有人写的却卡顿到用户想摔鼠标**。
|
||||
|
||||
这就像学开车。新手只关心"车能不能开动",但老司机会关心"什么时候该换挡、什么时候该刹车、怎么开最省油"。浏览器就是你开的那辆"车",理解它的"工作习性",你才能开得又快又稳。
|
||||
|
||||
<div style="display: flex; gap: 20px; margin: 20px 0;">
|
||||
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||
|
||||
**🐢 新手思维(只关注功能)**
|
||||
- 只要页面能显示就行
|
||||
- 卡顿是浏览器的问题
|
||||
- 性能优化是后期才考虑的事
|
||||
|
||||
</div>
|
||||
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||
|
||||
**🚀 进阶思维(关注体验)**
|
||||
- 流畅度是用户体验的核心
|
||||
- 理解浏览器工作流程
|
||||
- 写代码时就考虑性能
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
**理解渲染管线,就是从"能跑"到"跑得快"的关键一步。**
|
||||
|
||||
### 1.2 一个真实的踩坑故事:为什么"优化"后反而更卡了?
|
||||
|
||||
::: warning 小张的性能踩坑记
|
||||
小张是一家电商公司的前端工程师,负责优化商品详情页。这个页面展示商品信息时卡得要死,用户投诉不断。
|
||||
|
||||
小张想:"页面卡应该是因为DOM太多了,我先用`display:none`隐藏起来,修改完再显示,这样浏览器就不会重复渲染了吧?"
|
||||
|
||||
于是他写了这样的代码:
|
||||
|
||||
```javascript
|
||||
// 你以为的"优化"
|
||||
const container = document.getElementById('list')
|
||||
container.style.display = 'none' // 先隐藏,应该不会触发渲染了吧?
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const item = document.createElement('div')
|
||||
item.style.width = Math.random() * 100 + 'px' // 随机宽度
|
||||
container.appendChild(item)
|
||||
}
|
||||
|
||||
container.style.display = 'block' // 最后显示,一次性渲染
|
||||
```
|
||||
|
||||
结果测试后发现,页面**更卡了**!小张懵了:明明已经"优化"了,为什么反而更慢?
|
||||
|
||||
后来前端负责人看了代码,点出问题所在:**虽然元素被隐藏了,但你每次修改`style.width`仍然会触发浏览器的样式计算和布局标记,浏览器在后台做了大量无用功**。
|
||||
|
||||
正确的做法是用`DocumentFragment`在内存中批量操作,最后一次性插入DOM,只触发一次渲染。
|
||||
:::
|
||||
|
||||
::: info 💡 核心启示
|
||||
不了解浏览器的工作流程,你可能会"自作聪明"地写出一堆"优化代码",结果反而让性能更差。**理解渲染管线,你才知道哪些操作是昂贵的、哪些是廉价的,从而避免在错误的地方用力。**
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心概念:什么是"渲染管线"?
|
||||
|
||||
::: tip 🤔 什么是"渲染"?
|
||||
**渲染(Rendering)**,简单说就是浏览器把代码"画"成你看到的网页的过程。
|
||||
|
||||
你可以把它想象成**印刷厂印书**:
|
||||
- **HTML** = 书稿内容(文字、图片、章节)
|
||||
- **CSS** = 排版要求(字体大小、颜色、间距)
|
||||
- **JavaScript** = 动态修改(作者临时改稿、调整排版)
|
||||
|
||||
浏览器拿到这些"材料"后,要经过一道道"工序",最后才能"印刷"出你看到的网页。这一系列工序,就是**渲染管线(Rendering Pipeline)**。
|
||||
:::
|
||||
|
||||
为了帮你更好地理解,我们用一家**面包店**来比喻浏览器的渲染流程。
|
||||
|
||||
### 2.1 用面包店比喻理解渲染管线
|
||||
|
||||
想象你在经营一家面包店,每天要为顾客制作各种面包。这个过程中涉及到的环节,与浏览器的渲染流程惊人地相似:
|
||||
|
||||
| 阶段 | 🥖 面包店比喻 | 浏览器实际工作 | 具体例子 |
|
||||
|------|-------------|--------------|----------|
|
||||
| **1. 准备食材** | 整理原料清单(面粉、鸡蛋、奶油...) | **构建DOM树**:把HTML解析成树形结构 | 你写`<div><p>Hello</p></div>`,浏览器解析成`div→p→"Hello"`的树 |
|
||||
| **2. 准备配方** | 整理配方卡(每种面包的配料比例) | **构建CSSOM树**:把CSS解析成规则树 | 你写`.title { color: red }`,浏览器记录"`.title`的文字是红色" |
|
||||
| **3. 制定计划** | 根据原料和配方,决定今天要做什么面包 | **构建渲染树**:合并DOM和CSSOM,只保留可见元素 | `<script>`标签不显示,所以不在渲染树里 |
|
||||
| **4. 摆放位置** | 把面包摆到展示柜,决定每个面包放哪 | **布局(Layout)**:计算每个元素的尺寸和位置 | 算出"这个div宽200px、高100px,在屏幕的(50, 50)位置" |
|
||||
| **5. 上色装饰** | 给面包刷蛋液、撒芝麻、挤奶油 | **绘制(Paint)**:把元素的颜色、边框、阴影等"画"出来 | 把"红色文字"真正画到屏幕上 |
|
||||
| **6. 组装完成** | 把所有面包层叠在一起,摆成漂亮的样子 | **合成(Composite)**:把多个图层合并成最终画面 | GPU把背景层、文字层、图片层合并成一张完整画面 |
|
||||
|
||||
::: tip 📊 从表格中你能看到什么?
|
||||
让我们逐行解读这张表,理解渲染管线的每个阶段:
|
||||
|
||||
**阶段1-2(准备阶段)**:浏览器先"看懂"你的代码。HTML和CSS是分开解析的,因为它们职责不同——HTML决定"有什么内容",CSS决定"长什么样"。
|
||||
|
||||
**阶段3(合并阶段)**:为什么要"合并"?因为不是所有HTML元素都会显示(比如`<head>`、`<script>`),浏览器需要把"可见元素"和"它们的样式"结合在一起,形成一张"施工图"。
|
||||
|
||||
**阶段4-5(绘制阶段)**:布局是"算位置",绘制是"上颜色"。布局改变(比如改宽度)会导致绘制,但绘制改变(比如改颜色)不会导致布局。
|
||||
|
||||
**阶段6(合成阶段)**:现代浏览器的"魔法"。传统方式是"一次性画完"(CPU慢),现代方式是"分层绘制+GPU合成"(快),这就是为什么`transform`动画比`width`动画流畅的原因。
|
||||
:::
|
||||
|
||||
### 2.2 渲染管线的五个阶段
|
||||
|
||||
<RenderingPipelineDemo />
|
||||
|
||||
---
|
||||
|
||||
## 3. 第一阶段:构建DOM树和CSSOM树
|
||||
|
||||
### 3.1 为什么要"树"化?
|
||||
|
||||
::: tip 🤔 什么是DOM?
|
||||
**DOM(Document Object Model,文档对象模型)**,是浏览器把HTML文档转换成的一种树形结构,方便JavaScript操作页面元素。
|
||||
|
||||
你可以把它想象成**家谱树**:
|
||||
- 最顶端是"祖先"(`<html>`)
|
||||
- 下面是"子代"(`<body>`、`<head>`)
|
||||
- 再下面是"孙代"(`<div>`、`<p>`、`<span>`)
|
||||
|
||||
**为什么要转成树?** 因为树形结构很方便"查找"和"修改"。比如你想找到"所有class是`title`的元素",浏览器可以在树上快速搜索,而不是从一堆乱七八糟的文本里慢慢找。
|
||||
:::
|
||||
|
||||
浏览器拿到HTML后,不会马上显示,而是要先"理解"它。这个过程分为三步:
|
||||
|
||||
**第一步:词法分析——把代码拆成"词"**
|
||||
|
||||
```html
|
||||
<div class="container">
|
||||
<p>Hello World</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
浏览器看到这段代码,会先"拆词":
|
||||
- `<div>` → "开始标签div"
|
||||
- `class="container"` → "属性class,值container"
|
||||
- `<p>` → "开始标签p"
|
||||
- `Hello World` → "文本内容"
|
||||
- `</p>` → "结束标签p"
|
||||
- `</div>` → "结束标签div"
|
||||
|
||||
**第二步:语法分析——把"词"组装成"节点"**
|
||||
|
||||
浏览器根据HTML规则,把这些"词"组装成"节点":
|
||||
- 元素节点:`<div>`、`<p>`
|
||||
- 属性节点:`class="container"`
|
||||
- 文本节点:`"Hello World"`
|
||||
|
||||
**第三步:构建树——建立"父子关系"**
|
||||
|
||||
最后,浏览器根据标签的嵌套关系,构建出树形结构:
|
||||
|
||||
```
|
||||
Document(文档根节点)
|
||||
└── html
|
||||
└── body
|
||||
└── div.class = "container"
|
||||
└── p
|
||||
└── "Hello World"
|
||||
```
|
||||
|
||||
### 3.2 CSSOM树:样式的"规则手册"
|
||||
|
||||
::: tip 🤔 什么是CSSOM?
|
||||
**CSSOM(CSS Object Model,CSS对象模型)**,是浏览器把CSS规则转换成的树形结构,用来计算每个元素的最终样式。
|
||||
|
||||
你可以把它想象成**服装搭配指南**:
|
||||
- 上层规则(body的字体)会影响下层(所有子元素)
|
||||
- 如果有冲突(比如同一元素多个规则指定不同颜色),要按"优先级"决定用哪个
|
||||
- 最终算出每个元素该穿什么"衣服"
|
||||
:::
|
||||
|
||||
CSSOM的构建过程和DOM类似,但有一个关键区别:**CSS是"继承"和"层叠"的**。
|
||||
|
||||
::: details 查看CSSOM构建过程
|
||||
**原始CSS:**
|
||||
```css
|
||||
body {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
color: red; /* 会覆盖body的color */
|
||||
}
|
||||
|
||||
.container p {
|
||||
font-weight: bold;
|
||||
}
|
||||
```
|
||||
|
||||
**构建后的CSSOM树:**
|
||||
```
|
||||
StyleSheet
|
||||
├── body
|
||||
│ ├── font-size: 16px
|
||||
│ └── color: #333
|
||||
└── .container
|
||||
├── width: 100%
|
||||
├── color: red (优先级更高,覆盖body的color)
|
||||
└── p
|
||||
└── font-weight: bold
|
||||
```
|
||||
:::
|
||||
|
||||
### 3.3 踩坑实录:为什么我的CSS"不生效"?
|
||||
|
||||
**坑一:CSS选择器权重冲突**
|
||||
|
||||
::: details 查看常见错误
|
||||
```css
|
||||
/* 你写的CSS */
|
||||
#header { color: red; } /* id选择器,权重100 */
|
||||
.title { color: blue; } /* class选择器,权重10 */
|
||||
|
||||
/* HTML */
|
||||
<div id="header" class="title">这段文字是什么颜色?</div>
|
||||
```
|
||||
|
||||
你以为是蓝色,结果是**红色**。因为id选择器的权重(100)比class选择器(10)高。
|
||||
:::
|
||||
|
||||
**坑二:HTML标签没闭合,浏览器"自动修复"**
|
||||
|
||||
::: details 查看浏览器如何修复错误HTML
|
||||
```html
|
||||
<!-- 你写的HTML -->
|
||||
<div>
|
||||
<p>这是一段文字
|
||||
</div>
|
||||
|
||||
<!-- 浏览器修复后 -->
|
||||
<div>
|
||||
<p>这是一段文字</p> <!-- 浏览器自动帮你闭合标签 -->
|
||||
</div>
|
||||
```
|
||||
|
||||
浏览器很"宽容",会自动修复你的错误。但这种宽容是有代价的——浏览器需要额外计算来猜测你的意图,**会影响性能**。
|
||||
:::
|
||||
|
||||
<DomToRenderTreeDemo />
|
||||
|
||||
---
|
||||
|
||||
## 4. 第二阶段:构建渲染树
|
||||
|
||||
### 4.1 为什么需要"渲染树"?
|
||||
|
||||
你可能会问:**"已经有了DOM树和CSSOM树,为什么还要再构建一个渲染树?直接用DOM不行吗?"**
|
||||
|
||||
答案是:**DOM树包含了太多"无用"信息**。
|
||||
|
||||
比如下面这段HTML:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<title>页面标题</title>
|
||||
<style>/* CSS代码 */</style>
|
||||
<script>/* JavaScript代码 */</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<p>可见内容</p>
|
||||
</div>
|
||||
<div style="display: none">
|
||||
<p>隐藏内容(display:none)</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**DOM树会包含所有元素**:
|
||||
- `<head>`、`<title>`、`<style>`、`<script>`(这些不显示)
|
||||
- `display: none`的div(也不显示)
|
||||
|
||||
但**渲染树只包含"要画到屏幕上"的元素**:
|
||||
- 去掉`<head>`及其子元素
|
||||
- 去掉`display: none`的div
|
||||
|
||||
### 4.2 渲染树的构建规则
|
||||
|
||||
浏览器在构建渲染树时,会遵循一套规则:
|
||||
|
||||
| 场景 | 处理方式 | 示例 | 性能影响 |
|
||||
|------|---------|------|----------|
|
||||
| `display: none` | **完全排除**出渲染树 | 元素及其子元素都不可见 | ✅ 减少渲染工作量 |
|
||||
| `visibility: hidden` | **包含在渲染树中**,但不绘制 | 占据空间,但完全透明 | ⚠️ 仍需布局计算 |
|
||||
| `opacity: 0` | **包含在渲染树中**,但透明 | 可交互(能点击),但看不见 | ⚠️ 仍需布局计算 |
|
||||
| 不在视口内 | **包含在渲染树中**,暂不绘制 | 滚动到视口时才绘制 | ⚠️ 但仍在渲染树中 |
|
||||
|
||||
::: tip 📊 从表格中你能看到什么?
|
||||
**关键发现**:`display: none`是唯一"真正省性能"的隐藏方式,因为元素完全不在渲染树里,浏览器不会为它做任何布局和绘制工作。
|
||||
|
||||
而`visibility: hidden`和`opacity: 0`虽然"看不见",但仍在渲染树中,浏览器仍需计算它们的布局(占据空间)。如果你需要"隐藏但不影响布局"(比如做淡入淡出动画),可以用`opacity`;如果需要"完全隐藏且不占空间",用`display: none`。
|
||||
:::
|
||||
|
||||
### 4.3 踩坑实录:为什么设置了display:none,页面还是卡?
|
||||
|
||||
::: danger ❌ 常见误区:以为display:none的元素"不存在"
|
||||
很多人以为设置`display: none`后,元素就"消失"了,怎么操作都不会影响性能。这是**错误**的!
|
||||
|
||||
虽然`display: none`的元素不在渲染树中,但你通过JavaScript修改它的属性时,浏览器仍需要:
|
||||
1. **重新计算样式**(匹配CSS规则)
|
||||
2. **跟踪变化**(为未来显示做准备)
|
||||
|
||||
看下面这个"优化"例子:
|
||||
:::
|
||||
|
||||
::: details 查看"无效优化"的代码
|
||||
```javascript
|
||||
// ❌ 你以为的"优化":先隐藏,修改完再显示
|
||||
const container = document.getElementById('list')
|
||||
container.style.display = 'none'
|
||||
|
||||
// 疯狂操作DOM
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const item = document.createElement('div')
|
||||
item.style.width = Math.random() * 100 + 'px' // 改变宽度!
|
||||
item.textContent = `Item ${i}`
|
||||
container.appendChild(item)
|
||||
}
|
||||
|
||||
container.style.display = 'block'
|
||||
|
||||
// 问题:每次修改style.width,浏览器都要重新计算样式,
|
||||
// 即使元素是display:none!
|
||||
```
|
||||
|
||||
**✅ 正确的优化姿势:**
|
||||
```javascript
|
||||
// 使用DocumentFragment批量操作
|
||||
const container = document.getElementById('list')
|
||||
const fragment = document.createDocumentFragment() // 虚拟容器
|
||||
|
||||
// 所有操作都在内存中的fragment上进行
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const item = document.createElement('div')
|
||||
item.style.width = Math.random() * 100 + 'px'
|
||||
item.textContent = `Item ${i}`
|
||||
fragment.appendChild(item) // 不影响真实DOM
|
||||
}
|
||||
|
||||
// 一次性插入真实DOM,只触发一次渲染
|
||||
container.appendChild(fragment)
|
||||
```
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 第三阶段:布局与重排
|
||||
|
||||
### 5.1 什么是"布局"?
|
||||
|
||||
::: tip 🤔 什么是布局(Layout)?
|
||||
**布局**,也叫**回流(Reflow)**,是浏览器计算渲染树中每个元素"在什么位置、占多大空间"的过程。
|
||||
|
||||
你可以把它想象成**装修设计师测量房间**:
|
||||
- 先测量每个房间的长宽
|
||||
- 决定家具摆在哪里
|
||||
- 算出每个家具的坐标
|
||||
|
||||
**为什么布局很"贵"?** 因为一个元素的变化可能影响其他元素。比如你把一个div变宽了,它旁边的div可能被挤下去,导致整个页面重新计算。
|
||||
:::
|
||||
|
||||
### 5.2 触发重排的"雷区"
|
||||
|
||||
以下是常见的会触发重排的操作,**建议收藏并背诵**:
|
||||
|
||||
| 类别 | 属性/操作 | 性能影响 | 替代方案 |
|
||||
|------|----------|----------|----------|
|
||||
| **尺寸** | `width`, `height`, `min/max-width/height` | 💀💀💀 | 用`transform: scale()`代替 |
|
||||
| **位置** | `top`, `right`, `bottom`, `left` | 💀💀💀 | 用`transform: translate()`代替 |
|
||||
| **边距** | `margin`, `padding` | 💀💀 | 用`transform`或`gap`代替 |
|
||||
| **边框** | `border-width` | 💀💀 | 尽量避免频繁修改 |
|
||||
| **内容** | 文字内容变化、图片加载 | 💀💀 | 预留空间,避免布局抖动 |
|
||||
| **字体** | `font-size`, `line-height` | 💀💀💀 | 尽量避免频繁修改 |
|
||||
| **显示** | `display`值改变 | 💀💀💀 | 用`visibility`或`opacity`代替(如不需要完全隐藏) |
|
||||
| **查询** | `offsetWidth`, `offsetHeight`等 | 💀💀💀💀💀 | **批量读取,避免布局抖动** |
|
||||
|
||||
::: tip 📊 从表格中你能看到什么?
|
||||
**关键发现**:
|
||||
1. **几何属性(宽高位置)最昂贵**:它们会触发完整的布局计算
|
||||
2. **查询属性比修改更危险**:读取`offsetWidth`会**强制同步布局**(详见5.4节)
|
||||
3. **transform和opacity是性能最好的**:它们不触发重排,只触发合成
|
||||
:::
|
||||
|
||||
### 5.3 踩坑实录:为什么我的动画卡成PPT?
|
||||
|
||||
**坑:用width做动画**
|
||||
|
||||
::: details 查看性能差的动画代码
|
||||
```css
|
||||
/* ❌ 坏的动画:触发重排 */
|
||||
.box {
|
||||
width: 100px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.box:hover {
|
||||
width: 200px; /* 改变宽度会触发重排! */
|
||||
}
|
||||
```
|
||||
|
||||
每一帧动画都会触发重排,浏览器需要:
|
||||
1. 重新计算宽度
|
||||
2. 重新计算位置(可能影响其他元素)
|
||||
3. 重新绘制
|
||||
|
||||
**✅ 好的动画:用transform**
|
||||
```css
|
||||
/* ✅ 好的动画:只触发合成 */
|
||||
.box {
|
||||
width: 100px;
|
||||
transform: scaleX(1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.box:hover {
|
||||
transform: scaleX(2); /* 缩放不触发重排! */
|
||||
}
|
||||
```
|
||||
|
||||
`transform`直接由GPU处理,不会触发重排和重绘,动画丝般顺滑。
|
||||
:::
|
||||
|
||||
### 5.4 性能杀手:强制同步布局
|
||||
|
||||
::: danger 💀 最危险的性能问题:布局抖动
|
||||
**强制同步布局(Forced Synchronous Layout)**,也叫**布局抖动(Layout Thrashing)**,是最常见也是最严重的性能问题。
|
||||
|
||||
它的原因是:**JavaScript在读取布局属性(如`offsetWidth`)时,浏览器必须立即执行布局计算,才能返回准确值。**
|
||||
|
||||
如果你"读写交替",就会导致浏览器反复"布局→读取→布局→读取",形成恶性循环。
|
||||
:::
|
||||
|
||||
::: details 查看布局抖动的代码
|
||||
```javascript
|
||||
// ❌ 极坏:读写交替,导致布局抖动
|
||||
const elements = document.querySelectorAll('.item')
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const height = elements[i].offsetHeight // 读取 → 强制布局
|
||||
elements[i].style.width = (height * 2) + 'px' // 写入 → 标记需要重排
|
||||
// 下一次循环的读取又会强制布局...恶性循环!
|
||||
}
|
||||
|
||||
// 如果有100个元素,就会触发100次布局计算!
|
||||
```
|
||||
|
||||
**✅ 正确的优化姿势:读写分离**
|
||||
```javascript
|
||||
const elements = document.querySelectorAll('.item')
|
||||
|
||||
// 第一步:批量读取(先全部读完)
|
||||
const heights = []
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
heights.push(elements[i].offsetHeight) // 只触发一次布局
|
||||
}
|
||||
|
||||
// 第二步:批量写入(再全部写)
|
||||
requestAnimationFrame(() => {
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
elements[i].style.width = (heights[i] * 2) + 'px' // 只触发一次重排
|
||||
}
|
||||
})
|
||||
```
|
||||
:::
|
||||
|
||||
<LayoutReflowDemo />
|
||||
|
||||
---
|
||||
|
||||
## 6. 第四阶段:绘制与重绘
|
||||
|
||||
### 6.1 什么是"绘制"?
|
||||
|
||||
::: tip 🤔 什么是绘制(Paint)?
|
||||
**绘制**,是浏览器把"布局计算好"的元素真正"画"到屏幕上的过程。
|
||||
|
||||
你可以把它想象成**给房间刷漆**:
|
||||
- 布局阶段 = 量尺寸、画线
|
||||
- 绘制阶段 = 真正刷漆、贴壁纸
|
||||
|
||||
**绘制没有布局那么昂贵,但也不便宜。** 频繁绘制仍会影响性能,尤其是复杂元素(阴影、渐变等)。
|
||||
:::
|
||||
|
||||
### 6.2 触发重绘的信号
|
||||
|
||||
与重排不同,重绘只涉及"外观"的改变,不涉及"几何"的改变:
|
||||
|
||||
| 类别 | 属性 | 性能影响 | 备注 |
|
||||
|------|------|----------|------|
|
||||
| **颜色** | `color`, `background-color` | 💀 | 最常见的重绘触发者 |
|
||||
| **背景** | `background-image`, `background-position` | 💀💀 | 图片比纯色慢 |
|
||||
| **边框** | `border-color`, `border-style` | 💀 | 改变边框颜色/样式 |
|
||||
| **文字** | `text-decoration`, `text-shadow` | 💀💀 | 阴影比纯文字慢 |
|
||||
| **盒阴影** | `box-shadow` | 💀💀💀 | 复杂的阴影很慢 |
|
||||
| **圆角** | `border-radius` | 💀 | 改变圆角大小 |
|
||||
| **透明度** | `opacity` | ✅ | **特殊:不触发重绘,只触发合成** |
|
||||
|
||||
::: tip 📊 从表格中你能看到什么?
|
||||
**关键发现**:`opacity`是特殊的!它和`transform`一样,不会触发重绘,而是直接触发合成阶段。这就是为什么用`opacity`做淡入淡出动画性能最好的原因。
|
||||
|
||||
另外,**阴影和渐变比重绘更昂贵**,因为它们需要复杂的像素计算。如果你的页面有很多`box-shadow`,考虑用伪元素或图片代替。
|
||||
:::
|
||||
|
||||
### 6.3 踩坑实录:为什么我的hover效果卡?
|
||||
|
||||
**坑:用box-shadow做hover动画**
|
||||
|
||||
::: details 查看性能差的hover效果
|
||||
```css
|
||||
/* ❌ 坏的hover效果:box-shadow动画很慢 */
|
||||
.card {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); /* 阴影很慢! */
|
||||
}
|
||||
```
|
||||
|
||||
`box-shadow`需要逐像素计算,动画时会卡顿。
|
||||
|
||||
**✅ 好的做法:用transform或伪元素**
|
||||
```css
|
||||
/* ✅ 好的hover效果:用transform */
|
||||
.card {
|
||||
transform: translateY(0);
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px); /* 只在hover时改阴影,不做动画 */
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
<PaintLayerDemo />
|
||||
|
||||
---
|
||||
|
||||
## 7. 第五阶段:合成与GPU加速
|
||||
|
||||
### 7.1 什么是"合成"?
|
||||
|
||||
::: tip 🤔 什么是合成(Composite)?
|
||||
**合成**,是现代浏览器的"魔法",它把页面的不同部分分成多个**层(Layer)**,然后利用**GPU(图形处理器)**来并行合成最终的画面。
|
||||
|
||||
你可以把它想象成**Photoshop的图层**:
|
||||
- 传统方式 = 所有东西画在一层上(CPU串行,慢)
|
||||
- 合成方式 = 分层画,最后合并(GPU并行,快)
|
||||
|
||||
**为什么合成快?** 因为GPU擅长处理"图像合成"这种并行任务,比CPU快几十倍。
|
||||
:::
|
||||
|
||||
### 7.2 哪些元素会被提升到"合成层"?
|
||||
|
||||
浏览器会自动将某些元素提升到独立的合成层。以下是常见的触发条件:
|
||||
|
||||
| 触发条件 | CSS属性/值 | 性能影响 | 注意事项 |
|
||||
|---------|-----------|----------|----------|
|
||||
| **3D变换** | `transform: translate3d()`, `rotate3d()` | ✅✅✅ | 动画性能最佳 |
|
||||
| **硬件加速hack** | `transform: translateZ(0)` | ✅✅ | 俗称"强制GPU加速" |
|
||||
| **透明度动画** | `opacity`变化(配合动画) | ✅✅✅ | 不触发重绘 |
|
||||
| **固定定位** | `position: fixed` | ✅ | 避免滚动时重复布局 |
|
||||
| **Will-Change** | `will-change: transform, opacity` | ✅✅ | 提前创建层,注意内存 |
|
||||
| **Canvas/WebGL** | `<canvas>`, WebGL内容 | ✅✅ | 天然在独立层中 |
|
||||
| **Video** | `<video>` | ✅✅ | 独立层,防止相互影响 |
|
||||
|
||||
::: tip 📊 从表格中你能看到什么?
|
||||
**关键发现**:`transform`和`opacity`是性能最好的动画属性,因为它们不触发重排和重绘,直接触发合成。这就是为什么性能优化指南总是说"用transform和opacity做动画"。
|
||||
|
||||
但要注意:**每个合成层都要占用GPU内存**,滥用`translateZ(0)`会导致内存爆炸(详见7.4节)。
|
||||
:::
|
||||
|
||||
### 7.3 踩坑实录:合成层太多反而卡?
|
||||
|
||||
::: danger 💀 过度优化的陷阱
|
||||
有人听说"GPU加速快",就给所有元素都加`transform: translateZ(0)`,结果页面反而更卡了。
|
||||
|
||||
**问题原因**:
|
||||
每个合成层需要在GPU中存储一份"纹理"(位图),占用内存。如果一个页面有100个合成层,GPU内存可能被撑爆,导致低端设备崩溃或降级到CPU渲染。
|
||||
:::
|
||||
|
||||
::: details 查看"过度优化"的代码
|
||||
```css
|
||||
/* ❌ 错误做法:给所有元素都开启GPU加速 */
|
||||
.card { transform: translateZ(0); }
|
||||
.button { transform: translateZ(0); }
|
||||
.icon { transform: translateZ(0); }
|
||||
/* ... 100个元素都加 ... */
|
||||
|
||||
/* 结果:GPU内存爆炸,页面卡死 */
|
||||
```
|
||||
|
||||
**✅ 正确的做法:按需使用**
|
||||
```css
|
||||
/* 策略1:只给真正需要动画的元素开启 */
|
||||
.card {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px); /* 自动创建合成层 */
|
||||
}
|
||||
|
||||
/* 策略2:用will-change提示浏览器 */
|
||||
.card {
|
||||
will-change: transform; /* 提前创建层 */
|
||||
}
|
||||
|
||||
/* 策略3:动画结束后移除 */
|
||||
.card:not(:hover) {
|
||||
will-change: auto; /* 释放GPU内存 */
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
<CompositeDemo />
|
||||
|
||||
---
|
||||
|
||||
## 8. 事件循环:JavaScript的"分身术"
|
||||
|
||||
::: tip 🤔 什么是事件循环?
|
||||
**事件循环(Event Loop)**,是JavaScript实现"异步"的机制。因为JavaScript是**单线程**的(一次只能做一件事),但它又要处理用户点击、网络请求、定时器等多种任务,所以需要一套"调度系统"来管理这些任务。
|
||||
|
||||
你可以把它想象成**快递分拣中心**:
|
||||
- **Call Stack(调用栈)** = 当前正在处理的快递
|
||||
- **Web APIs** = 外部合作仓库(定时器、网络请求等)
|
||||
- **Callback Queue(回调队列)** = 待处理的快递架
|
||||
- **Event Loop(事件循环)** = 分拣机器人(不断检查"是否可以处理下一个任务")
|
||||
:::
|
||||
|
||||
### 8.1 宏任务与微任务
|
||||
|
||||
早期的JavaScript只有一套任务队列。但随着异步编程变复杂,浏览器引入了两类任务:
|
||||
|
||||
| 类型 | 常见来源 | 优先级 | 执行时机 |
|
||||
|------|---------|--------|----------|
|
||||
| **宏任务** | `setTimeout`/`setInterval`、I/O操作、UI渲染 | 低 | 每个事件循环周期执行一个 |
|
||||
| **微任务** | `Promise.then`、`MutationObserver` | 高 | 当前宏任务结束后,立即清空所有微任务 |
|
||||
|
||||
**执行顺序的"口诀"**:
|
||||
|
||||
```
|
||||
1. 执行当前宏任务(比如<script>整体)
|
||||
2. 执行过程中产生的所有微任务(Promise.then等)
|
||||
↳ 微任务可以产生新的微任务,全部清空后才继续
|
||||
3. 如果有需要,进行UI渲染(重排/重绘)
|
||||
4. 开启下一轮事件循环,执行下一个宏任务
|
||||
```
|
||||
|
||||
### 8.2 踩坑实录:Promise比setTimeout快?
|
||||
|
||||
::: danger ❌ 常见误解:setTimeout(fn, 0)会"立即"执行
|
||||
很多人以为`setTimeout(fn, 0)`是"0毫秒后立即执行",这是**错误**的理解。
|
||||
|
||||
实际上,`setTimeout(fn, 0)`的含义是:**"至少等待0毫秒后,将回调加入宏任务队列"**。但它需要等待当前调用栈清空、微任务队列清空、可能的UI渲染完成后,才能执行。
|
||||
:::
|
||||
|
||||
::: details 查看执行顺序
|
||||
```javascript
|
||||
console.log('1. Start')
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('2. setTimeout callback')
|
||||
}, 0)
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
console.log('3. Promise.then')
|
||||
})
|
||||
|
||||
console.log('4. End')
|
||||
|
||||
// 你以为的输出顺序:
|
||||
// 1. Start
|
||||
// 4. End
|
||||
// 2. setTimeout callback ← setTimeout(0)不是立即吗?
|
||||
// 3. Promise.then
|
||||
|
||||
// 实际的输出顺序:
|
||||
// 1. Start
|
||||
// 4. End
|
||||
// 3. Promise.then ← Promise.then比setTimeout先执行!
|
||||
// 2. setTimeout callback
|
||||
```
|
||||
|
||||
**执行流程图解:**
|
||||
```
|
||||
调用栈(Call Stack) 宏任务队列 微任务队列
|
||||
[setTimeout callback] [Promise.then callback]
|
||||
|
||||
1. console.log('1. Start')
|
||||
→ 输出: 1. Start
|
||||
|
||||
2. setTimeout(fn, 0)
|
||||
→ 将回调加入宏任务队列 ← [setTimeout callback]
|
||||
|
||||
3. Promise.resolve().then()
|
||||
→ 将回调加入微任务队列 ← [Promise.then callback]
|
||||
|
||||
4. console.log('4. End')
|
||||
→ 输出: 4. End
|
||||
|
||||
5. 调用栈清空,检查微任务队列
|
||||
→ 发现Promise.then回调
|
||||
→ 执行: console.log('3. Promise.then')
|
||||
→ 输出: 3. Promise.then
|
||||
|
||||
6. 微任务队列清空
|
||||
→ 可能需要UI渲染(如果有变化)
|
||||
|
||||
7. 检查宏任务队列
|
||||
→ 发现setTimeout回调
|
||||
→ 执行: console.log('2. setTimeout callback')
|
||||
→ 输出: 2. setTimeout callback
|
||||
```
|
||||
:::
|
||||
|
||||
::: tip 💡 核心启示
|
||||
**微任务比宏任务"更急"**。如果你希望某个操作在"当前代码块结束后、但UI更新前"尽快执行,用`Promise.then`或`queueMicrotask`。
|
||||
|
||||
`setTimeout(0)`不保证立即执行,它至少会被延迟到当前调用栈清空、微任务队列清空之后。
|
||||
:::
|
||||
|
||||
<EventLoopDemo />
|
||||
|
||||
<MacroMicroTaskDemo />
|
||||
|
||||
---
|
||||
|
||||
## 9. 性能优化实战:让你的网页"飞"起来
|
||||
|
||||
理解了渲染管线的工作流程后,我们来看看如何优化。以下是五个最实用的优化技巧。
|
||||
|
||||
### 9.1 黄金法则:避免强制同步布局
|
||||
|
||||
**问题**:交替读取和写入布局属性,导致布局抖动。
|
||||
|
||||
::: details 查看优化前后对比
|
||||
```javascript
|
||||
// ❌ 极坏:读写交替,导致布局抖动
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const height = elements[i].offsetHeight // 读取 → 强制布局
|
||||
elements[i].style.height = (height * 2) + 'px' // 写入 → 标记需要重排
|
||||
// 下一次循环的读取又会强制布局...恶性循环!
|
||||
}
|
||||
|
||||
// ✅ 极好:先全部读取,再全部写入
|
||||
// 第一步:批量读取
|
||||
const heights = []
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
heights.push(elements[i].offsetHeight)
|
||||
}
|
||||
|
||||
// 第二步:批量写入
|
||||
requestAnimationFrame(() => {
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
elements[i].style.height = (heights[i] * 2) + 'px'
|
||||
}
|
||||
})
|
||||
```
|
||||
:::
|
||||
|
||||
### 9.2 使用transform和opacity做动画
|
||||
|
||||
**问题**:用`width`、`height`、`left`、`top`做动画会触发重排。
|
||||
|
||||
::: details 查看优化前后对比
|
||||
```css
|
||||
/* ❌ 坏的动画:触发重排 */
|
||||
.box {
|
||||
transition: width 0.3s, left 0.3s;
|
||||
}
|
||||
.box.moving {
|
||||
width: 200px;
|
||||
left: 100px;
|
||||
}
|
||||
|
||||
/* ✅ 好的动画:只触发合成 */
|
||||
.box {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.box.moving {
|
||||
transform: translateX(100px) scaleX(2);
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
### 9.3 虚拟滚动:解决大数据列表
|
||||
|
||||
**问题**:列表项数量达到数千时,DOM节点数量过多导致性能问题。
|
||||
|
||||
**核心思想**:只渲染视口内可见的列表项(加上少量缓冲),DOM节点数量固定,与数据总量无关。
|
||||
|
||||
<RenderingPerformanceDemo />
|
||||
|
||||
::: details 查看虚拟滚动的实现
|
||||
```vue
|
||||
<template>
|
||||
<div class="virtual-list" @scroll="handleScroll">
|
||||
<!-- 占位元素,撑起滚动条 -->
|
||||
<div class="phantom" :style="{ height: totalHeight + 'px' }"></div>
|
||||
|
||||
<!-- 实际渲染的列表项 -->
|
||||
<div class="content" :style="{ transform: `translateY(${offsetY}px)` }">
|
||||
<div
|
||||
v-for="item in visibleItems"
|
||||
:key="item.id"
|
||||
class="item"
|
||||
:style="{ height: itemHeight + 'px' }"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
items: Array,
|
||||
itemHeight: { type: Number, default: 50 }
|
||||
})
|
||||
|
||||
const scrollTop = ref(0)
|
||||
const buffer = 5 // 缓冲数量
|
||||
|
||||
// 可视区域能显示多少项
|
||||
const visibleCount = computed(() => 10)
|
||||
|
||||
// 起始索引
|
||||
const startIndex = computed(() =>
|
||||
Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - buffer)
|
||||
)
|
||||
|
||||
// 结束索引
|
||||
const endIndex = computed(() =>
|
||||
Math.min(props.items.length, startIndex.value + visibleCount.value + buffer * 2)
|
||||
)
|
||||
|
||||
// 当前可视的数据
|
||||
const visibleItems = computed(() =>
|
||||
props.items.slice(startIndex.value, endIndex.value)
|
||||
)
|
||||
|
||||
// 总高度
|
||||
const totalHeight = computed(() => props.items.length * props.itemHeight)
|
||||
|
||||
// 偏移量
|
||||
const offsetY = computed(() => startIndex.value * props.itemHeight)
|
||||
|
||||
const handleScroll = (e) => {
|
||||
scrollTop.value = e.target.scrollTop
|
||||
}
|
||||
</script>
|
||||
```
|
||||
:::
|
||||
|
||||
### 9.4 防抖与节流:减少事件触发频率
|
||||
|
||||
**问题**:频繁触发的事件(如scroll、resize)会导致性能问题。
|
||||
|
||||
::: details 查看防抖与节流的实现
|
||||
```javascript
|
||||
// 防抖(Debounce):延迟执行,如果在延迟时间内再次触发,则重新计时
|
||||
function debounce(fn, delay) {
|
||||
let timer = null
|
||||
return function (...args) {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => fn.apply(this, args), delay)
|
||||
}
|
||||
}
|
||||
|
||||
// 节流(Throttle):固定时间间隔执行
|
||||
function throttle(fn, interval) {
|
||||
let lastTime = 0
|
||||
return function (...args) {
|
||||
const now = Date.now()
|
||||
if (now - lastTime >= interval) {
|
||||
lastTime = now
|
||||
fn.apply(this, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
window.addEventListener('scroll', debounce(handleScroll, 200))
|
||||
window.addEventListener('resize', throttle(handleResize, 100))
|
||||
```
|
||||
:::
|
||||
|
||||
### 9.5 懒加载:延迟加载非关键资源
|
||||
|
||||
**问题**:首屏加载太多资源导致页面打开慢。
|
||||
|
||||
::: details 查看懒加载的实现
|
||||
```javascript
|
||||
// 图片懒加载
|
||||
const lazyImages = document.querySelectorAll('img[data-src]')
|
||||
|
||||
const imageObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target
|
||||
img.src = img.dataset.src // 加载真实图片
|
||||
img.removeAttribute('data-src')
|
||||
observer.unobserve(img) // 停止观察
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
lazyImages.forEach(img => imageObserver.observe(img))
|
||||
```
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 10. 总结:渲染管线优化的本质
|
||||
|
||||
通过本文的学习,我们可以得出以下核心结论:
|
||||
|
||||
**从实践来看**:不是优化越多越好,而是优化越"对位"越好。理解浏览器的渲染管线,才能知道在哪里用力、在哪里放手。
|
||||
|
||||
**从成本视角看**:
|
||||
- 大部分性能浪费来自对布局属性的**频繁读写交替**,需要通过读写分离、批量处理来解决
|
||||
- 复杂的动画效果如果触发了重排和重绘,往往源于使用了"错误的属性",需要通过`transform`和`opacity`来解决
|
||||
- 面对大量数据的列表渲染,单纯依靠虚拟DOM已经不够,必须结合**虚拟滚动**等技术
|
||||
|
||||
**目标是:在给定的浏览器和硬件条件下,让每一个渲染步骤的投入都具备明确的性能收益。**
|
||||
|
||||
---
|
||||
|
||||
## 11. 名词对照表
|
||||
|
||||
| 英文术语 | 中文对照 | 解释 |
|
||||
| :--- | :--- | :--- |
|
||||
| **DOM** | 文档对象模型 | 浏览器将HTML文档解析后形成的树形结构,JavaScript可以通过DOM API操作页面元素 |
|
||||
| **CSSOM** | CSS对象模型 | 浏览器将CSS解析后形成的树形结构,与DOM结合用于计算最终样式 |
|
||||
| **Render Tree** | 渲染树 | 由DOM树和CSSOM树合并而成,只包含可见节点,用于后续的布局计算和绘制 |
|
||||
| **Layout** | 布局 | 计算渲染树中每个节点的几何信息(位置、大小)的过程,也称为Reflow(重排) |
|
||||
| **Reflow** | 重排/回流 | 当元素的尺寸、位置等几何属性发生变化时,浏览器需要重新计算布局的过程 |
|
||||
| **Paint** | 绘制/重绘 | 将布局计算后的元素样式(颜色、背景、边框等)绘制到屏幕上的过程 |
|
||||
| **Repaint** | 重绘 | 当元素的外观属性(如颜色、背景)变化但不影响几何属性时,触发的绘制更新 |
|
||||
| **Composite** | 合成 | 将多个绘制层(Layer)合并为最终屏幕图像的过程,通常在GPU上执行 |
|
||||
| **Layer** | 层/合成层 | 浏览器为了优化渲染而创建的独立绘制表面,可以单独变换和合成 |
|
||||
| **Event Loop** | 事件循环 | JavaScript的异步执行机制,负责调度宏任务和微任务的执行 |
|
||||
| **Call Stack** | 调用栈 | 记录当前正在执行的JavaScript函数的数据结构 |
|
||||
| **Macro Task** | 宏任务 | 事件循环中优先级较低的任务类型,如setTimeout、setInterval、I/O操作等 |
|
||||
| **Micro Task** | 微任务 | 事件循环中优先级较高的任务类型,如Promise.then、MutationObserver等 |
|
||||
| **Forced Synchronous Layout** | 强制同步布局 | 在JavaScript中交替读取和写入布局属性,导致浏览器被迫立即执行布局计算的性能问题 |
|
||||
| **Layout Thrashing** | 布局抖动 | 频繁的强制同步布局导致的性能急剧下降现象 |
|
||||
| **Virtual Scrolling** | 虚拟滚动 | 只渲染视口内可见列表项的技术,用于优化大数据列表的性能 |
|
||||
| **RAF** | 请求动画帧 | 浏览器提供的API,用于在下一次重绘前执行动画相关的JavaScript代码 |
|
||||
@@ -0,0 +1,352 @@
|
||||
# 浏览器是一个操作系统
|
||||
|
||||
> **学习指南**:本章节无需编程基础。我们将用**"网购"**的生活化比喻,配合**真实的技术过程**,带你一步步理解浏览器如何将一行网址变成丰富多彩的页面。
|
||||
|
||||
---
|
||||
|
||||
## 0. 引言:当你按下回车键的那一刻
|
||||
|
||||
想象你正在进行一次**网购**。你需要:
|
||||
|
||||
1. **填写订单**(选好商品,确认收货地址)
|
||||
2. **系统查找仓库**(根据店铺名找到具体的发货仓库)
|
||||
3. **建立物流通道**(确保仓库正常营业且能发货)
|
||||
4. **仓库发货**(快递员把包裹送上门)
|
||||
5. **拆箱体验**(打开包裹,看到心仪的商品)
|
||||
|
||||
**访问网页的过程和网购惊人地相似!**
|
||||
|
||||
当你在浏览器输入 `google.com` 并按下回车,你就是那个"买家",浏览器通过一系列操作,最终把远方服务器上的"商品"(网页内容)送到你的屏幕上。
|
||||
|
||||
<UrlToBrowserQuickStart />
|
||||
|
||||
---
|
||||
|
||||
## 1. 第一步:填写"订单" —— URL 解析
|
||||
|
||||
### 生活比喻:填写购物单
|
||||
|
||||
假设你只在订单上写"买鞋子",仓库肯定不知道发哪双。你需要写清楚:
|
||||
|
||||
- **店铺类型**(官方旗舰店/普通店)
|
||||
- **店铺名称**(Nike 官方店)
|
||||
- **商品位置**(男鞋区/跑鞋系列)
|
||||
- **具体型号**(Air Max 90)
|
||||
- **备注信息**(我要红色的)
|
||||
|
||||
### 真实过程:浏览器解析 URL
|
||||
|
||||
**URL(Uniform Resource Locator,统一资源定位符)**就是浏览器世界的"商品定位码"。当你在地址栏输入 `https://www.example.com:8080/path/page.html?id=123#section`,浏览器会立即拆解它:
|
||||
|
||||
| URL 部分 | 示例值 | 网购类比 | 技术作用 |
|
||||
| -------------------------- | -------------------- | -------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| **协议** `https://` | 安全超文本传输协议 | **物流方式**:保密配送(HTTPS)vs 普通配送(HTTP) | 决定使用什么规则通信。`http` 是普通传输,`https` 是加密传输 |
|
||||
| **域名** `www.example.com` | 服务器的人类可读名字 | **店铺名称**:京东超市 | 告诉浏览器要找哪台服务器。域名是为了让人记住,最终要转换成 IP 地址 |
|
||||
| **端口** `:8080` | 服务器的具体"门牌号" | **柜台编号**:3号柜台(默认不写) | 服务器上可能有多个服务,端口指定访问哪一个。HTTP 默认 80,HTTPS 默认 443 |
|
||||
| **路径** `/path/page.html` | 服务器上的文件位置 | **货架位置**:日用品区/第三排 | 指定服务器上的具体资源位置 |
|
||||
| **查询参数** `?id=123` | 附加信息 | **订单备注**:红色、XL码 | 传递给服务器的额外数据,如搜索关键词、页码等 |
|
||||
| **锚点** `#section` | 页面内的位置 | **说明书页码**:翻到第5页 | 页面加载后自动滚动到指定位置,不发送给服务器 |
|
||||
|
||||
<UrlParserDemo />
|
||||
|
||||
> **关键理解**:URL 的存在是为了让**人类**能记住和输入。计算机最终需要的是 **IP 地址**(就像快递员最终需要的是具体的仓库地址,而不是"Nike 官方店"这个名字)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 第二步:查"地址簿" —— DNS 查询
|
||||
|
||||
### 生活比喻:查仓库地址
|
||||
|
||||
你下单写的是"Nike 官方店",但物流系统不知道仓库在哪。它需要查地址簿:
|
||||
|
||||
1. 先查**常用地址**(最近买过这家吗)→ 浏览器缓存
|
||||
2. 没有的话问**小区快递点**(他们知道大区域的分配)→ 本地 DNS 服务器
|
||||
3. 问**总部调度中心**(知道.com类店铺归谁管)→ 根域名服务器
|
||||
4. 问**品牌管理处**(最终找到 Nike 店铺的真实发货仓库)→ 权威域名服务器
|
||||
|
||||
### 真实过程:DNS 分层查询
|
||||
|
||||
**DNS(Domain Name System,域名系统)**是互联网的"分布式地址簿查询系统"。由于全球有数十亿个域名,采用分层架构来分散查询压力:
|
||||
|
||||
```
|
||||
你(浏览器)
|
||||
↓ 问:google.com 的 IP 是多少?
|
||||
本地 DNS 服务器(你的网络运营商,如电信/联通)
|
||||
↓ 问:.com 归谁管?
|
||||
根域名服务器(全球13组根服务器,管理所有顶级域)
|
||||
↓ 告诉:去问 .com 的管理者
|
||||
顶级域服务器(Verisign 管理 .com)
|
||||
↓ 告诉:去问 google.com 的管理者
|
||||
权威域名服务器(Google 自己的 DNS 服务器)
|
||||
↓ 告诉:google.com 的 IP 是 142.250.80.46
|
||||
返回 IP 地址给浏览器
|
||||
```
|
||||
|
||||
**查询类型说明:**
|
||||
|
||||
- **递归查询(Recursive Query)**:浏览器只发一次请求,本地 DNS 负责层层查询后返回结果
|
||||
- **迭代查询(Iterative Query)**:每一层只告诉下一层去哪查,浏览器需要多次查询
|
||||
- **缓存机制**:查询结果会被缓存,下次直接返回,大大加速访问
|
||||
|
||||
<DnsLookupDemo />
|
||||
|
||||
> **为什么需要这么多层?** 想象一下如果全世界只有一个地址簿,几十亿人同时查,早就崩溃了。分层设计让每个层级只管理自己的"辖区",既高效又可靠。
|
||||
|
||||
---
|
||||
|
||||
## 3. 第三步:打电话确认 —— TCP 三次握手
|
||||
|
||||
### 生活比喻:建立物流通道
|
||||
|
||||
假设物流车直接开到仓库,结果:
|
||||
|
||||
- 仓库关门了 → 白跑一趟
|
||||
- 仓库爆仓不接单 → 无法发货
|
||||
- 找不到卸货口 → 无法对接
|
||||
|
||||
**所以在真正发货之前,必须先建立可靠的运输通道**。
|
||||
|
||||
### 真实过程:TCP 三次握手
|
||||
|
||||
**TCP(Transmission Control Protocol,传输控制协议)**是确保数据可靠传输的规则。在传输商品(数据)前,必须通过"三次握手"建立连接:
|
||||
|
||||
```
|
||||
客户端(你的电脑) 服务器(商家仓库)
|
||||
| |
|
||||
|--- SYN=1 --------------------->| 第1次:你好,我在家,准备收货!(SYN)
|
||||
| |
|
||||
|<-- SYN=1, ACK=1 ---------------| 第2次:收到!我也准备好发货了,你在家吗?(SYN-ACK)
|
||||
| |
|
||||
|--- ACK=1 --------------------->| 第3次:在的!请发货吧。(ACK)
|
||||
| |
|
||||
===== 通道建立,开始发货 =====
|
||||
```
|
||||
|
||||
**为什么是三次,不是两次?**
|
||||
|
||||
- **第一次(SYN)**:客户端证明自己能发送
|
||||
- **第二次(SYN-ACK)**:服务器证明自己能接收和发送
|
||||
- **第三次(ACK)**:客户端证明自己能接收
|
||||
|
||||
三次握手确保:**双方都能发、双方都能收** —— 四个条件都满足,才能可靠传输。
|
||||
|
||||
**TCP 还负责:**
|
||||
|
||||
- **数据分包**:大数据拆成小数据包传输
|
||||
- **顺序重组**:确保数据包按正确顺序组装
|
||||
- **错误重传**:丢包后自动重新发送
|
||||
- **流量控制**:根据网络状况调整发送速度
|
||||
|
||||
<TcpHandshakeDemo />
|
||||
|
||||
> **HTTPS 的额外步骤**:如果是 HTTPS(安全的网站),在 TCP 握手后还会进行 **TLS 握手**(1-RTT 或 2-RTT),双方交换加密密钥,确保之后的对话内容只有双方能看懂,就像用暗语通话。
|
||||
|
||||
---
|
||||
|
||||
## 4. 第四步:"买家"和"商家"的对话 —— HTTP 请求与响应
|
||||
|
||||
### 生活比喻:仓库发货
|
||||
|
||||
物流车到达仓库:"这是订单(HTTP请求),**我要取回商品(网页 HTML 源代码)!**"
|
||||
仓库管理员核对:"订单有效,这是你要的包裹(**HTML 文件**),请拿好。"
|
||||
|
||||
### 真实过程:HTTP 协议通信
|
||||
|
||||
**HTTP(HyperText Transfer Protocol,超文本传输协议)**是浏览器和服务器之间的"对话规则"。通道建立后,浏览器发送**取货请求**,**核心目标是拿回网页的源代码(HTML 文件)**:
|
||||
|
||||
**HTTP 请求示例:**
|
||||
|
||||
```http
|
||||
GET /index.html HTTP/1.1 ← 请求方法 + 路径 + 协议版本
|
||||
Host: www.example.com ← 目标主机(支持虚拟主机,一台服务器可托管多个网站)
|
||||
User-Agent: Chrome/120.0 ← 客户端标识(服务器可据此返回适配内容)
|
||||
Accept: text/html,application/xhtml+xml ← 可接受的响应格式
|
||||
Accept-Language: zh-CN,zh;q=0.9 ← 偏好的语言
|
||||
Accept-Encoding: gzip, deflate ← 支持的压缩格式
|
||||
Connection: keep-alive ← 保持连接(复用 TCP 连接)
|
||||
Cookie: session_id=abc123 ← 身份凭证
|
||||
```
|
||||
|
||||
::: tip 💡 开发者顿悟:这不就是 API 吗?
|
||||
**一模一样!**
|
||||
你平时写的 API 调用(`fetch` / `axios`)和浏览器访问网页,在 **HTTP 层面完全是同一个东西**。
|
||||
|
||||
它们都是发送一个请求,服务器返回一段文本数据。
|
||||
|
||||
- 如果服务器给的是 **HTML**,浏览器就把它**画出来**(变成网页)。
|
||||
- 如果服务器给的是 **JSON**,你的代码就把它**存起来**(用于逻辑处理)。
|
||||
|
||||
**根本就没有"两种"请求,只有同一种 HTTP 请求,只是返回的数据格式(Content-Type)不同而已。**
|
||||
这也是为什么理解了 HTTP,你就理解了 90% 的后端 API 原理。
|
||||
|
||||
如果你想深入学习 API 开发,请参考 [API 章节](./api-intro.md)。
|
||||
:::
|
||||
|
||||
**常见 HTTP 方法:**
|
||||
|
||||
- `GET`:获取资源(安全、幂等,可被缓存)
|
||||
- `POST`:提交数据(创建资源,如注册、登录)
|
||||
- `PUT`:更新资源(完整替换)
|
||||
- `PATCH`:部分更新资源
|
||||
- `DELETE`:删除资源
|
||||
- `HEAD`:获取响应头(不返回主体,用于检查资源是否存在)
|
||||
|
||||
**服务器返回 HTTP 响应:**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK ← 协议版本 + 状态码 + 状态描述
|
||||
Date: Mon, 23 May 2025 12:00:00 GMT ← 服务器时间
|
||||
Content-Type: text/html; charset=UTF-8 ← 内容类型和编码
|
||||
Content-Length: 1234 ← 内容长度(字节)
|
||||
Cache-Control: max-age=3600 ← 缓存策略
|
||||
Set-Cookie: user_id=xyz789 ← 设置 Cookie
|
||||
|
||||
```
|
||||
|
||||
**HTTP 状态码分类:**
|
||||
|
||||
| 状态码 | 类别 | 含义 | 生活类比 |
|
||||
| ----------- | ---------- | ---------------- | -------------------------------- |
|
||||
| **200** | 成功 | 请求成功处理 | "订单确认,马上发货" |
|
||||
| **301/302** | 重定向 | 资源已移动 | "本店搬家了,请去新店下单" |
|
||||
| **304** | 未修改 | 缓存仍有效 | "你上次买的还能用,不用重新发货" |
|
||||
| **400** | 客户端错误 | 请求格式错误 | "订单填写模糊,看不懂" |
|
||||
| **401** | 未授权 | 需要身份验证 | "请先出示会员卡" |
|
||||
| **403** | 禁止访问 | 权限不足 | "非内部人员禁止入内" |
|
||||
| **404** | 未找到 | 资源不存在 | "仓库里没这款商品" |
|
||||
| **500** | 服务器错误 | 服务器内部错误 | "仓库起火了,暂时发不了货" |
|
||||
| **502** | 网关错误 | 上游服务器无响应 | "总仓没货了,分仓也调不到" |
|
||||
| **503** | 服务不可用 | 服务器过载或维护 | "爆单了,暂停接单" |
|
||||
|
||||
<HttpExchangeDemo />
|
||||
|
||||
---
|
||||
|
||||
## 5. 第五步:拆开"包裹" —— 浏览器渲染
|
||||
|
||||
### 生活比喻:拆箱与组装
|
||||
|
||||
你终于收到了快递包裹(HTTP 响应),但打开一看,里面不是现成的家具,而是一堆**零件**(HTML)和一本**组装说明书**(CSS)。作为"买家"(浏览器),你需要亲自动手组装:
|
||||
|
||||
1. **拆开包装**:取出所有零件,核对清单(解析 HTML → DOM 树)。
|
||||
2. **阅读说明**:看懂说明书,知道哪个零件该装哪、什么颜色(解析 CSS → CSSOM 树)。
|
||||
3. **分类整理**:挑出需要组装的零件,扔掉包装泡沫(`display: none`),准备组装(构建渲染树)。
|
||||
4. **测量位置**:用尺子量好房间尺寸,决定每个家具具体摆在哪(布局/回流)。
|
||||
5. **上色装饰**:给家具刷漆、贴贴纸(绘制)。
|
||||
6. **最终展示**:打扫干净,开灯展示(合成)。
|
||||
|
||||
### 真实过程:浏览器渲染引擎
|
||||
|
||||
浏览器收到的是 **HTML/CSS/JavaScript 代码**(枯燥的文本),但它要变成**像素画面**(精美的网页)。这个过程叫做**渲染(Rendering)**,由浏览器的**渲染引擎**(如 Chrome 的 Blink、Safari 的 WebKit)执行。
|
||||
|
||||
#### 步骤1:解析 HTML → 构建 DOM 树 (零件清单)
|
||||
|
||||
浏览器读取 HTML 字节流,将其解析为**DOM(Document Object Model,文档对象模型)树**。这就像把一堆散乱的零件整理成一个有层级关系的清单:
|
||||
|
||||
```html
|
||||
<!-- 原始 HTML -->
|
||||
<div class="header">标题</div>
|
||||
<div class="content">内容</div>
|
||||
```
|
||||
|
||||
```text
|
||||
DOM 树结构:
|
||||
Document
|
||||
└─ html
|
||||
└─ body
|
||||
├─ div.header ("标题")
|
||||
└─ div.content ("内容")
|
||||
```
|
||||
|
||||
#### 步骤2:解析 CSS → 构建 CSSOM 树 (说明书)
|
||||
|
||||
浏览器解析所有的 CSS(内联、外部文件),构建**CSSOM(CSS Object Model)树**。这就像理解说明书上的样式规则:
|
||||
|
||||
```css
|
||||
.header {
|
||||
color: blue;
|
||||
font-size: 24px;
|
||||
} /* 标题要是蓝色的 */
|
||||
.content {
|
||||
display: none;
|
||||
} /* 内容暂时隐藏 */
|
||||
```
|
||||
|
||||
#### 步骤3:合并 → 渲染树 (准备组装)
|
||||
|
||||
DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
|
||||
关键点:**只有"可见"的元素才会在渲染树中**。
|
||||
|
||||
- `.header`:在渲染树中(可见)。
|
||||
- `.content`:**不在**渲染树中(因为 `display: none`,就像被扔掉的包装纸,不需要组装)。
|
||||
|
||||
#### 步骤4:布局 (Layout / Reflow) —— 测量尺寸
|
||||
|
||||
浏览器计算渲染树中每个节点在屏幕上的**精确坐标和大小**。
|
||||
|
||||
- "这个标题框宽 100px,高 50px,放在屏幕左上角 (0,0) 位置。"
|
||||
- 这个过程叫**重排 (Reflow)**。如果窗口大小变了(比如手机横屏),所有元素的位置都要重新计算,非常消耗性能。
|
||||
|
||||
#### 步骤5:绘制 (Paint) —— 上色
|
||||
|
||||
知道位置后,浏览器开始填充像素:画背景色、文字颜色、边框、阴影等。
|
||||
|
||||
#### 步骤6:合成 (Composite) —— 最终展示
|
||||
|
||||
现代浏览器会将页面分成多个**图层 (Layers)** 分别绘制(比如 3D 变换、滚动条独立图层),最后由 GPU 将它们像 Photoshop 图层一样叠加在一起,呈现在屏幕上。
|
||||
|
||||
<BrowserRenderingDemo />
|
||||
|
||||
> **💡 你知道吗?**
|
||||
>
|
||||
> **布局和绘制**是浏览器最忙碌的时候。网页里的元素越多、结构越复杂,浏览器就需要花更多时间来计算位置和上色。这就是为什么有的复杂网页打开会卡顿的原因。
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结:一次完整的"网购"之旅
|
||||
|
||||
让我们回顾整个旅程:
|
||||
|
||||
| 阶段 | 技术术语 | 网购类比 | 核心任务 | 关键技术 |
|
||||
| ----------- | ---------- | -------- | ------------------ | ------------------------------ |
|
||||
| **1. 解析** | URL 解析 | 填写订单 | 理解买家想买什么 | 协议、域名、端口、路径、参数 |
|
||||
| **2. 查询** | DNS 查询 | 查仓库址 | 找到店铺的发货仓库 | 递归/迭代查询、缓存机制 |
|
||||
| **3. 连接** | TCP 握手 | 建立通道 | 确保物流通畅 | 三次握手、序列号、流量控制 |
|
||||
| **4. 对话** | HTTP 交换 | 仓库发货 | 提交订单并收货 | 请求方法、状态码、头部字段 |
|
||||
| **5. 展示** | 浏览器渲染 | 拆箱组装 | 把商品展示出来 | DOM、CSSOM、渲染树、布局、绘制 |
|
||||
|
||||
**整个过程通常在几百毫秒内完成** —— 想想这有多么不可思议!
|
||||
|
||||
你的浏览器在不到1秒的时间里:
|
||||
|
||||
- 解析了一个复杂的地址
|
||||
- 查询了分布在全球的 DNS 服务器
|
||||
- 和千里之外的服务器建立了可靠连接
|
||||
- 进行了一次完整的 HTTP 对话
|
||||
- 把枯燥的代码变成了精美的画面
|
||||
|
||||
这就是互联网的魅力:**复杂的技术,简单的体验**。
|
||||
|
||||
---
|
||||
|
||||
## 7. 名词速查表 (Glossary)
|
||||
|
||||
| 名词 | 全称 | 简单解释 |
|
||||
| ----------- | ----------------------------- | -------------------------------------------------------------------------- |
|
||||
| **URL** | Uniform Resource Locator | **统一资源定位符**。网页的"地址",告诉浏览器去哪里找资源。 |
|
||||
| **DNS** | Domain Name System | **域名系统**。互联网的"电话簿",把人类可读的域名转换成机器可读的 IP 地址。 |
|
||||
| **IP 地址** | Internet Protocol Address | **互联网协议地址**。每台联网设备的唯一"门牌号",如 `192.168.1.1`。 |
|
||||
| **TCP** | Transmission Control Protocol | **传输控制协议**。确保数据可靠传输的"规则",通过三次握手建立连接。 |
|
||||
| **HTTP** | HyperText Transfer Protocol | **超文本传输协议**。浏览器和服务器"对话"的规则。 |
|
||||
| **HTTPS** | HTTP Secure | **安全的 HTTP**。在 HTTP 基础上加了加密(TLS/SSL),保护数据安全。 |
|
||||
| **HTML** | HyperText Markup Language | **超文本标记语言**。网页的"骨架",定义内容的结构。 |
|
||||
| **CSS** | Cascading Style Sheets | **层叠样式表**。网页的"皮肤",定义内容的外观。 |
|
||||
| **DOM** | Document Object Model | **文档对象模型**。浏览器把 HTML 转换成的树形结构,方便操作。 |
|
||||
| **CSSOM** | CSS Object Model | **CSS 对象模型**。浏览器把 CSS 转换成的树形结构。 |
|
||||
| **渲染** | Rendering | 浏览器把代码转换成屏幕像素的过程。 |
|
||||
| **RTT** | Round Trip Time | **往返时间**。数据包从发送到接收确认的时间,影响网页加载速度。 |
|
||||
|
||||
---
|
||||
|
||||
> **恭喜!** 现在当你再次在地址栏输入网址时,你已经能看到屏幕背后的那个忙碌而精彩的数字世界了。
|
||||
> **恭喜!** 现在当你再次在地址栏输入网址时,你已经能看到屏幕背后的那个忙碌而精彩的数字世界了。
|
||||
@@ -0,0 +1,879 @@
|
||||
# 前端工程化全貌
|
||||
::: tip 🎯 核心问题
|
||||
**如何把你写的代码,变成用户浏览器能跑的网站?** 这就像是问:如何把原材料变成成品,还要保证质量、控制成本?本章将带你深入理解前端工程化的核心概念和构建流程。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要"工程化"?
|
||||
|
||||
### 1.1 从简单到复杂:前端开发的演变
|
||||
|
||||
回顾十年前的前端开发,那时候的我们工作方式非常简单:写几个 HTML 页面,内嵌一些 CSS 和 JavaScript,直接把文件拖到浏览器里就能看效果,部署的时候也只需要把文件夹上传到服务器,一个网站的总代码量可能也就几十 KB。那是一个"所见即所得"的时代,开发流程简单直接,几乎没有"工程化"这个概念。
|
||||
|
||||
但现代前端开发完全变了样。我们现在用 TypeScript 代替 JavaScript,这意味着需要编译;我们用 Vue 或 React 的组件化开发方式,需要额外的转换;我们用 Sass 或 Less 写 CSS,需要预处理;我们通过 npm 安装各种依赖包,最终需要打包。一个中大型项目的前端依赖可能上千个,总大小几百 MB,这与十年前的"简单直接"形成了鲜明对比。
|
||||
|
||||
<div style="display: flex; gap: 20px; margin: 20px 0;">
|
||||
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||
|
||||
**👴 十年前的开发方式**
|
||||
- 写几个 HTML + CSS + JS 就是一个项目
|
||||
- 直接拖到浏览器就能看效果
|
||||
- 上传文件夹到服务器就完成部署
|
||||
- 整个项目代码量通常只有几十 KB
|
||||
|
||||
</div>
|
||||
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||
|
||||
**🚀 现代的开发方式**
|
||||
- 使用 TypeScript,需要编译才能运行
|
||||
- 使用 Vue/React,需要转换成原生 JS
|
||||
- 使用 npm 包管理,需要打包合并
|
||||
- 项目依赖动辄几百 MB
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
**这就是"前端工程化"要解决的问题:如何管理复杂度,让开发效率更高、代码质量更好、用户体验更优。**
|
||||
|
||||
<BuildPipelineDemo />
|
||||
|
||||
### 1.2 一个真实的踩坑故事:为什么你需要了解构建原理
|
||||
|
||||
你可能会说:"我用 Vite 或者 Create React App,开箱即用,为什么还需要了解这些构建原理?" 让我讲一个真实的故事,你就会明白为什么这些知识如此重要。
|
||||
|
||||
::: warning 小明的踩坑记
|
||||
小明是一个刚入职的前端新人,公司用的是 Vite 搭建的项目。有一天,产品经理跑过来说首页加载太慢了,用户都在抱怨,需要尽快优化。
|
||||
|
||||
小明立刻行动起来:他压缩了图片、实现了路由懒加载、启用了 Gzip 压缩...一顿操作猛如虎,但首页加载速度依然很慢,问题根本没有解决。
|
||||
|
||||
后来他请教师傅,师傅打开浏览器的开发者工具,看了一眼网络请求,立刻发现了问题所在:`vendor.js` 文件竟然有 2MB!原来小明为了使用某个日期格式化函数,直接引入了 `moment.js` 整个库,而 `moment.js` 包含了 100 多种语言的 locale 文件,大部分都是项目根本用不到的。
|
||||
|
||||
解决方案很简单:把 `moment.js` 换成 `dayjs`,或者按需引入 `date-fns`。这样改动之后,2MB 的体积瞬间变成了 2KB,首页加载速度提升了十几倍。
|
||||
|
||||
小明从此明白了一个道理:**不了解构建和打包原理,你连问题出在哪都不知道,更别提解决问题了。**
|
||||
:::
|
||||
|
||||
::: info 💡 核心启示
|
||||
构建工具不是黑魔法,理解它的工作原理能让你在遇到问题时快速定位、精准解决。更重要的是,它能在设计架构和选择依赖时帮你做出更明智的决策。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心概念:转译、打包、构建
|
||||
|
||||
::: tip 🤔 这些概念和构建有什么关系?
|
||||
转译、打包就是流水线上的关键工序。
|
||||
|
||||
当你运行 `npm run build` 时,构建工具会依次执行:
|
||||
1. **代码检查** → 发现错误
|
||||
2. **转译** → 把新语法翻译成浏览器能懂的代码
|
||||
3. **打包** → 把分散的文件合并起来
|
||||
4. **优化** → 压缩体积、删除无用代码
|
||||
|
||||
所以,**转译和打包是构建流程的核心环节**。理解它们,你才能知道构建工具到底在做什么,为什么有时候构建很慢,为什么有时候打包后体积很大。
|
||||
:::
|
||||
|
||||
在深入学习具体工具之前,我们需要先搞清楚这几个核心概念。为了帮助你更好地理解,我们用一个餐厅的比喻来类比它们之间的关系。
|
||||
|
||||
### 2.1 用餐厅比喻理解三个概念
|
||||
|
||||
想象你经营一家餐厅,每天要为顾客提供各种美食。这个过程中涉及到的环节,与前端工程化的三个核心概念惊人地相似:
|
||||
|
||||
| 概念 | 🍽️ 餐厅比喻 | 实际作用 | 具体例子 |
|
||||
|------|-------------|----------|----------|
|
||||
| **转译** | 把中文菜谱翻译成英文,让外国厨师也能看懂 | 把新语法转换成浏览器能理解的旧语法 | 你写 `const name = user?.name`,转译后变成 `var name = user && user.name` |
|
||||
| **打包** | 把各桌点的菜装成一个个外卖盒,方便配送 | 把分散的模块文件合并成少数几个文件 | 你写了 50 个 .js 文件,打包后变成 2 个文件 |
|
||||
| **构建** | 从接单、做菜、打包到配送的完整流程 | 从源代码到生产代码的完整转换过程 | 执行 `npm run build` 后,src 文件夹变成 dist 文件夹 |
|
||||
|
||||
### 2.2 转译(Transpile):代码的"翻译官"
|
||||
|
||||
转译,顾名思义就是"转换+编译",它的核心作用是把一种编程语言(或其新版本)转换成另一种(或其旧版本)。你可能会有疑问:为什么要这样做?直接写浏览器支持的代码不就行了吗?
|
||||
|
||||
答案在于浏览器兼容性问题。虽然 JavaScript 每年都会发布新版本,带来更强大的语法和 API,但浏览器的更新速度远远跟不上。如果你使用了最新的 ES2022 语法,在旧版浏览器上可能完全无法运行。转译工具的作用就是把你的"超前代码"转换成"保守代码",确保在所有浏览器上都能正常运行。
|
||||
|
||||
::: details 🔧 转译示例:看看转译做了什么
|
||||
让我们看一个具体的例子。下面是你写的代码,使用了 ES2020 的可选链操作符和空值合并操作符:
|
||||
|
||||
```js
|
||||
// 你写的(ES2020+)
|
||||
const result = data?.items?.map(item => item.name) ?? []
|
||||
```
|
||||
|
||||
这段代码很简洁优雅,但在旧浏览器上会报语法错误。转译工具会把它转换成等价的、兼容性更好的代码:
|
||||
|
||||
```js
|
||||
// 转译后(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
|
||||
: []
|
||||
```
|
||||
|
||||
可以看到,一行简洁的代码被转换成了多行"啰嗦"的代码,但后者可以在任何浏览器上正常运行。
|
||||
:::
|
||||
|
||||
**常用的转译工具:**
|
||||
|
||||
- **Babel** 是最老牌、生态最丰富的 JavaScript 转译器,几乎可以处理所有现代语法。它的插件系统非常强大,但也因为灵活性高导致配置相对复杂。
|
||||
- **SWC** 是用 Rust 语言重写的转译器,速度比 Babel 快 20 倍以上,正在被越来越多的项目采用,包括 Next.js 等知名框架。
|
||||
- **esbuild** 是用 Go 语言编写的,同样以速度著称,Vite 在开发模式下就使用它来进行快速转译。
|
||||
|
||||
::: details 🔍 我的项目用的是什么转译工具?
|
||||
你不需要刻意选择,通常是由项目脚手架决定的:
|
||||
|
||||
| 项目类型 | 默认转译工具 |
|
||||
|---------|-------------|
|
||||
| Vite 项目 | esbuild(开发模式)+ esbuild/rollup(生产模式) |
|
||||
| Create React App | Babel |
|
||||
| Next.js | SWC(新版本)/ Babel(旧版本) |
|
||||
| Vue CLI | Babel |
|
||||
|
||||
想知道自己项目用的是什么?打开 `package.json`,搜索 `babel`、`@babel/core` 这些关键词。如果找到了,说明用的是 Babel;如果没有,很可能是 esbuild 或 SWC。
|
||||
|
||||
**其实你不需要关心这个**——这些工具对开发者是"透明"的,你只管写代码,它们会在后台默默工作。
|
||||
:::
|
||||
|
||||
### 2.3 打包(Bundle):模块的"打包员"
|
||||
|
||||
打包是指把多个分散的模块文件合并成一个(或几个)文件的过程。在早期的前端开发中,我们习惯把所有代码写在一个 JS 文件里,但随着项目规模增大,这种方式变得难以维护。现代前端采用模块化开发,每个功能一个文件,但浏览器加载大量小文件会带来性能问题,这就需要打包工具来帮忙。
|
||||
|
||||
::: tip 📦 什么是 ES 模块?
|
||||
你可能听说过"ES 模块"这个词,它到底是什么?
|
||||
|
||||
**先区分两个概念**:
|
||||
- **ECMAScript(ES)**:是 JavaScript 的语言标准规范,定义了语法和 API
|
||||
- **ES 模块**:是 ECMAScript 标准中定义的模块化方案,通过 `import` 和 `export` 语法导入导出代码
|
||||
|
||||
打个比方:ECMAScript 就像"普通话标准",而 ES 模块就像"普通话中的某种表达方式"。
|
||||
|
||||
```js
|
||||
// utils.js - 导出模块
|
||||
export function add(a, b) { return a + b }
|
||||
export function subtract(a, b) { return a - b }
|
||||
|
||||
// main.js - 导入模块
|
||||
import { add, subtract } from './utils.js'
|
||||
console.log(add(1, 2)) // 3
|
||||
```
|
||||
|
||||
**ES 版本小知识**:ECMAScript 每年都会发布新版本:
|
||||
- **ES5(2009)**:经典版本,几乎所有浏览器都支持
|
||||
- **ES6/ES2015**:里程碑式大更新,引入了 `let/const`、箭头函数、**ES 模块**、`class` 等
|
||||
- **ES2016-ES2024**:每年持续添加新特性(如 `async/await`、可选链 `?.` 等)
|
||||
|
||||
ES 模块正是在 ES6(2015年)引入的。在此之前,JavaScript 没有官方的模块系统,开发者只能用各种"民间方案"(如 CommonJS、AMD),这导致了模块规范不统一的问题。ES 模块统一了这些规范,成为现代前端开发的基石。
|
||||
:::
|
||||
|
||||
**为什么需要打包?** 主要有三个原因:首先,虽然现代浏览器已经支持 ES 模块,但在生产环境中加载上百个小文件仍然会带来性能开销;其次,打包过程可以进行 Tree Shaking,自动删除未使用的代码,减小文件体积;最后,打包后可以做代码分割,实现按需加载,提升首屏速度。
|
||||
|
||||
::: details 📁 打包前后对比:看看打包做了什么
|
||||
**打包前的源码结构**(分散的多个文件):
|
||||
```
|
||||
src/
|
||||
├── index.js (入口文件,导入其他模块)
|
||||
├── utils/
|
||||
│ ├── a.js (工具函数 A)
|
||||
│ ├── b.js (工具函数 B)
|
||||
│ └── c.js (工具函数 C)
|
||||
└── components/
|
||||
└── Button.vue (按钮组件)
|
||||
```
|
||||
|
||||
**打包后的产物**(合并后的少数文件):
|
||||
```
|
||||
dist/
|
||||
├── index.[hash].js (主入口代码)
|
||||
├── vendor.[hash].js (第三方库代码)
|
||||
└── assets/
|
||||
└── logo.[hash].png (静态资源)
|
||||
```
|
||||
|
||||
打包工具会分析文件之间的依赖关系,按照正确的顺序把它们合并到一起,同时进行各种优化。
|
||||
:::
|
||||
|
||||
👇 **动手试试看**:
|
||||
下面这个演示展示了代码分割如何实现按需加载。点击不同的路由,观察哪些代码被加载了:
|
||||
|
||||
<CodeSplittingDemo />
|
||||
|
||||
### 2.4 构建(Build):完整的"生产线"
|
||||
|
||||
构建是一个更广义的概念,它涵盖了从源代码到可部署产物的完整转换过程。一个完整的构建流程通常包括以下步骤:
|
||||
|
||||
1. **预编译阶段**:把 TypeScript 编译成 JavaScript,把 Sass 编译成 CSS
|
||||
2. **代码检查阶段**:运行 ESLint 进行代码规范检查,运行 TypeScript 类型检查
|
||||
3. **依赖解析阶段**:分析模块之间的依赖关系,构建依赖图
|
||||
|
||||
👇 **动手看看**:
|
||||
下面这个演示展示了项目中模块之间的依赖关系图谱。点击不同的节点,观察模块是如何相互引用的:
|
||||
|
||||
<DependencyGraphDemo />
|
||||
|
||||
4. **转译阶段**:使用 Babel 等工具转换语法,确保兼容性
|
||||
5. **打包阶段**:合并模块文件,应用 Tree Shaking 删除无用代码
|
||||
6. **优化阶段**:压缩代码、分割代码、提取公共模块
|
||||
7. **资源处理阶段**:压缩图片、生成雪碧图、处理字体文件
|
||||
8. **产物生成阶段**:输出最终文件到 dist 目录
|
||||
|
||||
理解这个完整流程非常重要,因为当构建出现问题时,你需要知道问题出在哪个环节,才能有针对性地解决。
|
||||
|
||||
---
|
||||
|
||||
## 3. 实战:一个团队的工程化演进之路
|
||||
|
||||
::: tip 🤔 什么是"工程化"?
|
||||
说了半天"工程化",它到底是什么意思?
|
||||
|
||||
**简单来说,工程化就是把"手工作坊"变成"现代化工厂"的过程。**
|
||||
|
||||
想象一下:你在家做饭,想吃什么就做什么,很自由。但如果要开一家餐厅,每天服务几百个顾客,就不能再"想做什么做什么"了——你需要标准化的菜谱、规范的操作流程、统一的原材料采购,这样才能保证每道菜的质量稳定、出餐效率高。
|
||||
|
||||
前端开发也一样。一个人写小项目,怎么写都行。但团队协作、项目变大后,就需要:
|
||||
- **统一的代码规范**:大家都按同样的方式写代码
|
||||
- **自动化工具**:让机器帮我们检查错误、转换代码、打包文件
|
||||
- **标准化流程**:从开发到上线有一套清晰的步骤
|
||||
|
||||
**这就是工程化:用工具和规范,让开发更高效、代码更可靠、协作更顺畅。**
|
||||
:::
|
||||
|
||||
讲了这么多概念,让我们看一个真实的案例:某创业公司是如何从"直接写 HTML"一步步进化到"现代化工程化流程"的。通过这个案例,你会更直观地理解工程化到底解决了什么问题。
|
||||
|
||||
::: tip 📖 背景知识:jQuery、Vue、React 是什么?
|
||||
在开始案例之前,先简单介绍一下这些名词:
|
||||
|
||||
- **jQuery**:十多年前最流行的 JavaScript 库,用来简化 DOM 操作(比如"点击按钮后改变文字")。现在已经被 Vue、React 等现代框架取代,但很多老项目还在用。
|
||||
- **Vue / React**:现代前端开发的主流框架。它们让你用"组件"的方式组织代码,数据和视图自动同步,开发效率更高。你现在学的很可能就是其中之一。
|
||||
|
||||
**简单理解**:jQuery 是"手动挡",你要自己操作每一个元素;Vue/React 是"自动挡",你只需要告诉它数据是什么,它会自动更新界面。
|
||||
:::
|
||||
|
||||
### 3.1 演进的全景图
|
||||
|
||||
::: tip 🤔 什么是脚手架?
|
||||
脚手架就是帮你"搭好项目骨架"的工具。比如 `npm create vite@latest` 会自动创建一个配置好的项目,里面有目录结构、配置文件、示例代码,你直接开始写业务代码就行。
|
||||
|
||||
**没有脚手架的时代**:你要手动创建文件夹、写配置文件、安装依赖...一个项目搭建下来可能要半天。
|
||||
**有脚手架的时代**:一条命令,30 秒搞定。
|
||||
:::
|
||||
|
||||
下面这张表展示了工程化演进的四个阶段,你可以看到构建工具、脚手架、框架是如何一步步进化的:
|
||||
|
||||
| 阶段 | 构建工具 | 脚手架 | 框架 | 核心变化 |
|
||||
|------|---------|--------|------|----------|
|
||||
| **阶段一:原始时代** | 无(直接运行) | 无(手动建文件) | jQuery | 没有任何工具,全靠手工 |
|
||||
| **阶段二:模块化** | Webpack + Babel | 简单模板复制 | Vue 2 / React | 开始有构建流程,但配置很麻烦 |
|
||||
| **阶段三:现代化** | Vite | create-vite / create-react-app | Vue 3 / React 18 | 开箱即用,零配置启动 |
|
||||
| **阶段四:持续优化** | Vite + 插件 | 自定义脚手架模板 | 框架 + TypeScript | 团队规范化、模板化 |
|
||||
|
||||
::: tip 📊 从表格中你能看到什么?
|
||||
让我们逐行解读这张表:
|
||||
|
||||
**阶段一 → 阶段二**:从"没有工具"到"有了工具"。这是质的飞跃——你开始用构建工具处理代码,用框架组织项目。但代价是配置复杂,新人上手难。
|
||||
|
||||
**阶段二 → 阶段三**:从"能用"到"好用"。Vite 把原来需要手动配置的东西都自动化了,脚手架一键生成项目,开发体验大幅提升。你现在大概率就处在这个阶段。
|
||||
|
||||
**阶段三 → 阶段四**:从"个人好用"到"团队高效"。当团队变大后,需要统一的技术栈和规范,这时候会自定义脚手架模板,让所有项目保持一致的风格。
|
||||
|
||||
**总结一下**:工程化演进不只是"构建工具变快了",而是**整个开发体验的升级**——从手动搭建项目到脚手架一键生成,从复杂配置到开箱即用,从各自为战到团队规范。
|
||||
:::
|
||||
|
||||
### 3.2 阶段一:原始时代——全靠手工
|
||||
|
||||
为什么叫"原始时代"?因为这个阶段没有任何自动化工具,所有事情都要手动完成——创建文件夹、写代码、管理依赖、调试问题,全部靠人工。
|
||||
|
||||
在这个阶段,团队只有 3 个前端工程师,做一个管理后台项目。项目很小,大家各写各的,看起来没什么问题。但随着项目变大,问题开始暴露出来。
|
||||
|
||||
**开发方式**:
|
||||
- **构建工具**:无,直接写 HTML/JS/CSS,浏览器直接运行
|
||||
- **脚手架**:无,手动创建文件夹和文件
|
||||
- **框架**:jQuery,用选择器操作 DOM
|
||||
|
||||
**这个阶段的特点**:
|
||||
- ✅ **优点**:简单直接,没有学习成本,写完就能跑
|
||||
- ❌ **缺点**:代码一多就乱,团队协作困难,没有代码检查容易出 bug
|
||||
|
||||
::: details 查看当时的项目结构和代码方式
|
||||
**项目结构**(手动创建):
|
||||
```
|
||||
project/
|
||||
├── index.html
|
||||
├── login.html
|
||||
├── css/
|
||||
│ ├── bootstrap.css
|
||||
│ └── custom.css
|
||||
├── js/
|
||||
│ ├── jquery.js
|
||||
│ ├── bootstrap.js
|
||||
│ └── app.js
|
||||
└── images/
|
||||
```
|
||||
|
||||
**遇到的问题**:
|
||||
1. **全局变量污染**:所有变量都在全局命名空间,不同文件中的同名变量会互相覆盖
|
||||
2. **依赖管理混乱**:jQuery 插件必须先加载 jQuery,script 标签顺序错了就报错
|
||||
3. **代码难以复用**:想复用某个功能,只能复制粘贴代码
|
||||
4. **没有代码检查**:变量拼写错误等低级问题,只能运行后才发现
|
||||
|
||||
**当时的临时解决方案**:
|
||||
```js
|
||||
// 用自执行函数模拟模块化(IIFE 模式)
|
||||
var ModuleA = (function () {
|
||||
var privateVar = 'private' // 私有变量,外部无法访问
|
||||
|
||||
function privateFn() {
|
||||
console.log(privateVar)
|
||||
}
|
||||
|
||||
return {
|
||||
publicMethod: function () {
|
||||
privateFn() // 暴露公共方法
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
// 依赖管理全靠注释说明
|
||||
/**
|
||||
* @requires jquery.js (must load first)
|
||||
* @requires bootstrap.js
|
||||
*/
|
||||
```
|
||||
:::
|
||||
|
||||
这种开发方式在小项目中还能应付,但随着团队扩大到 8 人、项目变得越来越复杂,这些问题开始严重影响开发效率和代码质量,团队迫切需要一种更好的组织方式。
|
||||
|
||||
### 3.3 阶段二:模块化时代——开始有工具链
|
||||
|
||||
原始时代的问题积累到一定程度,团队终于决定引入现代化工具链。这是一个重要的转折点——从"手工劳动"进入"机械化生产"。
|
||||
|
||||
但这个阶段也有代价:工具链的学习成本很高,配置文件复杂,新人上手需要时间。
|
||||
|
||||
**开发方式**:
|
||||
- **构建工具**:Webpack + Babel,需要写配置文件
|
||||
- **脚手架**:复制旧项目模板,手动改配置
|
||||
- **框架**:Vue 2 / React,组件化开发
|
||||
|
||||
**这个阶段的特点**:
|
||||
- ✅ **优点**:模块化开发,代码可维护性大幅提升,有代码检查
|
||||
- ❌ **缺点**:配置复杂,启动慢,脚手架简陋容易出错
|
||||
|
||||
::: details 查看引入工具链后的变化
|
||||
**项目结构**(Webpack + Vue 2 时代):
|
||||
```
|
||||
my-project/
|
||||
├── build/ # 构建配置(这个阶段配置很复杂!)
|
||||
│ ├── webpack.base.js
|
||||
│ ├── webpack.dev.js
|
||||
│ └── webpack.prod.js
|
||||
├── config/ # 环境配置
|
||||
│ ├── index.js
|
||||
│ ├── dev.env.js
|
||||
│ └── prod.env.js
|
||||
├── src/
|
||||
│ ├── components/ # 组件
|
||||
│ ├── views/ # 页面
|
||||
│ ├── router/ # 路由
|
||||
│ ├── store/ # 状态管理
|
||||
│ ├── App.vue
|
||||
│ └── main.js
|
||||
├── static/ # 静态资源
|
||||
├── .eslintrc.js # ESLint 配置
|
||||
├── .babelrc # Babel 配置
|
||||
├── package.json
|
||||
└── index.html
|
||||
```
|
||||
|
||||
**配置文件示例**(这就是为什么说"配置复杂"):
|
||||
```js
|
||||
// webpack.base.js - 仅仅是基础配置就有这么多内容
|
||||
const path = require('path')
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin')
|
||||
|
||||
module.exports = {
|
||||
entry: './src/main.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../dist'),
|
||||
filename: '[name].[contenthash].js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.vue$/, loader: 'vue-loader' },
|
||||
{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
|
||||
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
|
||||
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
|
||||
{ test: /\.(png|jpg|gif)$/, loader: 'url-loader', options: { limit: 8192 } }
|
||||
]
|
||||
},
|
||||
plugins: [new VueLoaderPlugin()],
|
||||
resolve: {
|
||||
extensions: ['.js', '.vue', '.json'],
|
||||
alias: { '@': path.resolve(__dirname, '../src') }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**带来的改善**:
|
||||
1. **模块化开发**:每个文件就是一个模块,通过 import/export 清晰管理依赖关系
|
||||
2. **代码复用**:组件和工具函数可以在不同项目中复用,不用再复制粘贴
|
||||
3. **代码质量**:ESLint 在保存时自动检查,TypeScript 在编译时发现类型错误
|
||||
4. **性能优化**:Webpack 的代码分割和懒加载让首屏加载速度大幅提升
|
||||
|
||||
**新的痛点**:
|
||||
1. **配置复杂**:webpack.config.js 动辄几百行,新人很难上手
|
||||
2. **启动慢**:冷启动 30 秒以上,改代码热更新要等 5 秒
|
||||
3. **脚手架简陋**:复制旧项目模板,经常忘记改配置,导致各种奇怪问题
|
||||
:::
|
||||
|
||||
### 3.4 阶段三:现代化时代——开箱即用
|
||||
|
||||
阶段二的痛点(配置复杂、启动慢)困扰了开发者很多年。直到 2021 年,Vite 的出现彻底改变了这一切。
|
||||
|
||||
Vite 的核心理念是"约定优于配置"——它内置了合理的默认配置,你不需要写几百行配置文件,开箱即用。这就像从"自己组装电脑"变成了"买品牌机",省去了大量折腾的时间。
|
||||
|
||||
2021 年之后,团队开始用 Vite 替代 Webpack,开发体验得到了质的提升。
|
||||
|
||||
**开发方式**:
|
||||
- **构建工具**:Vite,零配置启动,秒级热更新
|
||||
- **脚手架**:`npm create vite@latest`,一键生成项目
|
||||
- **框架**:Vue 3 / React 18,更强大的组件系统
|
||||
|
||||
**这个阶段的特点**:
|
||||
- ✅ **优点**:秒级启动,热更新极快,配置简单,新人友好
|
||||
- ❌ **缺点**:生态还在完善中,某些特殊需求可能需要额外配置
|
||||
|
||||
::: details Vite 带来的变化
|
||||
**项目结构**(Vite + Vue 3 时代):
|
||||
```
|
||||
my-project/
|
||||
├── src/
|
||||
│ ├── components/ # 组件
|
||||
│ ├── views/ # 页面
|
||||
│ ├── router/ # 路由
|
||||
│ ├── stores/ # 状态管理(Pinia)
|
||||
│ ├── assets/ # 静态资源
|
||||
│ ├── App.vue
|
||||
│ └── main.js
|
||||
├── public/ # 公共资源
|
||||
├── vite.config.js # 配置文件(简洁!)
|
||||
├── package.json
|
||||
└── index.html
|
||||
```
|
||||
|
||||
**配置文件对比**(Vite 配置有多简洁):
|
||||
```js
|
||||
// vite.config.js - 整个配置文件就这么点
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: { '@': '/src' }
|
||||
}
|
||||
})
|
||||
// 对比上面 Webpack 的配置,是不是简洁太多了?
|
||||
```
|
||||
|
||||
| 对比项 | 阶段二(Webpack) | 阶段三(Vite) | 体验提升 |
|
||||
|--------|---------|------|------|
|
||||
| 创建项目 | 复制模板,手动改配置 | `npm create vite@latest` | 30 秒搞定 |
|
||||
| 冷启动 | 30s+ | <1s | **快 30 倍** |
|
||||
| 热更新 | 3-5s | <100ms | **快 30 倍** |
|
||||
| 配置文件 | 几百行 | 几十行甚至不需要 | **大幅简化** |
|
||||
|
||||
**实际体验对比**:
|
||||
```bash
|
||||
# 阶段二:使用 Webpack
|
||||
npm run dev
|
||||
# 等待 30 秒...喝杯咖啡回来还在编译
|
||||
# [INFO] Compiled successfully in 30123ms
|
||||
# 修改代码 -> 保存 -> 等待 5 秒 -> 终于看到效果
|
||||
|
||||
# 阶段三:使用 Vite
|
||||
npm create vite@latest my-project # 一键创建项目
|
||||
cd my-project && npm install
|
||||
npm run dev
|
||||
# 等待 300 毫秒...还没反应过来就好了
|
||||
# [INFO] ready in 312ms
|
||||
# 修改代码 -> 保存 -> 瞬间看到效果
|
||||
```
|
||||
:::
|
||||
|
||||
### 3.5 阶段四:持续优化——团队规范化
|
||||
|
||||
当工具链成熟后,团队开始关注更深层次的问题:如何让团队协作更高效?如何避免重复踩坑?如何统一代码风格?
|
||||
|
||||
这个阶段的核心是"规范化"——不只是工具好用,还要让团队所有人用同样的方式工作。
|
||||
|
||||
**开发方式**:
|
||||
- **构建工具**:Vite + 自定义插件,适配团队特殊需求
|
||||
- **脚手架**:团队内部脚手架模板,统一技术栈和规范
|
||||
- **框架**:Vue 3 / React 18 + TypeScript,类型安全
|
||||
|
||||
**这个阶段的特点**:
|
||||
- ✅ **优点**:团队协作高效,代码风格统一,新人入职有模板可循
|
||||
- ❌ **缺点**:需要投入时间维护脚手架和规范,有一定维护成本
|
||||
|
||||
**这个阶段会做什么?**
|
||||
1. **自定义脚手架模板**:把团队常用的配置、目录结构、公共组件打包成模板,新项目一键生成
|
||||
2. **引入 TypeScript**:让代码有类型检查,减少运行时错误
|
||||
3. **建立代码规范**:ESLint 规则、Git 提交规范、代码审查流程
|
||||
4. **持续集成/持续部署(CI/CD)**:代码提交后自动测试、自动部署
|
||||
|
||||
::: details 团队规范化阶段的项目结构
|
||||
**项目结构**(团队内部模板 + TypeScript):
|
||||
```
|
||||
my-project/
|
||||
├── .husky/ # Git hooks(提交前自动检查)
|
||||
├── src/
|
||||
│ ├── components/ # 组件
|
||||
│ ├── views/ # 页面
|
||||
│ ├── router/ # 路由
|
||||
│ ├── stores/ # 状态管理
|
||||
│ ├── api/ # API 接口
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── types/ # TypeScript 类型定义
|
||||
│ ├── assets/ # 静态资源
|
||||
│ ├── App.vue
|
||||
│ └── main.ts # 注意是 .ts 不是 .js
|
||||
├── public/
|
||||
├── .eslintrc.cjs # ESLint 配置(团队统一规则)
|
||||
├── .prettierrc # Prettier 配置(代码格式化)
|
||||
├── tsconfig.json # TypeScript 配置
|
||||
├── vite.config.ts # Vite 配置
|
||||
├── package.json
|
||||
└── README.md # 项目文档
|
||||
```
|
||||
|
||||
**团队规范化的具体体现**:
|
||||
```js
|
||||
// tsconfig.json - TypeScript 配置,类型安全
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"strict": true, // 开启严格模式
|
||||
"noImplicitAny": true, // 禁止隐式 any
|
||||
"baseUrl": ".",
|
||||
"paths": { "@/*": ["src/*"] }
|
||||
}
|
||||
}
|
||||
|
||||
// .eslintrc.cjs - 团队统一的代码规范
|
||||
module.exports = {
|
||||
extends: [
|
||||
'plugin:vue/vue3-recommended',
|
||||
'@vue/standard',
|
||||
'@vue/typescript/recommended'
|
||||
],
|
||||
rules: {
|
||||
'no-console': 'warn', // 禁止 console.log
|
||||
'no-debugger': 'error', // 禁止 debugger
|
||||
'vue/multi-word-component-names': 'error' // 组件名必须是多词
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**常见踩坑与解决方案**:
|
||||
|
||||
**坑一:引入整个库而不是按需引入**
|
||||
|
||||
这是最常见的错误之一。很多时候我们只需要一个库中的某个函数,却不小心引入了整个库。
|
||||
|
||||
```js
|
||||
// ❌ 错误做法:引入整个 moment.js(2.5MB!)
|
||||
import moment from 'moment'
|
||||
const formattedDate = moment(date).format('YYYY-MM-DD')
|
||||
|
||||
// ✅ 正确做法:使用更轻量的 dayjs(2KB)
|
||||
import dayjs from 'dayjs'
|
||||
const formattedDate = dayjs(date).format('YYYY-MM-DD')
|
||||
|
||||
// 或者按需导入 date-fns 的函数
|
||||
import { format } from 'date-fns'
|
||||
const formattedDate = format(date, 'yyyy-MM-dd')
|
||||
```
|
||||
|
||||
**坑二:Tree Shaking 失效**
|
||||
|
||||
Tree Shaking 是打包工具自动删除未使用代码的功能,但它需要正确的导入方式才能生效。
|
||||
|
||||
```js
|
||||
// ❌ 错误做法:这会引入整个 lodash(70KB+)
|
||||
import _ from 'lodash'
|
||||
_.debounce(fn, 200)
|
||||
|
||||
// ✅ 正确做法:只导入需要的函数
|
||||
import debounce from 'lodash/debounce'
|
||||
|
||||
// 或者使用 lodash-es(ES 模块版本,支持 Tree Shaking)
|
||||
import { debounce } from 'lodash-es'
|
||||
```
|
||||
|
||||
👇 **动手试试看**:
|
||||
下面这个演示展示了 Tree Shaking 的工作原理。勾选你需要的函数,观察打包后的体积变化:
|
||||
|
||||
<TreeShakingDemo />
|
||||
|
||||
**坑三:没有使用文件 Hash,导致缓存问题**
|
||||
|
||||
浏览器会缓存静态资源以提高加载速度,但如果文件名不变,更新代码后用户可能还在使用旧版本。
|
||||
|
||||
```js
|
||||
// ❌ 问题场景:文件名固定,用户缓存了旧版本
|
||||
// <script src="/js/app.js"></script>
|
||||
|
||||
// ✅ 正确做法:使用 content hash
|
||||
// Vite/Webpack 会自动处理:
|
||||
// <script src="/js/app.a3f7b2c.js"></script>
|
||||
// 内容变化时 hash 也会变化,浏览器会自动获取新版本
|
||||
```
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 原理深入:Vite 为什么这么快?
|
||||
|
||||
了解了实际案例后,让我们深入看看 Vite 的工作原理,理解它为什么能比传统工具快这么多。
|
||||
|
||||
<BundlerComparisonDemo />
|
||||
|
||||
### 4.1 两种截然不同的工作方式
|
||||
|
||||
传统打包工具(如 Webpack)的工作方式是"先打包后服务":在启动开发服务器之前,它必须先把整个应用的所有模块打包成一个或几个 bundle 文件。这个过程中需要遍历所有源文件、解析依赖关系、转换代码、合并文件,项目越大,这个过程就越慢。
|
||||
|
||||
```
|
||||
传统打包工具的工作流程:
|
||||
|
||||
源代码 (100+ 文件)
|
||||
↓
|
||||
[构建时全部打包] ← 这一步非常耗时!
|
||||
↓
|
||||
Bundle (单个/几个大文件)
|
||||
↓
|
||||
浏览器请求 → 返回打包后的文件
|
||||
```
|
||||
|
||||
Vite 的工作方式完全不同,它采用了"按需编译"的策略:启动时几乎不做任何打包工作,直接启动开发服务器。当浏览器请求某个模块时,Vite 才会实时编译这个模块并返回。
|
||||
|
||||
```
|
||||
Vite 的工作流程:
|
||||
|
||||
源代码 (100+ 文件)
|
||||
↓
|
||||
[不打包!直接启动服务器] ← 几乎瞬间完成
|
||||
↓
|
||||
浏览器请求 index.html
|
||||
↓
|
||||
浏览器发现 <script type="module">,继续请求 JS 文件
|
||||
↓
|
||||
Vite 实时编译请求的模块 → 返回编译后的代码
|
||||
↓
|
||||
浏览器按需加载,用到的才请求
|
||||
```
|
||||
|
||||
### 4.2 Vite 工作流程的三个关键时刻
|
||||
|
||||
**启动时:冷启动秒开**
|
||||
|
||||
Vite 启动时只做两件事:启动一个静态文件服务器,预处理一些依赖信息。它不需要打包,不需要编译所有文件,所以几乎瞬间就能启动完成。
|
||||
|
||||
**请求时:按需编译**
|
||||
|
||||
当浏览器通过 `<script type="module">` 请求 JavaScript 文件时,Vite 会拦截这个请求,实时编译代码后再返回。它会把 TypeScript 转成 JavaScript,把 Vue 单文件组件拆分成 template/script/style,把 CSS 预处理器编译成原生 CSS。
|
||||
|
||||
**修改时:极速热更新**
|
||||
|
||||
当你修改代码并保存时,Vite 会通过 WebSocket 通知浏览器,只更新发生变化的模块,而不是刷新整个页面。由于模块粒度很细(一个文件就是一个模块),更新速度非常快,通常在 100 毫秒以内。
|
||||
|
||||
👇 **动手看看**:
|
||||
下面这个演示对比了传统刷新和 HMR 热更新的区别:
|
||||
|
||||
<HotReloadDemo />
|
||||
|
||||
::: tip 💡 生产环境为什么还是要打包?
|
||||
你可能会问:既然不打包这么快,为什么生产环境还是要打包呢?原因有几个:首先,虽然 HTTP/2 支持多路复用,但加载大量小文件仍然有性能开销;其次,打包过程可以进行更激进的优化,比如代码压缩、作用域提升、更彻底的 Tree Shaking;最后,打包后可以做更好的缓存策略和 CDN 分发。所以 Vite 在生产构建时使用 Rollup 进行打包。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. Webpack 的 Loader 和 Plugin
|
||||
|
||||
虽然 Vite 越来越流行,但很多老项目仍在使用 Webpack,而且 Webpack 的设计思想对理解构建工具很有帮助。如果你需要维护使用 Webpack 的项目,了解它的两个核心概念——Loader 和 Plugin——是必不可少的。
|
||||
|
||||
### 5.1 Loader:文件转换器
|
||||
|
||||
Webpack 的核心理念是"一切皆模块",但 Webpack 本身只理解 JavaScript。Loader 的作用就是把其他类型的文件转换成 Webpack 能处理的 JavaScript 模块。
|
||||
|
||||
比如,当你 import 一个 `.vue` 文件时,`vue-loader` 会把它转换成 JavaScript 组件对象;当你 import 一个 `.scss` 文件时,`sass-loader` 会把它编译成 CSS,然后 `css-loader` 解析其中的 `@import` 和 `url()`,最后 `style-loader` 把 CSS 注入到页面的 `<style>` 标签中。
|
||||
|
||||
### 5.2 Plugin:功能扩展器
|
||||
|
||||
Plugin 的能力比 Loader 更强,它可以访问 Webpack 的完整构建生命周期,在各个阶段执行自定义逻辑。比如,`HtmlWebpackPlugin` 可以自动生成 HTML 文件并注入打包后的资源引用;`MiniCssExtractPlugin` 可以把 CSS 提取成独立文件而不是内嵌在 JS 中;`BundleAnalyzerPlugin` 可以分析打包后的文件组成,帮助你找出体积过大的模块。
|
||||
|
||||
### 5.3 Loader 与 Plugin 的区别
|
||||
|
||||
| 对比项 | Loader | Plugin |
|
||||
|--------|--------|--------|
|
||||
| **核心职责** | 文件转换,把非 JS 文件转成 JS 模块 | 功能扩展,干预构建过程的各个环节 |
|
||||
| **执行时机** | 在模块加载时执行,针对单个文件 | 贯穿整个构建生命周期,可以监听各种事件 |
|
||||
| **配置位置** | `module.rules` 数组中配置 | `plugins` 数组中实例化 |
|
||||
| **典型例子** | `babel-loader`、`vue-loader`、`sass-loader` | `HtmlWebpackPlugin`、`MiniCssExtractPlugin` |
|
||||
|
||||
---
|
||||
|
||||
## 6. Vite 配置模板
|
||||
|
||||
理论讲得差不多了,下面是一个可以直接使用的 Vite 配置模板,涵盖了大多数项目需要的常用功能。你可以根据自己的项目需求进行删减和调整。
|
||||
|
||||
::: details 点击查看完整配置
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig(({ mode }) => ({
|
||||
// 基础路径配置
|
||||
base: './', // 部署时的基础路径,相对路径更灵活
|
||||
|
||||
// 路径别名,让 import 更简洁
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
'@components': resolve(__dirname, 'src/components'),
|
||||
'@utils': resolve(__dirname, 'src/utils'),
|
||||
'@api': resolve(__dirname, 'src/api')
|
||||
}
|
||||
},
|
||||
|
||||
// CSS 配置
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
// 自动导入全局样式变量
|
||||
additionalData: `@use "@/styles/vars.scss" as *;`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 开发服务器配置
|
||||
server: {
|
||||
port: 3000, // 端口号
|
||||
open: true, // 自动打开浏览器
|
||||
cors: true, // 允许跨域
|
||||
// API 代理配置,解决开发环境跨域问题
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 构建配置
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: mode !== 'production', // 生产环境不生成 sourcemap
|
||||
|
||||
// Rollup 打包配置
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 代码分割策略:把不同类型的依赖打包到不同文件
|
||||
manualChunks: {
|
||||
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||
'ui-vendor': ['element-plus'],
|
||||
'utils-vendor': ['lodash-es', 'axios', 'dayjs']
|
||||
},
|
||||
// 文件命名规则
|
||||
entryFileNames: 'js/[name]-[hash].js',
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
// 大于 500KB 的 chunk 会触发警告
|
||||
chunkSizeWarningLimit: 500
|
||||
},
|
||||
|
||||
// 插件配置
|
||||
plugins: [
|
||||
vue() // Vue 3 支持
|
||||
]
|
||||
}))
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
这个配置涵盖了日常开发的主要需求:路径别名让 import 语句更简洁,开发服务器代理解决了跨域问题,代码分割策略优化了加载性能,压缩配置移除了调试代码。
|
||||
|
||||
---
|
||||
|
||||
## 6.1 SourceMap:调试压缩代码的秘密武器
|
||||
|
||||
你可能注意到了配置中的 `sourcemap` 选项。什么是 SourceMap?它为什么这么重要?
|
||||
|
||||
在生产环境中,我们的代码会被压缩、合并、转译,最终变成一行难以阅读的"天书"。当代码出错时,浏览器只能告诉你错误发生在压缩后代码的第 1 行第 1234 个字符——这对调试毫无帮助。SourceMap 的作用就是建立一个映射关系,让你在浏览器开发者工具中看到的仍然是原始的源代码。
|
||||
|
||||
👇 **动手看看**:
|
||||
下面这个演示展示了 SourceMap 如何将压缩后的代码映射回源代码:
|
||||
|
||||
<SourceMapDemo />
|
||||
|
||||
---
|
||||
|
||||
## 6.2 资源指纹:长期缓存与版本控制
|
||||
|
||||
在配置中你可能注意到文件名带有 `[hash]`,这就是资源指纹。它的作用是实现长期缓存策略:当文件内容不变时,hash 也不变,浏览器可以直接使用缓存;当文件内容变化时,hash 随之变化,浏览器会自动获取新版本。
|
||||
|
||||
👇 **动手试试看**:
|
||||
下面这个演示展示了资源指纹如何影响浏览器缓存行为。点击"重新构建"模拟代码变更,开启/关闭 Hash 观察缓存命中的变化:
|
||||
|
||||
<AssetFingerprintDemo />
|
||||
|
||||
|
||||
## 7. 总结
|
||||
|
||||
让我们用一张表格来回顾前端工程化的核心概念:
|
||||
|
||||
| 概念 | 一句话解释 | 解决的问题 | 代表工具 |
|
||||
|------|-----------|-----------|----------|
|
||||
| **转译** | 把新语法"翻译"成旧语法 | 浏览器兼容性 | Babel、SWC、esbuild |
|
||||
| **打包** | 把多个文件合并成少数文件 | 减少请求、模块管理 | Webpack、Rollup、Vite |
|
||||
| **构建** | 从源码到产物的完整流程 | 自动化、优化 | 上述所有工具 |
|
||||
| **Tree Shaking** | 删除未使用的代码 | 减小文件体积 | Webpack、Rollup |
|
||||
| **Code Splitting** | 把代码分成多个小块按需加载 | 首屏性能优化 | Webpack、Vite |
|
||||
| **HMR** | 热模块替换,不刷新更新 | 开发体验 | Webpack、Vite |
|
||||
|
||||
|
||||
::: info 写在最后
|
||||
前端工程化是一个持续演进的话题,工具会变,但核心理念不变:**用自动化手段提高效率、保证质量、优化性能**。理解了这些基本原理,无论工具如何更新换代,你都能快速上手、从容应对。
|
||||
|
||||
希望这篇文章能帮助你建立起对前端工程化的整体认知。当你在实际项目中遇到构建相关的问题时,能够知道从哪里入手、如何定位、怎样解决。
|
||||
:::
|
||||
@@ -0,0 +1,3 @@
|
||||
# 前端框架的本质
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,460 @@
|
||||
# 前端框架对比(React / Vue / Svelte / Angular)
|
||||
::: tip 🎯 核心问题
|
||||
**为什么网页越来越复杂?前端技术为什么要不断演进?** 这个问题会带你理解从简单网页到现代 Web 应用的技术演变之路。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要关注前端演进史?
|
||||
|
||||
### 1.1 从"电子海报"到"桌面应用"
|
||||
|
||||
想象一下你在街上看到的**海报**:
|
||||
|
||||
- ✅ 有内容(文字、图片)
|
||||
- ✅ 有设计(颜色、排版)
|
||||
- ❌ 但你跟它说话,它不会回应
|
||||
- ❌ 你点击某个地方,不会发生什么
|
||||
|
||||
**最早的网页**就是这样的"电子海报":只能看、不能改、内容固定。
|
||||
|
||||
**现代网页**完全不同了。它们像**桌面应用**(VS Code、Figma):
|
||||
|
||||
- ✅ 可以编辑文档、画图、玩游戏
|
||||
- ✅ 实时响应你的每个操作
|
||||
- ✅ 甚至可以离线工作
|
||||
|
||||
**这种转变的核心原因: 网页的功能越来越复杂,需要更高效的技术和开发方式。**
|
||||
|
||||
### 1.2 一个生活的比喻:盖房子
|
||||
|
||||
前端技术的演进,就像盖房子方式的进化:
|
||||
|
||||
| 时代 | 🏠 盖房比喻 | 实际特点 | 优缺点 |
|
||||
| --------- | ------------------ | ---------------------------- | --------------------------- |
|
||||
| **2000s** | **贴海报** | 静态网页,写好 HTML 就行 | ✅ 简单 ❌ 不能互动 |
|
||||
| **2010s** | **请工人手动装修** | jQuery 时代,手动操作每个元素 | ✅ 能互动 ❌ 代码乱、难维护 |
|
||||
| **2020s** | **用乐高搭房子** | Vue/React 时代,组件化开发 | ✅ 高效、可维护 ❌ 学习曲线 |
|
||||
|
||||
::: tip 💡 从表格中你能看到什么?
|
||||
|
||||
**阶段一 → 阶段二**: 从"不能动"到"能动"。这是质的飞跃——网页开始有交互,但代价是代码变得混乱。
|
||||
|
||||
**阶段二 → 阶段三**: 从"能用"到"好用"。组件化让代码像积木一样可复用,大幅提升开发效率。
|
||||
|
||||
**核心思想**: 技术演进不是"为了新而新",而是为了解决上一个阶段的痛点。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 第一阶段:静态网页与"切图"(2000s)
|
||||
|
||||
<FrontendEvolutionDemo />
|
||||
|
||||
### 2.1 这个时代是什么样的?
|
||||
|
||||
**开发方式**:
|
||||
|
||||
- 写几个 HTML 文件
|
||||
- 内嵌一些 CSS 和 JavaScript
|
||||
- 直接把文件拖到浏览器就能看效果
|
||||
- 上传文件夹到服务器就完成部署
|
||||
|
||||
**特点**:
|
||||
|
||||
- ✅ **优点**: 简单直接,没有学习成本,写完就能跑
|
||||
- ❌ **缺点**: 无法实现复杂交互,代码一多就乱
|
||||
|
||||
::: details 查看当时的项目结构
|
||||
|
||||
```
|
||||
project/
|
||||
├── index.html
|
||||
├── login.html
|
||||
├── css/
|
||||
│ ├── bootstrap.css
|
||||
│ └── custom.css
|
||||
├── js/
|
||||
│ ├── jquery.js
|
||||
│ └── app.js
|
||||
└── images/
|
||||
```
|
||||
|
||||
**遇到的问题**:
|
||||
|
||||
1. **全局变量污染**: 所有变量都在全局命名空间,容易互相覆盖
|
||||
2. **依赖管理混乱**: 必须按正确顺序加载 JS 文件,否则会报错
|
||||
3. **代码难以复用**: 想复用某个功能,只能复制粘贴
|
||||
:::
|
||||
|
||||
### 2.2 "切图"是什么?
|
||||
|
||||
<SliceRequestDemo />
|
||||
|
||||
你可能听说过"切图"这个词。它是早期前端的主要工作:
|
||||
|
||||
**什么是切图?**
|
||||
|
||||
设计师用 Photoshop 设计好页面 → 前端把设计切成小图片 → 用 HTML 把图片拼成页面
|
||||
|
||||
**为什么这么慢?**
|
||||
|
||||
网页上的每张小图片,浏览器都要发一次**网络请求**。请求越多,加载越慢。
|
||||
|
||||
::: tip 💡 雪碧图(Sprite)
|
||||
|
||||
为了减少请求数,出现了"雪碧图"技术:把很多小图合成一张大图。
|
||||
|
||||
优点是请求数变少,缺点是制作和维护都很麻烦。
|
||||
|
||||
这个阶段的教训:**请求太多是性能大敌**。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 第二阶段:jQuery 时代 - "手动搬砖"(2010s)
|
||||
|
||||
### 3.1 为什么需要 jQuery?
|
||||
|
||||
随着网页变复杂,原生 JavaScript 的问题暴露出来:
|
||||
|
||||
- ❌ **API 繁琐**: 简单的操作也要写很多代码
|
||||
- ❌ **浏览器兼容**: 不同浏览器的 API 不一样,要写很多兼容代码
|
||||
- ❌ **选择器弱**: 找元素很麻烦
|
||||
|
||||
**jQuery** 诞生了。它让 JavaScript 变得简单:
|
||||
|
||||
```javascript
|
||||
// 原生 JavaScript (繁琐)
|
||||
const element = document.getElementById('title')
|
||||
|
||||
// jQuery (简洁)
|
||||
const element = $('#title')
|
||||
```
|
||||
|
||||
### 3.2 jQuery 的思路:亲手改页面
|
||||
|
||||
<JQueryVsStateDemo />
|
||||
|
||||
jQuery 的核心思路是**命令式**: 你告诉浏览器"怎么做"。
|
||||
|
||||
```javascript
|
||||
// 找到标题元素
|
||||
$('#title').text('新标题')
|
||||
|
||||
// 找到按钮并禁用
|
||||
$('#submit-btn').attr('disabled', true)
|
||||
|
||||
// 找到列表并添加一项
|
||||
$('ul').append('<li>新项目</li>')
|
||||
```
|
||||
|
||||
**问题**: 你需要记住页面上有哪些元素,每次数据变化都要手动更新所有相关元素。
|
||||
|
||||
::: warning ⚠️ jQuery 的痛点
|
||||
|
||||
想象你在做一个购物车:
|
||||
|
||||
```javascript
|
||||
// 用户点击"添加到购物车"
|
||||
function addToCart() {
|
||||
cartCount++ // 数据变化
|
||||
|
||||
// 你要手动更新所有相关地方
|
||||
$('#cart-count').text(cartCount) // 右上角小红点
|
||||
$('#cart-page-count').text(cartCount) // 购物车页面
|
||||
$('#checkout-price').text(calculatePrice()) // 结算按钮
|
||||
|
||||
// 如果漏了一个地方,页面就不一致了!
|
||||
}
|
||||
```
|
||||
|
||||
**这就是"手动搬砖"的代价**: 容易出错,难以维护。
|
||||
:::
|
||||
|
||||
### 3.3 移动端普及:响应式设计的出现
|
||||
|
||||
这个阶段还有一个重要变化:**手机和平板开始流行**。
|
||||
|
||||
<ResponsiveGridDemo />
|
||||
|
||||
网页必须适配不同屏幕。这需要**响应式布局**: 同一套 HTML/CSS,自动根据屏幕宽度变换布局。
|
||||
|
||||
**响应式布局的核心: 媒体查询 (Media Query)**
|
||||
|
||||
```css
|
||||
/* 电脑屏幕(大于 640px) */
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机屏幕(小于 640px) */
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip 💡 响应式就像"智能相框"
|
||||
|
||||
想象你在不同房间看同一张照片:
|
||||
|
||||
- 在**大客厅**(电脑屏幕),照片可以摆大一些,旁边还能放其他装饰品
|
||||
- 在**小卧室**(手机屏幕),照片需要缩小,其他装饰品要收起来
|
||||
|
||||
**响应式布局**就是"智能相框",它会自动根据房间大小调整展示方式。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 第三阶段:从"手动搬砖"到"数据驱动"(Vue/React)
|
||||
|
||||
### 4.1 为什么需要新框架?
|
||||
|
||||
jQuery 时代的问题积累到一定程度:
|
||||
|
||||
- **代码一多就乱**: 到处都是 DOM 操作,难以维护
|
||||
- **容易出 bug**: 漏更新一个地方,页面就不一致
|
||||
- **协作困难**: 多人修改同一个文件,容易冲突
|
||||
|
||||
**Vue / React** 的核心思路:**只改数据,页面自动更新**。
|
||||
|
||||
### 4.2 Vue/React 的思路:声明式 UI
|
||||
|
||||
<ImperativeVsDeclarativeDemo />
|
||||
|
||||
**jQuery (命令式)**:
|
||||
|
||||
```javascript
|
||||
// 你要告诉浏览器每一步怎么做
|
||||
$('#title').text('新标题')
|
||||
$('#title').css('color', 'red')
|
||||
$('#title').show()
|
||||
```
|
||||
|
||||
**Vue (声明式)**:
|
||||
|
||||
```javascript
|
||||
// 你只需告诉浏览器"要显示什么"
|
||||
data() {
|
||||
return {
|
||||
title: "新标题",
|
||||
color: "red",
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip 💡 命令式 vs 声明式
|
||||
|
||||
就像画一幅画:
|
||||
|
||||
- **命令式**: 你告诉画家"拿起笔,蘸红颜料,在坐标(10,10)画一个圈"
|
||||
- **声明式**: 你直接给画家一张照片,"给我画成这样"
|
||||
|
||||
Vue/React 就是"声明式": 你描述"页面长什么样",框架负责"怎么把它画出来"。
|
||||
:::
|
||||
|
||||
### 4.3 组件化:像搭乐高一样写页面
|
||||
|
||||
**Vue / React** 最强大的特性是**组件化**: 把页面拆成一个个独立的"积木"。
|
||||
|
||||
想象一下你在搭乐高:
|
||||
|
||||
- 你不需要"从头开始雕刻每一块积木"(从头写 HTML/CSS)
|
||||
- 你只需要"按说明书把积木拼在一起"(把组件组合起来)
|
||||
- 每个积木都是**独立的**,你可以在不同的套装里**重复使用**
|
||||
|
||||
**组件的好处**:
|
||||
|
||||
- **复用**: 写一个"商品卡片"组件,可以用 100 次
|
||||
- **封装**: 组件内部的状态不影响别人
|
||||
- **维护**: 修改一个组件,所有用到它的地方都会更新
|
||||
|
||||
### 4.4 SPA:单页应用的诞生
|
||||
|
||||
<RoutingModeDemo />
|
||||
|
||||
**Vue / React** 时代还有一个重要变化:**从 MPA 到 SPA**。
|
||||
|
||||
**MPA (Multi-Page Application)**:
|
||||
|
||||
- 点一个链接 → 整页刷新 → 显示新页面
|
||||
- 就像**翻书**: 每翻一页都要把旧书合上、去书架拿新书
|
||||
|
||||
**SPA (Single-Page Application)**:
|
||||
|
||||
- 点一个链接 → 只刷新内容区域 → 页面不刷新
|
||||
- 就像**同一本书里换章节**: 只擦掉旧内容、写上新内容
|
||||
|
||||
**SPA 的优点**:
|
||||
|
||||
- ✅ **体验丝滑**: 页面切换快
|
||||
- ✅ **状态好管理**: 输入的内容、滚动位置都在
|
||||
- ❌ **首屏可能慢**: 需要先下载 JavaScript
|
||||
- ❌ **SEO 要额外处理**: 搜索引擎可能抓不到内容(需要 SSR/SSG)
|
||||
|
||||
---
|
||||
|
||||
## 5. 渲染策略:从 CSR 到 SSR/SSG
|
||||
|
||||
<RenderingStrategyDemo />
|
||||
|
||||
## 6. 第四阶段:工程化与构建工具(2015s-2020s)
|
||||
|
||||
### 6.1 为什么需要"工程化"?
|
||||
|
||||
前端项目越来越大,不能再靠"手动引入脚本"。
|
||||
|
||||
**工程化**就是用工具和规范,让开发更高效、代码更可靠、协作更顺畅。
|
||||
|
||||
::: tip 💡 工程化 = 从"手工作坊"到"现代化工厂"
|
||||
|
||||
想象一下你在家做饭 vs 开餐厅:
|
||||
|
||||
- **在家做饭**: 想吃什么就做什么,很自由
|
||||
- **开餐厅**: 需要标准化的菜谱、规范的操作流程、统一的原材料采购
|
||||
|
||||
前端开发也一样:
|
||||
|
||||
- **小项目**: 怎么写都行
|
||||
- **大项目**: 需要统一的代码规范、自动化工具、标准化流程
|
||||
:::
|
||||
|
||||
### 6.2 构建工具:Webpack → Vite
|
||||
|
||||
**Webpack** (传统):
|
||||
|
||||
- 工作方式:**先打包,后服务**
|
||||
- 启动时: 打包所有代码 → 启动服务器
|
||||
- 问题:**慢**。项目越大,启动越慢(可能要等 30 秒)
|
||||
|
||||
**Vite** (现代):
|
||||
|
||||
- 工作方式:**按需编译**
|
||||
- 启动时: 不打包,直接启动服务器
|
||||
- 浏览器请求哪个文件,就实时编译哪个
|
||||
- 优势:**快**。通常 1 秒内启动
|
||||
|
||||
| 对比项 | Webpack | Vite | 提升 |
|
||||
| -------- | ------- | ------ | ------------ |
|
||||
| 冷启动 | 30s+ | <1s | **快 30 倍** |
|
||||
| 热更新 | 3-5s | <100ms | **快 30 倍** |
|
||||
| 配置文件 | 几百行 | 几十行 | **大幅简化** |
|
||||
|
||||
::: tip 💡 为什么 Vite 这么快?
|
||||
|
||||
**Webpack** 就像**整备家当搬家**:先把所有东西打包,再出门。
|
||||
|
||||
**Vite** 就像**轻装旅行**:只带必需品,用到什么再买什么。
|
||||
|
||||
在开发环境,大多数时候你只需要修改几个文件,Vite 只编译这几个文件,当然快。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结:演进的本质
|
||||
|
||||
前端技术的演进,本质上是在解决两个问题:
|
||||
|
||||
### 7.1 效率:从手动到自动
|
||||
|
||||
| 时代 | 开发方式 | 效率 |
|
||||
| --------- | ------------------------ | ---------- |
|
||||
| **2000s** | 手写 HTML/CSS/JS | ⭐ |
|
||||
| **2010s** | jQuery + 手动 DOM 操作 | ⭐⭐ |
|
||||
| **2020s** | Vue/React + 数据驱动 | ⭐⭐⭐ |
|
||||
| **现在** | 组件化 + 工程化 + 自动化 | ⭐⭐⭐⭐⭐ |
|
||||
|
||||
### 7.2 规模:从个人到团队
|
||||
|
||||
| 时代 | 项目规模 | 协作方式 |
|
||||
| --------- | ---------- | ----------------------- |
|
||||
| **2000s** | 几个文件 | 单人就能维护 |
|
||||
| **2010s** | 几十个文件 | 小团队,容易冲突 |
|
||||
| **2020s** | 几百个文件 | 中团队,需要规范 |
|
||||
| **现在** | 几千个文件 | 大团队,需要完整工程体系 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 学习路线图
|
||||
|
||||
### 8.1 如果你是零基础
|
||||
|
||||
**第 1 步: HTML/CSS/JavaScript 基础**
|
||||
|
||||
- 理解网页的三大基石
|
||||
- 能写出简单的静态页面
|
||||
|
||||
**第 2 步: 学习一个框架(Vue 推荐)**
|
||||
|
||||
- 理解"数据驱动"的思想
|
||||
- 掌握组件化开发
|
||||
|
||||
**第 3 步: 实战项目**
|
||||
|
||||
- 做一个完整的单页应用
|
||||
- 熟悉路由、状态管理、API 调用
|
||||
|
||||
### 8.2 如果你有基础
|
||||
|
||||
**进阶方向**:
|
||||
|
||||
- **工程化**: 学习 Vite/Webpack,理解构建流程
|
||||
- **性能优化**: 学习懒加载、代码分割、缓存策略
|
||||
- **TypeScript**: 为代码加上类型,提升可靠性
|
||||
- **服务端渲染**: 学习 Nuxt/Next.js,解决 SEO 和首屏问题
|
||||
|
||||
---
|
||||
|
||||
## 9. 名词速查表 (Glossary)
|
||||
|
||||
| 名词 | 英文 | 用人话解释 |
|
||||
| ---------------- | ----------------------- | --------------------------------------------- |
|
||||
| **DOM** | Document Object Model | 文档对象模型。用对象树表示页面,可被 JS 读写。 |
|
||||
| **jQuery** | - | 早期流行的 JS 库,简化了 DOM 操作。 |
|
||||
| **Vue/React** | - | 现代前端框架,采用数据驱动和组件化开发。 |
|
||||
| **组件** | Component | 可复用的 UI 单元,如按钮、卡片、导航栏。 |
|
||||
| **MPA** | Multi-Page Application | 多页应用。每次跳转都重新加载整个页面。 |
|
||||
| **SPA** | Single-Page Application | 单页应用。只加载一次,后续切换不刷新页面。 |
|
||||
| **路由** | Routing | 管理页面之间切换的规则和过程。 |
|
||||
| **SSR** | Server-Side Rendering | 服务端渲染。服务器生成 HTML 后发给浏览器。 |
|
||||
| **SSG** | Static Site Generation | 静态站点生成。构建时预渲染页面为静态 HTML。 |
|
||||
| **Webpack** | - | 传统打包工具,先打包后服务。 |
|
||||
| **Vite** | - | 现代构建工具,按需编译,速度极快。 |
|
||||
| **响应式** | Responsive Design | 页面自动适配不同屏幕尺寸的设计。 |
|
||||
| **媒体查询** | Media Query | CSS 的条件判断,根据屏幕宽度应用不同样式。 |
|
||||
| **命令式** | Imperative | 告诉程序"怎么做"。 |
|
||||
| **声明式** | Declarative | 告诉程序"要什么"。 |
|
||||
| **数据驱动** | Data-Driven | 只修改数据,界面自动更新。 |
|
||||
| **Tree Shaking** | - | 摇树优化。自动移除未使用的代码,减小包体积。 |
|
||||
| **代码分割** | Code Splitting | 把代码分成多个小块,按需加载。 |
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
前端技术的演进,本质上是**从"手工"到"工业化"的进化**:
|
||||
|
||||
- **2000s**: 手工时代,简单直接
|
||||
- **2010s**: 工具化时代,开始有框架
|
||||
- **2020s**: 工业化时代,组件化 + 工程化
|
||||
- **现在**: 智能化时代,AI 辅助开发
|
||||
|
||||
理解这个演进,你就能:
|
||||
|
||||
- 知道为什么要有 Vue/React
|
||||
- 理解"数据驱动"的价值
|
||||
- 明白工程化的必要性
|
||||
- 快速上手新技术
|
||||
|
||||
**下一步建议**:
|
||||
|
||||
- 如果你想快速上手,学习 **Vue 3** (推荐) 或 **React**
|
||||
- 如果你想深入理解,学习 **Vite** 构建流程
|
||||
- 如果你想提升代码质量,学习 **TypeScript**
|
||||
|
||||
祝你学习愉快!
|
||||
@@ -0,0 +1,551 @@
|
||||
# 图形与动画(Canvas / SVG / WebGL)
|
||||
::: tip 🎯 核心问题
|
||||
**如何在网页上画图、做动画、甚至开发游戏?** Canvas 提供了一个强大的 2D 绘图能力,让你用代码创造视觉内容。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要学 Canvas?
|
||||
|
||||
### 1.1 Canvas 是什么?
|
||||
|
||||
**Canvas (画布)** 是 HTML5 提供的一个通过 JavaScript 绘制 2D 图形的元素。
|
||||
|
||||
你可以把它想象成一张**数字画布**:
|
||||
|
||||
- 🖌️ 你可以用代码"画笔"在上面作画
|
||||
- 🎨 可以画任何东西: 简单的形状、复杂的图表、流畅的动画
|
||||
- 🎮 甚至可以做成完整的游戏
|
||||
|
||||
::: tip 💡 Canvas vs SVG:有什么区别?
|
||||
|
||||
在 Web 开发中,绘制图形主要有两种方式:
|
||||
|
||||
| 特性 | Canvas | SVG |
|
||||
| -------- | -------------------- | --------------------- |
|
||||
| **类型** | 位图(光栅图形) | 矢量图形 |
|
||||
| **DOM** | 单个 `<canvas>` 元素 | 每个图形都是 DOM 元素 |
|
||||
| **交互** | 需要手动计算碰撞 | 天然支持事件绑定 |
|
||||
| **性能** | 适合大量对象 | 适合少量复杂对象 |
|
||||
| **缩放** | 放大会失真 | 无限缩放不失真 |
|
||||
| **应用** | 游戏、数据可视化 | 图标、插画 |
|
||||
|
||||
**简单总结**:
|
||||
|
||||
- **Canvas** = 像素画,画完就变成像素,性能好但交互麻烦
|
||||
- **SVG** = 矢量图,每个图形都是对象,交互方便但对象多了会慢
|
||||
:::
|
||||
|
||||
### 1.2 Canvas 的应用场景
|
||||
|
||||
Canvas 的用途非常广泛,你可能每天都在用:
|
||||
|
||||
1. **数据可视化**: ECharts、Chart.js 的图表
|
||||
2. **游戏开发**: 网页游戏(如 Phaser.js 引擎)
|
||||
3. **图像处理**: 图片裁剪、滤镜、拼图(如 Fabric.js)
|
||||
4. **创意效果**: 粒子特效、动画背景
|
||||
5. **工程绘图**: CAD、流程图、思维导图
|
||||
|
||||
---
|
||||
|
||||
## 2. Canvas 基础
|
||||
|
||||
### 2.1 Canvas 元素和上下文
|
||||
|
||||
使用 Canvas 的第一步是在 HTML 中创建一个 `<canvas>` 元素:
|
||||
|
||||
```html
|
||||
<canvas id="myCanvas" width="600" height="400"></canvas>
|
||||
```
|
||||
|
||||
然后通过 JavaScript 获取**渲染上下文 (Rendering Context)**:
|
||||
|
||||
```javascript
|
||||
const canvas = document.getElementById('myCanvas')
|
||||
const ctx = canvas.getContext('2d') // 获取 2D 上下文
|
||||
```
|
||||
|
||||
::: tip 💡 关键概念
|
||||
|
||||
- **canvas** 是 DOM 元素,控制画布的大小和位置
|
||||
- **ctx** 是绘图工具,所有的绘制操作都通过它完成
|
||||
- **`"2d"`** 表示使用 2D 渲染上下文(WebGL 使用 `"webgl"`)
|
||||
:::
|
||||
|
||||
### 2.2 坐标系统:Canvas 的"地图规则"
|
||||
|
||||
Canvas 使用的是**屏幕坐标系**,这与传统数学坐标系有所不同:
|
||||
|
||||
- **原点 (0, 0)**: 在**左上角**(不是中心)
|
||||
- **X 轴**: 向右为正方向
|
||||
- **Y 轴**: **向下**为正方向(注意: 数学坐标系中 Y 轴向上)
|
||||
- **单位**: 像素 (px)
|
||||
|
||||
```javascript
|
||||
// 在左上角绘制一个矩形
|
||||
ctx.fillRect(0, 0, 10, 10)
|
||||
|
||||
// 在右下角绘制一个矩形
|
||||
ctx.fillRect(canvas.width - 10, canvas.height - 10, 10, 10)
|
||||
```
|
||||
|
||||
::: tip 💡 记忆技巧
|
||||
|
||||
想象你在看**屏幕**:
|
||||
|
||||
- 向右移 → X 增加 ✅
|
||||
- 向下移(滚动页面) → Y 增加 ✅
|
||||
- 向左移 → X 减少
|
||||
- 向上移(向上滚动) → Y 减少
|
||||
|
||||
这就是 Canvas 的坐标规则。
|
||||
:::
|
||||
|
||||
### 2.3 绘制基本形状
|
||||
|
||||
Canvas 提供了几种绘制基本形状的方法:
|
||||
|
||||
**矩形**:
|
||||
|
||||
```javascript
|
||||
// 填充矩形
|
||||
ctx.fillStyle = '#3498db'
|
||||
ctx.fillRect(x, y, width, height)
|
||||
|
||||
// 描边矩形
|
||||
ctx.strokeStyle = '#2c3e50'
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeRect(x, y, width, height)
|
||||
|
||||
// 清除矩形区域
|
||||
ctx.clearRect(x, y, width, height)
|
||||
```
|
||||
|
||||
**圆形**:
|
||||
|
||||
```javascript
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, y, radius, startAngle, endAngle)
|
||||
ctx.fill() // 或 ctx.stroke()
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
|
||||
- **x, y**: 圆心坐标
|
||||
- **radius**: 半径
|
||||
- **startAngle, endAngle**: 起始和结束角度(弧度制)
|
||||
- `0` = 3 点钟方向
|
||||
- `Math.PI / 2` = 6 点钟方向
|
||||
- `Math.PI` = 9 点钟方向
|
||||
|
||||
**线条**:
|
||||
|
||||
```javascript
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x1, y1) // 起点
|
||||
ctx.lineTo(x2, y2) // 终点
|
||||
ctx.stroke()
|
||||
```
|
||||
|
||||
### 2.4 颜色和样式
|
||||
|
||||
Canvas 支持多种颜色设置方式:
|
||||
|
||||
```javascript
|
||||
// 纯色
|
||||
ctx.fillStyle = '#3498db' // 十六进制
|
||||
ctx.fillStyle = 'rgb(52, 152, 219)' // RGB
|
||||
ctx.fillStyle = 'rgba(52, 152, 219, 0.5)' // RGBA(带透明度)
|
||||
|
||||
// 线性渐变
|
||||
const gradient = ctx.createLinearGradient(x1, y1, x2, y2)
|
||||
gradient.addColorStop(0, '#3498db')
|
||||
gradient.addColorStop(1, '#e74c3c')
|
||||
ctx.fillStyle = gradient
|
||||
|
||||
// 径向渐变
|
||||
const radialGradient = ctx.createRadialGradient(x1, y1, r1, x2, y2, r2)
|
||||
radialGradient.addColorStop(0, '#3498db')
|
||||
radialGradient.addColorStop(1, 'transparent')
|
||||
ctx.fillStyle = radialGradient
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 路径:Canvas 的"笔画"
|
||||
|
||||
### 3.1 什么是路径?
|
||||
|
||||
**路径 (Path)** 是 Canvas 中的核心概念。你可以把它想象成用笔画线的过程:
|
||||
|
||||
1. **`beginPath()`** - 开始新路径(拿起笔)
|
||||
2. **`moveTo()`** - 移动到起点(不画线)
|
||||
3. **`lineTo()` / `arc()`** - 绘制线条或曲线
|
||||
4. **`closePath()`** - 闭合路径(可选)
|
||||
5. **`fill()` / `stroke()`** - 填充或描边
|
||||
|
||||
```javascript
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(100, 100) // 移动到起点
|
||||
ctx.lineTo(200, 100) // 画横线
|
||||
ctx.lineTo(150, 150) // 画斜线
|
||||
ctx.closePath() // 闭合路径(回到起点)
|
||||
ctx.fill() // 填充
|
||||
```
|
||||
|
||||
### 3.2 绘制复杂形状
|
||||
|
||||
通过组合路径,可以绘制任意复杂的形状。
|
||||
|
||||
**三角形**:
|
||||
|
||||
```javascript
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(100, 50)
|
||||
ctx.lineTo(150, 150)
|
||||
ctx.lineTo(50, 150)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = '#e74c3c'
|
||||
ctx.fill()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 动画基础
|
||||
|
||||
### 4.1 动画循环
|
||||
|
||||
在 Canvas 中创建动画,核心是使用 **`requestAnimationFrame`** 方法。
|
||||
|
||||
```javascript
|
||||
function animate() {
|
||||
// 1. 清除画布(或绘制半透明背景产生拖尾效果)
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 2. 更新状态
|
||||
update()
|
||||
|
||||
// 3. 绘制
|
||||
draw()
|
||||
|
||||
// 4. 请求下一帧
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
// 启动动画
|
||||
animate()
|
||||
```
|
||||
|
||||
::: tip 💡 为什么用 requestAnimationFrame 而不是 setInterval?
|
||||
|
||||
- ✅ 自动优化,通常为 60FPS(每秒 60 帧)
|
||||
- ✅ 页面不可见时自动暂停,节省资源
|
||||
- ✅ 与浏览器刷新周期同步,避免画面撕裂
|
||||
:::
|
||||
|
||||
### 4.2 动画的本质
|
||||
|
||||
动画的本质是**快速连续绘制静态画面**。每帧需要:
|
||||
|
||||
1. **清除旧画面**: `ctx.clearRect()` 或用半透明背景覆盖
|
||||
2. **更新状态**: 计算新位置、新角度等
|
||||
3. **绘制新画面**: 重新绘制所有对象
|
||||
|
||||
```javascript
|
||||
// 清除画布
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 半透明背景(产生拖尾效果)
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 事件处理
|
||||
|
||||
Canvas 只是一个 DOM 元素,不像 SVG 那样每个图形都是独立的 DOM 元素。因此,我们需要**手动处理交互事件**。
|
||||
|
||||
### 5.1 鼠标事件
|
||||
|
||||
```javascript
|
||||
canvas.addEventListener('click', (e) => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
|
||||
console.log(`Clicked at (${x}, ${y})`)
|
||||
})
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
|
||||
// 检测是否悬停在某个对象上
|
||||
objects.forEach((obj) => {
|
||||
const dist = Math.sqrt((x - obj.x) ** 2 + (y - obj.y) ** 2)
|
||||
if (dist < obj.radius) {
|
||||
canvas.style.cursor = 'pointer'
|
||||
obj.hovered = true
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 5.2 拖拽实现
|
||||
|
||||
```javascript
|
||||
let isDragging = false
|
||||
let selectedObject = null
|
||||
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
const { x, y } = getMousePos(e)
|
||||
|
||||
objects.forEach((obj) => {
|
||||
const dist = Math.sqrt((x - obj.x) ** 2 + (y - obj.y) ** 2)
|
||||
if (dist < obj.radius) {
|
||||
isDragging = true
|
||||
selectedObject = obj
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (isDragging && selectedObject) {
|
||||
const { x, y } = getMousePos(e)
|
||||
selectedObject.x = x
|
||||
selectedObject.y = y
|
||||
draw() // 重绘
|
||||
}
|
||||
})
|
||||
|
||||
canvas.addEventListener('mouseup', () => {
|
||||
isDragging = false
|
||||
selectedObject = null
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 性能优化
|
||||
|
||||
随着绘制的对象增多,Canvas 性能会下降。以下是一些常用的优化技巧:
|
||||
|
||||
### 6.1 离屏 Canvas (Offscreen Canvas)
|
||||
|
||||
预渲染静态内容到离屏 Canvas,减少每帧的绘制操作:
|
||||
|
||||
```javascript
|
||||
// 创建离屏 Canvas
|
||||
const offscreenCanvas = document.createElement('canvas')
|
||||
const offscreenCtx = offscreenCanvas.getContext('2d')
|
||||
offscreenCanvas.width = 600
|
||||
offscreenCanvas.height = 400
|
||||
|
||||
// 预渲染背景
|
||||
function drawBackground(ctx) {
|
||||
ctx.fillStyle = '#f0f0f0'
|
||||
ctx.fillRect(0, 0, 600, 400)
|
||||
}
|
||||
drawBackground(offscreenCtx)
|
||||
|
||||
// 主渲染循环
|
||||
function draw() {
|
||||
// 直接复制预渲染的背景
|
||||
ctx.drawImage(offscreenCanvas, 0, 0)
|
||||
|
||||
// 只绘制动态对象
|
||||
objects.forEach((obj) => obj.draw(ctx))
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 减少重绘(脏矩形优化)
|
||||
|
||||
只重绘变化的部分:
|
||||
|
||||
```javascript
|
||||
function draw() {
|
||||
objects.forEach((obj) => {
|
||||
if (obj.moved) {
|
||||
// 清除旧位置
|
||||
ctx.clearRect(
|
||||
obj.oldX - obj.size,
|
||||
obj.oldY - obj.size,
|
||||
obj.size * 2,
|
||||
obj.size * 2
|
||||
)
|
||||
|
||||
// 绘制新位置
|
||||
obj.draw(ctx)
|
||||
|
||||
obj.moved = false
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 批量渲染
|
||||
|
||||
减少状态切换(fillStyle、strokeStyle 等):
|
||||
|
||||
```javascript
|
||||
// 按颜色分组
|
||||
const batches = {}
|
||||
objects.forEach((obj) => {
|
||||
if (!batches[obj.color]) {
|
||||
batches[obj.color] = []
|
||||
}
|
||||
batches[obj.color].push(obj)
|
||||
})
|
||||
|
||||
// 批量绘制相同颜色的对象
|
||||
Object.keys(batches).forEach((color) => {
|
||||
ctx.fillStyle = color // 只设置一次颜色
|
||||
batches[color].forEach((obj) => {
|
||||
ctx.beginPath()
|
||||
ctx.arc(obj.x, obj.y, obj.size, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 常见库与框架
|
||||
|
||||
虽然原生 Canvas 已经很强大,但在实际项目中,使用成熟的库可以大大提高开发效率。
|
||||
|
||||
### 7.1 Fabric.js
|
||||
|
||||
**特点**: 对象模型,支持交互
|
||||
|
||||
```javascript
|
||||
const canvas = new fabric.Canvas('c')
|
||||
|
||||
// 创建圆形
|
||||
const circle = new fabric.Circle({
|
||||
radius: 20,
|
||||
fill: '#3498db',
|
||||
left: 100,
|
||||
top: 100
|
||||
})
|
||||
|
||||
canvas.add(circle)
|
||||
|
||||
// 自动处理事件
|
||||
circle.on('click', () => {
|
||||
circle.set('fill', '#e74c3c')
|
||||
canvas.renderAll()
|
||||
})
|
||||
```
|
||||
|
||||
**适用场景**: 图片编辑器、白板工具、图形设计工具
|
||||
|
||||
### 7.2 PixiJS (WebGL)
|
||||
|
||||
**特点**: WebGL 加速,超高性能
|
||||
|
||||
```javascript
|
||||
const app = new PIXI.Application({
|
||||
width: 600,
|
||||
height: 400,
|
||||
backgroundColor: 0x1099bb
|
||||
})
|
||||
document.body.appendChild(app.view)
|
||||
|
||||
const graphics = new PIXI.Graphics()
|
||||
graphics.beginFill(0x3498db)
|
||||
graphics.drawCircle(300, 200, 50)
|
||||
graphics.endFill()
|
||||
app.stage.addChild(graphics)
|
||||
```
|
||||
|
||||
**适用场景**: 大型游戏、粒子系统、大量对象的场景
|
||||
|
||||
---
|
||||
|
||||
## 8. 总结与最佳实践
|
||||
|
||||
### 8.1 核心要点回顾
|
||||
|
||||
1. **Canvas 是位图画布**: 绘制后就是像素,无法直接修改已有内容
|
||||
2. **坐标系统**: 原点在左上角,Y 轴向下为正
|
||||
3. **路径系统**: beginPath → moveTo → lineTo → fill/stroke
|
||||
4. **动画原理**: 清除 → 更新 → 绘制 → requestAnimationFrame
|
||||
5. **事件处理**: 需要手动计算碰撞
|
||||
6. **性能优化**: 离屏 Canvas、脏矩形、批量渲染
|
||||
|
||||
### 8.2 最佳实践
|
||||
|
||||
**代码组织**:
|
||||
|
||||
```javascript
|
||||
// 使用类封装对象
|
||||
class GameObject {
|
||||
constructor(x, y) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
}
|
||||
|
||||
update() {
|
||||
// 更新状态
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
// 绘制
|
||||
}
|
||||
|
||||
isHit(x, y) {
|
||||
// 碰撞检测
|
||||
const dist = Math.sqrt((x - this.x) ** 2 + (y - this.y) ** 2)
|
||||
return dist < this.radius
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**性能优化清单**:
|
||||
|
||||
- ✅ 使用 `requestAnimationFrame` 而不是 `setInterval`
|
||||
- ✅ 减少状态切换(按颜色分组绘制)
|
||||
- ✅ 使用离屏 Canvas 预渲染静态内容
|
||||
- ✅ 只重绘变化的部分(脏矩形)
|
||||
- ✅ 限制对象数量,使用对象池
|
||||
- ✅ 避免 `save()` 和 `restore()` 的频繁调用
|
||||
|
||||
---
|
||||
|
||||
## 9. 名词速查表 (Glossary)
|
||||
|
||||
| 名词 | 解释 |
|
||||
| ------------------------- | ----------------------------------------------------------------------- |
|
||||
| **Context / 上下文** | Canvas 的渲染环境,通过 `getContext("2d")` 获取,所有绘制操作都通过它完成 |
|
||||
| **Path / 路径** | 由一系列点连接成的轨迹,是 Canvas 绘图的基础 |
|
||||
| **Stroke / 描边** | 绘制路径的轮廓线 |
|
||||
| **Fill / 填充** | 用颜色填充路径内部 |
|
||||
| **requestAnimationFrame** | 浏览器提供的动画 API,在每次重绘前调用回调函数 |
|
||||
| **Offscreen Canvas** | 离屏 Canvas,用于预渲染静态内容以提高性能 |
|
||||
| **Dirty Rect** | 脏矩形优化,只重绘变化的部分 |
|
||||
| **Collision Detection** | 碰撞检测,判断鼠标或对象是否点击了某个图形 |
|
||||
| **Raster vs Vector** | 位图 vs 矢量图,Canvas 是位图,SVG 是矢量图 |
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
现在你已经掌握了 Canvas 2D 的核心概念:
|
||||
|
||||
- **基本绘图**: 矩形、圆形、线条
|
||||
- **样式控制**: 颜色、渐变、阴影
|
||||
- **动画制作**: requestAnimationFrame + 清除重绘
|
||||
- **交互处理**: 鼠标事件、碰撞检测
|
||||
- **性能优化**: 离屏 Canvas、批量渲染
|
||||
|
||||
**下一步建议**:
|
||||
|
||||
- 如果你想深入学习动画,可以尝试制作一个**贪吃蛇游戏**或**打砖块游戏**
|
||||
- 如果你对数据可视化感兴趣,可以学习 **ECharts** 或 **D3.js**
|
||||
- 如果你想做游戏开发,可以尝试 **Phaser.js** 游戏引擎
|
||||
- 如果你对 WebGL 感兴趣,可以学习 **Three.js** 或 **PixiJS**
|
||||
|
||||
祝你学习愉快! 🎨
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
# JavaScript 语言深入
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# JavaScript 运行时
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 实时通信(WebSocket / SSE)
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,952 @@
|
||||
# 路由与导航
|
||||
::: tip 🎯 核心问题
|
||||
**为什么有些网站切换页面时不会白屏刷新,像 App 一样流畅?** 这就是前端路由的魔法。本章将带你从传统网站的"翻书式跳转",进入到单页应用的"幻灯片切换"世界,理解前端路由如何让用户体验提升一个档次。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要"前端路由"?
|
||||
|
||||
### 1.1 从传统网站到单页应用:用户体验的质变
|
||||
|
||||
回顾早期的网站浏览体验,每次点击链接都是一次"完整翻页"的过程:页面白屏一下、加载圈转动、整个页面重新渲染。如果网络慢,你还要盯着加载圈发呆几秒。这种体验在今天看来已经过时了,但当时这就是标准做法。
|
||||
|
||||
现代前端开发完全改变了这种模式。我们使用前端路由技术,让页面切换像手机 App 一样流畅——没有白屏、没有加载圈、用户几乎感觉不到"跳转"的过程。这种体验的提升不是魔法,而是前端路由系统的功劳。
|
||||
|
||||
<div style="display: flex; gap: 20px; margin: 20px 0;">
|
||||
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||
|
||||
**📖 传统网站(MPA)**
|
||||
- 点击链接 → 整页刷新
|
||||
- 每个页面是独立的 HTML 文件
|
||||
- 浏览器重新下载所有资源
|
||||
- 体验像"翻书",有明显的翻页过程
|
||||
|
||||
</div>
|
||||
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||
|
||||
**📱 单页应用(SPA)**
|
||||
- 点击链接 → 无刷新切换
|
||||
- 只有一个 HTML 入口文件
|
||||
- 只下载需要的数据
|
||||
- 体验像"幻灯片",流畅自然
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
**这就是"前端路由"要解决的核心问题:在不刷新页面的情况下,实现视图的切换和 URL 的同步更新。**
|
||||
|
||||
<RouteMatchingDemo />
|
||||
|
||||
### 1.2 一个真实的踩坑故事:为什么你需要理解路由模式
|
||||
|
||||
你可能会说:"我用 Vue Router 或者 React Router,配置一下就能用,为什么还需要了解这些底层原理?" 让我讲一个真实的故事,你就会明白为什么这些知识如此重要。
|
||||
|
||||
::: warning 小李的部署踩坑记
|
||||
小李是一个前端新人,刚入职就负责开发一个基于 Vue 的单页应用。在本地开发时一切正常,路由跳转丝般顺滑。但是当他把项目部署到测试服务器后,问题出现了:用户直接访问某个路由(如 `example.com/user/123`)或者在详情页刷新页面时,会看到 **404 Not Found** 错误。
|
||||
|
||||
小李懵了:明明本地能正常访问,为什么部署后就 404 了?他排查了很久,甚至怀疑是服务器配置问题。
|
||||
|
||||
后来他请教师兄,师兄一眼就看出了问题:小李用的是 History 模式,但服务器没有配置 fallback。当用户直接访问 `/user/123` 时,服务器会去查找这个路径对应的文件,但 SPA 的所有路由其实都指向同一个 `index.html`。解决方案很简单:配置服务器让所有路由都回退到 `index.html`,让前端路由接管后续处理。
|
||||
|
||||
小李从此明白了一个道理:**不理解路由模式的原理和服务器配置要求,你连为什么报错都不知道,更别提解决问题了。**
|
||||
:::
|
||||
|
||||
::: info 💡 核心启示
|
||||
前端路由不是"黑魔法",理解它的工作原理能让你在遇到部署、性能、SEO 问题时快速定位、精准解决。更重要的是,它能在项目架构设计时帮你做出更明智的选择——什么时候用 Hash 模式、什么时候用 History 模式、如何避免常见的坑。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心概念:路由、模式、导航
|
||||
|
||||
在深入具体实现之前,我们需要先搞清楚几个核心概念。为了帮助你更好地理解,我们用一个图书馆的比喻来类比它们之间的关系。
|
||||
|
||||
::: tip 🤔 这些概念和路由有什么关系?
|
||||
路由、模式、导航就是前端路由系统的三大支柱。
|
||||
|
||||
当你使用 Vue Router 或 React Router 时,框架会帮你处理:
|
||||
1. **路由映射** → 定义 URL 和组件的对应关系
|
||||
2. **模式选择** → 决定用 Hash 还是 History 模式
|
||||
3. **导航控制** → 处理页面跳转、浏览器前进后退
|
||||
|
||||
所以,**理解这三个概念,你才能知道路由系统到底在做什么,为什么有时候需要特殊配置,为什么部署时会出问题。**
|
||||
:::
|
||||
|
||||
### 2.1 用图书馆比喻理解路由系统
|
||||
|
||||
想象你在图书馆里找书,这个过程与前端路由的工作原理惊人地相似:
|
||||
|
||||
| 概念 | 📚 图书馆比喻 | 实际作用 | 具体例子 |
|
||||
|------|-------------|----------|----------|
|
||||
| **路由(Route)** | 书架编号和书籍的对应关系 | 定义 URL 和页面组件的映射关系 | `/user/123` 路径对应 `UserDetail.vue` 组件 |
|
||||
| **路由器(Router)** | 图书馆的指引系统和定位服务 | 管理所有路由、处理导航行为的核心模块 | Vue Router、React Router 就是路由器 |
|
||||
| **路由模式** | 索引方式(卡片目录 vs 电子系统) | 决定 URL 的形式和底层实现方式 | Hash 模式用 `#`、History 模式用普通路径 |
|
||||
| **导航** | 从一个书架走到另一个书架 | 在不同页面之间切换的行为 | 点击链接、编程式跳转、浏览器前进后退 |
|
||||
|
||||
::: tip 📊 从表格中你能看到什么?
|
||||
让我们逐行解读这张表:
|
||||
|
||||
**路由**:只是一个"配置",告诉系统"什么 URL 对应什么页面"。就像图书馆的书号对应一本书的位置。
|
||||
|
||||
**路由器**:是"管理者",负责根据当前的 URL 找到对应的组件并渲染。就像图书馆员根据你提供的书号帮你找到书。
|
||||
|
||||
**路由模式**:是"实现方式",决定了 URL 长什么样、底层用什么技术实现。就像图书馆可以用纸质目录,也可以用电子查询系统。
|
||||
|
||||
**导航**:是"行为",是用户触发页面切换的动作。就像你在图书馆里从 A 区走到 B 区。
|
||||
|
||||
理解这四者的区别非常重要:**路由是静态配置,路由器是动态管理者,模式是技术选型,导航是用户行为。**
|
||||
:::
|
||||
|
||||
### 2.2 路由(Route):URL 与组件的映射契约
|
||||
|
||||
路由,本质上就是一个"契约",它规定了访问某个 URL 时应该显示什么内容。在 Vue Router 中,一个典型的路由配置长这样:
|
||||
|
||||
```javascript
|
||||
const routes = [
|
||||
{
|
||||
path: '/', // URL 路径
|
||||
component: Home // 对应的组件
|
||||
},
|
||||
{
|
||||
path: '/user/:id', // 带参数的动态路由
|
||||
component: UserDetail,
|
||||
children: [ // 嵌套路由
|
||||
{ path: 'profile', component: UserProfile },
|
||||
{ path: 'posts', component: UserPosts }
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**你可能会有疑问:为什么不直接用 `<a>` 标签跳转,非要用路由?**
|
||||
|
||||
答案在于"单页应用"的本质:SPA 只有一个 HTML 页面,所有的页面切换其实都是在同一个页面内替换组件。如果你用传统的 `<a href="/user/123">`,浏览器会真的去请求 `/user/123` 这个路径,导致页面刷新或 404 错误。路由的作用就是拦截这些跳转行为,用 JavaScript 动态替换组件,从而实现无刷新切换。
|
||||
|
||||
::: details 🔧 路由配置的几种常见模式
|
||||
**静态路由**(最简单):
|
||||
```javascript
|
||||
{ path: '/home', component: Home }
|
||||
{ path: '/about', component: About }
|
||||
```
|
||||
|
||||
**动态路由**(带参数):
|
||||
```javascript
|
||||
{ path: '/user/:id', component: UserDetail }
|
||||
// 可以匹配 /user/123、/user/abc 等
|
||||
// 组件内可以通过 route.params.id 获取参数
|
||||
```
|
||||
|
||||
**嵌套路由**(父子关系):
|
||||
```javascript
|
||||
{
|
||||
path: '/user/:id',
|
||||
component: UserLayout, // 父组件
|
||||
children: [
|
||||
{ path: 'profile', component: UserProfile }, // 实际路径 /user/:id/profile
|
||||
{ path: 'posts', component: UserPosts } // 实际路径 /user/:id/posts
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**通配符路由**(404 页面):
|
||||
```javascript
|
||||
{ path: '/:pathMatch(.*)*', component: NotFound }
|
||||
// 匹配所有未定义的路由
|
||||
```
|
||||
:::
|
||||
|
||||
### 2.3 路由模式:Hash vs History 的本质区别
|
||||
|
||||
前端路由有两种主流的实现模式:Hash 模式和 History 模式。它们在 URL 表现形式、底层实现、兼容性等方面有本质区别。
|
||||
|
||||
::: tip 🤔 为什么需要两种模式?
|
||||
这其实是历史原因和技术权衡的结果。
|
||||
|
||||
**Hash 模式**是最早的前端路由实现方式,它利用 URL 中的 hash 部分(即 `#` 后面的内容)。hash 的变化不会触发页面刷新,而且兼容性极好(连 IE8 都支持)。
|
||||
|
||||
**History 模式**是 HTML5 推出后的"标准做法",它利用 History API 提供的 `pushState` 和 `replaceState` 方法,可以让 URL 变得更"正常"(没有 `#`),但需要服务端配合配置。
|
||||
|
||||
打个比方:Hash 模式就像"给房间门口贴个便利贴"(不影响房间结构),History 模式就像"重新给房间编号"(需要更新门牌系统)。
|
||||
:::
|
||||
|
||||
| 特性 | Hash 模式 | History 模式 |
|
||||
|------|-----------|--------------|
|
||||
| **URL 示例** | `https://example.com/#/user/123` | `https://example.com/user/123` |
|
||||
| **实现原理** | 监听 `hashchange` 事件 | 使用 History API (`pushState`、`replaceState`) |
|
||||
| **服务端配置** | 不需要(hash 不被发送到服务器) | **必须配置 fallback 到 index.html** |
|
||||
| **浏览器兼容性** | IE8+(几乎全部浏览器) | IE10+(现代浏览器) |
|
||||
| **SEO 友好度** | 较差(搜索引擎可能忽略 hash) | 良好(URL 结构清晰) |
|
||||
| **用户体验** | URL 有 `#`,看起来像"锚点跳转" | URL 美观,接近传统网站 |
|
||||
| **部署难度** | 低,无需特殊配置 | 高,需要正确配置服务器 |
|
||||
|
||||
<HashVsHistoryDemo />
|
||||
|
||||
::: tip 📊 从表格中你能看到什么?
|
||||
让我们逐行解读这张表:
|
||||
|
||||
**URL 示例**:Hash 模式的 URL 中有明显的 `#`,用户会一眼看出这是个"单页应用";History 模式的 URL 和传统网站一样,看起来更"专业"。
|
||||
|
||||
**实现原理**:Hash 模式监听的是 `hashchange` 事件(hash 变化时触发);History 模式用的是 HTML5 的 History API,可以"假装"页面跳转了,但实际不刷新。
|
||||
|
||||
**服务端配置**:这是最容易踩坑的地方!Hash 模式的 `#` 后面的内容不会发送到服务器,所以服务器不需要知道路由的存在;但 History 模式的完整路径会发送到服务器,如果服务器没配置好,会返回 404。
|
||||
|
||||
**SEO 友好度**:搜索引擎爬虫通常不会执行 JavaScript,Hash 模式的 URL 可能被忽略;History 模式的 URL 结构清晰,更容易被收录。
|
||||
|
||||
**部署难度**:Hash 模式"开箱即用",History 模式需要运维知识(Nginx、Apache 等)。这也是为什么很多个人项目默认用 Hash 模式的原因。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 演进之路:从传统网站到现代路由
|
||||
|
||||
讲了这么多概念,让我们看一个真实的案例:某电商网站是如何从"传统多页面"一步步进化到"现代单页应用路由"的。通过这个案例,你会更直观地理解前端路由解决了什么问题。
|
||||
|
||||
::: tip 📖 背景知识:MPA、SPA、SSR 是什么?
|
||||
在开始案例之前,先简单介绍一下这些名词:
|
||||
|
||||
- **MPA(Multi-Page Application)**:**多页面应用**,传统网站的开发方式。每个页面是独立的 HTML 文件,页面跳转会刷新整个页面。
|
||||
- **SPA(Single-Page Application)**:**单页面应用**,现代前端的主流方式。只有一个 HTML 入口,页面切换通过 JavaScript 动态替换组件,无刷新。
|
||||
- **SSR(Server-Side Rendering)**:**服务端渲染**,在服务器端生成完整的 HTML。结合了 SPA 和 MPA 的优点,首屏渲染快、SEO 好。
|
||||
|
||||
**简单理解**:MPA 是"每次翻页都重新画",SPA 是"在同一张纸上擦了再画",SSR 是"提前在纸上画好再给你"。
|
||||
:::
|
||||
|
||||
### 3.1 演进的全景图
|
||||
|
||||
下面这张表展示了前端应用的四个演进阶段,你可以看到路由技术是如何一步步发展的:
|
||||
|
||||
| 阶段 | 应用类型 | 路由实现 | 核心特点 | 用户体验 |
|
||||
|------|---------|---------|---------|---------|
|
||||
| **阶段一:传统多页** | MPA | 服务端路由 | 每个页面独立 HTML 文件 | 每次跳转都刷新 |
|
||||
| **阶段二:早期 SPA** | SPA(Hash 模式) | Hash 路由 | URL 带 `#`,兼容性好 | 无刷新,但 URL 不美观 |
|
||||
| **阶段三:现代 SPA** | SPA(History 模式) | History 路由 | URL 美观,需服务端配置 | 流畅,URL 接近传统网站 |
|
||||
| **阶段四:混合渲染** | SPA + SSR | 同构路由 | 首屏服务端渲染,后续前端路由 | 首屏快、SEO 好、体验流畅 |
|
||||
|
||||
::: tip 📊 从表格中你能看到什么?
|
||||
让我们逐行解读这张表:
|
||||
|
||||
**阶段一 → 阶段二**:从"有刷新"到"无刷新",这是质的飞跃。用户第一次体验到了"像 App 一样"的流畅感,但代价是 URL 中带着 `#`,看起来不太专业。
|
||||
|
||||
**阶段二 → 阶段三**:从"能用"到"好用"。History 模式让 URL 变得美观,更接近传统网站,但代价是增加了部署复杂度(需要配置服务器)。
|
||||
|
||||
**阶段三 → 阶段四**:从"体验好"到"体验好 + SEO 好"。SSR 解决了 SPA 的 SEO 问题,首屏渲染速度也更快,但实现复杂度大幅提升。
|
||||
|
||||
**总结一下**:前端路由演进不只是"切换变快了",而是**整个应用架构的升级**——从服务端主导到前端主导,再到前后端结合,每一步都在平衡用户体验、开发成本、SEO 等多个维度。
|
||||
:::
|
||||
|
||||
### 3.2 阶段一:传统多页应用——每次都刷新
|
||||
|
||||
为什么叫"传统多页应用"?因为这个阶段每个页面都是独立的 HTML 文件,页面跳转时浏览器会重新下载所有资源(HTML、CSS、JS)。这是最早的 Web 开发方式,现在很多传统网站仍然这样运作。
|
||||
|
||||
在这个阶段,电商网站"买得多"用的是典型的 MPA 架构:
|
||||
|
||||
**开发方式**:
|
||||
- **路由实现**:服务端路由,每个页面对应服务器上的一个 HTML 文件
|
||||
- **页面跳转**:使用 `<a href="/products/123">`,触发完整的页面刷新
|
||||
- **状态管理**:每次跳转都会丢失之前的页面状态(滚动位置、表单内容等)
|
||||
|
||||
**这个阶段的特点**:
|
||||
- ✅ **优点**:实现简单,对搜索引擎友好(SEO 好),浏览器前进后退开箱即用
|
||||
- ❌ **缺点**:每次跳转都刷新,用户体验差,服务器压力大(重复加载相同资源)
|
||||
|
||||
::: details 查看当时的项目结构和访问流程
|
||||
**项目结构**(服务端渲染的典型结构):
|
||||
```
|
||||
server/
|
||||
├── views/ # HTML 模板
|
||||
│ ├── index.html # 首页模板
|
||||
│ ├── products.html # 商品列表页模板
|
||||
│ └── product.html # 商品详情页模板
|
||||
├── public/ # 静态资源
|
||||
│ ├── css/
|
||||
│ ├── js/
|
||||
│ └── images/
|
||||
└── server.js # 服务器入口
|
||||
```
|
||||
|
||||
**页面跳转流程**:
|
||||
```
|
||||
1. 用户点击链接 <a href="/products/123">
|
||||
↓
|
||||
2. 浏览器发送 GET 请求到服务器
|
||||
↓
|
||||
3. 服务器渲染 product.html,插入数据
|
||||
↓
|
||||
4. 返回完整的 HTML 页面
|
||||
↓
|
||||
5. 浏览器解析 HTML、下载 CSS/JS、渲染页面
|
||||
↓
|
||||
6. 用户看到页面(这个过程通常需要 1-3 秒)
|
||||
```
|
||||
|
||||
**用户的痛点**:
|
||||
- 点击链接后页面白屏,等待时间长
|
||||
- 每次跳转都重新下载相同的 CSS/JS 文件
|
||||
- 浏览器前进后退会重新加载页面
|
||||
- 无法保存复杂的页面状态(如筛选条件、滚动位置)
|
||||
:::
|
||||
|
||||
这种开发方式在小网站还能接受,但随着网站规模变大、用户对体验要求提高,这些问题开始严重影响用户留存和转化率。
|
||||
|
||||
### 3.3 阶段二:早期单页应用——Hash 路由的时代
|
||||
|
||||
传统多页应用的问题积累到一定程度,"买得多"团队决定引入前端路由,升级到单页应用架构。这是一个重要的转折点——从"服务端主导"进入"前端主导"。
|
||||
|
||||
但这个阶段也有代价:URL 中带着 `#`,看起来不够专业,搜索引擎收录也有问题。
|
||||
|
||||
**开发方式**:
|
||||
- **路由实现**:Hash 路由,利用 URL 中的 `#` 部分
|
||||
- **页面跳转**:JavaScript 拦截链接点击,动态替换组件
|
||||
- **状态管理**:页面状态在客户端保持,不需要重新加载
|
||||
|
||||
**这个阶段的特点**:
|
||||
- ✅ **优点**:无刷新切换,用户体验流畅,服务器压力减小
|
||||
- ❌ **缺点**:URL 带 `#`,SEO 不友好,首次加载较慢
|
||||
|
||||
::: details 查看 Hash 路由的实现方式
|
||||
**项目结构**(早期 SPA 的典型结构):
|
||||
```
|
||||
project/
|
||||
├── index.html # 唯一的 HTML 入口文件
|
||||
├── css/
|
||||
│ └── app.css # 所有样式打包在一个文件
|
||||
├── js/
|
||||
│ ├── router.js # 简单的路由实现
|
||||
│ ├── views/ # 页面组件
|
||||
│ │ ├── Home.js
|
||||
│ │ ├── ProductList.js
|
||||
│ │ └── ProductDetail.js
|
||||
│ └── app.js # 应用入口
|
||||
└── server.js # 简单的静态文件服务器
|
||||
```
|
||||
|
||||
**Hash 路由的核心代码**:
|
||||
```javascript
|
||||
// router.js - 简化的 Hash 路由实现
|
||||
class HashRouter {
|
||||
constructor(routes) {
|
||||
this.routes = routes
|
||||
this.currentPath = null
|
||||
|
||||
// 监听 hash 变化
|
||||
window.addEventListener('hashchange', () => {
|
||||
this.matchRoute()
|
||||
})
|
||||
|
||||
// 初始化
|
||||
this.matchRoute()
|
||||
}
|
||||
|
||||
matchRoute() {
|
||||
// 获取当前 hash(去掉 #)
|
||||
const hash = window.location.hash.slice(1) || '/'
|
||||
const route = this.routes.find(r => r.path === hash)
|
||||
|
||||
if (route) {
|
||||
this.render(route.component)
|
||||
} else {
|
||||
this.render(NotFoundComponent)
|
||||
}
|
||||
}
|
||||
|
||||
render(component) {
|
||||
const app = document.getElementById('app')
|
||||
app.innerHTML = component.template()
|
||||
component.mount?.(app)
|
||||
}
|
||||
|
||||
navigate(path) {
|
||||
window.location.hash = path
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const router = new HashRouter([
|
||||
{ path: '/', component: Home },
|
||||
{ path: '/products', component: ProductList },
|
||||
{ path: '/products/:id', component: ProductDetail }
|
||||
])
|
||||
|
||||
// 导航
|
||||
router.navigate('/products/123')
|
||||
```
|
||||
|
||||
**URL 形式**:
|
||||
- 首页:`https://example.com/#/`
|
||||
- 商品列表:`https://example.com/#/products`
|
||||
- 商品详情:`https://example.com/#/products/123`
|
||||
|
||||
**带来的改善**:
|
||||
1. **用户体验提升**:页面切换无刷新,流畅自然
|
||||
2. **服务器压力减小**:只加载一次 HTML/CSS/JS,后续只请求数据
|
||||
3. **状态保持**:滚动位置、表单内容等状态可以在页面切换时保持
|
||||
4. **离线友好**:配合 Service Worker 可以实现离线访问
|
||||
|
||||
**新的痛点**:
|
||||
1. **URL 不美观**:`#` 让 URL 看起来像"锚点跳转",不够专业
|
||||
2. **SEO 问题**:搜索引擎爬虫可能忽略 hash 后的内容,导致页面无法被收录
|
||||
3. **首次加载慢**:需要一次性加载所有 JavaScript,首屏时间较长
|
||||
:::
|
||||
|
||||
### 3.4 阶段三:现代单页应用——History 路由成为主流
|
||||
|
||||
Hash 路由的痛点(URL 不美观、SEO 差)困扰了开发者很多年。随着 HTML5 的普及和浏览器兼容性的提升,History 路由逐渐成为主流。
|
||||
|
||||
History 路由利用 HTML5 History API,可以让 URL 变得"正常"(没有 `#`),但代价是需要服务端配合配置。
|
||||
|
||||
**开发方式**:
|
||||
- **路由实现**:History 路由,使用 `pushState` 和 `replaceState`
|
||||
- **路由库**:Vue Router、React Router 等成熟路由库
|
||||
- **服务端配置**:需要配置服务器将所有路由回退到 `index.html`
|
||||
|
||||
**这个阶段的特点**:
|
||||
- ✅ **优点**:URL 美观,SEO 友好,用户体验流畅
|
||||
- ❌ **缺点**:部署需要特殊配置,服务器端必须配合
|
||||
|
||||
::: details History 路由的实现和部署配置
|
||||
**项目结构**(现代 SPA 的典型结构):
|
||||
```
|
||||
project/
|
||||
├── public/
|
||||
│ └── index.html # 唯一的 HTML 入口
|
||||
├── src/
|
||||
│ ├── router/
|
||||
│ │ └── index.js # 路由配置
|
||||
│ ├── views/ # 页面组件
|
||||
│ │ ├── Home.vue
|
||||
│ │ ├── ProductList.vue
|
||||
│ │ └── ProductDetail.vue
|
||||
│ ├── App.vue
|
||||
│ └── main.js
|
||||
├── package.json
|
||||
└── vite.config.js # 构建配置
|
||||
```
|
||||
|
||||
**Vue Router 配置示例**:
|
||||
```javascript
|
||||
// src/router/index.js
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(), // History 模式
|
||||
routes: [
|
||||
{ path: '/', component: () => import('@/views/Home.vue') },
|
||||
{ path: '/products', component: () => import('@/views/ProductList.vue') },
|
||||
{ path: '/products/:id', component: () => import('@/views/ProductDetail.vue') },
|
||||
{ path: '/:pathMatch(.*)*', component: () => import('@/views/NotFound.vue') }
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
```
|
||||
|
||||
**URL 形式**:
|
||||
- 首页:`https://example.com/`
|
||||
- 商品列表:`https://example.com/products`
|
||||
- 商品详情:`https://example.com/products/123`
|
||||
|
||||
**关键:Nginx 配置**(部署时必须配置):
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
root /var/www/app;
|
||||
index index.html;
|
||||
|
||||
# 关键配置:所有路由都指向 index.html
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**为什么需要这个配置?**
|
||||
|
||||
```
|
||||
场景:用户直接访问 https://example.com/products/123
|
||||
|
||||
❌ 没有配置的情况:
|
||||
1. 浏览器向服务器请求 /products/123
|
||||
2. Nginx 在文件系统中查找 /products/123
|
||||
3. 找不到这个文件,返回 404
|
||||
|
||||
✅ 配置了 try_files 的情况:
|
||||
1. 浏览器向服务器请求 /products/123
|
||||
2. Nginx 尝试查找文件 → 不存在
|
||||
3. 回退到 /index.html(根据 try_files 规则)
|
||||
4. 浏览器加载 index.html
|
||||
5. Vue Router 接管,解析 /products/123
|
||||
6. 渲染 ProductDetail 组件
|
||||
7. 页面正常显示!
|
||||
```
|
||||
|
||||
**对比 Hash 模式的差异**:
|
||||
| 对比项 | Hash 模式 | History 模式 |
|
||||
|--------|----------|-------------|
|
||||
| URL | `/#/products/123` | `/products/123` |
|
||||
| 服务端配置 | 不需要 | **必须配置** |
|
||||
| 直接访问 | ✅ 正常工作 | ❌ 需要服务端支持 |
|
||||
| SEO | ⚠️ 较差 | ✅ 良好 |
|
||||
:::
|
||||
|
||||
### 3.5 阶段四:混合渲染——SPA + SSR 的终极方案
|
||||
|
||||
当 History 路由成熟后,团队开始关注更深层次的问题:如何既保留 SPA 的流畅体验,又解决 SEO 和首屏加载慢的问题?
|
||||
|
||||
这个阶段的核心是"同构渲染"——首屏在服务端渲染(SEO 好、加载快),后续交互在前端路由(体验流畅)。
|
||||
|
||||
**开发方式**:
|
||||
- **框架选择**:Next.js(React)、Nuxt.js(Vue)
|
||||
- **渲染策略**:服务端渲染 + 客户端水合(Hydration)
|
||||
- **路由模式**:History 模式(服务端已配置好)
|
||||
|
||||
**这个阶段的特点**:
|
||||
- ✅ **优点**:首屏快、SEO 好、后续交互流畅
|
||||
- ❌ **缺点**:实现复杂度高,需要服务端运行环境
|
||||
|
||||
::: details 混合渲染的工作原理
|
||||
**页面加载流程**:
|
||||
```
|
||||
1. 用户访问 /products/123
|
||||
↓
|
||||
2. 服务端接收到请求
|
||||
↓
|
||||
3. 服务端渲染 ProductDetail 组件 → 生成完整 HTML
|
||||
↓
|
||||
4. 返回 HTML 到浏览器(包含了完整的内容)
|
||||
↓
|
||||
5. 浏览器快速显示内容(首屏渲染快)
|
||||
↓
|
||||
6. 加载 JavaScript,执行"水合"(Hydration)
|
||||
↓
|
||||
7. 后续页面切换由前端路由接管(无刷新)
|
||||
```
|
||||
|
||||
**传统 SPA vs SSR 的首屏对比**:
|
||||
|
||||
| 对比项 | 传统 SPA | SSR |
|
||||
|--------|---------|-----|
|
||||
| 首屏内容 | 白屏 → 加载 JS → 渲染 | 立即显示内容 |
|
||||
| SEO | 爬虫可能看不到内容 | 爬虫能看到完整 HTML |
|
||||
| 首屏时间 | 较慢(需要加载 JS) | 较快(HTML 已包含内容) |
|
||||
| 后续交互 | 流畅(前端路由) | 流畅(前端路由) |
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 原理深入:路由是如何工作的?
|
||||
|
||||
了解了实际案例后,让我们深入看看前端路由的工作原理,理解 Hash 和 History 两种模式到底有什么不同。
|
||||
|
||||
<RouterArchitectureDemo />
|
||||
|
||||
### 4.1 Hash 模式的工作原理
|
||||
|
||||
Hash 模式的核心是利用 URL 中的 `hash` 部分(即 `#` 后面的内容)。hash 有两个重要特性:
|
||||
|
||||
1. **hash 的变化不会触发页面刷新**
|
||||
2. **hash 的变化会记录在浏览器历史栈中**
|
||||
|
||||
这意味着我们可以在不刷新页面的情况下改变 URL,同时浏览器的前进/后退按钮也能正常工作。
|
||||
|
||||
**工作流程**:
|
||||
|
||||
```
|
||||
用户点击链接 <a href="#/user/123">
|
||||
↓
|
||||
浏览器更新 URL(不刷新页面)
|
||||
https://example.com/#/user/123
|
||||
↓
|
||||
触发 hashchange 事件
|
||||
↓
|
||||
路由监听器捕获事件
|
||||
↓
|
||||
解析 hash 值 → /user/123
|
||||
↓
|
||||
匹配路由配置 → 找到 UserDetail 组件
|
||||
↓
|
||||
渲染组件到页面
|
||||
```
|
||||
|
||||
**核心代码实现**:
|
||||
|
||||
```javascript
|
||||
class HashRouter {
|
||||
constructor(routes) {
|
||||
this.routes = routes
|
||||
|
||||
// 监听 hash 变化
|
||||
window.addEventListener('hashchange', () => {
|
||||
this.loadRoute()
|
||||
})
|
||||
|
||||
// 初始化加载
|
||||
this.loadRoute()
|
||||
}
|
||||
|
||||
loadRoute() {
|
||||
// 获取当前 hash,去掉开头的 #
|
||||
const hash = window.location.hash.slice(1) || '/'
|
||||
const route = this.matchRoute(hash)
|
||||
|
||||
if (route) {
|
||||
this.render(route.component)
|
||||
}
|
||||
}
|
||||
|
||||
matchRoute(path) {
|
||||
return this.routes.find(r => r.path === path)
|
||||
}
|
||||
|
||||
render(component) {
|
||||
document.getElementById('app').innerHTML = component.template()
|
||||
}
|
||||
|
||||
push(path) {
|
||||
window.location.hash = path
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip 💡 Hash 模式的优点
|
||||
- **兼容性好**:IE8+ 都支持,几乎适用于所有浏览器
|
||||
- **部署简单**:不需要服务端配置,开箱即用
|
||||
- **实现简单**:只需要监听 `hashchange` 事件
|
||||
:::
|
||||
|
||||
### 4.2 History 模式的工作原理
|
||||
|
||||
History 模式利用 HTML5 History API,提供了 `pushState`、`replaceState` 等方法,可以改变 URL 而不刷新页面。
|
||||
|
||||
**核心 API**:
|
||||
|
||||
```javascript
|
||||
// 添加新的历史记录
|
||||
history.pushState(state, title, url)
|
||||
// 示例:history.pushState({id: 123}, '用户详情', '/user/123')
|
||||
|
||||
// 替换当前历史记录
|
||||
history.replaceState(state, title, url)
|
||||
|
||||
// 监听历史记录变化(前进/后退按钮)
|
||||
window.addEventListener('popstate', (event) => {
|
||||
// event.state 包含 pushState 时传入的 state
|
||||
})
|
||||
```
|
||||
|
||||
**工作流程**:
|
||||
|
||||
```
|
||||
用户点击链接 <a href="/user/123">
|
||||
↓
|
||||
JavaScript 拦截点击事件
|
||||
event.preventDefault()
|
||||
↓
|
||||
调用 history.pushState
|
||||
history.pushState({id: 123}, '用户详情', '/user/123')
|
||||
↓
|
||||
URL 更新(不刷新页面)
|
||||
https://example.com/user/123
|
||||
↓
|
||||
路由匹配并渲染组件
|
||||
↓
|
||||
用户点击浏览器后退按钮
|
||||
↓
|
||||
触发 popstate 事件
|
||||
↓
|
||||
路由监听器捕获事件
|
||||
↓
|
||||
根据新 URL 渲染对应组件
|
||||
```
|
||||
|
||||
**核心代码实现**:
|
||||
|
||||
```javascript
|
||||
class HistoryRouter {
|
||||
constructor(routes) {
|
||||
this.routes = routes
|
||||
|
||||
// 拦截所有链接点击
|
||||
document.addEventListener('click', (e) => {
|
||||
const link = e.target.closest('a')
|
||||
if (link && link.getAttribute('href').startsWith('/')) {
|
||||
e.preventDefault()
|
||||
this.push(link.getAttribute('href'))
|
||||
}
|
||||
})
|
||||
|
||||
// 监听浏览器前进/后退
|
||||
window.addEventListener('popstate', () => {
|
||||
this.loadRoute()
|
||||
})
|
||||
|
||||
// 初始化加载
|
||||
this.loadRoute()
|
||||
}
|
||||
|
||||
loadRoute() {
|
||||
const path = window.location.pathname
|
||||
const route = this.matchRoute(path)
|
||||
|
||||
if (route) {
|
||||
this.render(route.component)
|
||||
}
|
||||
}
|
||||
|
||||
push(path) {
|
||||
history.pushState({}, '', path)
|
||||
this.loadRoute()
|
||||
}
|
||||
|
||||
render(component) {
|
||||
document.getElementById('app').innerHTML = component.template()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: warning ⚠️ History 模式的陷阱
|
||||
History 模式最大的问题在于:**当用户直接访问某个 URL 或刷新页面时,浏览器会向服务器发送请求**。
|
||||
|
||||
如果服务器没有正确配置,会返回 404。解决方案是配置服务器让所有路由都回退到 `index.html`,让前端路由接管后续处理。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 路由配置实战指南
|
||||
|
||||
理论讲得差不多了,下面是实际项目中常用的路由配置模式和最佳实践。
|
||||
|
||||
### 5.1 基础路由配置
|
||||
|
||||
::: details Vue Router 完整配置示例
|
||||
|
||||
```javascript
|
||||
// src/router/index.js
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Home from '@/views/Home.vue'
|
||||
import NotFound from '@/views/NotFound.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/user/:id',
|
||||
name: 'UserDetail',
|
||||
component: () => import('@/views/UserDetail.vue'),
|
||||
props: true // 将路由参数作为 props 传递
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: NotFound
|
||||
}
|
||||
],
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// 滚动行为:返回时保持滚动位置,否则滚动到顶部
|
||||
if (savedPosition) {
|
||||
return savedPosition
|
||||
} else {
|
||||
return { top: 0 }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 5.2 路由懒加载:提升首屏性能
|
||||
|
||||
路由懒加载是指只在访问某个路由时才加载对应的组件,而不是一次性加载所有组件。这可以显著减少首屏加载时间。
|
||||
|
||||
```javascript
|
||||
// ❌ 一次性加载所有组件(首屏慢)
|
||||
import Home from '@/views/Home.vue'
|
||||
import About from '@/views/About.vue'
|
||||
import User from '@/views/User.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: Home },
|
||||
{ path: '/about', component: About },
|
||||
{ path: '/user', component: User }
|
||||
]
|
||||
|
||||
// ✅ 懒加载(首屏快)
|
||||
const routes = [
|
||||
{ path: '/', component: () => import('@/views/Home.vue') },
|
||||
{ path: '/about', component: () => import('@/views/About.vue') },
|
||||
{ path: '/user', component: () => import('@/views/User.vue') }
|
||||
]
|
||||
```
|
||||
|
||||
<CodeSplittingDemo />
|
||||
|
||||
::: tip 💡 懒加载的原理
|
||||
当你使用 `import('@/views/Home.vue')` 时,Webpack/Vite 会把这个组件打包成单独的文件。只有当用户访问这个路由时,才会下载对应的文件。
|
||||
|
||||
打个比方:懒加载就像"按需点菜",而不是一次性把所有菜都端上来。这样可以减少首屏加载时间,提升用户体验。
|
||||
:::
|
||||
|
||||
### 5.3 路由守卫:权限控制与导航拦截
|
||||
|
||||
路由守卫可以在路由跳转前后执行逻辑,常用于权限验证、页面标题设置、数据预加载等场景。
|
||||
|
||||
```javascript
|
||||
// 全局前置守卫
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// 设置页面标题
|
||||
document.title = to.meta.title || 'My App'
|
||||
|
||||
// 权限验证
|
||||
if (to.meta.requiresAuth) {
|
||||
const isAuthenticated = await checkAuth()
|
||||
if (!isAuthenticated) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
// 全局后置钩子
|
||||
router.afterEach((to, from) => {
|
||||
// 页面访问统计
|
||||
analytics.trackPageView(to.path)
|
||||
})
|
||||
|
||||
// 路由级守卫
|
||||
const routes = [
|
||||
{
|
||||
path: '/admin',
|
||||
component: Admin,
|
||||
meta: { requiresAuth: true, roles: ['admin'] },
|
||||
beforeEnter: (to, from, next) => {
|
||||
// 这个路由的专属逻辑
|
||||
if (hasPermission()) {
|
||||
next()
|
||||
} else {
|
||||
next('/403')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
::: tip 💡 路由守卫的常见用途
|
||||
- **权限验证**:检查用户是否有权限访问某个页面
|
||||
- **页面标题**:动态设置 document.title
|
||||
- **数据预加载**:在进入页面前提前获取数据
|
||||
- **进度条**:显示页面切换的进度条
|
||||
- **访问统计**:记录页面访问情况
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题与解决方案
|
||||
|
||||
### 6.1 部署后刷新 404
|
||||
|
||||
**问题**:本地开发正常,部署到服务器后,直接访问某个路由或刷新页面会显示 404。
|
||||
|
||||
**原因**:History 模式下,服务器会将 URL 当作文件路径去查找,但 SPA 的所有路由其实都指向 `index.html`。
|
||||
|
||||
**解决方案**:配置服务器 fallback。
|
||||
|
||||
```nginx
|
||||
# Nginx 配置
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
```
|
||||
|
||||
```apache
|
||||
# Apache 配置(.htaccess)
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteRule ^index\.html$ - [L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /index.html [L]
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
### 6.2 路由参数丢失
|
||||
|
||||
**问题**:页面刷新后,路由参数 `$route.params` 丢失。
|
||||
|
||||
**原因**:路由参数只在路由跳转时存在,刷新后需要从 URL 中重新解析。
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```javascript
|
||||
// ❌ 错误做法:只在 created 时获取参数
|
||||
created() {
|
||||
const userId = this.$route.params.id
|
||||
this.fetchUser(userId)
|
||||
}
|
||||
|
||||
// ✅ 正确做法:监听路由变化
|
||||
watch: {
|
||||
'$route.params.id': {
|
||||
immediate: true,
|
||||
handler(newId) {
|
||||
this.fetchUser(newId)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 页面切换时滚动位置异常
|
||||
|
||||
**问题**:页面切换后,滚动位置没有重置,或者返回时没有保持之前的位置。
|
||||
|
||||
**解决方案**:配置路由的 `scrollBehavior`。
|
||||
|
||||
```javascript
|
||||
const router = createRouter({
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// 返回时保持滚动位置
|
||||
if (savedPosition) {
|
||||
return savedPosition
|
||||
}
|
||||
// 跳转到锚点
|
||||
if (to.hash) {
|
||||
return { el: to.hash }
|
||||
}
|
||||
// 否则滚动到顶部
|
||||
return { top: 0 }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结
|
||||
|
||||
让我们用一张表格来回顾前端路由的核心概念:
|
||||
|
||||
| 概念 | 一句话解释 | 解决的问题 | 代表方案 |
|
||||
|------|-----------|-----------|----------|
|
||||
| **路由** | URL 和组件的映射关系 | 访问不同 URL 显示不同内容 | Vue Router、React Router |
|
||||
| **Hash 模式** | 利用 URL hash 实现路由 | 兼容性好、部署简单 | Vue Router Hash 模式 |
|
||||
| **History 模式** | 利用 History API 实现路由 | URL 美观、SEO 好 | Vue Router History 模式 |
|
||||
| **路由懒加载** | 按需加载路由组件 | 减少首屏加载时间 | `() => import('./Page.vue')` |
|
||||
| **路由守卫** | 路由跳转前后的钩子函数 | 权限控制、数据预加载 | `beforeEach`、`beforeEnter` |
|
||||
| **动态路由** | 带参数的路由 | 匹配一类路径而非单个 | `/user/:id` |
|
||||
|
||||
::: info 写在最后
|
||||
前端路由是现代单页应用的核心技术之一。从早期的 Hash 模式到现在主流的 History 模式,路由技术在不断进化,为用户提供更流畅的浏览体验。
|
||||
|
||||
理解路由的原理和模式,能让你在遇到部署、性能、SEO 问题时快速定位、精准解决。更重要的是,它能在项目架构设计时帮你做出更明智的选择——什么时候用 Hash、什么时候用 History、如何避免常见的坑。
|
||||
|
||||
希望这篇文章能帮助你建立起对前端路由的整体认知。当你在实际项目中遇到路由相关的问题时,能够知道从哪里入手、如何定位、怎样解决。
|
||||
:::
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
# TypeScript:给 JS 加上类型系统
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,827 @@
|
||||
# 网页性能的度量与优化
|
||||
::: tip 🎯 核心问题
|
||||
**为什么你的网页加载很慢,用户还在疯狂抱怨卡顿?** 这就像是问:为什么餐厅上菜慢、顾客等得不耐烦?本章将带你深入理解前端性能优化的核心概念,让你的网页"飞"起来。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要"性能优化"?
|
||||
|
||||
### 1.1 从能用到好用:性能优化的演变
|
||||
|
||||
十年前的网页非常简单,一个页面可能就几 KB,加载速度几乎感觉不到延迟。那时的我们根本不需要考虑性能优化——因为问题还没出现。
|
||||
|
||||
但现在完全不同了。现代网页的复杂度呈指数级增长:一个电商首页可能有几十张高清图片,一个社交平台可能同时加载上千条动态,一个管理后台可能包含几十个交互组件。这些"丰富"的功能背后,是庞大的代码量和资源体积,如果不好好优化,用户体验就会一塌糊涂。
|
||||
|
||||
<div style="display: flex; gap: 20px; margin: 20px 0;">
|
||||
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||
|
||||
**👴 十年前的网页**
|
||||
- 单个页面只有几 KB 到几十 KB
|
||||
- 只有文字和少量图片
|
||||
- 用户几乎感觉不到加载延迟
|
||||
- 不需要任何性能优化
|
||||
|
||||
</div>
|
||||
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||
|
||||
**🚀 现代的网页**
|
||||
- 单个页面可能几 MB 甚至更大
|
||||
- 有高清图片、视频、交互组件
|
||||
- 加载慢、滚动卡、点击反应迟钝
|
||||
- 必须做性能优化才能用
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
**这就是"性能优化"要解决的问题:让用户等待的时间更短,让操作更流畅。**
|
||||
|
||||
### 1.2 一个真实的踩坑故事:为什么你需要了解性能优化
|
||||
|
||||
你可能会说:"现在的网络这么快,设备这么好,还需要考虑性能优化吗?" 让我讲一个真实的故事,你就会明白为什么这些知识如此重要。
|
||||
|
||||
::: warning 小王的性能踩坑记
|
||||
小王是一个刚入职的前端工程师,负责开发公司的电商首页。他用了最新的 Vue 3、最流行的 UI 库,功能做得非常完善,自己在公司的高性能电脑上测试时一切正常。
|
||||
|
||||
但上线后第二天,客服部门就炸锅了——大量用户投诉说"网站太卡了"、"图片加载不出来"、"点击按钮半天没反应"。小王打开自己的开发机测试,一切都很流畅啊,他完全不理解问题出在哪里。
|
||||
|
||||
后来请师傅帮忙定位,师傅让他用一台普通的笔记本电脑,连上普通的 4G 网络,然后再测试自己的网站。小王这才傻眼了:首页加载要等十几秒,滚动列表时卡得像 PPT,点击按钮后要等好几秒才有反应。
|
||||
|
||||
原来小王的开发环境是顶配的 MacBook Pro + 千兆光纤,而大多数用户用的是普通设备 + 移动网络。他写的代码里有几十张未压缩的高清图片,引入了整个 UI 库但只用了几个组件,还在渲染时做了大量同步计算。
|
||||
|
||||
解决方案其实不复杂:压缩图片、按需引入组件、把计算放到后台线程、使用虚拟列表。这样改动之后,首页加载时间从十几秒变成了 2 秒,滚动也非常流畅,用户投诉立刻消失了。
|
||||
|
||||
小王从此明白了一个道理:**不了解性能优化,你写出来的代码在自己电脑上跑得飞快,但在用户设备上可能根本没法用。**
|
||||
:::
|
||||
|
||||
::: info 💡 核心启示
|
||||
性能优化不是可选项,而是必备技能。你要站在用户的视角思考问题——他们用的是普通设备、普通网络,如果你的代码在他们设备上跑不动,那就说明你需要优化了。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心概念:加载、渲染、交互
|
||||
|
||||
::: tip 🤔 这些概念和性能有什么关系?
|
||||
加载、渲染、交互就是用户访问网页的三个核心环节,每个环节都可能成为性能瓶颈。
|
||||
|
||||
当用户访问你的网页时,会依次经历:
|
||||
1. **加载** → 把 HTML/CSS/JS/图片 从服务器下载到浏览器
|
||||
2. **渲染** → 把下载的内容"画"成用户能看到的页面
|
||||
3. **交互** → 响应用户的点击、滚动等操作
|
||||
|
||||
所以,**性能优化就是让这三个环节都快起来**。理解它们,你才能知道性能瓶颈出在哪里,该用什么方法优化。
|
||||
:::
|
||||
|
||||
在深入学习具体优化技巧之前,我们需要先搞清楚这几个核心概念。为了帮助你更好地理解,我们用餐厅的比喻来类比它们之间的关系。
|
||||
|
||||
### 2.1 用餐厅比喻理解三个环节
|
||||
|
||||
想象你去一家餐厅吃饭,这个过程和访问网页惊人地相似:
|
||||
|
||||
| 环节 | 🍽️ 餐厅比喻 | 实际作用 | 具体例子 |
|
||||
|------|-------------|----------|----------|
|
||||
| **加载** | 把食材从仓库运送到厨房 | 把 HTML/CSS/JS/图片 从服务器下载到浏览器 | 用户打开网页,浏览器开始下载各种资源 |
|
||||
| **渲染** | 厨师把食材加工成菜肴 | 浏览器把代码转换成用户能看到的页面 | 浏览器解析 HTML、计算布局、绘制页面 |
|
||||
| **交互** | 服务员响应顾客的需求 | 浏览器响应点击、滚动等操作 | 用户点击按钮,页面做出反馈 |
|
||||
|
||||
### 2.2 加载(Loading):食材运送
|
||||
|
||||
加载是指把网页所需的各种资源(HTML、CSS、JavaScript、图片、字体等)从服务器下载到浏览器的过程。这个过程就像把食材从仓库运送到厨房,如果运送慢或者食材太多,厨房就得干等着。
|
||||
|
||||
**为什么加载会慢?** 主要有三个原因:首先,资源体积太大——一张未压缩的高清图片可能就有 5MB,相当于下载一本小说;其次,网络延迟——如果服务器在国外,或者用户用移动网络,每个请求都要等很久;最后,请求太多——浏览器同时下载的资源数量有限,太多资源就要排队。
|
||||
|
||||
::: details 🔍 看看加载阶段都做了什么
|
||||
当用户在浏览器地址栏输入网址并按下回车后,会依次发生:
|
||||
|
||||
1. **DNS 解析**:把域名(如 `www.example.com`)转换成 IP 地址(如 `192.168.1.1`),就像通过电话簿查找餐厅地址
|
||||
2. **TCP 连接**:浏览器和服务器建立连接,就像打电话前要先拨号
|
||||
3. **TLS 握手**:建立安全连接(HTTPS),就像确认对方身份
|
||||
4. **请求资源**:浏览器向服务器请求 HTML 文件
|
||||
5. **解析 HTML**:浏览器解析 HTML,发现需要 CSS、JS、图片等资源,继续请求
|
||||
6. **下载资源**:把所有需要的资源下载到本地
|
||||
7. **开始渲染**:下载完成后,开始渲染页面
|
||||
|
||||
前面的 1-4 步叫"首字节时间"(TTFB),后面的 5-7 步是真正的资源下载时间。
|
||||
:::
|
||||
|
||||
**常见的加载优化手段:**
|
||||
|
||||
- **压缩资源**:把文件变小(Gzip、Brotli 压缩)
|
||||
- **使用 CDN**:把文件存在离用户更近的服务器上
|
||||
- **懒加载**:只加载用户看得到的内容,剩下的等用户滚动时再加载
|
||||
- **代码分割**:把大文件拆成小文件,按需加载
|
||||
|
||||
### 2.3 渲染(Rendering):厨师做菜
|
||||
|
||||
渲染是指浏览器把下载的 HTML、CSS、JavaScript 转换成用户能看到的页面的过程。这个过程就像厨师把食材加工成菜肴,如果工序复杂、步骤多,上菜就会慢。
|
||||
|
||||
::: tip 📖 什么是"渲染"?
|
||||
你可能听说过"渲染"这个词,它到底是什么?
|
||||
|
||||
**简单来说,渲染就是把代码变成画面的过程。**
|
||||
|
||||
浏览器要做的事情包括:
|
||||
1. **解析 HTML** → 生成 DOM 树(页面的结构)
|
||||
2. **解析 CSS** → 生成 CSSOM 树(页面的样式)
|
||||
3. **合并** → 生成渲染树(结构和样式的结合)
|
||||
4. **布局** → 计算每个元素的位置和大小
|
||||
5. **绘制** → 把元素画出来
|
||||
6. **合成** → 把多个图层合并成最终画面
|
||||
|
||||
这个过程非常复杂,任何一个环节出问题,都会导致页面卡顿。
|
||||
:::
|
||||
|
||||
**为什么渲染会慢?** 主要有两个原因:首先,页面太复杂——如果一个页面有上万个 DOM 节点,浏览器计算布局和绘制就会非常耗时;其次,频繁修改页面——如果 JavaScript 代码频繁修改 DOM,会导致浏览器反复重新布局和绘制,消耗大量性能。
|
||||
|
||||
::: details 📁 看看渲染阶段都做了什么
|
||||
**渲染的完整流程**:
|
||||
|
||||
```
|
||||
HTML (字符串)
|
||||
↓
|
||||
[解析 HTML] → 生成 DOM 树
|
||||
↓
|
||||
DOM 树 (页面结构)
|
||||
|
||||
CSS (样式表)
|
||||
↓
|
||||
[解析 CSS] → 生成 CSSOM 树
|
||||
↓
|
||||
CSSOM 树 (页面样式)
|
||||
|
||||
DOM 树 + CSSOM 树
|
||||
↓
|
||||
[合并] → 生成渲染树
|
||||
↓
|
||||
渲染树 (要渲染的元素)
|
||||
↓
|
||||
[布局 Layout] → 计算每个元素的位置和大小
|
||||
↓
|
||||
[绘制 Paint] → 填充颜色、绘制文字
|
||||
↓
|
||||
[合成 Composite] → 合并多个图层
|
||||
↓
|
||||
最终画面
|
||||
```
|
||||
|
||||
**关键渲染路径(Critical Rendering Path)**:浏览器要尽快把第一屏内容渲染出来,让用户觉得"网站很快"。这叫"关键渲染路径优化"。
|
||||
:::
|
||||
|
||||
👇 **动手看看**:
|
||||
下面这个演示展示了浏览器是如何渲染页面的。点击"下一步",观察渲染的各个阶段:
|
||||
|
||||
<PerformanceOverviewDemo />
|
||||
|
||||
**常见的渲染优化手段:**
|
||||
|
||||
- **减少重排和重绘**:避免频繁修改 DOM,使用 `transform` 和 `opacity` 代替 `top` 和 `width`
|
||||
- **虚拟列表**:只渲染可见区域的内容,大量数据时性能提升明显
|
||||
- **CSS 动画**:用 CSS 动画代替 JavaScript 动画,性能更好
|
||||
|
||||
### 2.4 交互(Interaction):服务员响应
|
||||
|
||||
交互是指浏览器响应用户操作(点击、滚动、输入等)的过程。这个过程就像服务员响应顾客的需求,如果服务员忙不过来,顾客就得等。
|
||||
|
||||
**为什么交互会卡?** 主要原因是**主线程被阻塞了**。浏览器的 JavaScript 是单线程的,如果代码在执行复杂的计算,就没法响应用户的操作,导致页面卡顿。
|
||||
|
||||
::: tip 🤔 什么是"主线程"?
|
||||
浏览器有多个线程,但负责执行 JavaScript、渲染页面、响应用户操作的只有一个——**主线程**。
|
||||
|
||||
你可以把主线程想象成一个**忙碌的服务员**,他要做很多事情:
|
||||
- 执行 JavaScript 代码(计算数据、调用 API)
|
||||
- 渲染页面(布局、绘制)
|
||||
- 响应用户操作(点击按钮、滚动页面)
|
||||
|
||||
问题来了:**他只有一个人**。如果他在执行复杂的 JavaScript 计算(比如处理一万条数据),这时候用户点击了按钮,他是没法立即响应的,必须等计算完才行。这就是**卡顿**的根源。
|
||||
|
||||
**解决方案**:
|
||||
- 把复杂的计算放到 Web Worker(后台线程)
|
||||
- 使用时间切片,把大任务拆成小任务
|
||||
- 避免同步的复杂操作,改用异步
|
||||
:::
|
||||
|
||||
👇 **动手试试看**:
|
||||
下面这个演示对比了同步计算和 Web Worker 的区别。点击"开始计算",观察页面是否卡顿:
|
||||
|
||||
<PerformanceMetricsDemo />
|
||||
|
||||
**常见的交互优化手段:**
|
||||
|
||||
- **防抖和节流**:限制事件的触发频率(比如滚动事件、输入事件)
|
||||
- **Web Worker**:把复杂计算放到后台线程,不阻塞主线程
|
||||
- **时间切片**:把大任务拆成小任务,让浏览器有机会响应用户操作
|
||||
|
||||
---
|
||||
|
||||
## 3. 实战:一个团队的性能优化演进之路
|
||||
|
||||
讲了这么多概念,让我们看一个真实的案例:某创业公司是如何从"完全没考虑性能"一步步进化到"系统化性能优化"的。通过这个案例,你会更直观地理解性能优化到底解决了什么问题。
|
||||
|
||||
### 3.1 演进的全景图
|
||||
|
||||
下面这张表展示了性能优化的四个阶段,你可以看到优化手段、工具、指标是如何一步步进化的:
|
||||
|
||||
| 阶段 | 优化手段 | 监控工具 | 核心指标 | 核心变化 |
|
||||
|------|---------|---------|---------|----------|
|
||||
| **阶段一:原始时代** | 无(没考虑) | 无(凭感觉) | 无 | 完全没性能意识,能跑就行 |
|
||||
| **阶段二:手动优化** | 压缩图片、减少请求 | 浏览器 Network 面板 | 页面加载时间 | 开始有意识,但方法原始 |
|
||||
| **阶段三:系统化优化** | 代码分割、懒加载、虚拟列表 | Lighthouse、Performance 面板 | FCP、LCP、TBT | 用专业工具,有明确的优化目标 |
|
||||
| **阶段四:持续优化** | 性能预算、CI/CD 检查 | RUM、Lighthouse CI | INP、CLS、全链路监控 | 把性能纳入开发流程 |
|
||||
|
||||
::: tip 📊 从表格中你能看到什么?
|
||||
让我们逐行解读这张表:
|
||||
|
||||
**阶段一 → 阶段二**:从"没意识"到"有意识"。这是关键的一步——开发者开始意识到性能是个问题,并且尝试优化。但优化手段比较原始,主要靠感觉和经验。
|
||||
|
||||
**阶段二 → 阶段三**:从"手动"到"系统化"。这是质的飞跃——开始使用专业工具(Lighthouse、Performance 面板)来诊断性能问题,用科学的方法(代码分割、懒加载)来优化,而不是凭感觉。
|
||||
|
||||
**阶段三 → 阶段四**:从"一次性优化"到"持续优化"。当性能优化成为开发流程的一部分后,就需要建立监控体系(RUM、真实用户监控),在开发阶段就设置性能预算,防止退化。
|
||||
|
||||
**总结一下**:性能优化演进不只是"用了更多技术",而是**整个思维方式的升级**——从被动响应到主动预防,从凭感觉到数据驱动,从单次优化到持续改进。
|
||||
:::
|
||||
|
||||
### 3.2 阶段一:原始时代——完全没考虑
|
||||
|
||||
为什么叫"原始时代"?因为这个阶段完全没考虑性能问题——能跑就行。团队只有 3 个人,做一个简单的企业官网,项目很小,看起来没什么问题。
|
||||
|
||||
但随着项目变大、用户增多,问题开始暴露出来。
|
||||
|
||||
**开发方式**:
|
||||
- **优化手段**:无,直接开发,没考虑性能
|
||||
- **监控工具**:无,凭感觉判断快慢
|
||||
- **核心指标**:无
|
||||
|
||||
**这个阶段的特点**:
|
||||
- ✅ **优点**:开发快,没有额外的学习成本
|
||||
- ❌ **缺点**:用户体验差,网速慢时根本没法用
|
||||
|
||||
::: details 查看当时的问题
|
||||
**遇到的具体问题**:
|
||||
|
||||
1. **图片太大**:产品经理上传了一张 5MB 的首页 Banner 图,移动网络用户打开网页要等 1 分钟
|
||||
2. **没有压缩**:CSS 和 JS 文件完全没有压缩,体积是压缩后的 3 倍
|
||||
3. **没有缓存**:每次访问都要重新下载所有资源,老用户也要等
|
||||
4. **同步加载**:所有 JS 文件都在 `<head>` 中同步加载,阻塞页面渲染
|
||||
|
||||
**用户的反馈**:
|
||||
- "你们网站怎么打不开?"
|
||||
- "图片半天加载不出来,就是空白"
|
||||
- "点击按钮没反应,是不是网站坏了?"
|
||||
|
||||
**当时的临时解决方案**:
|
||||
```html
|
||||
<!-- 用 loading 遮罩"欺骗"用户 -->
|
||||
<div id="loading">加载中...</div>
|
||||
<script>
|
||||
// 页面加载完成后才移除遮罩
|
||||
window.onload = function() {
|
||||
document.getElementById('loading').style.display = 'none'
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
这完全是在"自欺欺人"——页面还是很慢,只是用户看不到而已。
|
||||
:::
|
||||
|
||||
### 3.3 阶段二:手动优化——开始有意识
|
||||
|
||||
原始时代的问题积累到一定程度,团队终于决定开始做性能优化。这是一个重要的转折点——从"完全不考虑"到"有意识地优化"。
|
||||
|
||||
但这个阶段的优化比较原始,主要靠压缩图片、合并文件等简单手段。
|
||||
|
||||
**开发方式**:
|
||||
- **优化手段**:手动压缩图片、合并 CSS/JS 文件、减少 HTTP 请求
|
||||
- **监控工具**:浏览器 Network 面板、简单的计时日志
|
||||
- **核心指标**:页面加载时间(手动用秒表计时)
|
||||
|
||||
**这个阶段的特点**:
|
||||
- ✅ **优点**:有明显改善,用户不再疯狂投诉
|
||||
- ❌ **缺点**:优化不系统,容易反复,缺少量化指标
|
||||
|
||||
::: details 查看手动优化的具体做法
|
||||
**手动优化手段**:
|
||||
|
||||
1. **手动压缩图片**:
|
||||
- 用 Photoshop 把每张图片手动"另存为 Web 格式"
|
||||
- 把 PNG 转 JPEG(有损压缩,但体积小很多)
|
||||
- 缩小图片尺寸(比如 2000px 宽的图缩小到 800px)
|
||||
|
||||
2. **手动合并文件**:
|
||||
```html
|
||||
<!-- 优化前:10 个 JS 文件 = 10 个请求 -->
|
||||
<script src="utils.js"></script>
|
||||
<script src="api.js"></script>
|
||||
<script src="component-a.js"></script>
|
||||
<script src="component-b.js"></script>
|
||||
...(还有 6 个)
|
||||
|
||||
<!-- 优化后:1 个合并的 JS 文件 = 1 个请求 -->
|
||||
<script src="all.js"></script>
|
||||
```
|
||||
|
||||
3. **把 CSS/JS 移到页面底部**:
|
||||
```html
|
||||
<body>
|
||||
<!-- 页面内容 -->
|
||||
<h1>欢迎访问</h1>
|
||||
|
||||
<!-- 优化:把 CSS/JS 放在最后 -->
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
```
|
||||
|
||||
**带来的改善**:
|
||||
- 图片体积从 5MB 减小到 500KB(减少 90%)
|
||||
- HTTP 请求数从 30 个减少到 5 个
|
||||
- 页面加载时间从 30 秒减少到 8 秒
|
||||
|
||||
**新的痛点**:
|
||||
1. **手动工作量大**:每次更新都要手动压缩图片、合并文件
|
||||
2. **容易忘记**:新人不知道要优化,直接上传原图
|
||||
3. **缺少量化**:只知道"快了一些",但不知道具体快多少
|
||||
:::
|
||||
|
||||
### 3.4 阶段三:系统化优化——用工具和数据说话
|
||||
|
||||
阶段二的问题(手动工作量大、缺少量化)困扰了团队很久。直到后来,团队发现了 Lighthouse、Performance 面板等专业工具,进入了系统化优化时代。
|
||||
|
||||
这个阶段的核心是**用数据驱动优化**——先用工具诊断问题,找到性能瓶颈,再有针对性地优化。
|
||||
|
||||
**开发方式**:
|
||||
- **优化手段**:代码分割、懒加载、虚拟列表、图片自动压缩
|
||||
- **监控工具**:Lighthouse、Chrome Performance 面板、WebPageTest
|
||||
- **核心指标**:FCP(首屏时间)、LCP(最大内容绘制)、TBT(总阻塞时间)
|
||||
|
||||
::: details 系统化优化的具体做法
|
||||
**使用 Lighthouse 诊断问题**:
|
||||
|
||||
Lighthouse 是 Google 开发的自动化性能测试工具,可以给出全面的性能报告和优化建议。
|
||||
|
||||
```bash
|
||||
# 使用 Lighthouse 测试网页
|
||||
lighthouse https://www.example.com --view
|
||||
```
|
||||
|
||||
Lighthouse 会给出:
|
||||
- **性能评分**(0-100 分)
|
||||
- **核心指标**(FCP、LCP、CLS、TBT、INP)
|
||||
- **优化建议**(比如"启用文本压缩"、"移除未使用的 JavaScript")
|
||||
|
||||
**关键指标解读**:
|
||||
|
||||
| 指标 | 全称 | 含义 | 理想值 |
|
||||
|------|------|------|--------|
|
||||
| **FCP** | First Contentful Paint | 首次内容绘制时间(用户看到第一块内容的时间) | <1.8s |
|
||||
| **LCP** | Largest Contentful Paint | 最大内容绘制时间(主要内容加载完成的时间) | <2.5s |
|
||||
| **TBT** | Total Blocking Time | 总阻塞时间(主线程被阻塞的总时间) | <200ms |
|
||||
| **CLS** | Cumulative Layout Shift | 累积布局偏移(页面元素乱跳的程度) | <0.1 |
|
||||
|
||||
:::
|
||||
|
||||
**这个阶段的特点**:
|
||||
- ✅ **优点**:优化有针对性,效果好,有量化指标
|
||||
- ❌ **缺点**:需要学习工具和指标,有一定门槛
|
||||
|
||||
::: details 查看系统化优化的具体技术
|
||||
**1. 代码分割(Code Splitting)**:
|
||||
|
||||
把大文件拆成小文件,按需加载。比如用户访问首页时,只加载首页需要的代码,等到点击"关于我们"时,再去加载关于页面的代码。
|
||||
|
||||
```js
|
||||
// 优化前:所有代码都在一个文件,一次性加载
|
||||
import About from './views/About.vue'
|
||||
import Contact from './views/Contact.vue'
|
||||
// ... 还有 10 个页面
|
||||
|
||||
// 优化后:懒加载,访问时才加载
|
||||
const About = () => import('./views/About.vue')
|
||||
const Contact = () => import('./views/Contact.vue')
|
||||
```
|
||||
|
||||
**效果**:首页加载的代码量减少 70%,首屏时间从 5 秒降到 1.5 秒。
|
||||
|
||||
**2. 图片懒加载(Lazy Loading)**:
|
||||
|
||||
只加载用户看得到的图片,滚动到可视区域时再加载其他图片。
|
||||
|
||||
```html
|
||||
<!-- 现代浏览器支持原生的懒加载 -->
|
||||
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" />
|
||||
```
|
||||
|
||||
**效果**:首页加载的图片数量从 20 张减少到 3 张,节省 80% 的带宽。
|
||||
|
||||
**3. 虚拟列表(Virtual Scrolling)**:
|
||||
|
||||
如果要渲染 10,000 条数据,不要真的创建 10,000 个 DOM 节点,而是只渲染可见区域的 20 条,滚动时动态替换。
|
||||
|
||||
```vue
|
||||
<!-- 使用 vue-virtual-scroller 组件 -->
|
||||
<RecycleScroller
|
||||
:items="items"
|
||||
:item-size="50"
|
||||
key-field="id"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div>{{ item.name }}</div>
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
```
|
||||
|
||||
**效果**:10,000 条数据从"卡死"变成"流畅滚动",内存占用减少 95%。
|
||||
:::
|
||||
|
||||
### 3.5 阶段四:持续优化——把性能纳入开发流程
|
||||
|
||||
当工具和方法成熟后,团队开始关注更深层次的问题:如何防止性能退化?如何让性能成为开发流程的一部分?
|
||||
|
||||
这个阶段的核心是**建立性能监控和预算体系**——不是上线后再优化,而是在开发阶段就预防性能问题。
|
||||
|
||||
**开发方式**:
|
||||
- **优化手段**:性能预算(Performance Budget)、Lighthouse CI、真实用户监控(RUM)
|
||||
- **监控工具**:Lighthouse CI、WebPageTest API、Google Analytics
|
||||
- **核心指标**:INP(交互延迟)、CLS(布局偏移)、全链路监控
|
||||
|
||||
::: details 持续优化的具体做法
|
||||
**1. 设置性能预算**:
|
||||
|
||||
在打包配置中设置限制,超过就报错,防止"无意中引入大文件"。
|
||||
|
||||
```js
|
||||
// vite.config.js
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 限制单个文件不超过 200KB
|
||||
chunkFileNames: 'js/[name]-[hash].js',
|
||||
}
|
||||
},
|
||||
// 超过 200KB 时发出警告
|
||||
chunkSizeWarningLimit: 200
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**2. Lighthouse CI**:
|
||||
|
||||
每次提交代码时,自动运行 Lighthouse 测试,如果性能分数下降,就阻止合并。
|
||||
|
||||
```yaml
|
||||
# .github/workflows/lighthouse.yml
|
||||
name: Lighthouse CI
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
lighthouse:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run Lighthouse CI
|
||||
uses: treosh/lighthouse-ci-action@v9
|
||||
with:
|
||||
urls: |
|
||||
https://staging.example.com
|
||||
budgetPath: ./budget.json
|
||||
```
|
||||
|
||||
**3. 真实用户监控(RUM)**:
|
||||
|
||||
在真实用户浏览器中收集性能数据,而不是只在开发环境测试。
|
||||
|
||||
```js
|
||||
// 发送性能数据到服务器
|
||||
const perfData = performance.getEntriesByType('navigation')[0]
|
||||
const lcp = performance.getEntriesByType('largest-contentful-paint')[0]
|
||||
|
||||
fetch('/api/perf', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
fcp: perfData.loadEventEnd - perfData.fetchStart,
|
||||
lcp: lcp.renderTime || lcp.loadTime,
|
||||
url: window.location.href
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 能及时发现性能退化(比如某次提交导致 LCP 从 2 秒变成 5 秒)
|
||||
- 能了解真实用户的体验(而不是开发环境的"理想状态")
|
||||
- 能针对性地优化最慢的那 10% 用户
|
||||
:::
|
||||
|
||||
**这个阶段会做什么?**
|
||||
|
||||
1. **性能预算**:限制文件大小、请求数量,超过就报警
|
||||
2. **CI/CD 检查**:每次提交代码自动测试性能,退化就阻止合并
|
||||
3. **真实用户监控**:收集真实用户的性能数据,持续改进
|
||||
4. **定期性能报告**:每周/每月生成性能报告,跟踪趋势
|
||||
|
||||
---
|
||||
|
||||
## 4. 常见性能瓶颈与解决方案
|
||||
|
||||
讲了这么多理论,让我们看看实际开发中最常见的性能问题,以及如何解决。
|
||||
|
||||
### 4.1 图片加载慢
|
||||
|
||||
**问题表现**:图片半天加载不出来,或者加载过程中页面跳动。
|
||||
|
||||
**原因**:
|
||||
- 图片体积太大(高清原图)
|
||||
- 图片尺寸太大(2000px 宽的图显示为 200px)
|
||||
- 没有懒加载(一次性加载所有图片)
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. **使用现代图片格式**(WebP、AVIF):
|
||||
|
||||
```html
|
||||
<!-- 现代:WebP 格式,体积小 30-70% -->
|
||||
<picture>
|
||||
<source srcset="image.webp" type="image/webp">
|
||||
<img src="image.jpg" alt="图片">
|
||||
</picture>
|
||||
```
|
||||
|
||||
2. **响应式图片**(根据设备大小加载不同尺寸):
|
||||
|
||||
```html
|
||||
<!-- 小设备加载小图,大设备加载大图 -->
|
||||
<img
|
||||
src="image-800.jpg"
|
||||
srcset="image-400.jpg 400w,
|
||||
image-800.jpg 800w,
|
||||
image-1200.jpg 1200w"
|
||||
sizes="(max-width: 600px) 400px,
|
||||
(max-width: 1200px) 800px,
|
||||
1200px"
|
||||
alt="响应式图片">
|
||||
```
|
||||
|
||||
3. **懒加载**(用户滚动到时再加载):
|
||||
|
||||
```html
|
||||
<!-- 现代:原生懒加载 -->
|
||||
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" />
|
||||
```
|
||||
|
||||
👇 **动手试试看**:
|
||||
下面这个演示对比了懒加载和不懒加载的区别。观察网络请求:
|
||||
|
||||
<ImageOptimizationDemo />
|
||||
|
||||
### 4.2 首屏加载慢
|
||||
|
||||
**问题表现**:用户打开网页,白屏时间很长。
|
||||
|
||||
**原因**:
|
||||
- 加载了太多不必要的代码
|
||||
- 关键渲染路径被阻塞
|
||||
- 没有做代码分割
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. **代码分割**(Code Splitting):
|
||||
|
||||
```js
|
||||
// 路由懒加载:访问时才加载
|
||||
const routes = [
|
||||
{
|
||||
path: '/about',
|
||||
component: () => import('./views/About.vue') // 访问 /about 时才加载
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
2. **预加载关键资源**(Preload):
|
||||
|
||||
```html
|
||||
<!-- 提前告知浏览器:这些资源很重要,优先加载 -->
|
||||
<link rel="preload" href="critical.css" as="style">
|
||||
<link rel="preload" href="hero-image.jpg" as="image">
|
||||
```
|
||||
|
||||
3. **内联关键 CSS**:
|
||||
|
||||
```html
|
||||
<!-- 把首屏需要的 CSS 直接内嵌在 HTML 中 -->
|
||||
<style>
|
||||
/* 首屏关键样式 */
|
||||
.hero { background: #000; color: #fff; }
|
||||
</style>
|
||||
```
|
||||
|
||||
### 4.3 滚动卡顿
|
||||
|
||||
**问题表现**:页面滚动时一卡一卡的,不流畅。
|
||||
|
||||
**原因**:
|
||||
- 渲染了太多 DOM 节点(比如 10,000 条数据)
|
||||
- 滚动事件监听器中有复杂计算
|
||||
- 频繁触发布局计算
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. **虚拟列表**(Virtual Scrolling):
|
||||
|
||||
```vue
|
||||
<!-- 只渲染可见区域的内容 -->
|
||||
<RecycleScroller
|
||||
:items="10000"
|
||||
:item-size="50"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div>{{ item.name }}</div>
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
```
|
||||
|
||||
👇 **动手看看**:
|
||||
下面这个演示对比了普通列表和虚拟列表的性能差异:
|
||||
|
||||
<VirtualScrollingDemo />
|
||||
|
||||
2. **节流滚动事件**(Throttle):
|
||||
|
||||
```js
|
||||
// 限制滚动事件的触发频率(最多每 100ms 触发一次)
|
||||
const throttledScroll = throttle(() => {
|
||||
updatePosition()
|
||||
}, 100)
|
||||
|
||||
window.addEventListener('scroll', throttledScroll)
|
||||
```
|
||||
|
||||
3. **使用 CSS `will-change`**:
|
||||
|
||||
```css
|
||||
/* 提前告知浏览器:这个元素会变化,请做好准备 */
|
||||
.scroll-container {
|
||||
will-change: transform;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 点击反应慢
|
||||
|
||||
**问题表现**:点击按钮后,要等好几秒才有反应。
|
||||
|
||||
**原因**:
|
||||
- 点击事件处理器中有复杂计算(阻塞主线程)
|
||||
- 没有使用防抖(用户快速点击多次,触发多次计算)
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. **防抖点击事件**(Debounce):
|
||||
|
||||
```js
|
||||
// 用户停止点击 300ms 后才执行
|
||||
const debouncedClick = debounce(() => {
|
||||
submitForm()
|
||||
}, 300)
|
||||
|
||||
button.addEventListener('click', debouncedClick)
|
||||
```
|
||||
|
||||
2. **使用 Web Worker**(把计算放到后台线程):
|
||||
|
||||
```js
|
||||
// 主线程
|
||||
const worker = new Worker('calculator.js')
|
||||
button.addEventListener('click', () => {
|
||||
worker.postMessage({ data: largeData })
|
||||
})
|
||||
|
||||
worker.onmessage = (e) => {
|
||||
// 计算完成,显示结果
|
||||
showResult(e.data.result)
|
||||
}
|
||||
|
||||
// calculator.js (Worker 线程)
|
||||
self.onmessage = (e) => {
|
||||
const result = heavyCalculation(e.data.data)
|
||||
self.postMessage({ result })
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 性能监控工具
|
||||
|
||||
性能优化不是一次性工作,需要持续监控。下面介绍常用的工具。
|
||||
|
||||
### 5.1 浏览器开发者工具
|
||||
|
||||
**Chrome DevTools** 是最常用的性能分析工具:
|
||||
|
||||
- **Network 面板**:查看资源加载情况
|
||||
- **Performance 面板**:分析运行时性能(FPS、主线程活动)
|
||||
- **Lighthouse**:一键生成性能报告
|
||||
|
||||
::: tip 如何使用 Performance 面板
|
||||
1. 打开 Chrome DevTools(F12)
|
||||
2. 切换到 Performance 面板
|
||||
3. 点击"Record"按钮
|
||||
4. 操作网页(滚动、点击等)
|
||||
5. 点击"Stop"停止录制
|
||||
6. 分析结果:看 FPS(帧率)、主线程活动、长任务等
|
||||
:::
|
||||
|
||||
### 5.2 Lighthouse
|
||||
|
||||
**Lighthouse** 是 Google 开发的自动化性能测试工具:
|
||||
|
||||
```bash
|
||||
# 命令行使用
|
||||
lighthouse https://www.example.com --view
|
||||
|
||||
# 或者在 Chrome DevTools 中使用
|
||||
# 打开 DevTools → Lighthouse → 点击 "Analyze page load"
|
||||
```
|
||||
|
||||
Lighthouse 会给出:
|
||||
- 性能评分(0-100 分)
|
||||
- 核心指标(FCP、LCP、CLS、TBT、INP)
|
||||
- 优化建议(按影响排序)
|
||||
|
||||
### 5.3 WebPageTest
|
||||
|
||||
**WebPageTest** 是在线性能测试工具,可以从多个地点、多种设备测试:
|
||||
|
||||
```bash
|
||||
# 访问 https://www.webpagetest.org
|
||||
# 输入网址,选择测试地点和设备,点击 "Start Test"
|
||||
```
|
||||
|
||||
WebPageTest 会给出:
|
||||
- 瀑布图(Waterfall):每个资源加载的时间线
|
||||
- 视频对比:优化前后的加载过程视频
|
||||
- 优化建议
|
||||
|
||||
---
|
||||
|
||||
## 6. 性能优化清单
|
||||
|
||||
下面是一个实用的性能优化清单,你可以按照这个顺序优化你的网页:
|
||||
|
||||
### 6.1 加载优化
|
||||
|
||||
- ✅ **压缩图片**:使用 WebP 格式,压缩质量 80-85%
|
||||
- ✅ **响应式图片**:根据设备大小加载不同尺寸的图片
|
||||
- ✅ **懒加载**:图片和组件懒加载,只加载可见内容
|
||||
- ✅ **代码分割**:按路由分割代码,按需加载
|
||||
- ✅ **压缩代码**:启用 Gzip/Brotli 压缩
|
||||
- ✅ **使用 CDN**:把静态资源放到 CDN,加速下载
|
||||
- ✅ **预加载关键资源**:使用 `<link rel="preload">`
|
||||
|
||||
### 6.2 渲染优化
|
||||
|
||||
- ✅ **减少重排重绘**:使用 `transform` 和 `opacity` 代替 `top` 和 `width`
|
||||
- ✅ **虚拟列表**:大量数据时使用虚拟滚动
|
||||
- ✅ **CSS 动画**:优先使用 CSS 动画,而不是 JavaScript 动画
|
||||
- ✅ **优化关键渲染路径**:内联关键 CSS,延迟加载非关键 CSS
|
||||
- ✅ **避免 @import**:`@import` 会阻塞渲染,改用 `<link>`
|
||||
|
||||
### 6.3 交互优化
|
||||
|
||||
- ✅ **防抖和节流**:滚动、输入、resize 事件使用防抖/节流
|
||||
- ✅ **Web Worker**:复杂计算放到后台线程
|
||||
- ✅ **时间切片**:大任务拆成小任务,避免长任务
|
||||
- ✅ **避免同步布局**:不要在循环中读取布局属性(如 `offsetHeight`)
|
||||
|
||||
### 6.4 缓存优化
|
||||
|
||||
- ✅ **HTTP 缓存**:配置 Cache-Control 和 ETag
|
||||
- ✅ **Service Worker**:缓存静态资源,实现离线访问
|
||||
- ✅ **LocalStorage**:缓存 API 数据,减少请求
|
||||
- ✅ **内存缓存**:使用 `Map`/`Object` 缓存计算结果
|
||||
|
||||
### 6.5 监控优化
|
||||
|
||||
- ✅ **Lighthouse CI**:每次提交代码自动测试性能
|
||||
- ✅ **真实用户监控**:收集真实用户的性能数据
|
||||
- ✅ **性能预算**:设置文件大小限制,超过报警
|
||||
- ✅ **定期性能报告**:每周/每月生成性能趋势报告
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结
|
||||
|
||||
让我们用一张表格来回顾前端性能优化的核心概念:
|
||||
|
||||
| 概念 | 一句话解释 | 解决的问题 | 常用手段 |
|
||||
|------|-----------|-----------|----------|
|
||||
| **加载优化** | 让资源下载更快 | 首屏慢、等待时间长 | 压缩图片、CDN、代码分割、懒加载 |
|
||||
| **渲染优化** | 让页面"画"得更快 | 滚动卡、点击慢 | 虚拟列表、减少重排重绘、CSS 动画 |
|
||||
| **交互优化** | 让响应更快 | 点击没反应、操作卡顿 | 防抖节流、Web Worker、时间切片 |
|
||||
| **缓存优化** | 避免重复下载 | 重复访问慢 | HTTP 缓存、Service Worker、LocalStorage |
|
||||
| **监控优化** | 持续发现问题 | 性能退化 | Lighthouse、RUM、性能预算 |
|
||||
|
||||
::: info 写在最后
|
||||
性能优化是一个持续演进的话题,工具会变,但核心理念不变:**站在用户的角度思考问题,让等待时间更短、让操作更流畅**。
|
||||
|
||||
理解了这些基本原理,无论技术如何更新换代,你都能快速上手、从容应对。
|
||||
|
||||
希望这篇文章能帮助你建立起对前端性能优化的整体认知。当你在实际项目中遇到性能问题时,能够知道从哪里入手、如何定位、怎样解决。
|
||||
:::
|
||||
Reference in New Issue
Block a user