Merge branch 'dev'

This commit is contained in:
sanbuphy
2026-02-18 16:05:04 +08:00
567 changed files with 44988 additions and 18499 deletions
@@ -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 + 15 枚)
**生活类比**:登山时,每次都选最陡的路往上走。虽然不一定能到最高峰,但通常能到不错的位置。
:::
### 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 💡 字符编码的演变
**ASCII1963年)**
- 用 7 位二进制表示 128 个字符
- 包括英文字母、数字、常用符号
- 问题:只能表示英语,无法表示中文等
**Unicode1991年)**
- 统一编码标准,覆盖世界上所有文字
- 目前已收录超过 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
- HDD100-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. JavaScriptWeb 开发必备)
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 BC = 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

@@ -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(() => {
为了方便大家理解每个选项的含义,在这里我们对菜单栏进行深入解析:
![](images/index-2026-01-09-11-35-55.png)
![](editors-and-ai/images/index-2026-01-09-11-35-55.png)
![](images/index-2026-01-09-11-36-23.png)
![](editors-and-ai/images/index-2026-01-09-11-36-23.png)
<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
**URLUniform 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 默认 80HTTPS 默认 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 分层查询
**DNSDomain 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 三次握手
**TCPTransmission 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 协议通信
**HTTPHyperText 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 字节流,将其解析为**DOMDocument 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(内联、外部文件),构建**CSSOMCSS 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**。
**MPAMulti-Page Application**
- 点一个链接 → 整页刷新 → 显示新页面
- 就像**翻书**:每翻一页都要把旧书合上、去书架拿新书
**SPASingle-Page Application**
- 点一个链接 → 只刷新内容区域 → 页面不刷新
- 就像**同一本书里换章节**:只擦掉旧内容、写上新内容
👇 **动手试试看**:体验 MPA 和 SPA 的区别
<RoutingModeDemo />
**SPA 的优点**
-**体验丝滑**:页面切换快
-**状态好管理**:输入的内容、滚动位置都在
-**首屏可能慢**:需要先下载 JavaScript
-**SEO 要额外处理**:搜索引擎可能抓不到内容(需要 SSR/SSG)
---
---
## 5. 渲染策略:从 CSR 到 SSR/SSG
::: tip 🤔 核心问题
**页面是在服务器生成,还是在浏览器生成?** 不同渲染策略各有优劣,选择合适的策略对性能和 SEO 至关重要。
:::
**CSRClient-Side Rendering)客户端渲染**
- 浏览器下载 JavaScript → 执行代码 → 生成页面
- 优点:交互流畅,服务器压力小
- 缺点:首屏慢,不利于 SEO
**SSRServer-Side Rendering)服务端渲染**
- 服务器生成 HTML → 发给浏览器 → 浏览器直接显示
- 优点:首屏快,利于 SEO
- 缺点:服务器压力大,实现复杂
**SSGStatic 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 ReactUI 库
**核心理念**:只负责视图层,其他问题交给社区
```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 的博客网站,用 NuxtVue 的 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**
祝你学习愉快! 🎨
File diff suppressed because it is too large Load Diff
@@ -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) // 10a 不受影响)
```
**对象和数组赋值 = 复制的是"地址"(指向同一个东西):**
```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 DOMJavaScript 看到的网页
网页在 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[] = ["张三", "李四", "王五"]
// 写法 2Array<类型>
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** | - | 可观测性,利用日志/指标/追踪理解系统运行状态 |
+3
View File
@@ -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"。本章将带你从零开始理解数据库的核心原理。
:::
+3
View File
@@ -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`(管理员账号)。
- **登录密码**:初始密码,或者是让你设置密码的链接。
有了这些信息,就可以用 **SSHSecure 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"heymy-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)。
**HTTPHyperText Transfer Protocol** 是用来传输网页的协议。可以把它理解成运输数据的卡车。但这辆卡车是**透明的**。里面装的东西所有人都能看见。在 HTTP 网站上输入的密码、填写的个人信息。在传输过程中可能被中间的任何人偷看到。
**HTTPSHTTP 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 一家的。
- **ELKElasticsearch + 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天后自动失效 |
| 访客 | 前台配一把钥匙给他 | 发一次性访客码,只能进会议室 |
**IAMIdentity 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:
MFAMulti-Factor Authentication,多因素认证),也叫 2FATwo-Factor Authentication,双因素认证),是一种安全机制,要求用户在登录时提供**两种或以上**不同类型的认证因素:
| 因素类型 | 是什么 | 例子 |
| :--- | :--- | :--- |
| **知识因素**(你知道什么) | 只有用户知道的信息 | 密码、PIN 码 |
| **持有因素**(你有什么) | 用户拥有的物理设备 | 手机、硬件密钥 |
| **生物因素**(你是什么) | 用户的生物特征 | 指纹、面部识别 |
| 因素类型 | 是什么 | 例子 |
| :------------------------- | :----------------- | :------------- |
| **知识因素**(你知道什么) | 只有用户知道的信息 | 密码、PIN 码 |
| **持有因素**(你有什么) | 用户拥有的物理设备 | 手机、硬件密钥 |
| **生物因素**(你是什么) | 用户的生物特征 | 指纹、面部识别 |
### 5.2 为什么 MFA 这么重要?
**真实数据告诉你答案**
| 攻击方式 | 没有 MFA 时的成功率 | 有 MFA 时的成功率 |
| :--- | :--- | :--- |
| 密码猜测/暴力破解 | 很高 | 极低(还需要第二因素) |
| 钓鱼攻击获取密码 | 很高 | 极低(钓鱼页面无法获取 MFA 码) |
| 密码泄露(其他网站泄露)| 很高 | 极低(不知道第二因素) |
| 攻击方式 | 没有 MFA 时的成功率 | 有 MFA 时的成功率 |
| :----------------------- | :------------------ | :------------------------------ |
| 密码猜测/暴力破解 | 很高 | 极低(还需要第二因素) |
| 钓鱼攻击获取密码 | 很高 | 极低(钓鱼页面无法获取 MFA 码) |
| 密码泄露(其他网站泄露) | 很高 | 极低(不知道第二因素) |
**微软安全报告(2020)**:启用 MFA 可以阻止 **99.9%** 的自动化攻击。
@@ -441,14 +437,14 @@ MFAMulti-Factor Authentication,多因素认证),也叫 2FATwo-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 @@ MFAMulti-Factor Authentication,多因素认证),也叫 2FATwo-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 1DNS 解析**(智能调度)
```
用户输入: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 @@ TTLTime 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. UAUser-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),它不
当我们谈论“工程”时,我们强调的是:**可复现、可验证、可转移**。
![](images/image7.png)
![](prompt-engineering/images/image7.png)
AI 模型像一个**黑盒子**:我们知道输入(提示词)和输出(回答),但很难完全掌控中间发生了什么。
@@ -69,7 +69,7 @@ AI 模型像一个**黑盒子**:我们知道输入(提示词)和输出(
大多数传统大模型(如 GPT-3.5, Llama 2)属于此类。它们**直觉式地反应**,说完上句接下句,不做深层逻辑推演。
![](images/image14.png)
![](prompt-engineering/images/image14.png)
- **特点**:快,但容易在复杂逻辑上犯错。
- **策略**:需要你把步骤拆解得非常细(Chain of Thought),一步步喂给它。
@@ -78,7 +78,7 @@ AI 模型像一个**黑盒子**:我们知道输入(提示词)和输出(
新一代模型(如 o1, R1)在回答前会进行“隐式推理”。
![](images/image13.png)
![](prompt-engineering/images/image13.png)
- **特点**:慢,但逻辑能力强,能自我纠错。
- **策略**:通常不需要复杂的 Prompt 技巧,直接说清楚目标即可,过多的“指手画脚”反而可能干扰它。
@@ -356,7 +356,7 @@ AI 最容易犯的毛病就是**不懂装懂**。
我们推荐使用 [SiliconFlow Playground](https://cloud.siliconflow.com/me/playground/chat)(或任何你习惯的 LLM 平台),按照下面的**3 个挑战**来验证你学到的技巧。
![](images/image15.png)
![](prompt-engineering/images/image15.png)
> **💡 操作提示**:点击右侧侧边栏的 "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

Some files were not shown because too many files have changed in this diff Show More