Merge branch 'dev'
@@ -0,0 +1,230 @@
|
||||
# 算法思维入门
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**如何高效地解决问题?** 你可能遇到过这样的困惑:同一个问题,有人写的代码跑几秒就出结果,有人写的跑几分钟还在转。差别往往在于算法。本章带你理解算法的核心思维方式。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:算法是什么?
|
||||
|
||||
想象你要在一本字典里找一个单词:
|
||||
|
||||
- **方法一**:从第一页开始,一页一页翻(线性查找)
|
||||
- **方法二**:根据首字母定位,再二分查找(二分查找)
|
||||
|
||||
两种方法都能找到,但效率天差地别。**算法就是解决问题的方法**。
|
||||
|
||||
<AlgorithmDemo />
|
||||
|
||||
**算法的核心指标:**
|
||||
|
||||
| 指标 | 含义 | 为什么重要 |
|
||||
|------|------|-----------|
|
||||
| **时间复杂度** | 运行时间随数据量增长的趋势 | 预测大规模数据的性能 |
|
||||
| **空间复杂度** | 内存占用随数据量增长的趋势 | 评估内存消耗 |
|
||||
| **正确性** | 是否总能得到正确结果 | 算法的基本要求 |
|
||||
|
||||
::: tip 📊 逐行解读这张表
|
||||
**时间复杂度**:用大 O 表示法描述。O(n) 表示数据量翻倍,时间翻倍;O(n²) 表示数据量翻倍,时间变成 4 倍。
|
||||
|
||||
**空间复杂度**:同样用大 O 表示法。有些算法用空间换时间(如哈希表),有些用时间换空间(如压缩算法)。
|
||||
|
||||
**正确性**:算法必须对所有可能的输入都能给出正确结果。边界条件(空输入、极大输入)最容易出错。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 二分查找:每次排除一半
|
||||
|
||||
### 1.1 二分查找的原理
|
||||
|
||||
::: tip 💡 二分查找如何工作?
|
||||
**前提**:数据必须有序
|
||||
|
||||
**过程**:
|
||||
1. 找到中间元素
|
||||
2. 如果中间元素等于目标,找到!
|
||||
3. 如果目标小于中间元素,在左半部分继续
|
||||
4. 如果目标大于中间元素,在右半部分继续
|
||||
5. 每次排除一半,直到找到或确定不存在
|
||||
|
||||
**时间复杂度**:O(log n)
|
||||
|
||||
**生活类比**:猜数字游戏。我想一个 1-100 的数,你每次猜中间,我告诉你大了还是小了。最多猜 7 次就能猜中(因为 2⁷ = 128 > 100)。
|
||||
:::
|
||||
|
||||
### 1.2 为什么二分查找这么快?
|
||||
|
||||
| 数据量 | 线性查找 | 二分查找 |
|
||||
|--------|---------|---------|
|
||||
| 100 | 100 次 | 7 次 |
|
||||
| 1,000 | 1,000 次 | 10 次 |
|
||||
| 1,000,000 | 1,000,000 次 | 20 次 |
|
||||
| 1,000,000,000 | 1,000,000,000 次 | 30 次 |
|
||||
|
||||
::: tip 💡 对数增长的威力
|
||||
二分查找的时间复杂度是 O(log n),这意味着:
|
||||
|
||||
- 10 亿数据,最多查找 30 次
|
||||
- 1 万亿数据,最多查找 40 次
|
||||
|
||||
这就是对数增长的威力——数据量增加 1000 倍,查找次数只增加 10 次。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 排序:将无序变有序
|
||||
|
||||
### 2.1 常见排序算法
|
||||
|
||||
| 算法 | 时间复杂度 | 特点 | 适用场景 |
|
||||
|------|-----------|------|---------|
|
||||
| **冒泡排序** | O(n²) | 简单但慢 | 教学、小数据量 |
|
||||
| **选择排序** | O(n²) | 简单但慢 | 小数据量 |
|
||||
| **插入排序** | O(n²) | 对近乎有序的数据快 | 小数据、近乎有序 |
|
||||
| **快速排序** | O(n log n) | 实际最快 | 通用排序 |
|
||||
| **归并排序** | O(n log n) | 稳定排序 | 需要稳定性的场景 |
|
||||
| **堆排序** | O(n log n) | 原地排序 | 内存受限场景 |
|
||||
|
||||
### 2.2 为什么快速排序"快"?
|
||||
|
||||
::: tip 💡 快速排序的原理
|
||||
**核心思想**:分治法
|
||||
|
||||
1. 选一个"基准"元素
|
||||
2. 把比基准小的放左边,比基准大的放右边
|
||||
3. 对左右两部分递归排序
|
||||
4. 合并结果
|
||||
|
||||
**为什么快?**
|
||||
- 每次划分后,基准元素就到了最终位置
|
||||
- 平均情况下,每次划分大约排除一半元素
|
||||
- 时间复杂度 O(n log n)
|
||||
|
||||
**生活类比**:整理书架。先抽出一本书,把比它薄的放左边,比它厚的放右边。然后对左右两堆分别重复这个过程。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 递归:自己调用自己
|
||||
|
||||
### 3.1 递归的本质
|
||||
|
||||
::: tip 💡 什么是递归?
|
||||
**递归**是函数调用自身的编程技巧。
|
||||
|
||||
**两个关键要素**:
|
||||
1. **基本情况**:什么时候停止递归?
|
||||
2. **递归步骤**:如何把问题分解成更小的子问题?
|
||||
|
||||
**经典例子:阶乘**
|
||||
```js
|
||||
function factorial(n) {
|
||||
if (n <= 1) return 1 // 基本情况
|
||||
return n * factorial(n - 1) // 递归步骤
|
||||
}
|
||||
```
|
||||
|
||||
**生活类比**:俄罗斯套娃。打开一个娃娃,里面是更小的娃娃,直到最小的那个打不开为止。
|
||||
:::
|
||||
|
||||
### 3.2 递归 vs 迭代
|
||||
|
||||
| 特性 | 递归 | 迭代(循环) |
|
||||
|------|------|-------------|
|
||||
| **代码简洁度** | 通常更简洁 | 可能更复杂 |
|
||||
| **内存消耗** | 较高(调用栈) | 较低 |
|
||||
| **性能** | 稍慢(函数调用开销) | 更快 |
|
||||
| **适用场景** | 树遍历、分治算法 | 简单重复任务 |
|
||||
|
||||
::: warning ⚠️ 递归的陷阱
|
||||
**栈溢出**:递归层次太深,调用栈空间耗尽。
|
||||
|
||||
**解决方法**:
|
||||
- 改用迭代
|
||||
- 使用尾递归优化(某些语言支持)
|
||||
- 限制递归深度
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 贪心算法:每步选最优
|
||||
|
||||
### 4.1 贪心的思想
|
||||
|
||||
::: tip 💡 什么是贪心算法?
|
||||
**贪心算法**在每一步都选择当前看起来最优的选择,希望最终得到全局最优解。
|
||||
|
||||
**适用条件**:
|
||||
1. **贪心选择性质**:局部最优能导致全局最优
|
||||
2. **最优子结构**:问题的最优解包含子问题的最优解
|
||||
|
||||
**经典例子:硬币找零**
|
||||
- 目标:用最少的硬币凑出指定金额
|
||||
- 贪心策略:每次选最大的硬币
|
||||
- 结果:67 元 = 50 + 10 + 5 + 1 + 1(5 枚)
|
||||
|
||||
**生活类比**:登山时,每次都选最陡的路往上走。虽然不一定能到最高峰,但通常能到不错的位置。
|
||||
:::
|
||||
|
||||
### 4.2 贪心的局限性
|
||||
|
||||
::: warning ⚠️ 贪心不一定得到最优解
|
||||
**反例:硬币找零**
|
||||
|
||||
如果硬币面值是 [1, 3, 4],要凑 6 元:
|
||||
- 贪心:4 + 1 + 1 = 3 枚
|
||||
- 最优:3 + 3 = 2 枚
|
||||
|
||||
贪心算法在这里失败了!
|
||||
|
||||
**教训**:贪心算法简单高效,但不总是能得到最优解。使用前要证明问题满足贪心条件。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 算法设计范式
|
||||
|
||||
| 范式 | 思想 | 典型算法 | 适用问题 |
|
||||
|------|------|---------|---------|
|
||||
| **分治** | 把问题分解成小问题 | 快速排序、归并排序 | 可分解的问题 |
|
||||
| **贪心** | 每步选最优 | 最小生成树、霍夫曼编码 | 有贪心性质的问题 |
|
||||
| **动态规划** | 记录子问题的解 | 背包问题、最短路径 | 有重叠子问题 |
|
||||
| **回溯** | 试错,走不通就回退 | 八皇后、全排列 | 搜索问题 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结:算法是解决问题的艺术
|
||||
|
||||
让我们用一个比喻总结各种算法思想:
|
||||
|
||||
| 思想 | 比喻 | 核心要点 |
|
||||
|------|------|---------|
|
||||
| **二分查找** | 猜数字 | 每次排除一半 |
|
||||
| **排序** | 整理书架 | 建立秩序 |
|
||||
| **递归** | 俄罗斯套娃 | 化大为小 |
|
||||
| **贪心** | 登山选路 | 局部最优 |
|
||||
|
||||
::: tip 💡 核心启示
|
||||
**算法的本质是"效率"和"正确性"的平衡。**
|
||||
|
||||
- 好的算法能让程序效率提升几个数量级
|
||||
- 但过度优化可能引入复杂性
|
||||
- 先保证正确,再追求效率
|
||||
|
||||
理解算法思维,比记住具体算法更重要:
|
||||
- 分治:把大问题分解成小问题
|
||||
- 贪心:每步选最优
|
||||
- 动态规划:记录子问题的解
|
||||
- 回溯:试错,走不通就回退
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- **算法导论**:系统学习算法的经典教材
|
||||
- **LeetCode**:通过刷题提升算法能力
|
||||
- **算法可视化**:直观理解算法执行过程
|
||||
- **竞赛算法**:学习更高级的算法技巧
|
||||
@@ -0,0 +1,625 @@
|
||||
# 网络:两台电脑如何对话
|
||||
::: tip 🎯 核心问题
|
||||
**当你在浏览器输入 www.baidu.com 并按下回车,到底发生了什么?** 这个简单动作背后,其实隐藏着一个庞大的"快递系统":从填写订单(URL)到查询地址簿(DNS),从建立运输通道(TCP)到快递员送货(HTTP),最终在你屏幕上展示(渲染)。本章带你完整理解这个神奇的过程。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 0. 五层模型总览:快递公司的组织架构
|
||||
|
||||
现代计算机网络就像一个**快递公司**,采用五层分层模型,每层负责不同的工作:
|
||||
|
||||
<CFNetworkLayers />
|
||||
|
||||
::: tip 💡 为什么需要分层?
|
||||
想象一个没有分工的快递公司:
|
||||
|
||||
- **每个人什么都干**:接电话、分拣、打包、开车送货...
|
||||
- **效率极低**:没人专精,什么都做不好
|
||||
- **难以扩展**:想加个"航空运输",所有员工都要重新培训
|
||||
|
||||
**分层设计**解决了这些问题:
|
||||
|
||||
- **模块化**:每层独立设计和实现,改一层不影响其他层
|
||||
- **易维护**:网络慢了?查物理层和数据链路层;安全问题?查应用层
|
||||
- **标准化**:统一的接口和协议,不同厂商的设备能互相通信
|
||||
- **可扩展**:新技术可以替换某一层,比如从铜线换成光纤,只需改物理层
|
||||
:::
|
||||
|
||||
| 层级 | 技术名称 | 快递公司类比 | 核心职责 | 常见协议/设备 |
|
||||
| ----- | ---------- | ---------------- | ---------------------------------------- | ------------------ |
|
||||
| **5** | 应用层 | **客户服务部门** | 处理具体业务(网页、邮件、文件传输) | HTTP, FTP, SMTP |
|
||||
| **4** | 传输层 | **包裹分拣组** | 确保包裹可靠送达(_TCP_)或快速送达(_UDP_) | TCP, UDP |
|
||||
| **3** | 网络层 | **路由规划部** | 规划最佳运输路线,选择走哪条路 | IP, 路由器 |
|
||||
| **2** | 数据链路层 | **车队管理** | 管理车辆之间的通信,MAC 地址寻址 | 以太网, 交换机 |
|
||||
| **1** | 物理层 | **道路和车辆** | 实际的物理传输(电缆、光纤、无线电波) | 网线, 光纤, 无线电 |
|
||||
|
||||
::: tip 📊 逐行解读这张表
|
||||
**第5层(应用层)**:这是你直接接触的层。浏览器打开网页、邮件客户端收发邮件,都是在调用这一层的服务。它负责处理"具体的业务逻辑"。
|
||||
|
||||
**第4层(传输层)**:应用层把数据给它,它负责决定用什么方式"寄送"。TCP 像挂号信(可靠但慢),UDP 像平信(快但可能丢)。这一层用**端口号**区分不同的应用程序。
|
||||
|
||||
**第3层(网络层)**:这是"全球定位系统"层。IP 地址就在这一层,路由器根据 IP 地址规划路线:"从北京到上海,应该走哪条高速公路?"
|
||||
|
||||
**第2层(数据链路层)**:这一层负责"两站之间"的运输。就像快递车从北京分拣中心开到天津分拣中心,这一段路的通信规则由数据链路层规定。MAC 地址(设备身份证)也在这一层。
|
||||
|
||||
**第1层(物理层)**:这是最底层,实实在在的物理介质。网线里的电信号、光纤里的光信号、Wi-Fi 的无线电波,都是物理层负责的。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 物理层:道路和车辆
|
||||
|
||||
### 1.1 基本概念
|
||||
|
||||
::: tip 💡 物理层是什么?
|
||||
物理层负责在物理介质上传输原始的比特流(0 和 1)。
|
||||
|
||||
**生活类比**:想象快递公司需要有**道路**和**运输车辆**:
|
||||
|
||||
- 道路可以是:高速公路(光纤)、普通公路(网线)、航空线路(无线电波)
|
||||
- 车辆可以是:卡车(有线传输)、飞机(无线传输)
|
||||
- 货物(数据)最终都要变成能在这些道路上运输的形式
|
||||
:::
|
||||
|
||||
**关键任务**:
|
||||
|
||||
- **定义物理设备标准**:RJ45 网线接口长什么样、光纤接口怎么接
|
||||
- **规定传输介质**:
|
||||
- 有线:双绞线(网线)、光纤、同轴电缆
|
||||
- 无线:Wi-Fi、蓝牙、4G/5G
|
||||
- **确定电气特性**:
|
||||
- 用多少电压代表 0 和 1?
|
||||
- 信号频率是多少?
|
||||
- 怎么编码(比如曼彻斯特编码)?
|
||||
|
||||
### 1.2 传输介质
|
||||
|
||||
**有线介质**:
|
||||
|
||||
| 类型 | 速度 | 距离 | 特点 | 用途 |
|
||||
| ------------ | ---------------- | -------- | ------------------------ | ------------------------ |
|
||||
| **双绞线** | 100Mbps - 10Gbps | 100m | 成本低,易安装,抗干扰一般 | 家庭、办公室网络 |
|
||||
| **光纤** | 1Gbps - 100Tbps | 几十公里 | 速度极快,抗干扰强,成本高 | 长距离、高带宽(跨海光缆) |
|
||||
| **同轴电缆** | 10Mbps - 1Gbps | 500m | 抗干扰好,但较粗 | 早期以太网、有线电视 |
|
||||
|
||||
::: tip 💡 为什么光纤这么快?
|
||||
光纤用**光**而不是电信号传输:
|
||||
|
||||
- 光的频率极高,能调制大量数据(就像用不同颜色的光同时传输)
|
||||
- 光在光纤中几乎不衰减,能传输几十公里
|
||||
- 不受电磁干扰(高压电线、雷电都不怕)
|
||||
|
||||
这就像用电信号寄快递(铜线)vs 用光速寄快递(光纤),速度差异是本质级别的。
|
||||
:::
|
||||
|
||||
**无线介质**:
|
||||
|
||||
| 类型 | 频段 | 速度 | 距离 | 用途 |
|
||||
| --------- | -------------------- | ------------------- | ------ | -------------------- |
|
||||
| **Wi-Fi** | 2.4GHz / 5GHz / 6GHz | 几十 Mbps - 几 Gbps | 几十米 | 家庭、办公室无线网络 |
|
||||
| **蓝牙** | 2.4GHz | 1-3 Mbps | 10m | 耳机、键鼠等短距设备 |
|
||||
| **4G/5G** | 700MHz - 39GHz | 10Mbps - 10Gbps | 几公里 | 移动网络 |
|
||||
|
||||
### 1.3 常见设备
|
||||
|
||||
**中继器(Repeater)**:
|
||||
|
||||
- **作用**:放大信号,延长传输距离
|
||||
- **生活类比**:快递中转站。快递车开了 500 公里需要加油、司机换班,中继器就是让信号"休息充电"的地方
|
||||
- **为什么需要**:电信号在铜线传输会衰减,传几百米就弱得识别不出了
|
||||
|
||||
**集线器(Hub)**:
|
||||
|
||||
- **作用**:多端口中继器,从一个口收到的信号复制到所有口
|
||||
- **缺点**:效率低,已被**交换机**取代
|
||||
- **生活类比**:一个大厅,一个人喊话,所有人都能听到,但不是喊给谁的
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据链路层:车队管理
|
||||
|
||||
### 2.1 基本概念
|
||||
|
||||
::: tip 💡 数据链路层做什么?
|
||||
数据链路层负责在**直连的两个节点**间传输数据帧。
|
||||
|
||||
**生活类比**:快递公司的**车队管理**:
|
||||
|
||||
- 快递车从北京分拣中心开到天津分拣中心(点对点)
|
||||
- 车上有司机(负责驾驶)、装卸工(负责搬运)
|
||||
- 两边分拣中心之间有约定:"每天 8 点发车""用标准尺寸的快递箱"
|
||||
:::
|
||||
|
||||
**核心功能**:
|
||||
|
||||
- **物理地址寻址(MAC 地址)**:每个网卡都有全球唯一的身份证号
|
||||
- **帧的封装和解封装**:把网络层的数据包"装进车厢"
|
||||
- **错误检测**:通过 CRC 校验,发现数据是否损坏
|
||||
- **介质访问控制**:多个设备共享一条线时,谁先谁后?(比如 Wi-Fi 多台设备连一个路由器)
|
||||
|
||||
### 2.2 MAC 地址:设备的身份证
|
||||
|
||||
**MAC 地址格式**:`00:1A:2B:3C:4D:5E`
|
||||
|
||||
::: tip 💡 MAC 地址 vs IP 地址
|
||||
这是初学者最容易混淆的两个概念:
|
||||
|
||||
| 特性 | MAC 地址 | IP 地址 |
|
||||
| ------------ | ----------------------- | --------------------------- |
|
||||
| **作用范围** | 局域网内(同一个 Wi-Fi) | 全球互联网 |
|
||||
| **分配方式** | 网卡出厂时烧录,全球唯一 | 由网络管理员动态分配 |
|
||||
| **变化** | 一般不变(除非换网卡) | 经常变化(连不同 Wi-Fi 会变) |
|
||||
| **类比** | 身份证号(跟随你一生) | 家庭住址(搬家就变) |
|
||||
| **层级** | 数据链路层(第2层) | 网络层(第3层) |
|
||||
|
||||
**生活类比**:你要寄快递:
|
||||
|
||||
- **MAC 地址** = 收件人身份证号(唯一标识这个人)
|
||||
- **IP 地址** = 收件人家庭住址(用于路由)
|
||||
|
||||
快递员实际上需要"住址"才能送货,但身份证号能确保"这个人"是唯一的。
|
||||
:::
|
||||
|
||||
**查看你的 MAC 地址**:
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
ipconfig /all
|
||||
# 找到 "物理地址",类似: 00-1A-2B-3C-4D-5E
|
||||
|
||||
# macOS/Linux
|
||||
ifconfig
|
||||
# 找到 "ether",类似: 00:1a:2b:3c:4d:5e
|
||||
```
|
||||
|
||||
### 2.3 以太网帧:快递车厢的结构
|
||||
|
||||
**以太网帧**就是数据链路层的"快递车厢",有一套标准格式:
|
||||
|
||||
```
|
||||
+------------+----------+---------+-----+----------+
|
||||
| 目标 MAC | 源 MAC | 类型 | 数据 | FCS |
|
||||
| (6 bytes) | (6 bytes) | (2 bytes)| | (4 bytes) |
|
||||
+------------+----------+---------+-----+----------+
|
||||
```
|
||||
|
||||
::: tip 💡 逐行理解帧结构
|
||||
**目标 MAC (6字节)**:这帧数据给谁的?就像快递单上的收件人
|
||||
|
||||
**源 MAC (6字节)**:这帧数据谁发的?就像寄件人信息
|
||||
|
||||
**类型 (2字节)**:车厢里装的是什么?
|
||||
|
||||
- `0x0800` = IPv4 数据包
|
||||
- `0x0806` = ARP 请求(查询 MAC 地址)
|
||||
- `0x86DD` = IPv6 数据包
|
||||
|
||||
**数据(46-1500字节)**:实际要传输的内容,就是网络层的 IP 数据包
|
||||
|
||||
**FCS (4字节)**:帧校验序列。接收方用这个检查数据是否损坏,就像快递单上的"完好无损"签章
|
||||
:::
|
||||
|
||||
### 2.4 交换机:聪明的交通指挥
|
||||
|
||||
**交换机**是数据链路层的核心设备。
|
||||
|
||||
::: tip 💡 交换机 vs 集线器
|
||||
**集线器**:
|
||||
|
||||
- 收到数据后,简单地"广播"到所有端口
|
||||
- 所有设备都能看到,不是给自己的也得收下来再丢弃
|
||||
- 效率低,安全性差
|
||||
|
||||
**交换机**:
|
||||
|
||||
- **学习 MAC 地址**:记住哪个端口连了哪个 MAC 地址
|
||||
- **智能转发**:只把数据发到目标设备所在的端口
|
||||
- **效率高**:设备 A 和 B 通信,设备 C 不会收到
|
||||
:::
|
||||
|
||||
**交换机工作流程**:
|
||||
|
||||
1. **学习**:设备 A (MAC: 11:11...) 发数据给交换机端口1
|
||||
- 交换机记下:"11:11... 在端口1"
|
||||
|
||||
2. **转发**:设备 A 要发数据给设备 B (MAC: 22:22...)
|
||||
- 交换机查表:"22:22... 在端口3"
|
||||
- 只把数据从端口3 发出去
|
||||
|
||||
3. **广播**:如果交换机不知道目标 MAC 在哪(比如第一次通信)
|
||||
- 向所有端口(除了来源端口)广播
|
||||
- "谁是 22:22...?" 目标设备回应后,交换机学习到它的位置
|
||||
|
||||
---
|
||||
|
||||
## 3. 网络层:路由规划部
|
||||
|
||||
### 3.1 IP 地址:互联网的门牌号
|
||||
|
||||
::: tip 💡 IP 地址是什么?
|
||||
**IP 地址**就像互联网上的**家庭住址**,每台联网设备都需要一个。
|
||||
|
||||
**IPv4 地址格式**:`192.168.1.1`
|
||||
|
||||
- 32 位,通常用点分十进制表示
|
||||
- 分为**网络部分**(前3段)和**主机部分**(最后1段)
|
||||
- `192.168.1` 是网络号(这个小区)
|
||||
- `.1` 是主机号(这个小区的1号房)
|
||||
:::
|
||||
|
||||
**IP 地址分类(像城市规模)**:
|
||||
|
||||
| 类别 | 范围示例 | 网络数 | 每个网络主机数 | 用途 | 类比 |
|
||||
| -------- | --------------------- | --------- | -------------- | ---------------- | ---------- |
|
||||
| **A 类** | 1.0.0.0 - 126.x.x.x | 126 | 16,777,214 | 超大型网络(早期) | 特大城市 |
|
||||
| **B 类** | 128.0.0.0 - 191.x.x.x | 16,384 | 65,534 | 中型网络 | 中等城市 |
|
||||
| **C 类** | 192.0.0.0 - 223.x.x.x | 2,097,152 | 254 | 小型网络(最常见) | 小区、村庄 |
|
||||
|
||||
::: tip 💡 私有 IP 地址:内网 vs 外网
|
||||
有些 IP 地址段被保留为"私有",不能直接在互联网上使用:
|
||||
|
||||
| 类别 | 私有 IP 范围 | 为什么用私有 IP? |
|
||||
| -------- | ------------------------------- | -------------------- |
|
||||
| **A 类** | `10.0.0.0 - 10.255.255.255` | 大型企业内网 |
|
||||
| **B 类** | `172.16.0.0 - 172.31.255.255` | 中型企业内网 |
|
||||
| **C 类** | `192.168.0.0 - 192.168.255.255` | 家庭、小公司(最常见) |
|
||||
|
||||
**生活类比**:
|
||||
|
||||
- **私有 IP** = 你家的门牌号("3单元501室")
|
||||
- **公网 IP** = 你家在地图上的地址("XX市XX区XX路XX号")
|
||||
|
||||
快递员(互联网)只能送到公网地址(你家楼门口),然后需要"路由器/NAT"转换到你家的私有地址。
|
||||
:::
|
||||
|
||||
### 3.2 子网划分:把大楼分成多个单元
|
||||
|
||||
::: tip 💡 为什么要划分子网?
|
||||
想象一个公司:
|
||||
|
||||
- **不划分子网**:财务部、技术部、市场部都在 `192.168.1.0` 网段
|
||||
- 广播风暴:一个人发广播,所有人都能收到
|
||||
- 安全问题:技术部的开发服务器,市场部也能访问
|
||||
- 管理混乱:网络出问题,不知道是哪个部门的
|
||||
|
||||
**划分子网**:
|
||||
|
||||
- 财务部:`192.168.1.0/24`
|
||||
- 技术部:`192.168.2.0/24`
|
||||
- 市场部:`192.168.3.0/24`
|
||||
|
||||
各部门隔离,广播不出部门,管理更清晰。
|
||||
:::
|
||||
|
||||
**子网掩码的作用**:
|
||||
|
||||
子网掩码用来区分 IP 地址的哪部分是"网络号",哪部分是"主机号"。
|
||||
|
||||
```
|
||||
IP: 192.168.1.10
|
||||
掩码: 255.255.255.0
|
||||
-----------------------
|
||||
网络号: 192.168.1.0 (前3段)
|
||||
主机号: .10 (最后1段)
|
||||
```
|
||||
|
||||
**CIDR 表示法**:`192.168.1.0/24`
|
||||
|
||||
- `/24` 表示前 24 位是网络位
|
||||
- 剩余 8 位是主机位(2^8 - 2 = 254 个可用 IP)
|
||||
|
||||
<CFSubnetCalculator />
|
||||
|
||||
### 3.3 路由器:GPS 导航
|
||||
|
||||
**路由器**是网络层的核心设备,负责"规划最佳路线"。
|
||||
|
||||
::: tip 💡 路由器怎么工作?
|
||||
**生活类比**:GPS 导航软件
|
||||
|
||||
- 你输入:"从北京天安门到上海外滩"
|
||||
- GPS 查询地图数据库,规划出最佳路线
|
||||
- 路线可能是:"北京 → 天津 → 济南 → 南京 → 上海"
|
||||
|
||||
**路由器的工作**:
|
||||
|
||||
1. 收到数据包,查看目标 IP 地址
|
||||
2. 查询**路由表**(路由器的"地图数据库")
|
||||
3. 选择最佳路径:"下一站该去哪个路由器?"
|
||||
4. 转发到下一跳
|
||||
:::
|
||||
|
||||
**路由表示例**:
|
||||
|
||||
```
|
||||
目标网络 子网掩码 网关 接口
|
||||
192.168.1.0 255.255.255.0 0.0.0.0 eth0
|
||||
192.168.2.0 255.255.255.0 192.168.1.2 eth0
|
||||
0.0.0.0 0.0.0.0 192.168.1.1 eth0 (默认网关)
|
||||
```
|
||||
|
||||
::: tip 💡 理解路由表
|
||||
**第1行**:"发往 192.168.1.0 网段的包,直接从 eth0 接口发出去"(本地网络,不需要网关)
|
||||
|
||||
**第2行**:"发往 192.168.2.0 网段的包,发给 192.168.1.2(它是这个网络的'门')"
|
||||
|
||||
**第3行(默认网关)**:"不知道怎么走的包,全部发给 192.168.1.1(它连接互联网,会继续帮你转发)"
|
||||
|
||||
这就像你去外地:
|
||||
|
||||
- 在本地:走路就到(直接路由)
|
||||
- 去隔壁城市:坐大巴(走网关)
|
||||
- 去国外:先到机场,再转机(默认网关 → 层层转发)
|
||||
:::
|
||||
|
||||
### 3.4 ICMP:网络诊断工具
|
||||
|
||||
**ICMP (Internet Control Message Protocol)** 用于网络诊断,最常用的就是 `ping` 命令。
|
||||
|
||||
**Ping 命令**:
|
||||
|
||||
```bash
|
||||
ping google.com
|
||||
|
||||
# 输出示例
|
||||
PING google.com (142.250.185.238): 56 data bytes
|
||||
64 bytes from 142.250.185.238: icmp_seq=0 ttl=117 time=12.4 ms
|
||||
64 bytes from 142.250.185.238: icmp_seq=1 ttl=117 time=11.8 ms
|
||||
```
|
||||
|
||||
::: tip 💡 理解 ping 的输出
|
||||
**`64 bytes`**:数据包大小(64 字节)
|
||||
|
||||
**`icmp_seq=0`**:这是第 0 个包(序列号)
|
||||
|
||||
**`ttl=117`**:Time To Live(生存时间)
|
||||
|
||||
- 每经过一个路由器减 1
|
||||
- 防止数据包在网络中无限循环
|
||||
- 117 表示这个包经过了 255-117=138 个路由器
|
||||
|
||||
**`time=12.4 ms`**:往返时间(RTT, Round Trip Time)
|
||||
|
||||
- 你的电脑发送请求 → google.com 收到 → google.com 回应 → 你的电脑收到
|
||||
- 整个过程花了 12.4 毫秒
|
||||
- 数值越小,网络延迟越低,网速越快
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 传输层:可靠送达 vs 快速送达
|
||||
|
||||
### 4.1 端口:应用的门牌号
|
||||
|
||||
::: tip 💡 为什么需要端口号?
|
||||
想象一台服务器:
|
||||
|
||||
- **只有 IP 地址**:数据包到了服务器,服务器不知道给哪个程序
|
||||
- Web 服务器要?
|
||||
- 邮件服务器要?
|
||||
- 数据库服务器要?
|
||||
|
||||
**端口号**就像"公司里的部门号":
|
||||
|
||||
- IP: 公司地址(XX 市XX 路 XX 号)
|
||||
- 端口: 部门(301 财务部、302 技术部、303 市场部)
|
||||
|
||||
数据包到了公司,前台(操作系统)根据"部门号"(端口)转发给对应部门(应用程序)。
|
||||
:::
|
||||
|
||||
**端口号范围**:
|
||||
|
||||
| 范围 | 类型 | 示例 | 需要权限? |
|
||||
| --------------- | -------- | ----------------------------- | --------------------------------- |
|
||||
| **0-1023** | 系统端口 | 80(HTTP)、443(HTTPS)、22(SSH) | ✅ 需要(防止普通用户占用关键服务) |
|
||||
| **1024-49151** | 注册端口 | 3306(MySQL)、5432(PostgreSQL) | ❌ 不需要 |
|
||||
| **49152-65535** | 动态端口 | 客户端临时使用 | ❌ 不需要 |
|
||||
|
||||
**常见端口速查**:
|
||||
|
||||
| 端口 | 服务 | 用途 |
|
||||
| --------- | ---------- | --------------- |
|
||||
| **21** | FTP | 文件传输 |
|
||||
| **22** | SSH | 远程登录(安全) |
|
||||
| **80** | HTTP | 网页(不安全) |
|
||||
| **443** | HTTPS | 网页(安全,加密) |
|
||||
| **3306** | MySQL | 数据库 |
|
||||
| **5432** | PostgreSQL | 数据库 |
|
||||
| **6379** | Redis | 缓存数据库 |
|
||||
| **27017** | MongoDB | 数据库 |
|
||||
|
||||
### 4.2 TCP vs UDP:挂号信 vs 平信
|
||||
|
||||
<CFTcpUdpComparison />
|
||||
|
||||
**选择建议**:
|
||||
|
||||
| 场景 | 选择 | 原因 |
|
||||
| ------------------ | ------- | ----------------------------------------- |
|
||||
| **邮件、文件传输** | **TCP** | 不能丢数据,一个字节错误都可能导致文件损坏 |
|
||||
| **视频、直播** | **UDP** | 实时性优先,丢几帧没关系,但不能卡顿 |
|
||||
| **网页浏览** | **TCP** | 可靠性重要,网页内容必须完整 |
|
||||
| **在线游戏** | **UDP** | 速度优先,位置信息晚到比没到好 |
|
||||
|
||||
::: tip 💡 深入理解:TCP 为什么可靠?
|
||||
TCP 通过以下机制保证可靠:
|
||||
|
||||
1. **三次握手**:确保双方都能发送和接收
|
||||
2. **序列号**:每个字节都有编号,丢包能发现
|
||||
3. **确认应答**:收到数据必须回复 ACK,没收到就重传
|
||||
4. **流量控制**:接收方告诉发送方"我的缓冲区快满了,慢点发"
|
||||
5. **拥塞控制**:网络拥堵时,降低发送速度,避免"堵死"
|
||||
|
||||
这就像寄挂号信:
|
||||
|
||||
- 要签收(ACK)
|
||||
- 丢了邮政局会重传
|
||||
- 太多信件会积压,需要控制发送速度
|
||||
:::
|
||||
|
||||
### 4.3 TCP 三次握手:建立可靠连接
|
||||
|
||||
```
|
||||
客户端 服务器
|
||||
| |
|
||||
| -------- SYN(seq=x) ---------> | 第1次:你好,我想和你通信(SYN)
|
||||
| | (x 是随机数,防止伪造)
|
||||
| |
|
||||
| <--- SYN-ACK(seq=y, ack=x+1) ---| 第2次:收到!我也想和你通信(SYN)
|
||||
| | 我收到了你的 x,所以 ack=x+1
|
||||
| |
|
||||
| -------- ACK(ack=y+1) --------> | 第3次:我收到了你的 y,所以 ack=y+1
|
||||
| | 连接建立成功!
|
||||
```
|
||||
|
||||
::: tip 💡 为什么需要三次,不是两次?
|
||||
想象打电话:
|
||||
|
||||
- **A**:你好!(SYN)
|
||||
- **B**:你好!(SYN-ACK) —- 此时 B 确认了 A 能收到,但 A 还不确定 B 能不能收到
|
||||
- **A**:我听到了!(ACK) —- 现在双方都知道对方能收能发
|
||||
|
||||
如果只有两次:
|
||||
|
||||
- A 发 SYN
|
||||
- B 回 SYN-ACK
|
||||
- 连接建立...但 B 不知道 A 有没有收到 SYN-ACK!如果 A 没收到,会重复发 SYN,但 B 以为已经建立连接,会出现问题
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 应用层:具体的业务
|
||||
|
||||
### 5.1 HTTP/HTTPS:网页的对话协议
|
||||
|
||||
**HTTP (HyperText Transfer Protocol)** 是浏览器和服务器之间的"对话规则"。
|
||||
|
||||
| 特性 | HTTP | HTTPS |
|
||||
| ---------- | ------------------------ | ----------------------------- |
|
||||
| **加密** | ❌ 否(明文,任何人都能看) | ✅ 是(TLS/SSL 加密) |
|
||||
| **端口** | 80 | 443 |
|
||||
| **安全性** | 低(密码、账号会被窃取) | 高(即使被拦截,看到的也是乱码) |
|
||||
| **性能** | 略快(无加密开销) | 略慢(加密解密需要时间) |
|
||||
| **SEO** | 不友好(搜索引擎会降权) | 友好(搜索引擎优先收录 HTTPS) |
|
||||
|
||||
**HTTP 请求方法**:
|
||||
|
||||
| 方法 | 描述 | 生活类比 | 示例 |
|
||||
| ---------- | ---------------------- | ---------------------------- | ---------------------- |
|
||||
| **GET** | 获取资源 | "我要看这个商品的详情" | 查看网页、加载图片 |
|
||||
| **POST** | 提交数据 | "我要下单,这是我的收货信息" | 登录、注册、提交表单 |
|
||||
| **PUT** | 更新资源(整体替换) | "我要完整更新这个商品的信息" | 修改用户资料(全部字段) |
|
||||
| **PATCH** | 部分更新 | "我只想改商品的名称" | 修改用户资料(只改名字) |
|
||||
| **DELETE** | 删除资源 | "我要删除这个订单" | 删除文章、删除评论 |
|
||||
| **HEAD** | 只获取响应头(不要内容) | "这个文件还在吗?有多大?" | 检查资源是否存在 |
|
||||
|
||||
**HTTP 状态码**(服务器给你的"回复"):
|
||||
|
||||
```
|
||||
2xx 成功
|
||||
- 200 OK:请求成功,这是你要的内容
|
||||
- 201 Created:创建成功(比如注册新用户)
|
||||
|
||||
3xx 重定向
|
||||
- 301 Moved Permanently:永久搬家了,请用新地址
|
||||
- 302 Found:暂时搬迁,请访问新地址
|
||||
|
||||
4xx 客户端错误(你发的问题)
|
||||
- 400 Bad Request:请求格式错误,服务器看不懂
|
||||
- 401 Unauthorized:未授权,请先登录
|
||||
- 403 Forbidden:禁止访问,即使登录也不行
|
||||
- 404 Not Found:资源不存在(网址错了?)
|
||||
|
||||
5xx 服务器错误(服务器的问题)
|
||||
- 500 Internal Server Error:服务器内部出错了
|
||||
- 502 Bad Gateway:网关错误,服务器连不上后端
|
||||
- 503 Service Unavailable:服务暂时不可用(过载或维护)
|
||||
```
|
||||
|
||||
### 5.2 DNS:互联网的地址簿
|
||||
|
||||
**DNS (Domain Name System)** 域名系统,把人类可读的域名转换成机器可读的 IP 地址。
|
||||
|
||||
::: tip 💡 为什么需要 DNS?
|
||||
**没有 DNS 的世界**:
|
||||
|
||||
- 你需要记住所有网站的 IP 地址
|
||||
- 访问百度:`https://110.242.68.66`(你能记住吗?)
|
||||
- IP 地址会变(服务器迁移),你需要重新记住
|
||||
|
||||
**有 DNS 的世界**:
|
||||
|
||||
- 记住域名:`baidu.com`
|
||||
- DNS 帮你转换:`baidu.com` → `110.242.68.66`
|
||||
- IP 变了?更新 DNS 记录就行,域名不用变
|
||||
:::
|
||||
|
||||
**DNS 查询过程**:
|
||||
|
||||
```
|
||||
你(浏览器)
|
||||
↓ 问:baidu.com 的 IP 是多少?
|
||||
本地 DNS 服务器(你的网络运营商,如电信/联通)
|
||||
↓ 不知道? 问:
|
||||
根域名服务器(全球13组,管理所有顶级域)
|
||||
↓ 告诉:去问 .cn 的管理者
|
||||
顶级域名服务器(Verisign 管理 .cn)
|
||||
↓ 告诉:去问 baidu.com 的管理者
|
||||
权威 DNS 服务器(Baidu 自己的 DNS)
|
||||
↓ 告诉:baidu.com 的 IP 是 110.242.68.66
|
||||
返回 IP 地址给浏览器
|
||||
```
|
||||
|
||||
**DNS 记录类型**:
|
||||
|
||||
| 类型 | 用途 | 示例 |
|
||||
| --------- | ---------------- | ------------------------------------------------------ |
|
||||
| **A** | 域名 → IPv4 地址 | `www.example.com → 93.184.216.34` |
|
||||
| **AAAA** | 域名 → IPv6 地址 | `www.example.com → 2606:2800:220:1:248:1893:25c8:1946` |
|
||||
| **CNAME** | 别名 | `www.baidu.com → a.baidu.com`(多个域名指向同一个 IP) |
|
||||
| **MX** | 邮件服务器 | `@example.com → mail.example.com`(邮件发到哪里) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结:网络五层模型核心要点
|
||||
|
||||
| 层级 | 核心概念 | 关键技术 | 生活类比 |
|
||||
| -------------- | ------------------- | -------------------- | ---------------------------------- |
|
||||
| **应用层** | 应用程序之间的通信 | HTTP, FTP, SMTP, DNS | 具体业务(寄快递、发邮件、浏览网页) |
|
||||
| **传输层** | 端到端的可靠传输 | TCP(可靠), UDP(快速) | 快递方式(挂号信 vs 平信) |
|
||||
| **网络层** | 路由选择,寻址 | IP, 路由器, ICMP | GPS 导航,规划路线 |
|
||||
| **数据链路层** | 点对点传输,MAC 寻址 | 以太网, 交换机, MAC | 车队管理,车辆之间通信 |
|
||||
| **物理层** | 实际的物理传输 | 光纤, 网线, 无线电波 | 道路和运输工具 |
|
||||
|
||||
**学习建议**:
|
||||
|
||||
- ✅ **从应用层往下学**:你每天都在用 HTTP,DNS,从熟悉的开始
|
||||
- ✅ **多用工具**:ping, traceroute, Wireshark,观察实际网络
|
||||
- ✅ **理解协议细节**:阅读 RFC 文档(比如 RFC 791 定义 IP)
|
||||
- ✅ **抓包分析**:用 Wireshark 观察 TCP 三次握手、HTTP 请求
|
||||
- ✅ **关注安全**:了解 DDoS、中间人攻击等常见威胁
|
||||
|
||||
掌握计算机网络,你就能理解互联网的运作原理,写出更高效的网络应用!
|
||||
|
||||
---
|
||||
|
||||
## 附录:名词速查表
|
||||
|
||||
| 名词 | 英文 | 用人话解释 |
|
||||
| --------------- | ----------------------------- | ------------------------------------------- |
|
||||
| **OSI 模型** | Open Systems Interconnection | 七层网络模型(理论标准) |
|
||||
| **TCP/IP 模型** | - | 实际使用的四层/五层模型 |
|
||||
| **MAC 地址** | Media Access Control | 网卡的物理地址,全球唯一,像身份证 |
|
||||
| **IP 地址** | Internet Protocol | 设备在互联网上的逻辑地址,像住址 |
|
||||
| **子网掩码** | Subnet Mask | 区分 IP 地址的网络部分和主机部分 |
|
||||
| **端口** | Port | 应用程序的"门牌号",区分同一台设备的不同服务 |
|
||||
| **TCP** | Transmission Control Protocol | 可靠传输协议,三次握手,不丢包 |
|
||||
| **UDP** | User Datagram Protocol | 快速传输协议,不保证可靠,可能丢包 |
|
||||
| **DNS** | Domain Name System | 域名系统,把域名转成 IP 地址 |
|
||||
| **HTTP** | HyperText Transfer Protocol | 超文本传输协议,网页通信规则 |
|
||||
| **HTTPS** | HTTP Secure | 加密的 HTTP,更安全 |
|
||||
| **路由器** | Router | 网络层设备,规划路线,连接不同网络 |
|
||||
| **交换机** | Switch | 数据链路层设备,智能转发数据帧 |
|
||||
| **TTL** | Time To Live | 生存时间,防止数据包无限循环 |
|
||||
| **RTT** | Round Trip Time | 往返时间,数据从发送到接收确认的时间 |
|
||||
@@ -0,0 +1,244 @@
|
||||
# 数据的编码、存储与传输
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**计算机如何表示和存储各种数据?** 文字、图片、视频、声音...这些在现实世界中形态各异的信息,是如何变成 0 和 1 的?又是如何存储和传输的?本章带你理解数据的编码、存储和传输原理。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:数据的生命周期
|
||||
|
||||
想象你要寄一封信给朋友:
|
||||
|
||||
1. **编码**:把想法变成文字(信息编码)
|
||||
2. **存储**:写在纸上(数据存储)
|
||||
3. **传输**:通过邮局寄出(数据传输)
|
||||
|
||||
计算机处理数据也是类似的过程:
|
||||
|
||||
| 阶段 | 做什么 | 核心问题 | 类比 |
|
||||
|------|--------|---------|------|
|
||||
| **编码** | 把信息变成 0 和 1 | 如何用二进制表示各种数据? | 把想法变成文字 |
|
||||
| **存储** | 把数据保存起来 | 数据存在哪里?怎么组织? | 写在纸上 |
|
||||
| **传输** | 把数据送到别处 | 如何可靠、高效地传输? | 邮局寄信 |
|
||||
|
||||
::: tip 📊 逐行解读这张表
|
||||
**编码**:计算机只认识 0 和 1,所以所有数据都要"翻译"成二进制。文字用 ASCII 或 Unicode 编码,数字用二进制表示,图片用像素值,声音用采样值。
|
||||
|
||||
**存储**:编码后的数据需要保存起来。存储介质从快到慢有:寄存器 → 缓存 → 内存 → SSD → 硬盘 → 云存储。越快的存储越贵、容量越小。
|
||||
|
||||
**传输**:数据需要在不同设备间流动。传输方式有串行(一位一位传)和并行(多位同时传)。现代高速接口(USB、PCIe)多采用串行方式。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 数据编码:用 0 和 1 表示一切
|
||||
|
||||
### 1.1 文本编码
|
||||
|
||||
<EncodingDemo />
|
||||
|
||||
::: tip 💡 字符编码的演变
|
||||
**ASCII(1963年)**:
|
||||
- 用 7 位二进制表示 128 个字符
|
||||
- 包括英文字母、数字、常用符号
|
||||
- 问题:只能表示英语,无法表示中文等
|
||||
|
||||
**Unicode(1991年)**:
|
||||
- 统一编码标准,覆盖世界上所有文字
|
||||
- 目前已收录超过 14 万个字符
|
||||
- 常用编码方式:UTF-8(变长编码,1-4 字节)
|
||||
|
||||
**UTF-8 的巧妙设计**:
|
||||
- ASCII 字符(0-127)只用 1 字节,完全兼容
|
||||
- 常用汉字用 3 字节
|
||||
- 根据"前导位"判断一个字符占几个字节
|
||||
:::
|
||||
|
||||
**常见字符编码对比:**
|
||||
|
||||
| 编码 | 字节数 | 支持字符 | 特点 |
|
||||
|------|--------|---------|------|
|
||||
| **ASCII** | 1 字节 | 128 个 | 仅英语,兼容性好 |
|
||||
| **UTF-8** | 1-4 字节 | 所有文字 | 变长编码,主流标准 |
|
||||
| **UTF-16** | 2-4 字节 | 所有文字 | 定长为主,Windows 常用 |
|
||||
| **GBK** | 1-2 字节 | 中英文 | 中文专用,不推荐新项目使用 |
|
||||
|
||||
### 1.2 数字编码
|
||||
|
||||
::: tip 💡 整数如何用二进制表示?
|
||||
**无符号整数**:直接用二进制表示
|
||||
- 8 位可以表示 0-255
|
||||
- 32 位可以表示 0 到约 42 亿
|
||||
|
||||
**有符号整数**:用补码表示
|
||||
- 最高位是符号位(0 正 1 负)
|
||||
- 正数:直接用二进制
|
||||
- 负数:正数的二进制取反加 1
|
||||
|
||||
**为什么用补码?**
|
||||
- 加法减法统一处理
|
||||
- 0 的表示唯一
|
||||
- 硬件实现简单
|
||||
:::
|
||||
|
||||
**浮点数表示(IEEE 754 标准):**
|
||||
|
||||
| 部分 | 作用 | 位数(32位浮点) |
|
||||
|------|------|-----------------|
|
||||
| **符号位** | 正负 | 1 位 |
|
||||
| **指数位** | 决定大小范围 | 8 位 |
|
||||
| **尾数位** | 决定精度 | 23 位 |
|
||||
|
||||
### 1.3 多媒体编码
|
||||
|
||||
**图像编码**:
|
||||
- **位图**:每个像素用 RGB 值表示(红绿蓝各 8 位)
|
||||
- **压缩**:JPEG(有损)、PNG(无损)
|
||||
- **矢量图**:用数学公式描述形状(SVG)
|
||||
|
||||
**音频编码**:
|
||||
- **采样**:把连续声波变成离散点
|
||||
- **量化**:把采样值变成数字
|
||||
- **压缩**:MP3(有损)、FLAC(无损)
|
||||
|
||||
**视频编码**:
|
||||
- 视频是一帧帧图像
|
||||
- 关键技术:帧间压缩(只记录变化部分)
|
||||
- 常见格式:H.264、H.265、VP9
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据存储:速度与容量的权衡
|
||||
|
||||
### 2.1 存储层次结构
|
||||
|
||||
<StorageDemo />
|
||||
|
||||
### 2.2 存储器类型
|
||||
|
||||
| 类型 | 原理 | 特点 | 应用 |
|
||||
|------|------|------|------|
|
||||
| **SRAM** | 触发器 | 极快,但昂贵 | CPU 缓存 |
|
||||
| **DRAM** | 电容充放电 | 较快,需刷新 | 内存 |
|
||||
| **Flash** | 浮栅晶体管 | 断电不丢失,有写入寿命 | SSD、U 盘 |
|
||||
| **HDD** | 磁盘磁性记录 | 容量大,有机械延迟 | 机械硬盘 |
|
||||
|
||||
### 2.3 存储的关键指标
|
||||
|
||||
::: tip 💡 如何评估存储性能?
|
||||
**访问时间**:从发出请求到获得数据的时间
|
||||
- 内存:约 100 纳秒
|
||||
- SSD:约 100 微秒
|
||||
- HDD:约 10 毫秒
|
||||
|
||||
**吞吐量**:单位时间能传输的数据量
|
||||
- 内存:几十 GB/s
|
||||
- SSD:几 GB/s
|
||||
- HDD:100-200 MB/s
|
||||
|
||||
**IOPS**:每秒能进行的读写操作次数
|
||||
- SSD:几万到几十万
|
||||
- HDD:几百
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据传输:从串行到并行
|
||||
|
||||
### 3.1 传输方式
|
||||
|
||||
<TransmissionDemo />
|
||||
|
||||
### 3.2 常见接口标准
|
||||
|
||||
| 接口 | 类型 | 速度 | 应用 |
|
||||
|------|------|------|------|
|
||||
| **USB 3.0** | 串行 | 5 Gbps | 外设连接 |
|
||||
| **USB 4** | 串行 | 40 Gbps | 高速外设 |
|
||||
| **SATA III** | 串行 | 6 Gbps | 硬盘接口 |
|
||||
| **PCIe 4.0 x16** | 串行(多通道) | 32 GB/s | 显卡、SSD |
|
||||
| **以太网** | 串行 | 1-100 Gbps | 网络传输 |
|
||||
|
||||
### 3.3 传输的可靠性
|
||||
|
||||
::: tip 💡 如何保证传输不出错?
|
||||
**校验机制**:
|
||||
- **奇偶校验**:简单的错误检测
|
||||
- **CRC 校验**:更强的错误检测能力
|
||||
- **校验和**:快速检测数据完整性
|
||||
|
||||
**纠错机制**:
|
||||
- **重传**:发现错误就重新发送
|
||||
- **前向纠错**:发送冗余信息,接收方能自动纠正
|
||||
|
||||
**流量控制**:
|
||||
- 防止发送方发太快,接收方来不及处理
|
||||
- 类似"确认收到再发下一个"
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 编码、存储、传输的协作
|
||||
|
||||
让我们看一个完整的例子:**保存一张照片到云端**
|
||||
|
||||
```
|
||||
1. 编码阶段
|
||||
- 相机传感器捕捉光线 → 模拟信号
|
||||
- ADC 转换 → 数字信号(RAW 格式)
|
||||
- JPEG 编码 → 压缩后的二进制数据
|
||||
|
||||
2. 存储阶段
|
||||
- 写入手机内存(RAM)→ 临时存储
|
||||
- 写入手机闪存(Flash)→ 持久存储
|
||||
|
||||
3. 传输阶段
|
||||
- 读取闪存数据 → 内存
|
||||
- 通过 Wi-Fi/4G 发送 → 网络传输
|
||||
- 云端接收 → 写入云端存储
|
||||
```
|
||||
|
||||
::: tip 💡 理解这个流程
|
||||
每一步都涉及编码、存储、传输:
|
||||
|
||||
1. **编码**:把图像变成二进制数据
|
||||
2. **存储**:在本地保存
|
||||
3. **传输**:通过网络发送到云端
|
||||
|
||||
这三个环节紧密配合,才能完成"保存照片到云端"这个看似简单的操作。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结:数据的三重奏
|
||||
|
||||
让我们用一个比喻总结编码、存储、传输:
|
||||
|
||||
| 概念 | 比喻 | 核心任务 |
|
||||
|------|------|---------|
|
||||
| **编码** | 翻译 | 把信息变成 0 和 1 |
|
||||
| **存储** | 仓库 | 把数据保存起来 |
|
||||
| **传输** | 快递 | 把数据送到目的地 |
|
||||
|
||||
::: tip 💡 核心启示
|
||||
**数据处理的本质是"转换、保存、移动"**。
|
||||
|
||||
- 编码解决"如何表示"的问题
|
||||
- 存储解决"如何保存"的问题
|
||||
- 传输解决"如何传递"的问题
|
||||
|
||||
理解了这三点,你就会明白:
|
||||
- 为什么不同文件格式要选择不同的编码方式
|
||||
- 为什么需要不同层次的存储介质
|
||||
- 为什么传输速度和可靠性需要平衡
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- **字符编码详解**:深入学习 ASCII、Unicode、UTF-8 的设计原理
|
||||
- **存储技术发展**:了解从磁带到 SSD 的技术演进
|
||||
- **网络传输协议**:学习 TCP/IP 如何保证可靠传输
|
||||
- **数据压缩算法**:了解 ZIP、JPEG、MP3 等压缩原理
|
||||
@@ -0,0 +1,214 @@
|
||||
# 数据结构
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**如何高效地组织和存储数据?** 你可能遇到过这样的困惑:为什么有些程序处理几万条数据很快,有些处理几百条就卡住了?答案往往在于数据结构的选择。本章带你理解常见数据结构的特点和适用场景。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:数据结构是什么?
|
||||
|
||||
想象你要整理一堆书:
|
||||
|
||||
- **堆在地上**:找书要一本本翻(链表)
|
||||
- **按编号放书架**:直接去对应位置拿(数组)
|
||||
- **按类别分柜子**:先找柜子再找书(哈希表)
|
||||
- **按书名排序**:二分查找,每次排除一半(树)
|
||||
|
||||
不同的整理方式,找书的效率天差地别。**数据结构就是数据的"整理方式"**。
|
||||
|
||||
<DataStructureDemo />
|
||||
|
||||
**常见数据结构分类:**
|
||||
|
||||
| 类型 | 特点 | 典型代表 | 适用场景 |
|
||||
|------|------|---------|---------|
|
||||
| **线性结构** | 数据排成一排 | 数组、链表、栈、队列 | 顺序处理、历史记录 |
|
||||
| **哈希结构** | 键值对映射 | 哈希表 | 快速查找、缓存 |
|
||||
| **树形结构** | 层次关系 | 二叉树、B树 | 排序、搜索、文件系统 |
|
||||
| **图结构** | 网状关系 | 有向图、无向图 | 社交网络、路径规划 |
|
||||
|
||||
::: tip 📊 逐行解读这张表
|
||||
**线性结构**:最简单的数据组织方式,数据一个接一个排列。数组适合随机访问,链表适合频繁插入删除。
|
||||
|
||||
**哈希结构**:用"键"直接找到"值",查找速度最快。但需要处理"冲突"问题(两个键映射到同一位置)。
|
||||
|
||||
**树形结构**:有层次关系的数据。二叉搜索树适合排序和搜索,B树适合磁盘存储(数据库索引)。
|
||||
|
||||
**图结构**:最复杂的结构,表示任意的关系网络。社交网络、地图导航都用图来建模。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 线性结构:最基础的组织方式
|
||||
|
||||
### 1.1 数组:连续存储
|
||||
|
||||
::: tip 💡 数组的特点
|
||||
**数组**是一块连续的内存空间,每个元素大小相同。
|
||||
|
||||
**优点**:
|
||||
- 随机访问快:`arr[100]` 直接计算地址,O(1)
|
||||
- 缓存友好:连续存储,CPU 缓存命中率高
|
||||
|
||||
**缺点**:
|
||||
- 插入删除慢:需要移动后面所有元素,O(n)
|
||||
- 大小固定:需要预先分配空间
|
||||
|
||||
**生活类比**:一排编号的储物柜,每个柜子大小相同。找第 10 号柜子直接去,但要在中间插入一个柜子,后面的都要往后挪。
|
||||
:::
|
||||
|
||||
### 1.2 链表:节点相连
|
||||
|
||||
::: tip 💡 链表的特点
|
||||
**链表**由一系列节点组成,每个节点包含数据和指向下一个节点的指针。
|
||||
|
||||
**优点**:
|
||||
- 插入删除快:只需修改指针,O(1)
|
||||
- 大小灵活:可以动态增长
|
||||
|
||||
**缺点**:
|
||||
- 访问慢:要从头开始遍历,O(n)
|
||||
- 额外空间:每个节点需要存储指针
|
||||
|
||||
**生活类比**:寻宝游戏,每个线索指向下一个地点。要找第 10 个线索,必须从第 1 个开始一步步找。
|
||||
:::
|
||||
|
||||
### 1.3 栈和队列:受限的线性结构
|
||||
|
||||
| 结构 | 规则 | 操作 | 类比 | 应用 |
|
||||
|------|------|------|------|------|
|
||||
| **栈** | 后进先出 (LIFO) | push/pop | 一摞盘子 | 函数调用、撤销操作 |
|
||||
| **队列** | 先进先出 (FIFO) | enqueue/dequeue | 排队买票 | 任务调度、消息队列 |
|
||||
|
||||
::: tip 💡 为什么要有"受限"的结构?
|
||||
栈和队列看起来比数组、链表功能少,但正是这种"限制"让它们有明确的用途:
|
||||
|
||||
- **栈**:函数调用时,最后调用的函数最先返回
|
||||
- **队列**:任务调度时,先来的任务先处理
|
||||
|
||||
限制带来简洁,简洁带来高效。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 哈希表:最快的查找
|
||||
|
||||
### 2.1 哈希表原理
|
||||
|
||||
::: tip 💡 哈希表如何工作?
|
||||
**哈希表**通过"哈希函数"把键映射到数组索引。
|
||||
|
||||
**工作流程**:
|
||||
1. 输入键(如 "apple")
|
||||
2. 哈希函数计算:`hash("apple") = 3`
|
||||
3. 直接去数组索引 3 的位置找
|
||||
|
||||
**冲突处理**:
|
||||
- 两个不同的键可能映射到同一索引
|
||||
- 解决方法:链地址法(同一位置用链表存储多个值)
|
||||
|
||||
**生活类比**:图书馆按书名首字母分柜子。"Apple" 开头的书都放 A 柜,"Banana" 开头的放 B 柜。找书时先确定柜子,再在柜子里找。
|
||||
:::
|
||||
|
||||
### 2.2 哈希表的时间复杂度
|
||||
|
||||
| 操作 | 平均情况 | 最坏情况 |
|
||||
|------|---------|---------|
|
||||
| **查找** | O(1) | O(n) |
|
||||
| **插入** | O(1) | O(n) |
|
||||
| **删除** | O(1) | O(n) |
|
||||
|
||||
::: warning ⚠️ 什么时候会退化?
|
||||
当所有键都映射到同一个索引时,哈希表退化为链表,所有操作变成 O(n)。
|
||||
|
||||
**避免方法**:
|
||||
- 选择好的哈希函数
|
||||
- 动态扩容(负载因子超过阈值时扩容)
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 树:层次结构
|
||||
|
||||
### 3.1 二叉搜索树
|
||||
|
||||
::: tip 💡 二叉搜索树的规则
|
||||
**二叉搜索树**是一种特殊的二叉树:
|
||||
- 左子树的所有值 < 根节点
|
||||
- 右子树的所有值 > 根节点
|
||||
|
||||
**查找过程**:
|
||||
1. 从根节点开始
|
||||
2. 如果目标值 < 当前值,往左走
|
||||
3. 如果目标值 > 当前值,往右走
|
||||
4. 每次比较排除一半节点
|
||||
|
||||
**时间复杂度**:O(log n),但最坏情况(变成链表)是 O(n)
|
||||
:::
|
||||
|
||||
### 3.2 平衡树
|
||||
|
||||
为了防止二叉搜索树退化,引入了**平衡树**:
|
||||
|
||||
| 类型 | 平衡方式 | 特点 |
|
||||
|------|---------|------|
|
||||
| **AVL 树** | 严格平衡(高度差 ≤ 1) | 查找最快,插入删除稍慢 |
|
||||
| **红黑树** | 近似平衡 | 综合性能好,应用最广 |
|
||||
| **B 树** | 多路平衡 | 适合磁盘存储,数据库索引 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 如何选择数据结构?
|
||||
|
||||
| 需求 | 推荐结构 | 原因 |
|
||||
|------|---------|------|
|
||||
| **快速随机访问** | 数组 | O(1) 索引访问 |
|
||||
| **频繁插入删除** | 链表 | O(1) 插入删除 |
|
||||
| **快速查找** | 哈希表 | O(1) 平均查找 |
|
||||
| **有序数据** | 平衡树 | O(log n) 查找,保持有序 |
|
||||
| **最近使用** | 栈 | LIFO 特性 |
|
||||
| **任务排队** | 队列 | FIFO 特性 |
|
||||
|
||||
::: tip 💡 选择数据结构的心法
|
||||
**没有最好的数据结构,只有最合适的数据结构。**
|
||||
|
||||
选择时要考虑:
|
||||
1. **主要操作是什么?** 查找?插入?删除?
|
||||
2. **数据量多大?** 小数据量差别不大,大数据量要慎重
|
||||
3. **数据有序吗?** 有序数据可以用二分查找
|
||||
4. **内存限制?** 某些结构需要额外空间
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结:数据结构是程序的基础
|
||||
|
||||
让我们用一个比喻总结各种数据结构:
|
||||
|
||||
| 结构 | 比喻 | 核心特点 |
|
||||
|------|------|---------|
|
||||
| **数组** | 编号储物柜 | 访问快,插入慢 |
|
||||
| **链表** | 寻宝线索 | 插入快,访问慢 |
|
||||
| **栈** | 一摞盘子 | 后进先出 |
|
||||
| **队列** | 排队队伍 | 先进先出 |
|
||||
| **哈希表** | 分类柜子 | 查找最快 |
|
||||
| **树** | 家族族谱 | 层次结构 |
|
||||
|
||||
::: tip 💡 核心启示
|
||||
**数据结构决定了程序的效率上限。**
|
||||
|
||||
- 选对数据结构,问题迎刃而解
|
||||
- 选错数据结构,再好的算法也无济于事
|
||||
|
||||
理解数据结构,就是理解"如何高效地组织数据"。这是每个程序员的基本功。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- **数据结构实现**:自己动手实现各种数据结构,加深理解
|
||||
- **高级数据结构**:学习跳表、布隆过滤器、并查集等
|
||||
- **数据库索引**:了解 B+ 树在数据库中的应用
|
||||
- **缓存设计**:学习 LRU 缓存如何结合哈希表和链表
|
||||
@@ -0,0 +1,255 @@
|
||||
# 操作系统(进程 / 内存 / 文件系统)
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**操作系统是做什么的?** 你可能每天都在用 Windows、macOS 或 Linux,但你知道它到底在忙什么吗?为什么需要它?没有它电脑还能用吗?本章带你理解操作系统的三大核心职责:管理进程、管理内存、管理文件。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:操作系统的角色
|
||||
|
||||
想象你开了一家餐厅。你需要:
|
||||
- **安排员工工作**:谁做菜、谁端盘子、谁收银(进程管理)
|
||||
- **管理厨房空间**:冰箱放什么、操作台怎么分配(内存管理)
|
||||
- **整理仓库物资**:食材怎么存放、怎么找(文件系统)
|
||||
|
||||
操作系统就是电脑的"餐厅经理",它负责协调所有资源,让程序能顺利运行。
|
||||
|
||||
**操作系统的三大核心职责:**
|
||||
|
||||
| 职责 | 管理对象 | 核心问题 | 类比 |
|
||||
|------|---------|---------|------|
|
||||
| **进程管理** | CPU 时间 | 谁先用 CPU?用多久? | 员工排班 |
|
||||
| **内存管理** | 内存空间 | 程序放哪里?怎么不冲突? | 厨房空间分配 |
|
||||
| **文件系统** | 磁盘数据 | 数据怎么存?怎么找? | 仓库物资管理 |
|
||||
|
||||
::: tip 📊 逐行解读这张表
|
||||
**进程管理**:CPU 是最宝贵的资源,操作系统要决定哪个程序先用、用多久。就像餐厅经理安排员工轮班,不能让所有人同时挤在厨房里。
|
||||
|
||||
**内存管理**:内存是程序的"工作台",操作系统要给每个程序分配空间,还要保证它们互不干扰。就像厨房空间有限,要合理分配给不同的厨师。
|
||||
|
||||
**文件系统**:磁盘是"仓库",操作系统要把数据有序地存进去,需要时能快速找到。就像仓库管理员整理货架,按类别、编号存放。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 进程管理:程序的"分身术"
|
||||
|
||||
### 1.1 什么是进程?
|
||||
|
||||
<ProcessDemo />
|
||||
|
||||
::: tip 💡 程序 vs 进程
|
||||
这是初学者最容易混淆的概念:
|
||||
|
||||
| 概念 | 定义 | 类比 | 特点 |
|
||||
|------|------|------|------|
|
||||
| **程序** | 静态的代码文件 | 菜谱 | 存在磁盘上,不会动 |
|
||||
| **进程** | 程序的运行实例 | 正在按菜谱做菜 | 在内存中运行,会变化 |
|
||||
|
||||
**关键区别**:
|
||||
- 一个程序可以启动多个进程(比如打开多个浏览器窗口)
|
||||
- 每个进程有独立的内存空间,互不干扰
|
||||
- 进程有生命周期:创建、运行、等待、终止
|
||||
:::
|
||||
|
||||
### 1.2 进程的状态
|
||||
|
||||
进程在运行过程中会在不同状态之间切换:
|
||||
|
||||
| 状态 | 含义 | 什么时候进入 | 类比 |
|
||||
|------|------|-------------|------|
|
||||
| **就绪 (Ready)** | 准备好运行,等 CPU | 进程刚创建,或从等待恢复 | 员工在休息室等排班 |
|
||||
| **运行 (Running)** | 正在 CPU 上执行 | 被调度器选中 | 员工正在工作 |
|
||||
| **等待 (Waiting)** | 等待 I/O 或其他资源 | 需要读磁盘、等网络 | 员工在等食材送达 |
|
||||
| **终止 (Terminated)** | 运行结束 | 程序退出或出错 | 员工下班 |
|
||||
|
||||
### 1.3 进程调度:谁先用 CPU?
|
||||
|
||||
::: tip 💡 为什么需要调度?
|
||||
CPU 核心数有限,但进程可能有几十上百个。操作系统需要决定:
|
||||
- 哪个进程先运行?
|
||||
- 运行多久?
|
||||
- 什么时候切换?
|
||||
|
||||
这就是**进程调度**要解决的问题。
|
||||
:::
|
||||
|
||||
**常见调度算法:**
|
||||
|
||||
| 算法 | 思路 | 优点 | 缺点 |
|
||||
|------|------|------|------|
|
||||
| **先来先服务 (FCFS)** | 谁先到谁先运行 | 简单公平 | 短任务可能等很久 |
|
||||
| **短作业优先 (SJF)** | 短任务优先 | 平均等待时间最短 | 需要预知任务长度 |
|
||||
| **时间片轮转 (RR)** | 每人运行一小段时间 | 公平,响应快 | 切换开销大 |
|
||||
| **优先级调度** | 重要任务优先 | 重要任务响应快 | 可能导致低优先级任务饿死 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 内存管理:程序的"工作台"
|
||||
|
||||
### 2.1 为什么需要内存管理?
|
||||
|
||||
<MemoryDemo />
|
||||
|
||||
::: tip 💡 如果没有内存管理会怎样?
|
||||
想象一个没有管理的厨房:
|
||||
|
||||
- **冲突**:两个厨师同时用同一个灶台,菜都糊了
|
||||
- **浪费**:有人占了整个厨房,其他人没地方做饭
|
||||
- **安全问题**:有人偷吃了别人的食材
|
||||
|
||||
操作系统通过**内存管理**解决这些问题:
|
||||
- 给每个进程分配独立的内存空间
|
||||
- 防止进程互相干扰(内存保护)
|
||||
- 高效利用有限的内存资源
|
||||
:::
|
||||
|
||||
### 2.2 虚拟内存:让每个进程都"以为"自己独占内存
|
||||
|
||||
::: tip 💡 什么是虚拟内存?
|
||||
**虚拟内存**是操作系统的一个"魔术":
|
||||
|
||||
- 每个进程都以为自己有 4GB(或更多)的内存空间
|
||||
- 实际上物理内存可能只有 8GB、16GB
|
||||
- 操作系统通过"映射"把虚拟地址转换成物理地址
|
||||
|
||||
**生活类比**:想象一个酒店:
|
||||
- 每个客人都以为自己独占整个酒店(虚拟空间)
|
||||
- 实际上酒店只有 100 间房(物理内存)
|
||||
- 前台(操作系统)负责分配房间、记录谁住哪里
|
||||
:::
|
||||
|
||||
**虚拟内存的好处:**
|
||||
|
||||
| 好处 | 说明 | 为什么重要 |
|
||||
|------|------|-----------|
|
||||
| **隔离保护** | 进程间内存互不干扰 | 一个崩溃不影响其他 |
|
||||
| **内存扩展** | 用磁盘当内存用 | 可以运行比物理内存大的程序 |
|
||||
| **简化编程** | 不用关心物理地址 | 程序员写代码更简单 |
|
||||
|
||||
### 2.3 内存分配策略
|
||||
|
||||
当进程需要内存时,操作系统如何分配?
|
||||
|
||||
| 策略 | 思路 | 特点 |
|
||||
|------|------|------|
|
||||
| **首次适应** | 找到第一个够大的空闲块 | 速度快 |
|
||||
| **最佳适应** | 找最小的够大的空闲块 | 内存利用率高 |
|
||||
| **最坏适应** | 找最大的空闲块 | 减少小碎片 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 文件系统:数据的"档案柜"
|
||||
|
||||
### 3.1 什么是文件系统?
|
||||
|
||||
<FilesystemDemo />
|
||||
|
||||
::: tip 💡 文件系统是什么?
|
||||
**文件系统**是操作系统管理磁盘数据的方式。
|
||||
|
||||
**生活类比**:想象一个图书馆:
|
||||
- 书架 = 磁盘
|
||||
- 书 = 文件
|
||||
- 目录卡片 = inode
|
||||
- 分类编号 = 路径
|
||||
|
||||
没有文件系统,磁盘就是一堆杂乱的数据。有了文件系统,我们可以:
|
||||
- 用"路径"找到文件(如 `/home/user/document.txt`)
|
||||
- 创建、删除、修改文件
|
||||
- 控制谁能访问哪些文件
|
||||
:::
|
||||
|
||||
### 3.2 inode:文件的"身份证"
|
||||
|
||||
::: tip 💡 inode 是什么?
|
||||
每个文件都有一个 **inode**(索引节点),记录了文件的元数据:
|
||||
|
||||
| 信息 | 说明 |
|
||||
|------|------|
|
||||
| inode 编号 | 文件的唯一标识 |
|
||||
| 文件大小 | 多少字节 |
|
||||
| 权限 | 谁能读写 |
|
||||
| 时间戳 | 创建、修改、访问时间 |
|
||||
| 数据块位置 | 文件内容存在哪些磁盘块 |
|
||||
|
||||
**关键理解**:
|
||||
- 文件名不在 inode 里!文件名只是目录中的一个条目
|
||||
- 一个文件可以有多个名字(硬链接)
|
||||
- 删除文件只是删除目录项,inode 可能还在
|
||||
:::
|
||||
|
||||
### 3.3 常见文件系统
|
||||
|
||||
| 文件系统 | 操作系统 | 特点 |
|
||||
|---------|---------|------|
|
||||
| **NTFS** | Windows | 支持大文件、权限控制 |
|
||||
| **APFS** | macOS | 加密、快照、高效 |
|
||||
| **ext4** | Linux | 稳定、高效、广泛使用 |
|
||||
| **FAT32** | 通用 | 兼容性好,但单文件最大 4GB |
|
||||
| **exFAT** | 通用 | 支持大文件,适合 U 盘 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 进程、内存、文件系统的协作
|
||||
|
||||
这三个子系统是如何配合工作的?让我们看一个完整的例子:
|
||||
|
||||
**场景:打开一个文档文件**
|
||||
|
||||
```
|
||||
1. 用户双击文件
|
||||
↓
|
||||
2. 文件系统:根据路径找到 inode,读取文件内容
|
||||
↓
|
||||
3. 进程管理:创建新进程(文档编辑器),分配 PID
|
||||
↓
|
||||
4. 内存管理:为新进程分配内存,加载程序代码和数据
|
||||
↓
|
||||
5. 进程运行:编辑器进程读取文件内容,显示在屏幕上
|
||||
```
|
||||
|
||||
::: tip 💡 理解这个流程
|
||||
每一步都涉及操作系统的核心功能:
|
||||
|
||||
1. **文件系统**:负责"找到文件"
|
||||
2. **进程管理**:负责"启动程序"
|
||||
3. **内存管理**:负责"给程序分配空间"
|
||||
|
||||
这三者紧密协作,才能完成一个看似简单的"打开文件"操作。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结:操作系统是"大管家"
|
||||
|
||||
让我们用一个比喻总结操作系统的三大职责:
|
||||
|
||||
| 职责 | 比喻 | 核心任务 |
|
||||
|------|------|---------|
|
||||
| **进程管理** | 餐厅排班员 | 安排谁先工作、工作多久 |
|
||||
| **内存管理** | 厨房管理员 | 分配工作台、防止冲突 |
|
||||
| **文件系统** | 仓库管理员 | 整理物资、快速查找 |
|
||||
|
||||
::: tip 💡 核心启示
|
||||
**操作系统的本质是"资源管理"**。
|
||||
|
||||
- CPU 时间是资源 → 进程管理
|
||||
- 内存空间是资源 → 内存管理
|
||||
- 磁盘空间是资源 → 文件系统
|
||||
|
||||
理解了这一点,你就会明白:
|
||||
- 为什么电脑会变慢(进程太多、内存不足)
|
||||
- 为什么需要重启(清理资源、释放内存)
|
||||
- 为什么文件要整理(提高查找效率)
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- **操作系统原理**:深入学习进程调度、内存分页、文件系统实现
|
||||
- **Linux 系统编程**:学习如何与操作系统交互(系统调用)
|
||||
- **并发编程**:学习多进程、多线程编程
|
||||
- **系统监控**:学习使用 top、htop、vmstat 等工具监控系统状态
|
||||
@@ -0,0 +1,398 @@
|
||||
# 编程语言图谱
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**为什么有这么多编程语言?它们之间有什么关系?** 从机器语言到现代高级语言,每种语言都有其设计哲学和适用场景。本章带你理解编程语言的演化历程和核心概念。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 0. 想象你要和外国人交流:
|
||||
|
||||
- **直接用肢体语言**:最原始,但效率极低(机器语言)
|
||||
- **学习对方的语言**:需要翻译,但表达丰富(高级语言)
|
||||
- **使用世界语**:设计完美,但没人用(某些学术语言)
|
||||
- **使用翻译软件**:自动转换,但可能不准确(编译器/解释器)
|
||||
|
||||
**编程语言就是人类与计算机沟通的桥梁**,不同的语言有不同的设计哲学。
|
||||
|
||||
<LanguageMapDemo />
|
||||
|
||||
---
|
||||
|
||||
## 1. 编程语言的演化
|
||||
|
||||
### 1.1 第一代:机器语言(1940s)
|
||||
|
||||
::: tip 💡 机器语言是什么?
|
||||
直接用 0 和 1 编写程序,计算机可以直接执行。
|
||||
|
||||
**示例**:让计算机计算 1 + 2
|
||||
```
|
||||
10110000 00000001 ; 将 1 放入寄存器
|
||||
10110001 00000010 ; 将 2 放入另一个寄存器
|
||||
10100010 ; 执行加法
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- 人类难以理解和记忆
|
||||
- 容易出错,一个 0 写成 1 就全错了
|
||||
- 不同 CPU 有不同的机器语言
|
||||
:::
|
||||
|
||||
### 1.2 第二代:汇编语言(1950s)
|
||||
|
||||
用**助记符**代替 0 和 1:
|
||||
|
||||
```asm
|
||||
MOV AX, 1 ; 将 1 放入 AX 寄存器
|
||||
MOV BX, 2 ; 将 2 放入 BX 寄存器
|
||||
ADD AX, BX ; 将 BX 加到 AX
|
||||
```
|
||||
|
||||
::: tip 💡 汇编语言 vs 机器语言
|
||||
| 特性 | 机器语言 | 汇编语言 |
|
||||
|------|---------|---------|
|
||||
| **可读性** | 极差 | 较好 |
|
||||
| **执行效率** | 最高 | 最高(汇编器直接转换) |
|
||||
| **移植性** | 无 | 无(依赖 CPU 架构) |
|
||||
| **使用场景** | 几乎不用 | 嵌入式、操作系统内核 |
|
||||
:::
|
||||
|
||||
### 1.3 第三代:高级语言(1950s - 至今)
|
||||
|
||||
**用接近自然语言的方式编程**:
|
||||
|
||||
```c
|
||||
int sum = 1 + 2; // C 语言
|
||||
```
|
||||
|
||||
**里程碑语言**:
|
||||
|
||||
| 年代 | 语言 | 意义 |
|
||||
|------|------|------|
|
||||
| **1957** | Fortran | 第一个高级语言,科学计算 |
|
||||
| **1958** | Lisp | 函数式编程鼻祖 |
|
||||
| **1959** | COBOL | 商业数据处理 |
|
||||
| **1972** | C | 系统编程,影响深远 |
|
||||
| **1983** | C++ | 面向对象 + C 的效率 |
|
||||
| **1991** | Python | 简洁优雅,AI 时代主角 |
|
||||
| **1995** | Java | 跨平台,企业应用 |
|
||||
| **1995** | JavaScript | Web 开发,无处不在 |
|
||||
| **2009** | Go | 并发友好,云原生 |
|
||||
| **2010** | Rust | 内存安全,系统编程新选择 |
|
||||
|
||||
### 1.4 第四代:领域特定语言(DSL)
|
||||
|
||||
为特定领域设计的语言:
|
||||
|
||||
| 语言 | 领域 | 示例 |
|
||||
|------|------|------|
|
||||
| **SQL** | 数据库查询 | `SELECT * FROM users` |
|
||||
| **HTML** | 网页结构 | `<div>Hello</div>` |
|
||||
| **CSS** | 样式定义 | `color: red;` |
|
||||
| **Regex** | 文本匹配 | `\d{3}-\d{4}` |
|
||||
| **MATLAB** | 数学计算 | `A = [1 2; 3 4]` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 编程范式:思考问题的方式
|
||||
|
||||
::: tip 💡 什么是编程范式?
|
||||
编程范式是**编程的思维方式**,决定了你如何组织代码和解决问题。
|
||||
|
||||
就像写作有不同的文体(诗歌、小说、论文),编程也有不同的"文体"。
|
||||
:::
|
||||
|
||||
### 2.1 命令式编程(Imperative)
|
||||
|
||||
**核心思想**:告诉计算机"怎么做"
|
||||
|
||||
```c
|
||||
// 计算数组总和
|
||||
int sum = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
sum += arr[i];
|
||||
}
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 关注**过程**和**步骤**
|
||||
- 通过**语句**改变程序状态
|
||||
- 最接近计算机实际执行方式
|
||||
|
||||
**代表语言**:C, Fortran, BASIC
|
||||
|
||||
### 2.2 面向对象编程(OOP)
|
||||
|
||||
**核心思想**:把数据和操作封装在"对象"中
|
||||
|
||||
```python
|
||||
class Dog:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def bark(self):
|
||||
print(f"{self.name} says woof!")
|
||||
|
||||
dog = Dog("Buddy")
|
||||
dog.bark() # Buddy says woof!
|
||||
```
|
||||
|
||||
**四大特性**:
|
||||
|
||||
| 特性 | 含义 | 生活类比 |
|
||||
|------|------|---------|
|
||||
| **封装** | 隐藏内部细节 | 汽车方向盘,不需要知道引擎原理 |
|
||||
| **继承** | 子类继承父类 | 儿子继承父亲的基因 |
|
||||
| **多态** | 同一接口不同实现 | 不同动物发出不同叫声 |
|
||||
| **抽象** | 提取共同特征 | "动物"是对猫、狗的抽象 |
|
||||
|
||||
**代表语言**:Java, C++, Python, Ruby
|
||||
|
||||
### 2.3 函数式编程(Functional)
|
||||
|
||||
**核心思想**:把计算视为函数求值,避免状态变化
|
||||
|
||||
```haskell
|
||||
-- 计算数组总和
|
||||
sum arr = foldl (+) 0 arr
|
||||
|
||||
-- 或者更简洁
|
||||
sum = foldl (+) 0
|
||||
```
|
||||
|
||||
**核心原则**:
|
||||
|
||||
| 原则 | 含义 | 好处 |
|
||||
|------|------|------|
|
||||
| **纯函数** | 相同输入永远产生相同输出 | 易测试、易推理 |
|
||||
| **不可变数据** | 数据一旦创建就不变 | 无副作用、线程安全 |
|
||||
| **高阶函数** | 函数可以作为参数和返回值 | 代码复用、灵活组合 |
|
||||
| **无副作用** | 函数不修改外部状态 | 可预测、易调试 |
|
||||
|
||||
**代表语言**:Haskell, Lisp, Erlang, F#
|
||||
|
||||
### 2.4 声明式编程(Declarative)
|
||||
|
||||
**核心思想**:告诉计算机"做什么",而不是"怎么做"
|
||||
|
||||
```sql
|
||||
-- 查询所有活跃用户
|
||||
SELECT name, email
|
||||
FROM users
|
||||
WHERE active = true
|
||||
ORDER BY created_at DESC
|
||||
```
|
||||
|
||||
**对比命令式**:
|
||||
|
||||
| 命令式 | 声明式 |
|
||||
|--------|--------|
|
||||
| "从第一行开始遍历..." | "给我所有活跃用户" |
|
||||
| "检查每个用户是否活跃..." | "按创建时间排序" |
|
||||
| "如果活跃就加入结果..." | 数据库自己决定怎么执行 |
|
||||
| "最后排序返回..." | |
|
||||
|
||||
**代表语言**:SQL, Prolog, HTML
|
||||
|
||||
---
|
||||
|
||||
## 3. 类型系统:数据的分类规则
|
||||
|
||||
::: tip 💡 什么是类型系统?
|
||||
类型系统是编程语言的**交通规则**,规定数据如何分类和操作。
|
||||
|
||||
就像现实世界:
|
||||
- **整数** = 整数类型(1, 2, 3...)
|
||||
- **文字** = 字符串类型("hello")
|
||||
- **是/否** = 布尔类型(true/false)
|
||||
:::
|
||||
|
||||
### 3.1 静态类型 vs 动态类型
|
||||
|
||||
| 特性 | 静态类型 | 动态类型 |
|
||||
|------|---------|---------|
|
||||
| **类型检查时机** | 编译时 | 运行时 |
|
||||
| **代码示例** | `int x = 1;` | `x = 1` |
|
||||
| **错误发现** | 编译期就发现 | 运行时才发现 |
|
||||
| **灵活性** | 较低 | 较高 |
|
||||
| **性能** | 较高(编译优化) | 较低(运行时检查) |
|
||||
| **代表语言** | Java, C++, Rust, TypeScript | Python, JavaScript, Ruby |
|
||||
|
||||
**静态类型示例(Java)**:
|
||||
|
||||
```java
|
||||
String name = "Alice";
|
||||
name = 123; // 编译错误!类型不匹配
|
||||
```
|
||||
|
||||
**动态类型示例(Python)**:
|
||||
|
||||
```python
|
||||
name = "Alice"
|
||||
name = 123 # 没问题,运行时类型改变
|
||||
```
|
||||
|
||||
### 3.2 强类型 vs 弱类型
|
||||
|
||||
| 特性 | 强类型 | 弱类型 |
|
||||
|------|--------|--------|
|
||||
| **类型转换** | 不允许隐式转换 | 允许隐式转换 |
|
||||
| **类型安全** | 高 | 低 |
|
||||
| **代码示例** | `"1" + 1` 报错 | `"1" + 1 = "11"` |
|
||||
| **代表语言** | Python, Java, Rust | JavaScript, PHP, C |
|
||||
|
||||
**弱类型示例(JavaScript)**:
|
||||
|
||||
```javascript
|
||||
console.log("1" + 1) // "11" (字符串拼接)
|
||||
console.log("1" - 1) // 0 (自动转数字)
|
||||
console.log([] + []) // "" (空字符串)
|
||||
console.log([] + {}) // "[object Object]"
|
||||
```
|
||||
|
||||
**强类型示例(Python)**:
|
||||
|
||||
```python
|
||||
"1" + 1 # TypeError: can only concatenate str to str
|
||||
```
|
||||
|
||||
### 3.3 类型推断
|
||||
|
||||
现代语言可以**自动推断**变量类型:
|
||||
|
||||
```typescript
|
||||
// TypeScript
|
||||
let x = 1; // 推断为 number
|
||||
let y = "hello"; // 推断为 string
|
||||
|
||||
// Rust
|
||||
let x = 1; // 推断为 i32
|
||||
let y = "hello"; // 推断为 &str
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 编译型 vs 解释型
|
||||
|
||||
::: tip 💡 程序如何运行?
|
||||
编程语言写的代码需要转换成机器能理解的指令,有两种主要方式:
|
||||
:::
|
||||
|
||||
### 4.1 编译型语言
|
||||
|
||||
**流程**:源代码 → 编译器 → 机器码 → 执行
|
||||
|
||||
```
|
||||
源代码 (main.c)
|
||||
↓
|
||||
编译器 (gcc)
|
||||
↓
|
||||
可执行文件 (main.exe)
|
||||
↓
|
||||
CPU 直接执行
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 执行速度快 | 编译时间长 |
|
||||
| 编译时发现错误 | 跨平台需要重新编译 |
|
||||
| 不需要运行时环境 | 调试较困难 |
|
||||
|
||||
**代表语言**:C, C++, Rust, Go
|
||||
|
||||
### 4.2 解释型语言
|
||||
|
||||
**流程**:源代码 → 解释器 → 逐行执行
|
||||
|
||||
```
|
||||
源代码 (main.py)
|
||||
↓
|
||||
解释器 (python)
|
||||
↓
|
||||
逐行解释执行
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 跨平台 | 执行速度慢 |
|
||||
| 开发调试快 | 运行时才能发现错误 |
|
||||
| 代码即运行 | 需要解释器环境 |
|
||||
|
||||
**代表语言**:Python, JavaScript, Ruby, PHP
|
||||
|
||||
### 4.3 混合型语言(JIT)
|
||||
|
||||
**即时编译(Just-In-Time)**:先解释执行,热点代码编译成机器码
|
||||
|
||||
```
|
||||
源代码
|
||||
↓
|
||||
字节码(中间代码)
|
||||
↓
|
||||
解释执行 + JIT 编译热点代码
|
||||
↓
|
||||
执行
|
||||
```
|
||||
|
||||
**代表语言**:Java, JavaScript (V8), Python (PyPy)
|
||||
|
||||
---
|
||||
|
||||
## 5. 如何选择编程语言?
|
||||
|
||||
::: tip 💡 没有最好的语言,只有最适合的语言
|
||||
选择语言要考虑:
|
||||
1. **问题领域**:Web 开发?系统编程?数据分析?
|
||||
2. **团队熟悉度**:团队擅长什么?
|
||||
3. **生态系统**:有没有现成的库?
|
||||
4. **性能需求**:需要多高的性能?
|
||||
5. **开发效率**:需要多快开发完成?
|
||||
:::
|
||||
|
||||
### 5.1 按应用场景选择
|
||||
|
||||
| 场景 | 推荐语言 | 原因 |
|
||||
|------|---------|------|
|
||||
| **Web 前端** | JavaScript, TypeScript | 浏览器原生支持 |
|
||||
| **Web 后端** | Java, Go, Python, Node.js | 生态成熟,框架丰富 |
|
||||
| **移动开发** | Swift (iOS), Kotlin (Android) | 官方推荐 |
|
||||
| **数据分析** | Python, R | 库丰富,社区活跃 |
|
||||
| **人工智能** | Python | TensorFlow, PyTorch |
|
||||
| **系统编程** | C, C++, Rust | 性能高,控制精细 |
|
||||
| **游戏开发** | C++, C#, Lua | 引擎支持 |
|
||||
| **嵌入式** | C, Rust | 资源受限环境 |
|
||||
| **云原生** | Go, Rust | 并发友好,部署简单 |
|
||||
|
||||
### 5.2 学习路线建议
|
||||
|
||||
**初学者**:
|
||||
1. Python(语法简单,应用广泛)
|
||||
2. JavaScript(Web 开发必备)
|
||||
3. 选择一门静态类型语言(Java 或 TypeScript)
|
||||
|
||||
**进阶**:
|
||||
1. 学习 C 理解底层
|
||||
2. 学习函数式编程思想(Haskell 或 F#)
|
||||
3. 学习 Rust 理解内存安全
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
::: tip 📚 核心要点
|
||||
1. **编程语言演化**:从机器语言到高级语言,越来越接近人类思维
|
||||
2. **编程范式**:命令式、面向对象、函数式、声明式,各有优劣
|
||||
3. **类型系统**:静态/动态、强/弱类型,影响代码安全和灵活性
|
||||
4. **运行方式**:编译型快但需编译,解释型慢但灵活
|
||||
5. **选择语言**:没有银弹,根据场景选择合适的工具
|
||||
:::
|
||||
|
||||
**下一步学习**:
|
||||
- [类型系统与编译原理入门](./type-systems-compilers) - 深入理解类型系统和编译过程
|
||||
- [数据结构](./data-structures) - 理解数据的组织方式
|
||||
- [算法思维入门](./algorithm-thinking) - 学习解决问题的方法
|
||||
@@ -0,0 +1,259 @@
|
||||
# 从晶体管到 CPU
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**计算机是怎么"思考"的?** 你可能知道 CPU 是电脑的"大脑",但这个大脑到底是怎么工作的?它怎么从一堆金属和塑料变成能执行程序、处理数据的智能设备?本章带你从最底层的晶体管开始,一步步理解 CPU 的构造原理。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:从沙子到智能
|
||||
|
||||
<TransistorDemo />
|
||||
|
||||
现代计算机的"思考"能力,归根结底来自于一个简单的东西:**开关**。
|
||||
|
||||
想象你有一个开关,可以控制灯的亮灭。现在,如果你有几十亿个这样的开关,并且能用它们组合出各种复杂的逻辑,会发生什么?这就是计算机的奥秘。
|
||||
|
||||
**从沙子到智能的层次结构:**
|
||||
|
||||
| 层级 | 名称 | 数量级 | 作用 | 类比 |
|
||||
|------|------|--------|------|------|
|
||||
| **1** | 晶体管 | 数十亿 | 最基本的开关单元 | 一个开关 |
|
||||
| **2** | 逻辑门 | 数亿 | 实现基本逻辑运算 | 开关组合 |
|
||||
| **3** | 功能单元 | 数百 | 实现特定功能(加法、存储等) | 功能模块 |
|
||||
| **4** | CPU 核心 | 1-128 | 完整的处理器 | 大脑 |
|
||||
|
||||
::: tip 📊 逐行解读这张表
|
||||
**第1层(晶体管)**:这是最底层的"开关"。现代 CPU 使用的是 MOSFET(金属氧化物半导体场效应晶体管),它的特点是:给栅极加电压,源极和漏极之间就导通;不加电压,就断开。这就是"用电控制电"的开关。
|
||||
|
||||
**第2层(逻辑门)**:把晶体管组合起来,就能实现"与"、"或"、"非"等逻辑运算。比如 AND 门:两个输入都为 1 时输出才为 1。这就像两个串联的开关,必须都按下灯才会亮。
|
||||
|
||||
**第3层(功能单元)**:把逻辑门组合起来,就能实现更复杂的功能。加法器能做加法,寄存器能存储数据,多路选择器能选择数据。这些是 CPU 的"器官"。
|
||||
|
||||
**第4层(CPU 核心)**:把功能单元组合起来,加上控制器、总线等,就形成了一个完整的 CPU 核心。它能取指令、解码、执行、写回结果——这就是"计算"的全部过程。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 晶体管:数字世界的开关
|
||||
|
||||
### 1.1 什么是晶体管?
|
||||
|
||||
::: tip 💡 晶体管是什么?
|
||||
**晶体管(Transistor)** 是一种半导体器件,它可以像开关一样控制电流的通断。
|
||||
|
||||
**生活类比**:想象一个水龙头:
|
||||
- **水龙头**:你用手拧开关,控制水流
|
||||
- **晶体管**:用电压控制开关,控制电流
|
||||
|
||||
关键区别是:晶体管不是用手拧,而是用"电"来控制。这意味着一个开关可以控制另一个开关,从而实现"自动控制"。
|
||||
:::
|
||||
|
||||
**晶体管的三个极:**
|
||||
|
||||
| 极 | 名称 | 作用 | 类比 |
|
||||
|---|------|------|------|
|
||||
| **源极 (Source)** | 电流入口 | 电流从这里进入 | 水管入口 |
|
||||
| **漏极 (Drain)** | 电流出口 | 电流从这里流出 | 水管出口 |
|
||||
| **栅极 (Gate)** | 控制端 | 控制是否导通 | 水龙头开关 |
|
||||
|
||||
### 1.2 晶体管如何表示 0 和 1?
|
||||
|
||||
计算机只认识 0 和 1,这和晶体管有什么关系?
|
||||
|
||||
::: tip 💡 用电压表示 0 和 1
|
||||
**核心思想**:用电压的高低来表示 0 和 1。
|
||||
|
||||
- **高电压(如 3.3V)**:表示 1
|
||||
- **低电压(如 0V)**:表示 0
|
||||
|
||||
这就像灯泡的亮和灭:
|
||||
- 灯亮 = 1
|
||||
- 灯灭 = 0
|
||||
|
||||
晶体管的作用就是"控制灯泡的亮灭"——给栅极加高电压,源极和漏极导通,"灯泡"亮了(输出 1);给栅极低电压,源极和漏极断开,"灯泡"灭了(输出 0)。
|
||||
:::
|
||||
|
||||
### 1.3 从一个开关到几十亿
|
||||
|
||||
你可能好奇:一个开关能做什么?答案是:一个开关做不了什么,但几十亿个开关组合起来,就能做任何计算。
|
||||
|
||||
**现代 CPU 的晶体管数量:**
|
||||
|
||||
| 年份 | CPU | 晶体管数量 | 制程工艺 |
|
||||
|------|-----|-----------|---------|
|
||||
| 1971 | Intel 4004 | 2,300 | 10μm |
|
||||
| 1993 | Intel Pentium | 310万 | 0.8μm |
|
||||
| 2006 | Intel Core 2 | 2.91亿 | 65nm |
|
||||
| 2020 | Apple M1 | 160亿 | 5nm |
|
||||
| 2023 | Apple M3 Max | 920亿 | 3nm |
|
||||
|
||||
::: tip 💡 什么是制程工艺?
|
||||
**制程工艺**(如 5nm、3nm)指的是晶体管的尺寸。数字越小,晶体管越小,同样面积能容纳的晶体管越多。
|
||||
|
||||
- **5nm**:大约是 50 个原子的宽度
|
||||
- **3nm**:大约是 30 个原子的宽度
|
||||
|
||||
制程越小,CPU 性能越强、功耗越低。但制造难度也指数级增加。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 逻辑门:用开关做运算
|
||||
|
||||
### 2.1 从晶体管到逻辑门
|
||||
|
||||
一个晶体管只是一个开关,但把多个晶体管组合起来,就能实现"逻辑运算"。
|
||||
|
||||
<LogicGateDemo />
|
||||
|
||||
### 2.2 基本逻辑门详解
|
||||
|
||||
**AND 门(与门)**:
|
||||
- **规则**:两个输入都为 1,输出才为 1
|
||||
- **生活类比**:串联的两个开关,必须都按下灯才亮
|
||||
- **应用**:判断"多个条件是否同时满足"
|
||||
|
||||
**OR 门(或门)**:
|
||||
- **规则**:任一个输入为 1,输出就为 1
|
||||
- **生活类比**:并联的两个开关,按任意一个灯就亮
|
||||
- **应用**:判断"是否满足任一条件"
|
||||
|
||||
**NOT 门(非门)**:
|
||||
- **规则**:输入和输出相反
|
||||
- **生活类比**:反相器,开变关、关变开
|
||||
- **应用**:取反操作
|
||||
|
||||
**XOR 门(异或门)**:
|
||||
- **规则**:两个输入不同时输出 1
|
||||
- **生活类比**:判断"两个值是否不同"
|
||||
- **应用**:比较、加法运算
|
||||
|
||||
### 2.3 用逻辑门做加法
|
||||
|
||||
<AdderDemo />
|
||||
|
||||
::: tip 💡 加法器是怎么工作的?
|
||||
**半加器**:处理两个 1 位二进制数相加
|
||||
- 输入:A、B(各 1 位)
|
||||
- 输出:和(S)、进位(C)
|
||||
- 公式:S = A XOR B,C = A AND B
|
||||
|
||||
**全加器**:处理两个 1 位二进制数相加,加上上一位的进位
|
||||
- 输入:A、B、Cin(进位输入)
|
||||
- 输出:和(S)、Cout(进位输出)
|
||||
|
||||
**多位加法器**:把多个全加器级联起来
|
||||
- 第 1 位加法器的进位输出,连接到第 2 位加法器的进位输入
|
||||
- 就像我们手算加法时"逢二进一"
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 功能单元:逻辑门的组合
|
||||
|
||||
### 3.1 常见功能单元
|
||||
|
||||
| 单元 | 功能 | 组成 | 类比 |
|
||||
|------|------|------|------|
|
||||
| **加法器** | 做加法 | 多个全加器级联 | 计算器的加法功能 |
|
||||
| **多路选择器** | 选择数据 | AND 门 + OR 门 | 多选一开关 |
|
||||
| **译码器** | 解码指令 | 多个 AND 门 | 翻译器 |
|
||||
| **寄存器** | 存储数据 | 触发器(锁存器) | 临时笔记本 |
|
||||
| **计数器** | 计数 | 触发器级联 | 计分牌 |
|
||||
|
||||
### 3.2 寄存器:存储 1 位数据
|
||||
|
||||
::: tip 💡 寄存器是怎么存储数据的?
|
||||
寄存器使用**触发器**电路来存储数据。触发器的特点是:一旦设置了状态,就能保持住,直到下一次改变。
|
||||
|
||||
**生活类比**:想象一个跷跷板:
|
||||
- 推一下左边,左边就沉下去,右边翘起来
|
||||
- 即使你松手,跷跷板也会保持这个状态
|
||||
- 只有再推一下,才会改变状态
|
||||
|
||||
触发器就是这样的"电子跷跷板",能"记住"上一次被设置的状态。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. CPU 架构:从功能单元到处理器
|
||||
|
||||
### 4.1 CPU 的核心组件
|
||||
|
||||
<CpuArchitectureDemo />
|
||||
|
||||
### 4.2 CPU 是如何执行指令的?
|
||||
|
||||
CPU 执行一条指令,需要经过四个阶段:
|
||||
|
||||
| 阶段 | 名称 | 做什么 | 类比 |
|
||||
|------|------|--------|------|
|
||||
| **1** | 取指 (Fetch) | 从内存读取指令 | 从书架上取书 |
|
||||
| **2** | 解码 (Decode) | 分析指令要做什么 | 阅读书的内容 |
|
||||
| **3** | 执行 (Execute) | 执行运算 | 按书中的指示行动 |
|
||||
| **4** | 写回 (Write Back) | 把结果存回寄存器 | 把结果记在笔记本上 |
|
||||
|
||||
::: tip 💡 指令周期
|
||||
这四个阶段组成一个**指令周期**。CPU 不断重复这个周期,一条一条执行指令,就实现了"计算"。
|
||||
|
||||
现代 CPU 使用**流水线技术**,让多个指令的不同阶段并行执行:
|
||||
- 第 1 条指令在执行时
|
||||
- 第 2 条指令在解码
|
||||
- 第 3 条指令在取指
|
||||
|
||||
这就像工厂流水线,大大提高了效率。
|
||||
:::
|
||||
|
||||
### 4.3 CPU 性能的关键指标
|
||||
|
||||
| 指标 | 含义 | 影响 | 典型值 |
|
||||
|------|------|------|--------|
|
||||
| **主频** | 每秒执行多少个时钟周期 | 主频越高,执行越快 | 3-5 GHz |
|
||||
| **核心数** | 独立的处理器数量 | 核心越多,并行能力越强 | 4-64 核 |
|
||||
| **缓存** | CPU 内部的高速存储 | 缓存越大,访问内存越少 | 8-64 MB |
|
||||
| **指令集** | CPU 能理解的指令集合 | 决定兼容性和功能 | x86、ARM |
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结:从沙子到智能
|
||||
|
||||
让我们回顾一下从晶体管到 CPU 的完整路径:
|
||||
|
||||
```
|
||||
沙子(硅)
|
||||
↓ 提纯、切割
|
||||
硅晶圆
|
||||
↓ 光刻、蚀刻、掺杂
|
||||
晶体管(开关)
|
||||
↓ 组合
|
||||
逻辑门(AND、OR、NOT...)
|
||||
↓ 组合
|
||||
功能单元(加法器、寄存器...)
|
||||
↓ 组合
|
||||
CPU 核心(ALU、控制器、寄存器组...)
|
||||
↓ 编程
|
||||
软件应用
|
||||
```
|
||||
|
||||
::: tip 💡 核心启示
|
||||
**计算机的本质是"开关的组合"**。
|
||||
|
||||
- 一个开关做不了什么
|
||||
- 但几十亿个开关,按特定方式组合,就能执行任何计算
|
||||
- 这就是"量变引起质变"的最好例证
|
||||
|
||||
理解这一点,你就会明白:
|
||||
- 为什么计算机只认识 0 和 1
|
||||
- 为什么编程语言最终都要翻译成机器码
|
||||
- 为什么算法效率如此重要(因为每一步操作都需要大量晶体管参与)
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
- **计算机组成原理**:深入了解 CPU、内存、I/O 的工作原理
|
||||
- **数字电路**:学习逻辑门、触发器、时序电路的设计
|
||||
- **计算机体系结构**:研究 CPU 的性能优化、流水线、缓存等
|
||||
- **汇编语言**:直接和 CPU 对话,理解指令执行过程
|
||||
@@ -0,0 +1,478 @@
|
||||
# 类型系统与编译原理入门
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**编程语言如何理解你的代码?** 当你写下 `int x = 10 + 5;` 时,编译器需要理解每个字符的含义、检查类型是否正确、优化代码、最终生成机器能执行的指令。本章带你理解这个神奇的过程。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 0. 想象你在翻译一本书:
|
||||
|
||||
- **识别单词**:把句子拆成一个个单词(词法分析)
|
||||
- **理解语法**:判断句子是否符合语法规则(语法分析)
|
||||
- **理解含义**:确保句子意思正确(语义分析)
|
||||
- **优化表达**:让句子更简洁(代码优化)
|
||||
- **翻译输出**:翻译成目标语言(代码生成)
|
||||
|
||||
**编译器就是编程语言的"翻译官"**,将人类可读的代码转换为机器可执行的指令。
|
||||
|
||||
<TypeSystemDemo />
|
||||
|
||||
---
|
||||
|
||||
## 1. 类型系统基础
|
||||
|
||||
### 1.1 什么是类型?
|
||||
|
||||
::: tip 💡 类型的本质
|
||||
类型是对数据的**分类**,规定了数据可以进行的操作。
|
||||
|
||||
就像现实世界:
|
||||
- **整数**:可以加减乘除,但不能分割
|
||||
- **字符串**:可以拼接、截取,但不能直接运算
|
||||
- **布尔**:只有 true/false,用于逻辑判断
|
||||
:::
|
||||
|
||||
**基本数据类型**:
|
||||
|
||||
| 类型 | 表示 | 占用空间 | 取值范围 |
|
||||
|------|------|---------|---------|
|
||||
| **整数** | int | 4 字节 | -2^31 到 2^31-1 |
|
||||
| **浮点数** | float | 4 字节 | 约 ±3.4 × 10^38 |
|
||||
| **双精度** | double | 8 字节 | 约 ±1.8 × 10^308 |
|
||||
| **字符** | char | 1 字节 | 0 到 255 |
|
||||
| **布尔** | bool | 1 字节 | true/false |
|
||||
|
||||
### 1.2 静态类型 vs 动态类型
|
||||
|
||||
::: tip 💡 核心区别
|
||||
**静态类型**:变量类型在**编译时**确定
|
||||
**动态类型**:变量类型在**运行时**确定
|
||||
:::
|
||||
|
||||
**静态类型示例(Java)**:
|
||||
|
||||
```java
|
||||
String name = "Alice"; // 编译时确定 name 是 String 类型
|
||||
name = 123; // 编译错误!类型不匹配
|
||||
```
|
||||
|
||||
**动态类型示例(Python)**:
|
||||
|
||||
```python
|
||||
name = "Alice" # 运行时 name 是 str 类型
|
||||
name = 123 # 运行时 name 变成 int 类型
|
||||
print(type(name)) # <class 'int'>
|
||||
```
|
||||
|
||||
**对比分析**:
|
||||
|
||||
| 特性 | 静态类型 | 动态类型 |
|
||||
|------|---------|---------|
|
||||
| **类型检查时机** | 编译时 | 运行时 |
|
||||
| **错误发现** | 早(编译期) | 晚(运行时) |
|
||||
| **代码灵活性** | 低 | 高 |
|
||||
| **执行性能** | 高(编译优化) | 低(运行时检查) |
|
||||
| **IDE 支持** | 好(自动补全) | 差(运行时才知道类型) |
|
||||
| **代表语言** | Java, C++, Rust, TypeScript | Python, JavaScript, Ruby |
|
||||
|
||||
### 1.3 强类型 vs 弱类型
|
||||
|
||||
::: tip 💡 核心区别
|
||||
**强类型**:不允许隐式类型转换
|
||||
**弱类型**:允许隐式类型转换
|
||||
:::
|
||||
|
||||
**弱类型示例(JavaScript)**:
|
||||
|
||||
```javascript
|
||||
console.log("1" + 1) // "11" - 字符串拼接
|
||||
console.log("1" - 1) // 0 - 自动转数字
|
||||
console.log([] + []) // "" - 空数组转空字符串
|
||||
console.log(true + 1) // 2 - 布尔转数字
|
||||
```
|
||||
|
||||
**强类型示例(Python)**:
|
||||
|
||||
```python
|
||||
"1" + 1 # TypeError: can only concatenate str to str
|
||||
"1" - 1 # TypeError: unsupported operand type(s)
|
||||
```
|
||||
|
||||
**类型系统四象限**:
|
||||
|
||||
| | 强类型 | 弱类型 |
|
||||
|---|--------|--------|
|
||||
| **静态** | Java, Rust, Haskell | C, C++ |
|
||||
| **动态** | Python, Ruby | JavaScript, PHP |
|
||||
|
||||
### 1.4 类型推断
|
||||
|
||||
现代语言可以**自动推断**变量类型,结合静态类型的安全性和动态类型的简洁性:
|
||||
|
||||
```typescript
|
||||
// TypeScript
|
||||
let x = 1; // 推断为 number
|
||||
let arr = [1, 2, 3]; // 推断为 number[]
|
||||
let fn = (x) => x; // 推断为 (x: any) => any
|
||||
|
||||
// Rust
|
||||
let x = 1; // 推断为 i32
|
||||
let s = "hello"; // 推断为 &str
|
||||
let v = vec![1, 2]; // 推断为 Vec<i32>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 编译原理基础
|
||||
|
||||
### 2.1 编译器的任务
|
||||
|
||||
::: tip 💡 编译器做什么?
|
||||
编译器将**源代码**转换为**目标代码**,主要完成:
|
||||
|
||||
1. **理解代码**:分析源代码的结构和含义
|
||||
2. **检查正确性**:发现语法和语义错误
|
||||
3. **优化代码**:提高执行效率
|
||||
4. **生成代码**:输出目标机器的指令
|
||||
:::
|
||||
|
||||
<CompilerDemo />
|
||||
|
||||
### 2.2 词法分析(Lexical Analysis)
|
||||
|
||||
**任务**:将源代码分解为**词法单元(Token)**
|
||||
|
||||
**示例**:
|
||||
|
||||
```
|
||||
源代码: int x = 10 + 5;
|
||||
|
||||
词法单元:
|
||||
[int] → 关键字
|
||||
[x] → 标识符
|
||||
[=] → 运算符
|
||||
[10] → 整数字面量
|
||||
[+] → 运算符
|
||||
[5] → 整数字面量
|
||||
[;] → 分隔符
|
||||
```
|
||||
|
||||
**词法分析器的工作**:
|
||||
|
||||
| 输入 | 处理 | 输出 |
|
||||
|------|------|------|
|
||||
| `int` | 匹配关键字表 | `KEYWORD(int)` |
|
||||
| `x` | 匹配标识符规则 | `IDENTIFIER(x)` |
|
||||
| `10` | 匹配数字规则 | `NUMBER(10)` |
|
||||
|
||||
### 2.3 语法分析(Syntax Analysis)
|
||||
|
||||
**任务**:根据语法规则,将 Token 流组织成**语法树(AST)**
|
||||
|
||||
**示例**:
|
||||
|
||||
```
|
||||
表达式: 1 + 2 * 3
|
||||
|
||||
语法树:
|
||||
+
|
||||
/ \
|
||||
1 *
|
||||
/ \
|
||||
2 3
|
||||
```
|
||||
|
||||
::: tip 💡 为什么是这棵树?
|
||||
根据运算优先级,`*` 优先级高于 `+`,所以 `2 * 3` 先结合。
|
||||
|
||||
如果表达式是 `(1 + 2) * 3`,语法树会变成:
|
||||
|
||||
```
|
||||
*
|
||||
/ \
|
||||
+ 3
|
||||
/ \
|
||||
1 2
|
||||
```
|
||||
:::
|
||||
|
||||
**语法规则(文法)**:
|
||||
|
||||
```
|
||||
表达式 → 表达式 + 项 | 表达式 - 项 | 项
|
||||
项 → 项 * 因子 | 项 / 因子 | 因子
|
||||
因子 → 数字 | (表达式)
|
||||
```
|
||||
|
||||
### 2.4 语义分析(Semantic Analysis)
|
||||
|
||||
**任务**:检查语义正确性,进行类型检查
|
||||
|
||||
**主要工作**:
|
||||
|
||||
| 工作 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **类型检查** | 检查类型是否匹配 | `int x = "hello";` → 错误 |
|
||||
| **作用域分析** | 检查变量是否声明 | 使用未声明变量 → 错误 |
|
||||
| **符号表构建** | 记录所有标识符信息 | 变量名、类型、作用域 |
|
||||
| **类型推断** | 推断表达式类型 | `1 + 2.0` → float |
|
||||
|
||||
**符号表示例**:
|
||||
|
||||
```
|
||||
int x = 10;
|
||||
float y = 3.14;
|
||||
string name = "Alice";
|
||||
|
||||
符号表:
|
||||
┌──────────┬────────┬─────────┐
|
||||
│ 名称 │ 类型 │ 作用域 │
|
||||
├──────────┼────────┼─────────┤
|
||||
│ x │ int │ global │
|
||||
│ y │ float │ global │
|
||||
│ name │ string │ global │
|
||||
└──────────┴────────┴─────────┘
|
||||
```
|
||||
|
||||
### 2.5 中间代码生成
|
||||
|
||||
**任务**:生成平台无关的中间表示(IR)
|
||||
|
||||
**三地址码示例**:
|
||||
|
||||
```
|
||||
源代码: int x = (a + b) * c;
|
||||
|
||||
三地址码:
|
||||
t1 = a + b
|
||||
t2 = t1 * c
|
||||
x = t2
|
||||
```
|
||||
|
||||
::: tip 💡 为什么需要中间代码?
|
||||
1. **平台无关**:一次编写,多平台编译
|
||||
2. **便于优化**:在 IR 层面进行优化
|
||||
3. **支持多语言**:不同语言可以编译到同一 IR
|
||||
|
||||
例如 LLVM IR 支持 C、C++、Rust、Swift 等多种语言。
|
||||
:::
|
||||
|
||||
### 2.6 代码优化
|
||||
|
||||
**任务**:提高代码执行效率
|
||||
|
||||
**常见优化技术**:
|
||||
|
||||
| 优化技术 | 说明 | 示例 |
|
||||
|---------|------|------|
|
||||
| **常量折叠** | 编译时计算常量表达式 | `10 + 5` → `15` |
|
||||
| **死代码消除** | 删除不会执行的代码 | `if (false) { ... }` → 删除 |
|
||||
| **内联展开** | 函数调用替换为函数体 | `add(1, 2)` → `1 + 2` |
|
||||
| **循环优化** | 减少循环开销 | 循环展开、循环不变量外提 |
|
||||
| **公共子表达式消除** | 避免重复计算 | `a+b` 计算一次,多次使用 |
|
||||
|
||||
**优化示例**:
|
||||
|
||||
```c
|
||||
// 优化前
|
||||
int x = 10 + 5; // 常量折叠
|
||||
int y = x * 2; // x 已知为 15
|
||||
if (false) { // 死代码
|
||||
printf("never");
|
||||
}
|
||||
|
||||
// 优化后
|
||||
int x = 15;
|
||||
int y = 30;
|
||||
// if 语句被删除
|
||||
```
|
||||
|
||||
### 2.7 目标代码生成
|
||||
|
||||
**任务**:生成目标机器的机器码
|
||||
|
||||
**汇编代码示例**:
|
||||
|
||||
```asm
|
||||
; int x = 15;
|
||||
mov eax, 15
|
||||
mov dword ptr [x], eax
|
||||
|
||||
; int y = 30;
|
||||
mov eax, 30
|
||||
mov dword ptr [y], eax
|
||||
```
|
||||
|
||||
**代码生成的主要任务**:
|
||||
|
||||
| 任务 | 说明 |
|
||||
|------|------|
|
||||
| **指令选择** | 选择合适的机器指令 |
|
||||
| **寄存器分配** | 决定哪些变量放在寄存器 |
|
||||
| **指令调度** | 安排指令顺序,提高流水线效率 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 编译型 vs 解释型 vs JIT
|
||||
|
||||
### 3.1 编译型语言
|
||||
|
||||
**流程**:源代码 → 编译器 → 机器码 → 执行
|
||||
|
||||
```
|
||||
main.c → [编译器] → main.exe → [CPU] → 执行
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- ✅ 执行速度快
|
||||
- ✅ 编译期发现错误
|
||||
- ❌ 编译时间长
|
||||
- ❌ 跨平台需要重新编译
|
||||
|
||||
**代表语言**:C, C++, Rust, Go
|
||||
|
||||
### 3.2 解释型语言
|
||||
|
||||
**流程**:源代码 → 解释器 → 逐行执行
|
||||
|
||||
```
|
||||
main.py → [解释器] → 逐行解释执行
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- ✅ 跨平台
|
||||
- ✅ 开发调试快
|
||||
- ❌ 执行速度慢
|
||||
- ❌ 运行时才能发现错误
|
||||
|
||||
**代表语言**:Python, Ruby, PHP
|
||||
|
||||
### 3.3 JIT(即时编译)
|
||||
|
||||
**流程**:源代码 → 字节码 → JIT 编译 → 执行
|
||||
|
||||
```
|
||||
源代码 → [编译器] → 字节码 → [JIT] → 机器码 → 执行
|
||||
```
|
||||
|
||||
**工作原理**:
|
||||
1. 先将源代码编译成字节码(中间代码)
|
||||
2. 解释器逐行执行字节码
|
||||
3. 发现热点代码(频繁执行),JIT 编译成机器码
|
||||
4. 后续直接执行机器码
|
||||
|
||||
**特点**:
|
||||
- ✅ 兼顾性能和跨平台
|
||||
- ✅ 热点代码执行快
|
||||
- ❌ 启动慢(需要预热)
|
||||
- ❌ 内存占用大
|
||||
|
||||
**代表语言**:Java (JVM), JavaScript (V8), Python (PyPy)
|
||||
|
||||
---
|
||||
|
||||
## 4. 实践:手写简单解释器
|
||||
|
||||
### 4.1 目标
|
||||
|
||||
实现一个简单的计算器,支持加减乘除:
|
||||
|
||||
```
|
||||
输入: 1 + 2 * 3
|
||||
输出: 7
|
||||
```
|
||||
|
||||
### 4.2 词法分析器
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
Token = namedtuple('Token', ['type', 'value'])
|
||||
|
||||
def tokenize(code):
|
||||
tokens = []
|
||||
for match in re.finditer(r'\d+|[+\-*/()]', code):
|
||||
value = match.group()
|
||||
if value.isdigit():
|
||||
tokens.append(Token('NUMBER', int(value)))
|
||||
else:
|
||||
tokens.append(Token(value, value))
|
||||
return tokens
|
||||
|
||||
# 测试
|
||||
print(tokenize('1 + 2 * 3'))
|
||||
# [Token(type='NUMBER', value=1), Token(type='+', value='+'), ...]
|
||||
```
|
||||
|
||||
### 4.3 语法分析器
|
||||
|
||||
```python
|
||||
class Parser:
|
||||
def __init__(self, tokens):
|
||||
self.tokens = tokens
|
||||
self.pos = 0
|
||||
|
||||
def parse(self):
|
||||
return self.expr()
|
||||
|
||||
def expr(self):
|
||||
result = self.term()
|
||||
while self.current() in ('+', '-'):
|
||||
op = self.consume()
|
||||
right = self.term()
|
||||
if op == '+':
|
||||
result += right
|
||||
else:
|
||||
result -= right
|
||||
return result
|
||||
|
||||
def term(self):
|
||||
result = self.factor()
|
||||
while self.current() in ('*', '/'):
|
||||
op = self.consume()
|
||||
right = self.factor()
|
||||
if op == '*':
|
||||
result *= right
|
||||
else:
|
||||
result //= right
|
||||
return result
|
||||
|
||||
def factor(self):
|
||||
token = self.consume()
|
||||
if token.type == 'NUMBER':
|
||||
return token.value
|
||||
elif token.value == '(':
|
||||
result = self.expr()
|
||||
self.consume() # )
|
||||
return result
|
||||
```
|
||||
|
||||
### 4.4 完整解释器
|
||||
|
||||
```python
|
||||
def evaluate(code):
|
||||
tokens = tokenize(code)
|
||||
parser = Parser(tokens)
|
||||
return parser.parse()
|
||||
|
||||
print(evaluate('1 + 2 * 3')) # 7
|
||||
print(evaluate('(1 + 2) * 3')) # 9
|
||||
print(evaluate('10 - 2 * 3')) # 4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结
|
||||
|
||||
::: tip 📚 核心要点
|
||||
1. **类型系统**:静态/动态、强/弱类型,影响代码安全和灵活性
|
||||
2. **编译流程**:词法分析 → 语法分析 → 语义分析 → 中间代码 → 优化 → 代码生成
|
||||
3. **执行方式**:编译型快但需编译,解释型慢但灵活,JIT 兼顾两者
|
||||
4. **实践价值**:理解编译原理有助于写出更好的代码
|
||||
:::
|
||||
|
||||
**下一步学习**:
|
||||
- [编程语言图谱](./programming-languages) - 了解更多编程语言
|
||||
- [数据结构](./data-structures) - 理解数据的组织方式
|
||||
- [算法思维入门](./algorithm-thinking) - 学习解决问题的方法
|
||||
@@ -1,5 +1,4 @@
|
||||
# 终端原理 (Introduction to Terminal Principles)
|
||||
|
||||
# 命令行与 Shell 脚本
|
||||
> 💡 **学习指南**:本章节旨在为零基础读者提供一个关于终端(Terminal)工作原理的系统性认知。无需具备计算机专业背景,我们将通过交互式演示,由浅入深地解析终端的运行机制。
|
||||
|
||||
## 0. 快速上手:如何打开终端?
|
||||
@@ -0,0 +1,2 @@
|
||||
# 调试的艺术
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 编辑器与 AI 编程助手
|
||||
|
||||
> 待实现
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 383 KiB After Width: | Height: | Size: 383 KiB |
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 278 KiB |
@@ -0,0 +1,3 @@
|
||||
# 环境变量与 PATH
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,348 @@
|
||||
# Git:代码的时光机
|
||||
::: tip 🎯 核心问题
|
||||
**写代码时最怕什么?** 写错了想回退、改崩了想重来、多人同时改同一个文件...这些头疼的事,Git 都能帮你搞定!它就像是代码世界的"时光机",让你随时回到过去,又能和队友在各自的"平行宇宙"里安全开发。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 0. 最常用的 5 个场景(直接照抄)
|
||||
|
||||
如果你只想"立刻能用",先把这块过一遍:每个场景都是现实工作中最常见的 Git 流程。
|
||||
|
||||
<GitScenariosDemo />
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要学 Git?三大痛点
|
||||
|
||||
### 1.1 痛点一:版本混乱
|
||||
|
||||
你是否经历过这种绝望?
|
||||
|
||||
```text
|
||||
论文_初稿.doc
|
||||
论文_修改版.doc
|
||||
论文_最终版.doc
|
||||
论文_最终版_打死不改版.doc
|
||||
论文_绝对是最后一次修改版.doc
|
||||
```
|
||||
|
||||
**Git 的解决方案**:不需要复制副本,一个文件夹搞定所有历史版本。想回到哪次修改,一键恢复。
|
||||
|
||||
### 1.2 痛点二:无法后悔
|
||||
|
||||
::: tip 💡 这个场景你一定遇到过
|
||||
写代码写了 3 小时,突然发现之前的思路更好,但已经改不回去了...或者删错了一段代码,想找回原来的版本。
|
||||
|
||||
有了 Git,这种情况永远不会发生。每次重要节点都能"存档",出问题随时"读档"重来。
|
||||
:::
|
||||
|
||||
### 1.3 痛点三:协作冲突
|
||||
|
||||
你和队友同时改同一个文件:
|
||||
|
||||
- 你改了 A 文件的第 10 行
|
||||
- 队友改了 A 文件的第 15 行
|
||||
- 怎么合并?谁覆盖谁?
|
||||
|
||||
**Git 的解决方案**:智能合并,自动处理大部分冲突。只有当你们真的改了同一行代码时,才需要手动决定用谁的。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心概念:三区模型
|
||||
|
||||
Git 的设计哲学其实很像**寄快递**。
|
||||
|
||||
<GitThreeAreasDemo />
|
||||
|
||||
### 2.1 三个区域是什么?
|
||||
|
||||
::: tip 📦 用快递理解 Git
|
||||
想象你在寄快递:
|
||||
|
||||
- **工作区(Working Dir)** = 你的**书桌**。你在这里整理要寄的东西,想怎么乱改都行。
|
||||
- **暂存区(Staging Area)** = **快递盒**。你把要寄的文件放进去(`git add`),准备打包。
|
||||
- **仓库(Repository)** = **快递柜**。一旦你封箱寄出(`git commit`),这个版本就被永久记录下来了。
|
||||
:::
|
||||
|
||||
| 区域 | 作用 | 对应命令 | 状态 |
|
||||
| ---------- | ------------------ | --------------------- | ------------- |
|
||||
| **工作区** | 你当前正在改的代码 | `git status` 查看修改 | 红色 = 未暂存 |
|
||||
| **暂存区** | 准备提交的文件 | `git add` 添加 | 绿色 = 已暂存 |
|
||||
| **仓库** | 永久保存的历史版本 | `git commit` 提交 | 只读,不能改 |
|
||||
|
||||
::: tip 💡 关键理解
|
||||
只有提交到**仓库**的内容才是安全的。工作区里没提交的内容,丢了就真丢了。所以经常`git commit`是好习惯!
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 基础工作流:存档三步走
|
||||
|
||||
日常开发中,你 90% 的时间都在重复这三个动作。
|
||||
|
||||
<GitWorkflowDemo />
|
||||
|
||||
### 3.1 第一步:修改代码(工作区)
|
||||
|
||||
在工作区写写画画,想怎么改就怎么改。这时候修改只在你本地,还没记录。
|
||||
|
||||
### 3.2 第二步:挑选文件(git add → 暂存区)
|
||||
|
||||
::: tip 🤔 为什么要先 add 再 commit?
|
||||
你可能问:为什么不能直接 commit 所有修改?
|
||||
|
||||
**答案**:因为有时候你不想一次性提交所有改动。
|
||||
|
||||
- 今天改了 5 个文件,但只想提交其中 3 个(完成了一个功能)
|
||||
- 另外 2 个文件还在调试中,不想现在提交
|
||||
|
||||
`git add` 让你有选择权:决定这次提交包含哪些文件。
|
||||
:::
|
||||
|
||||
**常用命令**:
|
||||
|
||||
```bash
|
||||
# 添加单个文件
|
||||
git add index.html
|
||||
|
||||
# 添加所有修改
|
||||
git add .
|
||||
|
||||
# 查看哪些文件被暂存了
|
||||
git status
|
||||
```
|
||||
|
||||
### 3.3 第三步:封箱提交(git commit → 仓库)
|
||||
|
||||
给这次修改起个名字(比如"修复了登录 Bug"),永久存档。
|
||||
|
||||
**重要:commit message 要写清楚!**
|
||||
|
||||
```bash
|
||||
# ❌ 不好的写法
|
||||
git commit -m "update"
|
||||
|
||||
# ✅ 好的写法
|
||||
git commit -m "feat: 添加用户登录功能"
|
||||
git commit -m "fix: 修复首页在 iOS 的显示问题"
|
||||
git commit -m "docs: 更新 README 的部署说明"
|
||||
```
|
||||
|
||||
::: tip 💡 commit message 规范
|
||||
推荐用**类型+描述**的格式:
|
||||
|
||||
- `feat:` 新功能
|
||||
- `fix:` 修复 bug
|
||||
- `docs:` 文档更新
|
||||
- `style:` 代码格式(不影响功能)
|
||||
- `refactor:` 重构(不改变功能)
|
||||
- `test:` 测试相关
|
||||
- `chore:` 构建/工具相关
|
||||
|
||||
这样以后翻历史记录,一眼就知道每次提交做了什么。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 平行宇宙:分支(Branch)的魔法
|
||||
|
||||
这是 Git 最强大的功能!
|
||||
|
||||
::: tip 🌌 用游戏理解分支
|
||||
想象你在玩游戏,前面有个大 Boss(上线新功能),你怕打不过导致游戏结束(系统崩溃)。
|
||||
|
||||
这时候,你可以开一个**分支(Branch)**,相当于**复制了一个平行世界**:
|
||||
|
||||
- 在**平行世界**(新分支)里打 Boss,输了也不怕,因为主世界(主分支)没影响
|
||||
- 打赢了就把成果"合并"回主世界
|
||||
- 多个队友可以在各自的平行世界开发,互不干扰
|
||||
:::
|
||||
|
||||
<GitBranchMergeDemo />
|
||||
|
||||
### 4.1 主分支 vs 开发分支
|
||||
|
||||
| 分支类型 | 作用 | 特点 |
|
||||
| ------------------- | -------------- | ------------------------------------ |
|
||||
| **main/master** | 稳定的线上版本 | 只有测试通过的代码才能进来 |
|
||||
| **dev/feature-xxx** | 你的试验田 | 这里炸了地球也没关系,不影响主分支 |
|
||||
| **hotfix-xxx** | 紧急修复 | 生产出 bug 时,从 main 开分支快速修复 |
|
||||
|
||||
### 4.2 分支操作流程
|
||||
|
||||
**创建分支并切换**:
|
||||
|
||||
```bash
|
||||
# 创建新分支
|
||||
git branch feature-login
|
||||
|
||||
# 切换到新分支
|
||||
git checkout feature-login
|
||||
|
||||
# 或者一步到位:创建并切换
|
||||
git checkout -b feature-login
|
||||
```
|
||||
|
||||
**在分支上开发**:
|
||||
|
||||
```bash
|
||||
# 在 feature-login 分支上改代码...
|
||||
git add .
|
||||
git commit -m "feat: 添加登录表单"
|
||||
```
|
||||
|
||||
**合并回主分支**:
|
||||
|
||||
```bash
|
||||
# 切回主分支
|
||||
git checkout main
|
||||
|
||||
# 合并 feature-login
|
||||
git merge feature-login
|
||||
|
||||
# 删除已合并的分支(可选)
|
||||
git branch -d feature-login
|
||||
```
|
||||
|
||||
::: tip 💡 什么时候用分支?
|
||||
**个人开发**:
|
||||
|
||||
- 要尝试新想法,不确定会不会搞崩现有代码 → 开分支
|
||||
- 修一个复杂 bug,需要多次实验 → 开分支
|
||||
|
||||
**团队开发**:
|
||||
|
||||
- 每个功能一个分支,互不干扰
|
||||
- 开发完提 Pull Request,队友 review 后再合并
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 常用命令速查表
|
||||
|
||||
| 命令 | 作用 | 人话解释 | 使用频率 |
|
||||
| ----------------------- | ---------- | ------------------------------ | --------------------- |
|
||||
| `git init` | 初始化 | "在这里建个新仓库" | 项目开始时用一次 |
|
||||
| `git status` | 查看状态 | "现在乱不乱?有没有东西没提交?" | ⭐⭐⭐⭐⭐ 极高频 |
|
||||
| `git add .` | 添加所有 | "把桌上所有文件都扔进快递盒" | ⭐⭐⭐⭐⭐ 每次提交前 |
|
||||
| `git add file.txt` | 添加单个 | "只要这个文件" | ⭐⭐⭐⭐ 选择性添加 |
|
||||
| `git commit -m "..."` | 提交 | "封箱!贴上标签,写上这次改了啥" | ⭐⭐⭐⭐⭐ 完成功能时 |
|
||||
| `git log` | 查看历史 | "翻翻以前的日记" | ⭐⭐⭐ 回顾历史 |
|
||||
| `git checkout -b dev` | 创建新分支 | "我要去平行宇宙 dev 探险了" | ⭐⭐⭐⭐ 开新功能 |
|
||||
| `git checkout main` | 切换分支 | "回地球(主分支)看看" | ⭐⭐⭐⭐ 切换任务 |
|
||||
| `git merge dev` | 合并分支 | "把平行宇宙的成果带回地球" | ⭐⭐⭐ 完成功能 |
|
||||
| `git branch` | 查看分支 | "现在有哪些平行世界?" | ⭐⭐⭐ 查看状态 |
|
||||
| `git branch -d feature` | 删除分支 | "这个平行世界不需要了,删掉" | ⭐⭐ 合并后清理 |
|
||||
| `git push` | 推送 | "把本地存档上传到云端" | ⭐⭐⭐⭐⭐ 团队协作 |
|
||||
| `git pull` | 拉取 | "把云端最新存档下载到本地" | ⭐⭐⭐⭐⭐ 团队协作 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 进阶:解决冲突与远程协作
|
||||
|
||||
### 6.1 冲突(Conflict)是什么?
|
||||
|
||||
当你和队友**同时修改了同一个文件的同一行代码**,Git 就会懵:"我该听谁的?"这就是**冲突(Conflict)**。
|
||||
|
||||
<GitConflictDemo />
|
||||
|
||||
### 6.2 怎么解决冲突?
|
||||
|
||||
**Step 1**:打开冲突文件,会看到这样的标记:
|
||||
|
||||
```text
|
||||
<<<<<<< HEAD
|
||||
你的代码
|
||||
=======
|
||||
队友的代码
|
||||
>>>>>>> feature-branch
|
||||
```
|
||||
|
||||
**Step 2**:手动选择要保留的代码,或合并两者:
|
||||
|
||||
```text
|
||||
# 保留你的代码 → 删除队友的部分和标记
|
||||
# 保留队友的 → 删除你的部分和标记
|
||||
# 合并两者 → 综合两边的代码
|
||||
```
|
||||
|
||||
**Step 3**:删除所有标记,保存文件
|
||||
|
||||
**Step 4**:重新提交
|
||||
|
||||
```bash
|
||||
git add 解决冲突的文件
|
||||
git commit # Git 会自动生成合并提交
|
||||
```
|
||||
|
||||
::: tip 💡 避免冲突的最佳实践
|
||||
|
||||
- **频繁沟通**:队友改同一个文件前,先打个招呼
|
||||
- **小步提交**:不要攒着大量代码最后才提交,增加冲突概率
|
||||
- **分支隔离**:不同功能用不同分支,减少直接冲突
|
||||
- **用 Pull Request**:合并前让队友 review,提前发现问题
|
||||
:::
|
||||
|
||||
### 6.3 远程仓库(Remote)
|
||||
|
||||
**远程仓库**(比如 GitHub/GitLab)就是**云端的备份中心**。
|
||||
|
||||
<GitRemoteDemo />
|
||||
|
||||
**核心操作**:
|
||||
|
||||
| 操作 | 命令 | 作用 |
|
||||
| ------------ | ---------------------------------------------- | ------------------------ |
|
||||
| **关联远程** | `git remote add origin https://github.com/...` | 第一次连接云端 |
|
||||
| **推送** | `git push -u origin main` | 把本地存档上传 |
|
||||
| **拉取** | `git pull` | 把云端最新存档下载并合并 |
|
||||
| **克隆** | `git clone https://github.com/...` | 复制整个仓库到本地 |
|
||||
|
||||
::: tip 💡 push 和 pull 的区别
|
||||
|
||||
- **push**:你的本地代码 → 云端(你改了东西,要同步给队友)
|
||||
- **pull**:云端代码 → 你的本地(队友改了东西,你要同步下来)
|
||||
|
||||
**最佳实践**:每天开始工作前先`git pull`,下班前`git push`,这样减少冲突。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结:Git 的核心思想
|
||||
|
||||
Git 不是简单的"版本备份",而是一个**完整的代码协作系统**:
|
||||
|
||||
| 特性 | 解决的问题 | 生活类比 |
|
||||
| ------------ | ------------------------------- | --------------------- |
|
||||
| **版本管理** | 代码改错了怎么办? | 时光机,随时回退 |
|
||||
| **分支** | 多人同时改同一个文件怎么办? | 平行宇宙,互不干扰 |
|
||||
| **暂存区** | 这次提交不想包含所有修改怎么办? | 快递盒,挑拣要寄的东西 |
|
||||
| **远程协作** | 怎么和队友共享代码? | 云备份,随时随地同步 |
|
||||
| **冲突处理** | 真的改到同一行了怎么办? | 智能合并 + 手动协调 |
|
||||
|
||||
**学习建议**:
|
||||
|
||||
1. **先用起来**:不要等"完全理解"再用,一边用一边理解
|
||||
2. **从简单开始**:个人项目先掌握`add/commit/push/pull`,团队项目再学分支
|
||||
3. **看 Git 图形化工具**:SourceTree、GitHub Desktop 等,可视化帮助理解
|
||||
4. **遇到问题不要慌**:Git 的设计就是为了让你能安全地尝试,大不了`git reset`
|
||||
|
||||
---
|
||||
|
||||
## 附录:名词速查表
|
||||
|
||||
| 名词 | 英文 | 用人话解释 |
|
||||
| -------- | ---------- | ------------------------------------- |
|
||||
| **仓库** | Repository | 存放所有版本历史的数据库 |
|
||||
| **提交** | Commit | 一次完整的版本记录,像存档点 |
|
||||
| **分支** | Branch | 独立的开发线,像平行宇宙 |
|
||||
| **合并** | Merge | 把一个分支的改动整合到另一个分支 |
|
||||
| **冲突** | Conflict | 同一行代码被修改多次,Git 不知道选哪个 |
|
||||
| **暂存** | Stage | 把修改加入"准备提交"的列表 |
|
||||
| **远程** | Remote | 云端的仓库副本(GitHub/GitLab) |
|
||||
| **克隆** | Clone | 复制整个远程仓库到本地 |
|
||||
| **推送** | Push | 本地 → 远程,上传代码 |
|
||||
| **拉取** | Pull | 远程 → 本地,下载代码 |
|
||||
| **检出** | Checkout | 切换到某个分支或版本 |
|
||||
| **HEAD** | - | 当前所在的分支/版本的指针 |
|
||||
@@ -181,9 +181,9 @@ onMounted(() => {
|
||||
|
||||
为了方便大家理解每个选项的含义,在这里我们对菜单栏进行深入解析:
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
<details class="custom-block details" id="vscode-file-menu">
|
||||
<summary>File(文件):项目与文件的打开/保存/工作区管理</summary>
|
||||
@@ -0,0 +1,3 @@
|
||||
# 包管理器(npm / pip / cargo)
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 端口与 localhost
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 正则表达式
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# SSH 与密钥认证
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 无障碍与国际化
|
||||
|
||||
> 待实现
|
||||
@@ -1,9 +1,24 @@
|
||||
# 浏览器渲染管线与事件循环
|
||||
|
||||
# 浏览器渲染管道
|
||||
::: tip 🎯 核心问题
|
||||
**为什么有些网页流畅如丝,有些却卡成PPT?** 浏览器是怎么把一堆HTML、CSS、JavaScript代码变成你眼前看到的网页的?本章将带你深入浏览器的"车间",理解它的工作流程,从而写出性能更好的网页。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
| 章节 | 内容 | 学完能干嘛 |
|
||||
|-----|------|-----------|
|
||||
| **第 1 章** | 为什么要理解渲染管线 | 理解性能优化的必要性 |
|
||||
| **第 2 章** | 渲染管线的五个阶段 | 掌握浏览器渲染的基本流程 |
|
||||
| **第 3 章** | 构建DOM树和CSSOM树 | 理解HTML和CSS如何被解析 |
|
||||
| **第 4 章** | 构建渲染树 | 知道哪些元素会被渲染 |
|
||||
| **第 5 章** | 布局与重排 | 避免触发昂贵的布局计算 |
|
||||
| **第 6 章** | 绘制与重绘 | 减少不必要的绘制操作 |
|
||||
| **第 7 章** | 合成与GPU加速 | 利用GPU提升动画性能 |
|
||||
| **第 8 章** | 事件循环 | 理解JavaScript的执行机制 |
|
||||
| **第 9 章** | 性能优化实战 | 掌握常用的性能优化技巧 |
|
||||
|
||||
每一章都从"理解原理"开始,不需要你会手写优化代码。遇到性能问题时,随时回来查就行。
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要理解"渲染管线"?
|
||||
@@ -934,7 +949,41 @@ lazyImages.forEach(img => imageObserver.observe(img))
|
||||
|
||||
---
|
||||
|
||||
## 10. 总结:渲染管线优化的本质
|
||||
## 10. 你现在应该能识别的性能问题
|
||||
|
||||
理解了浏览器的渲染管线后,你应该能识别以下常见的性能问题:
|
||||
|
||||
| 问题代码 | 问题所在 | 如何描述给AI |
|
||||
|---------|---------|-------------|
|
||||
| `element.style.width = ...` | 在循环中频繁修改宽度 | "这里会触发多次重排,请改用transform或者批量处理" |
|
||||
| `height = element.offsetHeight` | 在写入后立即读取布局属性 | "这是强制同步布局,请分离读写操作" |
|
||||
| `element.className = ...` | 频繁修改class触发样式重新计算 | "用classList.add/remove代替,减少样式计算" |
|
||||
| 动画用`width`/`left` | 触发重排和重绘,性能差 | "改用transform和opacity做动画" |
|
||||
| 给所有元素加`translateZ(0)` | 滥用GPU加速导致内存爆炸 | "只给需要动画的元素开启GPU加速" |
|
||||
| 列表项10000个全渲染 | DOM节点过多导致卡顿 | "实现虚拟滚动,只渲染可见区域" |
|
||||
| scroll事件里直接操作DOM | 触发频率太高导致卡顿 | "用requestAnimationFrame或节流优化" |
|
||||
| `box-shadow`做hover动画 | 复杂的阴影计算很慢 | "改用transform或伪元素,避免动画阴影" |
|
||||
|
||||
**如果你认真读了每一章的"踩坑实录",你还掌握了这些核心概念:**
|
||||
|
||||
- **渲染管线五阶段**:DOM/CSSOM → 渲染树 → 布局 → 绘制 → 合成
|
||||
- **重排 vs 重绘**:重排最昂贵(几何变化),重绘次之(外观变化)
|
||||
- **强制同步布局**:读写交替会导致布局抖动,必须分离
|
||||
- **GPU加速**:transform和opacity由GPU处理,性能最佳
|
||||
- **事件循环**:JavaScript是单线程的,通过任务队列实现异步
|
||||
|
||||
这些概念会帮你快速定位性能瓶颈。
|
||||
|
||||
::: info 💡 遇到性能问题时这样跟AI说
|
||||
- "动画卡顿,检查是否触发了重排或重绘"
|
||||
- "滚动性能差,可能需要节流或requestAnimationFrame"
|
||||
- "列表数据量大时卡顿,需要虚拟滚动"
|
||||
- "频繁修改样式导致性能问题,请用transform优化"
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 11. 总结:渲染管线优化的本质
|
||||
|
||||
通过本文的学习,我们可以得出以下核心结论:
|
||||
|
||||
@@ -949,7 +998,7 @@ lazyImages.forEach(img => imageObserver.observe(img))
|
||||
|
||||
---
|
||||
|
||||
## 11. 名词对照表
|
||||
## 12. 名词对照表
|
||||
|
||||
| 英文术语 | 中文对照 | 解释 |
|
||||
| :--- | :--- | :--- |
|
||||
@@ -0,0 +1,544 @@
|
||||
# 浏览器是一个操作系统
|
||||
|
||||
::: tip 前言
|
||||
你每天都在用浏览器——看视频、刷新闻、在线办公。但你有没有想过:**当你在地址栏输入一个网址并按下回车,背后发生了什么?**
|
||||
|
||||
这篇文章会用**"网购"**的生活化比喻,配合**真实的技术过程**,带你一步步理解浏览器如何将一行网址变成丰富多彩的页面。
|
||||
|
||||
读完这篇,你就能:
|
||||
- 理解从输入网址到显示页面的完整流程
|
||||
- 掌握 URL、DNS、TCP、HTTP 等核心概念
|
||||
- 了解浏览器如何渲染页面
|
||||
- 知道静态网站和动态网站的区别
|
||||
|
||||
**无需编程基础**,只需要你平时网购的经验即可。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
| 章节 | 内容 | 核心概念 |
|
||||
|-----|------|---------|
|
||||
| **第 1 章** | URL 解析 | 网址的结构和作用 |
|
||||
| **第 2 章** | DNS 查询 | 域名如何转换成 IP 地址 |
|
||||
| **第 3 章** | TCP 握手 | 如何建立可靠的连接 |
|
||||
| **第 4 章** | HTTP 通信 | 浏览器和服务器如何对话 |
|
||||
| **第 5 章** | 浏览器渲染 | 代码如何变成画面 |
|
||||
| **第 6 章** | 静态 vs 动态 | 网页内容的生成方式 |
|
||||
|
||||
---
|
||||
|
||||
## 0. 引言:当你按下回车键的那一刻
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**当你在浏览器输入网址并按下回车,后台发生了什么?** 为什么有的网页打开很快,有的很慢?为什么有时候会出现"找不到服务器"的错误?
|
||||
:::
|
||||
|
||||
### 生活比喻:一次网购之旅
|
||||
|
||||
想象你正在进行一次**网购**。整个过程可以分为 5 个步骤:
|
||||
|
||||
<div style="display: flex; gap: 20px; margin: 20px 0;">
|
||||
<div style="flex: 1; padding: 16px; background: var(--vp-c-bg-alt); border-radius: 12px;">
|
||||
|
||||
**🛒 第 1 步:填写订单**
|
||||
选好商品,确认收货地址
|
||||
|
||||
</div>
|
||||
<div style="flex: 1; padding: 16px; background: var(--vp-c-bg-alt); border-radius: 12px;">
|
||||
|
||||
**🗺️ 第 2 步:查找仓库**
|
||||
系统找到具体的发货仓库
|
||||
|
||||
</div>
|
||||
<div style="flex: 1; padding: 16px; background: var(--vp-c-bg-alt); border-radius: 12px;">
|
||||
|
||||
**📞 第 3 步:建立通道**
|
||||
确认仓库营业且能发货
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin: 20px 0;">
|
||||
<div style="flex: 1; padding: 16px; background: var(--vp-c-bg-alt); border-radius: 12px;">
|
||||
|
||||
**🚚 第 4 步:仓库发货**
|
||||
快递员把包裹送上门
|
||||
|
||||
</div>
|
||||
<div style="flex: 1; padding: 16px; background: var(--vp-c-bg-alt); border-radius: 12px;">
|
||||
|
||||
**🎁 第 5 步:拆箱体验**
|
||||
打开包裹,看到心仪的商品
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
**访问网页的过程和网购惊人地相似!**
|
||||
|
||||
当你在浏览器输入 `google.com` 并按下回车,你就是那个"买家",浏览器通过一系列操作,最终把远方服务器上的"商品"(网页内容)送到你的屏幕上。
|
||||
|
||||
<UrlToBrowserQuickStart />
|
||||
|
||||
::: info 💡 核心启示
|
||||
理解浏览器工作原理的关键是:**把复杂的技术过程映射到熟悉的生活场景**。网购的 5 个步骤完美对应了浏览器访问网页的 5 个技术阶段。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 第一步:填写"订单" —— URL 解析
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**为什么网址要写成这样?** `https://www.example.com:8080/path/page.html?id=123#section` — 这串字符到底有什么含义?
|
||||
:::
|
||||
|
||||
### 生活比喻:填写购物单
|
||||
|
||||
假设你只在订单上写"买鞋子",仓库肯定不知道发哪双。你需要写清楚:
|
||||
|
||||
- **店铺类型**(官方旗舰店/普通店)
|
||||
- **店铺名称**(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 />
|
||||
|
||||
::: info 💡 关键理解
|
||||
URL 的存在是为了让**人类**能记住和输入。计算机最终需要的是 **IP 地址**(就像快递员最终需要的是具体的仓库地址,而不是"Nike 官方店"这个名字)。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 第二步:查"地址簿" —— DNS 查询
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**为什么浏览器能找到网站?** 你输入的是人类可读的域名(如 `baidu.com`),但计算机真正需要的是数字地址(IP)。这中间发生了什么?
|
||||
:::
|
||||
|
||||
### 生活比喻:查仓库地址
|
||||
|
||||
你下单写的是"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 />
|
||||
|
||||
::: info 💡 为什么需要这么多层?
|
||||
想象一下如果全世界只有一个地址簿,几十亿人同时查,早就崩溃了。分层设计让每个层级只管理自己的"辖区",既高效又可靠。
|
||||
|
||||
这就是互联网设计的核心思想:**分布式系统**。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 第三步:打电话确认 —— TCP 三次握手
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**为什么需要"三次握手"?** 找到服务器地址后,为什么不能直接发送数据?为什么要先进行三次通信?
|
||||
:::
|
||||
|
||||
### 生活比喻:建立物流通道
|
||||
|
||||
假设物流车直接开到仓库,结果:
|
||||
|
||||
- 仓库关门了 → 白跑一趟
|
||||
- 仓库爆仓不接单 → 无法发货
|
||||
- 找不到卸货口 → 无法对接
|
||||
|
||||
**所以在真正发货之前,必须先建立可靠的运输通道**。
|
||||
|
||||
### 真实过程: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 请求与响应
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**浏览器和服务器在说什么?** 建立连接后,浏览器如何"告诉"服务器它想要什么?服务器又如何"回应"?
|
||||
:::
|
||||
|
||||
### 生活比喻:仓库发货
|
||||
|
||||
物流车到达仓库:"这是订单(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. 第五步:拆开"包裹" —— 浏览器渲染
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**代码怎么变成画面?** 服务器发来的是枯燥的 HTML/CSS/JavaScript 代码,浏览器如何把它们变成丰富多彩的网页?
|
||||
:::
|
||||
|
||||
### 生活比喻:拆箱与组装
|
||||
|
||||
你终于收到了快递包裹(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 />
|
||||
|
||||
::: info 💡 你知道吗?
|
||||
**布局和绘制**是浏览器最忙碌的时候。网页里的元素越多、结构越复杂,浏览器就需要花更多时间来计算位置和上色。这就是为什么有的复杂网页打开会卡顿的原因。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5.5 网页是怎么"生成"的?静态网站 vs 动态网站
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**网页内容从哪里来?** 前面我们讲了浏览器如何渲染页面,但服务器上的 HTML 文件是怎么来的?是提前做好还是现做?
|
||||
:::
|
||||
|
||||
前面我们讲的都是浏览器如何"拆开包裹"——把服务器发来的 HTML/CSS/JS 渲染成页面。但你有没有想过一个问题:**服务器上那个 HTML 文件是怎么来的?**
|
||||
|
||||
答案是:**有两种方式**,这就是静态网站和动态网站的区别。
|
||||
|
||||
### 静态网站:提前做好、直接给你
|
||||
|
||||
想象你去超市买饼干。货架上的饼干都是工厂已经生产好的,你直接拿走就行,不需要等。
|
||||
|
||||
**静态网站**就是这样的"成品"——网页在服务器上已经准备好了,你访问时服务器直接把现成的 HTML 文件发给你,不做任何额外处理。
|
||||
|
||||
**特点:**
|
||||
- ✅ 访问速度快(服务器直接发文件,不用计算)
|
||||
- ✅ 制作简单(写好 HTML 就能用)
|
||||
- ✅ 承载力强(可以用 CDN 分发,多少人访问都不怕)
|
||||
- ❌ 内容难更新(想改内容就要重新生成文件)
|
||||
|
||||
**常见例子:** 公司介绍页、产品文档、帮助中心、个人博客
|
||||
|
||||
### 动态网站:现点现做、每次不同
|
||||
|
||||
这次想象你去餐厅点餐。厨师根据你的订单现做,你点宫保鸡丁不会给你上糖醋里脊。
|
||||
|
||||
**动态网站**就是你访问时才"现场制作"的页面——服务器收到你的请求后,去数据库查资料、计算数据,然后生成一个全新的 HTML 发给你。
|
||||
|
||||
**特点:**
|
||||
- ✅ 内容实时(购物车显示最新库存、新闻随时更新)
|
||||
- ✅ 因人而异(登录后看到你的个人信息)
|
||||
- ✅ 功能强大(搜索、评论、推荐、支付都能实现)
|
||||
- ❌ 访问速度慢(服务器需要时间计算)
|
||||
- ❌ 服务器压力大(同时很多人访问要排队)
|
||||
|
||||
**常见例子:** 淘宝、微博、在线银行、在线文档
|
||||
|
||||
**需要服务器吗?** 动态网站确实需要某种"后端"来生成内容,但形式多样:
|
||||
- **传统服务器**:自己买/租服务器(阿里云 ECS、AWS EC2)
|
||||
- **Serverless**:不用管服务器,云厂商帮你运行代码(AWS Lambda、阿里云函数计算、Cloudflare Workers)
|
||||
- **调用第三方 API**:支付用 Stripe、天气用气象局 API,自己不写后端代码
|
||||
|
||||
::: tip 💡 静动态结合
|
||||
现在很多网站是"混合"的:网页主体是静态的,但某些部分(比如评论区、搜索框)是动态加载的。JavaScript 可以在页面加载后调用 API 获取数据,实现"静态页面 + 动态功能"。
|
||||
:::
|
||||
|
||||
### 📊 静态 vs 动态,一对比就清楚
|
||||
|
||||
| | 静态网站 | 动态网站 |
|
||||
|---|---------|---------|
|
||||
| **怎么来的** | 提前做好,存服务器上 | 访问时现做 |
|
||||
| **像什么** | 超市货架上的商品 | 餐厅现点的菜 |
|
||||
| **速度** | 快 | 慢(需要计算) |
|
||||
| **能改内容吗** | 难(要重新生成) | 容易(后台直接改) |
|
||||
| **适合做什么** | 展示型内容(介绍页、文档) | 交互型应用(购物、社交) |
|
||||
| **典型例子** | 公司官网、帮助文档 | 淘宝、微信、在线银行 |
|
||||
|
||||
### 🤔 常见疑问
|
||||
|
||||
**Q: 静态网站是不是不能用 JavaScript?**
|
||||
|
||||
当然不是!轮播图、折叠菜单、表单验证这些交互功能,静态网站都能用 JavaScript 实现。我们说的"静态""动态",是指**页面内容是不是提前准备好的**,跟有没有交互功能是两回事。
|
||||
|
||||
**Q: 动态网站一定要自己买服务器吗?**
|
||||
|
||||
不一定。除了传统服务器,你还可以用 Serverless(云函数)、或者直接调用第三方 API。现在的趋势是"能不动服务器就不动"——用静态网站 + JavaScript 调用 API 的方式,既快又省成本。
|
||||
|
||||
::: tip 💡 重要提示
|
||||
无论静态网站还是动态网站,**浏览器渲染的原理都是一样的**!服务器发来的是什么,浏览器就渲染什么。区别只在于:
|
||||
- 静态网站:服务器发来的是"成品"
|
||||
- 动态网站:服务器发来的是"现做的"
|
||||
|
||||
作为前端开发者,你主要关注的是浏览器如何处理收到的内容,而不是服务器怎么生成的。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结:一次完整的"网购"之旅
|
||||
|
||||
::: tip 🎉 学完本章,你应该能
|
||||
- 解释从输入网址到显示页面的完整流程
|
||||
- 理解 URL、DNS、TCP、HTTP 的作用和关系
|
||||
- 知道浏览器如何渲染页面
|
||||
- 区分静态网站和动态网站
|
||||
- 用生活化比喻向他人解释浏览器工作原理
|
||||
:::
|
||||
|
||||
让我们回顾整个旅程:
|
||||
|
||||
| 阶段 | 技术术语 | 网购类比 | 核心任务 | 关键技术 |
|
||||
| ----------- | ---------- | -------- | ------------------ | ------------------------------ |
|
||||
| **1. 解析** | URL 解析 | 填写订单 | 理解买家想买什么 | 协议、域名、端口、路径、参数 |
|
||||
| **2. 查询** | DNS 查询 | 查仓库址 | 找到店铺的发货仓库 | 递归/迭代查询、缓存机制 |
|
||||
| **3. 连接** | TCP 握手 | 建立通道 | 确保物流通畅 | 三次握手、序列号、流量控制 |
|
||||
| **4. 对话** | HTTP 交换 | 仓库发货 | 提交订单并收货 | 请求方法、状态码、头部字段 |
|
||||
| **5. 展示** | 浏览器渲染 | 拆箱组装 | 把商品展示出来 | DOM、CSSOM、渲染树、布局、绘制 |
|
||||
|
||||
**整个过程通常在几百毫秒内完成** —— 想想这有多么不可思议!
|
||||
|
||||
你的浏览器在不到1秒的时间里:
|
||||
|
||||
- 解析了一个复杂的地址
|
||||
- 查询了分布在全球的 DNS 服务器
|
||||
- 和千里之外的服务器建立了可靠连接
|
||||
- 进行了一次完整的 HTTP 对话
|
||||
- 把枯燥的代码变成了精美的画面
|
||||
|
||||
这就是互联网的魅力:**复杂的技术,简单的体验**。
|
||||
|
||||
::: info 💡 进阶学习
|
||||
如果你想深入了解某个环节,可以参考:
|
||||
- **API 开发**:[API 简介](./api-intro.md) - 学习如何设计和使用 API
|
||||
- **前端性能**:[前端性能优化](./frontend-performance.md) - 学习如何优化网页加载速度
|
||||
- **浏览器渲染**:[浏览器渲染管道](./browser-rendering-pipeline.md) - 深入了解渲染细节
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 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 | **往返时间**。数据包从发送到接收确认的时间,影响网页加载速度。 |
|
||||
|
||||
---
|
||||
|
||||
::: tip 🎓 恭喜
|
||||
现在当你再次在地址栏输入网址并按下回车时,你已经能看到屏幕背后的那个忙碌而精彩的数字世界了。
|
||||
|
||||
你理解了:
|
||||
- 为什么有时候网页打不开(DNS 解析失败、服务器宕机)
|
||||
- 为什么有的网页快、有的慢(网络延迟、服务器性能、页面复杂度)
|
||||
- 浏览器是如何把代码变成画面的(渲染管道)
|
||||
|
||||
**这就是理解技术原理的价值** — 遇到问题时,你能知道从哪里找原因,而不是束手无策。
|
||||
:::
|
||||
:::
|
||||
@@ -1,5 +1,4 @@
|
||||
# 前端工程化与构建流水线
|
||||
|
||||
# 前端工程化全貌
|
||||
::: tip 🎯 核心问题
|
||||
**如何把你写的代码,变成用户浏览器能跑的网站?** 这就像是问:如何把原材料变成成品,还要保证质量、控制成本?本章将带你深入理解前端工程化的核心概念和构建流程。
|
||||
:::
|
||||
@@ -0,0 +1,3 @@
|
||||
# 前端框架的本质
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,704 @@
|
||||
# 前端框架深度指南
|
||||
|
||||
::: tip 前言
|
||||
你已经学会了 HTML、CSS 和 JavaScript 基础,能做出简单的网页了。但随着网页功能越来越复杂,你可能会发现:用原生 JavaScript 写代码变得很难维护,改一处要动很多地方,多人协作时经常冲突。
|
||||
|
||||
这就是我们需要前端框架的原因——它让代码更有条理、更易维护、更高效开发。在 vibecoding 里,AI 会帮你写大部分代码。但你至少得能看懂不同框架的代码风格,知道它们的优缺点,这样 AI 才能帮你选择最合适的技术栈。
|
||||
|
||||
读完这篇,你就能:
|
||||
- 理解前端技术为什么要不断演进
|
||||
- 知道 Vue、React、Svelte、Angular 各有什么特点
|
||||
- 懂得"数据驱动"、"组件化"这些核心概念
|
||||
- 能根据项目选择合适的框架
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
| 章节 | 内容 | 学完能干嘛 |
|
||||
|-----|------|-----------|
|
||||
| **第 1 章** | 为什么要关注前端演进 | 明白技术演进是为了解决什么问题 |
|
||||
| **第 2 章** | 静态网页时代 | 了解最早期的网页开发方式 |
|
||||
| **第 3 章** | jQuery 时代 | 理解"命令式"编程的痛点 |
|
||||
| **第 4 章** | Vue/React 时代 | 掌握"声明式"和"数据驱动"思想 |
|
||||
| **第 5 章** | 渲染策略 | 知道 CSR、SSR、SSG 的区别和适用场景 |
|
||||
| **第 6 章** | 工程化工具 | 理解 Webpack、Vite 等构建工具的作用 |
|
||||
|
||||
每一章都从"为什么需要这个技术"开始,让你理解技术演进背后的逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要关注前端演进史?
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**为什么网页越来越复杂?前端技术为什么要不断演进?** 这个问题会带你理解从简单网页到现代 Web 应用的技术演变之路。
|
||||
:::
|
||||
|
||||
### 1.1 从"电子海报"到"桌面应用"
|
||||
|
||||
想象一下你在街上看到的**海报**:
|
||||
|
||||
- ✅ 有内容(文字、图片)
|
||||
- ✅ 有设计(颜色、排版)
|
||||
- ❌ 但你跟它说话,它不会回应
|
||||
- ❌ 你点击某个地方,不会发生什么
|
||||
|
||||
**最早的网页**就是这样的"电子海报":只能看、不能改、内容固定。
|
||||
|
||||
**现代网页**完全不同了。它们像**桌面应用**(VS Code、Figma):
|
||||
|
||||
- ✅ 可以编辑文档、画图、玩游戏
|
||||
- ✅ 实时响应你的每个操作
|
||||
- ✅ 甚至可以离线工作
|
||||
|
||||
**这种转变的核心原因:网页的功能越来越复杂,需要更高效的技术和开发方式。**
|
||||
|
||||
### 1.2 一个生活的比喻:盖房子
|
||||
|
||||
前端技术的演进,就像盖房子方式的进化:
|
||||
|
||||
| 时代 | 🏠 盖房比喻 | 实际特点 | 优缺点 |
|
||||
|------|-----------|---------|--------|
|
||||
| **2000s** | **贴海报** | 静态网页,写好 HTML 就行 | ✅ 简单 ❌ 不能互动 |
|
||||
| **2010s** | **请工人手动装修** | jQuery 时代,手动操作每个元素 | ✅ 能互动 ❌ 代码乱、难维护 |
|
||||
| **2020s** | **用乐高搭房子** | Vue/React 时代,组件化开发 | ✅ 高效、可维护 ❌ 学习曲线 |
|
||||
|
||||
::: tip 💡 从表格中你能看到什么?
|
||||
|
||||
**阶段一 → 阶段二**:从"不能动"到"能动"。这是质的飞跃——网页开始有交互,但代价是代码变得混乱。
|
||||
|
||||
**阶段二 → 阶段三**:从"能用"到"好用"。组件化让代码像积木一样可复用,大幅提升开发效率。
|
||||
|
||||
**核心思想**:技术演进不是"为了新而新",而是为了解决上一个阶段的痛点。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 2. 第一阶段:静态网页与"切图"(2000s)
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**最早的网页是什么样的?为什么那时候不需要框架?** 理解这个阶段的局限性,才能明白后来技术演进的必要性。
|
||||
:::
|
||||
|
||||
<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 "切图"是什么?
|
||||
|
||||
你可能听说过"切图"这个词。它是早期前端的主要工作:
|
||||
|
||||
**什么是切图?**
|
||||
|
||||
设计师用 Photoshop 设计好页面 → 前端把设计切成小图片 → 用 HTML 把图片拼成页面
|
||||
|
||||
**为什么这么慢?**
|
||||
|
||||
网页上的每张小图片,浏览器都要发一次**网络请求**。请求越多,加载越慢。
|
||||
|
||||
👇 **动手试试看**:观察图片请求对加载性能的影响
|
||||
|
||||
<SliceRequestDemo />
|
||||
|
||||
::: tip 💡 雪碧图(Sprite)
|
||||
|
||||
为了减少请求数,出现了"雪碧图"技术:把很多小图合成一张大图。
|
||||
|
||||
优点是请求数变少,缺点是制作和维护都很麻烦。
|
||||
|
||||
这个阶段的教训:**请求太多是性能大敌**。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 3. 第二阶段:jQuery 时代 - "手动搬砖"(2010s)
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**为什么需要 jQuery?它解决了什么问题,又带来了什么新问题?** 理解 jQuery 的局限性,才能明白 Vue/React 的价值。
|
||||
:::
|
||||
|
||||
### 3.1 为什么需要 jQuery?
|
||||
|
||||
随着网页变复杂,原生 JavaScript 的问题暴露出来:
|
||||
|
||||
- ❌ **API 繁琐**:简单的操作也要写很多代码
|
||||
- ❌ **浏览器兼容**:不同浏览器的 API 不一样,要写很多兼容代码
|
||||
- ❌ **选择器弱**:找元素很麻烦
|
||||
|
||||
**jQuery** 诞生了。它让 JavaScript 变得简单:
|
||||
|
||||
```javascript
|
||||
// 原生 JavaScript(繁琐)
|
||||
const element = document.getElementById('title')
|
||||
|
||||
// jQuery(简洁)
|
||||
const element = $('#title')
|
||||
```
|
||||
|
||||
### 3.2 jQuery 的思路:亲手改页面
|
||||
|
||||
jQuery 的核心思路是**命令式**:你告诉浏览器"怎么做"。
|
||||
|
||||
```javascript
|
||||
// 找到标题元素
|
||||
$('#title').text('新标题')
|
||||
|
||||
// 找到按钮并禁用
|
||||
$('#submit-btn').attr('disabled', true)
|
||||
|
||||
// 找到列表并添加一项
|
||||
$('ul').append('<li>新项目</li>')
|
||||
```
|
||||
|
||||
**问题**:你需要记住页面上有哪些元素,每次数据变化都要手动更新所有相关元素。
|
||||
|
||||
👇 **动手试试看**:对比 jQuery 和数据驱动的方式
|
||||
|
||||
<JQueryVsStateDemo />
|
||||
|
||||
::: warning ⚠️ jQuery 的痛点
|
||||
|
||||
想象你在做一个购物车:
|
||||
|
||||
```javascript
|
||||
// 用户点击"添加到购物车"
|
||||
function addToCart() {
|
||||
cartCount++ // 数据变化
|
||||
|
||||
// 你要手动更新所有相关地方
|
||||
$('#cart-count').text(cartCount) // 右上角小红点
|
||||
$('#cart-page-count').text(cartCount) // 购物车页面
|
||||
$('#checkout-price').text(calculatePrice()) // 结算按钮
|
||||
|
||||
// 如果漏了一个地方,页面就不一致了!
|
||||
}
|
||||
```
|
||||
|
||||
**这就是"手动搬砖"的代价**:容易出错,难以维护。
|
||||
:::
|
||||
|
||||
### 3.3 移动端普及:响应式设计的出现
|
||||
|
||||
这个阶段还有一个重要变化:**手机和平板开始流行**。
|
||||
|
||||
网页必须适配不同屏幕。这需要**响应式布局**:同一套 HTML/CSS,自动根据屏幕宽度变换布局。
|
||||
|
||||
**响应式布局的核心:媒体查询(Media Query)**
|
||||
|
||||
```css
|
||||
/* 电脑屏幕(大于 640px) */
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机屏幕(小于 640px) */
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
👇 **动手试试看**:调整浏览器宽度,观察响应式布局的效果
|
||||
|
||||
<ResponsiveGridDemo />
|
||||
|
||||
::: tip 💡 响应式就像"智能相框"
|
||||
|
||||
想象你在不同房间看同一张照片:
|
||||
|
||||
- 在**大客厅**(电脑屏幕),照片可以摆大一些,旁边还能放其他装饰品
|
||||
- 在**小卧室**(手机屏幕),照片需要缩小,其他装饰品要收起来
|
||||
|
||||
**响应式布局**就是"智能相框",它会自动根据房间大小调整展示方式。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 4. 第三阶段:从"手动搬砖"到"数据驱动"(Vue/React)
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**为什么需要 Vue/React?它们和 jQuery 的本质区别是什么?** 理解"声明式"和"数据驱动",是掌握现代前端框架的关键。
|
||||
:::
|
||||
|
||||
### 4.1 为什么需要新框架?
|
||||
|
||||
jQuery 时代的问题积累到一定程度:
|
||||
|
||||
- **代码一多就乱**:到处都是 DOM 操作,难以维护
|
||||
- **容易出 bug**:漏更新一个地方,页面就不一致
|
||||
- **协作困难**:多人修改同一个文件,容易冲突
|
||||
|
||||
**Vue / React** 的核心思路:**只改数据,页面自动更新**。
|
||||
|
||||
### 4.2 Vue/React 的思路:声明式 UI
|
||||
|
||||
**jQuery(命令式)**:
|
||||
|
||||
```javascript
|
||||
// 你要告诉浏览器每一步怎么做
|
||||
$('#title').text('新标题')
|
||||
$('#title').css('color', 'red')
|
||||
$('#title').show()
|
||||
```
|
||||
|
||||
**Vue(声明式)**:
|
||||
|
||||
```javascript
|
||||
// 你只需告诉浏览器"要显示什么"
|
||||
data() {
|
||||
return {
|
||||
title: "新标题",
|
||||
color: "red",
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
👇 **动手试试看**:对比命令式和声明式的区别
|
||||
|
||||
<ImperativeVsDeclarativeDemo />
|
||||
|
||||
::: tip 💡 命令式 vs 声明式
|
||||
|
||||
就像画一幅画:
|
||||
|
||||
- **命令式**:你告诉画家"拿起笔,蘸红颜料,在坐标(10,10)画一个圈"
|
||||
- **声明式**:你直接给画家一张照片,"给我画成这样"
|
||||
|
||||
Vue/React 就是"声明式":你描述"页面长什么样",框架负责"怎么把它画出来"。
|
||||
:::
|
||||
|
||||
### 4.3 组件化:像搭乐高一样写页面
|
||||
|
||||
**Vue / React** 最强大的特性是**组件化**:把页面拆成一个个独立的"积木"。
|
||||
|
||||
想象一下你在搭乐高:
|
||||
|
||||
- 你不需要"从头开始雕刻每一块积木"(从头写 HTML/CSS)
|
||||
- 你只需要"按说明书把积木拼在一起"(把组件组合起来)
|
||||
- 每个积木都是**独立的**,你可以在不同的套装里**重复使用**
|
||||
|
||||
**组件的好处**:
|
||||
|
||||
- **复用**:写一个"商品卡片"组件,可以用 100 次
|
||||
- **封装**:组件内部的状态不影响别人
|
||||
- **维护**:修改一个组件,所有用到它的地方都会更新
|
||||
|
||||
::: info 💡 识别技巧
|
||||
- 看到 `<ComponentName />` → 这是一个组件
|
||||
- 看到 `import xxx from './xxx.vue'` → 在导入一个组件
|
||||
- 看到 `props: {...}` → 组件接收的参数
|
||||
- 看到 `emit('xxx')` → 组件向父组件发送事件
|
||||
:::
|
||||
|
||||
### 4.4 SPA:单页应用的诞生
|
||||
|
||||
**Vue / React** 时代还有一个重要变化:**从 MPA 到 SPA**。
|
||||
|
||||
**MPA(Multi-Page Application)**:
|
||||
|
||||
- 点一个链接 → 整页刷新 → 显示新页面
|
||||
- 就像**翻书**:每翻一页都要把旧书合上、去书架拿新书
|
||||
|
||||
**SPA(Single-Page Application)**:
|
||||
|
||||
- 点一个链接 → 只刷新内容区域 → 页面不刷新
|
||||
- 就像**同一本书里换章节**:只擦掉旧内容、写上新内容
|
||||
|
||||
👇 **动手试试看**:体验 MPA 和 SPA 的区别
|
||||
|
||||
<RoutingModeDemo />
|
||||
|
||||
**SPA 的优点**:
|
||||
|
||||
- ✅ **体验丝滑**:页面切换快
|
||||
- ✅ **状态好管理**:输入的内容、滚动位置都在
|
||||
- ❌ **首屏可能慢**:需要先下载 JavaScript
|
||||
- ❌ **SEO 要额外处理**:搜索引擎可能抓不到内容(需要 SSR/SSG)
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 5. 渲染策略:从 CSR 到 SSR/SSG
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**页面是在服务器生成,还是在浏览器生成?** 不同渲染策略各有优劣,选择合适的策略对性能和 SEO 至关重要。
|
||||
:::
|
||||
|
||||
**CSR(Client-Side Rendering)客户端渲染**:
|
||||
|
||||
- 浏览器下载 JavaScript → 执行代码 → 生成页面
|
||||
- 优点:交互流畅,服务器压力小
|
||||
- 缺点:首屏慢,不利于 SEO
|
||||
|
||||
**SSR(Server-Side Rendering)服务端渲染**:
|
||||
|
||||
- 服务器生成 HTML → 发给浏览器 → 浏览器直接显示
|
||||
- 优点:首屏快,利于 SEO
|
||||
- 缺点:服务器压力大,实现复杂
|
||||
|
||||
**SSG(Static Site Generation)静态站点生成**:
|
||||
|
||||
- 构建时生成所有页面的 HTML
|
||||
- 优点:极快,完全静态,CDN 友好
|
||||
- 缺点:不适合动态内容
|
||||
|
||||
👇 **动手试试看**:对比不同渲染策略的特点
|
||||
|
||||
<RenderingStrategyDemo />
|
||||
|
||||
::: info 💡 如何选择?
|
||||
- **内容网站**(博客、文档):优先 SSG
|
||||
- **需要 SEO 的动态网站**(电商、新闻):使用 SSR
|
||||
- **后台管理系统**:使用 CSR
|
||||
- **混合需求**:考虑 Nuxt/Next.js 的混合渲染
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. 第四阶段:工程化与构建工具(2015s-2020s)
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**为什么前端需要"工程化"?构建工具到底在做什么?** 理解工程化,才能看懂现代前端项目的工作流程。
|
||||
:::
|
||||
|
||||
### 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. 主流框架对比
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**Vue、React、Svelte、Angular 各有什么特点?如何选择适合自己的框架?** 了解它们的设计理念和使用场景,才能做出明智的选择。
|
||||
:::
|
||||
|
||||
### 7.1 四大框架对比
|
||||
|
||||
| 特性 | Vue | React | Svelte | Angular |
|
||||
|------|-----|-------|--------|---------|
|
||||
| **设计理念** | 渐进式框架 | UI 库 | 编译时框架 | 完整平台 |
|
||||
| **学习曲线** | ⭐⭐ 简单 | ⭐⭐⭐ 中等 | ⭐⭐ 简单 | ⭐⭐⭐⭐ 陡峭 |
|
||||
| **性能** | 快 | 快 | **极快** | 快 |
|
||||
| **生态系统** | 完善 | **最完善** | 成长中 | 完善 |
|
||||
| **包大小** | 小 | 中等 | **最小** | 大 |
|
||||
| **适合场景** | 中小型项目 | 大型项目 | 性能要求高 | 企业级应用 |
|
||||
| **公司支持** | 尤雨溪(独立) | Meta | 社区 | Google |
|
||||
|
||||
### 7.2 Vue:渐进式框架
|
||||
|
||||
**核心理念**:渐进式采用,可以只用一部分,也可以用全家桶
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>{{ message }}</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
message: 'Hello Vue'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- ✅ 学习曲线平缓,中文文档完善
|
||||
- ✅ 模板语法直观,易于理解
|
||||
- ✅ 单文件组件(.vue)结构清晰
|
||||
- ✅ 适合快速开发
|
||||
|
||||
**缺点**:
|
||||
- ❌ 大型项目的状态管理需要额外学习 Vuex/Pinia
|
||||
- ❌ 灵活性略逊于 React
|
||||
|
||||
**适用场景**:
|
||||
- 中小型 Web 应用
|
||||
- 快速原型开发
|
||||
- 中文团队(文档友好)
|
||||
|
||||
### 7.3 React:UI 库
|
||||
|
||||
**核心理念**:只负责视图层,其他问题交给社区
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
const [message, setMessage] = useState('Hello React')
|
||||
return <div>{message}</div>
|
||||
}
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- ✅ 生态系统最完善,组件库丰富
|
||||
- ✅ JSX 语法灵活,表达能力强大
|
||||
- ✅ 虚拟 DOM 性能优秀
|
||||
- ✅ 适合大型项目
|
||||
|
||||
**缺点**:
|
||||
- ❌ 学习曲线较陡,需要掌握额外概念
|
||||
- ❌ 需要自己选择和搭配各种库
|
||||
- ❌ JSX 需要编译,不能直接在浏览器运行
|
||||
|
||||
**适用场景**:
|
||||
- 大型复杂应用
|
||||
- 需要丰富生态的项目
|
||||
- 跨平台开发(React Native)
|
||||
|
||||
### 7.4 Svelte:编译时框架
|
||||
|
||||
**核心理念**:没有虚拟 DOM,编译时将组件转换为高效的原生代码
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let message = 'Hello Svelte'
|
||||
</script>
|
||||
|
||||
<div>{message}</div>
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- ✅ **性能最优**(无虚拟 DOM 运行时开销)
|
||||
- ✅ 包体积最小
|
||||
- ✅ 语法简单直观
|
||||
- ✅ 响应式系统天然支持
|
||||
|
||||
**缺点**:
|
||||
- ❌ 生态相对较小
|
||||
- ❌ 社区规模不如 Vue/React
|
||||
- ❌ 第三方库较少
|
||||
|
||||
**适用场景**:
|
||||
- 性能要求极高的应用
|
||||
- 包体积敏感的项目
|
||||
- 愿意尝试新技术的团队
|
||||
|
||||
### 7.5 Angular:完整平台
|
||||
|
||||
**核心理念**:提供完整的解决方案,开箱即用
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: '<div>{{ message }}</div>'
|
||||
})
|
||||
export class AppComponent {
|
||||
message = 'Hello Angular'
|
||||
}
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- ✅ 功能完整,路由、HTTP、表单全都有
|
||||
- ✅ TypeScript 原生支持
|
||||
- ✅ 适合大型团队和项目
|
||||
- ✅ 代码规范统一
|
||||
|
||||
**缺点**:
|
||||
- ❌ 学习曲线陡峭
|
||||
- ❌ 概念多,复杂度高
|
||||
- ❌ 包体积大
|
||||
- ❌ 不适合小型项目
|
||||
|
||||
**适用场景**:
|
||||
- 大型企业级应用
|
||||
- 需要严格规范的团队
|
||||
- 已有 TypeScript 技术栈的项目
|
||||
|
||||
---
|
||||
|
||||
## 8. 总结:演进的本质
|
||||
|
||||
前端技术的演进,本质上是在解决两个问题:
|
||||
|
||||
### 8.1 效率:从手动到自动
|
||||
|
||||
| 时代 | 开发方式 | 效率 |
|
||||
|------|---------|------|
|
||||
| **2000s** | 手写 HTML/CSS/JS | ⭐ |
|
||||
| **2010s** | jQuery + 手动 DOM 操作 | ⭐⭐ |
|
||||
| **2020s** | Vue/React + 数据驱动 | ⭐⭐⭐ |
|
||||
| **现在** | 组件化 + 工程化 + 自动化 | ⭐⭐⭐⭐⭐ |
|
||||
|
||||
### 8.2 规模:从个人到团队
|
||||
|
||||
| 时代 | 项目规模 | 协作方式 |
|
||||
|------|---------|---------|
|
||||
| **2000s** | 几个文件 | 单人就能维护 |
|
||||
| **2010s** | 几十个文件 | 小团队,容易冲突 |
|
||||
| **2020s** | 几百个文件 | 中团队,需要规范 |
|
||||
| **现在** | 几千个文件 | 大团队,需要完整工程体系 |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 9. 学习路线图
|
||||
|
||||
### 9.1 如果你是零基础
|
||||
|
||||
**第 1 步:HTML/CSS/JavaScript 基础**
|
||||
|
||||
- 理解网页的三大基石
|
||||
- 能写出简单的静态页面
|
||||
|
||||
**第 2 步:学习一个框架(Vue 推荐)**
|
||||
|
||||
- 理解"数据驱动"的思想
|
||||
- 掌握组件化开发
|
||||
|
||||
**第 3 步:实战项目**
|
||||
|
||||
- 做一个完整的单页应用
|
||||
- 熟悉路由、状态管理、API 调用
|
||||
|
||||
### 9.2 如果你有基础
|
||||
|
||||
**进阶方向**:
|
||||
|
||||
- **工程化**:学习 Vite/Webpack,理解构建流程
|
||||
- **性能优化**:学习懒加载、代码分割、缓存策略
|
||||
- **TypeScript**:为代码加上类型,提升可靠性
|
||||
- **服务端渲染**:学习 Nuxt/Next.js,解决 SEO 和首屏问题
|
||||
|
||||
---
|
||||
|
||||
## 10. 你现在应该能识别的代码
|
||||
|
||||
通过阅读本章,你应该能够:
|
||||
|
||||
- ✅ 理解前端技术演进的脉络和原因
|
||||
- ✅ 区分 Vue、React、Svelte、Angular 的特点
|
||||
- ✅ 理解"命令式"和"声明式"的区别
|
||||
- ✅ 掌握"数据驱动"的核心思想
|
||||
- ✅ 知道组件化开发的价值
|
||||
- ✅ 了解 CSR、SSR、SSG 的适用场景
|
||||
- ✅ 理解构建工具(Webpack、Vite)的作用
|
||||
- ✅ 能根据项目选择合适的框架和技术栈
|
||||
|
||||
::: info 💡 实际应用
|
||||
当你用 AI 做项目时,你可以这样告诉它:
|
||||
|
||||
- "这是一个需要 SEO 的博客网站,用 Nuxt(Vue 的 SSR 框架)"
|
||||
- "这是一个后台管理系统,用 Vue + Element Plus,不需要 SSR"
|
||||
- "这是一个性能要求高的 Web 应用,考虑使用 Svelte"
|
||||
- "项目已经用 React 了,继续用 React 生态的库"
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 名词速查表
|
||||
|
||||
| 名词 | 英文 | 用人话解释 |
|
||||
|------|------|-----------|
|
||||
| **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。 |
|
||||
| **CSR** | Client-Side Rendering | 客户端渲染。浏览器通过 JS 生成页面。 |
|
||||
| **Webpack** | - | 传统打包工具,先打包后服务。 |
|
||||
| **Vite** | - | 现代构建工具,按需编译,速度极快。 |
|
||||
| **响应式** | Responsive Design | 页面自动适配不同屏幕尺寸的设计。 |
|
||||
| **媒体查询** | Media Query | CSS 的条件判断,根据屏幕宽度应用不同样式。 |
|
||||
| **命令式** | Imperative | 告诉程序"怎么做"。 |
|
||||
| **声明式** | Declarative | 告诉程序"要什么"。 |
|
||||
| **数据驱动** | Data-Driven | 只修改数据,界面自动更新。 |
|
||||
| **Tree Shaking** | - | 摇树优化。自动移除未使用的代码,减小包体积。 |
|
||||
| **代码分割** | Code Splitting | 把代码分成多个小块,按需加载。 |
|
||||
@@ -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**
|
||||
|
||||
祝你学习愉快! 🎨
|
||||
@@ -0,0 +1,857 @@
|
||||
# JavaScript 深度指南
|
||||
|
||||
::: tip 前言
|
||||
你已经学会了 HTML 和 CSS,能做出好看的网页了。但你可能会发现:点击按钮没反应,填了表单提交不了,网页就像一张"静态"的图片。
|
||||
|
||||
这就是我们需要 JavaScript 的原因——它让网页"活"起来。点击按钮能弹出菜单,输入文字能实时搜索,滚动页面能加载更多内容……这些交互效果都靠 JavaScript。
|
||||
|
||||
在 vibecoding 里,AI 会帮你写大部分代码。但你至少得能看懂代码在做什么,否则 AI 写错了你也发现不了。读完这篇,你就能:
|
||||
|
||||
- 读懂 AI 写的代码在做什么
|
||||
- 看出代码哪里有问题
|
||||
- 用清晰的话告诉 AI 怎么改
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
| 章节 | 内容 | 学完能干嘛 |
|
||||
|-----|------|-----------|
|
||||
| **第 1 章** | JavaScript 是什么 | 明白它在网页里扮演什么角色 |
|
||||
| **第 2 章** | 数据与变量 | 知道程序怎么存东西、怎么用东西 |
|
||||
| **第 3 章** | 函数与逻辑 | 看懂代码的判断、循环和复用逻辑 |
|
||||
| **第 4 章** | DOM 与事件 | 知道代码怎么控制网页、怎么响应用户操作 |
|
||||
| **第 5 章** | 实战技巧 | 拿到 AI 代码怎么读、遇到报错怎么说 |
|
||||
|
||||
每一章都从"能识别代码"开始,不需要你会手写。遇到不懂的代码,随时回来查就行。
|
||||
|
||||
---
|
||||
|
||||
## 1. JavaScript 是什么
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**为什么网页需要 JavaScript?** HTML 和 CSS 已经能让网页有内容、有样式了,为什么还要学一门新语言?
|
||||
:::
|
||||
|
||||
### 1.1 从"静态网页"到"动态应用"
|
||||
|
||||
<div style="display: flex; gap: 20px; margin: 20px 0;">
|
||||
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||
|
||||
**📄 没有 JavaScript 的网页**
|
||||
- 内容固定,无法交互
|
||||
- 点击按钮没反应
|
||||
- 填写表单提交不了
|
||||
- 页面不会自动更新
|
||||
|
||||
*就像一张纸质海报,只能看*
|
||||
|
||||
</div>
|
||||
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||
|
||||
**🚀 有 JavaScript 的网页**
|
||||
- 点击按钮弹出菜单
|
||||
- 输入文字实时搜索
|
||||
- 滚动自动加载内容
|
||||
- 数据实时更新显示
|
||||
|
||||
*就像一个真正的应用程序*
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
**用一句话理解三者的关系:**
|
||||
|
||||
| 技术 | 比喻 | 作用 |
|
||||
|------|------|------|
|
||||
| **HTML** | 骨架 | 定义网页的结构和内容 |
|
||||
| **CSS** | 皮肤 | 定义网页的外观和样式 |
|
||||
| **JavaScript** | 肌肉和神经系统 | 让网页能响应、能交互、能思考 |
|
||||
|
||||
### 1.2 为什么 vibecoding 也需要懂 JavaScript?
|
||||
|
||||
::: warning 刚学 JS 的开发者踩坑记
|
||||
一位刚学 JavaScript 的开发者用 AI 做了一个"计数器"应用:点击按钮,数字加 1。AI 生成的代码能正常工作。
|
||||
|
||||
但他想改成"点击加 2",对 AI 说:"让每次点击加 2。" AI 改了代码,可数字还是只加 1。
|
||||
|
||||
他问 AI 为啥没效果,AI 解释了一通,但他看不懂代码里的 `count = count + 1` 是什么意思,也不知道 AI 改的是不是这个地方。只能反复说"加 2 没效果",AI 又改了好几版,有的把初始值改成 2,有的在完全不相关的地方加了 2。
|
||||
|
||||
最后他看了第 2 章"变量"的概念,明白了 `count = count + 1` 是在把 count 的值加 1 再存回去。然后他对 AI 说:"把 `count + 1` 改成 `count + 2`。"
|
||||
|
||||
一次就改对了。
|
||||
|
||||
**这就是为什么要懂 JavaScript——不是为了手写代码,而是为了在 AI 没改对时,你能一眼看出问题在哪,一句话说到点子上。**
|
||||
:::
|
||||
|
||||
### 1.3 先睹为快:一段真实的 AI 代码
|
||||
|
||||
在深入学习之前,让我们先看一段 AI 生成的真实代码。不要担心看不懂,只要有个印象,后面我们会逐一讲解每个部分。
|
||||
|
||||
**场景**:做一个"点击按钮切换背景颜色"的功能
|
||||
|
||||
```javascript
|
||||
// 定义一组颜色
|
||||
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4']
|
||||
let currentIndex = 0
|
||||
|
||||
// 找到页面上的按钮
|
||||
const button = document.querySelector('#changeBtn')
|
||||
|
||||
// 给按钮添加点击事件
|
||||
button.addEventListener('click', () => {
|
||||
currentIndex = (currentIndex + 1) % colors.length
|
||||
document.body.style.backgroundColor = colors[currentIndex]
|
||||
})
|
||||
```
|
||||
|
||||
**这段代码在做什么?**
|
||||
|
||||
| 代码 | 作用 | 对应章节 |
|
||||
|------|------|----------|
|
||||
| `const colors = [...]` | 定义一组颜色数据 | 第 2 章:数组 |
|
||||
| `let currentIndex = 0` | 记录当前显示第几个颜色 | 第 2 章:变量 |
|
||||
| `document.querySelector(...)` | 找到页面上的按钮 | 第 4 章:DOM 查找 |
|
||||
| `button.addEventListener(...)` | 给按钮添加点击事件 | 第 4 章:事件监听 |
|
||||
| `() => {...}` | 定义点击后要执行的代码 | 第 3 章:箭头函数 |
|
||||
|
||||
::: info 💡 核心启示
|
||||
你不需要现在就理解每一行代码。只要记住:**JavaScript 代码就是一系列指令,告诉浏览器"当用户做某事时,应该发生什么"。**
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据篇:变量与数据类型
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**程序是怎么"记住"东西的?** 用户输入的内容、从服务器获取的数据、计算过程中的中间结果——这些信息都存在哪里?
|
||||
:::
|
||||
|
||||
### 2.1 变量:给数据起个名字
|
||||
|
||||
**变量就像一个有标签的盒子**——你可以把数据放进去,以后通过标签来取用。
|
||||
|
||||
```javascript
|
||||
const name = "张三" // 名字不会变,用 const
|
||||
let age = 25 // 年龄可能会变,用 let
|
||||
```
|
||||
|
||||
**为什么要区分 const 和 let?**
|
||||
|
||||
想象一下:你的身份证号码(const)这辈子都不会变,但你的年龄(let)每年都会变。JavaScript 让你用不同的关键字来表达这种"变与不变"的意图。
|
||||
|
||||
| 关键字 | 能否修改 | 使用场景 | 示例 |
|
||||
|--------|---------|----------|------|
|
||||
| `const` | ❌ 不能 | 值不会变的数据 | 身份证号、配置项、颜色列表 |
|
||||
| `let` | ✅ 能 | 值会变化的数据 | 计数器、当前选中的选项、用户输入 |
|
||||
|
||||
::: details 🔍 看一个具体的例子
|
||||
```javascript
|
||||
// 用 const:这些值不会变
|
||||
const PI = 3.14159
|
||||
const MAX_USERS = 100
|
||||
const APP_NAME = "TodoList"
|
||||
|
||||
// 用 let:这些值会变化
|
||||
let count = 0
|
||||
count = 1 // ✅ 可以修改
|
||||
|
||||
count = count + 1 // ✅ 可以基于原值计算
|
||||
|
||||
// 如果用 const 会怎样?
|
||||
const fixedCount = 0
|
||||
fixedCount = 1 // ❌ 报错!const 不能重新赋值
|
||||
```
|
||||
:::
|
||||
|
||||
👇 **动手试试看**:修改下面的代码,看看 const 和 let 的区别
|
||||
|
||||
<VariableBoxDemo />
|
||||
|
||||
### 2.2 数据类型:JavaScript 里的几种"东西"
|
||||
|
||||
JavaScript 把数据分成几种类型,最常用的有三种:
|
||||
|
||||
| 类型 | 说明 | 示例 | 实际场景 |
|
||||
|------|------|------|----------|
|
||||
| `string`(字符串)| 文本内容 | `"hello"`, `'你好'` | 用户名、商品描述、提示信息 |
|
||||
| `number`(数字)| 数值 | `42`, `3.14` | 价格、数量、评分 |
|
||||
| `boolean`(布尔值)| 是/否 | `true`, `false` | 是否登录、是否完成、是否可见 |
|
||||
|
||||
**还有两个特殊值需要知道:**
|
||||
|
||||
- `undefined` → 变量声明了,但还没给值
|
||||
- `null` → 故意设为空(表示"这里没有值")
|
||||
|
||||
::: details 🔍 模板字符串:更方便地拼接文本
|
||||
在 AI 代码里,你经常会看到用反引号(`` ` ``)包裹的字符串,里面还有 `${...}`:
|
||||
|
||||
```javascript
|
||||
const name = "张三"
|
||||
const age = 25
|
||||
|
||||
// 传统写法(麻烦)
|
||||
const message = "我叫" + name + ",今年" + age + "岁"
|
||||
|
||||
// 模板字符串(简洁)
|
||||
const message = `我叫${name},今年${age}岁`
|
||||
// 结果:"我叫张三,今年25岁"
|
||||
```
|
||||
|
||||
**识别要点**:看到反引号和 `${}`,就知道是在把变量插入到文本中。
|
||||
:::
|
||||
|
||||
### 2.3 对象和数组:把数据组织起来
|
||||
|
||||
**对象 = 一组有名字的属性**(像一张个人信息表)
|
||||
|
||||
```javascript
|
||||
const user = {
|
||||
name: "张三",
|
||||
age: 25,
|
||||
isVIP: true
|
||||
}
|
||||
|
||||
// 使用点号访问属性
|
||||
console.log(user.name) // "张三"
|
||||
console.log(user.age) // 25
|
||||
```
|
||||
|
||||
**数组 = 一组有顺序的数据**(像一个列表)
|
||||
|
||||
```javascript
|
||||
const colors = ['红色', '绿色', '蓝色']
|
||||
|
||||
// 用索引访问(从 0 开始)
|
||||
console.log(colors[0]) // "红色"
|
||||
console.log(colors[1]) // "绿色"
|
||||
```
|
||||
|
||||
**嵌套结构:对象里套数组、数组里套对象**
|
||||
|
||||
这是 AI 代码中最常见的数据结构:
|
||||
|
||||
```javascript
|
||||
const todos = [
|
||||
{ id: 1, text: "学习 JavaScript", done: false },
|
||||
{ id: 2, text: "做项目", done: true },
|
||||
{ id: 3, text: "写文档", done: false }
|
||||
]
|
||||
|
||||
// 访问:先取数组的第 0 项,再取它的 text 属性
|
||||
console.log(todos[0].text) // "学习 JavaScript"
|
||||
```
|
||||
|
||||
::: info 💡 识别技巧
|
||||
- 看到 `{}` → 这是一个对象,里面是一组 `名字: 值`
|
||||
- 看到 `[]` → 这是一个数组,里面是一组按顺序排列的值
|
||||
- 看到 `data[0].name` → 先取数组第 0 项,再取它的 name 属性
|
||||
:::
|
||||
|
||||
### 2.4 值与引用:一个容易踩的坑
|
||||
|
||||
这是新手最常遇到的问题之一!
|
||||
|
||||
**基本类型(string、number、boolean)赋值 = 复制一份全新的数据:**
|
||||
|
||||
```javascript
|
||||
let a = 10
|
||||
let b = a // b 得到 a 的副本
|
||||
b = 20
|
||||
console.log(a) // 10(a 不受影响)
|
||||
```
|
||||
|
||||
**对象和数组赋值 = 复制的是"地址"(指向同一个东西):**
|
||||
|
||||
```javascript
|
||||
let user1 = { name: "张三" }
|
||||
let user2 = user1 // user2 指向同一个对象
|
||||
user2.name = "李四" // 修改 user2 会影响 user1
|
||||
console.log(user1.name) // "李四"(user1 也变了!)
|
||||
```
|
||||
|
||||
**为什么要创建副本?**
|
||||
|
||||
在 React/Vue 中,直接修改数据会导致界面不更新。所以 AI 代码里经常看到 `[...array]` 或 `{...obj}`——它在创建副本,避免互相影响。
|
||||
|
||||
```javascript
|
||||
// 用展开运算符创建副本
|
||||
const arr1 = [1, 2, 3]
|
||||
const arr2 = [...arr1] // 创建新数组
|
||||
arr2.push(4)
|
||||
console.log(arr1) // [1, 2, 3](不受影响)
|
||||
console.log(arr2) // [1, 2, 3, 4]
|
||||
```
|
||||
|
||||
👇 **动手试试看**:观察修改副本时原数据的变化
|
||||
|
||||
<ReferenceDemo />
|
||||
|
||||
### 2.5 解构与展开:现代 JavaScript 的快捷写法
|
||||
|
||||
这两个语法在 AI 代码里到处都是,不认识就读不懂代码。
|
||||
|
||||
**解构赋值:从对象或数组里快速提取数据**
|
||||
|
||||
```javascript
|
||||
const user = { name: "张三", age: 25, city: "北京" }
|
||||
|
||||
// 传统写法(麻烦)
|
||||
const name = user.name
|
||||
const age = user.age
|
||||
|
||||
// 解构写法(简洁)
|
||||
const { name, age } = user
|
||||
// 效果一样,但一行搞定
|
||||
```
|
||||
|
||||
**展开运算符:复制并扩展数据**
|
||||
|
||||
```javascript
|
||||
// 复制数组并添加新元素
|
||||
const arr1 = [1, 2, 3]
|
||||
const arr2 = [...arr1, 4, 5] // [1, 2, 3, 4, 5]
|
||||
|
||||
// 复制对象并添加新属性
|
||||
const user1 = { name: "张三", age: 25 }
|
||||
const user2 = { ...user1, city: "北京" }
|
||||
// { name: "张三", age: 25, city: "北京" }
|
||||
```
|
||||
|
||||
::: info 💡 识别技巧
|
||||
- 看到 `const { name, age } = person` → 从 person 对象里提取 name 和 age
|
||||
- 看到 `...array` 或 `...obj` → 把数组或对象展开铺平
|
||||
- 你不需要能手写,但必须能读懂
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 逻辑篇:函数与流程控制
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**代码是怎么"做决定"和"重复做事"的?** 程序需要根据条件执行不同的操作,也需要重复执行某些任务——这些逻辑怎么表达?
|
||||
:::
|
||||
|
||||
### 3.1 条件判断:如果...就...否则...
|
||||
|
||||
**if/else:最基本的条件判断**
|
||||
|
||||
```javascript
|
||||
const age = 18
|
||||
|
||||
if (age >= 18) {
|
||||
console.log("成年人")
|
||||
} else {
|
||||
console.log("未成年")
|
||||
}
|
||||
```
|
||||
|
||||
**三元运算符:简写的 if/else**
|
||||
|
||||
```javascript
|
||||
// 完整写法(4 行)
|
||||
let message
|
||||
if (age >= 18) {
|
||||
message = "成年人"
|
||||
} else {
|
||||
message = "未成年"
|
||||
}
|
||||
|
||||
// 三元运算符(1 行)
|
||||
const message = age >= 18 ? "成年人" : "未成年"
|
||||
// 格式:条件 ? 条件为真时的值 : 条件为假时的值
|
||||
```
|
||||
|
||||
**&& 短路写法:React 代码里常见**
|
||||
|
||||
```javascript
|
||||
// 只有 isLoggedIn 为 true 时才显示用户面板
|
||||
isLoggedIn && <UserPanel />
|
||||
|
||||
// 等价于
|
||||
if (isLoggedIn) {
|
||||
return <UserPanel />
|
||||
}
|
||||
```
|
||||
|
||||
::: info 💡 识别技巧
|
||||
- 看到 `? :` → 这是三元运算符,简写的 if/else
|
||||
- 看到 `&&` → 前面为 true 才执行后面
|
||||
:::
|
||||
|
||||
### 3.2 函数:把操作打包起来
|
||||
|
||||
**函数 = 一道菜的配方**
|
||||
|
||||
- 定义函数 = 写下配方
|
||||
- 调用函数 = 按配方做菜
|
||||
- 参数 = 原料
|
||||
- 返回值 = 成品
|
||||
|
||||
```javascript
|
||||
// 定义函数(写下配方)
|
||||
function greet(name) {
|
||||
return "Hello " + name
|
||||
}
|
||||
|
||||
// 调用函数(按配方做菜)
|
||||
console.log(greet("张三")) // "Hello 张三"
|
||||
console.log(greet("李四")) // "Hello 李四"
|
||||
```
|
||||
|
||||
**三种写法,一眼识别:**
|
||||
|
||||
```javascript
|
||||
// 1. function 声明(传统写法)
|
||||
function greet(name) {
|
||||
return "Hello " + name
|
||||
}
|
||||
|
||||
// 2. 箭头函数(AI 代码里用得最多)
|
||||
const greet = (name) => {
|
||||
return "Hello " + name
|
||||
}
|
||||
|
||||
// 3. 箭头函数简写(只有一行时)
|
||||
const greet = (name) => "Hello " + name
|
||||
```
|
||||
|
||||
👇 **动手试试看**:输入不同的名字,看看函数怎么工作
|
||||
|
||||
<FunctionMachineDemo />
|
||||
|
||||
::: info 💡 识别技巧
|
||||
- 看到 `function` 或 `=>` → 这是一个函数
|
||||
- 看到 `fn()` → 在调用这个函数
|
||||
- 看到 `() => {}` → 箭头函数,现代 JS 的主流写法
|
||||
:::
|
||||
|
||||
### 3.3 数组方法:处理列表的利器
|
||||
|
||||
在 React/Vue 里,几乎每个列表渲染都会用到这些方法。
|
||||
|
||||
```javascript
|
||||
const todos = [
|
||||
{ id: 1, text: "学习", done: false },
|
||||
{ id: 2, text: "工作", done: true }
|
||||
]
|
||||
|
||||
// .map():把数组的每一项变成另一个东西
|
||||
const texts = todos.map(todo => todo.text)
|
||||
// ["学习", "工作"]
|
||||
|
||||
// .filter():筛选出符合条件的项
|
||||
const unfinished = todos.filter(todo => !todo.done)
|
||||
// [{ id: 1, text: "学习", done: false }]
|
||||
|
||||
// .find():找到第一个符合条件的项
|
||||
const found = todos.find(todo => todo.id === 1)
|
||||
// { id: 1, text: "学习", done: false }
|
||||
```
|
||||
|
||||
::: info 💡 识别技巧
|
||||
- 看到 `.map()` → 对数组做变换,返回新数组
|
||||
- 看到 `.filter()` → 筛选数组
|
||||
- 看到 `items.map(item => <li>{item.name}</li>)` → 把每个数据项变成列表标签
|
||||
:::
|
||||
|
||||
### 3.4 作用域:变量的"可见范围"
|
||||
|
||||
**用"房间"比喻:**
|
||||
|
||||
- 函数内部的变量就像房间里的东西,外面看不到
|
||||
- 但房间里的人可以看到走廊(外层作用域)的东西
|
||||
|
||||
```javascript
|
||||
const global = "全局变量" // 走廊里的东西
|
||||
|
||||
function room() {
|
||||
const local = "房间里的东西" // 房间里的东西
|
||||
console.log(global) // ✅ 能看到走廊
|
||||
}
|
||||
|
||||
console.log(local) // ❌ 报错!外面看不到房间里的东西
|
||||
```
|
||||
|
||||
**核心直觉:** 代码写在哪里,决定了它能看到什么变量。
|
||||
|
||||
👇 **动手试试看**:点击不同的作用域,看看能访问哪些变量
|
||||
|
||||
<ScopeDemo />
|
||||
|
||||
### 3.5 闭包:函数"记住"了它诞生时的环境
|
||||
|
||||
**不要把它当成独立的概念,从一个具体场景理解:**
|
||||
|
||||
```javascript
|
||||
function setupCounter() {
|
||||
let count = 0 // 这个变量在函数内部
|
||||
|
||||
return {
|
||||
add: () => { count++; return count },
|
||||
getCount: () => count
|
||||
}
|
||||
}
|
||||
|
||||
const counter = setupCounter()
|
||||
console.log(counter.add()) // 1
|
||||
console.log(counter.add()) // 2
|
||||
console.log(counter.getCount()) // 2
|
||||
```
|
||||
|
||||
**核心直觉:** 函数在被创建时,会"记住"它周围的变量,即使外层函数已经执行完了。
|
||||
|
||||
👇 **动手试试看**:观察闭包如何让函数"记住"状态
|
||||
|
||||
<ClosureDemo />
|
||||
|
||||
### 3.6 this:函数被谁调用
|
||||
|
||||
**不讲复杂的绑定规则,只讲最常见的场景:**
|
||||
|
||||
**场景 1:在对象的方法里,this 指向这个对象**
|
||||
|
||||
```javascript
|
||||
const user = {
|
||||
name: "张三",
|
||||
sayHi() {
|
||||
console.log("你好,我是" + this.name) // this 指向 user
|
||||
}
|
||||
}
|
||||
user.sayHi() // "你好,我是张三"
|
||||
```
|
||||
|
||||
**场景 2:在事件监听里,this 指向触发事件的元素**
|
||||
|
||||
```javascript
|
||||
button.addEventListener('click', function() {
|
||||
console.log(this) // this 指向 button 元素
|
||||
})
|
||||
|
||||
// 但箭头函数不会改变 this
|
||||
button.addEventListener('click', () => {
|
||||
console.log(this) // this 指向外层的 this
|
||||
})
|
||||
```
|
||||
|
||||
::: info 💡 遇到问题怎么办?
|
||||
如果 AI 代码里出现 this 相关的 bug(比如 `Cannot read property of undefined`),告诉 AI:"这个方法里的 this 指向不对,改成箭头函数或者用 bind"
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 交互篇:DOM、事件与异步
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**JavaScript 怎么跟网页"互动"?** 怎么找到页面上的元素?怎么响应用户的点击、输入?怎么从服务器获取数据?
|
||||
:::
|
||||
|
||||
### 4.1 DOM:JavaScript 看到的网页
|
||||
|
||||
网页在 JavaScript 眼里是一棵"树",每个 HTML 标签都是树上的一个"节点"。
|
||||
|
||||
```html
|
||||
<html>
|
||||
<body>
|
||||
<h1>标题</h1>
|
||||
<p>段落</p>
|
||||
<ul>
|
||||
<li>项目1</li>
|
||||
<li>项目2</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**JS 操控网页 = 找到节点 + 修改节点 + 创建/删除节点**
|
||||
|
||||
👇 **动手试试看**:点击节点,看看 DOM 树是怎么组织的
|
||||
|
||||
<DOMTreeDemo />
|
||||
|
||||
### 4.2 查找与修改元素
|
||||
|
||||
**查找元素:**
|
||||
|
||||
```javascript
|
||||
// 根据 CSS 选择器查找(最常用)
|
||||
const title = document.querySelector('h1') // 找第一个 h1
|
||||
const button = document.querySelector('#btn') // 找 id="btn" 的元素
|
||||
const items = document.querySelectorAll('.item') // 找所有 class="item" 的元素
|
||||
```
|
||||
|
||||
**修改元素:**
|
||||
|
||||
```javascript
|
||||
// 改文字
|
||||
title.textContent = "新标题"
|
||||
|
||||
// 改样式
|
||||
element.style.color = "red"
|
||||
element.style.fontSize = "20px"
|
||||
|
||||
// 改 CSS 类
|
||||
element.classList.add('active') // 添加类
|
||||
element.classList.remove('hidden') // 移除类
|
||||
element.classList.toggle('open') // 切换类(有就移除,没有就添加)
|
||||
```
|
||||
|
||||
::: info 💡 识别技巧
|
||||
- 看到 `document.querySelector` → 在查找网页元素
|
||||
- 看到 `.textContent` → 改文字
|
||||
- 看到 `.style.xxx` → 改样式
|
||||
- 看到 `.classList.add/remove/toggle` → 改 CSS 类
|
||||
:::
|
||||
|
||||
### 4.3 事件:当用户做了某个操作时...
|
||||
|
||||
**addEventListener:给元素添加事件监听**
|
||||
|
||||
```javascript
|
||||
button.addEventListener('click', () => {
|
||||
console.log("按钮被点击了")
|
||||
})
|
||||
```
|
||||
|
||||
**常见事件:**
|
||||
|
||||
| 事件 | 触发时机 | 实际场景 |
|
||||
|------|---------|----------|
|
||||
| `click` | 点击 | 按钮点击、链接跳转 |
|
||||
| `input` | 输入框内容变化 | 实时搜索、表单验证 |
|
||||
| `submit` | 表单提交 | 登录、注册、提交数据 |
|
||||
| `scroll` | 滚动页面 | 懒加载、回到顶部 |
|
||||
|
||||
**事件对象:获取更多信息**
|
||||
|
||||
```javascript
|
||||
input.addEventListener('input', (e) => {
|
||||
console.log(e.target.value) // 获取输入框的值
|
||||
e.preventDefault() // 阻止默认行为(比如表单提交后刷新页面)
|
||||
})
|
||||
```
|
||||
|
||||
::: info 💡 实际应用
|
||||
当你想给按钮加一个功能,本质上就是在告诉 AI:"给这个按钮添加一个点击事件,点击后执行某某操作"
|
||||
:::
|
||||
|
||||
### 4.4 异步:为什么有些操作不是立刻完成的
|
||||
|
||||
**餐厅比喻:**
|
||||
|
||||
点菜后不用站在厨房门口等,可以先做别的事,菜好了服务员会端过来。
|
||||
|
||||
**最常见场景:从服务器获取数据**
|
||||
|
||||
```javascript
|
||||
// 同步写法(会卡住页面,不要用)
|
||||
const data = fetch('/api/data') // ❌ 这样写会卡住
|
||||
|
||||
// 异步写法(正确)
|
||||
async function loadData() {
|
||||
try {
|
||||
const response = await fetch('/api/data')
|
||||
const data = await response.json()
|
||||
console.log(data)
|
||||
} catch (error) {
|
||||
console.error('出错了:', error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**async/await 语法:**
|
||||
|
||||
- `async` → 标记这个函数里有异步操作
|
||||
- `await` → 等待这个操作完成(但不会卡住页面)
|
||||
- `try/catch` → 处理可能出现的错误
|
||||
|
||||
👇 **动手试试看**:观察异步操作的执行顺序
|
||||
|
||||
<AsyncRestaurantDemo />
|
||||
|
||||
::: info 💡 识别技巧
|
||||
- 看到 `async/await` → 在等待耗时操作
|
||||
- 看到 `fetch()` → 在从服务器获取数据
|
||||
- 看到 `try/catch` → 在处理可能的错误
|
||||
:::
|
||||
|
||||
### 4.5 事件循环:JavaScript 到底怎么工作的
|
||||
|
||||
**不用术语"微任务/宏任务",用一个简单的模型理解:**
|
||||
|
||||
**JS 是一个"单人工位"**,同时只做一件事,但有一个"待办便签栏"(任务队列)。
|
||||
|
||||
当遇到要等待的操作(网络请求、定时器),JS 不是傻等,而是把"等好了之后做什么"贴到便签栏,自己继续往下执行。等当前事情做完了,才去看便签栏。
|
||||
|
||||
```javascript
|
||||
console.log("1")
|
||||
|
||||
setTimeout(() => console.log("2"), 0) // 即使是 0 秒,也会推迟
|
||||
|
||||
console.log("3")
|
||||
|
||||
// 输出:1, 3, 2(不是 1, 2, 3!)
|
||||
```
|
||||
|
||||
**为什么?**
|
||||
1. 执行 `console.log("1")` → 输出 1
|
||||
2. 遇到 `setTimeout` → 把回调贴到便签栏,继续往下
|
||||
3. 执行 `console.log("3")` → 输出 3
|
||||
4. 当前代码执行完了,去看便签栏
|
||||
5. 执行 `setTimeout` 的回调 → 输出 2
|
||||
|
||||
👇 **动手试试看**:观察代码的执行顺序
|
||||
|
||||
<JSEventLoopDemo />
|
||||
|
||||
::: info 💡 遇到问题怎么办?
|
||||
如果 AI 代码里数据还没获取到页面就渲染了,告诉 AI:"数据还没加载完就开始渲染了,需要添加 loading 状态,等数据到了再渲染"
|
||||
:::
|
||||
|
||||
### 4.6 模块:import 和 export
|
||||
|
||||
AI 生成的 React/Vue 代码第一行几乎都是 `import`。
|
||||
|
||||
**import = 从别的文件引入功能**
|
||||
|
||||
```javascript
|
||||
// 从工具文件引入函数
|
||||
import { formatDate } from './utils'
|
||||
|
||||
// 从第三方包引入
|
||||
import React from 'react'
|
||||
import { useState } from 'react'
|
||||
```
|
||||
|
||||
**export = 把功能暴露出去给别人用**
|
||||
|
||||
```javascript
|
||||
// utils.js
|
||||
export function formatDate(date) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 或者默认导出
|
||||
export default function formatDate(date) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**npm 包 = 别人写好的工具,安装后就能用**
|
||||
|
||||
```javascript
|
||||
// 安装包:npm install lodash
|
||||
// 使用包
|
||||
import _ from 'lodash'
|
||||
```
|
||||
|
||||
::: info 💡 识别技巧
|
||||
- 看到 `import` → 从别的文件引入功能
|
||||
- 看到 `export` → 把功能暴露给别人用
|
||||
- 看到 `from 'react'` → 从 React 包引入
|
||||
- 看到 `from './utils'` → 从本地文件引入
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 实战篇:读懂代码、看懂报错、精准描述
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**前面学了这么多语法,实际拿到 AI 代码时怎么用?** 怎么快速读懂代码?遇到报错怎么办?怎么让 AI 准确地帮你改代码?
|
||||
:::
|
||||
|
||||
### 5.1 拿到 AI 代码后怎么读
|
||||
|
||||
**四步法:**
|
||||
|
||||
| 步骤 | 看什么 | 示例 |
|
||||
|------|--------|------|
|
||||
| **第一步:看整体结构** | 有几个函数?分别做什么? | `loadData()` 加载数据,`renderList()` 渲染列表 |
|
||||
| **第二步:找入口** | 程序从哪里开始执行? | `addEventListener('click', ...)` 点击时开始 |
|
||||
| **第三步:追踪数据流** | 数据从哪里来?到哪里去? | 从 API 获取 → 解析 → 渲染到页面 |
|
||||
| **第四步:看细节逻辑** | 具体函数里怎么处理的? | 循环、判断、计算 |
|
||||
|
||||
**用第 1 章的代码示例做一次完整的"阅读演示":**
|
||||
|
||||
```javascript
|
||||
// 第一步:整体结构
|
||||
// - 一个颜色数组
|
||||
// - 一个变量记录当前索引
|
||||
// - 一个按钮的点击事件
|
||||
|
||||
// 第二步:入口点
|
||||
// button.addEventListener('click', ...) → 点击按钮时执行
|
||||
|
||||
// 第三步:数据流
|
||||
// colors(颜色数组)→ currentIndex(当前索引)→ backgroundColor(背景色)
|
||||
|
||||
// 第四步:细节逻辑
|
||||
// currentIndex = (currentIndex + 1) % colors.length
|
||||
// 这个公式的意思:每次 +1,但不超过数组长度(循环)
|
||||
```
|
||||
|
||||
### 5.2 常见报错速查
|
||||
|
||||
| 报错 | 大白话解释 | 怎么跟 AI 说 |
|
||||
|------|-----------|-------------|
|
||||
| `TypeError: Cannot read properties of undefined` | 你想从一个不存在的东西上取值 | "第 X 行报错,某某变量是 undefined,检查它的赋值逻辑" |
|
||||
| `ReferenceError: xxx is not defined` | 用了一个没有声明过的变量名 | "变量 xxx 没有定义,是不是拼写错了或者忘了导入" |
|
||||
| `TypeError: xxx is not a function` | 把一个不是函数的东西当函数调用了 | "xxx 不是函数,检查一下它的类型和来源" |
|
||||
| `SyntaxError: Unexpected token` | 语法写错了(括号不匹配、少了逗号等) | "第 X 行语法错误,检查括号和标点" |
|
||||
| `CORS error` | 浏览器阻止了跨域请求 | "遇到 CORS 错误,需要配置跨域资源共享" |
|
||||
| `404 Not Found` | 请求的资源不存在 | "API 返回 404,检查接口地址是否正确" |
|
||||
|
||||
### 5.3 如何精准描述问题
|
||||
|
||||
新手和熟练开发者的差距,往往就体现在**描述问题的精准度**上。
|
||||
|
||||
| ❌ 差的描述 | ✅ 好的描述 |
|
||||
|-----------|-----------|
|
||||
| "代码有 bug" | "点击删除按钮时,删除的不是当前项而是最后一项" |
|
||||
| "样式不对" | "标题应该居中,现在是左对齐" |
|
||||
| "数据显示不出来" | "fetch 请求返回了数据(控制台能看到),但页面没有重新渲染" |
|
||||
| "加一个功能" | "在用户列表页面添加一个搜索框,输入时实时过滤列表,按 name 字段模糊匹配" |
|
||||
| "点击没反应" | "点击按钮时控制台报错 'Cannot read property of undefined',错误在第 X 行" |
|
||||
|
||||
**一个实战练习:**
|
||||
|
||||
```javascript
|
||||
// 有 bug 的代码
|
||||
function deleteTodo(index) {
|
||||
todos.splice(index, 1) // 总是删除最后一项
|
||||
}
|
||||
|
||||
// 错误现象:无论点哪个删除按钮,删的都是最后一项
|
||||
```
|
||||
|
||||
**❌ 差的描述:** "删除功能有 bug"
|
||||
|
||||
**✅ 好的描述:** "点击删除按钮时,删除的不是当前项而是最后一项。代码里用了 splice(index, 1),但 index 可能不正确。需要改成用每个事项的唯一 id 来匹配删除。"
|
||||
|
||||
### 5.4 你现在应该能识别的代码
|
||||
|
||||
- 看到 `const/let` → 知道变量能不能重新赋值
|
||||
- 看到 `{}` → 对象 / 看到 `[]` → 数组
|
||||
- 看到 `{...obj}` 或 `[...arr]` → 在创建副本
|
||||
- 看到 `function` 或 `=>` → 定义了一段可重复执行的操作
|
||||
- 看到 `if/else` 或 `? :` → 代码在做判断
|
||||
- 看到 `.map()` / `.filter()` → 在变换或筛选数组
|
||||
- 看到 `document.querySelector` → 在查找网页元素
|
||||
- 看到 `addEventListener` → 在监听用户操作
|
||||
- 看到 `async/await` → 在等待耗时操作
|
||||
- 看到 `import/export` → 在引入或导出模块
|
||||
- 遇到报错 → 能读懂大意并精准描述给 AI
|
||||
|
||||
**如果你认真读了每章的"深入"部分,你还掌握了这些核心概念:**
|
||||
|
||||
- **值 vs 引用**:基本类型复制值,对象/数组复制的是地址
|
||||
- **作用域与闭包**:函数能"记住"它诞生时周围的变量
|
||||
- **this 的本质**:取决于函数被谁调用,而不是写在哪里
|
||||
- **事件循环**:JS 是单线程的,靠任务队列实现"不阻塞"
|
||||
|
||||
这些概念会帮你更快定位问题。
|
||||
|
||||
::: info 💡 遇到问题时这样跟 AI 说
|
||||
- "第 X 行报错 XXX,帮我看看是什么问题"
|
||||
- "这个函数的逻辑是 XXX,但结果不对,应该是 XXX"
|
||||
- "我想修改 XXX 功能,具体要求是 XXX"
|
||||
:::
|
||||
@@ -0,0 +1,599 @@
|
||||
# JavaScript 运行时深度指南
|
||||
|
||||
::: tip 前言
|
||||
你已经学会了 JavaScript 的基本语法,但你是否想过:
|
||||
- 代码到底在哪里运行?
|
||||
- 为什么同样的代码在浏览器和 Node.js 中行为不一样?
|
||||
- 为什么有时代码会"卡住",有时却能"并行"执行?
|
||||
|
||||
这篇文章会带你深入了解 JavaScript 的运行时环境,包括事件循环、调用栈、内存管理等。读完这篇,你就能理解代码为什么按某个顺序执行,快速定位异步相关的 bug,优化代码性能并避免内存泄漏。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
| 章节 | 内容 | 学完能干嘛 |
|
||||
|-----|------|-----------|
|
||||
| **第 1 章** | 运行时概述 | 理解 JavaScript 代码在哪里运行 |
|
||||
| **第 2 章** | 浏览器运行时 | 知道浏览器提供了哪些 Web API |
|
||||
| **第 3 章** | Node.js 运行时 | 了解服务器端的 JavaScript 环境 |
|
||||
| **第 4 章** | 事件循环深入 | 掌握宏任务和微任务的执行顺序 |
|
||||
| **第 5 章** | 调用栈与内存 | 理解代码执行过程和内存管理 |
|
||||
| **第 6 章** | 实战技巧 | 优化性能、调试内存泄漏 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 运行时概述
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**什么是"运行时"?** JavaScript 只是一门语言,为什么同样的代码在不同环境中会有不同的行为?
|
||||
:::
|
||||
|
||||
### 1.1 运行时是什么
|
||||
|
||||
**运行时 = JavaScript 引擎 + 环境提供的 API**
|
||||
|
||||
如果把 JavaScript 比作"编程语言",那么运行时就是"操作系统"——它决定了你的代码能做什么、不能做什么。
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ JavaScript 代码 │
|
||||
├─────────────────────────────────────┤
|
||||
│ JavaScript 引擎 (V8) │ ← 负责解析和执行代码
|
||||
├─────────────────────────────────────┤
|
||||
│ 运行时环境 (浏览器/Node.js) │ ← 提供额外能力
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**一个比喻:JavaScript 是"普通话",运行时是"城市"**
|
||||
|
||||
- JavaScript 语法(普通话)哪里都一样
|
||||
- 但不同城市提供的设施不一样:
|
||||
- 浏览器 = 有 DOM、window、fetch(就像城市有商场、图书馆)
|
||||
- Node.js = 有 fs、http、path(就像城市有工厂、高速公路)
|
||||
|
||||
### 1.2 两大主流运行时
|
||||
|
||||
| 特性 | 浏览器 | Node.js |
|
||||
|------|--------|---------|
|
||||
| **主要用途** | 网页交互、用户界面 | 服务器端应用、命令行工具 |
|
||||
| **全局对象** | `window` | `global` |
|
||||
| **DOM API** | ✅ 支持 | ❌ 不支持 |
|
||||
| **文件系统** | ❌ 受限 | ✅ 完整支持 |
|
||||
| **模块系统** | ES Modules | CommonJS + ES Modules |
|
||||
| **定时器** | `setTimeout`, `setInterval` | `setTimeout`, `setInterval` |
|
||||
| **网络请求** | `fetch`, `XMLHttpRequest` | `http`, `https` 模块 |
|
||||
|
||||
👇 **动手试试看**:对比浏览器和 Node.js 的环境差异
|
||||
|
||||
<RuntimeEnvironmentDemo />
|
||||
|
||||
::: info 💡 核心启示
|
||||
运行时决定了你能用什么 API。在浏览器能用的 DOM API,在 Node.js 里用不了;在 Node.js 能用的文件 API,在浏览器里也用不了。这就是为什么有些代码需要"环境判断"。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 浏览器运行时
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**浏览器提供了哪些能力让 JavaScript 操作网页?**
|
||||
:::
|
||||
|
||||
### 2.1 浏览器运行时的组成
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ JavaScript 引擎 │
|
||||
│ (V8 / SpiderMonkey) │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Web APIs │
|
||||
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ DOM │ │ BOM │ │ Network │ │
|
||||
│ │ 操作网页 │ │ 操作浏览器 │ │ 网络请求 │ │
|
||||
│ └─────────┘ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 事件循环 (Event Loop) │
|
||||
│ 负责协调代码执行、事件处理、任务调度 │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Web APIs 的三大类
|
||||
|
||||
**1. DOM API - 操作网页内容**
|
||||
|
||||
```javascript
|
||||
// 查找元素
|
||||
const title = document.querySelector('h1')
|
||||
|
||||
// 修改内容
|
||||
title.textContent = '新标题'
|
||||
|
||||
// 添加样式
|
||||
title.style.color = 'red'
|
||||
```
|
||||
|
||||
**2. BOM API - 操作浏览器**
|
||||
|
||||
```javascript
|
||||
// 页面跳转
|
||||
window.location.href = 'https://example.com'
|
||||
|
||||
// 浏览器存储
|
||||
localStorage.setItem('key', 'value')
|
||||
|
||||
// 浏览器历史
|
||||
history.back()
|
||||
```
|
||||
|
||||
**3. Network API - 网络请求**
|
||||
|
||||
```javascript
|
||||
// 发送 HTTP 请求
|
||||
fetch('/api/data')
|
||||
.then(response => response.json())
|
||||
.then(data => console.log(data))
|
||||
```
|
||||
|
||||
### 2.3 浏览器特有的事件机制
|
||||
|
||||
浏览器运行时最强大的功能之一是"事件驱动"——代码不需要一直运行,而是等用户操作时才执行。
|
||||
|
||||
```javascript
|
||||
button.addEventListener('click', () => {
|
||||
console.log('按钮被点击了')
|
||||
})
|
||||
```
|
||||
|
||||
**常见事件类型:**
|
||||
|
||||
| 事件类型 | 触发时机 | 实际场景 |
|
||||
|---------|---------|---------|
|
||||
| `click` | 鼠标点击 | 按钮交互 |
|
||||
| `input` | 输入框内容变化 | 实时搜索 |
|
||||
| `scroll` | 页面滚动 | 懒加载 |
|
||||
| `load` | 资源加载完成 | 初始化数据 |
|
||||
| `error` | 发生错误 | 错误处理 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Node.js 运行时
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**JavaScript 能在服务器端运行,靠的是什么?**
|
||||
:::
|
||||
|
||||
### 3.1 Node.js 的组成
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ JavaScript 引擎 │
|
||||
│ (V8) │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Node.js 内置模块 │
|
||||
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ fs │ │ http │ │ path │ │
|
||||
│ │ 文件操作 │ │ 网络服务器 │ │ 路径处理 │ │
|
||||
│ └─────────┘ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ libuv 事件循环库 │
|
||||
│ 跨平台的异步 I/O 支持 │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Node.js 特有能力
|
||||
|
||||
**1. 文件系统操作**
|
||||
|
||||
```javascript
|
||||
const fs = require('fs')
|
||||
|
||||
// 读取文件
|
||||
fs.readFile('./data.txt', 'utf8', (err, data) => {
|
||||
if (err) throw err
|
||||
console.log(data)
|
||||
})
|
||||
|
||||
// 写入文件
|
||||
fs.writeFile('./output.txt', 'Hello', (err) => {
|
||||
if (err) throw err
|
||||
console.log('写入成功')
|
||||
})
|
||||
```
|
||||
|
||||
**2. HTTP 服务器**
|
||||
|
||||
```javascript
|
||||
const http = require('http')
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||
res.end('<h1>Hello World</h1>')
|
||||
})
|
||||
|
||||
server.listen(3000)
|
||||
```
|
||||
|
||||
**3. 模块系统**
|
||||
|
||||
```javascript
|
||||
// CommonJS (Node.js 默认)
|
||||
const fs = require('fs')
|
||||
module.exports = { myFunction }
|
||||
|
||||
// ES Modules (现代方式)
|
||||
import fs from 'fs'
|
||||
export { myFunction }
|
||||
```
|
||||
|
||||
### 3.3 浏览器 vs Node.js 对比
|
||||
|
||||
| 特性 | 浏览器 | Node.js |
|
||||
|------|--------|---------|
|
||||
| **入口文件** | HTML 文件 | JavaScript 文件 |
|
||||
| **全局对象** | `window`, `document` | `global`, `process` |
|
||||
| **模块加载** | `<script>` 标签 | `require()` / `import` |
|
||||
| **安全性** | 沙箱环境,受限 | 可以访问系统资源 |
|
||||
| **用途** | 用户界面 | 后端服务、工具 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 事件循环深入
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**JavaScript 是单线程的,为什么能做到"不阻塞"?**
|
||||
:::
|
||||
|
||||
### 4.1 事件循环是什么
|
||||
|
||||
**事件循环 = JavaScript 的"任务调度中心"**
|
||||
|
||||
JavaScript 是单线程的,一次只能做一件事。但事件循环让它看起来能"同时"做很多事。
|
||||
|
||||
**核心机制:**
|
||||
|
||||
1. **执行同步代码** (调用栈)
|
||||
2. **处理异步任务** (任务队列)
|
||||
3. **等待新任务** (循环往复)
|
||||
|
||||
```
|
||||
调用栈 任务队列
|
||||
┌─────────┐ ┌──────────┐
|
||||
│ 任务 1 │ │ 宏任务 1 │
|
||||
│ 任务 2 │ ←──────────── │ 宏任务 2 │
|
||||
│ 任务 3 │ 执行完一个 │ 宏任务 3 │
|
||||
└─────────┘ 就取下一个 └──────────┘
|
||||
↓ ↑
|
||||
└────────────────────────┘
|
||||
事件循环不断检查
|
||||
```
|
||||
|
||||
### 4.2 宏任务 vs 微任务
|
||||
|
||||
这是面试和实际开发中最容易搞混的概念!
|
||||
|
||||
**宏任务 (Macrotask):**
|
||||
- `setTimeout`, `setInterval`
|
||||
- I/O 操作
|
||||
- UI 渲染
|
||||
|
||||
**微任务 (Microtask):**
|
||||
- `Promise.then`
|
||||
- `MutationObserver`
|
||||
- `queueMicrotask`
|
||||
|
||||
**执行顺序:同步代码 → 微任务 → 宏任务**
|
||||
|
||||
👇 **动手试试看**:观察宏任务和微任务的执行顺序
|
||||
|
||||
<TaskQueueDemo />
|
||||
|
||||
### 4.3 经典面试题
|
||||
|
||||
```javascript
|
||||
console.log('1')
|
||||
|
||||
setTimeout(() => console.log('2'), 0)
|
||||
|
||||
Promise.resolve().then(() => console.log('3'))
|
||||
|
||||
console.log('4')
|
||||
|
||||
// 输出: 1, 4, 3, 2
|
||||
```
|
||||
|
||||
**为什么是这个顺序?**
|
||||
|
||||
1. 执行同步代码:`console.log('1')`,`console.log('4')` → 输出 1, 4
|
||||
2. 检查微任务队列:`Promise.then` → 输出 3
|
||||
3. 检查宏任务队列:`setTimeout` → 输出 2
|
||||
|
||||
::: info 💡 实战技巧
|
||||
- 如果想让代码尽快执行,用微任务 (`Promise.then`)
|
||||
- 如果想延迟执行,用宏任务 (`setTimeout`)
|
||||
- 永远不要混用太多异步操作,否则会陷入"回调地狱"
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 调用栈与内存
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**代码是怎么被执行的?变量存在哪里?什么时候被回收?**
|
||||
:::
|
||||
|
||||
### 5.1 调用栈:函数执行的"足迹"
|
||||
|
||||
**调用栈 = 记录函数调用的"笔记本"**
|
||||
|
||||
每次调用一个函数,就会在栈上新增一条记录;函数执行完,记录就被移除。
|
||||
|
||||
```javascript
|
||||
function a() {
|
||||
b()
|
||||
}
|
||||
|
||||
function b() {
|
||||
c()
|
||||
}
|
||||
|
||||
function c() {
|
||||
console.log('执行完毕')
|
||||
}
|
||||
|
||||
a()
|
||||
```
|
||||
|
||||
**调用栈的变化:**
|
||||
|
||||
```
|
||||
步骤 1: 调用 a()
|
||||
┌─────────┐
|
||||
│ a │
|
||||
└─────────┘
|
||||
|
||||
步骤 2: a() 调用 b()
|
||||
┌─────────┐
|
||||
│ b │
|
||||
│ a │
|
||||
└─────────┘
|
||||
|
||||
步骤 3: b() 调用 c()
|
||||
┌─────────┐
|
||||
│ c │
|
||||
│ b │
|
||||
│ a │
|
||||
└─────────┘
|
||||
|
||||
步骤 4: c() 执行完,依次弹出
|
||||
┌─────────┐
|
||||
│ b │
|
||||
│ a │
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
👇 **动手试试看**:观察调用栈的变化
|
||||
|
||||
<CallStackDemo />
|
||||
|
||||
### 5.2 内存管理:垃圾去哪儿了
|
||||
|
||||
JavaScript 有"自动垃圾回收"机制——你不需要手动释放内存,引擎会帮你做。
|
||||
|
||||
**垃圾回收的原理:标记-清除算法**
|
||||
|
||||
1. **标记阶段**:从"根"开始,找到所有能访问的变量
|
||||
2. **清除阶段**:没被标记的变量就是"垃圾",会被回收
|
||||
|
||||
```javascript
|
||||
// 垃圾回收示例
|
||||
let obj1 = { name: '对象1' }
|
||||
let obj2 = { name: '对象2' }
|
||||
|
||||
// obj1 被重新赋值,原来的对象失去了引用
|
||||
obj1 = null // 原来的 { name: '对象1' } 会被回收
|
||||
|
||||
// obj2 还在使用中,不会被回收
|
||||
console.log(obj2.name)
|
||||
```
|
||||
|
||||
👇 **动手试试看**:观察垃圾回收的过程
|
||||
|
||||
<GarbageCollectionDemo />
|
||||
|
||||
### 5.3 内存泄漏:忘记清理的后果
|
||||
|
||||
**内存泄漏 = 该释放的内存没释放,越积越多**
|
||||
|
||||
常见原因:
|
||||
|
||||
**1. 全局变量太多**
|
||||
|
||||
```javascript
|
||||
// ❌ 错误:全局变量不会被回收
|
||||
globalCache = []
|
||||
|
||||
function addItem(item) {
|
||||
globalCache.push(item)
|
||||
}
|
||||
```
|
||||
|
||||
**2. 事件监听没移除**
|
||||
|
||||
```javascript
|
||||
// ❌ 错误:监听器没移除
|
||||
button.addEventListener('click', handleClick)
|
||||
|
||||
// ✅ 正确:不需要时移除监听
|
||||
button.removeEventListener('click', handleClick)
|
||||
```
|
||||
|
||||
**3. 闭包引用大对象**
|
||||
|
||||
```javascript
|
||||
// ❌ 错误:闭包一直引用大对象,不会被回收
|
||||
function createHandler() {
|
||||
const bigData = new Array(1000000).fill('data')
|
||||
return function() {
|
||||
console.log('处理中')
|
||||
}
|
||||
}
|
||||
|
||||
const handler = createHandler() // bigData 一直存在于内存中
|
||||
```
|
||||
|
||||
👇 **动手试试看**:观察内存泄漏是如何发生的
|
||||
|
||||
<MemoryLeakDemo />
|
||||
|
||||
::: info 💡 实战技巧
|
||||
- **定期检查**:打开浏览器 DevTools → Memory → Take Heap Snapshot,查看内存占用
|
||||
- **避免全局变量**:尽量用 `const` 和 `let`,不用 `var`
|
||||
- **及时清理**:事件监听、定时器用完要移除
|
||||
- **弱引用**:用 `WeakMap` 和 `WeakSet` 存储对象引用
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. 实战技巧
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**怎么写出高性能的 JavaScript 代码?遇到问题怎么调试?**
|
||||
:::
|
||||
|
||||
### 6.1 性能优化技巧
|
||||
|
||||
**1. 减少重排重绘**
|
||||
|
||||
```javascript
|
||||
// ❌ 错误:每次循环都触发重排
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
element.style.top = i + 'px'
|
||||
}
|
||||
|
||||
// ✅ 正确:批量修改
|
||||
element.style.transform = `translateY(${position}px)`
|
||||
```
|
||||
|
||||
**2. 使用事件委托**
|
||||
|
||||
```javascript
|
||||
// ❌ 错误:给每个按钮都添加监听
|
||||
buttons.forEach(btn => {
|
||||
btn.addEventListener('click', handleClick)
|
||||
})
|
||||
|
||||
// ✅ 正确:只给父元素添加一个监听
|
||||
container.addEventListener('click', (e) => {
|
||||
if (e.target.matches('.button')) {
|
||||
handleClick(e)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**3. 防抖和节流**
|
||||
|
||||
```javascript
|
||||
// 防抖:用户停止输入后再执行
|
||||
function debounce(fn, delay) {
|
||||
let timer
|
||||
return function(...args) {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => fn.apply(this, args), delay)
|
||||
}
|
||||
}
|
||||
|
||||
// 节流:限制执行频率
|
||||
function throttle(fn, delay) {
|
||||
let lastTime = 0
|
||||
return function(...args) {
|
||||
const now = Date.now()
|
||||
if (now - lastTime >= delay) {
|
||||
fn.apply(this, args)
|
||||
lastTime = now
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 调试技巧
|
||||
|
||||
**1. 用 DevTools 查看调用栈**
|
||||
|
||||
```javascript
|
||||
function a() {
|
||||
b()
|
||||
}
|
||||
|
||||
function b() {
|
||||
c()
|
||||
}
|
||||
|
||||
function c() {
|
||||
debugger // 在这里暂停,查看调用栈
|
||||
}
|
||||
|
||||
a()
|
||||
```
|
||||
|
||||
**2. 用 `console.trace()` 追踪执行路径**
|
||||
|
||||
```javascript
|
||||
function trackExecution() {
|
||||
console.trace('执行路径')
|
||||
// 会输出完整的调用栈
|
||||
}
|
||||
```
|
||||
|
||||
**3. 用 Performance 分析性能**
|
||||
|
||||
```javascript
|
||||
performance.mark('start')
|
||||
|
||||
// 执行一些代码
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
// ...
|
||||
}
|
||||
|
||||
performance.mark('end')
|
||||
performance.measure('循环性能', 'start', 'end')
|
||||
|
||||
const measure = performance.getEntriesByName('循环性能')[0]
|
||||
console.log(`执行时间: ${measure.duration}ms`)
|
||||
```
|
||||
|
||||
### 6.3 常见问题速查
|
||||
|
||||
| 问题 | 可能原因 | 解决方案 |
|
||||
|------|---------|---------|
|
||||
| **内存占用高** | 内存泄漏、缓存太多 | 检查全局变量、移除监听器 |
|
||||
| **页面卡顿** | 长任务阻塞主线程 | 拆分任务、用 Web Workers |
|
||||
| **事件不触发** | 监听器没绑定、元素不存在 | 检查 DOM 加载时机 |
|
||||
| **异步顺序错乱** | 混用宏任务和微任务 | 统一用 Promise 或 async/await |
|
||||
| **定时器不准** | 主线程阻塞 | 用 Web Workers 或 requestAnimationFrame |
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
你现在应该能理解:
|
||||
|
||||
- **运行时 = 引擎 + 环境 API**,不同运行时提供不同能力
|
||||
- **事件循环**负责协调同步代码、微任务、宏任务的执行顺序
|
||||
- **调用栈**记录函数执行过程,**栈溢出**是因为递归太深
|
||||
- **垃圾回收**自动清理不用的变量,但要注意**内存泄漏**
|
||||
- **性能优化**的关键是减少重排重绘、合理使用异步
|
||||
|
||||
::: info 💡 遇到问题时这样跟 AI 说
|
||||
- "这个函数执行太慢,帮我看看怎么优化性能"
|
||||
- "内存占用一直在涨,可能是内存泄漏,帮我检查一下"
|
||||
- "异步操作顺序不对,应该是先 A 再 B,现在是 A 和 B 几乎同时开始"
|
||||
- "事件监听器没有触发,检查一下元素是否已经加载到 DOM"
|
||||
:::
|
||||
@@ -0,0 +1,3 @@
|
||||
# 实时通信(WebSocket / SSE)
|
||||
|
||||
> 待实现
|
||||
@@ -1,5 +1,4 @@
|
||||
# 前端路由:单页应用的导航系统
|
||||
|
||||
# 路由与导航
|
||||
::: tip 🎯 核心问题
|
||||
**为什么有些网站切换页面时不会白屏刷新,像 App 一样流畅?** 这就是前端路由的魔法。本章将带你从传统网站的"翻书式跳转",进入到单页应用的"幻灯片切换"世界,理解前端路由如何让用户体验提升一个档次。
|
||||
:::
|
||||
@@ -1,5 +1,4 @@
|
||||
# 组件化与状态管理模式总览
|
||||
|
||||
# 状态管理哲学
|
||||
::: tip 🎯 核心问题
|
||||
**当应用越来越大,组件之间该如何优雅地共享和同步数据?** 你可能会遇到这样的困境:用户在商品页添加了购物车,但头部的购物车数量没更新;两个不相关的组件需要同一份数据,却不知道该怎么传递。本章将带你从"混乱的数据传递"进化到"清晰的状态管理"。
|
||||
:::
|
||||
@@ -0,0 +1,823 @@
|
||||
# TypeScript 深度指南
|
||||
|
||||
::: tip 前言
|
||||
你已经会写 JavaScript 了,但可能遇到过这些问题:
|
||||
- 变量赋值了错误类型,运行时才发现
|
||||
- 对象属性写错了名字,调试半天
|
||||
- 函数参数类型不对,改来改去
|
||||
|
||||
TypeScript 就是在代码运行前帮你发现这些问题的工具。读完这篇,你就能理解 TypeScript 为什么能提升代码质量,看懂类型注解、接口、泛型等核心概念,在 vibecoding 中更好地利用 AI 生成的代码。
|
||||
:::
|
||||
|
||||
**这篇文章会带你学什么?**
|
||||
|
||||
| 章节 | 内容 | 学完能干嘛 |
|
||||
|-----|------|-----------|
|
||||
| **第 1 章** | TypeScript 是什么 | 明白它和 JavaScript 的关系 |
|
||||
| **第 2 章** | 基础类型注解 | 知道怎么给变量标注类型 |
|
||||
| **第 3 章** | 对象类型与接口 | 定义数据结构的类型 |
|
||||
| **第 4 章** | 函数类型 | 给函数参数和返回值标注类型 |
|
||||
| **第 5 章** | 泛型 | 编写可复用的类型安全代码 |
|
||||
| **第 6 章** | 类型推断与实用技巧 | 知道何时需要显式注解 |
|
||||
|
||||
---
|
||||
|
||||
## 1. TypeScript 是什么
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**JavaScript 已经够用了,为什么还需要 TypeScript?** 多学一门语法值得吗?
|
||||
:::
|
||||
|
||||
### 1.1 从"运行时出错"到"编译时发现"
|
||||
|
||||
<div style="display: flex; gap: 20px; margin: 20px 0;">
|
||||
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||
|
||||
**🔴 JavaScript 的痛点**
|
||||
- 运行时才发现类型错误
|
||||
- 拼写错误难以察觉
|
||||
- 重构时容易遗漏
|
||||
- IDE 提示不够准确
|
||||
|
||||
*就像没有拼写检查的文档编辑器*
|
||||
|
||||
</div>
|
||||
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||
|
||||
**✅ TypeScript 的优势**
|
||||
- 写代码时就发现错误
|
||||
- 智能提示更准确
|
||||
- 重构更安全
|
||||
- 代码更易维护
|
||||
|
||||
*就像有拼写检查和语法高亮的编辑器*
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
**用一句话理解两者的关系:**
|
||||
|
||||
| 技术 | 比喻 | 作用 |
|
||||
|------|------|------|
|
||||
| **JavaScript** | 原始材料 | 可以直接运行的代码 |
|
||||
| **TypeScript** | 蓝图 + 质检 | 给 JavaScript 加类型检查,最后编译成 JavaScript |
|
||||
|
||||
### 1.2 为什么 vibecoding 也需要 TypeScript?
|
||||
|
||||
::: warning AI 写代码也会出错
|
||||
一位开发者用 AI 生成了一个用户管理功能。AI 写的 JavaScript 代码能运行,但有个问题:用户年龄应该是数字,但有时候会被错误地赋值为字符串。
|
||||
|
||||
结果在计算"是否成年"时,字符串 "25" 被当成字符串处理,导致判断失败。这个 bug 隐藏了很久,直到某个用户输入了非数字字符才暴露出来。
|
||||
|
||||
如果用 TypeScript,这段代码在写的时候就会报错:`不能将类型 "string" 分配给类型 "number"`。
|
||||
|
||||
**这就是 TypeScript 的价值——在 AI 写错类型时,你能第一时间发现。**
|
||||
:::
|
||||
|
||||
### 1.3 TypeScript 实际上是这样的
|
||||
|
||||
TypeScript 不是一门全新的语言,它只是 JavaScript 的"超集":
|
||||
|
||||
```typescript
|
||||
// 这是有效的 JavaScript,也是有效的 TypeScript
|
||||
const name = "张三"
|
||||
const age = 25
|
||||
function greet(user) {
|
||||
return `Hello ${user}`
|
||||
}
|
||||
|
||||
// 这是 TypeScript 特有的类型注解
|
||||
const name2: string = "李四"
|
||||
const age2: number = 30
|
||||
function greet2(user: string): string {
|
||||
return `Hello ${user}`
|
||||
}
|
||||
```
|
||||
|
||||
**关键理解:**
|
||||
- 所有 JavaScript 代码都是有效的 TypeScript 代码
|
||||
- TypeScript 添加了可选的**类型注解**
|
||||
- TypeScript 最终会编译成 JavaScript 运行
|
||||
|
||||
::: info 💡 核心启示
|
||||
TypeScript 不会改变代码的运行方式,它只是在编译时帮你检查类型是否正确。**你可以渐进地采用 TypeScript**——从给关键变量添加类型开始。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 基础类型注解
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**怎么告诉 TypeScript 一个变量应该是什么类型?** 类型注解的语法是怎样的?
|
||||
:::
|
||||
|
||||
### 2.1 类型注解语法
|
||||
|
||||
类型注解就是在变量名后面加上`: 类型`:
|
||||
|
||||
```typescript
|
||||
// 语法:变量名: 类型 = 值
|
||||
const name: string = "张三"
|
||||
let age: number = 25
|
||||
let isStudent: boolean = true
|
||||
```
|
||||
|
||||
👇 **动手试试看**:给变量添加类型注解
|
||||
|
||||
<TypeAnnotationDemo />
|
||||
|
||||
::: details 🔍 为什么有些地方不需要类型注解?
|
||||
TypeScript 可以根据赋值自动推断类型:
|
||||
|
||||
```typescript
|
||||
// 这些不需要类型注解,TypeScript 能自动推断
|
||||
const name = "张三" // 推断为 string
|
||||
const age = 25 // 推断为 number
|
||||
const isActive = true // 推断为 boolean
|
||||
|
||||
// 这些情况需要显式注解
|
||||
let data // ❌ 错误:不能推断类型
|
||||
let data: any // ✅ 可以,但失去了类型检查的好处
|
||||
|
||||
function add(a, b) { // ❌ 参数类型不明确
|
||||
return a + b
|
||||
}
|
||||
|
||||
function add2(a: number, b: number): number { // ✅ 类型明确
|
||||
return a + b
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
### 2.2 基本类型
|
||||
|
||||
TypeScript 支持所有 JavaScript 的基本类型:
|
||||
|
||||
| 类型 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `string` | 字符串 | `"hello"`, `'你好'` |
|
||||
| `number` | 数字(整数和小数) | `42`, `3.14` |
|
||||
| `boolean` | 布尔值 | `true`, `false` |
|
||||
| `null` / `undefined` | 空值 | `null`, `undefined` |
|
||||
| `array` | 数组 | `number[]`, `string[]` |
|
||||
| `object` | 对象 | `{ name: string; age: number }` |
|
||||
|
||||
**数组类型的两种写法:**
|
||||
|
||||
```typescript
|
||||
// 写法 1:类型[](更常用)
|
||||
const numbers: number[] = [1, 2, 3, 4, 5]
|
||||
const names: string[] = ["张三", "李四", "王五"]
|
||||
|
||||
// 写法 2:Array<类型>
|
||||
const numbers2: Array<number> = [1, 2, 3, 4, 5]
|
||||
const names2: Array<string> = ["张三", "李四", "王五"]
|
||||
```
|
||||
|
||||
**特殊类型:**
|
||||
|
||||
```typescript
|
||||
// any:任意类型(慎用,相当于关闭类型检查)
|
||||
let data: any = 42
|
||||
data = "现在可以是字符串"
|
||||
data = { name: "张三" } // 也可以是对象
|
||||
|
||||
// unknown:类型安全的 any
|
||||
let value: unknown = 42
|
||||
// if (typeof value === "number") {
|
||||
// console.log(value + 10) // 需要先检查类型才能用
|
||||
// }
|
||||
|
||||
// void:没有返回值
|
||||
function log(message: string): void {
|
||||
console.log(message)
|
||||
}
|
||||
|
||||
// never:永远不会返回
|
||||
function error(message: string): never {
|
||||
throw new Error(message)
|
||||
}
|
||||
```
|
||||
|
||||
::: info 💡 识别技巧
|
||||
- 看到 `: string` → 这是 string 类型的注解
|
||||
- 看到 `: number[]` → 这是数字数组的注解
|
||||
- 看到 `: void` → 这个函数没有返回值
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 对象类型与接口
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**怎么定义一个对象的类型?** 对象的属性应该是什么类型?
|
||||
:::
|
||||
|
||||
### 3.1 接口(Interface):定义对象的"形状"
|
||||
|
||||
接口是 TypeScript 中定义对象类型的主要方式:
|
||||
|
||||
```typescript
|
||||
// 定义一个 User 接口
|
||||
interface User {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
age?: number // 可选属性
|
||||
}
|
||||
|
||||
// 使用接口
|
||||
const user: User = {
|
||||
id: 1,
|
||||
name: "张三",
|
||||
email: "zhangsan@example.com",
|
||||
age: 25
|
||||
}
|
||||
|
||||
// age 是可选的,可以不提供
|
||||
const user2: User = {
|
||||
id: 2,
|
||||
name: "李四",
|
||||
email: "lisi@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
👇 **动手试试看**:创建符合接口定义的对象
|
||||
|
||||
<InterfaceDemo />
|
||||
|
||||
::: details 🔍 接口的其他特性
|
||||
```typescript
|
||||
// 只读属性
|
||||
interface User {
|
||||
readonly id: number // id 创建后不能修改
|
||||
name: string
|
||||
}
|
||||
|
||||
const user: User = {
|
||||
id: 1,
|
||||
name: "张三"
|
||||
}
|
||||
|
||||
user.id = 2 // ❌ 错误:不能修改只读属性
|
||||
user.name = "李四" // ✅ 可以修改
|
||||
|
||||
// 函数类型
|
||||
interface User {
|
||||
name: string
|
||||
greet: () => string // greet 是一个函数,返回 string
|
||||
}
|
||||
|
||||
const user: User = {
|
||||
name: "张三",
|
||||
greet: () => "Hello"
|
||||
}
|
||||
|
||||
// 继承接口
|
||||
interface Admin extends User {
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
const admin: Admin = {
|
||||
name: "管理员",
|
||||
greet: () => "Hello Admin",
|
||||
permissions: ["read", "write", "delete"]
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
### 3.2 类型别名(Type Alias)
|
||||
|
||||
除了接口,还可以用 `type` 定义类型别名:
|
||||
|
||||
```typescript
|
||||
// 类型别名
|
||||
type User = {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
// 联合类型
|
||||
type Status = "pending" | "success" | "error"
|
||||
|
||||
const status: Status = "success" // ✅
|
||||
// const status2: Status = "failed" // ❌ 错误:不在联合类型中
|
||||
|
||||
// 交叉类型(合并多个类型)
|
||||
type User = {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
type Timestamp = {
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
type UserWithTimestamp = User & Timestamp
|
||||
|
||||
const user: UserWithTimestamp = {
|
||||
id: 1,
|
||||
name: "张三",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
```
|
||||
|
||||
**接口 vs 类型别名:**
|
||||
|
||||
| 特性 | interface | type |
|
||||
|------|-----------|------|
|
||||
| 扩展 | `extends` | `&` 交叉类型 |
|
||||
| 重复声明 | 会自动合并 | 会报错 |
|
||||
| 适用场景 | 对象形状、类 | 联合类型、交叉类型、基本类型别名 |
|
||||
|
||||
::: info 💡 识别技巧
|
||||
- 看到 `interface` → 这是定义对象类型
|
||||
- 看到 `type` → 这是创建类型别名
|
||||
- 看到 `?` → 这是可选属性
|
||||
- 看到 `readonly` → 这是只读属性
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 函数类型
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**怎么给函数的参数和返回值标注类型?**
|
||||
:::
|
||||
|
||||
### 4.1 参数类型与返回值类型
|
||||
|
||||
```typescript
|
||||
// 完整的函数类型注解
|
||||
function add(a: number, b: number): number {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// 箭头函数
|
||||
const multiply = (a: number, b: number): number => {
|
||||
return a * b
|
||||
}
|
||||
|
||||
// 没有返回值
|
||||
function log(message: string): void {
|
||||
console.log(message)
|
||||
}
|
||||
|
||||
// 返回多种类型(联合类型)
|
||||
function parseInput(input: string): number | string {
|
||||
const num = parseFloat(input)
|
||||
return isNaN(num) ? input : num
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 可选参数与默认参数
|
||||
|
||||
```typescript
|
||||
// 可选参数(用 ? 标记)
|
||||
function greet(name: string, title?: string): string {
|
||||
return title ? `${title} ${name}` : name
|
||||
}
|
||||
|
||||
greet("张三") // "张三"
|
||||
greet("张三", "先生") // "先生 张三"
|
||||
|
||||
// 默认参数
|
||||
function greet2(name: string, title: string = "朋友"): string {
|
||||
return `${title} ${name}`
|
||||
}
|
||||
|
||||
greet2("李四") // "朋友 李四"
|
||||
greet2("李四", "博士") // "博士 李四"
|
||||
```
|
||||
|
||||
### 4.3 函数类型作为参数
|
||||
|
||||
```typescript
|
||||
// 接受函数作为参数
|
||||
function calculate(
|
||||
a: number,
|
||||
b: number,
|
||||
operation: (x: number, y: number) => number
|
||||
): number {
|
||||
return operation(a, b)
|
||||
}
|
||||
|
||||
calculate(10, 5, (x, y) => x + y) // 15
|
||||
calculate(10, 5, (x, y) => x * y) // 50
|
||||
|
||||
// 更清晰的写法:先定义函数类型
|
||||
type Operation = (x: number, y: number) => number
|
||||
|
||||
function calculate2(
|
||||
a: number,
|
||||
b: number,
|
||||
operation: Operation
|
||||
): number {
|
||||
return operation(a, b)
|
||||
}
|
||||
```
|
||||
|
||||
::: info 💡 识别技巧
|
||||
- 看到 `(a: number, b: number) => number` → 这是函数类型,描述参数和返回值
|
||||
- 看到 `: void` → 函数没有返回值
|
||||
- 看到 `?` → 参数是可选的
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 泛型
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**怎么编写能处理多种类型、但保持类型安全的代码?**
|
||||
:::
|
||||
|
||||
### 5.1 泛型的基本概念
|
||||
|
||||
泛型让你在定义函数、接口或类时,不预先指定具体的类型,而是在使用时再指定:
|
||||
|
||||
```typescript
|
||||
// 泛型函数:T 是类型变量
|
||||
function identity<T>(arg: T): T {
|
||||
return arg
|
||||
}
|
||||
|
||||
// 使用时明确指定类型
|
||||
const num1 = identity<number>(42) // 类型是 number
|
||||
const str1 = identity<string>("hello") // 类型是 string
|
||||
|
||||
// 类型推断:TypeScript 能自动推断
|
||||
const num2 = identity(42) // 推断为 number
|
||||
const str2 = identity("hello") // 推断为 string
|
||||
```
|
||||
|
||||
👇 **动手试试看**:使用泛型处理不同类型的数据
|
||||
|
||||
<GenericDemo />
|
||||
|
||||
### 5.2 泛型约束
|
||||
|
||||
限制泛型必须满足某些条件:
|
||||
|
||||
```typescript
|
||||
// 约束 T 必须有 length 属性
|
||||
interface HasLength {
|
||||
length: number
|
||||
}
|
||||
|
||||
function logLength<T extends HasLength>(arg: T): void {
|
||||
console.log(arg.length)
|
||||
}
|
||||
|
||||
logLength("hello") // ✅ 字符串有 length
|
||||
logLength([1, 2, 3]) // ✅ 数组有 length
|
||||
// logLength(42) // ❌ 数字没有 length 属性
|
||||
```
|
||||
|
||||
### 5.3 泛型接口和类
|
||||
|
||||
```typescript
|
||||
// 泛型接口
|
||||
interface Box<T> {
|
||||
value: T
|
||||
getValue(): T
|
||||
}
|
||||
|
||||
const numberBox: Box<number> = {
|
||||
value: 42,
|
||||
getValue: () => 42
|
||||
}
|
||||
|
||||
const stringBox: Box<string> = {
|
||||
value: "hello",
|
||||
getValue: () => "hello"
|
||||
}
|
||||
|
||||
// 泛型类
|
||||
class Storage<T> {
|
||||
private items: T[] = []
|
||||
|
||||
add(item: T): void {
|
||||
this.items.push(item)
|
||||
}
|
||||
|
||||
get(index: number): T {
|
||||
return this.items[index]
|
||||
}
|
||||
}
|
||||
|
||||
const numberStorage = new Storage<number>()
|
||||
numberStorage.add(1)
|
||||
numberStorage.add(2)
|
||||
// numberStorage.add("string") // ❌ 错误
|
||||
|
||||
const stringStorage = new Storage<string>()
|
||||
stringStorage.add("hello")
|
||||
// stringStorage.add(1) // ❌ 错误
|
||||
```
|
||||
|
||||
::: info 💡 识别技巧
|
||||
- 看到 `<T>` → 这是泛型类型变量
|
||||
- 看到 `<T extends SomeType>` → 泛型约束
|
||||
- 看到 `Array<T>` 或 `Promise<T>` → 内置泛型类型
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. 类型推断与实用技巧
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**什么时候需要显式类型注解?什么时候可以依赖推断?**
|
||||
:::
|
||||
|
||||
### 6.1 类型推断
|
||||
|
||||
TypeScript 能根据上下文自动推断类型:
|
||||
|
||||
```typescript
|
||||
// 变量初始化时的推断
|
||||
const name = "张三" // 推断为 string
|
||||
const age = 25 // 推断为 number
|
||||
const isActive = true // 推断为 boolean
|
||||
|
||||
// 数组推断
|
||||
const numbers = [1, 2, 3] // 推断为 number[]
|
||||
const mixed = [1, "hello", true] // 推断为 (number | string | boolean)[]
|
||||
|
||||
// 函数返回值推断
|
||||
function add(a: number, b: number) {
|
||||
return a + b // 推断返回值为 number
|
||||
}
|
||||
```
|
||||
|
||||
👇 **动手试试看**:观察 TypeScript 如何推断类型
|
||||
|
||||
<TypeInferenceDemo />
|
||||
|
||||
### 6.2 何时使用显式类型注解
|
||||
|
||||
::: details 推荐使用类型推断的场景
|
||||
```typescript
|
||||
// ✅ 推荐:简单的字面量赋值
|
||||
const count = 0
|
||||
const name = "张三"
|
||||
const isActive = true
|
||||
|
||||
// ✅ 推荐:函数返回值可以推断
|
||||
function getUserId(user: User) {
|
||||
return user.id // 推断为 number
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
::: details 推荐使用显式注解的场景
|
||||
```typescript
|
||||
// ✅ 推荐:函数参数(必须)
|
||||
function add(a: number, b: number) {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// ✅ 推荐:对象属性类型不明确
|
||||
const user: {
|
||||
id: number
|
||||
name: string
|
||||
metadata: Record<string, any>
|
||||
} = {
|
||||
id: 1,
|
||||
name: "张三",
|
||||
metadata: {} // 可能推断为 {},需要明确指定
|
||||
}
|
||||
|
||||
// ✅ 推荐:函数返回类型复杂
|
||||
function getUser(): User | null {
|
||||
// ...
|
||||
return null
|
||||
}
|
||||
|
||||
// ✅ 推荐:公共 API
|
||||
export function calculateTotal(prices: number[]): number {
|
||||
return prices.reduce((sum, price) => sum + price, 0)
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
### 6.3 类型守卫
|
||||
|
||||
在运行时检查类型:
|
||||
|
||||
```typescript
|
||||
// typeof 类型守卫
|
||||
function processValue(value: string | number) {
|
||||
if (typeof value === "string") {
|
||||
// 这里 TypeScript 知道 value 是 string
|
||||
console.log(value.toUpperCase())
|
||||
} else {
|
||||
// 这里 TypeScript 知道 value 是 number
|
||||
console.log(value * 2)
|
||||
}
|
||||
}
|
||||
|
||||
// instanceof 类型守卫
|
||||
class Dog {
|
||||
bark() {
|
||||
console.log("汪汪")
|
||||
}
|
||||
}
|
||||
|
||||
class Cat {
|
||||
meow() {
|
||||
console.log("喵喵")
|
||||
}
|
||||
}
|
||||
|
||||
function makeSound(animal: Dog | Cat) {
|
||||
if (animal instanceof Dog) {
|
||||
animal.bark() // TypeScript 知道这是 Dog
|
||||
} else {
|
||||
animal.meow() // TypeScript 知道这是 Cat
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义类型守卫
|
||||
interface User {
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
function isUser(value: any): value is User {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
typeof value.name === "string" &&
|
||||
typeof value.email === "string"
|
||||
)
|
||||
}
|
||||
|
||||
function processValue(value: unknown) {
|
||||
if (isUser(value)) {
|
||||
// 这里 value 是 User
|
||||
console.log(value.name)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 实用工具类型
|
||||
|
||||
TypeScript 提供了一些内置的工具类型:
|
||||
|
||||
```typescript
|
||||
// Partial:将所有属性变为可选
|
||||
interface User {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
type PartialUser = Partial<User>
|
||||
// 等价于:{ id?: number; name?: string; email?: string }
|
||||
|
||||
// Required:将所有属性变为必需
|
||||
type RequiredUser = Required<PartialUser>
|
||||
// 等价于:{ id: number; name: number; email: string }
|
||||
|
||||
// Pick:只保留指定的属性
|
||||
type UserBasicInfo = Pick<User, "id" | "name">
|
||||
// 等价于:{ id: number; name: string }
|
||||
|
||||
// Omit:排除指定的属性
|
||||
type UserWithoutEmail = Omit<User, "email">
|
||||
// 等价于:{ id: number; name: string }
|
||||
|
||||
// Record:创建对象类型
|
||||
type UserRoles = Record<string, boolean>
|
||||
// 等价于:{ [key: string]: boolean }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 实战技巧:在 vibecoding 中使用 TypeScript
|
||||
|
||||
::: tip 🤔 核心问题
|
||||
**怎么在 AI 辅助开发中更好地利用 TypeScript?**
|
||||
:::
|
||||
|
||||
### 7.1 让 AI 生成类型安全代码
|
||||
|
||||
**❌ 不好的提示词:**
|
||||
```
|
||||
帮我写一个用户管理功能
|
||||
```
|
||||
|
||||
**✅ 好的提示词:**
|
||||
```
|
||||
帮我写一个用户管理功能,使用 TypeScript。
|
||||
|
||||
数据结构定义如下:
|
||||
interface User {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
age: number
|
||||
}
|
||||
|
||||
需要实现:
|
||||
1. 获取用户列表:返回 User[]
|
||||
2. 创建用户:接受 Partial<User>,返回 User
|
||||
3. 更新用户:接受 id 和 Partial<User>,返回 User
|
||||
4. 删除用户:接受 id,返回 void
|
||||
|
||||
请确保所有函数都有完整的类型注解。
|
||||
```
|
||||
|
||||
### 7.2 看懂 TypeScript 错误信息
|
||||
|
||||
**常见错误及含义:**
|
||||
|
||||
| 错误信息 | 含义 | 解决方法 |
|
||||
|---------|------|---------|
|
||||
| `Type 'X' is not assignable to type 'Y'` | 类型 X 不能赋值给类型 Y | 检查类型是否匹配,或进行类型转换 |
|
||||
| `Property 'X' does not exist on type 'Y'` | 类型 Y 上不存在属性 X | 检查属性名拼写,或定义该属性 |
|
||||
| `Argument of type 'X' is not assignable to parameter of type 'Y'` | 参数类型不匹配 | 检查函数调用时的参数类型 |
|
||||
| `Type 'X' is missing the following properties from type 'Y'` | 类型 X 缺少类型 Y 的某些属性 | 补全缺失的属性 |
|
||||
|
||||
### 7.3 渐进式采用 TypeScript
|
||||
|
||||
如果你有一个 JavaScript 项目,可以渐进地迁移到 TypeScript:
|
||||
|
||||
1. **第一步:将文件重命名为 `.ts`**
|
||||
```bash
|
||||
# 从 utils.js 改为 utils.ts
|
||||
mv utils.js utils.ts
|
||||
```
|
||||
|
||||
2. **第二步:修复明显的类型错误**
|
||||
```typescript
|
||||
// 如果报错:Parameter 'a' implicitly has an 'any' type
|
||||
// 添加类型注解
|
||||
function add(a: number, b: number) {
|
||||
return a + b
|
||||
}
|
||||
```
|
||||
|
||||
3. **第三步:逐步添加类型定义**
|
||||
```typescript
|
||||
// 先用 any 快速修复
|
||||
function processUser(user: any) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 后续再完善类型
|
||||
interface User {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
function processUser(user: User) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
4. **第四步:启用更严格的类型检查**
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true, // 启用严格模式
|
||||
"noImplicitAny": true, // 禁止隐式 any
|
||||
"strictNullChecks": true // 严格空值检查
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 你现在应该能识别的代码
|
||||
|
||||
- 看到 `: string` → 这是 string 类型的注解
|
||||
- 看到 `: number[]` → 这是数字数组的注解
|
||||
- 看到 `interface User` → 这是定义对象类型
|
||||
- 看到 `type User =` → 这是类型别名
|
||||
- 看到 `<T>` → 这是泛型
|
||||
- 看到 `extends` → 接口继承或泛型约束
|
||||
- 看到 `?` → 可选属性
|
||||
- 看到 `readonly` → 只读属性
|
||||
- 看到 `|` → 联合类型
|
||||
- 看到 `&` → 交叉类型
|
||||
|
||||
**如果你认真读了每章的"深入"部分,你还掌握了这些核心概念:**
|
||||
|
||||
- **类型注解**:明确告诉 TypeScript 变量的类型
|
||||
- **接口**:定义对象的结构和类型
|
||||
- **泛型**:编写可复用的类型安全代码
|
||||
- **类型推断**:TypeScript 自动推断类型
|
||||
- **类型守卫**:运行时检查类型
|
||||
- **工具类型**:Partial、Required、Pick、Omit 等
|
||||
|
||||
::: info 💡 遇到问题时这样跟 AI 说
|
||||
- "这个函数的类型注解应该怎么写?参数是 X,返回值是 Y"
|
||||
- "帮我定义一个接口,描述这个数据结构:..."
|
||||
- "这个 TypeScript 错误是什么意思?怎么修复?"
|
||||
- "如何给这个泛型函数添加约束,确保 T 必须有某个属性?"
|
||||
:::
|
||||
@@ -1,5 +1,4 @@
|
||||
# 前端性能优化
|
||||
|
||||
# 网页性能的度量与优化
|
||||
::: tip 🎯 核心问题
|
||||
**为什么你的网页加载很慢,用户还在疯狂抱怨卡顿?** 这就像是问:为什么餐厅上菜慢、顾客等得不耐烦?本章将带你深入理解前端性能优化的核心概念,让你的网页"飞"起来。
|
||||
:::
|
||||
@@ -0,0 +1,553 @@
|
||||
# API 设计哲学(REST / GraphQL / gRPC)
|
||||
::: tip 🎯 核心问题
|
||||
**前后端如何高效对话?** 这就像问:餐厅的菜单怎么设计,客人一看就懂?服务员怎么记单,不会出错?上菜怎么规范,客人满意?API 设计解决的就是"对话规则"的问题。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要"API 设计"?
|
||||
|
||||
### 1.1 从混乱到规范
|
||||
|
||||
想象一下你走进一家餐厅:
|
||||
|
||||
- **菜单(API 文档)**:清楚标注了每道菜的口味、配料、价格
|
||||
- **服务员(HTTP 协议)**:用标准化的方式记录你的点餐
|
||||
- **后厨(服务端)**:按约定流程烹饪
|
||||
- **上菜(响应)**:盘子摆盘规范,附带小票说明
|
||||
|
||||
**好的 API 设计就像这套点餐系统**——双方约定好"说什么话"、"怎么说话"、"出错怎么办",才能高效协作。
|
||||
|
||||
但很多团队的真实情况是:
|
||||
|
||||
- 接口命名随心所欲:`/getUserData`、`/fetchUserInfo`、`/queryUserById` 并存
|
||||
- 错误处理五花八门:有的返回 HTTP 状态码,有的返回 `code: 500`,有的直接抛异常
|
||||
- 响应结构千人千面:同一个项目里,有的用 `data`,有的用 `result`,有的用 `content`
|
||||
|
||||
<RestfulDesignDemo />
|
||||
|
||||
**结果就是**:前后端互相猜,联调痛苦,维护成本高,新人入职一脸懵。
|
||||
|
||||
::: tip 💡 通俗解释
|
||||
**API**(Application Programming Interface,应用程序编程接口)就像"餐厅的服务协议":
|
||||
|
||||
- **RESTful** = 餐厅点餐:有菜单、有流程、有规范
|
||||
- **GraphQL** = 自助餐:想要什么,自己组合
|
||||
- **gRPC** = 快递:二进制格式,速度最快
|
||||
|
||||
**为什么需要设计?**
|
||||
|
||||
- 没有设计 = 服务员随机记单 → 后厨看不懂、客人等半天
|
||||
- 有设计 = 标准化流程 → 效率高、错误少
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. RESTful 设计:让你的 URL 会说话
|
||||
|
||||
### 2.1 什么是 RESTful?
|
||||
|
||||
**REST**(Representational State Transfer,表述性状态传递)是一种软件架构风格,由 Roy Fielding 在 2000 年提出。
|
||||
|
||||
**核心思想**:把网络上的所有事物都抽象为"资源"(Resource),用 URL 标识资源,用 HTTP 方法操作资源。
|
||||
|
||||
<ResourceAnalogyDemo />
|
||||
|
||||
::: tip 💡 资源是什么?
|
||||
**资源**(Resource)是 REST 架构的核心概念:
|
||||
|
||||
- 有唯一标识(URL)
|
||||
- 有表达方式(JSON/XML/HTML)
|
||||
- 有操作方法(GET/POST/PUT/DELETE)
|
||||
|
||||
**比喻**:
|
||||
|
||||
- URL 是"仓库的货架地址":`/warehouse/products` 是"产品区"
|
||||
- HTTP 方法是"允许的操作":GET(查看)、POST(入库)、PUT(更新)、DELETE(出库)
|
||||
|
||||
**关键点**:
|
||||
|
||||
- URL 是名词,不是动词:`/users` 而不是 `/getUsers`
|
||||
- HTTP 方法已经表达了操作,不需要在 URL 里重复
|
||||
:::
|
||||
|
||||
### 2.2 URL 设计的 7 个黄金法则
|
||||
|
||||
<HttpMethodsDemo />
|
||||
|
||||
| 法则 | 正确示例 | 错误示例 | 原因 |
|
||||
| ---------------------- | --------------------------------------------- | ----------------------------------------- | ---------------------------------------- |
|
||||
| **1. 用名词,不用动词** | `GET /users` | `GET /getUsers` | URL 是资源地址,不是操作 |
|
||||
| **2. 用复数** | `GET /orders` | `GET /order` | 一致性好,表示集合 |
|
||||
| **3. 小写字母** | `/user-profiles` | `/UserProfiles` | URL 大小写敏感,统一小写避免混乱 |
|
||||
| **4. 用连字符** | `/user-profiles` | `/user_profiles` | 连字符是 URL 规范,下划线在某些场景会转义 |
|
||||
| **5. 避免层级过深** | `/users/123/orders` | `/users/123/orders/456/items/789/status` | 超过 3 层考虑用查询参数或重构 |
|
||||
| **6. 用查询参数过滤** | `GET /products?category=phone&price_max=5000` | `GET /products/category/phone/price/5000` | 过滤条件多且变动,不适合放路径 |
|
||||
| **7. 版本号放 URL** | `/v1/users` 或 `v1.users.api.com` | 混用新旧接口无版本 | 便于灰度发布和向后兼容 |
|
||||
|
||||
::: tip 📊 从表格中你能看到什么?
|
||||
**规则 1-4**是"一致性"原则:
|
||||
|
||||
- 统一名词、复数、大小写,让团队所有人写的 URL 风格一致
|
||||
- 减少认知负担,一眼就知道这是什么资源
|
||||
|
||||
**规则 5-6**是"灵活性"原则:
|
||||
|
||||
- 层级太深 = 耦心度太高,难以维护
|
||||
- 查询参数 = 更灵活,可以组合任意过滤条件
|
||||
|
||||
**规则 7**是"兼容性"原则:
|
||||
|
||||
- API 是长期契约,不能随意改
|
||||
- 版本号让新旧接口并存,平滑升级
|
||||
:::
|
||||
|
||||
### 2.3 HTTP 方法的选择
|
||||
|
||||
| 方法 | 用途 | 幂等性\* | 安全性\*\* | 典型场景 |
|
||||
| ---------- | -------- | -------- | ---------- | -------------------------- |
|
||||
| **GET** | 获取资源 | 是 | 是 | 查询列表、查看详情 |
|
||||
| **POST** | 创建资源 | 否 | 否 | 新增用户、提交订单 |
|
||||
| **PUT** | 全量更新 | 是 | 否 | 替换整个用户资料 |
|
||||
| **PATCH** | 部分更新 | 否 | 否 | 修改用户昵称(只传一个字段) |
|
||||
| **DELETE** | 删除资源 | 是 | 否 | 删除用户、取消订单 |
|
||||
|
||||
> **\*幂等性**:多次执行结果相同。比如 PUT 同一个资源 10 次,结果还是那一个资源。\***\*安全性**:不会改变服务器状态。GET 是安全的,POST/PUT/DELETE 都不安全。
|
||||
|
||||
::: details 🔧 幂等性为什么重要?
|
||||
**场景**:用户点击"支付"按钮,但网络延迟,用户又点了一次。
|
||||
|
||||
- **幂等的操作**(PUT/DELETE):点击 10 次和点击 1 次,结果相同。不会重复扣款。
|
||||
- **不幂等的操作**(POST):点击 10 次,可能创建 10 个订单。
|
||||
|
||||
**解决方案**:
|
||||
|
||||
- **客户端**:按钮点击后禁用,防止重复提交
|
||||
- **服务端**:POST 操作用唯一 ID 校验,避免重复处理
|
||||
|
||||
**关键点**:幂等性是分布式系统正确性的重要保证。
|
||||
:::
|
||||
|
||||
### 2.4 实战示例:电商系统的 RESTful API
|
||||
|
||||
```
|
||||
# 用户模块
|
||||
GET /v1/users # 获取用户列表(支持分页、过滤)
|
||||
POST /v1/users # 创建新用户
|
||||
GET /v1/users/{id} # 获取用户详情
|
||||
PUT /v1/users/{id} # 全量更新用户信息
|
||||
PATCH /v1/users/{id} # 部分更新(如:修改密码)
|
||||
DELETE /v1/users/{id} # 删除用户
|
||||
|
||||
# 订单模块(嵌套资源,最多 3 层)
|
||||
GET /v1/users/{id}/orders # 获取某用户的所有订单
|
||||
POST /v1/users/{id}/orders # 为用户创建订单
|
||||
GET /v1/orders/{orderId} # 获取订单详情(扁平化,避免过深)
|
||||
PATCH /v1/orders/{orderId}/status # 更新订单状态(子资源操作)
|
||||
|
||||
# 商品模块(复杂过滤用查询参数)
|
||||
GET /v1/products?category=electronics&price_min=100&price_max=5000&sort=price_desc&page=2&page_size=20
|
||||
|
||||
# 复杂报表(特殊场景,URL 可带动词)
|
||||
POST /v1/reports/sales/export # 导出销售报表(非纯 CRUD,动词可接受)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 状态码:让错误"会说话"
|
||||
|
||||
### 3.1 为什么状态码很重要?
|
||||
|
||||
<StatusCodeDemo />
|
||||
|
||||
想象一下,如果你的 API 不管成功失败都返回 `200 OK`,客户端该怎么判断?
|
||||
|
||||
```json
|
||||
// ❌ 错误的做法:HTTP 200,但业务失败
|
||||
HTTP/1.1 200 OK
|
||||
{
|
||||
"success": false,
|
||||
"error": "用户不存在"
|
||||
}
|
||||
```
|
||||
|
||||
**问题在哪?**
|
||||
|
||||
- 缓存层(CDN、浏览器)会缓存这个"成功的"响应
|
||||
- 监控工具以为一切正常
|
||||
- 前端需要额外解析 JSON 才知道有没有错
|
||||
|
||||
**正确的做法**:用 HTTP 状态码表示传输层状态,和业务成功/失败解耦。
|
||||
|
||||
### 3.2 常用状态码速查表
|
||||
|
||||
| 状态码 | 含义 | 使用场景 | 响应体内容 |
|
||||
| ------------------------- | -------------- | ---------------------------------------------- | ------------------------ | --- |
|
||||
| **2xx 成功** | | | | |
|
||||
| 200 OK | 通用成功 | GET 查询成功、PUT/PATCH 更新成功 | 资源数据 |
|
||||
| 201 Created | 创建成功 | POST 创建资源成功 | 新资源数据 + Location 头 |
|
||||
| 202 Accepted | 已接受 | 异步任务提交成功(如:导出报表) | 任务状态/轮询地址 |
|
||||
| 204 No Content | 无内容 | DELETE 删除成功、PUT 更新但无需返回数据 | 空(用缓存) |
|
||||
| **3xx 重定向** | | | | |
|
||||
| 301 Moved Permanently | 永久重定向 | 资源 URL 永久变更(如:v1 废弃,跳转 v2) | 新 URL |
|
||||
| 302 Found | 临时重定向 | 临时跳转(较少用于 API) | 临时 URL |
|
||||
| 304 Not Modified | 未修改 | 缓存有效(配合 If-None-Match/If-Modified-Since) | 空(用缓存) |
|
||||
| **4xx 客户端错误** | | | | |
|
||||
| 400 Bad Request | 请求格式错误 | 参数缺失、JSON 格式错误、字段类型不对 | 错误详情 |
|
||||
| 401 Unauthorized | 未认证 | 缺少 Token、Token 过期、签名错误 | 认证方式说明 |
|
||||
| 403 Forbidden | 禁止访问 | 已登录但无权限(如:普通用户访问管理员接口) | 无权限说明 |
|
||||
| 404 Not Found | 资源不存在 | URL 错误、资源已删除 | 错误详情 |
|
||||
| 405 Method Not Allowed | 方法不允许 | 如:对只读资源调用 POST | 允许的 Methods |
|
||||
| 409 Conflict | 资源冲突 | 重复创建(唯一约束冲突)、乐观锁版本冲突 | 冲突详情 |
|
||||
| 422 Unprocessable Entity | 语义错误 | 请求格式对,但业务校验失败(如:密码太短) | 校验错误详情 |
|
||||
| 429 Too Many Requests | 请求过多 | 触发限流(Rate Limiting) | 重试时间 |
|
||||
| **5xx 服务端错误** | | | | |
|
||||
| 500 Internal Server Error | 服务器内部错误 | 未捕获的异常、代码 bug | 错误 ID(不要暴露堆栈) |
|
||||
| 502 Bad Gateway | 网关错误 | 反向代理(Nginx)无法连接到后端服务 | - |
|
||||
| 503 Service Unavailable | 服务不可用 | 服务正在维护、过载保护触发 | 恢复时间估计 |
|
||||
| 504 Gateway Timeout | 网关超时 | 后端响应太慢,被代理层切断 | - |
|
||||
|
||||
::: tip 📊 从表格中你能看到什么?
|
||||
**2xx(成功)** vs **3xx(重定向)** vs **4xx(客户端错误)** vs **5xx(服务端错误)**:
|
||||
|
||||
- **2xx**:一切正常,客户端可以继续
|
||||
- **3xx**:资源换地方了,告诉客户端去哪找
|
||||
- **4xx**:客户端搞错了,修正请求后重试
|
||||
- **5xx**:服务端出问题了,客户端等一等再试,或者联系管理员
|
||||
|
||||
**关键点**:正确的状态码让客户端、浏览器、CDN、监控工具都能正确理解响应。
|
||||
:::
|
||||
|
||||
### 3.3 状态码使用的"避坑指南"
|
||||
|
||||
**坑 1:所有错误都用 400**
|
||||
|
||||
```
|
||||
❌ GET /users/999 → 400 (用户不存在应该返回 404)
|
||||
❌ POST /login 密码错误 → 400 (应该返回 401 或 422)
|
||||
❌ 删除已删除的资源 → 400 (应该返回 404 或 204)
|
||||
```
|
||||
|
||||
**坑 2:业务状态混在 HTTP 状态码里**
|
||||
|
||||
```
|
||||
❌ 订单支付失败 → 402 Payment Required (这个状态码是为数字钱包预留的,不要滥用)
|
||||
✅ 订单支付失败 → 200 OK + body: { "code": "PAYMENT_FAILED", "message": "余额不足" }
|
||||
```
|
||||
|
||||
**坑 3:暴露敏感信息**
|
||||
|
||||
```
|
||||
❌ 500 响应里返回完整的堆栈跟踪、SQL 查询语句、数据库连接信息
|
||||
✅ 只返回 "错误 ID",详细日志记录到服务器,通过错误 ID 关联
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 请求与响应:标准化的数据契约
|
||||
|
||||
### 4.1 请求结构设计
|
||||
|
||||
<RequestStructureDemo />
|
||||
|
||||
**HTTP 请求由 3 部分组成**:
|
||||
|
||||
```http
|
||||
# 1. 请求行(方法 + URL + 协议版本)
|
||||
POST /v1/users HTTP/1.1
|
||||
|
||||
# 2. 请求头(元数据)
|
||||
Host: api.example.com
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||
X-Request-ID: req-123456789
|
||||
Accept: application/json
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
|
||||
# 3. 请求体(仅 POST/PUT/PATCH 需要)
|
||||
{
|
||||
"name": "张三",
|
||||
"email": "zhangsan@example.com",
|
||||
"phone": "13800138000"
|
||||
}
|
||||
```
|
||||
|
||||
#### 查询参数设计规范
|
||||
|
||||
```http
|
||||
# 分页(必须)
|
||||
GET /products?page=1&page_size=20
|
||||
|
||||
# 排序(可选)
|
||||
GET /products?sort=created_at&order=desc
|
||||
|
||||
# 过滤(可选,支持多种操作符)
|
||||
GET /products?price_min=100&price_max=5000 # 范围
|
||||
GET /products?category=electronics,clothing # 多选(IN)
|
||||
GET /products?status=active&is_featured=true # 布尔
|
||||
GET /products?search=iphone # 全文搜索
|
||||
|
||||
# 字段选择(可选,减少数据传输)
|
||||
GET /products?fields=id,name,price
|
||||
|
||||
# 组合使用
|
||||
GET /products?page=1&page_size=20&sort=price&order=asc&category=electronics&price_max=5000&fields=id,name,price
|
||||
```
|
||||
|
||||
#### 请求头设计规范
|
||||
|
||||
| 头部字段 | 用途 | 示例 | 是否必需 |
|
||||
| ------------------ | ------------ | ----------------------------------------- | --------------------- |
|
||||
| `Authorization` | 认证信息 | `Bearer eyJhbGciOiJIUzI1NiIs...` | 受保护接口必需 |
|
||||
| `Content-Type` | 请求体格式 | `application/json` | POST/PUT/PATCH 必需 |
|
||||
| `Accept` | 期望响应格式 | `application/json` | 建议携带 |
|
||||
| `Accept-Language` | 期望语言 | `zh-CN,zh;q=0.9,en;q=0.8` | 多语言应用必需 |
|
||||
| `X-Request-ID` | 请求唯一标识 | `req-550e8400-e29b-41d4-a716-44665544000` | 建议携带,便于追踪 |
|
||||
| `X-Client-Version` | 客户端版本 | `iOS-2.5.1` / `Web-3.0.0` | 建议携带,便于问题排查 |
|
||||
| `If-None-Match` | 缓存校验 | `"abc123"` | 可选,用于乐观并发控制 |
|
||||
|
||||
### 4.2 响应结构设计
|
||||
|
||||
<ResponseStructureDemo />
|
||||
|
||||
**标准化响应结构**(无论成功与否,结构一致):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": { ... },
|
||||
"request_id": "req-123456789",
|
||||
"timestamp": "2024-01-15T09:30:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 | 示例 |
|
||||
| ------------ | ------- | ---------------------------------------------------------- | ------------------------------------------- |
|
||||
| `code` | integer | 业务状态码,`0` 表示成功,非 `0` 表示失败 | `0`, `10001`, `20003` |
|
||||
| `message` | string | 状态描述,成功时为 `"success"`,失败时为错误描述 | `"success"`, `"用户不存在"` |
|
||||
| `data` | any | 业务数据,成功时返回具体数据,失败时可返回 `null` 或错误详情 | `{ "id": 1, "name": "张三" }` |
|
||||
| `request_id` | string | 请求唯一标识,用于问题追踪 | `"req-550e8400-e29b-41d4-a716-44665544000"` |
|
||||
| `timestamp` | string | 响应时间戳,ISO 8601 格式 | `"2024-01-15T09:30:00.000Z"` |
|
||||
|
||||
#### 分页响应结构
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"list": [
|
||||
{ "id": 1, "name": "商品A", "price": 100 },
|
||||
{ "id": 2, "name": "商品B", "price": 200 }
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"total": 156,
|
||||
"total_pages": 8,
|
||||
"has_next": true,
|
||||
"has_prev": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip 💡 为什么要 request_id?
|
||||
**request_id** 是问题追踪的关键:
|
||||
|
||||
1. **用户反馈**:"支付失败,错误 ID 是 abc123"
|
||||
→ 技术人员直接在日志里搜索 abc123,立即定位问题
|
||||
|
||||
2. **分布式追踪**:
|
||||
- 请求经过 API Gateway → Service A → Service B
|
||||
- 每个服务都记录相同的 request_id
|
||||
- 日志系统可以把所有相关日志聚合起来
|
||||
|
||||
3. **性能分析**:
|
||||
- 统计某个 request_id 的完整链路耗时
|
||||
|
||||
- 发现瓶颈在哪个服务
|
||||
|
||||
**关键点**:request_id 是可观测性的基础,没有它,分布式系统的问题排查是地狱模式。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 错误处理:优雅地"拒绝"
|
||||
|
||||
### 5.1 为什么错误处理如此重要?
|
||||
|
||||
<ErrorHandlingDemo />
|
||||
|
||||
一个好的错误处理机制,能让客户端"看状态码就知道怎么回事",而不是去猜。
|
||||
|
||||
**错误的示范**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 200 OK
|
||||
{
|
||||
"error": "出错了"
|
||||
}
|
||||
```
|
||||
|
||||
**问题**:
|
||||
|
||||
- HTTP 状态码说"成功",但业务说"出错"
|
||||
- 错误信息太笼统,无法定位问题
|
||||
- 没有错误代码,难以程序化判断
|
||||
|
||||
**正确的示范**:
|
||||
|
||||
```json
|
||||
HTTP/1.1 422 Unprocessable Entity
|
||||
{
|
||||
"code": 20003,
|
||||
"message": "密码强度不足",
|
||||
"errors": [
|
||||
{
|
||||
"field": "password",
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "密码必须包含至少 1 个大写字母、1 个小写字母、1 个数字,且长度至少 8 位"
|
||||
}
|
||||
],
|
||||
"request_id": "req-550e8400-e29b-41d4-a716-44665544000",
|
||||
"timestamp": "2024-01-15T09:30:00.000Z",
|
||||
"help_url": "https://docs.example.com/errors/20003"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 错误码设计规范
|
||||
|
||||
::: tip 💡 错误码的分层设计
|
||||
**分层错误码**的好处:
|
||||
|
||||
- **可程序化判断**:前端根据 `code` 字段决定行为,而不是解析 `message`
|
||||
- **国际化友好**:`code` 不变,`message` 可以根据用户语言返回不同文本
|
||||
- **文档化**:每个错误码都有文档,开发者可以查
|
||||
|
||||
**结构**:1XXYY
|
||||
|
||||
- 第 1 位(1):固定,表示错误
|
||||
- 第 2-3 位(XX):模块/层次
|
||||
- 第 3-4 位(YY):具体错误
|
||||
|
||||
**示例**:
|
||||
|
||||
- `10001`:通用错误(参数错误)
|
||||
- `10010`:用户模块(用户不存在)
|
||||
- `20003`:业务错误(密码强度不足)
|
||||
:::
|
||||
|
||||
#### 分层错误码体系
|
||||
|
||||
```
|
||||
错误码格式:1XXYY
|
||||
- 第 1 位(1):固定,表示错误
|
||||
- 第 2-3 位(XX):模块/层次
|
||||
- 第 4-5 位(YY):具体错误
|
||||
```
|
||||
|
||||
| 模块代码 | 模块名称 | 说明 |
|
||||
| -------- | ---------- | ------------------------ |
|
||||
| 00 | 通用 | 系统级、通用错误 |
|
||||
| 10 | 用户模块 | 注册、登录、用户信息相关 |
|
||||
| 11 | 认证授权 | Token、权限相关 |
|
||||
| 20 | 商品模块 | 商品 CRUD、库存相关 |
|
||||
| 30 | 订单模块 | 下单、支付、物流相关 |
|
||||
| 40 | 支付模块 | 支付渠道、退款相关 |
|
||||
| 50 | 营销模块 | 优惠券、活动相关 |
|
||||
| 90 | 第三方服务 | 短信、邮件、云存储等 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 版本控制:API 的"向后兼容"
|
||||
|
||||
### 6.1 为什么要做 API 版本控制?
|
||||
|
||||
<VersioningStrategyDemo />
|
||||
|
||||
场景:你的电商系统已经上线,App 有 100 万用户。现在需要修改订单接口,添加一个新字段,同时废弃一个旧字段。
|
||||
|
||||
**如果不做版本控制**:
|
||||
|
||||
- 新 App 调用新接口 → 正常工作
|
||||
- 旧 App 调用新接口 → 字段缺失,崩溃
|
||||
- 用户投诉 → 老板震怒 → 你背锅
|
||||
|
||||
**正确的做法**:
|
||||
|
||||
```
|
||||
/v1/orders - 旧接口,继续服务旧 App
|
||||
/v2/orders - 新接口,新功能在这里
|
||||
```
|
||||
|
||||
旧 App 继续调用 `/v1/orders`,新功能上线不会崩。等旧 App 用户都升级了,再考虑废弃 v1。
|
||||
|
||||
### 6.2 4 种版本控制策略
|
||||
|
||||
| 策略 | 示例 | 优点 | 缺点 | 推荐度 |
|
||||
| ----------------------- | ------------------------------------- | -------------------------- | ---------------------------------- | -------- |
|
||||
| **URL Path** | `/v1/users` | 最直观、易于缓存、文档清晰 | URL 变化 | ⭐⭐⭐⭐ |
|
||||
| **Header** | `API-Version: v1` | URL 不变 | 不直观,难以缓存,需要读 Header 文档 | ⭐⭐ |
|
||||
| **Content Negotiation** | `Accept: application/vnd.api.v1+json` | 标准 HTTP 规范 | 复杂,理解成本高 | ⭐⭐ |
|
||||
| **Query Parameter** | `/users?version=v1` | 简单 | 不专业,容易被忽视,缓存麻烦 | ⭐ |
|
||||
|
||||
**推荐做法**:URL Path 版本控制,简单直观,行业主流。
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结:API 设计 checklist
|
||||
|
||||
### 7.1 设计阶段
|
||||
|
||||
- [ ] URL 设计符合 RESTful 规范(名词、复数、小写、连字符)
|
||||
- [ ] HTTP 方法使用正确(GET/POST/PUT/PATCH/DELETE)
|
||||
- [ ] 状态码选择恰当(2xx/4xx/5xx 区分清楚)
|
||||
- [ ] 错误码体系设计完成(分层、易扩展)
|
||||
- [ ] 响应结构标准化(code/message/data 统一)
|
||||
- [ ] 版本控制策略确定(URL Path 推荐)
|
||||
- [ ] 分页/排序/过滤参数设计统一
|
||||
|
||||
### 7.2 实现阶段
|
||||
|
||||
- [ ] 所有接口都有完善的 OpenAPI 文档
|
||||
- [ ] 参数校验规则清晰(类型、长度、必填)
|
||||
- [ ] 敏感信息脱敏处理(密码、Token 等)
|
||||
- [ ] 错误日志记录完整(带 request_id)
|
||||
- [ ] 接口性能监控到位(响应时间、错误率)
|
||||
- [ ] 限流熔断策略配置(防刷、降级)
|
||||
|
||||
### 7.3 维护阶段
|
||||
|
||||
- [ ] 接口变更走评审流程(兼容性检查)
|
||||
- [ ] 废弃接口有明确的 Sunset 计划
|
||||
- [ ] 客户端接入文档及时更新
|
||||
- [ ] 错误码文档随代码同步维护
|
||||
|
||||
---
|
||||
|
||||
## 8. 名词对照表
|
||||
|
||||
| 英文术语 | 中文对照 | 解释 |
|
||||
| -------------------------- | ---------------- | ----------------------------------------------------- |
|
||||
| **REST** | 表述性状态传递 | 一种软件架构风格,用 URL 标识资源,用 HTTP 方法操作资源 |
|
||||
| **RESTful** | 符合 REST 规范的 | 遵循 REST 架构风格设计的 API |
|
||||
| **Endpoint** | 端点 | API 的具体 URL 地址,如 `/users` |
|
||||
| **Resource** | 资源 | REST 架构中的核心概念,网络上的任何事物都可抽象为资源 |
|
||||
| **URI** | 统一资源标识符 | 标识资源的字符串,URL 是 URI 的一种 |
|
||||
| **HTTP Method** | HTTP 方法 | GET、POST、PUT、PATCH、DELETE 等 |
|
||||
| **Status Code** | 状态码 | HTTP 响应中的 3 位数字,表示请求的处理结果 |
|
||||
| **Payload** | 载荷 | HTTP 请求或响应的主体数据 |
|
||||
| **Header** | 头部 | HTTP 请求或响应的元数据 |
|
||||
| **Query String** | 查询字符串 | URL 中 `?` 后面的参数部分 |
|
||||
| **Path Parameter** | 路径参数 | URL 路径中的变量,如 `/users/{id}` |
|
||||
| **Pagination** | 分页 | 大数据量时分批返回的机制 |
|
||||
| **Idempotency** | 幂等性 | 多次执行结果相同的特性 |
|
||||
| **Deprecation** | 弃用 | 标记即将废弃的功能或接口 |
|
||||
| **Backward Compatibility** | 向后兼容 | 新版本兼容旧版本的接口调用 |
|
||||
| **Rate Limiting** | 限流 | 限制单位时间内的请求数量 |
|
||||
| **OpenAPI** | 开放 API 规范 | 描述 REST API 的标准格式(原 Swagger) |
|
||||
| **SDK** | 软件开发工具包 | 封装 API 调用的开发工具包 |
|
||||
@@ -1,5 +1,4 @@
|
||||
# API 入门
|
||||
|
||||
<ApiQuickStartDemo />
|
||||
|
||||
👆 看见了吗?点一下按钮,230 毫秒后,一句格言就回来了。
|
||||
@@ -0,0 +1,3 @@
|
||||
# 异步任务队列与生产消费模型
|
||||
|
||||
> 待实现
|
||||
@@ -1,5 +1,4 @@
|
||||
# 鉴权原理与实战:从 HTTP Basic 到 JWT (Interactive Guide to Authentication)
|
||||
|
||||
# 认证与授权体系
|
||||
> 💡 **学习指南**:本章节带你深入理解后端系统的"门禁系统"——鉴权与授权。我们将从最基础的"你是谁"讲起,一步步掌握 Session、JWT、OAuth2.0 等现代鉴权方案。
|
||||
|
||||
<AuthEvolutionDemo />
|
||||
@@ -1,5 +1,4 @@
|
||||
# 后端编程语言选型指南:从问题出发做决策
|
||||
|
||||
# 后端语言对比(Node.js / Go / Java / Rust)
|
||||
::: tip 🎯 核心问题
|
||||
**"我们后端该用什么语言?"** 这就像问:"我应该买什么工具?" 答案永远不是"最好的",而是"最适合你的"。本章将带你全面了解主流后端编程语言的特点、应用场景和选择策略,帮助你做出明智的决策。
|
||||
:::
|
||||
@@ -0,0 +1,869 @@
|
||||
# 后端分层架构
|
||||
::: tip 🎯 核心问题
|
||||
**代码越写越乱,怎么组织才能清晰易懂?** 这就像问:你是把所有食材、厨具、调料都扔在一个抽屉里,还是用橱柜、冰箱、抽屉分类摆放?分层架构就是让代码"物归其位"的方法。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要分层?
|
||||
|
||||
### 1.1 从混乱到整洁
|
||||
|
||||
很多初学者在刚开始写后端代码时,都会遇到这样的困惑:
|
||||
|
||||
- **刚开始**:写一个用户注册接口,100行代码搞定,感觉挺简单
|
||||
- **三个月后**:业务越来越复杂,一个文件500行,改一行代码怕影响其他地方
|
||||
- **半年后**:来了新同事,看着代码发愁:"这个接口到底干了多少事?"
|
||||
|
||||
**问题的本质**:代码没有"章法",所有的逻辑都堆在一起,就像把食材、厨具、调料都扔在一个抽屉里。
|
||||
|
||||
<LayeredArchitectureDemo />
|
||||
|
||||
### 1.2 分层的思想:把抽屉换成橱柜
|
||||
|
||||
想象一下厨房的组织方式:
|
||||
|
||||
| 区域 | 存放物品 | 特点 |
|
||||
| -------- | ------------------ | ------------ |
|
||||
| **吊柜** | 不常用的锅具、囤货 | 取用最不方便 |
|
||||
| **台面** | 正在处理的食材 | 临时操作区 |
|
||||
| **抽屉** | 分类摆放的餐具 | 按需取用 |
|
||||
| **冰箱** | 生鲜食材 | 有保鲜条件 |
|
||||
|
||||
**分层架构**就是把代码也这样组织:每一层只关心自己的职责,层与层之间通过明确的"接口"交互,而不是随意互相调用。
|
||||
|
||||
::: tip 💡 通俗比喻:餐厅的分工
|
||||
把后端系统想象成一家餐厅:
|
||||
|
||||
- **Controller(控制器)** = 前厅接待员:迎接客人、接单、上菜
|
||||
- **Service(业务逻辑)** = 厨师:按照菜谱做菜,协调各个帮厨
|
||||
- **Repository(数据访问)** = 仓管员:从仓库取食材、存放剩余食材
|
||||
- **Domain(领域模型)** = 菜谱标准:定义宫保鸡丁是什么、用什么食材、什么口味
|
||||
|
||||
**关键点**:每个角色只做自己的事,不会越界。接待员不会自己跑进厨房炒菜,仓管员不会修改菜谱。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 四层架构的职责划分
|
||||
|
||||
### 2.1 四层架构概览
|
||||
|
||||
典型的后端分层架构包含四个核心层次:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Controller 层(控制器层) │ ← 接待员:接收请求,初步检查
|
||||
│ - 接收 HTTP 请求 │
|
||||
│ - 参数校验 │
|
||||
│ - 调用 Service │
|
||||
│ - 返回响应 │
|
||||
├─────────────────────────────────────┤
|
||||
│ Service 层(业务逻辑层) │ ← 厨师:处理核心业务
|
||||
│ - 业务逻辑编排 │
|
||||
│ - 事务管理 │
|
||||
│ - 调用 Repository │
|
||||
│ - 跨模块协调 │
|
||||
├─────────────────────────────────────┤
|
||||
│ Repository 层(数据访问层) │ ← 仓管员:管理数据存取
|
||||
│ - 数据库操作 │
|
||||
│ - ORM 映射 │
|
||||
│ - 查询封装 │
|
||||
├─────────────────────────────────────┤
|
||||
│ Domain 层(领域模型层) │ ← 菜谱标准:定义业务概念
|
||||
│ - 实体(Entity) │
|
||||
│ - 值对象(Value Object) │
|
||||
│ - 业务规则 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
::: tip 📊 从图解中你能看到什么?
|
||||
**自上而下**:从"接近用户"到"接近数据"
|
||||
|
||||
- **Controller**:最接近前端,处理HTTP协议相关的事情
|
||||
- **Service**:核心业务逻辑,但不关心数据怎么存、HTTP怎么传
|
||||
- **Repository**:只关心数据怎么存取,不关心业务含义
|
||||
- **Domain**:最核心的业务概念,所有层都依赖它
|
||||
|
||||
**依赖方向**:
|
||||
|
||||
```
|
||||
Controller → Service → Repository
|
||||
↓
|
||||
Domain(核心,不依赖任何层)
|
||||
```
|
||||
|
||||
这符合"依赖倒置原则":高层模块不应依赖低层模块的具体实现,而应依赖抽象(Domain)。
|
||||
:::
|
||||
|
||||
### 2.2 Controller 层:请求的"接待员"
|
||||
|
||||
<ControllerLayerDemo />
|
||||
|
||||
**职责**:
|
||||
|
||||
- 接收 HTTP 请求,解析参数
|
||||
- 进行基础的参数校验(格式、必填等)
|
||||
- 调用 Service 层执行业务逻辑
|
||||
- 封装响应,返回给客户端
|
||||
|
||||
**不该做的事**:
|
||||
|
||||
- ❌ 在这里写业务逻辑
|
||||
- ❌ 直接操作数据库
|
||||
- ❌ 处理事务
|
||||
|
||||
**类比**:就像餐厅的门童,负责迎接客人、检查预约、引导入座,但不负责做菜。
|
||||
|
||||
::: details 📋 实际代码示例
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
// ✅ 正确:Controller 只负责接收请求和返回响应
|
||||
@PostMapping
|
||||
public ResponseEntity<UserDTO> createUser(
|
||||
@RequestBody @Valid UserCreateRequest request) {
|
||||
// 1. Request DTO → Param DTO
|
||||
UserCreateParam param = UserCreateParam.builder()
|
||||
.username(request.getUsername())
|
||||
.password(encryptPassword(request.getPassword()))
|
||||
.email(request.getEmail())
|
||||
.build();
|
||||
|
||||
// 2. 调用 Service
|
||||
User user = userService.createUser(param);
|
||||
|
||||
// 3. Entity → Response DTO
|
||||
UserDTO response = UserDTO.builder()
|
||||
.id(user.getId())
|
||||
.username(user.getUsername())
|
||||
.email(user.getEmail())
|
||||
.build();
|
||||
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
|
||||
- 用 `@Valid` 自动校验参数格式
|
||||
- 用 DTO(Data Transfer Object)隔离前后端数据结构
|
||||
- 不包含任何业务逻辑,只做"翻译"和"调度"
|
||||
:::
|
||||
|
||||
### 2.3 Service 层:业务逻辑的"厨师"
|
||||
|
||||
<ServiceLayerDemo />
|
||||
|
||||
**职责**:
|
||||
|
||||
- 实现核心业务逻辑
|
||||
- 编排多个 Repository 的操作
|
||||
- 管理事务边界(@Transactional)
|
||||
- 处理跨模块的业务协调
|
||||
|
||||
**不该做的事**:
|
||||
|
||||
- ❌ 直接写 SQL(交给 Repository)
|
||||
- ❌ 处理 HTTP 相关的事情
|
||||
- ❌ 返回数据库实体给 Controller
|
||||
|
||||
**类比**:就像厨师按照菜谱做菜,需要协调各种食材(数据),把控菜品质量(业务正确性)。
|
||||
|
||||
::: details 📋 实际代码示例
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final EmailService emailService;
|
||||
|
||||
// ✅ 正确:Service 封装业务逻辑
|
||||
@Transactional
|
||||
public User createUser(UserCreateParam param) {
|
||||
// 1. 业务规则:检查用户名是否重复
|
||||
if (userRepository.existsByUsername(param.getUsername())) {
|
||||
throw new UserAlreadyExistsException();
|
||||
}
|
||||
|
||||
// 2. 创建用户实体
|
||||
User user = new User();
|
||||
user.setUsername(param.getUsername());
|
||||
user.setPassword(param.getPassword()); // 已经加密
|
||||
user.setEmail(param.getEmail());
|
||||
|
||||
// 3. 保存到数据库
|
||||
userRepository.save(user);
|
||||
|
||||
// 4. 发送欢迎邮件(跨模块协调)
|
||||
emailService.sendWelcomeEmail(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
|
||||
- 用 `@Transactional` 保证事务一致性
|
||||
- 抛出业务异常,让 Controller 统一处理
|
||||
- 不依赖 HTTP 概念,可以复用(如定时任务调用)
|
||||
:::
|
||||
|
||||
### 2.4 Repository 层:数据的"仓管员"
|
||||
|
||||
<RepositoryLayerDemo />
|
||||
|
||||
**职责**:
|
||||
|
||||
- 封装所有数据访问逻辑
|
||||
- 执行 CRUD 操作
|
||||
- 处理 ORM 映射
|
||||
- 封装查询条件
|
||||
|
||||
**不该做的事**:
|
||||
|
||||
- ❌ 写业务逻辑
|
||||
- ❌ 处理事务(Service 层管理)
|
||||
- ❌ 依赖上层模块
|
||||
|
||||
**类比**:就像餐厅的仓管员,负责从仓库取食材、存放剩余食材。厨师只需要告诉仓管员要什么,不需要知道仓库在哪、怎么取。
|
||||
|
||||
::: details 📋 实际代码示例
|
||||
|
||||
```java
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
// ✅ Spring Data JPA 自动实现
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
boolean existsByUsername(String username);
|
||||
|
||||
// ✅ 自定义复杂查询
|
||||
@Query("SELECT u FROM User u WHERE u.email = :email AND u.deleted = false")
|
||||
Optional<User> findActiveByEmail(@Param("email") String email);
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
|
||||
- Repository 是接口,不包含业务逻辑
|
||||
- 用方法名表达查询意图,不需要写实现
|
||||
- 可以用 `@Query` 自定义复杂查询
|
||||
:::
|
||||
|
||||
### 2.5 Domain 层:领域模型的"蓝图"
|
||||
|
||||
<DomainModelDemo />
|
||||
|
||||
**职责**:
|
||||
|
||||
- 定义业务实体(Entity)
|
||||
- 定义值对象(Value Object)
|
||||
- 封装业务规则
|
||||
- 作为所有层的共同依赖
|
||||
|
||||
**重要特性**:
|
||||
|
||||
- Domain 层不依赖任何其他层
|
||||
- 所有层都依赖 Domain 层
|
||||
- 是分层架构的基础
|
||||
|
||||
**类比**:就像餐厅的菜单和菜品标准,定义了什么是"宫保鸡丁"、用什么食材、什么口味。所有厨师都要按照这个标准来做。
|
||||
|
||||
::: details 📋 实际代码示例
|
||||
|
||||
```java
|
||||
// ✅ 实体(Entity):有唯一标识的业务对象
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true, nullable = false)
|
||||
private String username;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String password;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String email;
|
||||
|
||||
// ✅ 业务方法:封装业务规则
|
||||
public boolean isPasswordCorrect(String rawPassword) {
|
||||
return BCrypt.checkpw(rawPassword, this.password);
|
||||
}
|
||||
|
||||
public void changePassword(String oldPassword, String newPassword) {
|
||||
if (!isPasswordCorrect(oldPassword)) {
|
||||
throw new IncorrectPasswordException();
|
||||
}
|
||||
this.password = BCrypt.hashpw(newPassword);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 值对象(Value Object):通过属性值判断相等
|
||||
@Embeddable
|
||||
public class Email {
|
||||
|
||||
@Column(nullable = false)
|
||||
private String address;
|
||||
|
||||
public Email(String address) {
|
||||
if (!isValidEmail(address)) {
|
||||
throw new InvalidEmailException();
|
||||
}
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
private boolean isValidEmail(String address) {
|
||||
return address.matches("^[A-Za-z0-9+_.-]+@(.+)$");
|
||||
}
|
||||
|
||||
// ✅ 值对象不通过ID判断相等,而是通过属性值
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Email)) return false;
|
||||
return address.equals(((Email) o).address);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
|
||||
- Entity 有唯一标识,Value Object 通过属性值判断相等
|
||||
- 业务规则封装在 Domain 对象中,而不是散落在 Service 层
|
||||
- Domain 层是纯粹的业务逻辑,不依赖框架
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. DTO:层与层之间的"翻译官"
|
||||
|
||||
### 3.1 为什么需要 DTO?
|
||||
|
||||
<DtoFlowDemo />
|
||||
|
||||
想象一下:如果 Controller 直接把数据库实体(Entity)返回给前端,会发生什么?
|
||||
|
||||
```java
|
||||
// ❌ 错误的做法
|
||||
@Entity
|
||||
public class User {
|
||||
@Id
|
||||
private Long id;
|
||||
private String username;
|
||||
private String password; // 敏感信息!
|
||||
private String phone;
|
||||
private String email;
|
||||
private LocalDateTime createdAt;
|
||||
private Boolean isDeleted; // 内部字段!
|
||||
}
|
||||
|
||||
// 如果直接返回这个实体...
|
||||
// 前端会收到 password、isDeleted 等不应该暴露的字段
|
||||
```
|
||||
|
||||
::: tip 💡 通俗解释
|
||||
**DTO**(Data Transfer Object,数据传输对象)就像"菜单翻译":
|
||||
|
||||
- 厨师的菜谱(Domain Entity)包含:食材清单、烹饪步骤、火候、摆盘要求
|
||||
- 给客人看的菜单(Controller Response DTO)只包含:菜名、价格、图片、简介
|
||||
|
||||
**为什么要翻译**:
|
||||
|
||||
1. **安全**:不能把"后厨秘密"(如密码、删除标记)暴露给客人
|
||||
2. **简化**:客人只关心"这道菜是什么",不关心"怎么做的"
|
||||
3. **灵活**:同一道菜,堂食菜单和外卖菜单显示的内容可以不同
|
||||
:::
|
||||
|
||||
**DTO 的作用**:
|
||||
|
||||
- **解耦**:隔离数据库实体和 API 契约
|
||||
- **安全**:控制暴露的字段,避免泄露敏感信息
|
||||
- **灵活**:可以为不同场景定义不同的 DTO
|
||||
- **性能**:避免加载不必要的数据
|
||||
|
||||
### 3.2 不同层的 DTO 职责
|
||||
|
||||
| 层级 | DTO 类型 | 职责 | 示例 |
|
||||
| -------------- | ---------------------- | ------------------------------------------- | ------------------- |
|
||||
| **Controller** | Request / Response DTO | 定义 API 契约、参数校验、序列化 | `UserCreateRequest` |
|
||||
| **Service** | Param / Result DTO | 封装业务方法参数,解耦 Controller 与 Service | `UserCreateParam` |
|
||||
| **Repository** | Entity / DO | 映射数据库表结构,ORM 映射 | `UserEntity` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 依赖方向:分层架构的铁律
|
||||
|
||||
### 4.1 依赖倒置原则(DIP)
|
||||
|
||||
<DependencyDirectionDemo />
|
||||
|
||||
分层架构的核心规则:**上层模块不应依赖下层模块的具体实现,而应依赖于抽象。**
|
||||
|
||||
::: tip 💡 通俗解释
|
||||
**依赖倒置**(Dependency Inversion Principle):
|
||||
|
||||
**错误的做法**(依赖实现):
|
||||
|
||||
```
|
||||
Controller → UserServiceImpl → UserDaoImpl → UserEntity
|
||||
```
|
||||
|
||||
问题:
|
||||
|
||||
1. 每层都耦合了具体实现,换个实现要改很多代码
|
||||
2. 测试困难,Mock 需要修改实现类
|
||||
|
||||
**正确的做法**(依赖抽象):
|
||||
|
||||
```
|
||||
Controller → IUserService(接口) → IUserDao(接口) → UserEntity
|
||||
```
|
||||
|
||||
好处:
|
||||
|
||||
1. 上层只依赖接口,不关心实现
|
||||
2. 换实现只需改配置(如从 MySQL 换到 PostgreSQL)
|
||||
3. 容易 Mock 测试
|
||||
|
||||
**比喻**:
|
||||
|
||||
- ❌ 错误:你只去某家特定的超市买东西,超市关门你就买不到
|
||||
- ✅ 正确:你定义"买东西"这个接口,可以去任何超市实现
|
||||
:::
|
||||
|
||||
### 4.2 正确的依赖方向
|
||||
|
||||
```
|
||||
✅ 正确的依赖方向:
|
||||
|
||||
Controller → Service 接口 → Repository 接口 → Domain
|
||||
↑ ↑ ↑ ↑
|
||||
└-----------└----------------└--------------┘
|
||||
所有层都依赖 Domain,Domain 不依赖任何层
|
||||
|
||||
❌ 禁止的做法:
|
||||
- Service 直接依赖 Repository 实现
|
||||
- Controller 直接操作数据库
|
||||
- Domain 依赖 Service 或 Repository
|
||||
- 层与层之间形成循环依赖
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 实战案例:电商订单系统的分层实现
|
||||
|
||||
### 5.1 需求场景
|
||||
|
||||
实现一个电商订单创建功能:
|
||||
|
||||
- 用户选择商品,确认订单信息
|
||||
- 系统检查库存
|
||||
- 计算订单金额(商品价格 + 运费 - 优惠)
|
||||
- 创建订单记录
|
||||
- 扣减库存
|
||||
- 返回订单信息
|
||||
|
||||
::: details 📋 完整的四层代码
|
||||
**1. Domain 层:领域模型**
|
||||
|
||||
```java
|
||||
// 订单实体
|
||||
@Entity
|
||||
public class Order {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private Long userId;
|
||||
private List<OrderItem> items = new ArrayList<>();
|
||||
@Embedded
|
||||
private Money totalAmount;
|
||||
private OrderStatus status = OrderStatus.PENDING_PAYMENT;
|
||||
private LocalDateTime createdAt = LocalDateTime.now();
|
||||
|
||||
// ✅ 业务方法:计算订单总金额
|
||||
public void calculateTotal() {
|
||||
Money total = Money.zero();
|
||||
for (OrderItem item : items) {
|
||||
total = total.add(item.getSubTotal());
|
||||
}
|
||||
this.totalAmount = total;
|
||||
}
|
||||
|
||||
// ✅ 业务方法:取消订单
|
||||
public void cancel() {
|
||||
if (this.status != OrderStatus.PENDING_PAYMENT) {
|
||||
throw new IllegalStateException("只有待支付订单可以取消");
|
||||
}
|
||||
this.status = OrderStatus.CANCELLED;
|
||||
}
|
||||
}
|
||||
|
||||
// 值对象:金钱
|
||||
@Embeddable
|
||||
public class Money {
|
||||
private BigDecimal amount;
|
||||
private String currency;
|
||||
|
||||
public static Money zero() {
|
||||
return new Money(BigDecimal.ZERO, "CNY");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**2. Repository 层:数据访问**
|
||||
|
||||
```java
|
||||
@Repository
|
||||
public interface OrderRepository extends JpaRepository<Order, Long> {
|
||||
List<Order> findByUserIdOrderByCreatedAtDesc(Long userId);
|
||||
}
|
||||
|
||||
@Repository
|
||||
public interface ProductRepository extends JpaRepository<Product, Long> {
|
||||
// Spring Data JPA 自动实现
|
||||
}
|
||||
```
|
||||
|
||||
**3. Service 层:业务逻辑**
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OrderService {
|
||||
|
||||
private final OrderRepository orderRepository;
|
||||
private final ProductService productService;
|
||||
private final InventoryService inventoryService;
|
||||
|
||||
@Transactional
|
||||
public OrderDTO createOrder(OrderCreateParam param) {
|
||||
// 1. 验证商品并扣减库存
|
||||
List<OrderItem> items = new ArrayList<>();
|
||||
for (OrderItemParam itemParam : param.getItems()) {
|
||||
Product product = productService.getProduct(itemParam.getProductId());
|
||||
|
||||
boolean reserved = inventoryService.reserveStock(
|
||||
itemParam.getProductId(),
|
||||
itemParam.getQuantity()
|
||||
);
|
||||
if (!reserved) {
|
||||
throw new InsufficientStockException();
|
||||
}
|
||||
|
||||
OrderItem item = new OrderItem();
|
||||
item.setProductId(product.getId());
|
||||
item.setQuantity(itemParam.getQuantity());
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
// 2. 创建订单
|
||||
Order order = new Order();
|
||||
order.setUserId(param.getUserId());
|
||||
for (OrderItem item : items) {
|
||||
order.addItem(item);
|
||||
}
|
||||
|
||||
// 3. 计算总价(调用 Domain 方法)
|
||||
order.calculateTotal();
|
||||
|
||||
// 4. 保存订单
|
||||
orderRepository.save(order);
|
||||
|
||||
return OrderDTO.from(order);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4. Controller 层:API 入口**
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/orders")
|
||||
@RequiredArgsConstructor
|
||||
public class OrderController {
|
||||
|
||||
private final OrderService orderService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<OrderDTO> createOrder(
|
||||
@RequestBody @Valid OrderCreateRequest request,
|
||||
@AuthenticationPrincipal UserPrincipal user) {
|
||||
|
||||
// 1. Request → Param 转换
|
||||
OrderCreateParam param = OrderCreateParam.builder()
|
||||
.userId(user.getId())
|
||||
.items(request.getItems())
|
||||
.build();
|
||||
|
||||
// 2. 调用 Service
|
||||
OrderDTO order = orderService.createOrder(param);
|
||||
|
||||
// 3. 返回
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(order);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. 分层架构的演进:从混乱到整洁
|
||||
|
||||
### 6.1 初学者常犯的错误
|
||||
|
||||
::: details ❌ 错误一:Controller 里写业务逻辑
|
||||
|
||||
```java
|
||||
// ❌ 错误:Controller 里写了太多业务逻辑
|
||||
@RestController
|
||||
public class OrderController {
|
||||
|
||||
@Autowired private OrderRepository orderRepository;
|
||||
@Autowired private ProductRepository productRepository;
|
||||
|
||||
@PostMapping("/orders")
|
||||
public Order createOrder(@RequestBody CreateOrderRequest request) {
|
||||
// ❌ 太多的业务逻辑在这里...
|
||||
// 检查库存
|
||||
for (ItemRequest item : request.getItems()) {
|
||||
Product product = productRepository.findById(item.getProductId())
|
||||
.orElseThrow(() -> new RuntimeException("商品不存在"));
|
||||
|
||||
if (product.getStock() < item.getQuantity()) {
|
||||
throw new RuntimeException("库存不足");
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 直接操作数据库
|
||||
Order order = new Order();
|
||||
orderRepository.save(order);
|
||||
|
||||
return order;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**重构后**:
|
||||
|
||||
```java
|
||||
// ✅ Controller 只负责接收请求和返回响应
|
||||
@RestController
|
||||
public class OrderController {
|
||||
|
||||
@Autowired
|
||||
private OrderService orderService;
|
||||
|
||||
@PostMapping("/orders")
|
||||
public OrderDTO createOrder(@RequestBody @Valid CreateOrderRequest request) {
|
||||
OrderCreateParam param = OrderCreateParam.builder()
|
||||
.items(request.getItems())
|
||||
.build();
|
||||
|
||||
Order order = orderService.createOrder(param);
|
||||
|
||||
return OrderDTO.from(order);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: details ❌ 错误二:循环依赖
|
||||
|
||||
```java
|
||||
// ❌ 错误:Service 之间相互调用,形成循环依赖
|
||||
@Service
|
||||
public class OrderService {
|
||||
@Autowired
|
||||
private PaymentService paymentService; // A 依赖 B
|
||||
}
|
||||
|
||||
@Service
|
||||
public class PaymentService {
|
||||
@Autowired
|
||||
private OrderService orderService; // B 又依赖 A - 循环!
|
||||
}
|
||||
```
|
||||
|
||||
**解决方案:使用事件驱动**
|
||||
|
||||
```java
|
||||
// ✅ 发布事件,而不是直接调用
|
||||
@Service
|
||||
public class OrderService {
|
||||
@Autowired
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
public void payOrder(Long orderId, PaymentParam param) {
|
||||
Order order = orderRepository.findById(orderId).orElseThrow();
|
||||
order.pay(param.getPaymentMethod());
|
||||
orderRepository.save(order);
|
||||
|
||||
// ✅ 发布事件,解耦服务
|
||||
eventPublisher.publishEvent(new OrderPaidEvent(order));
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ PaymentService 监听事件
|
||||
@Service
|
||||
public class PaymentService {
|
||||
@EventListener
|
||||
@Transactional
|
||||
public void handleOrderPaid(OrderPaidEvent event) {
|
||||
// 处理支付相关逻辑
|
||||
createPaymentRecord(event);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 7. 分层架构 vs 整洁架构
|
||||
|
||||
<CleanArchitectureDemo />
|
||||
|
||||
### 7.1 两种架构的对比
|
||||
|
||||
| 特性 | 传统分层架构 | 整洁架构 |
|
||||
| ---------------- | -------------------- | ---------------------- |
|
||||
| **依赖方向** | 从上到下 | 从外到内 |
|
||||
| **核心业务位置** | Service 层 | Domain 层(中心) |
|
||||
| **框架依赖** | 较深(如 Spring) | 较浅(通过接口隔离) |
|
||||
| **可测试性** | 需要集成测试 | 核心可单元测试 |
|
||||
| **学习曲线** | 平缓 | 较陡 |
|
||||
| **适用场景** | 中小型项目、快速迭代 | 大型复杂业务、长期维护 |
|
||||
|
||||
::: tip 💡 核心区别
|
||||
**传统分层架构**:
|
||||
|
||||
- 依赖方向:Controller → Service → Repository → Domain
|
||||
- 框架(Spring)渗透到所有层
|
||||
- Service 层既包含业务逻辑,也依赖框架
|
||||
|
||||
**整洁架构**:
|
||||
|
||||
- 依赖方向:所有层都指向中心(Domain)
|
||||
- 通过接口隔离,框架只在外层
|
||||
- Domain 层纯粹的业务逻辑,完全不依赖框架
|
||||
|
||||
**比喻**:
|
||||
|
||||
- 传统分层:像盖楼,从下往上建,地基很重要但可以被替换
|
||||
- 整洁架构:像洋葱,核心业务在最内层,外层(框架)可以随时更换
|
||||
:::
|
||||
|
||||
### 7.2 如何选择?
|
||||
|
||||
**选择传统分层架构当...**
|
||||
|
||||
- 项目规模较小,业务相对简单
|
||||
- 团队对 DDD 不熟悉
|
||||
- 需要快速上线,验证市场
|
||||
- 技术栈相对固定
|
||||
|
||||
**选择整洁架构当...**
|
||||
|
||||
- 业务复杂,领域模型丰富
|
||||
- 需要长期维护和演进
|
||||
- 需要频繁切换技术栈
|
||||
- 团队有较强的设计能力
|
||||
|
||||
---
|
||||
|
||||
## 8. 总结:分层架构的核心要点
|
||||
|
||||
### 8.1 四层职责速查表
|
||||
|
||||
| 层级 | 主要职责 | 不该做的事 |
|
||||
| -------------- | ------------------------------------------ | -------------------------------------------- |
|
||||
| **Controller** | 接收请求、参数校验、调用 Service、返回响应 | 写业务逻辑、操作数据库、处理事务 |
|
||||
| **Service** | 业务逻辑编排、事务管理、协调 Repository | 直接写 SQL、处理 HTTP、返回实体给 Controller |
|
||||
| **Repository** | 数据访问、ORM 映射、查询封装 | 写业务逻辑、管理事务、依赖上层 |
|
||||
| **Domain** | 实体定义、业务规则、值对象 | 依赖其他层、处理持久化、处理 HTTP |
|
||||
|
||||
### 8.2 依赖方向铁律
|
||||
|
||||
```
|
||||
✅ 正确的依赖方向:
|
||||
|
||||
Controller → Service 接口 → Repository 接口 → Domain
|
||||
↑ ↑ ↑ ↑
|
||||
└-----------└----------------└--------------┘
|
||||
所有层都依赖 Domain,Domain 不依赖任何层
|
||||
|
||||
❌ 禁止的做法:
|
||||
- Service 直接依赖 Repository 实现
|
||||
- Controller 直接操作数据库
|
||||
- Domain 依赖 Service 或 Repository
|
||||
- 层与层之间形成循环依赖
|
||||
```
|
||||
|
||||
### 8.3 编码最佳实践
|
||||
|
||||
1. **接口优先**:Service 和 Repository 都定义接口,实现类通过 Spring 注入
|
||||
2. **DTO 隔离**:每层使用自己的 DTO,不要直接传递 Entity
|
||||
3. **事务在 Service**:使用 `@Transactional` 在 Service 方法上控制事务
|
||||
4. **异常处理**:Controller 统一处理异常,不要 try-catch 后吞掉异常
|
||||
5. **贫血模型 vs 充血模型**:根据团队熟悉程度选择,但建议 Domain 有基本的行为方法
|
||||
|
||||
### 8.4 常见面试问题
|
||||
|
||||
**Q1:为什么要分层?不分层可以吗?**
|
||||
|
||||
> A:分层的目的是解耦和关注点分离。小项目可以不分层,但随着业务复杂度的增加,不分层会导致代码难以维护、测试困难、团队协作效率低下。
|
||||
|
||||
**Q2:Controller 层可以写业务逻辑吗?**
|
||||
|
||||
> A:不可以。Controller 应该只负责接收请求、调用 Service、返回响应。业务逻辑应该封装在 Service 层,这样代码可以被复用,也更容易测试。
|
||||
|
||||
**Q3:什么是贫血模型和充血模型?**
|
||||
|
||||
> A:贫血模型是指 Entity 只有 getter/setter,业务逻辑都在 Service 层。充血模型是指 Entity 包含业务方法(如 `order.cancel()`),封装了业务规则。DDD 推荐充血模型,但贫血模型更简单易懂。
|
||||
|
||||
**Q4:如何处理跨多个 Service 的事务?**
|
||||
|
||||
> A:可以在上层 Service 中使用 `@Transactional`,调用多个下层 Service。或者使用分布式事务方案(如 Seata),但会增加系统复杂度。
|
||||
|
||||
---
|
||||
|
||||
## 9. 名词对照表
|
||||
|
||||
| 英文术语 | 中文对照 | 解释 |
|
||||
| ------------------------ | ------------ | ------------------------------------- |
|
||||
| **Layered Architecture** | 分层架构 | 将系统划分为多个层次,每层有明确的职责 |
|
||||
| **Controller** | 控制器 | 接收 HTTP 请求,调用 Service,返回响应 |
|
||||
| **Service** | 服务 | 封装业务逻辑,协调多个 Repository |
|
||||
| **Repository** | 仓储 | 封装数据访问逻辑,执行 CRUD 操作 |
|
||||
| **Domain** | 领域 | 定义业务实体、值对象和业务规则 |
|
||||
| **DTO** | 数据传输对象 | 层与层之间传递数据的载体 |
|
||||
| **Entity** | 实体 | 有唯一标识的领域对象,对应数据库表 |
|
||||
| **Value Object** | 值对象 | 没有唯一标识,通过属性值判断相等的对象 |
|
||||
| **Dependency Inversion** | 依赖倒置 | 高层模块不应依赖低层模块,都应依赖抽象 |
|
||||
| **Transaction** | 事务 | 保证一组操作原子性的机制 |
|
||||
| **Clean Architecture** | 整洁架构 | 以领域为核心的架构风格,强调依赖方向 |
|
||||
| **Anemic Domain Model** | 贫血模型 | 实体只有数据没有行为的模型 |
|
||||
| **Rich Domain Model** | 充血模型 | 实体包含数据和业务行为的模型 |
|
||||
|
||||
---
|
||||
|
||||
_本文档示例代码基于 Java + Spring Boot,但分层架构的思想适用于任何后端技术栈(Node.js、Python、Go 等)。_
|
||||
@@ -1,5 +1,4 @@
|
||||
# 缓存系统设计:从零到高性能架构
|
||||
|
||||
# 缓存的层次与策略
|
||||
::: tip 🎯 核心问题
|
||||
**为什么有些网站打开只需 50 毫秒,而有些却要等 5 秒?** 这就像问:为什么从书包拿书只要 1 秒,而要去图书馆找书要 10 分钟?答案就是——缓存。本章将带你深入理解缓存的核心原理、设计模式和实战技巧,让你的系统性能提升 100 倍。
|
||||
:::
|
||||
@@ -0,0 +1,3 @@
|
||||
# 客户端语言对比(Swift / Kotlin / Dart)
|
||||
|
||||
> 待实现
|
||||
@@ -1,5 +1,4 @@
|
||||
# 进程 / 线程 / 协程与服务并发模型
|
||||
|
||||
# 并发、异步与多线程
|
||||
> 💡 **学习指南**:并发编程是很多后端工程师的"阿喀琉斯之踵"——面试被问倒、线上出 Bug、性能调优没思路。本章节会围绕一个核心问题展开:**当10万个用户同时请求你的服务,你的代码会崩吗?**
|
||||
|
||||
在开始之前,建议你先补两块"基础砖":
|
||||
@@ -0,0 +1,3 @@
|
||||
# 跨平台方案对比(React Native / Flutter / Electron / Tauri)
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 文件存储与对象存储
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# HTTP 协议
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,479 @@
|
||||
# 消息队列与事件驱动
|
||||
::: tip 🎯 核心问题
|
||||
**当系统耦合严重、流量突增时,如何保证核心链路稳定?** 消息队列是现代分布式系统的"缓冲器"和"解耦器"。本文通过真实案例(餐厅叫号、快递分拣、秒杀系统)深入理解消息队列的设计哲学和工程实践。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要"消息队列"?
|
||||
|
||||
### 1.1 从一个真实案例说起:淘宝订单系统的演进
|
||||
|
||||
2012年,淘宝订单系统遭遇了一次严重故障。双11零点,流量瞬间涌入,订单服务直接调用库存服务、支付服务、物流服务...整个链路像多米诺骨牌一样接连倒下。
|
||||
|
||||
**当时的架构(紧耦合):**
|
||||
|
||||
```
|
||||
用户下单 → 订单服务 → 同步调用库存服务 → 同步调用支付服务 → 同步调用物流服务
|
||||
↓ ↓ ↓
|
||||
响应 200ms 响应 500ms 响应 300ms
|
||||
```
|
||||
|
||||
::: warning ⚠️ 紧耦合的致命问题
|
||||
|
||||
- **总响应时间** = 200 + 500 + 300 = 1000ms(用户等1秒)
|
||||
- **库存服务挂了** → 订单服务也挂(线程池耗尽)
|
||||
- **支付服务慢了** → 整个链路被拖慢
|
||||
- **无法水平扩展** → 只能垂直加机器(贵且有限)
|
||||
:::
|
||||
|
||||
**改进后的架构(引入消息队列):**
|
||||
|
||||
```
|
||||
用户下单 → 订单服务 → 发送"订单创建"消息 → 立即返回(50ms)
|
||||
↓
|
||||
消息队列(Kafka)
|
||||
↓
|
||||
┌─────────────┬─────────────┬─────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
库存服务 支付服务 物流服务 通知服务
|
||||
(异步扣减) (异步处理) (异步创建) (异步发送)
|
||||
```
|
||||
|
||||
::: tip ✨ 改进后的效果
|
||||
|
||||
- **用户响应时间** = 50ms(体验提升20倍)
|
||||
- **库存服务挂了** → 消息暂存队列,恢复后继续处理
|
||||
- **支付服务慢了** → 不影响订单创建
|
||||
- **可以水平扩展** → 增加消费者实例即可
|
||||
:::
|
||||
|
||||
### 1.2 消息队列的生活化比喻
|
||||
|
||||
**餐厅叫号系统**
|
||||
|
||||
想象你去一家网红餐厅:
|
||||
|
||||
- **没有叫号系统**: 顾客必须站在窗口等,窗口有限,后面的人排长队,餐厅压力大
|
||||
- **有叫号系统**: 点完餐给你一个号,你可以先坐下,叫到号了去取餐
|
||||
|
||||
**消息队列就是软件系统的"叫号系统"**:
|
||||
|
||||
- **生产者**(点餐的人) → 把消息(订单)放到队列
|
||||
- **队列**(叫号机) → 暂存消息
|
||||
- **消费者**(厨师) → 按自己的节奏处理消息
|
||||
|
||||
<PeakShavingDemo />
|
||||
|
||||
---
|
||||
|
||||
## 2. 什么是消息队列?(定义 + 核心三要素)
|
||||
|
||||
### 2.1 什么是"消息队列"?
|
||||
|
||||
::: tip 🤔 术语解释
|
||||
**消息队列(Message Queue, MQ)** 是一个存储消息的容器,生产者把消息放进去,消费者从里面取消息处理。它实现了"异步通信"——发送方不需要等待接收方处理完成。
|
||||
|
||||
**同步 vs 异步**:
|
||||
|
||||
- **同步**: 像打电话,对方必须接听才能交流
|
||||
- **异步**: 像发微信,发了就行,对方有空再看
|
||||
|
||||
这就像你给朋友打电话(同步) vs 发微信(异步)。
|
||||
:::
|
||||
|
||||
### 2.2 消息队列的核心三要素
|
||||
|
||||
#### 要素一:生产者(Producer)
|
||||
|
||||
**职责**: 创建并发送消息到队列。
|
||||
|
||||
**生活化比喻**: 生产者就像"寄件人",把信件(消息)送到邮局(队列)。
|
||||
|
||||
::: details 关键设计要点
|
||||
|
||||
- **发送方式**: 同步发送(可靠但阻塞) vs 异步发送(高性能但需处理回调)
|
||||
- **消息确认**: 等待 Broker 确认(At Least Once) vs 发送即忘(At Most Once)
|
||||
- **失败处理**: 重试策略、本地日志备份、死信队列
|
||||
:::
|
||||
|
||||
#### 要素二:消费者(Consumer)
|
||||
|
||||
**职责**: 从队列获取消息并处理。
|
||||
|
||||
**生活化比喻**: 消费者就像"收件人",从邮箱(队列)取出信件(消息)并处理。
|
||||
|
||||
::: details 关键设计要点
|
||||
|
||||
- **消费模式**: 推模式(Push,Broker主动推送) vs 拉模式(Pull,消费者主动拉取)
|
||||
- **消费确认**: 自动 ACK(高效但可能丢消息) vs 手动 ACK(可靠但需处理超时)
|
||||
- **并发控制**: 单线程顺序消费 vs 多线程并行消费
|
||||
- **失败处理**: 重试策略、死信队列、补偿机制
|
||||
:::
|
||||
|
||||
#### 要素三:Broker(消息代理)
|
||||
|
||||
**职责**: 接收、存储、转发消息。
|
||||
|
||||
**生活化比喻**: Broker 就像"邮局"或"快递中转站",负责接收、分拣、派送信件。
|
||||
|
||||
::: details 关键设计要点
|
||||
|
||||
- **存储模型**: 内存存储(低延迟) vs 磁盘存储(高可靠)
|
||||
- **复制策略**: 主从复制、多副本同步
|
||||
- **高可用机制**: 集群部署、自动故障转移
|
||||
- **扩展性**: 分区(Partition)、分片(Sharding)
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心问题一:如何解耦系统,避免"牵一发而动全身"?
|
||||
|
||||
### 3.1 紧耦合的悲剧:一个服务挂了,全盘皆输
|
||||
|
||||
**场景还原**: 某电商平台的早期架构
|
||||
|
||||
```
|
||||
订单服务直接调用下游服务:
|
||||
┌─────────────┐
|
||||
│ 订单服务 │
|
||||
└──────┬──────┘
|
||||
│
|
||||
├───────────┬───────────┬───────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│库存服务 │ │支付服务 │ │物流服务 │ │短信服务 │
|
||||
│ 200ms │ │ 500ms │ │ 300ms │ │ 100ms │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
::: tip 📊 痛点分析表
|
||||
| 痛点 | 具体表现 | 后果 |
|
||||
|------|----------|------|
|
||||
| **级联故障** | 库存服务挂掉,订单服务同步调用超时 | 订单服务线程池耗尽,无法处理新请求 |
|
||||
| **响应延迟** | 必须等待所有下游服务响应 | 用户等待1秒以上,体验极差 |
|
||||
| **扩展困难** | 新增积分服务,需要修改订单服务代码 | 发布周期变长,风险增加 |
|
||||
| **资源浪费** | 订单服务必须等待短信服务 | 数据库连接被长时间占用 |
|
||||
:::
|
||||
|
||||
### 3.2 解耦方案:引入消息队列作为"中间层"
|
||||
|
||||
**解耦后的架构:**
|
||||
|
||||
```
|
||||
订单服务只负责发消息,不关心谁消费:
|
||||
|
||||
┌─────────────┐
|
||||
│ 订单服务 │ ──发送"订单创建"消息──┐
|
||||
└─────────────┘ │
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ 消息队列 │
|
||||
│ (Kafka/RabbitMQ) │
|
||||
│ - 可靠存储 │
|
||||
│ - 多副本 │
|
||||
│ - 顺序保证 │
|
||||
└─────────┬─────────┘
|
||||
│
|
||||
┌───────────────────────┼───────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ 库存服务 │ │ 支付服务 │ │ 物流服务 │
|
||||
│ 订阅订单事件 │ │ 订阅订单事件 │ │ 订阅订单事件 │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
<DecouplingDemo />
|
||||
|
||||
::: tip ✨ 解耦的好处
|
||||
| 维度 | 解耦前 | 解耦后 |
|
||||
|------|--------|--------|
|
||||
| **故障隔离** | 库存挂 = 订单挂 | 库存挂,消息暂存队列,恢复后消费 |
|
||||
| **响应时间** | 1000ms(同步等待) | 50ms(发完消息即返回) |
|
||||
| **扩展性** | 新增服务需改订单代码 | 新增服务只需订阅主题 |
|
||||
| **系统复杂度** | 订单服务强依赖下游 | 订单服务只依赖消息队列 |
|
||||
:::
|
||||
|
||||
### 3.3 解耦的本质:从"直接调用"到"事件驱动"
|
||||
|
||||
**思维模式的转变:**
|
||||
|
||||
```
|
||||
传统思维(命令式):
|
||||
"订单服务命令库存服务:给我扣库存!"
|
||||
↓ 直接调用
|
||||
↓ 耦合度高,被调用方必须在线
|
||||
↓ 调用方需要知道被调用方的接口
|
||||
|
||||
事件驱动思维(声明式):
|
||||
"订单服务声明:订单已创建,谁关心谁来处理。"
|
||||
↓ 发送事件到消息队列
|
||||
↓ 解耦,消费者可以离线
|
||||
↓ 生产者不需要知道消费者的存在
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心问题二:如何削峰填谷,应对流量突增?
|
||||
|
||||
### 4.1 秒杀场景:10万QPS如何平稳处理?
|
||||
|
||||
**场景还原**: 某电商平台双11秒杀活动,预计峰值10万QPS,但数据库只能承受1000 QPS。
|
||||
|
||||
**直接冲击的后果:**
|
||||
|
||||
```
|
||||
用户请求 ──→ 应用服务器 ──→ 数据库
|
||||
10万/s 10万/s 1000/s(极限)
|
||||
↓
|
||||
连接池耗尽
|
||||
响应超时
|
||||
数据库崩溃
|
||||
↓
|
||||
雪崩效应(所有依赖数据库的服务都挂)
|
||||
```
|
||||
|
||||
::: tip 🌊 术语解释
|
||||
**QPS(Queries Per Second)**: 每秒查询数,衡量系统吞吐量的指标。
|
||||
|
||||
**10万QPS** 意味着每秒有10万个请求,就像10万人同时冲进商店。
|
||||
:::
|
||||
|
||||
### 4.2 削峰填谷方案:消息队列作为"蓄水池"
|
||||
|
||||
**架构设计:**
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────┐
|
||||
│ 秒杀系统架构 │
|
||||
├───────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 第一层:网关层(硬限流) │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ - 令牌桶限流:10万/s → 1万/s(丢弃90%请求) │ │
|
||||
│ │ - CDN 缓存静态资源(商品详情页) │ │
|
||||
│ │ - 验证码/排队页面(削峰第一层) │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 第二层:服务层(软限流) │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ - Nginx限流:1万/s → 5000/s │ │
|
||||
│ │ - Redis预扣库存(原子操作): │ │
|
||||
│ │ * 使用 Lua 脚本保证原子性 │ │
|
||||
│ │ * 库存不足直接返回"已售罄" │ │
|
||||
│ │ - 生成订单令牌(排队凭证) │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 第三层:消息队列层(核心削峰) │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Kafka/RocketMQ: │ │
|
||||
│ │ - 批量写入:5000/s → 1000/s(数据库承受能力) │ │
|
||||
│ │ - 消息持久化:落盘保证不丢消息 │ │
|
||||
│ │ - 多分区并行消费:提升吞吐量 │ │
|
||||
│ │ - 消费位点管理:支持故障恢复 │ │
|
||||
│ │ │ │
|
||||
│ │ 关键指标监控: │ │
|
||||
│ │ - 生产速率(Produce Rate) │ │
|
||||
│ │ - 消费速率(Consume Rate) │ │
|
||||
│ │ - 消息堆积(Lag) │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 第四层:消费层(异步处理) │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 订单处理消费者(多实例): │ │
|
||||
│ │ - 从 Kafka 拉取消息(1000/s,匹配数据库能力) │ │
|
||||
│ │ - 数据库事务:创建订单 + 扣减库存 │ │
|
||||
│ │ - 更新订单状态为"已创建" │ │
|
||||
│ │ - 发送订单创建成功通知(邮件/短信/推送) │ │
|
||||
│ │ - 确认消息消费(ACK) │ │
|
||||
│ │ │ │
|
||||
│ │ 消费者扩容策略: │ │
|
||||
│ │ - 当 Lag > 10000 时,自动增加消费者实例 │ │
|
||||
│ │ - 当 Lag < 1000 时,减少消费者实例(节省成本) │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
<PeakShavingDemo />
|
||||
|
||||
### 4.3 削峰填谷的数学原理
|
||||
|
||||
**流量平滑效果:**
|
||||
|
||||
```
|
||||
原始流量(尖峰): 平滑后流量:
|
||||
|
||||
10万/s │ ╱╲ 1000/s │████████████████
|
||||
│ ╱ ╲ │
|
||||
│ ╱ ╲ │
|
||||
1000/s│╱ ╲ 0/s │
|
||||
└─────────────── └────────────────
|
||||
0s 1s 2s 0s 20s
|
||||
|
||||
原始:10万/s 峰值,持续1秒
|
||||
平滑:1000/s 恒定速率,持续100秒
|
||||
```
|
||||
|
||||
**关键公式:**
|
||||
|
||||
```
|
||||
队列长度 = 生产者速率 × 持续时间 - 消费者速率 × 持续时间
|
||||
= 100,000 × 1 - 1,000 × 1
|
||||
= 99,000 条消息(峰值时队列堆积)
|
||||
|
||||
消费完所有消息所需时间 = 队列长度 / 消费者速率
|
||||
= 99,000 / 1,000
|
||||
= 99 秒
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心问题三:如何保证消息不丢失、不重复、有序?
|
||||
|
||||
### 5.1 消息可靠性:三道防线
|
||||
|
||||
消息可能在三个环节丢失:生产者发送时、Broker存储时、消费者处理时。
|
||||
|
||||
::: warning 🛡️ 三道防线
|
||||
**防线1:生产者确认(Producer ACK)**
|
||||
|
||||
- 发送消息时,等待 Broker 确认已收到
|
||||
- 如果没收到确认,重试或记录本地日志
|
||||
|
||||
**防线2:Broker持久化**
|
||||
|
||||
- 消息写入磁盘,而不是只在内存
|
||||
- 多副本同步,保证不丢数据
|
||||
|
||||
**防线3:消费者确认(Consumer ACK)**
|
||||
|
||||
- 处理完消息后,手动确认(ACK)
|
||||
- 如果处理失败,不确认,Broker重新投递
|
||||
:::
|
||||
|
||||
<ReliabilityDemo />
|
||||
|
||||
### 5.2 如何处理消息重复消费?
|
||||
|
||||
**消息重复可能在以下场景发生:**
|
||||
|
||||
1. **生产者重试**: 生产者发送消息后未收到ACK,重试发送同一条消息
|
||||
2. **消费者ACK超时**: 消费者处理完成但ACK超时,Broker重新投递
|
||||
3. **网络抖动**: 消费者ACK未到达Broker,Broker认为未消费
|
||||
4. **消费者重启**: 消费者重启后重新消费同一批消息
|
||||
|
||||
::: tip 💡 幂等性
|
||||
**幂等性**: 同一操作执行多次和执行一次的效果相同。
|
||||
|
||||
**生活中的幂等性**:
|
||||
|
||||
- **幂等**: 按电梯按钮(按10次和按1次,电梯都会来)
|
||||
- **非幂等**: 转账(转10元,执行两次会转20元)
|
||||
|
||||
**技术解决方案**: 为每条消息生成唯一ID,处理前检查是否已处理过。
|
||||
:::
|
||||
|
||||
<IdempotenceDemo />
|
||||
|
||||
---
|
||||
|
||||
## 6. 实战:如何选择消息队列?
|
||||
|
||||
### 6.1 四大主流消息队列对比
|
||||
|
||||
| 特性 | RabbitMQ | Kafka | RocketMQ | Redis Stream |
|
||||
| ------------ | ------------ | ------------ | -------------- | ------------ |
|
||||
| **定位** | 传统消息队列 | 分布式日志流 | 电商级消息队列 | 轻量级队列 |
|
||||
| **吞吐量** | ~1万/秒 | ~100万/秒 | ~10万/秒 | ~5万/秒 |
|
||||
| **延迟** | 微秒级 | 毫秒级 | 毫秒级 | 毫秒级 |
|
||||
| **可靠性** | 高(持久化) | 高(多副本) | 高(同步刷盘) | 中(AOF) |
|
||||
| **消息回溯** | 不支持 | 支持 | 支持 | 支持 |
|
||||
| **事务消息** | 支持(弱) | 不支持 | 支持(强) | 不支持 |
|
||||
| **延迟消息** | 支持 | 不支持 | 支持 | 不支持 |
|
||||
| **适用场景** | 传统企业应用 | 日志、大数据 | 电商、金融 | 小规模应用 |
|
||||
|
||||
::: tip 💡 选型建议
|
||||
**决策树:**
|
||||
|
||||
```
|
||||
选择消息队列:
|
||||
│
|
||||
├─ 需要事务消息(分布式事务)?
|
||||
│ ├─ 是 → RocketMQ(首选)或 RabbitMQ
|
||||
│ └─ 否 → 继续
|
||||
│
|
||||
├─ 需要处理海量日志/实时流?
|
||||
│ ├─ 是 → Kafka(首选)
|
||||
│ └─ 否 → 继续
|
||||
│
|
||||
├─ QPS > 1万/秒?
|
||||
│ ├─ 是 → RocketMQ 或 Kafka
|
||||
│ └─ 否 → 继续
|
||||
│
|
||||
├─ 需要复杂路由(如 headers 匹配)?
|
||||
│ ├─ 是 → RabbitMQ
|
||||
│ └─ 否 → 继续
|
||||
│
|
||||
├─ 已有 Redis 基础设施?
|
||||
│ ├─ 是 → Redis Stream(快速开始)
|
||||
│ └─ 否 → RabbitMQ(功能全面,学习曲线适中)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结:消息队列设计心法
|
||||
|
||||
### 7.1 核心原则回顾
|
||||
|
||||
| 原则 | 含义 | 实践要点 |
|
||||
| -------- | ---------------- | --------------------------------------- |
|
||||
| **解耦** | 服务间不直接依赖 | 通过消息队列通信,消费者故障不影响生产者 |
|
||||
| **削峰** | 平滑流量波动 | 消息队列作为蓄水池,消费者按恒定速率处理 |
|
||||
| **可靠** | 消息不丢失 | 生产者确认 + Broker持久化 + 消费者确认 |
|
||||
| **幂等** | 重复消费无影响 | 业务层面保证幂等性(唯一键、状态机) |
|
||||
| **有序** | 消息顺序保证 | 单分区有序或消费者端排序 |
|
||||
|
||||
### 7.2 设计检查清单
|
||||
|
||||
在引入消息队列前,问自己以下问题:
|
||||
|
||||
- [ ] 是否真的需要消息队列?(简单异步可以用线程池)
|
||||
- [ ] 消息丢失是否可以接受?(决定可靠性级别)
|
||||
- [ ] 消息重复是否会影响业务?(决定幂等性投入)
|
||||
- [ ] 消息顺序是否重要?(决定分区策略)
|
||||
- [ ] 消费者处理能力如何?(决定队列大小和告警阈值)
|
||||
- [ ] 如何处理消费失败?(决定重试和死信策略)
|
||||
|
||||
---
|
||||
|
||||
## 8. 名词速查表
|
||||
|
||||
| 名词 | 全称 | 解释 |
|
||||
| ----------------------- | ----------------- | --------------------------------------------------------------- |
|
||||
| **MQ** | Message Queue | **消息队列**。用于异步通信的中间件,实现生产者和消费者的解耦。 |
|
||||
| **Producer** | - | **生产者**。发送消息的一方。 |
|
||||
| **Consumer** | - | **消费者**。接收并处理消息的一方。 |
|
||||
| **Broker** | - | **消息代理**。存储和转发消息的服务端程序。 |
|
||||
| **Topic** | - | **主题**。消息的逻辑分类(如 "orders")。 |
|
||||
| **Queue** | - | **队列**。存储消息的物理容器。 |
|
||||
| **Partition** | - | **分区**。Kafka的概念,一个Topic可以分成多个Partition,提升并发。 |
|
||||
| **ACK** | Acknowledgment | **确认**。消费者处理完消息后,向Broker确认。 |
|
||||
| **Pub/Sub** | Publish/Subscribe | **发布订阅**。一种消息模式,一条消息可被多个消费者接收。 |
|
||||
| **P2P** | Point-to-Point | **点对点**。一种消息模式,一条消息只能被一个消费者接收。 |
|
||||
| **DLQ** | Dead Letter Queue | **死信队列**。存放无法消费的消息。 |
|
||||
| **Idempotence** | - | **幂等性**。多次执行结果相同。 |
|
||||
| **Throughput** | - | **吞吐量**。单位时间内处理的消息数量。 |
|
||||
| **Latency** | - | **延迟**。消息从发送到被接收的时间差。 |
|
||||
| **Persistence** | - | **持久化**。消息写入磁盘,而非仅存内存。 |
|
||||
| **Replication** | - | **副本**。为了高可用,消息被复制到多个节点。 |
|
||||
| **Transaction Message** | - | **事务消息**。保证本地事务和消息发送的一致性。 |
|
||||
| **Backpressure** | - | **背压**。消费者处理不过来时,通知生产者降速。 |
|
||||
| **Offset** | - | **偏移量**。消费者在分区中的消费位置。 |
|
||||
| **Rebalance** | - | **重平衡**。消费者组成员变化时,重新分配分区。 |
|
||||
@@ -0,0 +1,3 @@
|
||||
# 限流与背压控制
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 一个请求的完整旅程
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 搜索引擎原理
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 序列化与数据格式
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,602 @@
|
||||
# Web 框架的本质
|
||||
::: tip 🎯 核心问题
|
||||
**代码写好了,怎么让全世界的人都能访问?** 这就像问:你是想开一家路边小摊,还是经营一家跨国连锁餐厅?后端架构的选择,决定了你的"餐厅"能服务多少顾客。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要了解架构演进?
|
||||
|
||||
想象一下,你正在规划一次长途旅行。你可以选择骑自行车、开私家车、坐高铁,或者乘飞机。每种方式都有其适用的场景:自行车适合短距离且想锻炼身体的情况,飞机则适合跨越大陆的长途旅行。
|
||||
|
||||
**后端架构的选择也是如此。**
|
||||
|
||||
从互联网诞生到现在,后端架构经历了多次重大变革。每一次变革都不是为了"追新潮",而是为了解决当时面临的特定问题:
|
||||
|
||||
| 年代 | 核心问题 | 架构演进 |
|
||||
| ----- | ------------------------ | ------------------- |
|
||||
| 1990s | 如何把网站跑起来 | 物理服务器 |
|
||||
| 2000s | 代码越来越乱怎么维护 | 单体架构 + MVC |
|
||||
| 2010s | 系统太大怎么扩展和协作 | 微服务 + 容器化 |
|
||||
| 2020s | 如何降低运维成本和复杂性 | Serverless + 云原生 |
|
||||
|
||||
::: tip 📊 从表格中你能看到什么?
|
||||
让我们逐行解读这张表:
|
||||
|
||||
**1990s → 2000s**:从"能跑就行"到"需要维护"。网站从静态页面变成动态应用,代码量激增,需要更好的组织方式。
|
||||
|
||||
**2000s → 2010s**:从"单机"到"分布式"。用户量爆炸式增长,单台服务器扛不住了,需要拆分系统,水平扩展。
|
||||
|
||||
**2010s → 2020s**:从"自己运维"到"云服务"。容器和微服务虽然强大,但运维成本太高,Serverless 让开发者只关注业务逻辑。
|
||||
|
||||
**核心启示**:架构演进不是技术选型的游戏,而是**解决实际问题**的过程。每个阶段都有其适用的场景,没有"最好的架构",只有"最适合的架构"。
|
||||
:::
|
||||
|
||||
**了解架构演进的意义在于:**
|
||||
|
||||
1. **避免重复造轮子**:很多"新"概念其实早在几十年前就有雏形,了解历史能让你站在巨人的肩膀上
|
||||
2. **做出合理的技术选型**:没有最好的架构,只有最适合当前阶段的架构
|
||||
3. **理解技术背后的权衡**:每一次架构演进都是在**开发效率**、**系统性能**、**运维复杂度**之间做取舍
|
||||
4. **预判技术趋势**:历史总是押韵的,理解过去的演进规律有助于把握未来方向
|
||||
|
||||
<EvolutionIntroDemo />
|
||||
|
||||
---
|
||||
|
||||
## 2. 物理服务器时代 (1990s)
|
||||
|
||||
### 2.1 什么是物理服务器?
|
||||
|
||||
在互联网刚起步时,后端就是一台放在机房里的**物理服务器**(一台真实的电脑)。
|
||||
|
||||
::: tip 💡 通俗解释
|
||||
**物理服务器**就像你家里的台式机,但它:
|
||||
|
||||
- 7×24小时不关机
|
||||
- 放在专门的数据中心(有空调、UPS电源、消防系统)
|
||||
- 有更快的网络带宽(企业级光纤)
|
||||
- 有固定的公网IP地址(全世界都能访问)
|
||||
|
||||
这就好比你家 vs 餐厅:你家只是偶尔做饭,餐厅则是专业厨房,全天候营业,设备更专业。
|
||||
:::
|
||||
|
||||
### 2.2 核心特点
|
||||
|
||||
- **单机部署**:所有应用运行在一台物理机上
|
||||
- **手动运维**:需要人工上架、布线、安装系统
|
||||
- **垂直扩展**:性能不够时只能买更强的机器
|
||||
|
||||
::: details 🔧 垂直扩展 vs 水平扩展
|
||||
**垂直扩展**(Scale Up):升级单台服务器的配置(更多CPU、更大内存、更快硬盘)。
|
||||
|
||||
**水平扩展**(Scale Out):增加更多服务器,让它们一起工作。
|
||||
|
||||
**比喻**:
|
||||
|
||||
- 垂直扩展:把小餐厅改成大餐厅,装修更豪华,但只有一个厨师
|
||||
- 水平扩展:开连锁店,每个店规模不大,但有100家分店
|
||||
|
||||
**优缺点**:
|
||||
|
||||
- 垂直扩展简单,但有上限(顶级服务器很贵,且有限制)
|
||||
- 水平扩展理论上无限,但需要解决数据一致性问题
|
||||
:::
|
||||
|
||||
### 2.3 痛点
|
||||
|
||||
- **慢**:每次改代码都要手动上传,然后重启服务器
|
||||
- **贵**:扩容只能买更大的机器(垂直扩展)
|
||||
- **难扩展**:一台机器顶住所有请求,CPU满载时就只能排队
|
||||
|
||||
<PhysicalServerDemo />
|
||||
|
||||
### 2.4 物理服务器时代的优缺点
|
||||
|
||||
| 维度 | 评价 |
|
||||
| ------------ | ------------------------------------------------------------ |
|
||||
| **优点** | 完全掌控硬件,性能可预测;没有虚拟化开销;数据物理隔离,安全性高 |
|
||||
| **缺点** | 采购周期长(数周);前期投入大(CapEx);资源利用率低;扩容困难 |
|
||||
| **适用场景** | 金融核心系统、政府涉密系统、对数据主权有严格要求的场景 |
|
||||
|
||||
::: tip 💡 CapEx vs OpEx
|
||||
**CapEx**(Capital Expenditure):资本性支出,一次性投入大量资金购买硬件。
|
||||
|
||||
**OpEx**(Operating Expenditure):运营性支出,按使用量付费(如云服务器)。
|
||||
|
||||
**比喻**:
|
||||
|
||||
- CapEx:买房,一次性付几百万,之后每月只需交物业费
|
||||
- OpEx:租房,每月交房租,不用一次性掏大钱
|
||||
|
||||
**云时代**的启示:Serverless 和云服务让更多公司从 CapEx 转向 OpEx,降低创业门槛。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 单体架构时代 (2000s)
|
||||
|
||||
### 3.1 什么是单体架构?
|
||||
|
||||
随着框架的出现(Rails / Django / Spring),大家把所有功能都塞进一个应用里。
|
||||
|
||||
::: tip 💡 通俗解释
|
||||
**单体架构**(Monolith)就像一个超级商场:
|
||||
|
||||
- 服装区、食品区、电器区都在同一栋楼里
|
||||
- 所有员工在一个管理系统里工作
|
||||
- 如果整栋楼停电,所有区域都停止营业
|
||||
|
||||
对比微服务就像商业街:每家店独立运营,一家店关门不影响其他店。
|
||||
:::
|
||||
|
||||
<MonolithDemo />
|
||||
|
||||
### 3.2 核心特点
|
||||
|
||||
- **单一代码库**:所有功能模块在同一个项目中
|
||||
- **共享数据库**:所有模块共用同一个数据库
|
||||
- **统一部署**:整个应用作为一个整体打包部署
|
||||
|
||||
### 3.3 优点
|
||||
|
||||
- **开发简单**:一个项目搞定所有功能
|
||||
- **部署方便**:把一个大包扔到服务器上就行
|
||||
- **调试容易**:本地启动就能调试所有功能
|
||||
|
||||
### 3.4 痛点:雪崩效应
|
||||
|
||||
想象一下,如果"切菜"的师傅不小心切到了手(代码出了Bug),整个后厨都要停下来处理伤口,导致所有客人都吃不上饭。
|
||||
|
||||
这就是单体架构最大的风险:**隔离性差**。
|
||||
|
||||
::: details 🚨 真实的雪崩案例
|
||||
某电商公司双十一大促:
|
||||
|
||||
- 订单服务因为某个商品的价格计算错误,抛出异常
|
||||
- 异常没有被正确捕获,导致线程池耗尽
|
||||
- 所有后续请求(包括商品浏览、搜索、用户登录)都被阻塞
|
||||
- 整个网站彻底瘫痪,持续1小时
|
||||
|
||||
**如果用微服务**:
|
||||
|
||||
- 订单服务挂了,但商品浏览、搜索、用户登录仍然可用
|
||||
- 用户至少可以继续浏览商品,损失降到最低
|
||||
:::
|
||||
|
||||
### 3.5 单体架构的优缺点与适用场景
|
||||
|
||||
| 维度 | 评价 |
|
||||
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **优点** | 开发简单,无需考虑分布式复杂性;调试方便,本地启动即可调试全功能;部署简单,一个包即可运行;事务管理容易,单机数据库即可保证ACID |
|
||||
| **缺点** | 代码耦合度高,随着业务增长代码膨胀;技术栈单一,难以局部升级;扩展困难,只能整体扩容;故障隔离差,一个模块故障影响全局;团队协作效率低,多人改同一套代码 |
|
||||
| **适用场景** | 初创公司MVP验证、小型团队(<10人)、业务相对简单、对交付速度要求高于扩展性的场景 |
|
||||
| **不适用场景** | 大型团队并行开发、需要频繁发布不同模块、某些模块需要独立扩容的场景 |
|
||||
|
||||
::: tip 🎯 初学者建议
|
||||
如果你正在学习后端开发,**强烈建议从单体架构开始**:
|
||||
|
||||
1. **先学会走路**:理解HTTP、数据库、基本的MVC架构
|
||||
2. **再考虑跑步**:当项目真的遇到扩展性问题,再考虑微服务
|
||||
3. **避免过度设计**:很多公司的"微服务"其实是"分布式单体",更难维护
|
||||
|
||||
**学习路径**:
|
||||
|
||||
- 阶段1:用 Spring Boot / Django / Rails 写一个完整的单体应用
|
||||
- 阶段2:遇到性能瓶颈时,尝试拆分出1-2个服务
|
||||
- 阶段3:当团队规模>50人,系统真的复杂了,再全面微服务化
|
||||
:::
|
||||
|
||||
### 3.6 单体架构的技术栈
|
||||
|
||||
| 语言/框架 | 特点 | 代表企业 |
|
||||
| -------------------------- | ---------------------------- | --------------------- |
|
||||
| **Java + Spring** | 企业级开发首选,生态完善 | 阿里巴巴、京东 |
|
||||
| **PHP + Laravel/ThinkPHP** | 快速开发,适合中小型项目 | 早期 Facebook、微博 |
|
||||
| **Python + Django/Flask** | 开发效率高,适合快速原型 | Instagram、Pinterest |
|
||||
| **Ruby on Rails** | 约定优于配置,初创公司最爱 | GitHub、Twitter(早期) |
|
||||
| **Node.js + Express** | 前后端统一语言,I/O密集型场景 | Netflix、Uber |
|
||||
|
||||
---
|
||||
|
||||
## 4. 容器化与微服务 (2010s)
|
||||
|
||||
### 4.1 为什么需要微服务?
|
||||
|
||||
单体架构的痛点在2010年代集中爆发:
|
||||
|
||||
- **代码太庞大**:一个项目几百万行代码,新人入职要花一个月才能看懂
|
||||
- **部署太慢**:构建一次要30分钟,发布一次要小心翼翼
|
||||
- **协作太难**:100个开发者改同一个项目,代码冲突每天发生
|
||||
- **扩展太贵**:只需要扩展"聊天服务",却要复制整个应用
|
||||
|
||||
**微服务的核心思想**:把大应用拆成多个小服务,每个服务:
|
||||
|
||||
- 独立开发、独立部署
|
||||
- 有自己的数据库
|
||||
- 通过API通信
|
||||
|
||||
<ContainerDockerDemo />
|
||||
|
||||
::: tip 💡 Docker是什么?
|
||||
**Docker**就像是"集装箱":
|
||||
|
||||
- 每个集装箱里有独立的货物(代码 + 依赖库 + 运行环境)
|
||||
- 无论运到哪里(哪台服务器),打开集装箱就能直接开工
|
||||
- 不用担心"我这台机器没有Python 3.9"、"那个机器缺少某个库"
|
||||
|
||||
**比喻**:
|
||||
|
||||
- 没有 Docker:每次搬家,要把家具、电器、衣服一件件搬上卡车,到了新家再一件件摆好
|
||||
- 有 Docker:所有东西打包进集装箱,卡车直接运走,到了新家放下就能用
|
||||
|
||||
**核心价值**:"一次构建,到处运行"。
|
||||
:::
|
||||
|
||||
### 4.2 技术栈时间线
|
||||
|
||||
<TechStackTimelineDemo />
|
||||
|
||||
### 4.3 微服务架构
|
||||
|
||||
为了解决单体的问题,我们把大厨房拆成了很多个小厨房(服务):
|
||||
|
||||
- 专门负责用户的服务
|
||||
- 专门负责订单的服务
|
||||
- 专门负责支付的服务
|
||||
|
||||
<MicroservicesDemo />
|
||||
|
||||
### 4.4 Kubernetes 编排
|
||||
|
||||
当集装箱数量到达成百上千,就需要一个"港口调度系统":
|
||||
|
||||
- **Kubernetes (K8s)**:负责把容器安排到合适的机器上(调度、扩缩容、滚动更新)
|
||||
- **Service Mesh**:负责服务之间的交通规则(熔断、限流、重试、可观测)
|
||||
|
||||
<KubernetesDemo />
|
||||
|
||||
::: tip 💡 什么是"编排"?
|
||||
**编排**(Orchestration)是指自动管理大量容器的系统。
|
||||
|
||||
**比喻**:
|
||||
|
||||
- 没有 K8s:你手动管理100个容器,哪个挂了要手动重启,哪个流量大了要手动加机器
|
||||
- 有 K8s:你告诉它"我要这个服务一直有10个实例运行",它会自动完成:
|
||||
- 哪台服务器资源充足,就把容器调度到那里
|
||||
- 容器挂了,自动重启
|
||||
- 流量大了,自动扩容到20个实例
|
||||
- 更新代码时,滚动更新(先停1个旧实例,启动1个新实例,逐个替换)
|
||||
|
||||
**关键点**:微服务不是"拆开就好",真正的难点在于**治理和运维**。
|
||||
:::
|
||||
|
||||
### 4.5 微服务与容器化的优缺点
|
||||
|
||||
| 维度 | 评价 |
|
||||
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **优点** | 服务独立部署,技术栈可异构;故障隔离,单个服务崩溃不影响全局;按需扩展,热点服务单独扩容;团队协作友好,不同团队负责不同服务;代码库更小,易于理解和维护 |
|
||||
| **缺点** | 分布式复杂性高(网络延迟、分布式事务、服务发现);运维成本高,需要专业的DevOps团队;调试困难,问题可能需要跨多个服务追踪;数据一致性难以保证;部署和监控基础设施要求复杂 |
|
||||
| **适用场景** | 大型团队(>50人)、业务复杂需要分模块独立演进、某些模块需要独立扩容、需要多语言技术栈、对可用性要求高的系统 |
|
||||
| **不适用场景** | 小型团队、业务简单、流量小且稳定、没有专业运维团队的情况 |
|
||||
|
||||
::: details ⚠️ 微服务的陷阱
|
||||
**陷阱1:分布式单体**
|
||||
|
||||
拆了10个微服务,但它们之间紧密耦合:
|
||||
|
||||
- 服务A调用服务B,服务B调用服务C,服务C又调用服务A
|
||||
- 改一个功能,要同时改5个服务
|
||||
- 部署时,必须按顺序依次部署,否则系统报错
|
||||
|
||||
**这比单体更糟糕**:你拥有了单体的复杂性,又没有享受到微服务的独立部署好处。
|
||||
|
||||
**陷阱2:过度拆分**
|
||||
|
||||
把只有100行代码的功能也拆成一个独立服务:
|
||||
|
||||
- 10个服务,每个只有100行代码
|
||||
- 服务间通信的开销(网络序列化/反序列化)比实际业务逻辑还重
|
||||
- 运维成本爆炸:要部署、监控、日志收集10个服务
|
||||
|
||||
**正确做法**:从功能内聚的角度拆分,一个微服务应该是一个完整的业务能力(如"订单服务",而不是"订单创建服务"、"订单查询服务")。
|
||||
:::
|
||||
|
||||
### 4.6 微服务技术栈
|
||||
|
||||
| 类别 | 技术/工具 | 作用 |
|
||||
| ------------ | ---------------------------------- | -------------------- |
|
||||
| **容器化** | Docker, containerd | 应用打包与隔离 |
|
||||
| **编排调度** | Kubernetes, Docker Swarm | 容器管理与自动扩缩容 |
|
||||
| **服务发现** | Consul, etcd, ZooKeeper | 服务注册与发现 |
|
||||
| **API网关** | Kong, Zuul, Envoy | 统一入口、路由、限流 |
|
||||
| **配置中心** | Apollo, Nacos, Spring Cloud Config | 集中配置管理 |
|
||||
| **监控告警** | Prometheus, Grafana, ELK | 指标监控与日志分析 |
|
||||
| **链路追踪** | Jaeger, Zipkin, SkyWalking | 分布式请求追踪 |
|
||||
| **服务网格** | Istio, Linkerd | 流量治理与安全 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Serverless 与云原生时代 (2020s+)
|
||||
|
||||
### 5.1 为什么需要 Serverless?
|
||||
|
||||
微服务虽然好,但维护几十个小厨房还是很累。你需要担心:
|
||||
|
||||
- 厨房够不够大?(服务器扩容)
|
||||
- 停电了怎么办?(高可用)
|
||||
- 容器太多怎么管?(运维成本)
|
||||
|
||||
<ServerlessDemo />
|
||||
|
||||
::: tip 💡 Serverless 不是真的"没有服务器"
|
||||
**Serverless**的意思是"你不需要管理服务器",而不是真的没有服务器。
|
||||
|
||||
**比喻**:
|
||||
|
||||
- **物理服务器时代**:你买地、盖房、装修、雇厨师、买食材...全部自己来
|
||||
- **云服务器时代**:你租一个已经装修好的餐厅,但自己雇厨师、管理运营
|
||||
- **Serverless时代**:你只需要设计菜单,云端有共享厨房,有专业厨师,你下单他们做,按次付费
|
||||
|
||||
**核心变化**:
|
||||
|
||||
- 以前:买服务器 → 配环境 → 部署代码 → 监控 → 扩容 → 维护
|
||||
- 现在:写代码 → 上传 → 按使用量付费
|
||||
|
||||
**就像外卖**:你不需要厨房,只需要设计菜单,有人帮你做。
|
||||
:::
|
||||
|
||||
### 5.2 什么是 Serverless?
|
||||
|
||||
**Serverless = FaaS + BaaS**
|
||||
|
||||
**FaaS**(Function as a Service,函数即服务):
|
||||
|
||||
- 你只写函数(如"用户注册时发送欢迎邮件")
|
||||
- 云厂商负责运行这个函数,自动扩缩容
|
||||
- 典型代表:AWS Lambda、阿里云函数计算
|
||||
|
||||
**BaaS**(Backend as a Service,后端即服务):
|
||||
|
||||
- 登录 → Auth0 / Supabase Auth
|
||||
- 支付 → Stripe
|
||||
- 数据库 → Supabase / Firebase / DynamoDB
|
||||
- 消息 → Kafka / SQS
|
||||
|
||||
::: tip 🎯 Serverless 适用场景
|
||||
**最佳场景**:
|
||||
|
||||
1. **潮汐流量**:外卖软件,中午流量大,半夜没人。Serverless会自动在中午分配1000台机器,半夜缩减到0台
|
||||
2. **事件驱动**:"用户上传图片后,自动压缩图片"
|
||||
3. **快速验证**:小团队、MVP、黑客松项目
|
||||
|
||||
**不适合场景**:
|
||||
|
||||
1. **长时间运行的任务**:视频转码(可能跑1小时,函数最大执行时间通常只有15分钟)
|
||||
2. **需要低延迟的应用**:高频交易(冷启动延迟可能几十毫秒到几秒)
|
||||
3. **需要精细控制底层**:操作系统内核调优、GPU直接访问
|
||||
:::
|
||||
|
||||
### 5.3 Serverless 与云原生的优缺点
|
||||
|
||||
| 维度 | 评价 |
|
||||
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **优点** | 零运维成本,开发者只需关注业务代码;自动扩缩容,完美应对流量峰值;按需付费,无流量时成本接近零;快速上线,几分钟即可部署全球;高可用内置,云服务自动处理故障转移 |
|
||||
| **缺点** | 冷启动延迟(几百毫秒到数秒);运行时长限制(通常5-15分钟);调试困难,本地难以完全模拟云环境;供应商锁定风险;不适合长时间运行或计算密集型任务;成本在高频持续流量下可能反超传统方案 |
|
||||
| **适用场景** | 事件驱动处理(图片处理、消息通知);潮汐流量应用(活动页、促销);快速原型验证和MVP;低频API或后台任务;无专职运维团队的小团队 |
|
||||
| **不适用场景** | 需要持续低延迟的应用;长时间计算任务;对冷启动敏感的场景(高频交易);需要精细控制底层基础设施的场景 |
|
||||
|
||||
::: details 💰 成本对比:何时Serverless更贵?
|
||||
**场景1:低频访问**
|
||||
|
||||
- 传统服务器:每月$20(不管有没有人访问)
|
||||
- Serverless:100万次请求 × $0.0002/次 = $20(仅在有流量时付费)
|
||||
- **结论**:低频场景,Serverless更省钱
|
||||
|
||||
**场景2:高频持续访问**
|
||||
|
||||
- 传统服务器:每月$20
|
||||
- Serverless:1亿次请求 × $0.0002/次 = $20,000
|
||||
- **结论**:高频持续场景,传统服务器更省钱
|
||||
|
||||
**场景3:潮汐流量**
|
||||
|
||||
- 传统服务器:为了应对峰值,需要$100/月的服务器(平时资源利用率只有10%)
|
||||
- Serverless:峰值时$20,平时几乎$0
|
||||
- **结论**:潮汐流量场景,Serverless节省成本
|
||||
|
||||
**启示**:不要盲目上Serverless,要根据实际流量特征做成本测算。
|
||||
:::
|
||||
|
||||
### 5.4 Serverless 技术栈与平台
|
||||
|
||||
| 类别 | 技术/平台 | 特点 |
|
||||
| ------------ | ------------------------ | ---------------------------- |
|
||||
| **FaaS平台** | AWS Lambda | 最早的FaaS服务,生态最成熟 |
|
||||
| | Azure Functions | 微软云集成度高,.NET友好 |
|
||||
| | Google Cloud Functions | 与GCP服务深度集成 |
|
||||
| | 阿里云函数计算 | 国内生态完善,冷启动优化好 |
|
||||
| | 腾讯云云函数 | 与微信生态整合 |
|
||||
| | Vercel/Netlify Functions | 前端开发者友好,边缘部署 |
|
||||
| **BaaS服务** | Firebase | Google的移动端后端方案 |
|
||||
| | Supabase | PostgreSQL的Firebase开源替代 |
|
||||
| | AWS Amplify | AWS的移动和Web应用开发平台 |
|
||||
| **部署工具** | Serverless Framework | 多云部署,社区活跃 |
|
||||
| | Terraform | 基础设施即代码 |
|
||||
| | Pulumi | 用编程语言定义基础设施 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 各架构阶段对比与选型指南
|
||||
|
||||
### 6.1 架构演进全景对比
|
||||
|
||||
<ArchitectureComparisonDemo />
|
||||
|
||||
| 维度 | 物理服务器 | 单体架构 | 微服务+容器 | Serverless |
|
||||
| ---------------- | ---------------------- | ------------------ | ------------------------ | ------------------ |
|
||||
| **团队规模** | 1-5人 | 5-50人 | 50-500人 | 1-20人 |
|
||||
| **部署复杂度** | 极高 | 低 | 极高 | 极低 |
|
||||
| **运维成本** | 高 | 中 | 很高 | 低 |
|
||||
| **扩展性** | 差 | 垂直扩展有限 | 水平扩展优秀 | 自动扩展 |
|
||||
| **技术栈灵活性** | 无 | 单一 | 多样化 | 受限 |
|
||||
| **冷启动** | 无 | 无 | 容器启动时间 | 有延迟 |
|
||||
| **适用场景** | 遗留系统、特殊合规要求 | 初创公司、业务简单 | 大型互联网公司、复杂业务 | 快速验证、事件驱动 |
|
||||
|
||||
### 6.2 技术选型决策树
|
||||
|
||||
```
|
||||
开始选型
|
||||
│
|
||||
├─ 团队有专业运维人员?
|
||||
│ ├─ 是 → 考虑微服务或物理机
|
||||
│ └─ 否 → 继续判断
|
||||
│
|
||||
├─ 需要快速上线验证想法?
|
||||
│ ├─ 是 → Serverless 或单体
|
||||
│ └─ 否 → 继续判断
|
||||
│
|
||||
├─ 团队规模 > 50人?
|
||||
│ ├─ 是 → 考虑微服务
|
||||
│ └─ 否 → 继续判断
|
||||
│
|
||||
├─ 流量有明显峰谷特征?
|
||||
│ ├─ 是 → Serverless
|
||||
│ └─ 否 → 单体架构(推荐初创)
|
||||
│
|
||||
└─ 特殊要求(合规、遗留系统)?
|
||||
└─ 是 → 物理服务器
|
||||
```
|
||||
|
||||
::: tip 🎯 初学者选型建议
|
||||
**如果你是个开发者或小团队:**
|
||||
|
||||
1. **阶段0 (学习)**:本地跑单体应用,理解HTTP、数据库、基本架构
|
||||
2. **阶段1 (MVP)**:部署单体应用到云服务器(如阿里云ECS、AWS EC2)
|
||||
3. **阶段2 (增长)**:当团队>10人、业务变复杂,考虑拆分出1-2个微服务
|
||||
4. **阶段3 (成熟)**:当团队>50人、流量百万级,全面微服务化
|
||||
|
||||
**关键原则**:不要一开始就上微服务,那是"过早优化"。让架构随业务成长而演进。
|
||||
:::
|
||||
|
||||
### 6.3 不同场景下的推荐架构
|
||||
|
||||
#### 场景一:独立开发者/兼职项目
|
||||
|
||||
- **推荐架构**:Serverless (Vercel/Netlify) 或 单体应用
|
||||
- **理由**:几乎零运维成本,按需付费,快速上线
|
||||
- **示例技术栈**:Next.js + Vercel + Supabase
|
||||
|
||||
#### 场景二:初创公司MVP验证
|
||||
|
||||
- **推荐架构**:单体架构 + 云服务器
|
||||
- **理由**:开发速度快,团队可以专注于业务逻辑而非基础设施
|
||||
- **示例技术栈**:Spring Boot / Django / Rails + RDS + ECS
|
||||
|
||||
#### 场景三:成长型公司(10-50人团队)
|
||||
|
||||
- **推荐架构**:模块化单体 或 轻量级微服务
|
||||
- **理由**:开始面临代码耦合问题,但还不需要完整的微服务复杂度
|
||||
- **示例技术栈**:Spring Cloud / Go Micro + Kubernetes
|
||||
|
||||
#### 场景四:大型互联网公司
|
||||
|
||||
- **推荐架构**:微服务 + 服务网格 + 中台架构
|
||||
- **理由**:团队规模大,业务复杂,需要独立的发布节奏和技术栈
|
||||
- **示例技术栈**:自研RPC框架 + Istio + 自建PaaS平台
|
||||
|
||||
#### 场景五:事件驱动/潮汐流量应用
|
||||
|
||||
- **推荐架构**:Serverless + 事件总线
|
||||
- **理由**:流量波动大,需要极致的成本优化和自动扩缩容
|
||||
- **示例技术栈**:AWS Lambda + API Gateway + EventBridge
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结与学习路线
|
||||
|
||||
### 7.1 核心要点
|
||||
|
||||
后端架构的演进,本质上是在做**加法**和**减法**:
|
||||
|
||||
| 时代 | 架构 | 开发者要做的事 | 运维要做的事 |
|
||||
| :------------- | :----- | :--------------- | :----------------- |
|
||||
| **物理时代** | 单机 | 写脚本、手动部署 | 维护机房与硬件 |
|
||||
| **单体时代** | 一整块 | 写所有业务逻辑 | 维护几台大服务器 |
|
||||
| **微服务时代** | 拆分 | 关注单一业务 | 维护K8s集群(很累!) |
|
||||
| **Serverless** | 函数 | 只写核心函数 | 喝茶(云厂商全包了) |
|
||||
|
||||
**关键洞察**:
|
||||
|
||||
- 架构演进不是"新技术取代旧技术",而是**适用场景的变化**
|
||||
- 没有银弹,每个架构都有其适用的边界
|
||||
- 选择架构要考虑:团队规模、业务复杂度、流量特征、运维能力
|
||||
|
||||
### 7.2 学习路线建议
|
||||
|
||||
根据你的职业阶段,推荐以下学习路径:
|
||||
|
||||
#### 阶段一:打好基础(0-1年)
|
||||
|
||||
**目标**:理解后端核心概念,能独立开发单体应用
|
||||
|
||||
- 掌握一门后端语言(Java/Python/Go任选其一)
|
||||
- 学习HTTP协议和RESTful API设计
|
||||
- 掌握关系型数据库(MySQL/PostgreSQL)
|
||||
- 了解缓存基础(Redis)
|
||||
- 学习Git和基础Linux命令
|
||||
- **实践项目**:用单体架构完成一个CRUD应用(如博客系统、待办事项)
|
||||
|
||||
#### 阶段二:扩展能力(1-3年)
|
||||
|
||||
**目标**:理解分布式系统,能参与微服务开发
|
||||
|
||||
- 深入学习微服务架构和拆分策略
|
||||
- 掌握Docker和Kubernetes基础
|
||||
- 学习消息队列(Kafka/RabbitMQ)
|
||||
- 了解分布式事务和一致性
|
||||
- 掌握监控和日志(Prometheus/ELK)
|
||||
- **实践项目**:将单体应用拆分为3-5个微服务,使用Docker部署
|
||||
|
||||
#### 阶段三:专业深化(3-5年)
|
||||
|
||||
**目标**:能设计大型系统,具备技术选型能力
|
||||
|
||||
- 深入理解云原生架构(Service Mesh、Serverless)
|
||||
- 掌握容量规划和性能调优
|
||||
- 了解多活架构和灾备设计
|
||||
- 学习DDD(领域驱动设计)
|
||||
- 培养技术判断力和架构思维
|
||||
- **实践项目**:设计一个支持百万级用户的系统架构,包含高可用、弹性伸缩等方案
|
||||
|
||||
### 7.3 持续学习资源推荐
|
||||
|
||||
**书籍**:
|
||||
|
||||
- 《设计数据密集型应用》(DDIA)- 分布式系统必读
|
||||
- 《云原生模式》
|
||||
- 《微服务设计》
|
||||
- 《领域驱动设计》
|
||||
|
||||
**在线资源**:
|
||||
|
||||
- AWS/Azure/阿里云官方架构文档
|
||||
- CNCF(云原生计算基金会)项目文档
|
||||
- 各大公司技术博客(Netflix Tech Blog、阿里技术公众号等)
|
||||
|
||||
---
|
||||
|
||||
## 8. 名词速查表(Glossary)
|
||||
|
||||
| 名词 | 全称 | 解释 |
|
||||
| :---------------- | :-------------------------------- | :------------------------------------------------ |
|
||||
| **Backend** | - | 服务器端系统,负责处理业务逻辑、数据存储和对外接口 |
|
||||
| **CGI** | Common Gateway Interface | 早期动态网页技术,通过脚本处理请求并返回结果 |
|
||||
| **Monolith** | - | 单体架构,把所有业务逻辑打包在同一个应用中 |
|
||||
| **Microservices** | - | 微服务架构,把业务拆分成多个独立服务 |
|
||||
| **Container** | - | 容器化技术,把应用和依赖打包成可移植单元 |
|
||||
| **K8s** | Kubernetes | 容器编排平台,用于调度、扩缩容和治理容器 |
|
||||
| **Service Mesh** | - | 服务网格,负责微服务间通信治理、观测与安全 |
|
||||
| **Serverless** | - | 无服务计算,开发者只写函数,平台自动运行与扩缩容 |
|
||||
| **BaaS** | Backend as a Service | 即插即用的后端云服务(认证、数据库、支付等) |
|
||||
| **CI/CD** | Continuous Integration / Delivery | 持续集成与持续交付,自动化测试与部署流程 |
|
||||
| **Observability** | - | 可观测性,利用日志/指标/追踪理解系统运行状态 |
|
||||
@@ -0,0 +1,3 @@
|
||||
# A/B 测试与实验驱动
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 数据分析基础(统计 / 指标 / 漏斗)
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 数据治理与数据质量
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 数据模型全景(文档 / 图 / 时序 / 向量)
|
||||
|
||||
> 待实现
|
||||
@@ -1,5 +1,4 @@
|
||||
# 埋点设计:从原理到实战 (Interactive Guide to Event Tracking)
|
||||
|
||||
# 数据埋点与用户行为采集
|
||||
> 💡 **学习指南**:本章节带你深入理解数据采集的基石——埋点设计。我们将从最基础的"为什么要埋点"讲起,一步步掌握埋点方案、数据模型、处理流程,以及实战中的坑与解决方案。
|
||||
|
||||
<TrackingOverviewDemo />
|
||||
@@ -0,0 +1,3 @@
|
||||
# 数据可视化与仪表盘
|
||||
|
||||
> 待实现
|
||||
@@ -1,5 +1,4 @@
|
||||
# 数据库原理入门:为什么淘宝能在 0.01 秒内找到你的订单?
|
||||
|
||||
# 数据库原理(索引 / 事务 / 查询优化)
|
||||
::: tip 🎯 核心问题
|
||||
**为什么你的 Excel 查询要 10 秒,而淘宝搜索只要 0.01 秒?** 当数据从"几千条"变成"十亿条",从"单人使用"变成"千万人同时访问",Excel 就不够用了。数据库就是为解决这个问题而生的——它是专门处理海量数据、高并发访问的"超级 Excel"。本章将带你从零开始理解数据库的核心原理。
|
||||
:::
|
||||
@@ -0,0 +1,3 @@
|
||||
# SQL
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 分布式系统的挑战
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 高可用与容灾
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 从单体到微服务的演进
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 系统设计方法论
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,701 @@
|
||||
# CI / CD 自动化
|
||||
::: tip 🎯 核心问题
|
||||
**代码在本地跑得好好的,怎么让全世界的人都能访问?**
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要"服务上线"?
|
||||
|
||||
想象一下,你在自己家里做了一桌子菜,非常好吃。但问题是,只有自家人能吃到,邻居、保安、陌生人他们都尝不到。
|
||||
|
||||
怎么办?你需要**把菜端到餐厅里**。这就是"服务上线"要做的事——把你写的代码,从个人电脑,搬到一个7×24小时永远开着的"公共电脑"上。这样任何人只要能上网,就能访问你的网站。
|
||||
|
||||
<DeploymentOverviewDemo />
|
||||
|
||||
服务上线涉及很多环节。就像开餐厅不仅仅是端菜出去,你还需要租店面、装修、办执照、雇服务员等。开发网站也是同理。从代码到用户能访问的网站,中间隔着很多步骤。需要一步步完成构建、部署、配置网络、保证安全等工作。
|
||||
|
||||
下面我会把整个流程拆开来讲。每个环节都掰碎、揉细。保证连完全没基础的小白也能看懂。
|
||||
|
||||
---
|
||||
|
||||
## 2. 构建:把代码变成"可携带的包裹"
|
||||
|
||||
### 2.1 为什么要构建?
|
||||
|
||||
新手常问:代码写好了,为什么不能直接放到服务器上让用户访问?
|
||||
|
||||
要回答这个问题,先搞清楚你写的代码是什么格式。你可能用 Vue、React、Express、Koa 等框架。这些框架有一个共同特点:**它们不是给浏览器或服务器直接用的**。
|
||||
|
||||
举个例子。你写 Vue 代码时,是不是用过 `<template>`、`<script setup>` 这种标签?这种语法只有 Vue 认识。浏览器根本看不懂。浏览器只认识三种语言:HTML(网页结构)、CSS(网页样式)、JavaScript(网页逻辑)。Vue 组件语法对浏览器来说就像天书,完全无法理解。
|
||||
|
||||
所以在把代码放到服务器之前,必须做一件重要的事:**把它翻译成浏览器能看懂的语言**。这个翻译过程叫做"构建"(Build)。
|
||||
|
||||
### 2.2 构建具体做什么?
|
||||
|
||||
构建不只是翻译。它还会做很多优化。让网站跑起来更快、更省资源。详细说说它具体都干了哪些活:
|
||||
|
||||
**第一步:解析依赖**
|
||||
|
||||
写代码时,会用到各种第三方库。比如 Vue、Vue Router、Axios、Vite 等。这些库不可能每次都让用户从 npm 下载。那样太慢了。构建工具会分析代码,把所有依赖找出来。然后把它们"打包"到一起。
|
||||
|
||||
**第二步:编译转换**
|
||||
|
||||
这是最核心的一步。把 Vue 组件编译成 HTML 和 JavaScript。把 SASS/LESS 编译成 CSS。把 ES6+ 新语法转换成兼容性更好的 ES5 代码。这步完成后,代码就从"开发者能看懂的格式"变成"机器能执行的格式"。
|
||||
|
||||
**第三步:压缩混淆**
|
||||
|
||||
压缩就是把所有空格、换行、注释删掉。把变量名从英文单词改成单个字母。比如 `userName` 变成 `a`,`calculateTotalPrice` 变成 `b`。这样文件大小大幅减小。用户下载起来就快多了。混淆后的代码人类基本看不懂。也能起到一点"保护代码"的作用。
|
||||
|
||||
**第四步:代码分割**
|
||||
|
||||
可能写了10个页面。每个页面有自己的代码。但用户可能只访问其中一个页面。为什么要下载其他9个页面的代码?构建工具会把代码分割成多个小块。用户访问哪个页面就下载哪个页面的代码。这就是"按需加载"。能大幅提升首次访问的速度。
|
||||
|
||||
**第五步:生成哈希**
|
||||
|
||||
这是非常重要的一步。但很多人会忽略。构建完成后,文件名会变成类似 `app.abc123.js`、`vendor.def456.css` 这样的格式。后面那串字母数字混合的字符串叫"哈希"。
|
||||
|
||||
哈希的作用是:当代码有任何改动时,哈希值就会变化。浏览器就知道"这个文件变了,需要重新下载"。没变的文件,浏览器继续使用缓存。不用重复下载。这样既能保证用户看到最新代码,又能充分利用缓存提升速度。
|
||||
|
||||
<DeploymentBuildDemo />
|
||||
|
||||
### 2.3 怎么执行构建?
|
||||
|
||||
大多数现代前端项目都已经配好构建工具。只需要记住一个命令:
|
||||
|
||||
```bash
|
||||
# 如果用 npm
|
||||
npm run build
|
||||
|
||||
# 如果用 yarn
|
||||
yarn build
|
||||
|
||||
# 如果用 pnpm
|
||||
pnpm build
|
||||
```
|
||||
|
||||
运行完后,去项目根目录找一个叫 `dist` 的文件夹(有时也叫 `build` 或 `.output`)。里面就是构建好的所有文件。这些文件就是最终要上传到服务器的东西。不需要再做任何修改。直接拖到服务器上就行。
|
||||
|
||||
### 2.4 构建产物里有什么?
|
||||
|
||||
打开 dist 文件夹,会看到里面主要是三类文件:
|
||||
|
||||
- **HTML文件**:通常叫 `index.html`。这是入口文件。浏览器首先加载的就是它。
|
||||
- **JS文件**:所有 JavaScript 代码。可能是1个也可能是好几个。
|
||||
- **CSS文件**:所有样式代码。可能内联在 HTML 里,也可能是单独的 CSS 文件。
|
||||
|
||||
如果是比较复杂的后端项目(比如 Node.js),构建产物可能是一个可执行文件,或者一个 Docker 镜像。但原理是一样的:把代码变成服务器能直接运行的形式。
|
||||
|
||||
---
|
||||
|
||||
## 3. 服务器:找一台永远不关门的"房子"
|
||||
|
||||
### 3.1 服务器到底是什么?
|
||||
|
||||
很多人第一次听到"服务器",觉得是什么高大上的神秘设备。其实没那么复杂。**服务器就是一台电脑**。一台永远不关机、一直插着网线的电脑。
|
||||
|
||||
可能有人问:我自己家里不是有电脑吗?为什么要额外花钱租服务器?
|
||||
|
||||
这个问题问得好。帮你分析一下:
|
||||
|
||||
首先,你家的电脑不可能24小时开着。你要出门、要睡觉、偶尔还会死机重启。但服务器不一样。它专门用来干这个。可以365天全年无休地运行。网站随时都能访问。
|
||||
|
||||
其次,你家的网络也不行。家用宽带的上传速度通常很慢。而且家用宽带的 IP 是动态变化的。今天是这个 IP,明天可能就变成另外一个了。根本没法用来做网站服务器。服务器用的是数据中心的高速网络。IP 固定,网速飞快。
|
||||
|
||||
第三,你家的电脑没有"公网IP"。什么叫公网IP?就是全世界独一无二的地址。只有有这个地址,别人才能在互联网上找到你的电脑。你家电脑的 IP 通常只能在你家局域网里用。外面的人根本找不到你。服务器就不同了。它有一个固定的公网 IP。全世界的人都能通过这个 IP 找到它。
|
||||
|
||||
<DeploymentServerDemo />
|
||||
|
||||
### 3.2 怎么选服务器?
|
||||
|
||||
选服务器主要看三个指标:**CPU核数**、**内存大小**、**硬盘空间**。这三个指标越高,服务器性能越好,价格也越贵。
|
||||
|
||||
对于刚入门的新手,完全没必要买特别贵的配置。记住一个简单的选法:
|
||||
|
||||
- **个人项目、学习练手**:1核2G内存,足够了。一个月大概几十块钱。
|
||||
- **小型商业项目**:2核4G内存。能承载每天几千到几万访问量。
|
||||
- **中型项目**:4核8G或更高。需要专业团队来运维了。
|
||||
|
||||
还有一个要考虑的点:**地域**。如果用户主要在中国,就买国内的服务器(阿里云、腾讯云),访问速度快。如果用户主要在海外,就买国外的服务器(AWS、Google Cloud、DigitalOcean),或者买香港的服务器。速度快而且不用备案。
|
||||
|
||||
### 3.3 国内还是国外?
|
||||
|
||||
这是个很重要的问题。很多人刚开始没想清楚。后期会遇到麻烦。
|
||||
|
||||
**买国内服务器**的好处是速度快、延迟低。缺点是需要备案(提交网站信息给国家相关部门审核)。通常要等一周到一个月。而且国内服务器价格相对贵一些。
|
||||
|
||||
**买国外服务器**的好处是不用备案。买了就能用。价格也可能更便宜。缺点是中国大陆用户访问速度可能慢一些。如果是香港或新加坡机房会好很多。
|
||||
|
||||
建议是:如果是个人项目、学习展示用的网站,买香港或海外的服务器。省去备案的麻烦。如果是做正规商业项目,需要长期运营,就买国内服务器。老老实实备案,后期会省很多麻烦。
|
||||
|
||||
### 3.4 主流云厂商对比
|
||||
|
||||
| 厂商 | 适合人群 | 特点 | 新用户价格 |
|
||||
|------|---------|------|-----------|
|
||||
| 阿里云 | 国内业务 | 市场占有率第一,生态完善 | 首年几十到一百多 |
|
||||
| 腾讯云 | 小程序、游戏 | 小程序云开发支持好 | 首年优惠力度大 |
|
||||
| 华为云 | 企业用户 | 政府、政务项目首选 | 价格偏高 |
|
||||
| DigitalOcean | 开发者 | 简单好用,价格透明 | $4/月起 |
|
||||
| Vercel | 前端项目 | 零配置,直接推送就上线 | 免费额度够用 |
|
||||
|
||||
新手最推荐 **阿里云** 或 **腾讯云** 的学生机/新用户优惠。通常一年只需要几十块钱。性价比极高。如果做的是纯前端项目,想省事,也可以直接用 **Vercel** 或 **Netlify**。连服务器都不用买。把代码推送上去就自动部署好了。
|
||||
|
||||
### 3.5 拿到服务器后该做什么?
|
||||
|
||||
买完服务器后,会收到一封邮件。里面包含几个重要信息:
|
||||
|
||||
- **IP地址**:一串类似 `123.45.67.89` 的数字。这是服务器在互联网上的门牌号。
|
||||
- **登录用户名**:通常是 `root`(管理员账号)。
|
||||
- **登录密码**:初始密码,或者是让你设置密码的链接。
|
||||
|
||||
有了这些信息,就可以用 **SSH(Secure Shell)** 远程登录到服务器上。对它进行各种配置。SSH 就像是给服务器发的一条加密的远程控制命令。让自己电脑上就能操作远在天边的服务器。
|
||||
|
||||
登录命令是这样的:
|
||||
|
||||
```bash
|
||||
ssh root@123.45.67.89
|
||||
# 按回车后会让你输入密码。输入正确的密码后就登录成功了。
|
||||
```
|
||||
|
||||
登录成功后,就进入了服务器的命令行界面。看起来和在自己电脑上开了一个终端窗口差不多。可以在这里安装软件、创建文件夹、修改配置。一切操作都和本地电脑一样。
|
||||
|
||||
---
|
||||
|
||||
## 4. 部署:把代码搬进"房子"
|
||||
|
||||
### 4.1 部署是什么?
|
||||
|
||||
部署就是租好了服务器(房子)之后,把代码(行李家具)搬进去。然后打开门开始营业的过程。
|
||||
|
||||
具体来说,部署包括以下几个步骤:
|
||||
|
||||
1. **把代码上传到服务器**:把构建产物从本地电脑传到服务器上。
|
||||
2. **安装依赖**:服务器上可能没有项目需要的各种包。需要安装。
|
||||
3. **配置环境变量**:比如数据库密码、API密钥等敏感信息。
|
||||
4. **启动服务**:让应用程序跑起来。开始监听用户的请求。
|
||||
|
||||
这四个步骤听起来挺复杂。但其实做起来没那么难。下面会详细介绍每一步怎么做。
|
||||
|
||||
<DeploymentServerDemo />
|
||||
|
||||
### 4.2 怎么把代码上传到服务器?
|
||||
|
||||
**方法一:FTP/SFTP 上传**
|
||||
|
||||
这是最直观的方式。就像用网盘一样。把文件拖到服务器上。可以在自己电脑上下载一个叫 **FileZilla** 的免费软件。填入服务器的IP、用户名、密码。就能像管理本地文件一样管理服务器上的文件了。
|
||||
|
||||
**方法二:Git 拉取**
|
||||
|
||||
这是更推荐的方式。先在 GitHub、GitLab 或 Gitee 上创建一个代码仓库。把代码推送到云端。然后在服务器上用 `git clone` 命令把代码拉下来。
|
||||
|
||||
这样好处是:后续更新代码只需要在服务器上执行 `git pull` 命令就行。不用每次都手动上传。而且代码存云端也安全。服务器重装了也不怕。
|
||||
|
||||
**方法三:CI/CD 自动部署**
|
||||
|
||||
这是最专业的方式。也是强烈推荐的方式。通过配置 CI/CD(持续集成/持续部署),只需要把代码推送到 GitHub。CI/CD 系统就会自动帮你完成:拉取代码 → 安装依赖 → 构建 → 部署的全过程。甚至不需要登录服务器。一切都是自动完成的。
|
||||
|
||||
### 4.3 部署的具体步骤
|
||||
|
||||
假设用最简单的方式——Git 手动部署。一步步演示整个过程:
|
||||
|
||||
**第一步:连接到服务器**
|
||||
|
||||
```bash
|
||||
ssh root@123.45.67.89
|
||||
```
|
||||
|
||||
**第二步:安装必要的软件**
|
||||
|
||||
如果是 Node.js 项目,需要先安装 Node.js:
|
||||
|
||||
```bash
|
||||
# 以 Ubuntu 系统为例
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
```
|
||||
|
||||
**第三步:拉取代码**
|
||||
|
||||
```bash
|
||||
# 创建放网站的目录
|
||||
mkdir -p /var/www/my-website
|
||||
cd /var/www/my-website
|
||||
|
||||
# 克隆代码仓库(需要先在GitHub上创建好仓库)
|
||||
git clone https://github.com/你的用户名/你的仓库名.git .
|
||||
```
|
||||
|
||||
**第四步:安装依赖并构建**
|
||||
|
||||
```bash
|
||||
# 安装项目依赖
|
||||
npm install
|
||||
|
||||
# 构建项目(生成 dist 目录)
|
||||
npm run build
|
||||
```
|
||||
|
||||
**第五步:用 PM2 启动服务**
|
||||
|
||||
为什么要用 PM2?它是一个进程管理工具。可以让网站在后台持续运行。就算服务器重启了也能自动启动。
|
||||
|
||||
```bash
|
||||
# 全局安装 PM2
|
||||
sudo npm install -g pm2
|
||||
|
||||
# 启动网站(假设入口文件是 index.js)
|
||||
pm2 start index.js
|
||||
|
||||
# 设置开机自启
|
||||
pm2 startup
|
||||
pm2 save
|
||||
```
|
||||
|
||||
**第六步:配置 Nginx 反向代理**
|
||||
|
||||
Node.js 应用通常跑在 3000 或 8080 这样的端口上。但用户访问的是 80 端口(HTTP默认端口)。需要用 Nginx 把 80 端口的请求转发到应用端口。
|
||||
|
||||
```bash
|
||||
# 安装 Nginx
|
||||
sudo apt install -y nginx
|
||||
|
||||
# 创建 Nginx 配置文件
|
||||
sudo nano /etc/nginx/sites-available/my-website
|
||||
```
|
||||
|
||||
在打开的编辑器里写入以下配置:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com www.example.com;
|
||||
|
||||
# 静态文件(构建产物)直接返回
|
||||
location / {
|
||||
root /var/www/my-website/dist;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API 请求转发到 Node.js 后端
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
保存退出后,启用这个配置:
|
||||
|
||||
```bash
|
||||
# 启用配置
|
||||
sudo ln -s /etc/nginx/sites-available/my-website /etc/nginx/sites-enabled/
|
||||
|
||||
# 测试配置是否有错误
|
||||
sudo nginx -t
|
||||
|
||||
# 重启 Nginx
|
||||
sudo systemctl restart nginx
|
||||
```
|
||||
|
||||
现在访问 `http://example.com`(记得先把域名解析到这个服务器IP),应该就能看到网站了!
|
||||
|
||||
---
|
||||
|
||||
## 5. 域名和 DNS:给网站起个好名字
|
||||
|
||||
### 5.1 为什么要买域名?
|
||||
|
||||
有了服务器 IP,为什么还要买域名?
|
||||
|
||||
想想看。让你记住一串数字 `123.45.67.89` 是不是很困难?是不是很容易敲错?但让你记住 `baidu.com`、`taobao.com` 这样的名字是不是就简单多了?
|
||||
|
||||
域名就是网站的名字。好记、专业。还能体现品牌形象。想象一下。告诉别人"访问我做的网站,IP 是 123.45.67.89",和"访问 woshishuaige.com",哪个更像那么回事?
|
||||
|
||||
<DeploymentDnsDemo />
|
||||
|
||||
### 5.2 DNS 是什么?
|
||||
|
||||
好。现在买了一个域名。比如叫 `my-awesome-website.com`。但问题来了:电脑只认识 IP 地址。不认识 "my-awesome-website.com" 这种人类语言啊。
|
||||
|
||||
这就需要 DNS 出场了。DNS 的全称是 "Domain Name System"。翻译过来就是"域名系统"。可以把它理解成一本巨大的"电话簿"。专门负责把人类好记的域名翻译成电脑能看懂的 IP 地址。
|
||||
|
||||
当在浏览器里输入 `my-awesome-website.com` 并回车时。背后发生了这些事情:
|
||||
|
||||
1. 浏览器问 DNS:"hey,my-awesome-website.com 的 IP 地址是多少?"
|
||||
2. DNS 查了一下"电话簿",告诉浏览器:"它的 IP 是 123.45.67.89"
|
||||
3. 浏览器根据这个 IP 地址,找到了服务器,发出了请求
|
||||
|
||||
整个过程通常只需要几十毫秒。用户完全感知不到。
|
||||
|
||||
### 5.3 怎么配置 DNS?
|
||||
|
||||
配置 DNS 通常有两个地方可以操作:
|
||||
|
||||
**方式一:在域名购买商那里配置**
|
||||
|
||||
在哪里买的域名,就去哪里配置 DNS 记录。最常见的记录类型是 **A 记录**:
|
||||
|
||||
- **记录类型**:A
|
||||
- **主机记录**:通常填 `@`(代表域名本身,如 my-awesome-website.com)或者 `www`(代表 www.my-awesome-website.com)
|
||||
- **记录值**:服务器 IP 地址,如 `123.45.67.89`
|
||||
|
||||
**方式二:使用第三方 DNS 服务**
|
||||
|
||||
很多专业玩家不用域名商自带的 DNS。而是用 Cloudflare、阿里云 DNSPod、腾讯云 DNS 这些专业的 DNS 服务商。这些服务通常更稳定、解析速度更快。还自带 CDN、DDoS 防护等增值功能。
|
||||
|
||||
### 5.4 DNS 生效要多久?
|
||||
|
||||
这是很多人关心的问题。答案是:**不一定。通常几分钟到 24 小时**。
|
||||
|
||||
DNS 修改后,全球所有的 DNS 服务器需要同步这个变更。这就像往大海里扔一颗石子。波浪需要时间才能传到远方。有些 DNS 服务器更新快,几分钟就生效了。有些比较慢,可能需要等很久。
|
||||
|
||||
可以用以下命令检查 DNS 是否生效:
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
ping 你的域名
|
||||
|
||||
# Mac/Linux
|
||||
ping 你的域名
|
||||
```
|
||||
|
||||
如果 ping 得通,显示的是服务器的 IP。说明 DNS 已经生效了。
|
||||
|
||||
---
|
||||
|
||||
## 6. HTTPS:给网站装一把"锁"
|
||||
|
||||
### 6.1 HTTP 和 HTTPS 的区别
|
||||
|
||||
可能注意到了。有些网站地址是 `http://` 开头的。有些是 `https://` 开头的。这个"s"很重要。它代表"安全"(Secure)。
|
||||
|
||||
**HTTP(HyperText Transfer Protocol)** 是用来传输网页的协议。可以把它理解成运输数据的卡车。但这辆卡车是**透明的**。里面装的东西所有人都能看见。在 HTTP 网站上输入的密码、填写的个人信息。在传输过程中可能被中间的任何人偷看到。
|
||||
|
||||
**HTTPS(HTTP Secure)** 是给这辆卡车加了一个**密封的集装箱**。还配了一把钥匙。只有发送方和接收方有钥匙。中间的人就算截获了也看不懂里面是什么东西。这就是加密传输。
|
||||
|
||||
<DeploymentHttpsDemo />
|
||||
|
||||
### 6.2 为什么要 HTTPS?
|
||||
|
||||
第一个原因:**安全**。没有 HTTPS,用户在网站上输入的密码是明文传输的。但凡有点技术的人都能截获。这年头,谁敢用没有 HTTPS 的网站?
|
||||
|
||||
第二个原因:**浏览器警告**。现在 Chrome、Edge 这些主流浏览器都会对没有 HTTPS 的网站显示"不安全"的警告。用户一看 warning 图标。跑了都来不及。更别说注册、充值了。
|
||||
|
||||
第三个原因:**SEO**。Google、百度这些搜索引擎都会优先收录 HTTPS 的网站。SEO 效果会更好。
|
||||
|
||||
### 6.3 怎么获取 HTTPS 证书?
|
||||
|
||||
以前 HTTPS 证书很贵。每年要花几百甚至几千块钱。现在好了。出了一个叫 **Let's Encrypt** 的组织。提供完全免费的 SSL/TLS 证书。而且社区有很多自动化工具帮你安装和续期。
|
||||
|
||||
**方式一:使用 Certbot(推荐)**
|
||||
|
||||
Certbot 是一个自动申请和配置 Let's Encrypt 证书的工具。非常简单:
|
||||
|
||||
```bash
|
||||
# 安装 Certbot
|
||||
sudo apt install -y certbot python3-certbot-nginx
|
||||
|
||||
# 一键申请证书并配置 Nginx
|
||||
sudo certbot --nginx -d example.com -d www.example.com
|
||||
```
|
||||
|
||||
运行过程中会问几个问题。比如邮箱(用于证书到期提醒)。回答完后证书就自动配置好了。访问网站会发现地址栏多了一个小锁🔒。
|
||||
|
||||
证书有效期是 90 天。但 Certbot 会帮你设置定时任务自动续期。基本不用管它。
|
||||
|
||||
**方式二:使用 Cloudflare**
|
||||
|
||||
如果使用了 Cloudflare 的 DNS 服务。那 HTTPS 证书根本不用自己配置。Cloudflare 会自动为域名提供 HTTPS 支持。而且连 90 天续期的问题都帮你解决了。
|
||||
|
||||
### 6.4 配置 HTTPS 后发生了什么变化?
|
||||
|
||||
配置好 HTTPS 后,用户访问从原来的 `http://example.com` 变成了 `https://example.com`。这个变化带来了一系列的安全保障:
|
||||
|
||||
1. **加密传输**:用户和服务器之间的所有通信都是加密的。
|
||||
2. **身份验证**:证书可以证明"我真的是这个网站"。防止钓鱼网站。
|
||||
3. **数据完整性**:能检测到数据是否被篡改。
|
||||
|
||||
---
|
||||
|
||||
## 7. CI/CD:让机器人帮你干活
|
||||
|
||||
### 7.1 什么是 CI/CD?
|
||||
|
||||
CI/CD 是两个词的缩写:**C**ontinuous **I**ntegration(持续集成)和 **C**ontinuous **D**eployment(持续部署)。可以理解为一套帮你自动干活的机器人系统。
|
||||
|
||||
在没有 CI/CD 的时候。每次要发布新功能。流程是这样的:
|
||||
|
||||
1. 打开电脑,登录 GitHub
|
||||
2. 拉取最新代码
|
||||
3. 运行测试,看看有没有bug
|
||||
4. 手动构建项目
|
||||
5. 登录服务器
|
||||
6. 拉取最新代码
|
||||
7. 安装依赖
|
||||
8. 构建项目
|
||||
9. 重启服务
|
||||
|
||||
这9个步骤。每次发布都要手动做一遍。烦不烦?而且很容易漏掉某一步。比如忘记运行测试、忘记重启服务等。
|
||||
|
||||
有了 CI/CD 之后。流程变成了这样:
|
||||
|
||||
1. 把代码 push 到 GitHub
|
||||
2. 喝茶坐等
|
||||
3. (机器人自动完成上面9个步骤)
|
||||
4. 网站自动更新了
|
||||
|
||||
<DeploymentCicdDemo />
|
||||
|
||||
这就是 CI/CD 的魅力:**只需要把代码推上去。剩下的全部自动完成**。
|
||||
|
||||
### 7.2 CI/CD 的工作流程
|
||||
|
||||
一个典型的 CI/CD 流程是这样的:
|
||||
|
||||
**第一步:代码提交(Push)**
|
||||
|
||||
完成了新功能的开发。把代码 push 到 GitHub。
|
||||
|
||||
**第二步:CI(持续集成)触发**
|
||||
|
||||
GitHub 检测到代码变动。通知 CI 系统(GitHub Actions、GitLab CI 等)开始工作。
|
||||
|
||||
**第三步:安装依赖和测试**
|
||||
|
||||
CI 系统会启动一台虚拟电脑。在上面:
|
||||
- 安装项目需要的各种依赖
|
||||
- 运行测试代码,确保没有 bug
|
||||
- 构建项目,生成产物
|
||||
|
||||
如果测试失败。CI 会发邮件通知。这次部署就停了。不会把有问题的代码部署到生产环境。
|
||||
|
||||
**第四步:CD(持续部署)执行**
|
||||
|
||||
测试全部通过后。CI 系统会:
|
||||
- 通过 SSH 连接到服务器
|
||||
- 拉取最新代码
|
||||
- 安装依赖
|
||||
- 构建项目
|
||||
- 重启服务
|
||||
|
||||
整个过程可能只需要几分钟。全部自动完成。
|
||||
|
||||
### 7.3 怎么配置 GitHub Actions?
|
||||
|
||||
GitHub Actions 是 GitHub 自带的 CI/CD 功能。不需要额外付费(免费额度足够个人项目用)。配置起来也非常简单。
|
||||
|
||||
在项目根目录下创建 `.github/workflows/deploy.yml` 文件。写入以下配置:
|
||||
|
||||
```yaml
|
||||
name: Deploy to Production
|
||||
|
||||
# 触发条件:每当 main 分支有代码推送时
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
# 任务列表
|
||||
jobs:
|
||||
# 部署任务
|
||||
deploy:
|
||||
# 在什么系统上运行
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# 具体步骤
|
||||
steps:
|
||||
# 1. 检出代码
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# 2. 安装 Node.js 环境
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
# 3. 安装依赖并构建
|
||||
- name: Install and Build
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
# 4. 部署到服务器
|
||||
- name: Deploy to Server
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
cd /var/www/my-website
|
||||
git pull origin main
|
||||
npm install
|
||||
npm run build
|
||||
pm2 restart all
|
||||
```
|
||||
|
||||
这个配置文件告诉 GitHub Actions:
|
||||
|
||||
- 当 main 分支有新代码时触发
|
||||
- 在一台 Ubuntu 电脑上执行任务
|
||||
- 先安装 Node.js 18
|
||||
- 然后安装依赖并构建项目
|
||||
- 最后通过 SSH 连接到服务器,执行一系列部署命令
|
||||
|
||||
配置好之后。每次 `git push origin main`。GitHub 就会自动开始部署。非常方便。
|
||||
|
||||
---
|
||||
|
||||
## 8. 监控和日志:做网站的"守夜人"
|
||||
|
||||
### 8.1 为什么要监控?
|
||||
|
||||
网站上线后。理论上应该 7×24 小时不间断运行。但现实世界没有这么美好。服务器可能会宕机。网络可能会抖动。代码可能会有bug。在真实的生产环境中。各种意外情况都有可能发生。
|
||||
|
||||
如果没有监控。就只能等用户打电话告诉你"网站打不开了"。这时候往往已经晚了。用户可能已经流失了。
|
||||
|
||||
有了监控之后。可以:
|
||||
|
||||
- **提前发现问题**:CPU 使用率 90% 了。提前加服务器。
|
||||
- **快速定位问题**:网站慢了。查监控看是哪里瓶颈。
|
||||
- **心里有底**:每天多少人访问、访问量什么时候最高。
|
||||
|
||||
<DeploymentMonitorDemo />
|
||||
|
||||
### 8.2 监控哪些指标?
|
||||
|
||||
最重要的监控指标就这几个:
|
||||
|
||||
| 指标 | 正常范围 | 超过怎么办 |
|
||||
|------|---------|-----------|
|
||||
| CPU 使用率 | < 70% | 升级服务器配置或优化代码 |
|
||||
| 内存使用率 | < 80% | 检查是否有内存泄漏 |
|
||||
| 磁盘使用率 | < 80% | 清理日志或无用文件 |
|
||||
| 网站可达性 | 100% | 检查服务是否正常运行 |
|
||||
| 响应时间 | < 2 秒 | 优化数据库查询或加缓存 |
|
||||
| 错误率 | < 1% | 查看错误日志定位问题 |
|
||||
|
||||
### 8.3 怎么配置监控?
|
||||
|
||||
**最简单的方案:Uptime Robot**
|
||||
|
||||
注册 uptimerobot.com。添加网站URL。它会每 5 分钟自动检查一次网站是否正常。网站挂了会发邮件通知你。免费版本可以监控 50 个网站。对个人项目来说完全够用。
|
||||
|
||||
**进阶方案:阿里云/腾讯云监控**
|
||||
|
||||
如果服务器是在阿里云或腾讯云买的。它们自带监控功能。配置一下阈值报警就行。
|
||||
|
||||
**专业方案:Prometheus + Grafana**
|
||||
|
||||
这两个是监控领域的"瑞士军刀"。功能非常强大。可以监控任何能想到的指标。还能做出漂亮的可视化图表。不过配置起来比较复杂。适合有一定经验的开发者。
|
||||
|
||||
### 8.4 日志:出了问题怎么查?
|
||||
|
||||
监控告诉你"网站出问题了"。但具体是什么问题、为什么出问题。需要靠**日志**来定位。
|
||||
|
||||
日志就是程序运行时的"日记本"。记录了程序运行过程中的点点滴滴:
|
||||
|
||||
- 哪个用户在什么时候访问了什么页面
|
||||
- 数据库查询花了多长时间
|
||||
- 有没有报错,错误信息是什么
|
||||
|
||||
**最基础的日志用法**
|
||||
|
||||
在服务器上查看应用日志:
|
||||
|
||||
```bash
|
||||
# 查看 PM2 的日志
|
||||
pm2 logs
|
||||
|
||||
# 查看 Nginx 的访问日志
|
||||
tail -f /var/log/nginx/access.log
|
||||
|
||||
# 查看 Nginx 的错误日志
|
||||
tail -f /var/log/nginx/error.log
|
||||
```
|
||||
|
||||
**进阶的日志方案**
|
||||
|
||||
如果项目比较复杂。推荐使用专业的日志收集工具:
|
||||
|
||||
- **Loki**:免费开源。和 Prometheus 一家的。
|
||||
- **ELK(Elasticsearch + Logstash + Kibana)**:功能强大。但配置复杂。
|
||||
- **Sentry**:专门用于收集应用错误的工具。能自动收集报错信息。
|
||||
|
||||
### 8.5 告警:出问题怎么第一时间知道?
|
||||
|
||||
监控告诉你有问题。但如果没有盯着监控面板看,怎么办?这就需要**告警**了。
|
||||
|
||||
告警就是当监控系统检测到异常时。自动通过短信、微信、钉钉、邮件等方式通知你。可以设置不同的告警级别:
|
||||
|
||||
- **紧急(网站完全挂掉)**:发短信+打电话。必须马上知道。
|
||||
- **严重(错误率飙升)**:发钉钉/微信消息。看到就处理。
|
||||
- **一般(CPU 偏高)**:发邮件汇总。一天看一次就行。
|
||||
|
||||
告警配置的核心原则是:**分级告警,别把自己烦死**。如果什么鸡毛蒜皮的小事都给你发短信。用不了多久你就会把告警关掉。
|
||||
|
||||
---
|
||||
|
||||
## 9. 常见问题速查表
|
||||
|
||||
| 问题现象 | 可能原因 | 解决方法 |
|
||||
|---------|---------|---------|
|
||||
| 网站打不开 | 域名没解析 / 服务器挂了 / Nginx 没启动 | `ping 域名` 看通不通;`pm2 list` 看服务状态;`systemctl status nginx` 看 Nginx |
|
||||
| 打开是空白页面 | 构建产物路径不对 / 静态文件没正确配置 | 检查 Nginx 的 root 路径是否指向 dist 目录 |
|
||||
| 404 页面找不到 | 路由没正确配置 / 路径拼写错误 | Nginx 配置里加上 `try_files $uri $uri/ /index.html` |
|
||||
| 502 Bad Gateway | 后端服务挂了 / 端口没开 | `pm2 list` 看进程是否在运行;检查端口是否正确 |
|
||||
| 403 Forbidden | 权限不对 / 索引目录没开 | 检查文件权限 `chmod -R 755`;Nginx 配置加上 `autoindex on` |
|
||||
| HTTPS 证书过期 | 证书到期没续期 | `certbot renew` 手动续期;检查自动续期定时任务 |
|
||||
| 更新后看不到变化 | 浏览器缓存 / CDN 缓存 | Ctrl+Shift+R 强制刷新;去 CDN 控制台"刷新缓存" |
|
||||
| 网站打开很慢 | 带宽不够 / 没开缓存 / 没配置 CDN | 升级服务器带宽;配置 Redis 缓存;接入 CDN |
|
||||
| 数据库连不上 | 数据库没启动 / 密码错了 / 权限问题 | 检查数据库服务状态;核对配置里的连接信息 |
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
服务上线是一个系统性的大工程。涉及从代码构建到服务器部署、从网络配置到安全防护、从监控告警到日志分析的方方面面。对于初学者来说。不需要一开始就追求完美。先把最小可用版本(MVP)跑起来。然后在此基础上逐步完善。
|
||||
|
||||
整个流程的核心要点可以归纳为以下几点:
|
||||
|
||||
### 核心流程
|
||||
|
||||
1. **构建** → 用 `npm run build` 把代码变成浏览器能看懂的 HTML/CSS/JS
|
||||
2. **部署** → 把构建产物上传到服务器。用 Nginx 配置反向代理。
|
||||
3. **域名** → 购买域名并配置 DNS 解析到服务器 IP
|
||||
4. **HTTPS** → 用 Let's Encrypt 申请免费证书。保护数据传输安全。
|
||||
5. **CI/CD** → 配置自动化部署。代码 push 后自动上线。
|
||||
6. **监控** → 配置监控和告警。出问题第一时间知道。
|
||||
|
||||
### 学习路线建议
|
||||
|
||||
- **第1天**:用 Vercel/Netlify 部署一个静态网页。体验一下"代码变成网站"的感觉。
|
||||
- **第1周**:租一台云服务器。手动部署一个 Node.js 项目。配置域名和 HTTPS。
|
||||
- **第2-4周**:配置完整的 CI/CD 流程。建立监控和告警体系。
|
||||
- **持续学习**:学习 Docker 容器化、学习 Kubernetes 集群、学习微服务架构。
|
||||
|
||||
---
|
||||
|
||||
## 名词速查表
|
||||
|
||||
| 名词 | 英文 | 用人话解释 |
|
||||
|------|------|-----------|
|
||||
| 构建 | Build | 把源代码翻译打包成浏览器能执行的格式 |
|
||||
| 部署 | Deploy | 把代码放到服务器上让用户能访问 |
|
||||
| 服务器 | Server | 7×24小时不关机、联网的电脑 |
|
||||
| 域名 | Domain | 网站的好记名字(如 baidu.com) |
|
||||
| DNS | Domain Name System | 把域名翻译成 IP 地址的"电话簿" |
|
||||
| HTTP | HyperText Transfer Protocol | 网页传输协议(不安全,明文传输) |
|
||||
| HTTPS | HTTP Secure | 加密传输的网页协议(安全) |
|
||||
| Nginx | Engine X | 高性能 Web 服务器。做反向代理的。 |
|
||||
| 反向代理 | Reverse Proxy | 站在门口的服务员。把请求转发给后端。 |
|
||||
| SSH | Secure Shell | 远程登录服务器的加密工具 |
|
||||
| CDN | Content Delivery Network | 全球分布的服务器网络。加快访问速度。 |
|
||||
| CI/CD | Continuous Integration/Deployment | 自动化流水线。代码 push 后自动测试部署。 |
|
||||
| SSL/TLS | Secure Sockets Layer / Transport Layer Security | 加密协议。给 HTTPS 提供安全保障。 |
|
||||
| PM2 | Process Manager 2 | Node.js 进程管理器。让应用持续运行。 |
|
||||
@@ -1,5 +1,4 @@
|
||||
# 云账号与权限管理模型:IAM / RAM 角色与权限关系
|
||||
|
||||
# 云身份与权限管理
|
||||
> **学习指南**:提示词工程解决的是"怎么把话说清楚",云账号权限管理解决的是"谁能做什么事"。本章节会围绕一个问题展开:**在云端世界里,如何既能方便地授权,又不把钥匙交给不该给的人?**
|
||||
|
||||
在开始之前,建议你先补两块"基础砖":
|
||||
@@ -36,12 +35,12 @@
|
||||
|
||||
想象一下,你们公司搬到了一栋新写字楼:
|
||||
|
||||
| 场景 | 没有 IAM 的做法 | 有 IAM 的做法 |
|
||||
| :--- | :--- | :--- |
|
||||
| 新员工入职 | 给他一把能开所有门的万能钥匙 | 给他一张门禁卡,只能刷他办公区域的门 |
|
||||
| 员工离职 | 钥匙丢了就丢了,也不知道谁拿着 | 立即在系统里注销他的门禁卡,所有门都打不开了 |
|
||||
| 外包人员 | 把钥匙借给他几天 | 发临时门禁卡,设置3天后自动失效 |
|
||||
| 访客 | 前台配一把钥匙给他 | 发一次性访客码,只能进会议室 |
|
||||
| 场景 | 没有 IAM 的做法 | 有 IAM 的做法 |
|
||||
| :--------- | :----------------------------- | :------------------------------------------- |
|
||||
| 新员工入职 | 给他一把能开所有门的万能钥匙 | 给他一张门禁卡,只能刷他办公区域的门 |
|
||||
| 员工离职 | 钥匙丢了就丢了,也不知道谁拿着 | 立即在系统里注销他的门禁卡,所有门都打不开了 |
|
||||
| 外包人员 | 把钥匙借给他几天 | 发临时门禁卡,设置3天后自动失效 |
|
||||
| 访客 | 前台配一把钥匙给他 | 发一次性访客码,只能进会议室 |
|
||||
|
||||
**IAM(Identity and Access Management,身份与访问管理)**,就像是这套"智能门禁系统":
|
||||
|
||||
@@ -55,13 +54,13 @@
|
||||
|
||||
不同的云厂商都有自己的 IAM 实现:
|
||||
|
||||
| 云厂商 | 服务名称 | 核心概念 |
|
||||
| :--- | :--- | :--- |
|
||||
| **AWS** | IAM (Identity and Access Management) | User、Group、Role、Policy |
|
||||
| **阿里云** | RAM (Resource Access Management) | 用户、用户组、角色、策略 |
|
||||
| **腾讯云** | CAM (Cloud Access Management) | 用户、用户组、角色、策略 |
|
||||
| **华为云** | IAM | 用户、用户组、委托、策略 |
|
||||
| **Azure** | Azure AD + RBAC | User、Group、Role、RBAC |
|
||||
| 云厂商 | 服务名称 | 核心概念 |
|
||||
| :--------- | :----------------------------------- | :------------------------ |
|
||||
| **AWS** | IAM (Identity and Access Management) | User、Group、Role、Policy |
|
||||
| **阿里云** | RAM (Resource Access Management) | 用户、用户组、角色、策略 |
|
||||
| **腾讯云** | CAM (Cloud Access Management) | 用户、用户组、角色、策略 |
|
||||
| **华为云** | IAM | 用户、用户组、委托、策略 |
|
||||
| **Azure** | Azure AD + RBAC | User、Group、Role、RBAC |
|
||||
|
||||
虽然名字不同,但**核心概念都是相通的**:
|
||||
|
||||
@@ -80,11 +79,11 @@
|
||||
|
||||
用一个办公室的场景来类比:
|
||||
|
||||
| 概念 | 类比 | 适用场景 | 特点 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **用户(User)** | 正式员工,有自己的工位和门禁卡 | 长期、稳定的团队成员 | 有永久凭证(密码、AK/SK) |
|
||||
| **用户组(Group)** | 部门,如"技术部"、"销售部" | 批量管理权限 | 不能登录,只是权限容器 |
|
||||
| **角色(Role)** | 临时访客证、外包临时卡 | 临时授权、跨账号访问 | 没有永久凭证,靠"扮演"获取临时凭证 |
|
||||
| 概念 | 类比 | 适用场景 | 特点 |
|
||||
| :------------------ | :----------------------------- | :------------------- | :--------------------------------- |
|
||||
| **用户(User)** | 正式员工,有自己的工位和门禁卡 | 长期、稳定的团队成员 | 有永久凭证(密码、AK/SK) |
|
||||
| **用户组(Group)** | 部门,如"技术部"、"销售部" | 批量管理权限 | 不能登录,只是权限容器 |
|
||||
| **角色(Role)** | 临时访客证、外包临时卡 | 临时授权、跨账号访问 | 没有永久凭证,靠"扮演"获取临时凭证 |
|
||||
|
||||
### 2.2 真实案例:一个创业公司的权限演进
|
||||
|
||||
@@ -151,13 +150,13 @@ IAM Role 有两个核心组成部分:
|
||||
|
||||
用一个话剧表演的类比:
|
||||
|
||||
| 概念 | 类比 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Role(角色)** | 剧本里的"哈姆雷特" | 定义了要演什么戏(权限)|
|
||||
| **Trust Policy** | 导演说"谁能演哈姆雷特" | 可能是"本剧团的演员"(本账号用户)、"隔壁剧团借来的演员"(跨账号)、"特邀嘉宾"(外部 IdP)|
|
||||
| **Permission Policy** | 剧本内容 | 哈姆雷特能做什么:说台词、决斗、发疯(具体权限)|
|
||||
| **Assume Role** | 演员上台表演 | 小李被导演选中演哈姆雷特,上台后他就拥有了剧本里定义的所有权限 |
|
||||
| **临时凭证** | 演出证 | 小李拿到一个"临时演出证",演出结束后就失效了 |
|
||||
| 概念 | 类比 | 说明 |
|
||||
| :-------------------- | :--------------------- | :----------------------------------------------------------------------------------------- |
|
||||
| **Role(角色)** | 剧本里的"哈姆雷特" | 定义了要演什么戏(权限) |
|
||||
| **Trust Policy** | 导演说"谁能演哈姆雷特" | 可能是"本剧团的演员"(本账号用户)、"隔壁剧团借来的演员"(跨账号)、"特邀嘉宾"(外部 IdP) |
|
||||
| **Permission Policy** | 剧本内容 | 哈姆雷特能做什么:说台词、决斗、发疯(具体权限) |
|
||||
| **Assume Role** | 演员上台表演 | 小李被导演选中演哈姆雷特,上台后他就拥有了剧本里定义的所有权限 |
|
||||
| **临时凭证** | 演出证 | 小李拿到一个"临时演出证",演出结束后就失效了 |
|
||||
|
||||
### 3.2 策略(Policy):权限的"语法"
|
||||
|
||||
@@ -174,11 +173,7 @@ IAM Policy 是一个 JSON 文档,定义了"谁能对什么资源做什么操
|
||||
{
|
||||
"Sid": "AllowS3ReadWrite",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:GetObject",
|
||||
"s3:PutObject",
|
||||
"s3:DeleteObject"
|
||||
],
|
||||
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
|
||||
"Resource": "arn:aws:s3:::my-app-bucket/*",
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
@@ -201,15 +196,15 @@ IAM Policy 是一个 JSON 文档,定义了"谁能对什么资源做什么操
|
||||
|
||||
**关键字段解释**:
|
||||
|
||||
| 字段 | 含义 | 示例 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Version** | Policy 语法版本 | "2012-10-17" |
|
||||
| **Statement** | 权限声明数组,可包含多个规则 | [...] |
|
||||
| **Sid** | 声明 ID,可选,用于标识这条规则 | "AllowS3ReadWrite" |
|
||||
| **Effect** | 效果:Allow(允许)或 Deny(拒绝) | "Allow" |
|
||||
| **Action** | 允许/拒绝的操作,支持通配符 | "s3:GetObject", "s3:*" |
|
||||
| **Resource** | 作用的资源,用 ARN 标识 | "arn:aws:s3:::bucket/*" |
|
||||
| **Condition** | 可选,满足特定条件时才生效 | 区域限制、MFA 要求等 |
|
||||
| 字段 | 含义 | 示例 |
|
||||
| :------------ | :--------------------------------- | :----------------------- |
|
||||
| **Version** | Policy 语法版本 | "2012-10-17" |
|
||||
| **Statement** | 权限声明数组,可包含多个规则 | [...] |
|
||||
| **Sid** | 声明 ID,可选,用于标识这条规则 | "AllowS3ReadWrite" |
|
||||
| **Effect** | 效果:Allow(允许)或 Deny(拒绝) | "Allow" |
|
||||
| **Action** | 允许/拒绝的操作,支持通配符 | "s3:GetObject", "s3:\*" |
|
||||
| **Resource** | 作用的资源,用 ARN 标识 | "arn:aws:s3:::bucket/\*" |
|
||||
| **Condition** | 可选,满足特定条件时才生效 | 区域限制、MFA 要求等 |
|
||||
|
||||
### 3.3 权限的优先级:Deny > Allow > 默认拒绝
|
||||
|
||||
@@ -246,6 +241,7 @@ IAM 的权限评估逻辑可以用一句话总结:**显式 Deny 永远赢,
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
|
||||
- 开发者虽然有 `s3:*` 的 Allow 权限
|
||||
- 但敏感目录有显式的 Deny 规则
|
||||
- Deny 优先级更高,所以开发者无法访问敏感数据
|
||||
@@ -261,10 +257,10 @@ IAM 的权限评估逻辑可以用一句话总结:**显式 Deny 永远赢,
|
||||
|
||||
Access Key(访问密钥)是云服务提供的一种长期凭证,用于程序化的 API 调用。它由两部分组成:
|
||||
|
||||
| 组成部分 | 名称 | 作用 | 类比 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Access Key ID** | 访问密钥 ID | 标识你是谁(类似于用户名) | 银行卡号 |
|
||||
| **Secret Access Key** | 秘密访问密钥 | 证明你是你(类似于密码) | 银行卡密码 |
|
||||
| 组成部分 | 名称 | 作用 | 类比 |
|
||||
| :-------------------- | :----------- | :------------------------- | :--------- |
|
||||
| **Access Key ID** | 访问密钥 ID | 标识你是谁(类似于用户名) | 银行卡号 |
|
||||
| **Secret Access Key** | 秘密访问密钥 | 证明你是你(类似于密码) | 银行卡密码 |
|
||||
|
||||
### 4.2 为什么 AK/SK 是"高危物品"?
|
||||
|
||||
@@ -302,12 +298,12 @@ upload_file('./test.jpg', 'my-company-bucket', 'uploads/test.jpg')
|
||||
|
||||
**这个案例告诉我们什么?**
|
||||
|
||||
| 错误做法 | 正确做法 |
|
||||
| :--- | :--- |
|
||||
| 把 AK/SK 硬编码在代码中 | 使用 IAM Role,让程序自动获取临时凭证 |
|
||||
| 把 AK/SK 提交到 Git 仓库 | 使用 `.gitignore` 忽略配置文件,使用密钥管理服务 |
|
||||
| 长期使用同一个 AK/SK 不轮换 | 定期轮换 AK/SK,使用临时凭证替代长期凭证 |
|
||||
| 给 AK/SK 分配过大权限 | 遵循最小权限原则,只授予必要的权限 |
|
||||
| 错误做法 | 正确做法 |
|
||||
| :-------------------------- | :----------------------------------------------- |
|
||||
| 把 AK/SK 硬编码在代码中 | 使用 IAM Role,让程序自动获取临时凭证 |
|
||||
| 把 AK/SK 提交到 Git 仓库 | 使用 `.gitignore` 忽略配置文件,使用密钥管理服务 |
|
||||
| 长期使用同一个 AK/SK 不轮换 | 定期轮换 AK/SK,使用临时凭证替代长期凭证 |
|
||||
| 给 AK/SK 分配过大权限 | 遵循最小权限原则,只授予必要的权限 |
|
||||
|
||||
### 4.3 AK/SK 的安全使用指南
|
||||
|
||||
@@ -356,7 +352,7 @@ jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # 关键:允许请求 OIDC token
|
||||
id-token: write # 关键:允许请求 OIDC token
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -374,13 +370,13 @@ jobs:
|
||||
|
||||
**总结:AK/SK 使用的安全层级**
|
||||
|
||||
| 安全等级 | 做法 | 适用场景 | 风险等级 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 最高 | 使用 IAM Role(无长期凭证) | EC2、Lambda、ECS、CI/CD | 极低 |
|
||||
| 高 | 使用 OIDC Federation | GitHub Actions、GitLab CI | 低 |
|
||||
| 中 | 使用密钥管理服务 | 本地开发、小团队 | 中 |
|
||||
| 低 | 使用环境变量 | 快速原型、个人项目 | 高 |
|
||||
| 极低 | 硬编码在代码中 | 任何场景都不推荐 | 极高 |
|
||||
| 安全等级 | 做法 | 适用场景 | 风险等级 |
|
||||
| :------- | :-------------------------- | :------------------------ | :------- |
|
||||
| 最高 | 使用 IAM Role(无长期凭证) | EC2、Lambda、ECS、CI/CD | 极低 |
|
||||
| 高 | 使用 OIDC Federation | GitHub Actions、GitLab CI | 低 |
|
||||
| 中 | 使用密钥管理服务 | 本地开发、小团队 | 中 |
|
||||
| 低 | 使用环境变量 | 快速原型、个人项目 | 高 |
|
||||
| 极低 | 硬编码在代码中 | 任何场景都不推荐 | 极高 |
|
||||
|
||||
---
|
||||
|
||||
@@ -392,21 +388,21 @@ jobs:
|
||||
|
||||
MFA(Multi-Factor Authentication,多因素认证),也叫 2FA(Two-Factor Authentication,双因素认证),是一种安全机制,要求用户在登录时提供**两种或以上**不同类型的认证因素:
|
||||
|
||||
| 因素类型 | 是什么 | 例子 |
|
||||
| :--- | :--- | :--- |
|
||||
| **知识因素**(你知道什么) | 只有用户知道的信息 | 密码、PIN 码 |
|
||||
| **持有因素**(你有什么) | 用户拥有的物理设备 | 手机、硬件密钥 |
|
||||
| **生物因素**(你是什么) | 用户的生物特征 | 指纹、面部识别 |
|
||||
| 因素类型 | 是什么 | 例子 |
|
||||
| :------------------------- | :----------------- | :------------- |
|
||||
| **知识因素**(你知道什么) | 只有用户知道的信息 | 密码、PIN 码 |
|
||||
| **持有因素**(你有什么) | 用户拥有的物理设备 | 手机、硬件密钥 |
|
||||
| **生物因素**(你是什么) | 用户的生物特征 | 指纹、面部识别 |
|
||||
|
||||
### 5.2 为什么 MFA 这么重要?
|
||||
|
||||
**真实数据告诉你答案**:
|
||||
|
||||
| 攻击方式 | 没有 MFA 时的成功率 | 有 MFA 时的成功率 |
|
||||
| :--- | :--- | :--- |
|
||||
| 密码猜测/暴力破解 | 很高 | 极低(还需要第二因素) |
|
||||
| 钓鱼攻击获取密码 | 很高 | 极低(钓鱼页面无法获取 MFA 码) |
|
||||
| 密码泄露(其他网站泄露)| 很高 | 极低(不知道第二因素) |
|
||||
| 攻击方式 | 没有 MFA 时的成功率 | 有 MFA 时的成功率 |
|
||||
| :----------------------- | :------------------ | :------------------------------ |
|
||||
| 密码猜测/暴力破解 | 很高 | 极低(还需要第二因素) |
|
||||
| 钓鱼攻击获取密码 | 很高 | 极低(钓鱼页面无法获取 MFA 码) |
|
||||
| 密码泄露(其他网站泄露) | 很高 | 极低(不知道第二因素) |
|
||||
|
||||
**微软安全报告(2020)**:启用 MFA 可以阻止 **99.9%** 的自动化攻击。
|
||||
|
||||
@@ -441,14 +437,14 @@ MFA(Multi-Factor Authentication,多因素认证),也叫 2FA(Two-Factor
|
||||
|
||||
随着业务增长,很多公司会使用**多账号架构**来隔离不同环境:
|
||||
|
||||
| 账号类型 | 用途 | 权限要求 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Master Account** | 组织管理、账单结算 | 几乎不使用 |
|
||||
| **Security Audit** | 集中收集所有账号的日志 | 只读访问其他账号 |
|
||||
| **Shared Services** | 共享资源(镜像仓库等) | 其他账号只读访问 |
|
||||
| **Development** | 开发环境 | 开发者完全权限 |
|
||||
| **Staging** | 测试/预发布环境 | 测试人员权限 |
|
||||
| **Production** | 生产环境 | 严格限制,需要审批 |
|
||||
| 账号类型 | 用途 | 权限要求 |
|
||||
| :------------------ | :--------------------- | :----------------- |
|
||||
| **Master Account** | 组织管理、账单结算 | 几乎不使用 |
|
||||
| **Security Audit** | 集中收集所有账号的日志 | 只读访问其他账号 |
|
||||
| **Shared Services** | 共享资源(镜像仓库等) | 其他账号只读访问 |
|
||||
| **Development** | 开发环境 | 开发者完全权限 |
|
||||
| **Staging** | 测试/预发布环境 | 测试人员权限 |
|
||||
| **Production** | 生产环境 | 严格限制,需要审批 |
|
||||
|
||||
**问题:Shared Services 账号里的镜像,怎么让 Production 账号的 EC2 拉取?**
|
||||
|
||||
@@ -498,6 +494,7 @@ MFA(Multi-Factor Authentication,多因素认证),也叫 2FA(Two-Factor
|
||||
**步骤二:获取 Role ARN**
|
||||
|
||||
创建完成后,复制 Role 的 ARN:
|
||||
|
||||
```
|
||||
arn:aws:iam::SHARED_SERVICES_ACCOUNT_ID:role/CrossAccountECRReadRole
|
||||
```
|
||||
@@ -691,38 +688,38 @@ aws ecr describe-repositories --registry-id SHARED_SERVICES_ACCOUNT_ID
|
||||
|
||||
### 8.1 十大 IAM 反模式
|
||||
|
||||
| # | 反模式 | 为什么不好 | 正确做法 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 1 | 使用根账号进行日常操作 | 根账号拥有所有权限,一旦泄露无法限制损害 | 创建 IAM 管理员账号,根账号仅在必要时使用 |
|
||||
| 2 | 给所有人 AdministratorAccess | 违反最小权限原则,增加误操作和内部威胁风险 | 按角色分组,只授予必要的权限 |
|
||||
| 3 | 在代码中硬编码 AK/SK | AK/SK 容易通过 GitHub 泄露,且难以轮换 | 使用 IAM Role、环境变量或密钥管理服务 |
|
||||
| 4 | 长期不轮换 AK/SK | 增加凭证泄露后的风险敞口时间 | 设置 90 天轮换策略,或更好的——使用临时凭证 |
|
||||
| 5 | 忽略 MFA | 密码泄露后账号直接沦陷 | 为所有 IAM 用户启用 MFA,尤其是高权限用户 |
|
||||
| 6 | 不使用 CloudTrail | 无法审计谁做了什么操作,出事后无法溯源 | 启用 CloudTrail,并将日志存储到独立的审计账号 |
|
||||
| 7 | IAM Policy 过于宽松 | 如 `Resource: "*"`、`Action: "*"`,增加攻击面 | 明确指定资源 ARN 和具体 Action |
|
||||
| 8 | 不清理离职员工的 IAM User | 僵尸账号可能成为后门 | 建立离职流程,立即禁用并删除 IAM User |
|
||||
| 9 | 不使用 IAM Access Analyzer | 无法发现过度宽松的资源策略(如公开 S3 bucket) | 启用 IAM Access Analyzer,定期检查外部访问 |
|
||||
| 10 | 不在测试环境验证 Policy | 直接在生产环境应用 Policy,可能导致服务中断 | 使用 IAM Policy Simulator 测试,先在测试环境验证 |
|
||||
| # | 反模式 | 为什么不好 | 正确做法 |
|
||||
| :-- | :--------------------------- | :--------------------------------------------- | :----------------------------------------------- |
|
||||
| 1 | 使用根账号进行日常操作 | 根账号拥有所有权限,一旦泄露无法限制损害 | 创建 IAM 管理员账号,根账号仅在必要时使用 |
|
||||
| 2 | 给所有人 AdministratorAccess | 违反最小权限原则,增加误操作和内部威胁风险 | 按角色分组,只授予必要的权限 |
|
||||
| 3 | 在代码中硬编码 AK/SK | AK/SK 容易通过 GitHub 泄露,且难以轮换 | 使用 IAM Role、环境变量或密钥管理服务 |
|
||||
| 4 | 长期不轮换 AK/SK | 增加凭证泄露后的风险敞口时间 | 设置 90 天轮换策略,或更好的——使用临时凭证 |
|
||||
| 5 | 忽略 MFA | 密码泄露后账号直接沦陷 | 为所有 IAM 用户启用 MFA,尤其是高权限用户 |
|
||||
| 6 | 不使用 CloudTrail | 无法审计谁做了什么操作,出事后无法溯源 | 启用 CloudTrail,并将日志存储到独立的审计账号 |
|
||||
| 7 | IAM Policy 过于宽松 | 如 `Resource: "*"`、`Action: "*"`,增加攻击面 | 明确指定资源 ARN 和具体 Action |
|
||||
| 8 | 不清理离职员工的 IAM User | 僵尸账号可能成为后门 | 建立离职流程,立即禁用并删除 IAM User |
|
||||
| 9 | 不使用 IAM Access Analyzer | 无法发现过度宽松的资源策略(如公开 S3 bucket) | 启用 IAM Access Analyzer,定期检查外部访问 |
|
||||
| 10 | 不在测试环境验证 Policy | 直接在生产环境应用 Policy,可能导致服务中断 | 使用 IAM Policy Simulator 测试,先在测试环境验证 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 名词对照表
|
||||
|
||||
| 英文术语 | 中文对照 | 解释 |
|
||||
| :--- | :--- | :--- |
|
||||
| **IAM (Identity and Access Management)** | 身份与访问管理 | 云服务中管理用户身份和访问权限的服务 |
|
||||
| **RAM (Resource Access Management)** | 资源访问管理 | 阿里云的 IAM 服务名称 |
|
||||
| **Root Account** | 根账号 | 注册云账号时创建的拥有者账号,拥有最高权限 |
|
||||
| **IAM User** | IAM 用户/子账号 | 由根账号创建的子身份,用于日常操作 |
|
||||
| **IAM Role** | IAM 角色 | 临时性权限载体,无长期凭证,需要被"扮演" |
|
||||
| **IAM Policy** | IAM 策略 | JSON 格式的权限规则定义 |
|
||||
| **ARN** | 亚马逊资源名称 | 全局唯一的资源标识符 |
|
||||
| **AK/SK** | 访问密钥/密钥 | 程序访问云 API 的凭证 |
|
||||
| **STS** | 安全令牌服务 | 提供临时安全凭证的服务 |
|
||||
| **MFA** | 多因素认证 | 需要两个或以上因素的认证方式 |
|
||||
| **SSO** | 单点登录 | 用户一次登录即可访问多个系统的认证方式 |
|
||||
| **ExternalId** | 外部 ID | 用于防止困惑代理攻击的安全标识符 |
|
||||
| **CloudTrail** | 云审计服务 | 记录云账号中所有 API 调用和操作的日志服务 |
|
||||
| 英文术语 | 中文对照 | 解释 |
|
||||
| :--------------------------------------- | :-------------- | :----------------------------------------- |
|
||||
| **IAM (Identity and Access Management)** | 身份与访问管理 | 云服务中管理用户身份和访问权限的服务 |
|
||||
| **RAM (Resource Access Management)** | 资源访问管理 | 阿里云的 IAM 服务名称 |
|
||||
| **Root Account** | 根账号 | 注册云账号时创建的拥有者账号,拥有最高权限 |
|
||||
| **IAM User** | IAM 用户/子账号 | 由根账号创建的子身份,用于日常操作 |
|
||||
| **IAM Role** | IAM 角色 | 临时性权限载体,无长期凭证,需要被"扮演" |
|
||||
| **IAM Policy** | IAM 策略 | JSON 格式的权限规则定义 |
|
||||
| **ARN** | 亚马逊资源名称 | 全局唯一的资源标识符 |
|
||||
| **AK/SK** | 访问密钥/密钥 | 程序访问云 API 的凭证 |
|
||||
| **STS** | 安全令牌服务 | 提供临时安全凭证的服务 |
|
||||
| **MFA** | 多因素认证 | 需要两个或以上因素的认证方式 |
|
||||
| **SSO** | 单点登录 | 用户一次登录即可访问多个系统的认证方式 |
|
||||
| **ExternalId** | 外部 ID | 用于防止困惑代理攻击的安全标识符 |
|
||||
| **CloudTrail** | 云审计服务 | 记录云账号中所有 API 调用和操作的日志服务 |
|
||||
|
||||
---
|
||||
|
||||
@@ -756,6 +753,7 @@ aws ecr describe-repositories --registry-id SHARED_SERVICES_ACCOUNT_ID
|
||||
---
|
||||
|
||||
> **延伸阅读**:
|
||||
>
|
||||
> - [AWS IAM 官方文档](https://docs.aws.amazon.com/iam/)
|
||||
> - [阿里云 RAM 官方文档](https://www.aliyun.com/product/ram)
|
||||
> - [AWS IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)
|
||||
@@ -1,5 +1,4 @@
|
||||
# 云服务厂商入门指南 (Cloud Services Guide)
|
||||
|
||||
# 云平台实战
|
||||
> **学习指南**:云服务厂商不是"买服务器的网站",而是"像水电公司一样提供计算能力的基础设施"。本章节会围绕一个核心问题展开:**从零开始,如何理解并使用云服务?** 我们会用真实场景、生动类比和实战步骤,帮你建立云服务的完整认知地图。
|
||||
|
||||
在开始之前,建议你先了解:
|
||||
@@ -1,5 +1,4 @@
|
||||
# 对象存储 + CDN 加速路径:从上传到用户访问
|
||||
|
||||
# 对象存储与 CDN
|
||||
> 💡 **学习指南**:本文会带你走完一条完整的链路——从文件上传到用户下载。你会看到对象存储如何像"智能仓库"一样管理海量文件,CDN 如何像"快递网点"一样把内容送到用户家门口,以及这中间有哪些"坑"等着你跳进去。建议先了解基础的 HTTP 请求和 DNS 解析原理。
|
||||
|
||||
在开始之前,建议你先补几块"基础砖":
|
||||
@@ -31,13 +30,13 @@
|
||||
|
||||
**核心区别一览**:
|
||||
|
||||
| 维度 | 传统文件系统 | 对象存储 |
|
||||
| :--- | :--- | :--- |
|
||||
| **组织方式** | 层级目录树 | 扁平键值对 |
|
||||
| **访问协议** | POSIX(本地文件操作) | HTTP/REST API |
|
||||
| **扩展性** | 单机容量有限 | 近乎无限水平扩展 |
|
||||
| **元数据** | 基础属性(大小、时间) | 丰富的自定义元数据 |
|
||||
| **典型场景** | 本地办公文档 | 图片/视频/备份/静态资源 |
|
||||
| 维度 | 传统文件系统 | 对象存储 |
|
||||
| :----------- | :--------------------- | :---------------------- |
|
||||
| **组织方式** | 层级目录树 | 扁平键值对 |
|
||||
| **访问协议** | POSIX(本地文件操作) | HTTP/REST API |
|
||||
| **扩展性** | 单机容量有限 | 近乎无限水平扩展 |
|
||||
| **元数据** | 基础属性(大小、时间) | 丰富的自定义元数据 |
|
||||
| **典型场景** | 本地办公文档 | 图片/视频/备份/静态资源 |
|
||||
|
||||
### 1.2 对象存储的核心概念
|
||||
|
||||
@@ -46,6 +45,7 @@
|
||||
桶是对象存储的顶级容器,相当于一个独立的命名空间。所有对象都必须存放在某个桶中。
|
||||
|
||||
**命名规则**(以阿里云 OSS 为例):
|
||||
|
||||
- 全局唯一:在整个云厂商的所有用户中不能重复
|
||||
- 只能包含小写字母、数字和短横线
|
||||
- 必须以小写字母或数字开头和结尾
|
||||
@@ -73,11 +73,11 @@
|
||||
|
||||
对象存储提供多层权限控制:
|
||||
|
||||
| 层级 | 控制方式 | 典型场景 |
|
||||
| :--- | :--- | :--- |
|
||||
| **桶级别** | Bucket Policy(资源策略) | 禁止所有外网访问、只允许特定 IP |
|
||||
| **对象级别** | ACL(访问控制列表) | 公开图片、私有文档 |
|
||||
| **临时授权** | STS(安全令牌服务) | 前端直传、移动端上传 |
|
||||
| 层级 | 控制方式 | 典型场景 |
|
||||
| :----------- | :------------------------ | :------------------------------ |
|
||||
| **桶级别** | Bucket Policy(资源策略) | 禁止所有外网访问、只允许特定 IP |
|
||||
| **对象级别** | ACL(访问控制列表) | 公开图片、私有文档 |
|
||||
| **临时授权** | STS(安全令牌服务) | 前端直传、移动端上传 |
|
||||
|
||||
**安全红线**:永远不要把 AccessKey ID 和 AccessKey Secret 写在前端代码里!正确做法是:前端向你的后端申请临时 STS 凭证,后端验证身份后返回带过期时间的临时凭证。
|
||||
|
||||
@@ -102,11 +102,13 @@
|
||||
#### 边缘节点:离用户最近的"快递站"
|
||||
|
||||
边缘节点是 CDN 网络中最接近用户的层级,通常部署在:
|
||||
|
||||
- 运营商机房(联通/电信/移动)
|
||||
- 大城市互联网交换中心
|
||||
- 重要交通枢纽
|
||||
|
||||
**中国主要 CDN 节点分布**:
|
||||
|
||||
- 一线城市:北京、上海、广州、深圳
|
||||
- 二线城市:杭州、南京、成都、武汉、西安
|
||||
- 海外:香港、新加坡、东京、硅谷、法兰克福
|
||||
@@ -116,11 +118,13 @@
|
||||
#### 源站:内容的"总仓库"
|
||||
|
||||
源站是 CDN 回源获取内容的地方,可以是:
|
||||
|
||||
- 对象存储(OSS/COS/S3)
|
||||
- 自建服务器(ECS/物理机)
|
||||
- 负载均衡(SLB/CLB)
|
||||
|
||||
**关键配置**:
|
||||
|
||||
- **回源 HOST**:CDN 节点访问源站时使用的域名/IP
|
||||
- **回源协议**:HTTP 还是 HTTPS
|
||||
- **回源端口**:80、443 还是自定义端口
|
||||
@@ -128,10 +132,12 @@
|
||||
#### 中间层节点:"区域分拨中心"
|
||||
|
||||
在边缘节点和源站之间,CDN 通常还有一层或多层中间节点:
|
||||
|
||||
- **汇聚节点**:聚合多个边缘节点的回源请求,减少源站压力
|
||||
- **区域中心**:负责一个大区的内容分发和调度
|
||||
|
||||
这种分层架构的好处:
|
||||
|
||||
1. **降低源站压力**:1000 个边缘节点的请求,可能只需要向源站发起 10 次
|
||||
2. **提高命中率**:热门内容在中间层就被拦截,不需要回源
|
||||
3. **故障隔离**:某条链路出问题,可以自动切换到其他路径
|
||||
@@ -143,14 +149,17 @@
|
||||
<CachePolicyDemo />
|
||||
|
||||
**Step 1:DNS 解析**(智能调度)
|
||||
|
||||
```
|
||||
用户输入:cdn.example.com/image.jpg
|
||||
↓
|
||||
DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
||||
```
|
||||
|
||||
这里的关键是**智能 DNS**:根据用户的运营商、地理位置、节点负载,返回最优的 CDN 节点 IP。
|
||||
|
||||
**Step 2:边缘节点查找**(缓存命中?)
|
||||
|
||||
```
|
||||
请求到达北京联通 CDN 节点(1.2.3.4)
|
||||
↓
|
||||
@@ -160,6 +169,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
||||
```
|
||||
|
||||
**Step 3:回源获取**(层层向上)
|
||||
|
||||
```
|
||||
边缘节点未命中
|
||||
↓
|
||||
@@ -173,6 +183,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
||||
```
|
||||
|
||||
**Step 4:缓存并返回**(下次更快)
|
||||
|
||||
```
|
||||
内容沿链路返回
|
||||
↓
|
||||
@@ -198,17 +209,20 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
||||
```
|
||||
|
||||
**流程**:
|
||||
|
||||
1. 用户选择文件,点击上传
|
||||
2. 文件先上传到你的后端服务器
|
||||
3. 后端接收完整文件后,再转上传到对象存储
|
||||
4. 返回上传结果给用户
|
||||
|
||||
**优点**:
|
||||
|
||||
- 实现简单,前后端都好控制
|
||||
- 可以在后端做文件校验、格式转换
|
||||
- 敏感操作可以记录日志、做权限校验
|
||||
|
||||
**缺点**:
|
||||
|
||||
- **带宽双吃**:用户上传占用一次带宽,服务器转传又占用一次
|
||||
- **服务器压力大**:大文件会占用大量内存和 CPU
|
||||
- **上传慢**:相当于多了一道中转,用户感知到的上传时间更长
|
||||
@@ -224,6 +238,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
||||
```
|
||||
|
||||
**流程**:
|
||||
|
||||
1. 用户选择文件,前端先向后端申请"上传凭证"
|
||||
2. 后端验证用户身份,向对象存储服务申请**临时 STS 凭证**(带过期时间)
|
||||
3. 后端把临时凭证返回给前端
|
||||
@@ -231,12 +246,14 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
||||
5. 对象存储返回上传结果,前端通知后端"上传完成"
|
||||
|
||||
**优点**:
|
||||
|
||||
- **上传快**:少了中转环节,用户感知速度最快
|
||||
- **服务器压力小**:只处理凭证签发,不处理文件流
|
||||
- **带宽省**:只走一次上传流量
|
||||
- **安全性高**:临时凭证有过期时间,泄露也危害有限
|
||||
|
||||
**缺点**:
|
||||
|
||||
- 实现稍复杂,需要理解 STS、签名机制
|
||||
- 前端需要处理分片上传、断点续传等逻辑
|
||||
- 跨域(CORS)需要配置
|
||||
@@ -261,21 +278,21 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
||||
|
||||
**为什么需要分片?**
|
||||
|
||||
| 场景 | 不分片 | 分片 |
|
||||
| :--- | :--- | :--- |
|
||||
| **网络波动** | 传了 99% 断网,全部重传 | 只重传失败的分片 |
|
||||
| **上传速度** | 单线程,速度慢 | 多线程并行,速度快 |
|
||||
| **内存占用** | 需要缓存整个文件 | 只需缓存当前分片 |
|
||||
| **进度显示** | 只有 0% 和 100% | 精确到每个分片的进度 |
|
||||
| 场景 | 不分片 | 分片 |
|
||||
| :----------- | :---------------------- | :------------------- |
|
||||
| **网络波动** | 传了 99% 断网,全部重传 | 只重传失败的分片 |
|
||||
| **上传速度** | 单线程,速度慢 | 多线程并行,速度快 |
|
||||
| **内存占用** | 需要缓存整个文件 | 只需缓存当前分片 |
|
||||
| **进度显示** | 只有 0% 和 100% | 精确到每个分片的进度 |
|
||||
|
||||
**主流云厂商的分片规格**:
|
||||
|
||||
| 厂商 | 分片大小限制 | 最大分片数 | 最小分片大小 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **阿里云 OSS** | 100MB | 10000 | 100KB |
|
||||
| **腾讯云 COS** | 5GB | 10000 | 1MB |
|
||||
| **AWS S3** | 5GB | 10000 | 5MB(推荐) |
|
||||
| **七牛云** | 100MB | 10000 | 4MB |
|
||||
| 厂商 | 分片大小限制 | 最大分片数 | 最小分片大小 |
|
||||
| :------------- | :----------- | :--------- | :----------- |
|
||||
| **阿里云 OSS** | 100MB | 10000 | 100KB |
|
||||
| **腾讯云 COS** | 5GB | 10000 | 1MB |
|
||||
| **AWS S3** | 5GB | 10000 | 5MB(推荐) |
|
||||
| **七牛云** | 100MB | 10000 | 4MB |
|
||||
|
||||
### 3.2 CDN 回源策略详解
|
||||
|
||||
@@ -284,6 +301,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
||||
#### 什么是"回源"?
|
||||
|
||||
CDN 边缘节点缓存了源站的内容,但当:
|
||||
|
||||
- 用户请求的内容**第一次被访问**
|
||||
- 缓存的内容**已过期(TTL 到期)**
|
||||
- 缓存被**手动刷新/预热**
|
||||
@@ -292,11 +310,11 @@ CDN 节点就需要向**源站**请求最新内容,这个过程就叫"回源"
|
||||
|
||||
#### 回源的三种模式
|
||||
|
||||
| 模式 | 原理 | 适用场景 | 优缺点 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **直接回源** | CDN 节点 → 源站 | 源站有公网 IP,且流量不大 | 简单直接,但源站压力大 |
|
||||
| **中间源回源** | CDN 节点 → 中间层 → 源站 | 大型网站,多层缓存架构 | 分担源站压力,架构复杂 |
|
||||
| ** OSS/COS 作为源站** | CDN 节点 → 对象存储 | 静态资源、图片、视频 | 最佳实践,成本低、性能好 |
|
||||
| 模式 | 原理 | 适用场景 | 优缺点 |
|
||||
| :-------------------- | :----------------------- | :------------------------ | :----------------------- |
|
||||
| **直接回源** | CDN 节点 → 源站 | 源站有公网 IP,且流量不大 | 简单直接,但源站压力大 |
|
||||
| **中间源回源** | CDN 节点 → 中间层 → 源站 | 大型网站,多层缓存架构 | 分担源站压力,架构复杂 |
|
||||
| ** OSS/COS 作为源站** | CDN 节点 → 对象存储 | 静态资源、图片、视频 | 最佳实践,成本低、性能好 |
|
||||
|
||||
#### 回源配置实战
|
||||
|
||||
@@ -315,6 +333,7 @@ CDN 节点就需要向**源站**请求最新内容,这个过程就叫"回源"
|
||||
```
|
||||
|
||||
关键配置项:
|
||||
|
||||
- **源站类型**:OSS/COS 域名 或 自定义源站
|
||||
- **回源协议**:HTTP 还是 HTTPS(建议 HTTPS)
|
||||
- **回源 HOST**:访问源站时使用的 Host 头
|
||||
@@ -332,6 +351,7 @@ CDN 边缘节点
|
||||
```
|
||||
|
||||
主备模式:
|
||||
|
||||
```
|
||||
CDN 边缘节点
|
||||
├─ 主源站 A (健康时全部流量)
|
||||
@@ -342,12 +362,13 @@ CDN 边缘节点
|
||||
|
||||
这里有个容易混淆的概念:
|
||||
|
||||
| 指标 | 定义 | 计费关系 |
|
||||
| :--- | :--- | :--- |
|
||||
| **CDN 下行带宽** | 从 CDN 节点到用户的流量 | 通常按流量计费的 CDN 费用 |
|
||||
| **回源带宽** | 从源站到 CDN 节点的流量 | 通常对象存储或源站出流量费用 |
|
||||
| 指标 | 定义 | 计费关系 |
|
||||
| :--------------- | :---------------------- | :--------------------------- |
|
||||
| **CDN 下行带宽** | 从 CDN 节点到用户的流量 | 通常按流量计费的 CDN 费用 |
|
||||
| **回源带宽** | 从源站到 CDN 节点的流量 | 通常对象存储或源站出流量费用 |
|
||||
|
||||
**省钱技巧**:
|
||||
|
||||
- 提高 CDN 命中率(让更多请求命中缓存,减少回源)
|
||||
- 设置合理的缓存时间(TTL)
|
||||
- 使用预热功能,在用户访问前就缓存热点内容
|
||||
@@ -362,10 +383,12 @@ CDN 边缘节点
|
||||
CDN 如何判断两次请求是否应该返回同一个缓存副本?靠的就是**缓存键**。
|
||||
|
||||
**默认缓存键通常包括**:
|
||||
|
||||
- URL 路径(不含查询参数)
|
||||
- 例如:`/images/photo.jpg`
|
||||
|
||||
**问题场景**:
|
||||
|
||||
```
|
||||
用户 A 请求:/images/photo.jpg?w=100&h=100 (100x100 缩略图)
|
||||
用户 B 请求:/images/photo.jpg?w=800&h=600 (800x600 大图)
|
||||
@@ -375,14 +398,15 @@ CDN 如何判断两次请求是否应该返回同一个缓存副本?靠的就
|
||||
|
||||
**解决方案:自定义缓存键规则**
|
||||
|
||||
| 规则 | 示例 | 效果 |
|
||||
| :--- | :--- | :--- |
|
||||
| **保留指定查询参数** | 保留 `w`、`h` | 不同尺寸分别缓存 |
|
||||
| **保留所有查询参数** | 保留全部 | 完全精确匹配 |
|
||||
| 规则 | 示例 | 效果 |
|
||||
| :------------------- | :------------------------ | :------------------------ |
|
||||
| **保留指定查询参数** | 保留 `w`、`h` | 不同尺寸分别缓存 |
|
||||
| **保留所有查询参数** | 保留全部 | 完全精确匹配 |
|
||||
| **忽略特定查询参数** | 忽略 `token`、`timestamp` | 带时间戳的 URL 能命中缓存 |
|
||||
| **包含请求头** | 包含 `Accept-Language` | 不同语言返回不同内容 |
|
||||
| **包含请求头** | 包含 `Accept-Language` | 不同语言返回不同内容 |
|
||||
|
||||
**实战配置示例**(阿里云 CDN):
|
||||
|
||||
```
|
||||
缓存键规则:
|
||||
- URL 路径:/images/*
|
||||
@@ -396,13 +420,13 @@ TTL(Time To Live)决定了内容在 CDN 节点上缓存多久。设置太短
|
||||
|
||||
**按文件类型设置 TTL 的建议**:
|
||||
|
||||
| 文件类型 | 建议 TTL | 原因 |
|
||||
| :--- | :--- | :--- |
|
||||
| HTML 页面 | 0-5 分钟 | 内容频繁更新,需要实时 |
|
||||
| 文件类型 | 建议 TTL | 原因 |
|
||||
| :---------- | :---------------------- | :----------------------------- |
|
||||
| HTML 页面 | 0-5 分钟 | 内容频繁更新,需要实时 |
|
||||
| JS/CSS 文件 | 1 年(配合文件名 hash) | 内容不变,文件名变化即缓存失效 |
|
||||
| 图片/视频 | 7-30 天 | 更新频率低,可长期缓存 |
|
||||
| 字体文件 | 1 年 | 几乎不变 |
|
||||
| API 响应 | 0-5 分钟(视业务) | 数据实时性要求高 |
|
||||
| 图片/视频 | 7-30 天 | 更新频率低,可长期缓存 |
|
||||
| 字体文件 | 1 年 | 几乎不变 |
|
||||
| API 响应 | 0-5 分钟(视业务) | 数据实时性要求高 |
|
||||
|
||||
**前端工程化配合 CDN 的最佳实践**:
|
||||
|
||||
@@ -425,11 +449,11 @@ output: {
|
||||
|
||||
当你更新了源站内容,但 CDN 缓存还没过期,用户看到的还是旧内容:
|
||||
|
||||
| 刷新类型 | 效果 | 耗时 | 适用场景 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **URL 刷新** | 指定 URL 的缓存失效 | 5-10 分钟 | 单个文件更新 |
|
||||
| **目录刷新** | 指定目录下所有内容失效 | 10-30 分钟 | 批量更新 |
|
||||
| **全站刷新** | 整个域名的缓存全部失效 | 30 分钟以上 | 紧急回滚 |
|
||||
| 刷新类型 | 效果 | 耗时 | 适用场景 |
|
||||
| :----------- | :--------------------- | :---------- | :----------- |
|
||||
| **URL 刷新** | 指定 URL 的缓存失效 | 5-10 分钟 | 单个文件更新 |
|
||||
| **目录刷新** | 指定目录下所有内容失效 | 10-30 分钟 | 批量更新 |
|
||||
| **全站刷新** | 整个域名的缓存全部失效 | 30 分钟以上 | 紧急回滚 |
|
||||
|
||||
**重要提醒**:刷新只是让缓存失效,下次请求会回源拉取新内容。不要在高峰期大批量刷新,否则可能导致源站被打爆。
|
||||
|
||||
@@ -458,12 +482,14 @@ output: {
|
||||
### 4.1 智能 DNS 调度
|
||||
|
||||
传统 DNS 解析:
|
||||
|
||||
```
|
||||
用户问:cdn.example.com 的 IP 是什么?
|
||||
DNS 答:1.2.3.4(固定的)
|
||||
```
|
||||
|
||||
智能 DNS 解析:
|
||||
|
||||
```
|
||||
用户(北京联通)问:cdn.example.com 的 IP 是什么?
|
||||
智能 DNS:让我查查... 北京联通的 CDN 节点是 1.2.3.4
|
||||
@@ -486,6 +512,7 @@ DNS 答:1.2.3.4(固定的)
|
||||
传统 DNS 有个问题:**DNS 劫持和解析延迟**。
|
||||
|
||||
**HTTP DNS 方案**:
|
||||
|
||||
```
|
||||
客户端 → 绕过系统 DNS → 直接问 HTTP DNS 服务(如 223.5.5.5:80)
|
||||
↓
|
||||
@@ -495,11 +522,13 @@ DNS 答:1.2.3.4(固定的)
|
||||
```
|
||||
|
||||
优势:
|
||||
|
||||
- 防劫持:不走运营商 DNS
|
||||
- 更精准:可以按客户端网络质量选择 IP
|
||||
- 实时性:故障切换更快
|
||||
|
||||
**实战建议**:
|
||||
|
||||
- 移动端 APP 强烈建议接入 HTTP DNS
|
||||
- Web 端可以使用 CDN 提供的 CNAME 调度
|
||||
- 关键业务可以做多 IP 容灾(一个域名返回多个 IP)
|
||||
@@ -513,6 +542,7 @@ DNS 答:1.2.3.4(固定的)
|
||||
### 5.1 为什么 CDN 上 HTTPS 很重要?
|
||||
|
||||
**场景对比**:
|
||||
|
||||
```
|
||||
无 HTTPS:
|
||||
用户访问 http://cdn.example.com/image.jpg
|
||||
@@ -539,14 +569,15 @@ HTTP/2 多路复用生效
|
||||
|
||||
#### 证书管理
|
||||
|
||||
| 方案 | 说明 | 成本 | 适用场景 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **云厂商免费证书** | 阿里云/腾讯云等提供 | 免费 | 单域名,快速上手 |
|
||||
| **Let's Encrypt** | 社区免费证书 | 免费 | 自动化部署 |
|
||||
| 方案 | 说明 | 成本 | 适用场景 |
|
||||
| :--------------------- | :-------------------- | :------------- | :--------------- |
|
||||
| **云厂商免费证书** | 阿里云/腾讯云等提供 | 免费 | 单域名,快速上手 |
|
||||
| **Let's Encrypt** | 社区免费证书 | 免费 | 自动化部署 |
|
||||
| **商业 DV/OV/EV 证书** | 赛门铁克、GeoTrust 等 | ¥几百-几万/年 | 企业级、需要绿条 |
|
||||
| **泛域名证书** | *.example.com | ¥几千/年 | 多子域名 |
|
||||
| **泛域名证书** | \*.example.com | ¥几千/年 | 多子域名 |
|
||||
|
||||
**实战建议**:
|
||||
|
||||
- 测试环境:Let's Encrypt 或云厂商免费证书
|
||||
- 生产环境:泛域名证书(省事)或单域名 OV 证书(省钱)
|
||||
- 注意证书过期时间,设置自动续期提醒
|
||||
@@ -554,18 +585,21 @@ HTTP/2 多路复用生效
|
||||
#### HTTPS 优化配置
|
||||
|
||||
**TLS 版本选择**:
|
||||
|
||||
```
|
||||
推荐配置:仅 TLS 1.2 和 TLS 1.3
|
||||
兼容配置:TLS 1.1 + TLS 1.2 + TLS 1.3(兼容老旧浏览器)
|
||||
```
|
||||
|
||||
**密码套件**:
|
||||
|
||||
```
|
||||
推荐:ECDHE 密钥交换 + AES-GCM 加密
|
||||
禁用:DES、RC4、MD5、SHA1
|
||||
```
|
||||
|
||||
**OCSP Stapling**:
|
||||
|
||||
```
|
||||
功能:CDN 节点预获取证书吊销状态
|
||||
效果:减少客户端验证时间 200-500ms
|
||||
@@ -573,6 +607,7 @@ HTTP/2 多路复用生效
|
||||
```
|
||||
|
||||
**TLS 会话复用**:
|
||||
|
||||
```
|
||||
Session ID 复用:客户端带着上次 Session ID,服务端恢复会话
|
||||
Session Ticket 复用:服务端把会话状态加密发给客户端,下次带来
|
||||
@@ -582,6 +617,7 @@ Session Ticket 复用:服务端把会话状态加密发给客户端,下次
|
||||
### 5.3 HTTP/2 与 HTTP/3 在 CDN 上的应用
|
||||
|
||||
**HTTP/2 多路复用**:
|
||||
|
||||
```
|
||||
HTTP/1.1:
|
||||
请求 1 (index.html) ────────────────→
|
||||
@@ -603,6 +639,7 @@ HTTP/2:
|
||||
```
|
||||
|
||||
**HTTP/2 服务端推送**:
|
||||
|
||||
```
|
||||
场景:用户请求 index.html,里面引用了 style.css 和 script.js
|
||||
|
||||
@@ -620,6 +657,7 @@ HTTP/2 推送:
|
||||
```
|
||||
|
||||
**HTTP/3 (QUIC)**:
|
||||
|
||||
```
|
||||
HTTP/2 的问题:基于 TCP,队头阻塞
|
||||
→ 一个 TCP 包丢失,整个连接等待重传
|
||||
@@ -654,6 +692,7 @@ CDN 带宽 = 所有边缘节点的出流量总和
|
||||
```
|
||||
|
||||
**带宽与流量的关系**:
|
||||
|
||||
```
|
||||
1 Mbps 带宽持续跑 1 小时 = 450 MB 流量
|
||||
(计算:1,000,000 bps × 3600s ÷ 8 ÷ 1024 ÷ 1024 ≈ 429 MB)
|
||||
@@ -689,13 +728,13 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
||||
|
||||
**命中率低的常见原因**:
|
||||
|
||||
| 原因 | 现象 | 解决方案 |
|
||||
| :--- | :--- | :--- |
|
||||
| 缓存时间太短 | TTL 只有几分钟 | 根据文件类型调整 TTL |
|
||||
| 查询参数变化 | URL 带随机数 | 配置忽略特定参数 |
|
||||
| 缓存键设置不当 | 不该区分的被区分了 | 优化缓存键规则 |
|
||||
| 内容更新频繁 | 文件经常被覆盖 | 使用版本号或 hash 文件名 |
|
||||
| 首次访问多 | 新内容或新节点 | 提前预热 |
|
||||
| 原因 | 现象 | 解决方案 |
|
||||
| :------------- | :----------------- | :----------------------- |
|
||||
| 缓存时间太短 | TTL 只有几分钟 | 根据文件类型调整 TTL |
|
||||
| 查询参数变化 | URL 带随机数 | 配置忽略特定参数 |
|
||||
| 缓存键设置不当 | 不该区分的被区分了 | 优化缓存键规则 |
|
||||
| 内容更新频繁 | 文件经常被覆盖 | 使用版本号或 hash 文件名 |
|
||||
| 首次访问多 | 新内容或新节点 | 提前预热 |
|
||||
|
||||
### 6.2 日志分析与问题排查
|
||||
|
||||
@@ -712,18 +751,19 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
||||
|
||||
关键字段解释:
|
||||
|
||||
| 字段 | 说明 | 分析价值 |
|
||||
| :--- | :--- | :--- |
|
||||
| `cache_status` | 缓存状态 | HIT(命中)、MISS(未命中)、EXPIRED(过期) |
|
||||
| `response_time` | 响应时间(ms) | 判断用户体验,>500ms 需优化 |
|
||||
| `http_status` | HTTP 状态码 | 404/500 错误排查 |
|
||||
| `bytes_sent` | 发送字节数 | 带宽统计 |
|
||||
| 字段 | 说明 | 分析价值 |
|
||||
| :-------------- | :------------- | :------------------------------------------- |
|
||||
| `cache_status` | 缓存状态 | HIT(命中)、MISS(未命中)、EXPIRED(过期) |
|
||||
| `response_time` | 响应时间(ms) | 判断用户体验,>500ms 需优化 |
|
||||
| `http_status` | HTTP 状态码 | 404/500 错误排查 |
|
||||
| `bytes_sent` | 发送字节数 | 带宽统计 |
|
||||
|
||||
#### 常见问题排查
|
||||
|
||||
**问题 1:用户反映访问慢**
|
||||
|
||||
排查步骤:
|
||||
|
||||
```
|
||||
1. 看日志 response_time
|
||||
- 如果很大(>500ms):检查是缓存 MISS 还是源站慢
|
||||
@@ -739,6 +779,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
||||
**问题 2:缓存不生效,每次都回源**
|
||||
|
||||
排查清单:
|
||||
|
||||
```
|
||||
□ 源站响应头是否有 Cache-Control: no-cache / private?
|
||||
□ URL 是否带随机参数(如 ?_=123456)?
|
||||
@@ -750,6 +791,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
||||
**问题 3:费用暴涨**
|
||||
|
||||
排查方向:
|
||||
|
||||
```
|
||||
1. 看账单明细
|
||||
- CDN 流量费高:检查是否有大文件被频繁访问,或被盗链
|
||||
@@ -848,6 +890,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
||||
#### 对象存储配置
|
||||
|
||||
**存储桶规划**:
|
||||
|
||||
```
|
||||
Bucket: myapp-images-prod
|
||||
├─ 目录结构:
|
||||
@@ -872,6 +915,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
||||
```
|
||||
|
||||
**CORS 跨域配置**:
|
||||
|
||||
```xml
|
||||
<CORSConfiguration>
|
||||
<CORSRule>
|
||||
@@ -890,6 +934,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
||||
#### CDN 加速配置
|
||||
|
||||
**缓存策略配置**:
|
||||
|
||||
```
|
||||
全局默认规则:
|
||||
├─ 缓存键:URL 路径 + 保留 w、h、format 查询参数
|
||||
@@ -916,6 +961,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
||||
```
|
||||
|
||||
**HTTPS 优化配置**:
|
||||
|
||||
```
|
||||
证书配置:
|
||||
├─ 证书类型:泛域名证书 *.myapp.com
|
||||
@@ -1031,14 +1077,14 @@ rules:
|
||||
```yaml
|
||||
# 带宽封顶配置
|
||||
bandwidth_cap:
|
||||
daily_limit: 500 # Mbps,日峰值超过则自动停用 CDN
|
||||
monthly_limit: 10000 # GB,月流量超过则停用
|
||||
daily_limit: 500 # Mbps,日峰值超过则自动停用 CDN
|
||||
monthly_limit: 10000 # GB,月流量超过则停用
|
||||
|
||||
# 告警阈值
|
||||
alerts:
|
||||
- threshold: 70% # 达到 70% 发告警
|
||||
- threshold: 70% # 达到 70% 发告警
|
||||
channels: [sms, email]
|
||||
- threshold: 90% # 达到 90% 打电话
|
||||
- threshold: 90% # 达到 90% 打电话
|
||||
channels: [phone]
|
||||
```
|
||||
|
||||
@@ -1049,12 +1095,14 @@ bandwidth_cap:
|
||||
### 8.1 架构设计原则
|
||||
|
||||
**原则 1:动静分离**
|
||||
|
||||
```
|
||||
动态内容(API、HTML)→ 走源站或边缘函数
|
||||
静态内容(图片、JS、CSS、视频)→ 走 CDN + 对象存储
|
||||
```
|
||||
|
||||
**原则 2:就近服务**
|
||||
|
||||
```
|
||||
用户在哪里,内容就缓存到哪里
|
||||
→ 选择覆盖广的 CDN 服务商
|
||||
@@ -1063,6 +1111,7 @@ bandwidth_cap:
|
||||
```
|
||||
|
||||
**原则 3:分层缓存**
|
||||
|
||||
```
|
||||
浏览器本地缓存(最强)
|
||||
↓
|
||||
@@ -1074,6 +1123,7 @@ CDN 中间层/区域节点(兜底)
|
||||
```
|
||||
|
||||
**原则 4:成本与体验的平衡**
|
||||
|
||||
```
|
||||
存储分级:热数据标准存储,冷数据归档存储
|
||||
缓存策略:高频内容长 TTL,低频内容短 TTL
|
||||
@@ -1084,24 +1134,28 @@ CDN 中间层/区域节点(兜底)
|
||||
### 8.2 避坑清单
|
||||
|
||||
**存储桶命名与权限**
|
||||
|
||||
- [ ] 桶名全局唯一,避免被占用
|
||||
- [ ] 私有文件不要设置为公共读
|
||||
- [ ] AccessKey 不要写在前端代码里,用 STS 临时凭证
|
||||
- [ ] 启用服务端加密(SSE)保护敏感数据
|
||||
|
||||
**CDN 缓存配置**
|
||||
|
||||
- [ ] HTML 文件 TTL 不要太长(建议 < 5 分钟)
|
||||
- [ ] JS/CSS 建议用带 hash 的文件名,TTL 设为 1 年
|
||||
- [ ] 缓存键要合理,不要把用户信息等变量放进去
|
||||
- [ ] 重要更新后记得刷新缓存或预热
|
||||
|
||||
**HTTPS 安全**
|
||||
|
||||
- [ ] 证书不要过期,设置自动续期
|
||||
- [ ] 最低 TLS 版本建议 1.2
|
||||
- [ ] 开启 HSTS 防止降级攻击
|
||||
- [ ] 敏感 Cookie 设置 Secure 和 HttpOnly
|
||||
|
||||
**成本控制**
|
||||
|
||||
- [ ] 开启带宽封顶告警,防止异常流量
|
||||
- [ ] 低频/归档存储有最小存储时间和提前删除费,注意规则
|
||||
- [ ] 回源流量费也很贵,努力提高 CDN 命中率
|
||||
@@ -1170,7 +1224,12 @@ class DirectUploader {
|
||||
const formData = new FormData()
|
||||
|
||||
// 构建表单字段(不同厂商字段名不同)
|
||||
const formFields = this._buildFormFields(credentials, fileKey, file.type, options)
|
||||
const formFields = this._buildFormFields(
|
||||
credentials,
|
||||
fileKey,
|
||||
file.type,
|
||||
options
|
||||
)
|
||||
Object.entries(formFields).forEach(([key, value]) => {
|
||||
formData.append(key, value)
|
||||
})
|
||||
@@ -1210,7 +1269,11 @@ class DirectUploader {
|
||||
const fileKey = this._generateFileKey(file, options.directory)
|
||||
|
||||
// 1. 初始化分片上传
|
||||
const uploadId = await this._initMultipartUpload(credentials, fileKey, file.type)
|
||||
const uploadId = await this._initMultipartUpload(
|
||||
credentials,
|
||||
fileKey,
|
||||
file.type
|
||||
)
|
||||
|
||||
// 2. 计算分片
|
||||
const parts = []
|
||||
@@ -1232,21 +1295,30 @@ class DirectUploader {
|
||||
|
||||
// 支持断点续传:检查哪些分片已上传
|
||||
if (options.resume) {
|
||||
const existingParts = await this._listParts(credentials, fileKey, uploadId)
|
||||
const existingParts = await this._listParts(
|
||||
credentials,
|
||||
fileKey,
|
||||
uploadId
|
||||
)
|
||||
for (const part of existingParts) {
|
||||
uploadedParts.push(part)
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤出未上传的分片
|
||||
const pendingParts = parts.filter(p =>
|
||||
!uploadedParts.some(up => up.partNumber === p.number)
|
||||
const pendingParts = parts.filter(
|
||||
(p) => !uploadedParts.some((up) => up.partNumber === p.number)
|
||||
)
|
||||
|
||||
// 并发上传
|
||||
const uploadPart = async (part) => {
|
||||
try {
|
||||
const etag = await this._uploadPart(credentials, fileKey, uploadId, part)
|
||||
const etag = await this._uploadPart(
|
||||
credentials,
|
||||
fileKey,
|
||||
uploadId,
|
||||
part
|
||||
)
|
||||
return { partNumber: part.number, etag }
|
||||
} catch (error) {
|
||||
failedParts.push({ part, error })
|
||||
@@ -1271,11 +1343,18 @@ class DirectUploader {
|
||||
|
||||
// 检查是否所有分片都上传成功
|
||||
if (uploadedParts.length !== totalParts) {
|
||||
throw new Error(`Upload incomplete: ${uploadedParts.length}/${totalParts} parts uploaded`)
|
||||
throw new Error(
|
||||
`Upload incomplete: ${uploadedParts.length}/${totalParts} parts uploaded`
|
||||
)
|
||||
}
|
||||
|
||||
// 4. 完成分片上传(合并分片)
|
||||
await this._completeMultipartUpload(credentials, fileKey, uploadId, uploadedParts)
|
||||
await this._completeMultipartUpload(
|
||||
credentials,
|
||||
fileKey,
|
||||
uploadId,
|
||||
uploadedParts
|
||||
)
|
||||
|
||||
return {
|
||||
url: this._getFileUrl(fileKey),
|
||||
@@ -1316,7 +1395,11 @@ class DirectUploader {
|
||||
|
||||
_getFileUrl(key) {
|
||||
return `https://${this.bucket}.${this.provider === 'oss' ? 'oss' : 'cos'}-${this.region}.${
|
||||
this.provider === 'oss' ? 'aliyuncs.com' : this.provider === 'cos' ? 'myqcloud.com' : 'amazonaws.com'
|
||||
this.provider === 'oss'
|
||||
? 'aliyuncs.com'
|
||||
: this.provider === 'cos'
|
||||
? 'myqcloud.com'
|
||||
: 'amazonaws.com'
|
||||
}/${key}`
|
||||
}
|
||||
|
||||
@@ -1385,7 +1468,9 @@ async function uploadVideo(file) {
|
||||
parallel: 3, // 3 个并发
|
||||
resume: true, // 支持断点续传
|
||||
onProgress: (progress) => {
|
||||
console.log(`上传进度: ${progress.percent}%, 已传 ${progress.loaded}/${progress.total}`)
|
||||
console.log(
|
||||
`上传进度: ${progress.percent}%, 已传 ${progress.loaded}/${progress.total}`
|
||||
)
|
||||
},
|
||||
onPartComplete: (part) => {
|
||||
console.log(`分片 ${part.number} 上传完成`)
|
||||
@@ -1464,9 +1549,7 @@ router.post('/credentials', async (req, res) => {
|
||||
'oss:AbortMultipartUpload',
|
||||
'oss:ListParts'
|
||||
],
|
||||
Resource: [
|
||||
`acs:oss:*:*:${config.oss.bucket}/${prefix}*`
|
||||
]
|
||||
Resource: [`acs:oss:*:*:${config.oss.bucket}/${prefix}*`]
|
||||
}
|
||||
],
|
||||
Version: '1'
|
||||
@@ -1495,7 +1578,13 @@ router.post('/credentials', async (req, res) => {
|
||||
prefix: prefix, // 文件路径前缀
|
||||
// 安全限制
|
||||
maxSize: 100 * 1024 * 1024, // 最大 100MB
|
||||
allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4']
|
||||
allowedTypes: [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'video/mp4'
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1565,27 +1654,27 @@ module.exports = router
|
||||
const refererConfig = {
|
||||
// 白名单模式:只允许以下 Referer 访问
|
||||
allowList: [
|
||||
'*.myapp.com', // 主站
|
||||
'*.myapp.cn', // 国内站
|
||||
'localhost:*', // 本地开发
|
||||
'*.myapp.com', // 主站
|
||||
'*.myapp.cn', // 国内站
|
||||
'localhost:*', // 本地开发
|
||||
'127.0.0.1:*'
|
||||
],
|
||||
|
||||
// 黑名单模式(可选):禁止以下 Referer
|
||||
blockList: [
|
||||
'*. competitor.com', // 竞争对手
|
||||
'*. competitor.com', // 竞争对手
|
||||
'spam-site.com'
|
||||
],
|
||||
|
||||
// 空 Referer 处理:是否允许直接访问(浏览器地址栏输入 URL)
|
||||
allowEmptyReferer: false // 生产环境建议 false,测试环境可 true
|
||||
allowEmptyReferer: false // 生产环境建议 false,测试环境可 true
|
||||
}
|
||||
|
||||
// 2. URL 鉴权(更安全的防盗链,带时间戳和签名)
|
||||
class URLAuth {
|
||||
constructor(config) {
|
||||
this.key = config.key // 鉴权密钥,只在服务端保存
|
||||
this.expireTime = config.expireTime || 3600 // 默认 1 小时有效期
|
||||
this.key = config.key // 鉴权密钥,只在服务端保存
|
||||
this.expireTime = config.expireTime || 3600 // 默认 1 小时有效期
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1645,11 +1734,14 @@ class URLAuth {
|
||||
// 使用示例
|
||||
const auth = new URLAuth({
|
||||
key: 'your-secret-key-only-known-by-server',
|
||||
expireTime: 3600 // 1 小时有效期
|
||||
expireTime: 3600 // 1 小时有效期
|
||||
})
|
||||
|
||||
// 服务端生成带签名的 URL
|
||||
const signedUrl = auth.sign('https://cdn.example.com/private/document.pdf', 7200)
|
||||
const signedUrl = auth.sign(
|
||||
'https://cdn.example.com/private/document.pdf',
|
||||
7200
|
||||
)
|
||||
// 结果:https://cdn.example.com/private/document.pdf?sign=xxxxx&t=1699123456
|
||||
|
||||
// CDN 边缘或源站验证
|
||||
@@ -1662,15 +1754,12 @@ if (!result.valid) {
|
||||
const ipConfig = {
|
||||
// 只允许特定 IP 访问(适合内部系统)
|
||||
whiteList: [
|
||||
'192.168.1.0/24', // 内网网段
|
||||
'192.168.1.0/24', // 内网网段
|
||||
'10.0.0.0/8'
|
||||
],
|
||||
|
||||
// 禁止特定 IP 访问(封禁攻击者)
|
||||
blackList: [
|
||||
'1.2.3.4',
|
||||
'5.6.7.8'
|
||||
]
|
||||
blackList: ['1.2.3.4', '5.6.7.8']
|
||||
}
|
||||
|
||||
// 4. UA(User-Agent)黑白名单
|
||||
@@ -1687,7 +1776,7 @@ const uaConfig = {
|
||||
|
||||
// 只允许浏览器访问(严格模式)
|
||||
whiteList: [
|
||||
'Mozilla/*', // 现代浏览器
|
||||
'Mozilla/*', // 现代浏览器
|
||||
'AppleWebKit/*'
|
||||
]
|
||||
}
|
||||
@@ -1697,28 +1786,28 @@ const uaConfig = {
|
||||
|
||||
## 10. 名词对照表
|
||||
|
||||
| 英文术语 | 中文对照 | 解释 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Object Storage** | 对象存储 | 一种数据存储架构,将数据作为对象管理,而非文件系统层级结构。适合存储图片、视频、备份等非结构化数据。 |
|
||||
| **Bucket** | 存储桶 | 对象存储中的顶级容器,用于组织和隔离数据。每个桶有独立的权限控制和配置。 |
|
||||
| **Object** | 对象/文件对象 | 对象存储的基本单元,包含数据本身、元数据(Metadata)和全局唯一键(Key)。 |
|
||||
| **CDN** | 内容分发网络 | Content Delivery Network,通过在全球部署边缘节点,将网站内容缓存到离用户最近的位置,加速访问速度。 |
|
||||
| **Edge Node** | 边缘节点 | CDN 网络中部署在各地的缓存服务器,直接为用户提供内容访问服务。 |
|
||||
| **Origin** | 源站 | CDN 回源获取内容的服务器,可以是对象存储、ECS 或自建服务器。 |
|
||||
| **Cache Hit** | 缓存命中 | 用户请求的内容在 CDN 边缘节点已存在,直接返回,无需回源。 |
|
||||
| **Cache Miss** | 缓存未命中 | 边缘节点没有请求的内容,需要回源获取。 |
|
||||
| **Hit Ratio** | 命中率 | 缓存命中次数占总请求次数的比例。命中率越高,回源越少,成本越低。 |
|
||||
| **TTL** | 生存时间/缓存时间 | Time To Live,内容在 CDN 节点上缓存的有效期。过期后需要重新回源。 |
|
||||
| **Back to Source** | 回源 | CDN 边缘节点向源站请求内容的过程。 |
|
||||
| **Purge/Refresh** | 刷新缓存 | 强制使 CDN 缓存失效,下次请求回源获取最新内容。 |
|
||||
| **Preheat** | 预热 | 在正式发布前,主动将内容推送到 CDN 节点,让用户第一次访问就能命中缓存。 |
|
||||
| **CORS** | 跨域资源共享 | Cross-Origin Resource Sharing,浏览器的安全机制,控制不同域之间的资源访问。 |
|
||||
| **Referer** | 来源页面 | HTTP 请求头字段,指示请求是从哪个页面链接过来的。用于防盗链。 |
|
||||
| **STS** | 安全令牌服务 | Security Token Service,颁发临时访问凭证的服务,用于前端直传等场景。 |
|
||||
| **Multipart Upload** | 分片上传 | 将大文件切分成多个小分片并行上传,支持断点续传,提高上传效率和可靠性。 |
|
||||
| **ETag** | 实体标签 | HTTP 响应头,用于标识资源的特定版本,常用于缓存验证。 |
|
||||
| **S3 API** | S3 兼容接口 | AWS S3 的对象存储 API 规范,多数云厂商的对象存储都兼容此接口。 |
|
||||
| **Canonical Query String** | 规范查询字符串 | 签名字符串的一部分,用于计算请求签名,确保请求不被篡改。 |
|
||||
| 英文术语 | 中文对照 | 解释 |
|
||||
| :------------------------- | :---------------- | :--------------------------------------------------------------------------------------------------- |
|
||||
| **Object Storage** | 对象存储 | 一种数据存储架构,将数据作为对象管理,而非文件系统层级结构。适合存储图片、视频、备份等非结构化数据。 |
|
||||
| **Bucket** | 存储桶 | 对象存储中的顶级容器,用于组织和隔离数据。每个桶有独立的权限控制和配置。 |
|
||||
| **Object** | 对象/文件对象 | 对象存储的基本单元,包含数据本身、元数据(Metadata)和全局唯一键(Key)。 |
|
||||
| **CDN** | 内容分发网络 | Content Delivery Network,通过在全球部署边缘节点,将网站内容缓存到离用户最近的位置,加速访问速度。 |
|
||||
| **Edge Node** | 边缘节点 | CDN 网络中部署在各地的缓存服务器,直接为用户提供内容访问服务。 |
|
||||
| **Origin** | 源站 | CDN 回源获取内容的服务器,可以是对象存储、ECS 或自建服务器。 |
|
||||
| **Cache Hit** | 缓存命中 | 用户请求的内容在 CDN 边缘节点已存在,直接返回,无需回源。 |
|
||||
| **Cache Miss** | 缓存未命中 | 边缘节点没有请求的内容,需要回源获取。 |
|
||||
| **Hit Ratio** | 命中率 | 缓存命中次数占总请求次数的比例。命中率越高,回源越少,成本越低。 |
|
||||
| **TTL** | 生存时间/缓存时间 | Time To Live,内容在 CDN 节点上缓存的有效期。过期后需要重新回源。 |
|
||||
| **Back to Source** | 回源 | CDN 边缘节点向源站请求内容的过程。 |
|
||||
| **Purge/Refresh** | 刷新缓存 | 强制使 CDN 缓存失效,下次请求回源获取最新内容。 |
|
||||
| **Preheat** | 预热 | 在正式发布前,主动将内容推送到 CDN 节点,让用户第一次访问就能命中缓存。 |
|
||||
| **CORS** | 跨域资源共享 | Cross-Origin Resource Sharing,浏览器的安全机制,控制不同域之间的资源访问。 |
|
||||
| **Referer** | 来源页面 | HTTP 请求头字段,指示请求是从哪个页面链接过来的。用于防盗链。 |
|
||||
| **STS** | 安全令牌服务 | Security Token Service,颁发临时访问凭证的服务,用于前端直传等场景。 |
|
||||
| **Multipart Upload** | 分片上传 | 将大文件切分成多个小分片并行上传,支持断点续传,提高上传效率和可靠性。 |
|
||||
| **ETag** | 实体标签 | HTTP 响应头,用于标识资源的特定版本,常用于缓存验证。 |
|
||||
| **S3 API** | S3 兼容接口 | AWS S3 的对象存储 API 规范,多数云厂商的对象存储都兼容此接口。 |
|
||||
| **Canonical Query String** | 规范查询字符串 | 签名字符串的一部分,用于计算请求签名,确保请求不被篡改。 |
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# 域名、DNS 与 HTTPS
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# Docker 容器化
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,611 @@
|
||||
# 网关与反向代理
|
||||
::: tip 🎯 核心问题
|
||||
**在高并发的互联网架构中,如何把流量安全、高效地送到正确的服务?** 反向代理解决"流量怎么分发",API网关解决"请求怎么处理"。本文通过真实案例(前台接待、保安系统、智能路由)深入理解网关的设计哲学和工程实践。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要"网关"?
|
||||
|
||||
### 1.1 从一个真实案例说起:某电商的架构演进
|
||||
|
||||
某电商平台在业务快速增长时遇到了严重的架构问题:
|
||||
|
||||
**场景还原:**
|
||||
|
||||
```
|
||||
阶段一:直接暴露服务
|
||||
客户端 → 直接调用用户服务、订单服务、支付服务...
|
||||
↓
|
||||
问题1:服务IP暴露,存在安全隐患
|
||||
问题2:无法统一做认证、限流
|
||||
问题3:新增服务需要修改客户端配置
|
||||
```
|
||||
|
||||
::: warning ⚠️ 直接暴露的致命问题
|
||||
|
||||
- **安全隐患**: 所有服务IP暴露,容易被攻击
|
||||
- **功能重复**: 每个服务都要做认证、限流、日志
|
||||
- **扩展困难**: 新增服务要修改所有客户端
|
||||
- **协议混乱**: 有的用HTTP,有的用gRPC,客户端要适配
|
||||
:::
|
||||
|
||||
**改进后的架构(引入网关):**
|
||||
|
||||
```
|
||||
客户端 → API网关(Nginx/Kong) → 内部服务
|
||||
↓
|
||||
统一认证、限流、路由
|
||||
↓
|
||||
客户端只知道网关地址
|
||||
```
|
||||
|
||||
::: tip ✨ 改进后的效果
|
||||
|
||||
- **安全**: 真实服务IP隐藏,只有网关对外
|
||||
- **功能收敛**: 认证、限流、日志在网关统一处理
|
||||
- **扩展容易**: 新增服务只需网关配置路由
|
||||
- **协议统一**: 对外HTTP,内部可用gRPC
|
||||
:::
|
||||
|
||||
### 1.2 网关的生活化比喻
|
||||
|
||||
**前台接待**
|
||||
|
||||
想象你去一家大公司:
|
||||
|
||||
- **没有前台**: 访客直接找各部门,不知道在哪,公司乱成一团
|
||||
- **有前台**: 访客先到前台,前台问清楚来意,再引导到对应部门
|
||||
|
||||
**API网关就是系统的"前台"**:
|
||||
|
||||
- **反向代理**: 前台,引导访客到正确的部门
|
||||
- **API网关**: 智能前台,还能检查访客身份(认证)、限制访问人数(限流)
|
||||
|
||||
<ReverseProxyDemo />
|
||||
|
||||
---
|
||||
|
||||
## 2. 什么是反向代理?
|
||||
|
||||
### 2.1 正向代理 vs 反向代理
|
||||
|
||||
::: tip 🤔 术语解释
|
||||
**正向代理(Forward Proxy)**:
|
||||
|
||||
- 部署在客户端侧
|
||||
- 代替客户端访问外部资源
|
||||
- 典型应用:VPN、翻墙工具
|
||||
- 例子:公司网络,你通过代理访问外网
|
||||
|
||||
**反向代理(Reverse Proxy)**:
|
||||
|
||||
- 部署在服务器端
|
||||
- 接收客户端请求并转发给内部服务
|
||||
- 客户端只知道代理存在,不知道真实服务器
|
||||
- 例子:Nginx、HAProxy
|
||||
:::
|
||||
|
||||
**对比表:**
|
||||
|
||||
| 维度 | 正向代理 | 反向代理 |
|
||||
| ------------ | ------------------------ | ------------------------ |
|
||||
| **部署位置** | 客户端侧 | 服务器端 |
|
||||
| **服务对象** | 客户端 | 服务器 |
|
||||
| **典型应用** | VPN、翻墙 | 负载均衡、网关 |
|
||||
| **透明性** | 服务器看到代理IP | 客户端看到代理IP |
|
||||
| **目的** | 隐藏真实客户端、加速访问 | 隐藏真实服务器、负载均衡 |
|
||||
|
||||
### 2.2 反向代理的核心价值
|
||||
|
||||
::: details 价值一:负载均衡
|
||||
将流量分发到多个后端服务器,避免单点过载。
|
||||
|
||||
```
|
||||
客户端
|
||||
↓
|
||||
Nginx(反向代理)
|
||||
↓
|
||||
┌─────────┬─────────┬─────────┐
|
||||
│ 服务器1 │ 服务器2 │ 服务器3 │
|
||||
└─────────┴─────────┴─────────┘
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: details 价值二:安全防护
|
||||
隐藏真实服务器IP,防止直接攻击。统一在代理层做安全防护。
|
||||
|
||||
```
|
||||
客户端 → 只能看到Nginx的IP
|
||||
真实服务器 → 只在内网,外部无法直接访问
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: details 价值三:SSL终结
|
||||
在代理层处理HTTPS加密解密,后端服务用HTTP,降低后端计算开销。
|
||||
|
||||
```
|
||||
HTTPS客户端 → Nginx(加密/解密) → HTTP后端服务
|
||||
↑
|
||||
SSL终结点
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. Nginx:为什么能扛起百万并发?
|
||||
|
||||
### 3.1 Master-Worker进程模型
|
||||
|
||||
Nginx采用**多进程**架构,而不是多线程:
|
||||
|
||||
**Master进程(管理者)**:
|
||||
|
||||
- 负责读取和验证配置文件
|
||||
- 管理Worker进程(启动、停止、重新加载)
|
||||
- 不处理具体请求
|
||||
|
||||
**Worker进程(工作者)**:
|
||||
|
||||
- 实际处理HTTP请求
|
||||
- 每个Worker是独立的进程,相互隔离
|
||||
- 数量通常设置为CPU核心数,避免上下文切换开销
|
||||
|
||||
::: tip 💡 优势
|
||||
|
||||
- **隔离性好**: 一个Worker崩溃,不影响其他Worker
|
||||
- **充分利用多核**: 每个Worker独立运行
|
||||
- **避免多线程复杂性**: 无需处理锁、竞争等问题
|
||||
:::
|
||||
|
||||
### 3.2 事件驱动 + 异步非阻塞
|
||||
|
||||
这是Nginx高性能的核心秘密:
|
||||
|
||||
**传统Apache(多进程/线程模型)**:
|
||||
|
||||
- 一个连接 = 一个进程/线程
|
||||
- 并发数受限于系统进程/线程数
|
||||
- 大量连接时,进程切换开销巨大
|
||||
|
||||
**Nginx(事件驱动模型)**:
|
||||
|
||||
- 使用epoll(Linux)/kqueue(macOS)等高效I/O多路复用机制
|
||||
- 一个Worker进程可以同时处理数万个连接
|
||||
- 连接没有数据时,不会占用CPU,有新数据时通过事件通知唤醒
|
||||
|
||||
::: tip 生活化比喻
|
||||
|
||||
- **Apache**: 餐厅里每个顾客配一个服务员(进程),顾客多需要大量服务员
|
||||
- **Nginx**: 一个超级服务员,同时服务所有顾客,谁需要服务就去谁那里,而不是一直站在某个顾客旁边
|
||||
:::
|
||||
|
||||
<NginxArchitectureDemo />
|
||||
|
||||
---
|
||||
|
||||
## 4. 什么是API网关?
|
||||
|
||||
### 4.1 为什么需要API网关?
|
||||
|
||||
**想象一个没有网关的系统:**
|
||||
|
||||
- 客户端需要知道多个服务的地址(用户服务、订单服务、支付服务...)
|
||||
- 每个服务都要自己做认证、限流、日志
|
||||
- 协议不统一,有的用HTTP,有的用gRPC
|
||||
- 服务升级时,客户端也需要跟着改
|
||||
|
||||
::: warning ⚠️ 没有网关的问题
|
||||
|
||||
- **客户端复杂**: 需要配置多个服务地址
|
||||
- **功能重复**: 每个服务都要实现认证、限流
|
||||
- **协议混乱**: 客户端要适配多种协议
|
||||
- **升级困难**: 服务升级,客户端也要改
|
||||
:::
|
||||
|
||||
**有了API网关之后:**
|
||||
|
||||
- 客户端只需要知道网关地址,网关负责路由到正确服务
|
||||
- 认证、限流、日志等横切逻辑统一在网关处理
|
||||
- 网关可以做协议转换,对外统一暴露HTTP
|
||||
- 后端服务升级,只需要改网关配置,客户端无感知
|
||||
|
||||
<ApiGatewayDemo />
|
||||
|
||||
### 4.2 API网关的核心功能
|
||||
|
||||
| 功能 | 说明 | 典型场景 |
|
||||
| :----------- | :----------------------------------------- | :----------------------------------------------- |
|
||||
| **路由转发** | 根据URL、Header等规则,将请求转发到不同服务 | `/api/users` → 用户服务,`/api/orders` → 订单服务 |
|
||||
| **负载均衡** | 同一个服务有多实例时,分摊流量 | 用户服务有3台实例,轮询分发请求 |
|
||||
| **认证鉴权** | 统一校验JWT、OAuth Token | 未登录用户无法访问`/api/admin` |
|
||||
| **限流熔断** | 控制流量上限,防止服务被压垮 | 每秒最多1000请求,超过返回429 |
|
||||
| **协议转换** | 对外HTTP,内部可转gRPC | 客户端用HTTP,网关转gRPC调用内部服务 |
|
||||
| **灰度发布** | 按Header或比例,将部分流量导到新版本 | 5%用户体验新版本,95%用旧版本 |
|
||||
| **日志监控** | 统一记录请求日志,便于分析和排障 | 记录每次请求的耗时、状态码、返回大小 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 网关实战:如何构建完整的网关架构?
|
||||
|
||||
### 5.1 完整架构图
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────┐
|
||||
│ 客户端(浏览器/APP) │
|
||||
└───────────────────────────┬─────────────────────────────────────────┘
|
||||
│ HTTPS
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────────────┐
|
||||
│ 外层:CDN + WAF │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ CDN(内容分发网络) │ │
|
||||
│ │ - 静态资源缓存(图片、CSS、JS) │ │
|
||||
│ │ - 就近访问,降低延迟 │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ WAF(Web应用防火墙) │ │
|
||||
│ │ - 防护SQL注入、XSS攻击 │ │
|
||||
│ │ - 拦截恶意Bot、爬虫 │ │
|
||||
│ │ - CC攻击防护 │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────────────┐
|
||||
│ 中层:API网关(Nginx/Kong) │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 第一层:SSL终结 + 安全防护 │ │
|
||||
│ │ - HTTPS / TLS 1.3 │ │
|
||||
│ │ - HSTS、安全响应头 │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 第二层:认证与鉴权 │ │
|
||||
│ │ - JWT Token校验 │ │
|
||||
│ │ - OAuth 2.0 / SSO集成 │ │
|
||||
│ │ - API Key管理 │ │
|
||||
│ │ - 权限校验(RBAC) │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 第三层:流量控制 │ │
|
||||
│ │ - 限流- 令牌桶/漏桶算法 │ │
|
||||
│ │ - 熔断- 防止故障扩散 │ │
|
||||
│ │ - 降级- 服务不可用时的备用方案 │ │
|
||||
│ │ - 灰度发布- 按比例分配流量 │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 第四层:路由与负载均衡 │ │
|
||||
│ │ - 路径路由- Path-based Routing) │ │
|
||||
│ │ - 域名路由- Host-based Routing) │ │
|
||||
│ │ - Header路由- Header-based Routing) │ │
|
||||
│ │ - 负载均衡算法- 轮询/加权/最少连接/IP哈希) │ │
|
||||
│ │ - 服务发现- Service Discovery)集成 │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 第五层:协议转换与数据处理 │ │
|
||||
│ │ - SSL终结- HTTPS ↔ HTTP) │ │
|
||||
│ │ - 协议转换- HTTP ↔ gRPC / WebSocket) │ │
|
||||
│ │ - 请求/响应转换- JSON ↔ XML) │ │
|
||||
│ │ - 数据压缩- Gzip / Brotli) │ │
|
||||
│ │ - 缓存- Cache)- 静态资源和API响应 │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────────────┐
|
||||
│ 内层:微服务集群 │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ 用户服务 │ │ 订单服务 │ │ 商品服务 │ │ 支付服务 │ │
|
||||
│ │ User Svc │ │ Order Svc │ │ Product Svc │ │ Payment Svc │ │
|
||||
│ │ │ │ │ │ │ │ │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ └────────────────┴────────────────┴────────────────┘ │
|
||||
│ │ │
|
||||
│ 服务发现与配置中心/ etcd) │
|
||||
│ - 服务注册与发现 │
|
||||
│ - 健康检查 │
|
||||
│ - KV配置存储 │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.2 路由与负载均衡
|
||||
|
||||
网关的核心职责之一,就是**把请求送到正确的地方**。这涉及两个关键能力:**路由**(去哪台服务器)和**负载均衡**(怎么分配流量)。
|
||||
|
||||
::: details 路由规则:从URL到服务
|
||||
想象一个电商系统,不同的URL对应不同的服务:
|
||||
|
||||
- `/api/users/*` → 用户服务
|
||||
- `/api/orders/*` → 订单服务
|
||||
- `/api/products/*` → 商品服务
|
||||
- `/api/pay/*` → 支付服务
|
||||
|
||||
**Nginx配置示例:**
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name api.example.com;
|
||||
|
||||
# 用户服务
|
||||
location /api/users/ {
|
||||
proxy_pass http://user-service;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# 订单服务
|
||||
location /api/orders/ {
|
||||
proxy_pass http://order-service;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# 商品服务
|
||||
location /api/products/ {
|
||||
proxy_pass http://product-service;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# 支付服务(需要更高安全级别)
|
||||
location /api/pay/ {
|
||||
# 限制IP访问
|
||||
allow 10.0.0.0/8;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://payment-service;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: details 负载均衡:四种策略对比
|
||||
当同一个服务有多个实例时,如何选择?
|
||||
|
||||
| 策略 | 原理 | 适用场景 | 优点 | 缺点 |
|
||||
| :----------- | :------------------------------------------------ | :----------------- | :------------------- | :--------------------------- |
|
||||
| **轮询** | 按顺序依次分配给每台服务器 | 服务器性能相近 | 简单公平 | 不考虑服务器当前负载 |
|
||||
| **加权轮询** | 按权重比例分配,权重高的分配更多 | 服务器性能不均 | 充分利用高性能服务器 | 需要合理设置权重 |
|
||||
| **最少连接** | 分配给当前连接数最少的服务器 | 长连接场景、视频流 | 动态适应负载变化 | 需要实时统计连接数 |
|
||||
| **IP哈希** | 根据客户端IP计算哈希,同一IP永远分配到同一台服务器 | 需要会话保持 | 保证会话一致性 | 某个IP流量大时会造成单点压力 |
|
||||
|
||||
**Nginx配置示例:**
|
||||
|
||||
```nginx
|
||||
# 加权轮询
|
||||
upstream backend_weighted {
|
||||
server 10.0.1.10:8080 weight=3; # 性能好,承担更多流量
|
||||
server 10.0.1.11:8080 weight=2;
|
||||
server 10.0.1.12:8080 weight=1; # 性能差,承担较少流量
|
||||
}
|
||||
|
||||
# 最少连接
|
||||
upstream backend_least_conn {
|
||||
least_conn;
|
||||
server 10.0.1.10:8080;
|
||||
server 10.0.1.11:8080;
|
||||
server 10.0.1.12:8080;
|
||||
}
|
||||
|
||||
# IP哈希(会话保持)
|
||||
upstream backend_ip_hash {
|
||||
ip_hash;
|
||||
server 10.0.1.10:8080;
|
||||
server 10.0.1.11:8080;
|
||||
server 10.0.1.12:8080;
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
<LoadBalancingDemo />
|
||||
|
||||
---
|
||||
|
||||
## 6. 网关安全:如何守护系统大门?
|
||||
|
||||
### 6.1 认证与鉴权
|
||||
|
||||
**传统方式(每个服务各自认证):**
|
||||
|
||||
- 用户服务、订单服务、支付服务...每个都要校验JWT
|
||||
- 代码重复,维护困难
|
||||
- secret分散在各个服务,泄露风险高
|
||||
|
||||
**网关统一认证:**
|
||||
|
||||
- 客户端携带Token访问网关
|
||||
- 网关校验Token合法性(签名、过期时间)
|
||||
- 校验通过后,将用户信息(如user_id)添加到请求头,转发给后端服务
|
||||
- 后端服务无需校验,直接从Header获取用户信息
|
||||
|
||||
::: tip 💡 核心思想
|
||||
**认证在网关,鉴权在服务**:
|
||||
|
||||
- **认证**: 你是谁?(校验Token,获取用户身份)
|
||||
- **鉴权**: 你能做什么?(根据用户角色判断权限)
|
||||
|
||||
就像公司前台:前台认证你的身份(身份证),但具体权限由各部门判断。
|
||||
:::
|
||||
|
||||
<AuthMiddlewareDemo />
|
||||
|
||||
### 6.2 HTTPS与SSL终结
|
||||
|
||||
**为什么需要HTTPS?**
|
||||
|
||||
1. **安全**: 防止数据在传输过程中被窃取
|
||||
2. **合规**: 现代浏览器对HTTP网站显示"不安全"警告
|
||||
3. **SEO**: 搜索引擎优先收录HTTPS网站
|
||||
|
||||
**SSL终结方案:**
|
||||
|
||||
- 只在网关层配置HTTPS和证书
|
||||
- 网关负责TLS握手和加解密
|
||||
- 网关和后端服务之间使用HTTP明文传输(内部网络可信)
|
||||
- 后端服务专注于业务逻辑,无需处理TLS
|
||||
|
||||
::: tip 💡 SSL终结的优势
|
||||
|
||||
- **简化管理**: 证书只在网关配置,后端无需配置
|
||||
- **降低开销**: 后端服务不需要处理TLS握手
|
||||
- **统一更新**: 证书更新只需在网关操作
|
||||
:::
|
||||
|
||||
<SslTerminationDemo />
|
||||
|
||||
---
|
||||
|
||||
## 7. 限流与熔断:如何防止系统被"流量洪水"冲垮?
|
||||
|
||||
### 7.1 限流算法对比
|
||||
|
||||
| 算法 | 核心思想 | 突发流量 | 适用场景 | 实现复杂度 |
|
||||
| :----------- | :------------------------ | :-------------------------- | :----------------------------- | :--------- |
|
||||
| **令牌桶** | 桶里装令牌,有令牌才能通过 | 允许一定程度的突发 | API限流、带宽控制 | 中等 |
|
||||
| **漏桶** | 请求进桶,匀速流出处理 | 强制平滑,突发会被缓存或拒绝 | 需要严格匀速处理的场景 | 中等 |
|
||||
| **滑动窗口** | 统计时间窗口内的请求数 | 严格按窗口计数,超出一律拒绝 | 精确统计(如"1分钟内最多100次") | 较高 |
|
||||
|
||||
### 7.2 Nginx限流配置实战
|
||||
|
||||
```nginx
|
||||
# 定义限流区域(放在http块中)
|
||||
|
||||
# 1. 基于IP的限流(漏桶算法)
|
||||
# zone=mylimit:10m - 区域名称和内存大小(10MB约可存储16万IP)
|
||||
# rate=10r/s - 每秒允许10个请求
|
||||
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
|
||||
|
||||
# 2. 基于IP的连接数限制(防止单个IP建立过多连接)
|
||||
limit_conn_zone $binary_remote_addr zone=addr:10m;
|
||||
|
||||
# 3. 基于服务端点的限流(不区分IP,保护后端整体)
|
||||
limit_req_zone $server_name zone=server_limit:10m rate=100r/s;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name api.example.com;
|
||||
|
||||
# 用户服务 - 普通限流
|
||||
location /api/users/ {
|
||||
# 应用限流
|
||||
# burst=20 - 桶容量,允许突发20个请求
|
||||
# nodelay - 不延迟处理突发请求(立即处理或拒绝)
|
||||
limit_req zone=mylimit burst=20 nodelay;
|
||||
|
||||
# 限制单个IP的连接数
|
||||
limit_conn addr 10;
|
||||
|
||||
proxy_pass http://user-service;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# 订单服务 - 更严格的限流
|
||||
location /api/orders/ {
|
||||
# 更严格的限流:每秒5个请求
|
||||
limit_req_zone $binary_remote_addr zone=order_limit:10m rate=5r/s;
|
||||
limit_req zone=order_limit burst=10 nodelay;
|
||||
|
||||
proxy_pass http://order-service;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# 限流后的处理
|
||||
# 当请求被限流时,返回429 Too Many Requests
|
||||
error_page 429 /429.html;
|
||||
location = /429.html {
|
||||
internal;
|
||||
return 429 '{"error": "Too Many Requests", "message": "Rate limit exceeded. Please try again later."}';
|
||||
add_header Content-Type application/json;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
::: tip 💡 限流策略建议
|
||||
|
||||
- **普通接口**: 每秒10个请求,允许突发20个
|
||||
- **重要接口**(支付、订单): 每秒5个请求,允许突发10个
|
||||
- **全局保护**: 所有请求总和不超过每秒100个
|
||||
:::
|
||||
|
||||
<RateLimitingDemo />
|
||||
|
||||
### 7.3 熔断:防止故障扩散
|
||||
|
||||
**熔断器的工作原理:**
|
||||
|
||||
1. **关闭状态**: 正常转发请求,同时统计错误率
|
||||
2. **开启状态**: 当错误率超过阈值,熔断器开启,直接返回错误,不再转发请求
|
||||
3. **半开状态**: 经过一段时间后,允许少量请求通过试探,如果成功则关闭熔断器
|
||||
|
||||
::: tip 💡 核心思想
|
||||
**熔断就像电路保险丝**: 电流过大时,保险丝自动熔断,保护整个电路不被烧毁。
|
||||
|
||||
类似地,当后端服务出现大量错误时,熔断器"跳闸",快速失败,防止故障扩散到整个系统。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 8. 总结:网关设计的核心思维
|
||||
|
||||
### 8.1 核心原则回顾
|
||||
|
||||
| 原则 | 含义 | 实践要点 |
|
||||
| ------------ | -------------------- | ------------------------------ |
|
||||
| **路由** | 把请求送到正确的地方 | 路径路由、域名路由、Header路由 |
|
||||
| **负载均衡** | 分摊流量到多台服务器 | 轮询、加权、最少连接、IP哈希 |
|
||||
| **安全** | 守护系统大门 | 认证鉴权、HTTPS、WAF |
|
||||
| **限流** | 防止被流量冲垮 | 令牌桶、漏桶、滑动窗口 |
|
||||
| **熔断** | 防止故障扩散 | 快速失败、降级方案 |
|
||||
| **可观测** | 监控和排障 | 日志、指标、链路追踪 |
|
||||
|
||||
### 8.2 技术选型建议
|
||||
|
||||
::: tip 💡 选型决策树
|
||||
|
||||
```
|
||||
选择网关:
|
||||
│
|
||||
├─ 只需要反向代理、负载均衡?
|
||||
│ ├─ 是 → Nginx(首选)
|
||||
│ └─ 否 → 继续
|
||||
│
|
||||
├─ 需要丰富的插件生态?
|
||||
│ ├─ 是 → Kong(基于Nginx)
|
||||
│ └─ 否 → 继续
|
||||
│
|
||||
├─ Spring Cloud 全家桶?
|
||||
│ ├─ 是 → Spring Cloud Gateway
|
||||
│ └─ 否 → Nginx
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 9. 名词速查表
|
||||
|
||||
| 名词 | 英文 | 解释 |
|
||||
| ------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **反向代理** | Reverse Proxy | 部署在服务器端,接收客户端请求并转发给内部服务的代理服务。客户端只知道反向代理的存在,不知道真实服务器地址。 |
|
||||
| **正向代理** | Forward Proxy | 部署在客户端侧,代替客户端访问外部资源的代理服务。服务端看到的是代理的IP,不知道真实客户端。典型应用:VPN、翻墙工具。 |
|
||||
| **API网关** | API Gateway | 位于客户端和后端服务之间的中间层,提供路由、认证、限流、日志等功能,是微服务架构的"统一大门"。 |
|
||||
| **负载均衡** | Load Balancing | 将请求流量分配到多台服务器,避免单台服务器过载,提高系统可用性和性能。 |
|
||||
| **SSL终结** | SSL Termination | 在网关层处理HTTPS加密解密,后端服务使用HTTP,降低后端计算开销,简化证书管理。 |
|
||||
| **限流** | Rate Limiting | 限制单位时间内的请求数量,防止系统被突发流量压垮。常用算法:令牌桶、漏桶、滑动窗口。 |
|
||||
| **熔断** | Circuit Breaking | 当依赖服务出现故障时,自动切断调用,防止故障扩散,并提供降级方案。 |
|
||||
| **会话保持** | Session Persistence | 确保同一客户端的请求始终路由到同一台后端服务器,用于需要保持会话状态的场景。 |
|
||||
| **健康检查** | Health Check | 定期检查后端服务的健康状态,自动剔除故障节点,保证流量只发送到健康的服务实例。 |
|
||||
| **灰度发布** | Canary Release | 将少量流量导到新版本,验证稳定性后逐步扩大比例,降低发布风险。 |
|
||||
| **WAF** | Web Application Firewall | Web应用防火墙,防护SQL注入、XSS、CC攻击等Web安全威胁。 |
|
||||
| **CDN** | Content Delivery Network | 内容分发网络,在全球部署边缘节点,加速静态资源访问。 |
|
||||
@@ -0,0 +1,3 @@
|
||||
# 故障排查与应急响应
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# 基础设施即代码
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# Kubernetes 编排
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# Linux 基础
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,400 @@
|
||||
# 负载均衡与网关
|
||||
::: tip 🎯 核心问题
|
||||
**当单台服务器扛不住时,如何把流量"聪明地"分配到多个服务器实例?** 负载均衡是现代分布式系统的"分发员"。本文通过真实案例(奶茶店收银、快递分拣、交通指挥)深入理解负载均衡的设计哲学和工程实践。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要"负载均衡"?
|
||||
|
||||
### 1.1 从一个真实案例说起:某网站的架构演进
|
||||
|
||||
某创业公司在用户量快速增长时遇到了严重的性能问题:
|
||||
|
||||
**场景还原:**
|
||||
|
||||
```
|
||||
阶段一:单台服务器
|
||||
用户 → 服务器(1核2G)
|
||||
↓
|
||||
日活1000 → 活跃时间:1000人同时访问
|
||||
↓
|
||||
问题:CPU 100%,响应慢,经常宕机
|
||||
```
|
||||
|
||||
::: warning ⚠️ 单台服务器的致命问题
|
||||
|
||||
- **性能瓶颈**: CPU 100%,响应时间> 5秒
|
||||
- **单点故障**: 服务器挂了,整个网站不可用
|
||||
- **扩展困难**: 只能垂直升级(加CPU、内存),贵且有限
|
||||
:::
|
||||
|
||||
**改进后的架构(引入负载均衡):**
|
||||
|
||||
```
|
||||
阶段二:多台服务器 + 负载均衡
|
||||
用户 → 负载均衡器(Nginx)
|
||||
↓
|
||||
├→ 服务器1 (1核2G)
|
||||
├→ 服务器2 (1核2G)
|
||||
└→ 服务器3 (1核2G)
|
||||
```
|
||||
|
||||
::: tip ✨ 改进后的效果
|
||||
|
||||
- **性能提升**: 3台服务器并行处理,响应时间< 1秒
|
||||
- **高可用**: 1台服务器挂了,其他服务器继续服务
|
||||
- **水平扩展**: 需要更多性能?加服务器就行
|
||||
:::
|
||||
|
||||
### 1.2 负载均衡的生活化比喻
|
||||
|
||||
**奶茶店收银台**
|
||||
|
||||
想象你开了一家网红奶茶店:
|
||||
|
||||
- **1个收银台**: 顾客排队,后面的人等不及,差评
|
||||
- **3个收银台**: 员工分配顾客到不同收银台,效率提升3倍
|
||||
|
||||
**负载均衡就是"收银台分配员"**:
|
||||
|
||||
- **用户**(顾客) → 请求服务
|
||||
- **负载均衡器**(分配员) → 把请求分配到不同服务器
|
||||
- **服务器**(收银台) → 处理请求
|
||||
|
||||
<LoadBalancerTypesDemo />
|
||||
|
||||
---
|
||||
|
||||
## 2. 什么是负载均衡?
|
||||
|
||||
### 2.1 四层负载均衡(L4):只看门牌号
|
||||
|
||||
**工作在传输层(TCP/UDP)**,就像快递小哥只看你家的**门牌号(IP地址+端口号)**,不关心你家是做什么。
|
||||
|
||||
**特点:**
|
||||
|
||||
- **速度超快**: 只做简单的地址转发,不解析数据包内容
|
||||
- **适用场景**: 数据库连接、Redis缓存、长连接游戏服务器
|
||||
- **代表产品**: LVS(Linux Virtual Server)、AWS NLB、Azure Load Balancer
|
||||
|
||||
::: details 工作原理
|
||||
|
||||
```
|
||||
客户端请求 → L4负载均衡器 → 后端服务器
|
||||
↓
|
||||
只看IP + Port
|
||||
↓
|
||||
快速转发(不解包内容)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 2.2 七层负载均衡(L7):检查包裹内容
|
||||
|
||||
**工作在应用层(HTTP/HTTPS)**,就像快递小哥不仅看门牌号,还会**打开包裹检查内容**,根据内容决定怎么送。
|
||||
|
||||
**特点:**
|
||||
|
||||
- **智能路由**: 可以根据URL路径、HTTP头、Cookie等做精细化路由
|
||||
- **高级功能**: SSL卸载、内容缓存、压缩、安全WAF
|
||||
- **适用场景**: Web应用、API网关、微服务架构
|
||||
- **代表产品**: Nginx、HAProxy、AWS ALB、Envoy
|
||||
|
||||
::: details 工作原理
|
||||
|
||||
```
|
||||
客户端请求 → L7负载均衡器 → 解析HTTP内容
|
||||
↓
|
||||
检查URL、Header、Cookie
|
||||
↓
|
||||
智能路由到特定服务器
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 2.3 L4 vs L7 对比一览
|
||||
|
||||
| 维度 | 四层负载均衡(L4) | 七层负载均衡(L7) |
|
||||
| :------------- | :------------------- | :------------------------ |
|
||||
| **工作层级** | 传输层(TCP/UDP) | 应用层(HTTP/HTTPS) |
|
||||
| **决策依据** | IP地址 + 端口号 | URL、Header、Cookie、Body |
|
||||
| **处理速度** | 极快(内核态处理) | 较快(用户态解析) |
|
||||
| **功能丰富度** | 基础转发 | SSL卸载、缓存、压缩、WAF |
|
||||
| **典型场景** | 数据库、游戏、长连接 | Web应用、API网关、微服务 |
|
||||
| **代表产品** | LVS、AWS NLB | Nginx、HAProxy、AWS ALB |
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心问题一:如何避免"坏掉"的服务器继续接客?
|
||||
|
||||
### 3.1 健康检查:别让"生病"的服务器拖累系统
|
||||
|
||||
想象一下,你的某个收银台突然坏了,但分配员不知道,还在源源不断地把顾客分过去。结果队伍越来越长,顾客怨声载道。
|
||||
|
||||
**健康检查(Health Check)就是防止这种情况发生的"哨兵"**。它定期"体检"每台服务器,发现"生病"的立即从队列中移除,等"康复"了再请回来。
|
||||
|
||||
<!-- <HealthCheckDemo /> -->
|
||||
|
||||
### 3.2 主动健康检查 vs 被动健康检查
|
||||
|
||||
**主动健康检查(Active Health Check)**: 负载均衡器主动"敲门"问服务器"你还在吗?"
|
||||
|
||||
- 定期发送探测请求(如 HTTP /health、TCP ping)
|
||||
- 响应超时或返回错误码则认为不健康
|
||||
- **优点**: 检测结果准确可靠
|
||||
- **缺点**: 产生额外的探测流量
|
||||
|
||||
**被动健康检查(Passive Health Check)**: 负载均衡器"观察"真实业务流量的响应情况
|
||||
|
||||
- 统计实际请求的响应时间、错误率
|
||||
- 连续多次失败则认为不健康
|
||||
- **优点**: 不产生额外流量
|
||||
- **缺点**: 需要足够的流量样本才能判定
|
||||
|
||||
::: details 阈值设定表
|
||||
| 指标 | 健康阈值 | 不健康阈值 | 说明 |
|
||||
|:---|:---|:---|:---|
|
||||
| **HTTP状态码** | 200-399 | 400+或超时 | 4xx/5xx都认为失败 |
|
||||
| **TCP连接** | 成功建立 | 连接超时 | 检查端口是否可达 |
|
||||
| **响应时间** | < 500ms | > 2000ms | 超时时间通常设为2-5秒 |
|
||||
| **连续失败次数** | - | 3次 | 避免单次抖动误判 |
|
||||
| **检查间隔** | - | 5s | 太频繁会增加负载 |
|
||||
|
||||
::: tip 💡 踸见坑:阈值设置太"敏感"
|
||||
某团队将健康检查的响应时间阈值设为100ms,而他们的应用平均响应时间在80-120ms之间波动。结果是服务器频繁被标记为"不健康",导致流量在健康和不健康之间反复横跳,系统整体可用率反而下降。
|
||||
|
||||
**正确的做法**: 阈值应该设置为**P99响应时间的2-3倍**,给正常波动留出足够的缓冲空间。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心问题二:如何保证"老顾客"一直找同一个"收银员"?
|
||||
|
||||
### 4.1 会话保持:让"老顾客"一直找同一个"收银员"
|
||||
|
||||
想象你是奶茶店的常客,每次来都由同一个店员接待。她知道你的口味偏好(半糖、去冰),服务起来又快又贴心。但如果每次来都换一个新人,你得一遍遍重复同样的要求,效率大打折扣。
|
||||
|
||||
**会话保持(Session Persistence/Sticky Session)** 就是解决这个问题的方法:确保同一个用户的请求,始终被路由到同一台后端服务器。
|
||||
|
||||
<SessionPersistenceDemo />
|
||||
|
||||
### 4.2 三种会话保持机制对比
|
||||
|
||||
| 机制 | 实现原理 | 优点 | 缺点 | 适用场景 |
|
||||
| :------------- | :---------------------------------------- | :------------------------------ | :---------------------------- | :---------------------- |
|
||||
| **Cookie插入** | LB在响应中插入Cookie,后续请求携带此Cookie | 不受IP变化影响,首次请求即可保持 | 客户端需支持Cookie,可能被禁用 | 电商购物车、登录态保持 |
|
||||
| **IP哈希** | 对客户端IP做哈希计算,映射到特定服务器 | 无需客户端支持,无状态 | IP变化会丢失会话,难以均匀分布 | 无Cookie环境、WebSocket |
|
||||
| **粘性会话表** | LB维护会话到服务器的映射表 | 支持会话复制和故障转移 | 占用LB内存,需要额外同步 | 高可用要求严格的场景 |
|
||||
|
||||
::: tip 💡 使用建议
|
||||
|
||||
- **Cookie插入**: 优先推荐,兼容性好
|
||||
- **IP哈希**: 只用于WebSocket等特殊场景
|
||||
- **粘性会话表**: 配合Cookie,提供故障转移能力
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心问题三:如何实现零停机部署?
|
||||
|
||||
### 5.1 蓝绿部署:"一键切换"的零停机发布
|
||||
|
||||
**核心思想**: 同时维护两套完全相同的生产环境(蓝环境和绿环境),但只有一个环境对外提供服务。
|
||||
|
||||
<BlueGreenDeploymentDemo />
|
||||
|
||||
**工作流程:**
|
||||
|
||||
1. **初始状态**: 蓝环境运行v1.0(生产),绿环境待命。
|
||||
2. **部署新版本**: 在绿环境部署v1.1,进行内部冒烟测试。
|
||||
3. **切换流量**: 将负载均衡器指向绿环境,流量瞬间切换到v1.1。
|
||||
4. **监控观察**: 观察绿环境运行状态,确认无异常。
|
||||
5. **保留旧版本**: 蓝环境保持v1.0一段时间(如24小时),作为快速回滚的保险。
|
||||
|
||||
::: tip ✨ 优缺点分析
|
||||
| 优点 | 缺点 |
|
||||
|:---|:---|
|
||||
| ✅ 零停机时间,切换在毫秒级完成 | ❌ 资源成本高,需要同时维护两套环境 |
|
||||
| ✅ 快速回滚,发现问题立即切回原环境 | ❌ 数据库Schema变更时需要特别处理兼容性 |
|
||||
| ✅ 新环境可完整测试后再接管流量 | ❌ 不适用于有状态服务(如WebSocket长连接) |
|
||||
|
||||
:::
|
||||
|
||||
### 5.2 金丝雀发布:"小步快跑"的灰度策略
|
||||
|
||||
金丝雀发布得名于历史上的"煤矿金丝雀"——矿工带着金丝雀下井,如果金丝雀出现异常,说明有毒气体泄漏,矿工立即撤离。在软件发布中,金丝雀发布就是先让一小部分用户试用新版本,观察没有问题后再逐步扩大范围。
|
||||
|
||||
<CanaryReleaseDemo />
|
||||
|
||||
**核心思想:**
|
||||
|
||||
1. **小流量先行**: 先将1%的流量导入新版本服务器。
|
||||
2. **观察指标**: 持续监控错误率、延迟、业务关键指标。
|
||||
3. **逐步放量**: 如果一切正常,逐步将比例提升到5%、10%、25%、50%、100%。
|
||||
4. **快速回滚**: 一旦发现异常,立即将所有流量切回旧版本。
|
||||
|
||||
::: tip 💡 金丝雀发布的优势
|
||||
| 优势 | 说明 |
|
||||
|:---|:---|
|
||||
| 🎯 **风险可控** | 即使新版本有严重Bug,也只影响少量用户 |
|
||||
| 📊 **真实验证** | 在真实生产环境验证,比测试环境更可靠 |
|
||||
| 🚀 **快速迭代** | 团队可以更自信地频繁发布新功能 |
|
||||
| 💰 **资源友好** | 不需要像蓝绿部署那样准备两套完整环境 |
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. 核心问题四:如何让系统自己"呼吸"?
|
||||
|
||||
### 6.1 自动扩缩容:让系统像餐厅一样"灵活排班"
|
||||
|
||||
想象你开了一家餐厅:
|
||||
|
||||
- **午餐高峰期**: 需要10个服务员,但下午3点闲时只需要2个
|
||||
- 如果一直维持10个\*\*: 人工成本爆炸
|
||||
- 如果一直只有2个: 高峰期顾客等不及,全跑了
|
||||
|
||||
**自动扩缩容(Auto Scaling)** 就是让系统像餐厅一样"灵活排班"——忙的时候自动加服务器,闲的时候自动减服务器。
|
||||
|
||||
<AutoScalingDemo />
|
||||
|
||||
### 6.2 扩容指标的选择
|
||||
|
||||
自动扩缩容的核心是回答一个问题:\*\* **什么时候该加机器?什么时候该减机器?**
|
||||
|
||||
常见的决策指标:
|
||||
|
||||
| 指标 | 扩容阈值 | 缩容阈值 | 适用场景 |
|
||||
| :------------------ | :--------- | :--------- | :--------------- |
|
||||
| **CPU使用率** | > 70% | < 30% | 计算密集型应用 |
|
||||
| **内存使用率** | > 75% | < 40% | 内存密集型应用 |
|
||||
| **QPS(每秒请求数)** | > 1000/s | < 400/s | API网关、Web服务 |
|
||||
| **连接数** | > 5000 | < 1000 | 数据库、消息队列 |
|
||||
| **自定义业务指标** | 视业务而定 | 视业务而定 | 特定业务场景 |
|
||||
|
||||
::: tip 💡 扩容策略的"坑"与"解"
|
||||
|
||||
**坑1:扩容反应太慢,流量洪峰已经把系统打挂了**
|
||||
|
||||
某电商大促期间,设置CPU > 80%触发扩容,但监控采集有1分钟延迟,新实例启动需要3分钟。结果流量来得太快,扩容还没完成,服务器已经被打挂。
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- **提前扩容**: 基于历史数据预测流量高峰,提前30分钟开始扩容
|
||||
- **多级阈值**: 设置60%预警(开始预热新实例)、70%正式扩容、80%紧急扩容
|
||||
- **快速扩容**: 使用容器化部署,新实例30秒内启动(相比虚拟机3-5分钟)
|
||||
|
||||
**坑2:扩容太激进,成本爆炸**
|
||||
|
||||
某创业公司设置了激进的自动扩容策略:CPU > 50%就扩容。结果一个正常的业务波动就触发了扩容,服务器数量从5台膨胀到30台,月底云账单吓哭了CTO。
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- **设置扩容冷却时间**: 一次扩容后,至少等待5分钟才能再次扩容
|
||||
- **设置最大实例数**: max = 当前实例数 × 2,防止无限膨胀
|
||||
- **区分突刺和趋势**: 只有连续3个周期都超过阈值才扩容,避免单点突刺触发
|
||||
|
||||
**坑3:缩容太快,刚扩容的机器马上就缩了**
|
||||
|
||||
某团队设置了CPU < 30%缩容。扩容后流量还在消化,CPU短暂回落到25%,触发了缩容。刚缩完CPU又飙到80%,又触发扩容——系统在"扩容-缩容-扩容"中疯狂震荡。
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- **缩容更保守**: 扩容阈值70%,缩容阈值25%,中间有足够的缓冲带
|
||||
- **缩容冷却时间更长**: 扩容后至少等待10分钟才能缩容
|
||||
- **渐进式缩容**: 一次只缩1台,观察后再决定要不要继续缩
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 7. 实战:如何选择负载均衡器?
|
||||
|
||||
### 7.1 主流负载均衡器对比
|
||||
|
||||
| 特性 | Nginx | HAProxy | Envoy | 云厂商负载均衡 |
|
||||
| -------------- | ------------------------------- | --------------------- | -------------- | -------------- |
|
||||
| **定位** | 高性能反向代理/负载均衡 | 开源负载均衡 | 云原生代理 | 托管负载均衡 |
|
||||
| **性能** | 极高(C语言,事件驱动) | 高(事件驱动) | 高(C++/Rust) | 极高 |
|
||||
| **功能丰富度** | 基础负载均衡、静态文件、缓存 | 丰富的负载均衡算法 | 高级路由、观测 | 功能全面 |
|
||||
| **配置** | 配置文件(nginx.conf) | 配置文件(haproxy.cfg) | API/配置文件 | UI控制台 |
|
||||
| **扩展** | C模块/Lua脚本 | Lua脚本 | WASM/Filter | 插件 |
|
||||
| **适用场景** | 静态资源、七层负载均衡、SSL终结 | 七层负载均衡、高可用 | 服务网格、多云 | 快速上手 |
|
||||
|
||||
::: tip 💡 选型建议
|
||||
**决策树:**
|
||||
|
||||
```
|
||||
选择负载均衡器:
|
||||
│
|
||||
├─ 只需要基础的四层负载均衡?
|
||||
│ ├─ 是 → LVS(开源免费)或 云厂商NLB
|
||||
│ └─ 否 → 继续
|
||||
│
|
||||
├─ 需要服务网格、多云部署?
|
||||
│ ├─ 是 → Envoy
|
||||
│ └─ 否 → 继续
|
||||
│
|
||||
├─ 需要极其复杂的配置和插件?
|
||||
│ ├─ 是 → HAProxy
|
||||
│ └─ 否 → 继续
|
||||
│
|
||||
├─ 需要高性能+简单配置?
|
||||
│ ├─ 是 → Nginx(首选)
|
||||
│ └─ 继续
|
||||
│
|
||||
├─ 想要托管运维?
|
||||
│ ├─ 是 → 云厂商负载均衡(AWS ALB、阿里SLB)
|
||||
│ └─ Nginx自建
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 8. 总结:负载均衡的核心思维
|
||||
|
||||
### 8.1 核心原则回顾
|
||||
|
||||
| 原则 | 含义 | 实践要点 |
|
||||
| -------- | -------------------------- | ------------------------------------- |
|
||||
| **分层** | L4处理"快递分拣"(快但简单) | L4处理数据库、游戏;L7处理Web、API |
|
||||
| **冗余** | 单点故障是架构的敌人 | 通过多实例、多区域部署提升可用性 |
|
||||
| **渐进** | 发布新版本不要"一刀切" | 蓝绿部署实现零停机;金丝雀实现风险可控 |
|
||||
| **弹性** | 系统应该像生命体一样"呼吸" | 忙时自动扩容,闲时自动缩容 |
|
||||
|
||||
### 8.2 设计检查清单
|
||||
|
||||
在引入负载均衡前,问自己以下问题:
|
||||
|
||||
- [ ] 是否真的需要负载均衡?(单机性能是否真的不够)
|
||||
- [ ] 选择L4还是L7?(根据业务场景)
|
||||
- [ ] 如何处理会话保持?(Cookie、IP哈希、会话表)
|
||||
- [ ] 如何实现健康检查?(主动、被动、阈值设置)
|
||||
- [ ] 如何实现零停机?(蓝绿部署、金丝雀)
|
||||
- [ ] 如何实现弹性?(扩缩指标、冷却时间、最大实例数)
|
||||
|
||||
---
|
||||
|
||||
## 9. 名词速查表
|
||||
|
||||
| 名词 | 英文 | 解释 |
|
||||
| ---------------- | --------------------- | ---------------------------------------- | ------------------------------ |
|
||||
| **负载均衡器** | Load Balancer | 将流量分发到多个后端服务器的设备或软件 |
|
||||
| **四层负载均衡** | L4 Load Balancing | 基于传输层(TCP/UDP)的负载均衡 |
|
||||
| **七层负载均衡** | L7 Load Balancing | 基于应用层(HTTP/HTTPS)的负载均衡 |
|
||||
| **健康检查** | Health Check | 定期检查后端服务器的健康状态的机制 |
|
||||
| **会话保持** | Session Persistence | 确保同一用户的请求始终路由到同一台服务器 |
|
||||
| **粘性会话** | Sticky Session | 另一种称呼,同Session Persistence |
|
||||
| **蓝绿部署** | Blue-Green Deployment | 两套环境切换的零停机发布策略 |
|
||||
| **金丝雀发布** | Canary Release | 小流量先行验证的灰度发布策略 |
|
||||
| **自动扩缩容** | Auto Scaling | 根据负载自动增加或减少服务器数量 |
|
||||
| **水平扩展** | Horizontal Scaling | 增加服务器数量来提升处理能力 |
|
||||
| **垂直扩展** | Vertical Scaling | 提升单机配置(CPU、内存)来提升处理能力 |
|
||||
| **多区域** | Multi-Region | 在多个地理区域部署服务 |
|
||||
| **多活** | Active-Active | 多个区域同时对外提供服务 |
|
||||
| **主备** | Active-Standby | 只有一个区域提供服务,其他待命 |
|
||||
| **数据同步** | Data Replication | 跨区域的数据复制机制 |
|
||||
| **RTO** | RTO | 恢复时间目标 | 系统故障后需要在多长时间内恢复 |
|
||||
| **RPO** | RPO | 恢复点目标 | 系统故障后可以接受的数据丢失量 |
|
||||
@@ -1,5 +1,4 @@
|
||||
# 线上运维:从监控到故障排查 (Interactive Guide to Operations)
|
||||
|
||||
# 监控、日志与告警
|
||||
> 💡 **学习指南**:本章节无需编程基础,通过交互式演示带你了解运维的完整知识体系。从监控告警到故障排查,从容量规划到自动化运维,全面掌握线上系统运维技能。
|
||||
|
||||
## 0. 引言:系统上线只是开始
|
||||
@@ -1,5 +1,4 @@
|
||||
# Agent 智能体入门 (Interactive Intro to AI Agent)
|
||||
|
||||
# AI Agent 与工具调用
|
||||
> 💡 **学习指南**:本章节无需编程基础,通过交互式演示带你深入了解 AI Agent(智能体)的工作原理。我们将从最基本的"工具调用"讲起,一直到 Agent 是如何规划、记忆和协作的。
|
||||
|
||||
<AgentQuickStartDemo />
|
||||
@@ -1,5 +1,4 @@
|
||||
# AI 词典:主流模型、产品形态与应用场景一览
|
||||
|
||||
# AI 能力词典
|
||||
随着生成式 AI 技术在各类产品和业务场景中的广泛落地,一个越来越现实的问题摆在每个我们面前: **到底有哪些 AI 能力可以用?** 在具体的需求里,又 **该选择哪一种能力、哪一类模型或哪一个产品来承载?**
|
||||
|
||||
面对这种困惑,最直观的做法或许是 “临时抱佛脚”:**遇到需求再搜索市面上云服务厂商的产品 API,或者是对应模型,搜索市面上的商业级解决方案对照文档与 Demo进行处理** 。看到图片需求就想到图像生成,碰到文本任务就找来大模型,涉及语音交互就想起 ASR 和 TTS,再在海量 API 与服务中货比三家。然而,把零散的产品堆在一起,与在企业级场景中系统性地规划、选型和组合 AI 能力,是两件截然不同的事情。仅靠临时查资料与经验判断,会带来能力认知碎片化、方案设计随意、能力复用困难等一系列严峻挑战。
|
||||
@@ -1,5 +1,4 @@
|
||||
# 人工智能进化史:从"教它规则"到"让它创造"
|
||||
|
||||
# AI 简史与核心概念
|
||||
> 💡 **学习指南**:本章节通过交互式演示,带你回顾 AI 如何从“只会算数的机器”进化成“能写诗的艺术家”。
|
||||
>
|
||||
> 我们将聚焦于三次核心的思维跃迁:从**教它规则**,到**让它模仿**,最终实现**让它创造**。同时,我们也会梳理关键的历史节点,带你理清技术发展的脉络。
|
||||
@@ -0,0 +1,3 @@
|
||||
# AI 原生应用设计
|
||||
|
||||
> 待实现
|
||||
@@ -0,0 +1,3 @@
|
||||
# AI 协议(MCP 等)
|
||||
|
||||
> 待实现
|
||||
@@ -1,5 +1,4 @@
|
||||
# 上下文工程入门 (Context Engineering)
|
||||
|
||||
# 上下文工程
|
||||
> 💡 **学习指南**:提示词工程解决的是“怎么把话说清楚”,上下文工程解决的是“让模型在合适的时刻看到合适的信息”。本章节会围绕一个问题展开:**在有限的上下文窗口里,如何既让模型懂你,又不把钱烧光?**
|
||||
|
||||
在开始之前,建议你先补两块“基础砖”:
|
||||
@@ -0,0 +1,3 @@
|
||||
# Embedding 与向量检索
|
||||
|
||||
> 待实现
|
||||
@@ -1,5 +1,4 @@
|
||||
# AI 绘画与生图模型入门 (Image Generation Intro)
|
||||
|
||||
# 图像生成原理
|
||||
> 💡 **学习指南**:提示词工程是“教 AI 说话”,而生图模型则是“教 AI 做梦”。本章节将带你拆解 AI 画笔背后的魔法——它是如何从一堆毫无意义的噪点中,变出足以乱真的艺术品的?
|
||||
|
||||
在开始之前,建议你先体验一下“神笔马良”的感觉。
|
||||
@@ -1,5 +1,4 @@
|
||||
# 大语言模型入门 (Interactive Intro to LLM)
|
||||
|
||||
# 大语言模型的工作原理
|
||||
> 💡 **学习指南**:本章节无需编程基础,通过交互式演示带你深入了解大语言模型(LLM)的底层工作原理。我们将从最基础的分词讲起,一直到 GPT 是如何训练和推理的。
|
||||
|
||||
<LlmQuickStartDemo />
|
||||
@@ -0,0 +1,3 @@
|
||||
# 模型微调与部署
|
||||
|
||||
> 待实现
|
||||
@@ -1,5 +1,4 @@
|
||||
# 多模态大模型入门 (VLM Intro)
|
||||
|
||||
# 多模态模型(视觉 / 音频 / 视频)
|
||||
> 💡 **学习指南**:本章节无需深厚的计算机视觉背景,通过交互式演示带你理解 AI 是如何拥有“眼睛”的。我们将揭秘 GPT-4V、Qwen-VL 等模型背后的核心原理。
|
||||
|
||||
<VlmQuickStartDemo />
|
||||
@@ -0,0 +1,3 @@
|
||||
# 神经网络与深度学习
|
||||
|
||||
> 待实现
|
||||
@@ -22,7 +22,7 @@ AI 本质上是一个**概率预测机器**(Next Token Predictor),它不
|
||||
|
||||
当我们谈论“工程”时,我们强调的是:**可复现、可验证、可转移**。
|
||||
|
||||

|
||||

|
||||
|
||||
AI 模型像一个**黑盒子**:我们知道输入(提示词)和输出(回答),但很难完全掌控中间发生了什么。
|
||||
|
||||
@@ -69,7 +69,7 @@ AI 模型像一个**黑盒子**:我们知道输入(提示词)和输出(
|
||||
|
||||
大多数传统大模型(如 GPT-3.5, Llama 2)属于此类。它们**直觉式地反应**,说完上句接下句,不做深层逻辑推演。
|
||||
|
||||

|
||||

|
||||
|
||||
- **特点**:快,但容易在复杂逻辑上犯错。
|
||||
- **策略**:需要你把步骤拆解得非常细(Chain of Thought),一步步喂给它。
|
||||
@@ -78,7 +78,7 @@ AI 模型像一个**黑盒子**:我们知道输入(提示词)和输出(
|
||||
|
||||
新一代模型(如 o1, R1)在回答前会进行“隐式推理”。
|
||||
|
||||

|
||||

|
||||
|
||||
- **特点**:慢,但逻辑能力强,能自我纠错。
|
||||
- **策略**:通常不需要复杂的 Prompt 技巧,直接说清楚目标即可,过多的“指手画脚”反而可能干扰它。
|
||||
@@ -356,7 +356,7 @@ AI 最容易犯的毛病就是**不懂装懂**。
|
||||
|
||||
我们推荐使用 [SiliconFlow Playground](https://cloud.siliconflow.com/me/playground/chat)(或任何你习惯的 LLM 平台),按照下面的**3 个挑战**来验证你学到的技巧。
|
||||
|
||||

|
||||

|
||||
|
||||
> **💡 操作提示**:点击右侧侧边栏的 "Add Model for Comparison",可以左右分屏对比两个模型(比如 Qwen-Max vs Llama-3)对同一个 Prompt 的反应。
|
||||
|
||||
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 263 KiB |
|
Before Width: | Height: | Size: 319 KiB After Width: | Height: | Size: 319 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 788 KiB After Width: | Height: | Size: 788 KiB |