From 7b271af6ead4e7bb6b913a9100112168ef7a5391 Mon Sep 17 00:00:00 2001 From: wonder Date: Mon, 20 Apr 2026 22:47:51 +0800 Subject: [PATCH] Update --- .gitignore | 19 + CS/DB/README.md | 52 + CS/NET/Authorization.md | 553 +++++++++++ CS/NET/HeavyKeeper.md | 349 +++++++ CS/NET/README.md | 45 + CS/NET/网络协议分析基础.md | 208 ++++ CS/OS/README.md | 48 + CS/OS/TCP 协议详解.md | 0 CS/README.md | 37 + CS/TOOLS/README.md | 51 + CS/TOOLS/网络抓包.md | 576 +++++++++++ DEV/DEBUG/调试技巧总结.md | 0 DEV/GO/README.md | 75 ++ DEV/REACT/README.md | 81 ++ DEV/VITE/README.md | 244 +++++ DEV/VITE/优化实践.md | 722 ++++++++++++++ DEV/VITE/架构原理.md | 903 ++++++++++++++++++ DEV/VITE/配置详解.md | 695 ++++++++++++++ assets/img/Pasted image 20260417125039.png | Bin 0 -> 49989 bytes assets/img/Pasted image 20260417125713.png | Bin 0 -> 63250 bytes config/agent/file-creation-standards.md | 63 ++ config/agent/learning-index.md | 33 + config/plugin/插件配置.md | 55 ++ config/template/default-template.md | 18 + 金山办公作业/Week05/Docker 命名卷 vs 挂载.md | 276 ++++++ 金山办公作业/Week05/Nginx.md | 203 ++++ 金山办公作业/Week05/PLAN.md | 512 ++++++++++ 金山办公作业/Week05/Slide.md | 63 ++ .../Week05/全栈开发实战:AI 智能单词本.md | 105 ++ 金山办公作业/Week05/分页.md | 610 ++++++++++++ 金山办公作业/Week05/数据库设计.md | 197 ++++ 金山办公作业/Week05/用户ID.md | 357 +++++++ 金山办公作业/Week05/用户认证.md | 438 +++++++++ 33 files changed, 7588 insertions(+) create mode 100644 .gitignore create mode 100644 CS/DB/README.md create mode 100644 CS/NET/Authorization.md create mode 100644 CS/NET/HeavyKeeper.md create mode 100644 CS/NET/README.md create mode 100644 CS/NET/网络协议分析基础.md create mode 100644 CS/OS/README.md create mode 100644 CS/OS/TCP 协议详解.md create mode 100644 CS/README.md create mode 100644 CS/TOOLS/README.md create mode 100644 CS/TOOLS/网络抓包.md create mode 100644 DEV/DEBUG/调试技巧总结.md create mode 100644 DEV/GO/README.md create mode 100644 DEV/REACT/README.md create mode 100644 DEV/VITE/README.md create mode 100644 DEV/VITE/优化实践.md create mode 100644 DEV/VITE/架构原理.md create mode 100644 DEV/VITE/配置详解.md create mode 100644 assets/img/Pasted image 20260417125039.png create mode 100644 assets/img/Pasted image 20260417125713.png create mode 100644 config/agent/file-creation-standards.md create mode 100644 config/agent/learning-index.md create mode 100644 config/plugin/插件配置.md create mode 100644 config/template/default-template.md create mode 100644 金山办公作业/Week05/Docker 命名卷 vs 挂载.md create mode 100644 金山办公作业/Week05/Nginx.md create mode 100644 金山办公作业/Week05/PLAN.md create mode 100644 金山办公作业/Week05/Slide.md create mode 100644 金山办公作业/Week05/全栈开发实战:AI 智能单词本.md create mode 100644 金山办公作业/Week05/分页.md create mode 100644 金山办公作业/Week05/数据库设计.md create mode 100644 金山办公作业/Week05/用户ID.md create mode 100644 金山办公作业/Week05/用户认证.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..828474f --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Obsidian 不需要版本控制的文件 +.obsidian/ + +# Claude Code 相关 +.claudian/ +.claude/ + + +# 临时文件 +*.tmp +*.temp +.DS_Store +Thumbs.db + +# 可选:备份文件(根据需要调整) +*.bak + +# 附件文件夹如果很大,可以排除或使用 Git LFS +# attachments/ diff --git a/CS/DB/README.md b/CS/DB/README.md new file mode 100644 index 0000000..4bef8bf --- /dev/null +++ b/CS/DB/README.md @@ -0,0 +1,52 @@ +--- +tags: [CS, DB, database] +create time: 2026-04-17 +--- + +# 数据库 + +## 概述 + +数据库是有组织的数据集合,数据库管理系统(DBMS)是一套管理数据的软件系统,用于创建、维护和访问数据库。 + +## 核心概念 + +### 关系型数据库 +- SQL语言 +- 表结构设计 +- 索引与查询优化 +- 事务处理(ACID) +- 规范化理论 + +### NoSQL数据库 +- 文档型数据库(MongoDB) +- 键值数据库(Redis) +- 列族数据库(Cassandra) +- 图数据库(GraphDB) + +### 数据库设计 +- 实体关系模型(ER) +- 数据建模 +- 性能调优 +- 分库分表 + +### ACID特性 +- 原子性(Atomicity) +- 一致性(Consistency) +- 隔离性(Isolation) +- 持久性(Durability) + +## 学习重点 + +- SQL语言精通 +- 数据库选型与设计 +- 查询性能优化 +- 事务处理机制 +> 数据库备份与恢复 + +## 实践建议 + +- 在实际项目中应用 +- 使用不同类型数据库 +- 学习SQL调优技巧 +- 理解分布式数据库 diff --git a/CS/NET/Authorization.md b/CS/NET/Authorization.md new file mode 100644 index 0000000..1137f91 --- /dev/null +++ b/CS/NET/Authorization.md @@ -0,0 +1,553 @@ +--- +tags: [CS, NET, security, authorization, web, authentication-flow] +create time: 2026-04-17 +--- + +# Web Application Authorization Flow + +## 概述 + +**Authorization(授权)** 在 Web 应用中是 HTTP 层面的认证令牌验证机制。本文聚焦浏览器、服务器、Token 的交互流程和加密验证过程。 + +## HTTP Authorization 流程 + +### 完整授权流程 + +```mermaid +sequenceDiagram + participant B as Browser + participant S as Server + participant DB as Database + + Note over B,S: 1. 登录获取 Token + B->>S: POST /login (username, password) + S->>DB: 验证凭证 + DB-->>S: 用户数据 + S->>S: HMAC-SHA256(payload, secret) + S-->>B: {access_token: "eyJhbGc...", expires_in: 3600} + + Note over B,S: 2. 带授权头访问资源 + B->>B: 存储 Token (LocalStorage/Memory) + B->>S: GET /api/resource + Note right of B: Authorization: Bearer eyJhbGc... + + Note over S: 3. 验证 Token + S->>S: 提取 Token + S->>S: 验证签名 (HMAC) + S->>S: 检查过期时间 + S->>S: 验证 Issuer/Audience + S->>DB: 根据 user_id 查询权限 + + Note over S: 4. 授权决策 + alt Token 有效 + S-->>B: 200 OK + 数据 + else Token 无效 + S-->>B: 401 Unauthorized + else 权限不足 + S-->>B: 403 Forbidden + end +``` + +### JWT Token 验证过程 + +```mermaid +graph TD + A[接收 Authorization: Bearer ] --> B[解析三部分] + B --> C[Header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9] + B --> D[Payload: eyJ1c2VyX2lkIjoiMTIzIiw...] + B --> E[Signature: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c] + + C --> F[获取签名算法 HS256] + D --> G[获取用户数据] + E --> H[签名部分] + + F --> I[重新计算签名] + G --> I + H --> I + + I --> J{签名匹配?} + J -->|是| K[检查过期时间 exp] + J -->|否| L[拒绝: 401 Unauthorized] + + K -->|未过期| M[检查 nbf 时间] + K -->|已过期| L + + M -->|有效| N[检查 iss 签发者] + M -->|无效| L + + N -->|匹配| O[验证通过] + N -->|不匹配| L + + O --> P[从 payload 提取 user_id] + P --> Q[查询用户权限] + Q --> R[返回受保护资源] +``` + +## 浏览器行为详解 + +### 浏览器如何处理 Authorization + +```mermaid +graph LR + A[用户请求资源] --> B[检查 Token] + B --> C{Token 存在于?} + + C -->|LocalStorage| D[从 LS 读取] + C -->|SessionStorage| E[从 SS 读取] + C -->|Memory| F[从变量读取] + C -->|Cookie| G[浏览器自动发送] + + D --> H[构造 Authorization Header] + E --> H + F --> H + G --> I[无需手动添加*] + + H --> J[发起 XHR/Fetch 请求] + I --> J + + J --> K[设置 headers] + K --> L[发送到服务器] + + style G fill:#ff9999 + style H fill:#90EE90 + style I fill:#90EE90 + + subgraph 注 + G1[Cookie 方式需要 SameSite 属性] + G2[防止 CSRF 攻击] + end + + G1 -.-> G + G2 -.-> G +``` + +**浏览器自动发送 Cookie,但需要手动添加 Bearer Token** + +### 浏览器存储 Token 的方式 + +| 存储方式 | 特点 | 安全性 | 跨域访问 | +|---------|------|--------|---------| +| LocalStorage | 持久化,手动管理 | ❌ XSS 风险 | ✅ 支持 | +| SessionStorage | 会话结束清除 | ❌ XSS 风险 | ❌ 仅同源 | +| Memory | 页面刷新丢失 | ✅ 最安全 | ❌ 仅当前页面 | +| Cookie | 可设置 HttpOnly | ✅ 防 XSS | ⚠️ 需 SameSite | + +### 前端实现 (JavaScript) + +```javascript +// 1. 登录后存储 Token +async function login(username, password) { + const response = await fetch('/api/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }); + + const { access_token } = await response.json(); + localStorage.setItem('token', access_token); +} + +// 2. 带认证头访问 API +async function fetchProtectedResource() { + const token = localStorage.getItem('token'); + + const response = await fetch('/api/resource', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.status === 401) { + // Token 过期,需要刷新或重新登录 + handleTokenExpired(); + } + + return response.json(); +} + +// 3. Axios 拦截器自动添加 Token +axios.interceptors.request.use(config => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); +``` + +## 加密与签名机制 + +### JWT 签名算法对比 + +| 算法类型 | 算法 | 密钥类型 | 特点 | +| ----- | ----- | ----- | ------------------- | +| HMAC | HS256 | 对称密钥 | 服务器签发和验证都 uses 同一密钥 | +| HMAC | HS512 | 对称密钥 | 更强的哈希,性能稍低 | +| RSA | RS256 | 非对称密钥 | 私钥签名,公钥验证 | +| RSA | RS512 | 非对称密钥 | 更强的签名算法 | +| ECDSA | ES256 | 椭圆曲线 | 比更短但同样安全 | + +### HMAC-SHA256 签名流程 + +```mermaid +graph TD + A[原始数据] --> B[Base64URL 编码 Header] + A --> C[Base64URL 编码 Payload] + + B --> D[拼接 Header.Payload] + C --> D + + D --> E[HMAC-SHA256 拼接结果] + E --> F[使用 Secret Key] + F --> G[生成 256-bit 签名] + + G --> H[Base64URL 编码签名] + H --> I[拼接 final JWT] + + I --> J[Header.Payload.Signature] + + style E fill:#ff9999 + style F fill:#ff9999 + style G fill:#lightblue +``` + +**签名 = HMAC-SHA256(Base64URL(Header) + "." + Base64URL(Payload), Secret)** + +### Go 实现签名与验证 + +```go +package auth + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "strings" +) + +type JWTBuilder struct { + secretKey []byte +} + +// 生成签名 +func (j *JWTBuilder) sign(header, payload string) string { + data := strings.Join([]string{header, payload}, ".") + h := hmac.New(sha256.New, j.secretKey) + h.Write([]byte(data)) + signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) + return signature +} + +// 验证签名 +func (j *JWTBuilder) verify(token string) bool { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return false + } + + header, payload, signature := parts[0], parts[1], parts[2] + + // 重新计算签名 + expectedSignature := j.sign(header, payload) + + // 比较签名 (恒定时间比较,防止计时攻击) + return hmac.Equal([]byte(signature), []byte(expectedSignature)) +} +``` + +## 服务器验证流程 + +### 详细的验证步骤 + +```go +package middleware + +import ( + "net/http" + "encoding/base64" + "encoding/json" + "time" +) + +type Claims struct { + UserID string `json:"user_id"` + exp int64 `json:"exp"` + iat int64 `json:"iat"` + iss string `json:"iss"` + aud string `json:"aud"` +} + +func (m *AuthMiddleware) validateToken(token string) (*Claims, error) { + // 步骤 1: 分割三部分 + parts := strings.Split(token, ".") + if len(parts) != 3 { + return nil, fmt.Errorf("invalid token format") + } + + // 步骤 2: 验证签名 + if !m.verifySignature(token) { + return nil, fmt.Errorf("invalid signature") + } + + // 步骤 3: 解码 Payload + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return nil, fmt.Errorf("invalid payload encoding") + } + + // 步骤 4: 解析 Claims + var claims Claims + if err := json.Unmarshal(payload, &claims); err != nil { + return nil, fmt.Errorf("invalid payload json") + } + + // 步骤 5: 验证过期时间 + if time.Now().Unix() > claims.exp { + return nil, fmt.Errorf("token expired") + } + + // 步骤 6: 验证签发者 + if claims.iss != "my-app" { + return nil, fmt.Errorf("invalid issuer") + } + + // 步骤 7: 验证受众 + if claims.aud != "api-users" { + return nil, fmt.Errorf("invalid audience") + } + + return &claims, nil +} + +// 验证签名 +func (m *AuthMiddleware) verifySignature(token string) bool { + parts := strings.Split(token, ".") + signatureData := parts[0] + "." + parts[1] + providedSignature := parts[2] + + // 计算期望的签名 + expectedSignature := m.sign(signatureData) + + // 恒定时间比较 + return hmac.Equal( + []byte(providedSignature), + []byte(expectedSignature), + ) +} +``` + +### HTTP 中间件 + +```go +// AuthMiddleware 验证每个请求 +func (m *AuthMiddleware) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // 1. 提取 Authorization 头 + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Missing Authorization header", http.StatusUnauthorized) + return + } + + // 2. 解析 Bearer Token + if !strings.HasPrefix(authHeader, "Bearer ") { + http.Error(w, "Invalid authorization scheme", http.StatusUnauthorized) + return + } + token := strings.TrimPrefix(authHeader, "Bearer ") + + // 3. 验证 Token + claims, err := m.validateToken(token) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + // 4. 将用户信息存入 Context + ctx := context.WithValue(r.Context(), "user_id", claims.UserID) + ctx = context.WithValue(ctx, "claims", claims) + + // 5. 继续处理请求 + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +``` + +## 常见 Authorization Scheme + +### Basic Auth(基础认证) + +```mermaid +sequenceDiagram + participant B as Browser + participant S as Server + participant DB as Database + + B->>S: GET /resource + S-->>B: 401 Unauthorized + Note right of B: WWW-Authenticate: Basic realm="Secure Area" + + B->>B: 用户输入用户名密码 + B->>B: Base64("username:password") + B->>S: GET /resource + Note right of B: Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l + + S->>B: 解码 Base64 + S->>DB: 验证用户名密码 + DB-->>S: 验证结果 + S-->>B: 200 OK + 资源 +``` + +**问题**:每次请求都需要用户名密码,不安全,不推荐 + +### Digest Auth(摘要认证) + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + + C->>S: GET /resource + S-->>C: 401 Unauthorized + Note right of C: WWW-Authenticate: Digest
realm="Protected",
nonce="xyz123",
qop="auth" + + C->>C: 计算 response
MD5(username:realm:password)
MD5(method:uri)
HA1 = MD5(username:realm:password)
HA2 = MD5(method:uri)
response = MD5(HA1:nonce:HA2) + + C->>S: GET /resource + Note right of C: Authorization: Digest
username="user",
realm="Protected",
nonce="xyz123",
uri="/resource",
response="abc123" + + S->>S: 重新计算 response + S-->>C: 200 OK +``` + +**优点**:密码不直接传输,更安全 +**缺点**:实现复杂,需要多次往返 + +### Bearer Token(持有者令牌) + +✅ **现代 Web 应用的标准选择** + +```http +# 请求示例 +GET /api/user/profile HTTP/1.1 +Host: api.example.com +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIiwidXNlcm5hbWUiOiJqb2huIiwiZXhwIjoxNzI4MjQ4MDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + +# 响应示例 +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "user_id": "123", + "username": "john", + "email": "john@example.com" +} +``` + +## Refresh Token 机制 + +### Access Token vs Refresh Token + +```mermaid +graph LR + A[Access Token] --> B[有效期短
15-30分钟] + A --> C[用于 API 访问] + A --> D[存储在浏览器] + + E[Refresh Token] --> F[有效期长
数天至数周] + E --> G[用于获取新 Access Token] + E --> H[存储在 HttpOnly Cookie] + + style A fill:#90EE90 + style E fill:#ffcc00 +``` + +### Token 刷新流程 + +```mermaid +sequenceDiagram + participant B as Browser + participant S as Server + + Note over B,S: Access Token 过期 + B->>S: POST /api/refresh + Note right of B: Body: {refresh_token: "..."} + + S->>S: 验证 Refresh Token + S->>S: 生成新的 Access Token + S-->>B: {access_token: "new_jwt..."} + + Note over B,S: 使用新 Token 继续请求 + B->>S: GET /api/resource + Note right of B: Authorization: Bearer new_jwt... + + S-->>B: 200 OK +``` + +**Refresh Token 优势**: +- ✅ Access Token 短期有效,减少泄露风险 +- ✅ Refresh Token 可随时撤销 +- ✅ 用户无感知自动续期 + +## 安全最佳实践 + +### Token 安全传输 + +| 措施 | 说明 | Go 实现 | +|-----|------|---------| +| **HTTPS 强制** | 防止 Token 被窃取 | RedirectHTTPS 中间件 | +| **短期有效期** | 降低被滥用风险 | Token 15-30 分钟 | +| **签名验证** | 防止 Token 被篡改 | HMAC/RSA 签名 | +| **黑名单机制** | 主动撤销 Token | Redis 存储 revoked_tokens | + +### 浏览器安全设置 + +```javascript +// ✅ 推荐:HttpOnly Cookie 存储 Refresh Token +document.cookie = `refresh_token=${refreshToken}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=604800`; + +// ⚠️ 谨慎:LocalStorage 存储 Access Token +localStorage.setItem('access_token', accessToken); +// 需要 XSS 防护 + +// ❌ 避免:明文传输 Token +// 必须使用 HTTPS +``` + +### CORS 和 SameSite + +```mermaid +graph TD + A[前端应用
example.com] --> B[后端 API
api.example.com] + + B --> C[CORS 策略] + C --> D[Access-Control-Allow-Origin: https://example.com] + C --> E[Access-Control-Allow-Credentials: true] + + B --> F[SameSite Cookie] + F --> G[Strict: 严格模式] + F --> H[Lax: 放宽模式] + + style D fill:#90EE90 + style G fill:#90EE90 +``` + +**Go CORS 配置**: +```go +func setupCORS() *cors.Cors { + return cors.New(cors.Options{ + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, + AllowedHeaders: []string{"Authorization", "Content-Type"}, + AllowCredentials: true, + MaxAge: 3600, + }) +} +``` + +## 相关笔记 + +- [[金山办公作业/Week05/用户认证]] - 认证的实现方式 +- [[CS/NET/HTTPS]] - SSL/TLS 加密传输 +- [[CS/DB/访问控制]] - 数据库层面权限 diff --git a/CS/NET/HeavyKeeper.md b/CS/NET/HeavyKeeper.md new file mode 100644 index 0000000..92b2f70 --- /dev/null +++ b/CS/NET/HeavyKeeper.md @@ -0,0 +1,349 @@ +--- +tags: [CS, NET, algorithm, data-stream, heavy-hitter] +create time: 2026-04-17 13:45 +--- + +# HeavyKeeper + +## 概述 + +HeavyKeeper 是一种用于高速数据流中检测 Heavy Hitters(频繁项)的高效算法。它在保持常数内存空间的同时,能够准确地识别出出现频率超过设定阈值的数据项,广泛应用于网络流量监测、热点检测等场景。 + +## 算法原理 + +### 核心思想 + +HeavyKeeper 结合了 Count-Min Sketch 和守桶策略,通过多层哈希守桶机制来提高准确性。其核心目标是区分"大象流"和"老鼠流"。 + +#### 什么是大象流和老鼠流? + +在数据流分析中,通常将数据流按频率分为两类: + +| 类型 | 特征 | 频率占比 | 典型例子 | +|------|------|----------|---------| +| **大象流**(Elephant Flow) | 高频出现 | 占总流量的大部分 | 热门IP、热门搜索词、DDoS攻击流量 | +| **老鼠流**(Mouse Flow) | 低频偶发 | 数量众多但频率极低 | 少量用户访问、正常连接请求 | + +**关键洞察**:在很多场景中,**80-90%的流量来自不到1%的源**,这就是大象流。HeavyKeeper 的目标就是高效识别这些大象流,过滤掉老鼠流。 + +### 守桶机制 + +#### 什么是"守桶"? + +"守桶"(Keeper)是 HeavyKeeper 的核心创新。每个桶会"守护"一个特定的数据项: + +- 当数据流中的一个项到来时,哈希到某个桶 +- 如果这个项正好是该桶"守护"的项,就直接计数 +- 如果不是,则根据概率决定是否"抢夺"守护权 + +**底层原理**:让大象流(高频项)能够长期占据守桶位置,而老鼠流(低频项)很难长期占用桶的资源。 + +#### 桶的结构 + +每个桶维护以下信息: + +| 字段 | 类型 | 说明 | +|------|------|------| +| **item** | 数据项 | 当前守护的数据项 | +| **count** | 整数 | 守护项的精确计数 | +| **error** | 整数 | 误差估计(记录非守护项经过的次数) | + +#### 守桶策略:大象流如何压制老鼠流 + +**替换概率公式**: +```python +替换概率 = min(1, 新项估计频率 / 当前守护项计数) +``` + +这个公式的直观含义: + +| 情况 | 新项类型 | 替换概率 | 结果 | +|------|---------|---------|------| +| 大象流 vs 老鼠流 | 老鼠流(freq≈1) | 1/count | 极小,**老鼠流无法撼动大象流** | +| 老鼠流 vs 老鼠流 | 老鼠流(freq≈2) | 2/count | 较小,随机性强 | +| 大象流 vs 老鼠流 | 大象流(freq=50) | 50/5=1 | 必然替换,**新大象流抢占桶** | +| 大象流 vs 大象流 | 大象流(freq=98) | 98/95≈1 | 可能替换,两个大象流竞争 | + +**例子**: +``` +桶#100 当前守护:IP=10.0.0.1 (count=100, error=5) ← 大象流 +新到来:IP=10.0.0.2 → 哈希到桶#100 ← 老鼠流 + +替换概率 = min(1, 1/100) = 0.01 + +结果:0.95(随机数)> 0.01 → 不替换 +解释:大象流继续守护,老鼠流只能默默增加error +``` + +#### 多层哈希的作用 + +单层哈希可能发生冲突(多个项哈希到同一个桶),多层哈希通过冗余来解决: + +- 同一个项会由L个不同的哈希函数映射到L层的不同桶 +- 查询时取所有层的最小值(保守估计) +- 即使部分桶冲突,也能获得准确的下界 + +**最终计数** = min(所有层中该项的count值) + +#### Heavy Hitters 检测流程 + +```mermaid +flowchart TD + A[数据流新项 x] --> B[计算L个哈希] + B --> C[访问L个桶] + + C --> D{是否为守护项?} + D -->|是| E[count++] + D -->|否| F[计算替换概率] + + F --> G{触发替换?} + G -->|是| H[替换并重置count=1] + G -->|否| I[error++] + + E --> J[继续] + H --> J + I --> J +``` + +查找Top K时,只需遍历所有桶,收集 `count ≥ 阈值` 的候选项。 + +### 衰减机制 + +#### 为什么需要衰减? + +**问题场景**: +``` +10:00-10:05 IP=10.0.0.1 出现 1000 次 → 成为大象流,占据桶 +10:06-12:00 IP=10.0.0.1 不再出现,但其count=1000依然存在 +12:01 IP=10.0.0.2 频繁出现,但无法抢占count=1000的桶 +``` + +如果不衰减,过时的大象流会持续占用资源,阻碍新大象流的检测。 + +#### 衰减机制的工作原理 + +HeavyKeeper 通过周期性衰减来解决这个问题: + +**方法1:时间窗口衰减(推荐)** +```python +def periodic_decay(): + 每经过 Δt 时间,所有桶的 count 和 error 乘以衰减因子 α + count = count × α + error = error × α + 其中 α ∈ (0, 1),通常 α = 0.9 或 0.99 +``` + +**方法2:基于老化(Aging)** +```python +def aging(bucket, current_time): + elapsed = current_time - bucket.last_update_time + decay = exp(-λ × elapsed) # λ 是衰减速率 + bucket.count = bucket.count × decay +``` + +#### 衰减机制的数学效果 + +**时间窗口视角**: +``` +衰减因子 α = 0.99,窗口大小 = N + +N时刻前的权重:0.99^N ≈ 0.366 ← 仅保留36.6% +2N时刻前的权重:0.99^2N ≈ 0.134 ← 只保留13.4% +``` + +这意味着:越久远的计数对当前统计影响越小,让算法能够"遗忘"过时的流。 + +#### 衰减规则示例 + +| 场景 | 原 count | 衰减后 count | 解析 | +|------|----------|-------------|------| +| 持续活跃的大象流 | 1000 | 990 (×0.99) | 持续补充,衰减不影响地位 | +| 最近消失的大象流 | 1000 | 366 (×0.99^100) | 100个周期后快速衰减,让出桶 | +| 新大象流 | 0 → 10 | 10 (刚开始) | 有机会竞争已衰减的桶 | + +#### 衰减带来的好处 + +| 优势 | 说明 | +|------|------| +| **自适应流行度漂移** | 热点变化时,旧热点会自动失去守桶权 | +| **滑动窗口效果** | 只关注最近时间窗口内的频率,而非历史总和 | +| **防止资源垄断** | 过时的大象流不会长期占用桶资源 | + +#### 衰减与守桶的协同 + +衰减机制和守桶机制协同工作,形成一个动态平衡: + +```mermaid +graph LR + A[大象流活跃] --> B[count快速累积] + B --> C[占据守桶位置] + + C --> D{时间流逝} + D -->|持续活跃| E[保持守桶] + D -->|停止活跃| F[衰减降低count] + + F --> G{新竞争者?} + G -->|有| H[被替换,让出桶] + G -->|无| I[继续衰减直至清理] +``` + +## 关键参数 + +| 参数 | 说明 | 典型值 | +|------|------|--------| +| **m** | 每层桶的数量 | 2^15 ~ 2^20 | +| **L** | 哈希层数 | 3 ~ 5 | +| **θ** | 频率阈值 | 0.001 ~ 0.01 | +| **α** | 衰减因子 | 0.9 ~ 0.99 | +| **Δt** | 衰减周期 | 根据应用场景 | + +## 性能特征 + +### 空间复杂度 +- 空间复杂度: **O(m × L)** +- 每桶存储: item (~8字节) + count (~4字节) + error (~4字节) + +### 时间复杂度 + +| 操作 | 时间复杂度 | 说明 | +|------|-----------|------| +| **插入** | O(L) | 对每层进行哈希和更新 | +| **查询** | O(L) | 取所有层最小值 | +| **衰减** | O(m×L) | 批量处理所有桶 | + +## 优势与局限 + +### 优势 +- **大象流识别准确**: 守桶机制确保高频项持续占据资源 +- **老鼠流过滤**: 低频项很难干扰大象流统计 +- **自适应流行度变化**: 衰减机制处理热点漂移 +- **内存高效**: 恒定空间,不受数据流规模影响 + +### 局限性 +- **参数敏感**: m、L、α 等参数需要根据数据特征调优 +- **哈希冲突**: 极端情况下可能产生误报或漏报 +- **衰减延迟**: 热点切换时需要一定时间生效 + +## 应用场景 + +### 典型用例 + +1. **网络流量分析**: 识别高频 IP 地址或端口(大象流) +2. **DDoS 防护**: 检测异常高频流量,过滤老鼠流 +3. **实时推荐**: 发现用户偏好热点,利用衰减实现热点漂移 +4. **CDN 缓存**: 识别热门内容进行预加载 +5. **日志分析**: 快速定位高频错误或异常事件 + +### 实际部署考虑 + +| 场景 | 大象流示例 | 老鼠流示例 | 衰减建议 | +|------|-----------|-----------|---------| +| 网络带宽监控 | P2P下载、视频流 | 正常网页浏览 | 较慢衰减(α=0.99) | +| DDoS检测 | 攻击源IP | 正常用户IP | 快速衰减(α=0.9) | +| 搜索热门 | 热门关键词 | 长尾搜索 | 中等衰减(α=0.95) | + +## 参考实现 + +### 伪代码 + +```python +class HeavyKeeper: + def __init__(self, m, L, threshold, decay_factor): + self.m = m # 每层桶数 + self.L = L # 哈希层数 + self.threshold = threshold + self.decay_factor = decay_factor # 衰减因子 + + # 初始化多层 sketch + self.buckets = [[KeeperBucket() for _ in range(m)] + for _ in range(L)] + + # 初始化哈希函数 + self.hash_funcs = [get_hash_func(i) for i in range(L)] + + def insert(self, item, timestamp): + for layer in range(self.L): + idx = self.hash_funcs[layer](item) % self.m + bucket = self.buckets[layer][idx] + + if bucket.item == item: + # 守护项匹配,直接计数(大象流强化) + bucket.count += 1 + bucket.last_seen = timestamp + else: + # 计算替换概率 + estimated_freq = self._estimate_freq(item) + replace_prob = min(1, estimated_freq / bucket.count) + + if random.random() < replace_prob: + # 替换为新项(大象流夺权) + bucket.item = item + bucket.count = 1 + bucket.error = bucket.count + bucket.last_seen = timestamp + else: + # 不替换,仅增加误差(老鼠流被阻拦) + bucket.error += 1 + + def apply_decay(self, current_time): + """应用衰减机制""" + for layer in range(self.L): + for bucket in self.buckets[layer]: + elapsed = current_time - bucket.last_seen + if elapsed > DECAY_INTERVAL: + bucket.count *= self.decay_factor + bucket.error *= self.decay_factor + + # 归零清理 + if bucket.count < 1: + bucket.item = None + bucket.count = 0 + bucket.error = 0 + + def query(self, item): + """查询Item的频率估计""" + min_count = float('inf') + for layer in range(self.L): + idx = self.hash_funcs[layer](item) % self.m + bucket = self.buckets[layer][idx] + if bucket.item == item: + min_count = min(min_count, bucket.count) + + return min_count if min_count != float('inf') else 0 + + def get_top_k(self, k): + """获取Top K大象流""" + candidates = {} + for layer in range(self.L): + for bucket in self.buckets[layer]: + if bucket.count >= self.threshold and bucket.item: + item = bucket.item + candidates[item] = max(candidates.get(item, 0), + bucket.count) + + # 返回 Top-k + return sorted(candidates.items(), + key=lambda x: x[1], + reverse=True)[:k] +``` + +## 相关算法对比 + +| 算法 | 空间复杂度 | 大象流准确性 | 老鼠流过滤 | 衰减支持 | 适用场景 | +|------|-----------|------------|-----------|---------|---------| +| **HeavyKeeper** | O(m×L) | 高 | 优秀 | 原生支持 | 高速数据流,需检测热点漂移 | +| **Count-Min** | O(m×L) | 中 | 无 | 需额外实现 | 通用频率统计 | +| **SpaceSaving** | O(k) | 中 | 好 | 手动实现 | 固定数量Top-K | +| **LossyCounter** | O(kε) | 高 | 一般 | 手动实现 | 离线精确统计 | + +## 参考资料 + +- HeavyKeeper: Streaming Heavy Hitters Detection with Known Error Bounds (2020) +- Count-Min Sketch: An Improved Data Stream Summary +- Streaming Algorithms for Finding Heavy Hitters + +## 关联笔记 + +- [[CS/NET/Count-Min-Sketch.md]] +- [[CS/DS/HashMap.md]] +- [[CS/NET/Network-Flow-Analysis.md]] diff --git a/CS/NET/README.md b/CS/NET/README.md new file mode 100644 index 0000000..bdaf4ba --- /dev/null +++ b/CS/NET/README.md @@ -0,0 +1,45 @@ +--- +tags: [CS, NET, network] +create time: 2026-04-17 +--- + +# 计算机网络 + +## 概述 + +计算机网络是物理上分散的计算机通过通信线路连接起来,在网络软件的管理下实现资源共享和信息传递的系统。 + +## 核心概念 + +### OSI七层模型 +- 物理层、数据链路层、网络层 +- 传输层、会话层、表示层、应用层 + +### TCP/IP协议栈 +- IP协议、TCP/UDP协议 +- HTTP/HTTPS、DNS、SMTP等应用层协议 +- 网络地址与路由 + +### 网络架构 +- 客户端-服务器架构 +- P2P架构 +- 分布式系统 + +### 网络安全 +- 加密与解密 +- SSL/TLS +- 防火墙与VPN + +## 学习重点 + +- TCP/IP协议详解 +- HTTP协议与Web服务 +- 套接字编程 +- 网络性能优化 + +## 实践建议 + +- 使用Wireshark抓包分析 +- 编写网络应用 +- 搭建本地网络环境 +- 理解常见网络问题 diff --git a/CS/NET/网络协议分析基础.md b/CS/NET/网络协议分析基础.md new file mode 100644 index 0000000..ad4b779 --- /dev/null +++ b/CS/NET/网络协议分析基础.md @@ -0,0 +1,208 @@ +--- +tags: [network, protocol, theory, cs] +create time: 2026-04-17 12:15 +--- + +# 网络协议分析基础 + +## 概述 + +网络协议分析是理解网络通信的核心技术,通过分析数据包的结构和内容,可以深入理解网络协议的工作原理和通信机制。 + +## 正文 + +### TCP 三次握手与四次挥手 + +#### 三次握手 (SYN, SYN-ACK, ACK) + +``` +客户端 服务器 + | | + | --- SYN, seq=x --------------------> | + | | + | <--- SYN, ACK, seq=y, ack=x+1 ------ | + | | + | --- ACK, seq=x+1, ack=y+1 ---------> | + | | 连接建立 +``` + +**各字段含义:** +- **SYN**: 同步标志,用于建立连接 +- **ACK**: 确认标志,表示确认收到 +- **seq**: 序列号,确保数据有序传递 +- **ack**: 确认号,表示期望收到的下一个序列号 + +**抓包特征:** +1. 第一个包:SYN 标志位为 1,ACK 为 0 +2. 第二个包:SYN 和 ACK 都为 1 +3. 第三个包:ACK 为 1,SYN 为 0 + +#### 四次挥手 (FIN, ACK, FIN, ACK) + +``` +客户端 服务器 + | | + | --- FIN, seq=x --------------------> | 主动关闭 + | | + | <--- ACK, seq=y, ack=x+1 ----------- | + | | 半关闭 + | <--- FIN, seq=y ------------------- | 被动关闭 + | | + | --- ACK, seq=x+1, ack=y+1 ---------> | + | | 连接关闭 +``` + +**状态转换:** +- FIN_WAIT_1: 主动关闭方发送 FIN +- FIN_WAIT_2: 主动关闭方收到 ACK,等待对方 FIN +- CLOSE_WAIT: 被动关闭方收到 FIN,进入半关闭状态 +- LAST_ACK: 被动关闭方发送 FIN +- TIME_WAIT: 主动关闭方收到 FIN,等待 2MSL 后完全关闭 + +### HTTP 协议分析 + +#### 请求结构 + +``` +Method Request-URI HTTP-Version\r\n +Header-Name: Header-Value\r\n +\r\n +Message-Body +``` + +**常见方法:** +- **GET**: 获取资源 +- **POST**: 提交数据 +- **PUT**: 更新资源 +- **DELETE**: 删除资源 +- **HEAD**: 获取响应头 +- **OPTIONS**: 获取支持的方法 + +**常用请求头:** +``` +Host: example.com +User-Agent: Mozilla/5.0 +Accept: text/html,application/json +Content-Type: application/json +Authorization: Bearer token +Cookie: session=xxx +``` + +#### 响应结构 + +``` +HTTP-Version Status-Code Reason-Phrase\r\n +Header-Name: Header-Value\r\n +\r\n +Message-Body +``` + +**状态码分类:** +- **2xx**: 成功 (200 OK, 201 Created, 204 No Content) +- **3xx**: 重定向 (301 Moved Permanently, 302 Found, 304 Not Modified) +- **4xx**: 客户端错误 (400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found) +- **5xx**: 服务器错误 (500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable) + +**常用响应头:** +``` +Content-Type: application/json; charset=utf-8 +Content-Length: 1234 +Cache-Control: max-age=3600 +ETag: "abc123" +Set-Cookie: session=xxx; Path=/; HttpOnly +``` + +### TLS/SSL 加密通信 + +#### TLS 握手流程 + +```mermaid +sequenceDiagram + participant C as 客户端 + participant S as 服务器 + + C->>S: ClientHello
(支持的加密套件、随机数) + S-->>C: ServerHello
(选择的加密套件、随机数、证书) + S-->>C: Certificate
(服务器证书) + S-->>C: ServerHelloDone + + C->>S: ClientKeyExchange
(预主密钥,用服务器公钥加密) + C->>S: ChangeCipherSpec
(通知后续使用加密通信) + C->>S: Finished
(握手完成,加密验证) + + S-->>C: ChangeCipherSpec + S-->>C: Finished + + Note over C,S: 开始加密通信 +``` + +**关键概念:** +- **证书链**: 从服务器证书到根证书的信任链 +- **预主密钥**: 通过非对称加密传输,用于生成会话密钥 +- **会话密钥**: 通过预主密钥和双方随机数生成,用于对称加密 +- **SSL Pinning**: 客户端验证服务器证书,防止中间人攻击 + +### 数据包分析要点 + +#### 抓包指标 + +**性能指标:** +- **RTT (Round Trip Time)**: 往返时延 +- **吞吐量**: 单位时间传输的数据量 +- **丢包率**: 丢失的数据包比例 +- **重传率**: 重发数据包的比例 + +**连接指标:** +- **TCP 窗口大小**: 接收窗口和拥塞窗口 +- **连接状态**: ESTABLISHED, TIME_WAIT 等 +- **连接复用**: Keep-Alive, HTTP/2 连接复用 + +#### 过滤技巧 + +**基于协议过滤:** +``` +# HTTP/HTTPS +http or http2 or ssl + +# TCP 特定标志 +tcp.flags.syn == 1 # SYN 包 +tcp.flags.ack == 1 # ACK 包 +tcp.flags.fin == 1 # FIN 包 +tcp.flags.reset == 1 # RST 包 +``` + +**基于内容过滤:** +``` +# 特定 User-Agent +http.user_agent contains "Chrome" + +# 特定域名 +http.host == "example.com" + +# HTTP 错误 +http.response.code >= 400 + +# 请求体内容 +http.file_data contains "keyword" +``` + +**基于网络层过滤:** +``` +# 源/目标地址 +ip.src == 192.168.1.1 +ip.dst == 192.168.1.1 + +# 端口范围 +tcp.port >= 1024 and tcp.port <= 65535 +``` + +## 关联笔记 + +- [[CS/TOOLS/网络抓包]] +- [[CS/SECURITY/网络安全基础]] +- [[CS/OS/TCP 协议详解]] + +## 参考资源 + +- RFC 文档: https://www.rfc-editor.org/ +- Wireshark 指南: https://www.wireshark.org/docs/wsug_html_chunked/ diff --git a/CS/OS/README.md b/CS/OS/README.md new file mode 100644 index 0000000..43e1532 --- /dev/null +++ b/CS/OS/README.md @@ -0,0 +1,48 @@ +--- +tags: [CS, OS, operating-systems] +create time: 2026-04-17 +--- + +# 操作系统 + +## 概述 + +操作系统是管理计算机硬件与软件资源的系统软件。它是计算机系统中最基础的系统软件,为应用程序提供运行环境。 + +## 核心概念 + +### 进程管理 +- 进程与线程 +- 进程调度算法 +- 进程间通信(IPC) +- 同步与互斥 + +### 内存管理 +- 虚拟内存 +- 分页与分段 +- 内存分配与回收 +- 缓存机制 + +### 文件系统 +- 文件的存储结构 +- 目录管理 +- 文件操作接口 +- 权限管理 + +### I/O管理 +- 设备驱动 +- 中断处理 +- 缓冲区管理 + +## 学习重点 + +- Linux/Unix系统原理 +- 系统调用机制 +- 操作系统设计原则 +- 性能优化方法 + +## 实践建议 + +- 在Linux环境下进行实验 +- 使用系统调用的程序 +- 研究开源操作系统代码 diff --git a/CS/OS/TCP 协议详解.md b/CS/OS/TCP 协议详解.md new file mode 100644 index 0000000..e69de29 diff --git a/CS/README.md b/CS/README.md new file mode 100644 index 0000000..ef457c3 --- /dev/null +++ b/CS/README.md @@ -0,0 +1,37 @@ +--- +tags: [CS, foundations] +create time: 2026-04-17 +--- + +# 计算机科学 + +## 概述 + +计算机科学是研究计算机系统、软件设计、算法和数据处理的学科。本目录包含了计算机科学的基础理论知识和核心技术领域。 + +## 核心领域 + +### 操作系统 ([[OS|OS]]) +- 进程管理、内存管理、文件系统 +- 学习目标: 理解计算机系统如何管理资源 + +### 计算机网络 ([[NET|NET]]) +- 网络协议、网络架构、通信原理 +- 学习目标: 掌握数据在网络中传输的机制 + +### 数据库 ([[DB|DB]]) +- 关系型数据库、NoSQL、查询优化 +- 学习目标: 学习数据存储和检索技术 + +### 工具使用 ([[TOOLS|TOOLS]]) +- 开发工具、版本控制、容器化、CLI +- 学习目标: 掌握日常开发必备工具 + +## 学习建议 + +建议从操作系统和网络开始打好基础,再深入数据库技术。工具使用贯穿整个学习过程,可以边学边用。 + +### 理论与实践结合 +- 阅读理论笔记后进行实践 +- 在软件开发中应用所学知识 +- 通过项目加深理解 diff --git a/CS/TOOLS/README.md b/CS/TOOLS/README.md new file mode 100644 index 0000000..d7bc37d --- /dev/null +++ b/CS/TOOLS/README.md @@ -0,0 +1,51 @@ +--- +tags: [CS, TOOLS, dev-tools] +create time: 2026-04-17 +--- + +# 工具使用 + +## 概述 + +工欲善其事,必先利其器。高效的工具使用可以极大提升开发和学习的效率。 + +## 核心工具类别 + +### 开发工具 +- **编辑器/IDE**: Vim、VS Code、IntelliJ +- **代码补全与Linting**: Copilot、ESLint、golangci-lint +- **调试工具**: GDB、Chrome DevTools、Delve + +### 版本控制 +- **Git**: 版本管理、分支策略 +- **GitHub/GitLab**: 代码托管、协作开发 +- **Git工作流**: Git Flow、GitHub Flow + +### 容器化与部署 +- **Docker**: 容器化应用 +- **Docker Compose**: 多容器编排 +- **Kubernetes**: 容器编排(可选) + +### 命令行工具 +- **Shell编程**: Bash/Zsh脚本 +- **包管理器**: apt, brew, npm, go modules +- **系统工具**: grep, sed, awk, ssh, tmux + +### 构建与测试 +- **构建工具**: Make, Webpack, Vite +- **测试框架**: Jest, Go Test, PyTest +- **持续集成**: GitHub Actions, Travis CI + +## 学习重点 + +- Git的高级用法 +- Shell脚本编程 +- Docker容器化 +- 命令行效率提升 + +## 实践建议 + +- 在日常工作中持续使用 +- 学习快捷键和最佳实践 +- 定期更新工具版本 +- 关注工具生态发展 diff --git a/CS/TOOLS/网络抓包.md b/CS/TOOLS/网络抓包.md new file mode 100644 index 0000000..e171e97 --- /dev/null +++ b/CS/TOOLS/网络抓包.md @@ -0,0 +1,576 @@ +--- +tags: [network, tools, whistle, packet-capture, proxy] +create time: 2026-04-17 12:20 +--- + +# 网络抓包 + +## 概述 + +网络抓包是调试和分析网络请求的核心技术。本文档以 whistle 支持的抓包功能为核心,介绍网络请求的抓取、分析、修改和重放方法。 + +理论性的协议分析内容请参考 [[CS/NET/网络协议分析基础]]。 + +## 正文 + +### 代理状态下的网络请求流程 + +在配置代理后,网络请求会经过代理服务器转发。以下是完整的请求流程: + +```mermaid +sequenceDiagram + participant Client as 客户端 (浏览器/应用) + participant Proxy as 代理服务器 (Whistle) + participant Target as 目标服务器 + + %% 1. 客户端发起请求 + Client->>Proxy: ✅ 1. 发起 HTTP/HTTPS 请求 + Note over Client,Proxy: URL: https://api.example.com/data
Method: GET
Headers: {...} + + %% 2. 代理接收并解析 + Proxy->>Proxy: 🔍 2. 接收请求并解析 + Note over Proxy: 检查规则匹配
- 规则: reqHeaders?
- 规则: resBody?
- 规则: 映射? + + %% 分支: 是否有转发规则 + alt 有转发规则 + Proxy->>Proxy: 📝 3. 应用转发规则 + Note over Proxy: 执行修改操作
- 修改请求头
- 替换域名
- 重写路径 + end + + %% 分支: HTTPS 请求 + alt HTTPS 请求 + Proxy->>Proxy: 🔒 4. SSL/TLS 握手 + Note over Proxy: - 代理充当中间人
- 使用自签名证书
- 解密请求内容 + end + + %% 3. 代理转发请求 + Proxy->>Target: ⬆️ 5. 转发请求到目标服务器 + Note over Proxy,Target: 处理后的请求
Header + Body + + %% 4. 目标服务器处理 + Target->>Target: ⚙️ 6. 处理请求 + Note over Target: 业务逻辑处理
生成响应数据 + + %% 5. 目标服务器返回响应 + Target-->>Proxy: ⬇️ 7. 返回响应 + Note over Target,Proxy: Status: 200
Headers: {...}
Body: {...} + + %% 6. 代理接收并记录 + Proxy->>Proxy: 💾 8. 接收并记录响应 + Note over Proxy: - 记录到抓包列表
- 计算耗时
- 保存完整信息 + + %% 分支: 是否有响应修改规则 + alt 有响应修改规则 + Proxy->>Proxy: 🎭 9. 应用响应规则 + Note over Proxy: - 修改响应头
- 替换响应体
- 修改状态码
- Mock 数据 + end + + %% 7. 代理转发响应 + Proxy-->>Client: ✅ 10. 返回响应给客户端 + Note over Proxy,Client: 最终的响应数据
Client 可以正常处理 + + %% 循环: 请求重放 + rect rgba(0, 255, 0, 0.1) + Client->>Proxy: 🔄 11. 重放请求 (可选) + Proxy->>Client: 📤 12. 返回重放结果 + Note over Client,Proxy: 复现 Bug
接口调试
压力测试 + end +``` + +**流程说明:** + +1. **请求发起**: 客户端(浏览器或应用)向配置的代理服务器发送请求 +2. **代理解析**: 代理服务器接收请求,解析 URL、方法、头部等信息 +3. **规则匹配**: 代理检查配置的转发规则,包括请求/响应修改、域名映射等 +4. **HTTPS 处理**: 对于 HTTPS 请求,代理通过中间人模式解密和重新加密请求 +5. **转发请求**: 代理将处理后的请求转发到目标服务器 +6. **响应处理**: 目标服务器处理请求并返回响应 +7. **记录数据**: 代理记录完整的请求/响应信息到抓包列表 +8. **应用规则**: 如果配置了响应修改规则,代理会修改响应内容 +9. **返回响应**: 代理将最终响应返回给客户端 +10. **请求重放**: 可以对已记录的请求进行重放,用于调试和测试 + +**Whistle 的核心作用:** +- 🔍 **可见性**: 记录所有经过代理的请求 +- 🎭 **可控性**: 可以修改和重放请求 +- 🔄 **灵活性**: 支持域名映射和数据 Mock +- 📊 **调试性**: 提供详细的请求/响应分析 + +### Whistle 工具介绍 + +#### 为什么选择 Whistle + +Whistle 是一款基于 HTTP 代理的跨平台调试工具,相比传统抓包工具具有以下优势: + +- 跨平台支持 (Windows, Mac, Linux) +- 内置抓包功能,支持 HTTP/HTTPS +- 便捷的请求修改和重放 +- 支持域名映射、远程调试 +- 界面友好,操作简单 +- 支持插件扩展 +- 配置灵活,规则强大 + +#### 安装与启动 + +```bash +# 全局安装 +npm install -g whistle + +# 启动服务 +w2 start + +# 启动指定端口 +w2 start -p 8899 + +# 停止服务 +w2 stop + +# 重启服务 +w2 restart +``` + +![[Pasted image 20260417125039.png]] +![[Pasted image 20260417125713.png]] +**访问界面:** +- 默认地址: http://127.0.0.1:8899 +- 默认账号: whistle (首次安装无密码) + +#### 代理配置 + +**命令行配置:** +```bash +# Mac/Linux +export http_proxy=http://127.0.0.1:8899 +export https_proxy=http://127.0.0.1:8899 + +# Windows +set http_proxy=http://127.0.0.1:8899 +set https_proxy=http://127.0.0.1:8899 +``` + +**系统代理配置:** +1. 打开系统网络设置 +2. 配置 HTTP/HTTPS 代理为 127.0.0.1:8899 +3. 应用并保存 + +**手机代理配置:** +1. 确保手机和电脑在同一局域网 +2. 查看电脑 IP 地址 (如 192.168.1.100) +3. 手机 WiFi 设置中配置代理: + - 服务器: 192.168.1.100 + - 端口: 8899 +4. 浏览器访问 https://rootca.pro 安装信任证书 + +### 核心功能 + +#### 1. 抓包查看 + +**功能特性:** +- 显示所有 HTTP/HTTPS 请求 +- 显示请求/响应头和内容 +- 支持 WebSocket 抓包 +- 支持长连接和断点续传 +- 按域名、状态码、方式筛选 + +**界面说明:** +- **请求列表**: 显示所有请求的摘要信息 (URL、方法、状态码、耗时) +- **请求详情**: 点击请求查看完整信息 (请求头、响应头、请求体、响应体) +- **时间线**: 显示请求的时间顺序和耗时 +- **统计**: 显示请求总数、成功数、失败数等统计信息 + +#### 2. 请求修改 + +**修改方法:** + +1. **Values 方式 (规则面板)**: +``` +# 修改请求头 +example.com reqHeaders://{test} + +# 在 Values 中定义变量 +test: +x-custom-header: custom-value +x-another-header: another-value +``` + +2. **正则匹配修改**: +``` +# 修改所有匹配的请求 +/^https:\/\/example\.com\/(.*)/ reqHeaders://{test} +``` + +3. **修改请求体**: +``` +example.com reqBody://{test-body} + +# Values +test-body: +{"modified": "data"} +``` + +4. **修改响应**: +``` +# 修改响应头 +example.com resHeaders://{test-res} + +# 修改响应体 +example.com resBody://{test-body} + +# 修改响应状态码 +example.com statusCode://404 +``` + +#### 3. 请求重放 + +**功能作用:** +- 复现问题 bug +- 压力测试 +- 接口调试 +- 自动化脚本测试 + +**操作步骤:** +1. 在请求列表中选择要重放的请求 +2. 点击"重放"按钮 +3. 选择重放次数 +4. 开始重放 + +**批量重放:** +- 支持选中多个请求批量重放 +- 支持导出请求列表为脚本 +- 支持自定义重放间隔 + +#### 4. 域名映射 + +**本地开发映射:** +``` +# 映射到本地服务 +api.example.com 127.0.0.1:3000 + +# 映射到远程服务 +api.example.com www.test-api.com + +# 路径映射 +example.com/api local.path/to/api +``` + +**多环境切换:** +``` +# 开发环境 +dev.example.com api-dev.example.com + +# 测试环境 +test.example.com api-test.example.com + +# 生产环境 +prod.example.com api.example.com +``` + +#### 5. 接口 Mock + +**数据 Mock:** +``` +# Mock 接口响应 +api.example.com/user/info resBody://{mock-user} + +# Values 中定义 Mock 数据 +mock-user: +{ + "code": 200, + "data": { + "name": "测试用户", + "age": 25, + "avatar": "https://example.com/avatar.png" + }, + "message": "success" +} +``` + +**Mock 模板:** +``` +# 文件 Mock +api.example.com/list resBody://{./mock/list.json} + +# 使用 mockjs 语法 +api.example.com/list resBody://{./mock/list.js} +``` + +### 高级用法 + +#### 1. Bypass 跳过代理 + +**配置跳过规则:** +``` +# 跳过特定域名 +www.google.com bypass://* + +# 跳过 IP 地址 +192.168.1.100 bypass://* + +# 跳过本地地址 +192.168.* bypass://* +10.* bypass://* +``` + +#### 2. 插件使用 + +**常用插件:** +``` +# 安装插件 +w2 install whot + +# 使用插件 +example.com whot:// + +# 查看插件 +w2 list +``` + +#### 3. WebSocket 调试 + +**WebSocket 抓包:** +1. 在网络列表中找到 WebSocket 请求 +2. 点击查看连接详情 +3. 实时查看发送和接收的消息 +4. 支持文本和二进制消息 + +#### 4. HTTPS 处理 + +**自签名证书概念:** + +**什么是自签名证书?** + +自签名证书是指不由受信任的证书颁发机构(CA,如 DigiCert、Let's Encrypt 等)签发的 SSL/TLS 证书,而是由服务器自己生成并签名的证书。 + +**与传统证书的区别:** + +| 特性 | 受信任证书 | 自签名证书 | +|------|------------|------------| +| **签发者** | 受信任的 CA 机构 | 服务器自己 | +| **浏览器信任** | ✅ 自动信任 | ❌ 警告不安全 | +| **成本** | 付费(免费付费都有) | 免费 | +| **验证流程** | CA 验证身份 | 无验证 | +| **适用场景** | 生产环境、对外服务 | 开发测试、内网代理 | + +**为什么代理抓包需要自签名证书?** + +在 HTTPS 抓包中,代理服务器使用自签名证书的原理: + +```mermaid +flowchart TD + subgraph 正常HTTPS流程 + A1[客户端] -->|建立加密连接| B1[目标服务器] + B1 -->|发送官方证书
由CA签发| A1 + A1 -->|验证CA信任| B1 + end + + subgraph 代理抓包流程 + A2[客户端] -->|建立加密连接| B2[代理服务器] + B2 -->|发送自签名证书
代理自己签发| A2 + A2 -->|❓ 证书无效?| + A2 -->|⚠️ 需要手动信任| C{是否信任代理证书} + C -->|是| B2 + C -->|否| D[连接失败] + + B2 -->|解密查看内容| E[代理检查内容
应用修改规则] + E -->|重新加密| B2 + B2 -->|转发到真实服务器| F[目标服务器] + F -->|响应| B2 + B2 -->|返回给客户端| A2 + end +``` + +**代理使用自签名证书的原因:** + +1. **中间人攻击(MITM)的本质**: + - HTTPS 的目的是防止中间人攻击 + - 代理抓包本质上就是"合法的中间人" + - 需要解密 HTTPS 流量才能查看和修改 + +2. **无法动态生成官方证书**: + - 代理无法为每个域名申请真实证书 + - 代理需要处理成千上万个不同域名的请求 + - 自签名证书是最灵活的解决方案 + +3. **时间成本和工作效率**: + - 为每个抓包域名单独申请证书不现实 + - 自签名证书可以立即生成和使用 + +**Whistle 证书工作原理:** + +``` +1. 用户访问 https://api.example.com + ↓ +2. Whistle 拦截请求 + ↓ +3. Whistle 为 api.example.com 动态生成自签名证书 + (证书上的域名是 api.example.com) + ↓ +4. Whistle 将此证书发送给浏览器 + ↓ +5. 浏览器识别出这不是受信任的 CA 签发的证书 + (因为根证书不在浏览器信任列表中) + ↓ +6. 如果用户手动导入了 Whistle 的根证书并信任 + ↓ +7. 浏览器信任该证书,连接建立成功 + ↓ +8. Whistle 可以解密并查看 HTTPS 内容 +``` + +**信任证书:** +1. 访问 https://rootca.pro 下载证书 +2. 安装到系统信任根证书 +3. 移动端需要额外配置 + +**SSL Pinning 绕过:** +- 开发环境可以关闭 SSL Pinning +- 使用测试证书配置 +- 使用移动端调试工具 (如 Frida) + +### 实践场景 + +#### 场景 1: 前端开发调试 + +**需求:** +- 查看接口请求和响应 +- 修改接口数据进行测试 +- Mock 接口用于前端开发 + +**配置:** +``` +# 查看接口日志 +api.example.com log:// + +# 修改接口响应 +api.example.com resBody://{mock-data} + +# 映射到本地服务 +api.example.com 127.0.0.1:8080 +``` + +#### 场景 2: 移动端开发调试 + +**需求:** +- 抓取手机应用的网络请求 +- 查看和修改接口数据 +- 解决跨域问题 + +**配置:** +``` +# 启用 CORS +api.example.com resCors:// * + +# 允许所有域 +api.example.com reqHeaders://{test} + +test: +Access-Control-Allow-Origin: * +Access-Control-Allow-Credentials: true +``` + +#### 场景 3: 接口测试和压测 + +**需求:** +- 重复某接口的请求 +- 测试并发请求 +- 验证接口性能 + +**操作:** +1. 选择目标接口 +2. 点击"重放" → "多次重放" +3. 设置重放次数和间隔 +4. 查看结果和统计 + +#### 场景 4: 问题复现 + +**需求:** +- 在本地复现线上问题 +- 使用线上数据调试 +- 分离前端和后端问题 + +**配置:** +``` +# 使用线上接口,本地前端 +localhost:8080/api online.example.com/api + +# 使用本地接口,线上前端 +www.example.com/api 127.0.0.1:8080/api +``` + +### 常见问题 + +#### 1. 抓不到 HTTPS 请求 + +**解决方法:** +- 确认已安装并信任根证书 +- 检查系统代理是否配置正确 +- 重启浏览器和应用 +- 清除浏览器缓存 + +#### 2. 证书不信任 + +**解决方法:** +- Windows: 安装到"受信任的根证书颁发机构" +- Mac: 安装钥匙串并设置为"始终信任" +- 移动端: 进入设置 → 通用 → 关于本机 → 证书信任设置 + +#### 3. 代理配置后无法上网 + +**解决方法:** +- 检查 whistle 是否启动 +- 确认代理地址和端口正确 +- 检查防火墙设置 +- 尝试使用 bypass:// 规则 + +#### 4. 规则不生效 + +**排查步骤:** +1. 检查规则语法是否正确 +2. 确认规则没有冲突 +3. 查看规则优先级 +4. 使用 log:// 查看匹配情况 + +### 最佳实践 + +#### 1. 规则组织 + +``` +# 按功能分类 +# ----- 本地开发 ----- +api-dev.example.com 127.0.0.1:3000 +static-dev.example.com 127.0.0.1:8080 + +# ----- 测试环境 ----- +api-test.example.com test-api.example.com + +# ----- Mock 数据 ----- +api.example.com/user resBody://{./mock/user.json} +api.example.com/list resBody://{./mock/list.json} + +# ----- 特殊规则 ----- +google.com bypass://* +``` + +#### 2. 性能优化 + +- 及时清理过多的抓包记录 +- 使用过滤器只关注相关请求 +- 对于高流量场景使用 bypass:// 规则 + +#### 3. 安全建议 + +- 不要在公共WiFi下使用代理抓包 +- 及时清理缓存中的敏感信息 +- 不要在生产环境使用长时间的代理 +- 定期备份重要的配置和规则 + +## 关联笔记 + +- [[CS/NET/网络协议分析基础]] +- [[CS/SECURITY/网络安全基础]] +- [[DEV/DEBUG/调试技巧总结]] + +## 参考资源 + +- Whistle 官方文档: https://wproxy.org/whistle/ +- Whistle GitHub: https://github.com/avwo/whistle diff --git a/DEV/DEBUG/调试技巧总结.md b/DEV/DEBUG/调试技巧总结.md new file mode 100644 index 0000000..e69de29 diff --git a/DEV/GO/README.md b/DEV/GO/README.md new file mode 100644 index 0000000..6faca86 --- /dev/null +++ b/DEV/GO/README.md @@ -0,0 +1,75 @@ +--- +tags: [DEV, GO, golang, backend] +create time: 2026-04-17 +--- + +# Go语言服务端开发 + +## 概述 + +Go语言(Golang)是Google开发的开源编程语言,特别适合构建高性能、高并发的服务端应用。它的设计简洁高效,具有优秀的并发支持和强大的标准库。 + +## Go语言核心特性 + +### 语言特性 +- 静态类型、编译型语言 +- 强类型、简洁的语法 +- 自动车管内存(GC) +- 首类函数与闭包 +- 接口与隐式实现 + +### 并发模型 +- **Goroutine**: 轻量级线程 +- **Channel**: 通信机制 +- **Select**: 多路复用 +- **Context**: 上下文管理 + +### 标准库 +- net/http: HTTP服务端 +- database/sql: 数据库操作 +- encoding/json: JSON处理 +- io/io/ioutil: I/O操作 + +## 学习路径 + +### 基础语法 +- 变量与常量 +- 数据类型(切片slice、映射map) +- 流程控制 +- 函数与方法 +- 结构体与接口 + +### 进阶特性 +- 并发编程(Goroutine + Channel) +- 错误处理机制 +- 并发安全 +- 反射 + +### 服务端开发 +- HTTP服务器搭建 +- 路由框架(Gin, Echo等) +- 中间件设计 +- 数据库ORM(GORM) +- 配置管理 + +### 实践项目 +- RESTful API +- WebSocket服务 +- gRPC微服务 +- 分布式系统组件 + +## 学习重点 + +- Go的并发模型和最佳实践 +- 错误处理的设计哲学 +- 接口的使用与理解 +- 性能优化技巧 +- Go Modules依赖管理 + +## 实践建议 + +- 阅读Go源代码(标准库) +- 实现并发程序 +- 使用主流Go框架 +- 参与开源项目 +- 关注Go官方博客和最佳实践 diff --git a/DEV/REACT/README.md b/DEV/REACT/README.md new file mode 100644 index 0000000..e48b390 --- /dev/null +++ b/DEV/REACT/README.md @@ -0,0 +1,81 @@ +--- +tags: [DEV, REACT, frontend, javascript] +create time: 2026-04-17 +--- + +# React前端开发 + +## 概述 + +React是Facebook开发的用于构建用户界面的JavaScript库。它采用组件化思想,通过声明式编程方式,使得复杂数的逻辑可以轻松维护和扩展。 + +## React核心概念 + +### 组件化开发 +- 函数组件与类组件 +- Props(属性)传递 +- 组件生命周期 +- 组件组合模式 + +### State管理 +- useState Hook +- useReducer Hook +- Context API +- 第三方状态管理(Redux, Zustand) + +### 副作用处理 +- useEffect Hook +- 自定义Hooks +- 依赖数组管理 +- 清理函数 + +### Hooks生态 +- useRef: 访问DOM元素 +- useMemo/useCallback: 性能优化 +- useLayoutEffect: 同步副作用 +- 自定义Hooks代码复用 + +## 学习路径 + +### JavaScript基础 +- ES6+语法(箭头函数、解构、模块化) +- 异步编程(Promise, async/await) +- DOM操作与事件 +- 模块系统 + +### React核心 +- JSX语法 +- 组件概念 +- Props与State +- 事件处理 +- 条件渲染与列表渲染 + +### 进阶特性 +- Hooks原理与实践 +- Context与状态管理 +- 性能优化 +- 表单处理 +- 路由(React Router) + +### 生态工具 +- 构建工具: Vite, Next.js +- UI组件库: MUI, Ant Design, Tailwind CSS +- 状态管理: Redux Toolkit, Zustand +- 数据获取: React Query, SWR +- 测试: Jest, React Testing Library + +## 学习重点 + +- React Hooks的深入理解 +- 组件设计模式 +- 性能优化技巧 +- 状态管理选型 +- 前端工程化 + +## 实践建议 + +- 从小项目开始,逐步构建复杂应用 +- 学习React DevTools调试 +- 关注React官方文档变化 +- 实践常见的UI场景(表单、表格、图表等) +- 学习Next.js等全栈框架 diff --git a/DEV/VITE/README.md b/DEV/VITE/README.md new file mode 100644 index 0000000..538b34e --- /dev/null +++ b/DEV/VITE/README.md @@ -0,0 +1,244 @@ +--- +tags: [DEV, VITE, build-tool, frontend] +create time: 2026-04-18 +--- + +# Vite 构建工具 + +[配置详解](./配置详解.md) | [架构原理](./架构原理.md) | [优化实践](./优化实践.md) + +## 概述 + +Vite 是下一代前端构建工具,以其极快的开发体验和优秀的生产构建性能著称。不同于传统构建工具的全量打包,Vite 利用浏览器原生 ES Module 能力,实现了真正的按需编译。 + +## 核心优势 + +🚀 **极速启动** - 无全量打包,启动时间 < 1s +⚡ **闪电 HMR** - 模块级热更新,保持应用状态 +🎯 **统一工具链** - 开发/生产环境一致,配置简化 +🔌 **丰富生态** - 官方插件支持主流框架 + +## 快速开始 + +### 项目创建 + +```bash +npm create vite@latest my-app -- --template react-ts +cd my-app +npm install +npm run dev +``` + +### 核心命令 + +| 命令 | 功能 | 使用场景 | +|------|------|----------| +| `npm run dev` | 启动开发服务器 | 日常开发调试 | +| `npm run build` | 生产环境构建 | 部署准备 | +| `npm run preview` | 预览构建产物 | 本地测试生产版本 | + +## 核心概念 + +### 开发环境 vs 生产环境 + +```mermaid +graph LR + A[浏览器请求] --> B{环境判断} + B -->|开发| C[Dev Server] + B -->|生产| D[静态文件] + C --> E[ESM 按需编译] + E --> F[即时响应] + D --> G[CDN/HTTP 服务器] +``` + +### 构建流程对比 + +| 特性 | Vite | Webpack | +|------|------|---------| +| 启动速度 | < 1s | 10-30s | +| 热更新 | 模块级 < 100ms | 全量重编译 | +| 配置复杂度 | 低 | 高 | +| 构建产物 | Rollup 优化 | Webpack 打包 | + +## 技术栈支持 + +### 前端框架 +- ✅ React + TypeScript +- ✅ Vue 3 + TypeScript +- ✅ Svelte + TypeScript +- ✅ Preact + TypeScript + +### 构建特性 +- 📦 ES Modules 原生支持 +- 🎨 CSS 预处理器 +- 🔧 TypeScript 开箱即用 +- 📡 Asset 资源处理 +- 🔄 Dynamic Import 支持 + +## 项目结构建议 + +``` +project/ +├── src/ +│ ├── assets/ # 静态资源 +│ ├── components/ # 组件 +│ ├── hooks/ # 自定义 Hooks +│ ├── lib/ # 工具库 +│ ├── types/ # TypeScript 类型 +│ ├── App.tsx # 根组件 +│ └── main.tsx # 入口文件 +├── public/ # 公共资源 +├── vite.config.ts # Vite 配置 +└── tsconfig.json # TypeScript 配置 +``` + +## 基础配置示例 + +```typescript +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@components': path.resolve(__dirname, './src/components') + } + }, + server: { + port: 3000, + open: true + } +}) +``` + +## 常见配置场景 + +### TypeScript 项目 +```typescript +// tsconfig.json +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + } +} +``` + +### 环境变量 +```typescript +// .env.development +VITE_API_URL=http://localhost:8080/api +VITE_DEBUG_MODE=true +VITE_MOCK_DATA=true + +// 使用方式 +const apiUrl = import.meta.env.VITE_API_URL +``` + +## 生态工具 + +### 官方插件 +- `@vitejs/plugin-vue` - Vue 支持 +- `@vitejs/plugin-react` - React 支持 +- `@vitejs/plugin-legacy` - 传统浏览器兼容 + +### 推荐工具链 +- **代码质量**: ESLint + Prettier +- **状态管理**: Zustand / Redux Toolkit +- **UI 组件**: Ant Design / MUI +- **路由**: React Router v6 +- **HTTP 客户端**: Axios + +## 学习路径 + +### 基础阶段 +1. 掌握基本命令和配置 +2. 理解开发/生产环境差异 +3. 熟悉环境变量使用 + +### 进阶阶段 +1. 深入构建配置 +2. 学习插件开发 +3. 性能优化实践 + +### 高级阶段 +1. 理解底层架构原理 +2. 定制化解决方案 +3. 大型项目工程化 + +## 技术特点 + +### 为什么选择 Vite? + +1. **开发效率提升** + - 零配置启动,开箱即用 + - 热更新速度快,大幅提升开发体验 + - 现代工具链,学习成本低 + +2. **构建性能优异** + - 基于 Rollup 的高质量产出 + - 智能代码分割和 Tree-shaking + - 生产环境高度优化 + +3. **生态社区活跃** + - 官方支持和持续更新 + - 丰富的插件生态 + - 广泛的行业采用 + +## 与其他工具对比 + +| 工具 | 启动速度 | HMR 性能 | 构建质量 | 学习曲线 | +|------|----------|----------|----------|----------| +| Vite | ⚡⚡⚡ | ⚡⚡⚡ | ⚡⚡⚡ | ⚡⚡⚡ | +| Webpack | ⚡ | ⚡⚡ | ⚡⚡⚡ | ⚡ | +| Next.js | ⚡⚡ | ⚡⚡⚡ | ⚡⚡⚡ | ⚡⚡ | +| Parcel | ⚡⚡⚡ | ⚡⚡ | ⚡⚡ | ⚡⚡⚡ | + +## 适用场景 + +### 推荐使用 +- ✅ SPA 单页应用开发 +- ✅ React/Vue 项目构建 +- ✅ 中大型前端项目 +- ✅ 需要快速开发迭代 + +### 需要评估 +- 🤔 复杂的构建需求 (Web Workers, WASM) +- 🤔 高度定制化构建流程 +- 🤔 遗留项目迁移成本 + +## 相关文档 + +- [[DEV/VITE/配置详解.md]] - 详细配置说明 +- [[DEV/VITE/架构原理.md]] - 技术实现原理 +- [[DEV/VITE/优化实践.md]] - 性能优化指南 +- [[DEV/REACT/README.md]] - React 开发指南 + +## 技术资源 + +- [Vite 官方文档](https://vitejs.dev/) +- [Vite GitHub 仓库](https://github.com/vitejs/vite) +- [Vite 生态插件](https://github.com/vitejs/awesome-vite) + +## 下一步行动 + +1. 创建一个新的 React + TypeScript 项目 +2. 配置开发和生产环境 +3. 实现 HMR 功能 +4. 进行第一次生产构建 diff --git a/DEV/VITE/优化实践.md b/DEV/VITE/优化实践.md new file mode 100644 index 0000000..5f0ae40 --- /dev/null +++ b/DEV/VITE/优化实践.md @@ -0,0 +1,722 @@ +--- +tags: [DEV, VITE, performance, optimization] +create time: 2026-04-18 +--- + +# Vite 优化实践 + +## 性能优化总览 + +```mermaid +graph TD + A[Vite 性能优化] --> B[开发环境] + A --> C[生产构建] + A --> D[运行时优化] + + B --> B1[依赖预构建] + B --> B2[智能缓存] + B --> B3[热更新优化] + + C --> C1[代码分割] + C --> C2[资源优化] + C --> C3[构建配置] + + D --> D1[按需加载] + D --> D2[缓存策略] + D --> D3[监控分析] +``` + +## 开发环境优化 + +### 依赖预构建优化 + +#### 智能依赖识别 +```typescript +import { defineConfig } from 'vite' + +export default defineConfig({ + optimizeDeps: { + // 手动指定需要预构建的依赖 + include: [ + 'react', + 'react-dom', + 'antd', + 'lodash' + ], + // 排除不需要预构建的模块 + exclude: [ + // 已是 ESM 格式的包 + 'vue', + // 开发时频繁修改的包 + './src/common/*' + ], + // 强制重新构建 + force: process.env.NODE_ENV === 'development' + } +}) +``` + +#### 预构建性能调优 +```typescript +export default defineConfig({ + optimizeDeps: { + // esbuild 配置 + esbuildOptions: { + target: 'es2020', + // 保持类名和函数名,便于调试 + keepNames: true, + // 启用打包内联 + bundle: true, + // 外部化某些依赖 + external: ['some-large-library'] + } + } +}) +``` + +### 服务器配置优化 + +#### 高效的代理配置 +```typescript +export default defineConfig({ + server: { + proxy: { + // API 代理 + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + // 路径重写 + rewrite: (path) => path.replace(/^\/api/, ''), + // 超时设置 + timeout: 30000, + // 代理错误处理 + configure: (proxy, _options) => { + proxy.on('error', (err, _req, res) => { + res.end(JSON.stringify({ error: err.message })) + }) + } + }, + // 静态资源代理 + '/static': { + target: 'http://localhost:9000', + changeOrigin: true + } + } + } +}) +``` + +#### 开发服务器性能 +```typescript +export default defineConfig({ + server: { + // 禁用自动浏览器打开 + open: false, + + // 严格的端口模式 + strictPort: false, + + // 仅监听 localhost + host: '127.0.0.1', + + // HMR 配置 + hmr: { + // 覆盖层配置 + overlay: { + runtimeErrors: false, // 不显示运行时错误 + errors: true, // 显示编译错误 + warnings: false // 不显示警告 + } + }, + + // 文件监听优化 + watch: { + usePolling: false, // 禁用轮询(默认) + interval: 100, // 轮询间隔 + ignored: [ + '**/node_modules/**', // 忽略 node_modules + '**/.git/**', // 忽略 .git 目录 + '**/dist/**', // 忽略构建输出 + 'coverage/**' // 忽略测试覆盖率 + ] + } + } +}) +``` + +### 缓存策略 + +#### 依赖缓存优化 +```typescript +export default defineConfig({ + optimizeDeps: { + // 缓存目录 + cacheDir: 'node_modules/.vite', + + // 锁文件 + lockfile: true, + + // 禁用缓存调试 + // force: true + } +}) + +// 清理依赖缓存 +// npm run dev -- --force +``` + +#### 模块缓存配置 +```typescript +export default defineConfig({ + build: { + // 模块预加载 + modulePreload: { + polyfill: false, // 禁用预加载 polyfill + resolveDependencies: false // 禁用依赖解析 + } + } +}) +``` + +## 生产构建优化 + +### 代码分割策略 + +#### 智能代码分割 +```typescript +export default defineConfig({ + build: { + rollupOptions: { + output: { + // 手动代码分割配置 + manualChunks: (id) => { + // 节点模块分割 + if (id.includes('node_modules')) { + if (id.includes('react')) { + return 'vendor-react' + } + if (id.includes('antd')) { + return 'vendor-ui' + } + if (id.includes('lodash') || id.includes('dayjs')) { + return 'vendor-utils' + } + return 'vendor-other' + } + + // 应用代码分割 + if (id.includes('pages/')) { + const pageName = id.split('/pages/')[1].split('/')[0] + return `pages/${pageName}` + } + + // 组件分割 + if (id.includes('components/')) { + const componentName = id.split('/components/')[1].split('/')[0] + return `components/${componentName}` + } + } + } + } + } +}) +``` + +#### 按路由懒加载 +```typescript +// React Router 懒加载示例 +import { lazy, Suspense } from 'react' + +// 路由组件懒加载 +const HomePage = lazy(() => import(/* webpackChunkName: "home" */ '@/pages/Home')) +const AboutPage = lazy(() => import(/* webpackChunkName: "about" */ '@/pages/About')) +const DashboardPage = lazy(() => import(/* webpackChunkName: "dashboard" */ '@/pages/Dashboard')) + +// 使用 Suspense 包裹 +function App() { + return ( + Loading...}> + + } /> + } /> + } /> + + + ) +} +``` + +### 资源优化 + +#### CSS 代码分割 +```typescript +export default defineConfig({ + build: { + cssCodeSplit: true, // 启用 CSS 代码分割 + cssTarget: 'chrome80', // CSS 目标浏览器 + cssMinify: 'lightningcss' // CSS 压缩器 + }, + css: { + devSourcemap: false, // 开发环境不生成 Source Map + + // PostCSS 配置 + postcss: './postcss.config.js', + + preprocessorOptions: { + scss: { + api: 'modern-compiler' // 使用现代编译器 + } + } + } +}) +``` + +#### 静态资源优化 +```typescript +export default defineConfig({ + build: { + // 资源处理 + assetsInlineLimit: 4096, // 小于 4kb 的资源内联 + + rollupOptions: { + output: { + // 资源文件命名 + assetFileNames: (assetInfo) => { + const info = assetInfo.name?.split('.') ?? [] + const extType = info[info.length - 1] + + if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name ?? '')) { + return `media/[name]-[hash].[ext]` + } + if (/\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/i.test(assetInfo.name ?? '')) { + return `images/[name]-[hash].[ext]` + } + if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name ?? '')) { + return `fonts/[name]-[hash].[ext]` + } + return `assets/[name]-[hash].[ext]` + } + } + } + } +}) +``` + +### 压缩与优化 + +#### 代码压缩配置 +```typescript +export default defineConfig({ + build: { + minify: 'terser', // 压缩器选择: terser | esbuild + terserOptions: { + compress: { + // 移除 console + drop_console: true, + drop_debugger: true, + + // 纯函数优化 + pure_funcs: [ + 'console.log', + 'console.info', + 'console.debug' + ], + + // 代码优化 + ecma: 2020, + arguments: true, + dead_code: true, + side_effects: true + }, + mangle: { + // 变量名混淆 + toplevel: true, + properties: { + regex: /^_/ // 混淆以下划线开头的属性 + }, + keep_classnames: false, + keep_fnames: false + }, + format: { + // 保留版权注释 + comments: false, + // 不移除代码 + preserveAnnotations: false + } + }, + + // 不显示压缩后大小 + reportCompressedSize: false + } +}) +``` + +#### 构建产物分析 +```typescript +import { visualizer } from 'rollup-plugin-visualizer' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [ + visualizer({ + filename: './dist/stats.html', + open: true, + gzipSize: true, + brotliSize: true, + template: 'treemap' // treemap | sunburst | network + }) + ] +}) + +// 构建后分析 +// npm run build +``` + +## 运行时优化 + +### 按需导入 + +#### UI 组件按需导入 +```typescript +// Ant Design 按需导入 +import { ConfigProvider } from 'antd' +import Button from 'antd/es/button' +import Input from 'antd/es/input' + +// 或者使用自动导入插件 +import Components from 'unplugin-vue-components/vite' +import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers' + +export default defineConfig({ + plugins: [ + react(), + Components({ + resolvers: [ + AntDesignVueResolver({ + importStyle: 'less' // 按需导入样式 + }) + ] + }) + ] +}) +``` + +#### 工具库按需导入 +```typescript +// 原始导入方式(导入整个库) +// import _ from 'lodash' +// const result = _.map(arr, (item) => item.value) + +// 按需导入方式 +import { map } from 'lodash-es' +const result = map(arr, (item) => item.value) + +// 或者使用 lodash-unified-plugin +``` + +### 缓存策略 + +#### HTTP 缓存配置 +```typescript +// Nginx 配置示例 +location / { + try_files $uri $uri/ /index.html; + + # 关键资源缓存策略 + location ~* \.(js|css)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # 图片资源缓存 + location ~* \.(jpg|jpeg|png|gif|webp|svg)$ { + expires 6M; + add_header Cache-Control "public, max-age=15552000"; + } + + # HTML 不缓存 + location ~* \.html$ { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } +} +``` + +#### 浏览器缓存优化 +```typescript +// 预加载关键资源 +// 在 index.html 中添加 + + + +// React 应用中的预加载 +const preloadComponent = (path: string) => { + const link = document.createElement('link') + link.rel = 'prefetch' + link.href = path + document.head.appendChild(link) +} + +// 根据路由预加载 +const routes = [ + { path: '/dashboard', preload: () => preloadComponent('/assets/dashboard-[hash].js') } +] +``` + +### 性能监控 + +#### 运行时性能监控 +```typescript +// 性能监控工具 +class PerformanceMonitor { + private metrics: Map = new Map() + + measure(name: string, fn: () => void) { + const start = performance.now() + fn() + const duration = performance.now() - start + + this.metrics.set(name, duration) + + // 开发环境打印性能指标 + if (import.meta.env.DEV) { + console.log(`[Performance] ${name}: ${duration.toFixed(2)}ms`) + } + } + + getMetrics() { + return Object.fromEntries(this.metrics) + } +} + +// 使用示例 +const monitor = new PerformanceMonitor() +monitor.measure('app-init', () => { + initializeApp() +}) +``` + +#### 构建 time analysis +```typescript +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + // 构建时间限制 + chunkSizeWarningLimit: 1000, + + // 构建并行化 + parallel: true, + + // 构建统计信息 + reportCompressedSize: false + } +}) + +// 使用 time 命令测量构建时间 +// time npm run build +``` + +## 实际案例分析 + +### 大型 React 项目优化 + +#### 问题场景 +- 🔧 项目包含 200+ 组件 +- 📦 构建后的 main.js 超过 2MB +- 🚀 首屏加载时间 5-8 秒 + +#### 优化方案 +```typescript +// vite.config.ts +export default defineConfig({ + build: { + rollupOptions: { + output: { + manualChunks: { + // 核心框架 + 'react-core': ['react', 'react-dom', 'react-router-dom'], + + // UI 组件库 + 'antd-core': ['antd'], + 'antd-icons': ['@ant-design/icons'], + + // 状态管理 + 'state-management': ['zustand', 'immer'], + + // 工具库 + 'utils': ['lodash-es', 'dayjs', 'axios'], + + // 业务模块 + 'business-user': [/src\/modules\/user/], + 'business-order': [/src\/modules\/order/], + 'business-product': [/src\/modules\/product/] + } + } + } + } +}) +``` + +#### 优化结果 +- ✅ main.js 减少到 800KB +- ✅ 首屏加载时间降低到 2-3 秒 +- ✅ 构建时间从 3 分钟减少到 1 分钟 + +### TypeScript 项目编译优化 + +#### 编译速度优化 +```typescript +// tsconfig.json +{ + "compilerOptions": { + "incremental": true, // 增量编译 + "tsBuildInfoFile": ".tsbuildinfo", + "skipLibCheck": true, // 跳过类型声明文件检查 + "skipDefaultLibCheck": true + }, + + "references": [ // 项目引用 + { "path": "./packages/component" } + ] +} +``` + +#### 类型检查性能 +```typescript +// 按需类型检查 +// .githooks/pre-commit +#!/bin/bash +git diff --cached --name-only | grep '\.tsx?$' | xargs npx tsc --noEmit + +// 或者在开发环境只检查当前文件 +// IDE 配置 TypeScript Server 模式 +``` + +### 组件库开发优化 + +#### 库模式配置 +```typescript +// vite.config.ts (组件库) +export default defineConfig({ + build: { + lib: { + entry: path.resolve(__dirname, 'src/index.ts'), + name: 'MyComponentLibrary', + fileName: (format) => `my-component-library.${format}.js`, + formats: ['es', 'umd', 'cjs'] + }, + rollupOptions: { + // 外部化 react + external: ['react', 'react-dom'], + output: { + globals: { + react: 'React', + 'react-dom': 'ReactDOM' + } + } + }, + // 压缩 + minify: 'terser', + sourcemap: true + } +}) +``` + +## 监控与调试 + +### 开发环境监控 + +```typescript +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [ + // 性能监控插件 + { + name: 'performance-monitor', + transform(code, id) { + // 监控大文件 + if (code.length > 100000) { + console.warn(`[Large File] ${id}: ${code.length} bytes`) + } + return null + } + } + ] +}) +``` + +### 生产环境调试 + +```typescript +// Source Map 配置 +export default defineConfig({ + build: { + sourcemap: true, // 生产环境生成 Source Map + sourceMapDebug: false + } +}) + +// 部署环境配置错误的 Source Map +// 设置为 true 便于问题追踪,false 提高安全性 +``` + +## 最佳实践总结 + +### ✅ 推荐实践 + +1. **依赖管理** + - 使用 `include` 明确指定预构建依赖 + - 定期清理依赖缓存 + - 优先选择 ESM 格式的包 + +2. **代码组织** + - 按路由和功能模块进行代码分割 + - 使用动态导入实现懒加载 + - 合理组织组件和工具函数 + +3. **性能优化** + - 启用 Tree-shaking 移除死代码 + - 使用现代压缩工具 + - 优化资源加载策略 + +4. **监控分析** + - 定期分析构建产物 + - 监控应用运行性能 + - 持续优化配置 + +### ❌ 避免问题 + +1. **过度配置** + - 不要过度使用配置插件 + - 避免不必要的依赖 + - 保持配置简洁 + +2. **忽略缓存** + - 不要忽略依赖缓存的清理 + - 注意缓存失效策略 + - 合理配置缓存时间 + +3. **盲目优化** + - 不要在没有性能问题时过度优化 + - 优先解决真正的性能瓶颈 + - 基于数据驱动的优化决策 + +## 工具推荐 + +### 构建分析工具 +- **vite-bundle-visualizer** - 构建产物可视化 +- **rollup-plugin-visualizer** - 深度构建分析 +- **source-map-explorer** - Source Map 分析 + +### 性能测试工具 +- **Lighthouse** - 页面性能评估 +- **WebPageTest** - 多地性能测试 +- **Chrome DevTools Performance** - 运行时性能分析 + +优化是一个持续的过程,需要根据项目的具体情况和性能数据来调整策略。通过合理的配置和分析工具,可以让 Vite 项目在各种场景下表现优异。 + +相关文档: +- [[DEV/VITE/配置详解.md]] - 详细配置说明 +- [[DEV/VITE/架构原理.md]] - 理解优化原理 +- [[DEV/VITE/README.md]] - 快速入门指南 diff --git a/DEV/VITE/架构原理.md b/DEV/VITE/架构原理.md new file mode 100644 index 0000000..88d5f92 --- /dev/null +++ b/DEV/VITE/架构原理.md @@ -0,0 +1,903 @@ +--- +tags: [DEV, VITE, architecture, internals] +create time: 2026-04-18 +--- + +# Vite 架构原理 + +## 核心设计理念 + +Vite 的核心创新在于**利用浏览器原生能力和分层优化策略**,将开发环境和生产环境采用完全不同的处理方式。 + +```mermaid +graph TB + A[用户请求] --> B{环境判断} + B -->|开发环境| C[Dev Server] + B -->|生产环境| D[Pre-built Assets] + C --> E[ESM 按需编译] + E --> F[即时返回] + D --> G[Rollup 优化产物] + G --> H[CDN/HTTP 服务器] +``` + +## 开发环境架构 + +### 服务启动流程 + +```mermaid +sequenceDiagram + participant CLI as 用户命令行 + participant Config as 配置加载器 + participant Server as Dev Server + participant Plugin as 插件系统 + participant Monitor as 文件监听器 + + CLI->>Config: 读取配置文件 + Config->>Server: 创建服务器实例 + Server->>Plugin: 注册所有插件 + Plugin->>Server: 返回钩子函数 + Server->>Monitor: 启动文件监听 + Monitor->>Server: 建立连接 + Server-->>CLI: 服务器启动完成 +``` + +### HTTP 请求处理流程 + +```mermaid +graph TD + A[HTTP 请求] --> B[中间件层] + B --> C{请求类型判断} + C -->|HTML 文件| D[响应 HTML] + C -->|JS/TS 文件| E[transform 链] + C -->|CSS 文件| F[CSS 处理器] + C -->|静态资源| G[资源处理] + E --> H[Plugin 转换] + H --> I[返回编译后代码] + F --> I + G --> J[返回资源引用] +``` + +### 请求处理实现 + +```typescript +class ViteDevServer { + private pluginContainer: PluginContainer + private fileWatcher: FSWatcher + private moduleGraph: ModuleGraph + + async handleRequest(req: IncomingMessage, res: ServerResponse) { + const url = req.url! + + try { + // 1. 处理 HTML 入口文件 + if (url.endsWith('.html')) { + await this.handleHtmlRequest(url, res) + return + } + + // 2. 处理模块请求 + if (this.isModuleRequest(url)) { + const module = await this.transformModule(url) + this.sendModuleResponse(module, res) + return + } + + // 3. 处理静态资源 + const asset = await this.resolveAsset(url) + if (asset) { + this.sendAssetResponse(asset, res) + return + } + } catch (error) { + this.handleError(error, res) + } + } +} +``` + +## 模块系统实现 + +### 模块图结构 + +```mermaid +graph LR + A[main.tsx] --> B[App.tsx] + A --> C[router.tsx] + B --> D[Header.tsx] + B --> E[Counter.tsx] + C --> F[Pages.tsx] + E --> G[useState.ts] + E --> H[useEffect.ts] +``` + +### 模块节点实现 + +```typescript +class ModuleNode { + public readonly id: string + public url: string + public file: string + + // 依赖关系 + public importers = new Set() + public importedModules = new Set() + public importedBindings: Record = {} + + // 转换结果 + public transformResult: TransformResult | null = null + public lastHMRTimestamp = 0 + + // SSR 信息 + public ssrModule: any = null + public ssrTransformResult: TransformResult | null = null + + constructor(id: string) { + this.id = id + this.url = id + this.file = path.normalize(id) + } +} + +class ModuleGraph { + private modules = new Map() + private fileToModulesMap = new Map>() + + async ensureEntryFromUrl(url: string): Promise { + const resolved = await this.resolveUrl(url) + return this.ensureModule(resolved.id, resolved.url) + } + + async ensureModule( + id: string, + url: string, + ssr?: boolean + ): Promise { + let module = this.modules.get(id) + + if (!module) { + module = new ModuleNode(id) + this.modules.set(id, module) + + // 建立文件映射 + const file = path.resolve(id) + let fileModules = this.fileToModulesMap.get(file) + if (!fileModules) { + fileModules = new Set() + this.fileToModulesMap.set(file, fileModules) + } + fileModules.add(module) + } + + return module + } + + updateModule(module: ModuleNode, transformed: TransformResult) { + module.transformResult = transformed + module.lastHMRTimestamp = Date.now() + } + + /** + * 失效模块及其导入者 + */ + invalidateModule(mod: ModuleNode): void { + mod.transformResult = null + mod.ssrModule = null + mod.ssrTransformResult = null + + // 级联失效所有导入者 + const invalidators = new Set() + const queue: ModuleNode[] = [...mod.importers] + + while (queue.length > 0) { + const importer = queue.pop()! + invalidators.add(importer) + + // 如果导入者的代码需要重新编译 + if (!importer.transformResult) { + importer.importers.forEach(dep => { + if (!invalidators.has(dep)) { + queue.push(dep) + } + }) + } + } + } + + /** + * 检测循环依赖 + */ + hasCircularDependency(module: ModuleNode): boolean { + const visited = new Set() + const stack = [module.id] + + while (stack.length > 0) { + const currentId = stack.pop()! + + if (visited.has(currentId)) { + console.warn(`[vite] Circular dependency detected: ${currentId}`) + return true + } + + visited.add(currentId) + const currentModule = this.modules.get(currentId) + + if (currentModule) { + currentModule.importedModules.forEach(dep => { + if (!visited.has(dep.id)) { + stack.push(dep.id) + } + }) + } + } + + return false + } +} +``` + +## ES Module 编译器 + +### 源码转换流水线 + +```mermaid +graph TD + A[原始源代码] --> B[Plugin Transform 钩子] + B --> C[语法转换] + C --> D[依赖注入] + D --> E[包装 ESM] + E --> F[生成 Source Map] + F --> G[返回处理后的代码] +``` + +### 依赖预构建 + +```typescript +class DepsOptimizer { + private depsCacheDir: string + private scanner: DepsOptimizer + + async scanImports(): Promise { + const entries = await this.getEntryPoints() + + const discovered = await esbuild.context({ + entryPoints: entries, + bundle: true, + write: false, + onEnd: (result) => { + const dependencies = this.extractDependencies(result.metafile) + this.updateOptimizedDeps(dependencies) + } + }) + + return discovered + } + + async optimizeDeps(deps: Record): Promise { + const optimizedDeps = new Map() + + for (const [id, file] of Object.entries(deps)) { + try { + // 转换为 ESM + const result = await esbuild.build({ + entryPoints: [file], + bundle: true, + format: 'esm', + target: 'esnext', + write: false, + packages: 'external' + }) + + optimizedDeps.set(id, file) + + // 缓存到 disk + await this.saveOptimizedDep(id, result) + } catch (e) { + console.error(`Failed to optimize dependency: ${id}`, e) + } + } + } + + async saveOptimizedDep(id: string, result: BuildResult): Promise { + const outputPath = path.join(this.depsCacheDir, `${id}.js`) + const metaPath = path.join(this.depsCacheDir, `${id}.js.meta.json`) + + await fs.writeFile( + outputPath, + result.outputFiles[0].text, + 'utf-8' + ) + + await fs.writeFile( + metaPath, + JSON.stringify({ + file: id, + src: path.basename(outputPath) + }), + 'utf-8' + ) + } +} +``` + +## 热模块替换 (HMR) + +### HMR 工作机制 + +```mermaid +sequenceDiagram + participant File as 文件系统 + participant Watcher as 监听器 + participant Server as Dev Server + participant WS as WebSocket + participant Client as 浏览器客户端 + + File->>Watcher: 文件变更 + Watcher->>Server: 触发 change 事件 + Server->>Server: 失效相关模块 + Server->>Server: 重新编译变更模块 + Server->>WS: 发送 HMR 更新 + WS->>Client: 接收更新通知 + Client->>Client: 执行模块替换 + Client->>Server: 发送确认消息 +``` + +### HMR 协议 + +```typescript +// 服务端发送的 HMR 消息类型 +interface HMRCustomEvent { + type: 'custom' + event: string + data: any +} + +interface HMRUpdateEvent { + type: 'update' + updates: Array<{ + type: 'js-update' | 'css-update' | 'static-update' + path: string + acceptedPath: string + timestamp: number + }> +} + +interface HMRConnectedEvent { + type: 'connected' +} + +interface HMRPruneEvent { + type: 'prune' + paths: string[] +} + +// 客户端处理实现 +class HMRClient { + private ws: WebSocket + private pendingImports = new Map>() + + async handleUpdate(updates: HMRUpdateEvent['updates']) { + for (const update of updates) { + switch (update.type) { + case 'js-update': + await this.handleJSModuleUpdate(update) + break + case 'css-update': + await this.handleCSSUpdate(update) + break + case 'static-update': + this.handleStaticUpdate(update) + break + } + } + } + + private async handleJSModuleUpdate(update: Extract) { + const { path, timestamp, acceptedPath } = update + + // 使用浏览器原生 import API 重新导入模块 + const importPromise = import(`${path}?t=${timestamp}`).then((mod) => { + // 查找模块的热替换处理器 + const modUrl = acceptedPath || path + const modObject = window.__vite_module_cache__[modUrl] + + if (modObject && modObject.hot) { + // 触发热替换处理器 + modObject.hot.accept(mod) + + // 执行清理回调 + if (modObject.hot._dispose) { + modObject.hot._dispose() + } + } + }) + + this.pendingImports.set(path, importPromise) + } +} +``` + +### 组件级 HMR + +```typescript +// React 组件的 HMR 实现 +import { createHot } from '@vitejs/plugin-react/client' + +// 在开发环境中,Vite 会自动注入 +if (import.meta.hot) { + import.meta.hot.accept('./App.tsx', (newModule) => { + // React Fast Refresh 处理 + const prevComponent = window.__vite_plugin_react_component__ + window.__vite_plugin_react_component__ = newModule.default + + // 触发组件重新渲染 + window.__vite_plugin_react_rerender__() + }) +} +``` + +## 插件系统架构 + +### 插件生命周期 + +```mermaid +graph TD + A[Config 阶段] --> B[ConfigResolved] + B --> C[ConfigureServer] + C --> D[BuildStart] + D --> E[Transform Phase] + E --> F[Load Phase] + F --> G[BuildEnd] + G --> H[CloseBundle] +``` + +### 插件容器实现 + +```typescript +class PluginContainer { + private plugins: Plugin[] + private hooks: Record + + constructor(plugins: Plugin[]) { + this.plugins = plugins + this.hooks = {} + this.registerHooks(plugins) + } + + private registerHooks(plugins: Plugin[]) { + const hookNames: validHooks[] = [ + 'buildStart', 'buildEnd', + 'resolveId', 'load', 'transform', + 'configureServer' + ] + + hookNames.forEach(hookName => { + this.hooks[hookName] = plugins + .map(plugin => plugin[hookName]) + .filter(Boolean) + }) + } + + async resolveId(id: string, importer?: string): Promise { + const resolveHooks = this.hooks['resolveId'] + + for (const hook of resolveHooks) { + const result = await hook.call(this, id, importer) + + if (result) { + return result + } + } + + return null + } + + async load(id: string): Promise { + const loadHooks = this.hooks['load'] + + for (const hook of loadHooks) { + const result = await hook.call(this, id) + + if (result) { + return result + } + } + + return null + } + + async transform(code: string, id: string): Promise { + const transformHooks = this.hooks['transform'] + let result = { code, map: null } + + for (const hook of transformHooks) { + const transformed = await hook.call(this, result.code, id) + + if (transformed) { + result = { + code: transformed.code, + map: transformed.map || result.map + } + } + } + + return result.code !== code ? result : null + } +} +``` + +## 构建系统 (生产环境) + +### Rollup 集成 + +```mermaid +graph TD + A[入口文件] --> B[依赖图构建] + B --> C[模块解析] + C --> D[代码转换] + D --> E[代码分割] + E --> F[Tree Shaking] + F --> G[minification] + G --> H[输出生成] +``` + +### 构建流程实现 + +```typescript +class BuildSystem { + async build(config: ResolvedConfig): Promise { + // 1. 分析入口文件 + const entries = this.resolveEntries(config) + + // 2. 构建模块图 + const moduleGraph = await this.buildModuleGraph(entries) + + // 3. 代码优化 + const optimizedModules = await this.optimizeModules(moduleGraph) + + // 4. 代码分割 + const chunks = this.splitChunks(optimizedModules) + + // 5. 压缩混淆 + const minifyResults = await this.minifyChunks(chunks) + + // 6. 生成输出 + const outputs = this.generateOutputs(minifyResults) + + return { modules: outputs, warnings: [], errors: [] } + } + + private async optimizeModules(moduleGraph: ModuleGraph): Promise { + const modules = Array.from(moduleGraph.modules.values()) + + // 应用插件转换 + for (const module of modules) { + if (module.transformResult) { + const result = await this.pluginContainer.transform( + module.transformResult.code, + module.id + ) + + if (result) { + module.transformResult = result + } + } + } + + return modules + } + + private splitChunks(modules: ModuleNode[]): Chunk[] { + const chunks: Map = new Map() + + // 识别共享依赖 + const sharedDeps = this.findSharedDependencies(modules) + + // 按配置生成代码分割 + const manualChunksConfig = { + 'vendor-react': ['react', 'react-dom'], + 'vendor-ui': ['antd'] + } + + Object.entries(manualChunksConfig).forEach(([chunkName, depNames]) => { + const chunkModules = modules.filter(mod => + depNames.some(dep => mod.id.includes(dep)) + ) + + chunks.set(chunkName, { + name: chunkName, + modules: chunkModules, + fileName: `${chunkName}.js` + }) + }) + + return Array.from(chunks.values()) + } + + private async minifyChunks(chunks: Chunk[]): Promise { + const results: MinifyResult[] = [] + + for (const chunk of chunks) { + const code = this.generateChunkCode(chunk) + + // 使用 terser 进行压缩 + const minified = await terser.minify(code, { + compress: { + drop_console: true, + pure_funcs: ['console.log', 'console.info'] + }, + mangle: { + toplevel: true, + properties: { + regex: /^_/ + } + } + }) + + if (minified.code) { + results.push({ + chunkName: chunk.name, + code: minified.code, + map: minified.map + }) + } + } + + return results + } +} +``` + +## 性能优化机制 + +### 缓存策略 + +```mermaid +graph TD + A[请求模块] --> B{缓存检查} + B -->|内存缓存命中| C[返回缓存] + B -->|磁盘缓存命中| D[加载并返回] + B -->|缓存未命中| E[编译模块] + E --> F[写入缓存] + F --> C +``` + +### 多层缓存实现 + +```typescript +class CacheSystem { + private memoryCache = new Map() + private diskCache: DiskCache + private etagCache = new Map() + + async get(key: string): Promise { + // 1. 检查内存缓存 + const memEntry = this.memoryCache.get(key) + if (memEntry && !this.isExpired(memEntry)) { + return memEntry.content + } + + // 2. 检查磁盘缓存 + try { + const diskEntry = await this.diskCache.get(key) + if (diskEntry) { + // 写入内存缓存 + this.memoryCache.set(key, { + content: diskEntry.content, + etag: diskEntry.etag, + timestamp: Date.now() + }) + return diskEntry.content + } + } catch (e) { + // 缓存损坏,忽略 + } + + return null + } + + async set(key: string, content: string, options: CacheOptions = {}): Promise { + const etag = this.generateETag(content) + const entry: CacheEntry = { + content, + etag, + timestamp: Date.now(), + ttl: options.ttl || 0 + } + + // 写入内存缓存 + this.memoryCache.set(key, entry) + + // 持久化到磁盘 + if (options.persistent) { + await this.diskCache.set(key, { + content, + etag + }) + } + } + + private generateETag(content: string): string { + const hash = crypto.createHash('sha1') + hash.update(content) + return hash.digest('hex') + } + + private isExpired(entry: CacheEntry): boolean { + if (entry.ttl === 0) return false + return Date.now() - entry.timestamp > entry.ttl + } +} +``` + +### HTTP 缓存优化 + +```typescript +class HTTPCacheManager { + setupCacheHeaders(res: ServerResponse, content: string): void { + const etag = this.generateETag(content) + const lastModified = new Date().toUTCString() + + // 设置缓存头 + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') + res.setHeader('ETag', etag) + res.setHeader('Last-Modified', lastModified) + + // 检查客户端缓存 + if (this.request) { + const ifNoneMatch = this.request.headers['if-none-match'] + const ifModifiedSince = this.request.headers['if-modified-since'] + + if ((ifNoneMatch && ifNoneMatch === etag) || + (ifModifiedSince && ifModifiedSince === lastModified)) { + res.statusCode = 304 + res.end() + return true + } + } + + return false + } +} +``` + +## 进阶架构特性 + +### 虚拟模块系统 + +```typescript +// 虚拟模块插件示例 +export function virtualModulePlugin(): Plugin { + return { + name: 'virtual-module', + resolveId(id) { + if (id === 'virtual:env') { + return '\0virtual:env' + } + }, + load(id) { + if (id === '\0virtual:env') { + const env = { + NODE_ENV: process.env.NODE_ENV, + VERSION: '1.0.0' + } + return `export default ${JSON.stringify(env)}` + } + } + } +} + +// 使用虚拟模块 +import env from 'virtual:env' +console.log(env) // { NODE_ENV: 'development', VERSION: '1.0.0' } +``` + +### Source Map 生成 + +```typescript +class SourceMapGenerator { + async generateSourceMap( + originalCode: string, + transformedCode: string, + filePath: string + ): Promise { + const sourceMap: SourceMap = { + version: 3, + file: path.basename(filePath), + sourceRoot: '', + sources: [filePath], + names: [], + mappings: '' + + } + + // 使用 magic-string 生成精确的 mappings + const magicString = new MagicString(originalCode) + const transformed = new MagicString(transformedCode) + + // 映射转换的位置 + const mappings = this.generateMappings(originalCode, transformedCode) + sourceMap.mappings = mappings + + return sourceMap + } + + private generateMappings(original: string, transformed: string): string { + // 使用 VLQ 编码生成 mappings + const originalLines = original.split('\n') + const transformedLines = transformed.split('\n') + + const mappings: string[] = [] + let currentLine = 0 + let currentColumn = 0 + + for (let i = 0; i < transformedLines.length; i++) { + const lineMappings: number[][] = [] + + for (let j = 0; j < transformedLines[i].length; j++) { + // 找到原始代码中的对应位置 + const { line, column } = this.findOriginalPosition( + transformedLines[i], + j, + originalLines + ) + + if (line !== -1) { + lineMappings.push([ + j - currentColumn, // 生成的列偏移 + line - currentLine, // 原始行偏移 + column // 原始列 + ]) + + currentColumn = j + currentLine = line + } + } + + // 使用 VLQ 编码 + mappings.push(lineMappings.map(mapping => + this.encodeVLQ(mapping) + ).join(',')) + + currentLine = transformedLines.length + currentColumn = 0 + } + + return mappings.join(';') + } +} +``` + +## 架构对比 + +### 与传统构建工具对比 + +| 特性 | Vite | Webpack | Parcel | +|------|------|---------|--------| +| 启动原理 | ESM 按需编译 | 全量打包 | 零配置打包 | +| HMR 机制 | 模块级更新 | 全局重编译 | 智能更新 | +| 构建产出 | Rollup | 内置 | 内置 | +| 配置复杂度 | 低 | 高 | 极低 | +| 插件生态 | 新兴 | 成熟 | 有限 | +| 开发体验 | 优秀 | 可优化 | 良好 | +| 生产构建 | 高质量 | 高质量 | 良好 | + +### 适用场景分析 + +```mermaid +graph TD + A[项目类型] --> B{选择构建工具} + B -->|React/Vue SPA| C[Vite 推荐] + B -->|复杂构建需求| D[Webpack] + B -->|零配置快速开发| E[Parcel] + B -->|Next.js SSR| F[Next.js 内置] + + C --> G[👍 启动快
👍 HMR 优秀
👍 配置简单] + D --> H[👍 生态成熟
👍 高度可定制
👎 复杂] + E --> I[👍 开箱即用
👍 智能配置
👎 生态有限] +``` + +理解 Vite 的架构原理,不仅能帮助我们更好地使用它,还能为开发自定义插件和解决复杂问题提供理论支撑。Vite 的成功在于它巧妙地利用了现代浏览器和工具链的能力,选择了正确的技术路径。 + +相关文档: +- [[DEV/VITE/配置详解.md]] - 配置与插件开发 +- [[DEV/VITE/优化实践.md]] - 架构层面的性能优化 diff --git a/DEV/VITE/配置详解.md b/DEV/VITE/配置详解.md new file mode 100644 index 0000000..6c51de2 --- /dev/null +++ b/DEV/VITE/配置详解.md @@ -0,0 +1,695 @@ +--- +tags: [DEV, VITE, configuration, typescript] +create time: 2026-04-18 +--- + +# Vite 配置详解 + +## 配置文件结构 + +Vite 配置文件支持多种格式:`vite.config.js`、`vite.config.ts`,推荐使用 TypeScript 获得类型提示。 + +完整配置结构示例: + +```typescript +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig({ + // 插件配置 + plugins: [react()], + + // 路径别名 + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@components': path.resolve(__dirname, './src/components'), + '@hooks': path.resolve(__dirname, './src/hooks') + } + }, + + // 开发服务器配置 + server: { + port: 3000, + host: true, + open: true, + cors: true + }, + + // 构建配置 + build: { + outDir: 'dist', + sourcemap: true + } +}) +``` + +## 核心配置项 + +### resolve 模块解析 + +#### 路径别名 +```typescript +resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + // 支持目录别名 + '@components': path.resolve(__dirname, './src/components'), + '@utils': path.resolve(__dirname, './src/utils') + } +} +``` + +#### 扩展名解析 +```typescript +resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'] +} +``` + +### server 开发服务器 + +```typescript +server: { + // 端口配置 + port: 3000, + strictPort: false, // 端口被占用时自动尝试下一个 + host: true, // 监听所有网络地址 + + // 自动打开浏览器 + open: true, + openPage: '/dashboard', + + // CORS 配置 + cors: true, + + // 代理配置 + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, '') + }, + '/assets': { + target: 'http://localhost:9000', + changeOrigin: true + } + }, + + // HMR 配置 + hmr: { + overlay: true, // 显示错误覆盖层 + port: 24678 // HMR WebSocket 端口 + }, + + // 中间件模式 (用于 SSR) + middlewareMode: false +} +``` + +### load 环境变量 + +```bash +# .env.development +VITE_API_URL=http://localhost:8080/api +VITE_APP_NAME=Dev Environment + +# .env.production +VITE_API_URL=https://api.example.com +VITE_APP_NAME=Production +``` + +```typescript +// 类型定义 +interface ImportMetaEnv { + readonly VITE_API_URL: string + readonly VITE_APP_NAME: string +} + +// 使用环境变量 +const apiUrl = import.meta.env.VITE_API_URL +const appName = import.meta.env.VITE_APP_NAME + +// 自定义环境变量访问 +export default defineConfig(({ mode }) => { + return { + define: { + __APP_VERSION__: JSON.stringify(process.env.npm_package_version), + __ENV__: JSON.stringify(mode) + } + } +}) +``` + +### build 构建配置 + +```typescript +build: { + // 输出目录 + outDir: 'dist', + assetsDir: 'assets', + + // Source Map 配置 + sourcemap: false, // 生产环境关闭 + sourcemapExcludeSources: false, + + // 压缩配置 + minify: 'terser', // terser | esbuild + terserOptions: { + compress: { + drop_console: true, // 移除 console + drop_debugger: true, // 移除 debugger + pure_funcs: ['console.log', 'console.info'] + } + }, + + // 代码分割配置 + rollupOptions: { + output: { + // 手动代码分割 + manualChunks: { + 'vendor-react': ['react', 'react-dom'], + 'vendor-ui': ['antd', '@ant-design/icons'], + 'vendor-utils': ['lodash', 'dayjs'] + }, + // 文件名模式 + chunkFileNames: 'js/[name]-[hash].js', + entryFileNames: 'js/[name]-[hash].js', + assetFileNames: '[ext]/[name]-[hash].[ext]' + } + }, + + // 构建优化 + chunkSizeWarningLimit: 1000, // 警告限制 + rollupOptions: { + output: { + // 内联动态导入 + inlineDynamicImports: false, + // 保留模块结构 + preserveModules: false + } + }, + cssCodeSplit: true, + reportCompressedSize: false, + target: 'es2015' +} +``` + +### preview 预览配置 + +```typescript +preview: { + port: 4173, + strictPort: false, + host: true, + open: true, + + // 预览服务器配置 + cors: true, + + // 中间件 + middlewares: [ + // 自定义中间件 + ] +} +``` + +## CSS 配置 + +### CSS Modules +```typescript +css: { + modules: { + // 命名规范 + localsConvention: 'camelCase', // camelCase | camelCaseOnly | dashes | dashesOnly + + // 作用域行为 + scopeBehaviour: 'local', // local | global + + // 类名生成 + generateScopedName: '[name]__[local]___[hash:base64:5]', + + // Hash 生成函数 + hashPrefix: 'prefix', + + // 全局模块路径 + globalModulePaths: [/node_modules/] + } +} +``` + +### CSS 预处理器 +```typescript +css: { + preprocessorOptions: { + scss: { + additionalData: `@import "@/styles/variables.scss";`, + api: 'modern-compiler' // 使用现代编译器 + }, + less: { + modifyVars: { + 'primary-color': '#1890ff' + }, + javascriptEnabled: true + } + } +} +``` + +### PostCSS 配置 +```javascript +// postcss.config.js +export default { + plugins: { + autoprefixer: {}, + 'cssnano': { + preset: 'default' + } + } +} +``` + +## 插件系统 + +### 插件配置流程 + +```mermaid +graph TD + A[vite.config.ts] --> B[插件导入] + B --> C[插件配置] + C --> D[插件注册] + D --> E[构建流程] + E --> F[插件执行] +``` + +### 官方插件 + +#### React 插件 +```typescript +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [ + react({ + // Babel 转换 + babel: { + plugins: ['emotion'] + }, + // JSX 运行时 + jsxRuntime: 'automatic', // classic | automatic + // 开发工具 + devtools: true, + // 包含 + include: /\.(jsx|js|tsx|ts)$/, + // 排除 + exclude: /\.node_modules/ + }) + ] +}) +``` + +#### Vue 插件 +```typescript +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [ + vue({ + // Vue 编译器选项 + template: { + compilerOptions: { + isCustomElement: (tag) => tag.includes('-'), + whitespace: 'condense' + } + }, + // 脚本配置 + script: { + defineModel: true, + propsDestructure: true + }, + // 样式配置 + style: { + scoped: true + } + }) + ] +}) +``` + +### 自定义插件 + +#### 基础插件结构 +```typescript +import type { Plugin } from 'vite' + +export function myCustomPlugin(): Plugin { + return { + name: 'my-custom-plugin', + + // 配置阶段 + config(config) { + return { + // 返回配置修改 + } + }, + + configResolved(config) { + // 配置已解析 + console.log('Vite config resolved:', config) + }, + + // 配置开发服务器 + configureServer(server) { + // 自定义服务器中间件 + server.middlewares.use((req, res, next) => { + if (req.url === '/custom-endpoint') { + res.statusCode = 200 + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify({ message: 'Custom response' })) + } else { + next() + } + }) + + // 返回清理函数 + return () => { + console.log('Server closed') + } + }, + + // 转换钩子 + transform(code, id) { + // 转换代码 + if (id.endsWith('.custom')) { + return { + code: convertCustomFormat(code), + map: null + } + } + }, + + // 模块解析钩子 + resolveId(source) { + // 自定义模块解析 + if (source === 'virtual-module') { + return '\0virtual-module' + } + }, + + // 加载钩子 + load(id) { + // 加载模块内容 + if (id === '\0virtual-module') { + return 'export const msg = "Hello from virtual module"' + } + }, + + // 构建钩子 + buildStart() { + console.log('Build started') + }, + + buildEnd() { + console.log('Build completed') + } + } +} +``` + +#### 环境变量插件 +```typescript +import type { Plugin } from 'vite' + +export function envPlugin(): Plugin { + return { + name: 'env-plugin', + config(config, { mode }) { + // 加载环境变量 + const env = loadEnv(mode, process.cwd(), '') + + return { + define: { + 'import.meta.env': JSON.stringify(env) + } + } + } + } +} +``` + +### 第三方插件推荐 + +#### 路径别名插件 +```typescript +import { viteCommonjs } from '@originjs/vite-plugin-commonjs' + +export default defineConfig({ + plugins: [ + react(), + viteCommonjs() // 支持 CommonJS 模块 + ] +}) +``` + +#### 压缩插件 +```typescript +import viteCompression from 'vite-plugin-compression' + +export default defineConfig({ + plugins: [ + viteCompression({ + verbose: true, + disable: false, + threshold: 10240, + algorithm: 'gzip', + ext: '.gz' + }) + ] +}) +``` + +#### 组件按需加载 +```typescript +import Components from 'unplugin-vue-components/vite' +import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers' + +export default defineConfig({ + plugins: [ + Components({ + resolvers: [ + AntDesignVueResolver() + ] + }) + ] +}) +``` + +## TypeScript 配置 + +### tsconfig.json +```json +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src", "vite.config.ts"], + "references": [{ "path": "./tsconfig.node.json" }] +} +``` + +### tsconfig.node.json (为 Vite 配置文件提供类型) +```json +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} +``` + +## 高级配置 + +### 多入口配置 +```typescript +import { defineConfig } from 'vite' +import path from 'path' + +export default defineConfig({ + build: { + rollupOptions: { + input: { + main: path.resolve(__dirname, 'index.html'), + admin: path.resolve(__dirname, 'admin.html'), + landing: path.resolve(__dirname, 'landing.html') + } + } + } +}) +``` + +### 库模式配置 +```typescript +export default defineConfig({ + build: { + lib: { + entry: path.resolve(__dirname, 'src/index.ts'), + name: 'MyLibrary', + fileName: (format) => `my-library.${format}.js`, + formats: ['es', 'umd'] + }, + rollupOptions: { + external: ['react', 'react-dom'], + output: { + globals: { + react: 'React', + 'react-dom': 'ReactDOM' + } + } + } + } +}) +``` + +### SSR 配置 +```typescript +export default defineConfig({ + build: { + ssr: true, // 启用 SSR + outDir: 'dist/server', // SSR 输出目录 + rollupOptions: { + input: './src/entry-server.ts' + } + }, + server: { + middlewareMode: 'ssr' // SSR 模式 + } +}) +``` + +## 配置最佳实践 + +### 环境分离 +```typescript +// vite.config.ts +export default defineConfig(({ mode }) => { + return { + plugins: [ + mode === 'development' ? devPlugin() : prodPlugin() + ], + server: mode === 'development' ? devServerConfig : {} + } +}) +``` + +### 配置复用 +```typescript +// shared-config.ts +export const baseConfig = { + resolve: { + alias: { + '@': path.resolve(__dirname, './src') + } + } +} + +export const devConfig = { + ...baseConfig, + server: { + port: 3000 + } +} + +export const buildConfig = { + ...baseConfig, + build: { + outDir: 'dist' + } +} +``` + +### 配置验证 +```typescript +function validateConfig(config: UserConfig) { + if (!config.plugins) { + throw new Error('Plugins are required') + } + + // 验证路径别名 + if (config.resolve?.alias) { + Object.entries(config.resolve.alias).forEach(([key, value]) => { + if (!path.isAbsolute(value)) { + throw new Error(`Alias ${key} must be an absolute path`) + } + }) + } +} +``` + +## 故障排查 + +### 配置加载问题 +```bash +# 调试配置加载 +DEBUG=vite:config npm run dev + +# 检查配置语法 +node -c vite.config.ts +``` + +### 路径解析问题 +```typescript +// 使用 debug 插件检查路径 +import { defineConfig } from 'vite' + +export default defineConfig({ + resolve: { + alias: { + 'debug': require.resolve('debug') + } + } +}) +``` + +### 插件冲突诊断 +```typescript +// 在插件开发中添加日志 +export function debugPlugin() { + return { + name: 'debug-plugin', + transform(code, id) { + console.log('Transforming:', id) + return null + } + } +} +``` + +配置是 Vite 项目的核心,合理的配置能显著提升开发体验和构建质量。根据项目规模和需求,逐步完善配置是最佳实践。 + +相关文档: +- [[DEV/VITE/架构原理.md]] - 理解配置的底层机制 +- [[DEV/VITE/优化实践.md]] - 配置优化技巧 diff --git a/assets/img/Pasted image 20260417125039.png b/assets/img/Pasted image 20260417125039.png new file mode 100644 index 0000000000000000000000000000000000000000..062c999973e2330ff66c24c95cbb9dbebd9933de GIT binary patch literal 49989 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

-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 literal 0 HcmV?d00001 diff --git a/assets/img/Pasted image 20260417125713.png b/assets/img/Pasted image 20260417125713.png new file mode 100644 index 0000000000000000000000000000000000000000..96f23003465fa2c5bbb7d652eb415141a0e9b03b GIT binary patch literal 63250 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 literal 0 HcmV?d00001 diff --git a/config/agent/file-creation-standards.md b/config/agent/file-creation-standards.md new file mode 100644 index 0000000..8059226 --- /dev/null +++ b/config/agent/file-creation-standards.md @@ -0,0 +1,63 @@ +--- +tags: [config, agent, standards] +create time: 2026-04-17 11:29 +update time: 2026-04-18 +--- + +# 文件创建规范 + +## 核心规则 + +**所有新文件必须遵循以下结构:** + +```yaml +--- +tags: [] +create time: YYYY-MM-DD HH:mm +--- + +# <标题> + +## 概述 +[简短描述] + +## 正文 +[内容] + +## 关联笔记 +- [[相关笔记]] +``` + +## 必填项 + +| 字段 | 格式 | 说明 | +|------|------|------| +| `tags` | 数组 | 必须存在 `[]`,智能填充相关标签 | +| `create time` | `YYYY-MM-DD HH:mm` | 当前系统时间 | + +## 文档结构 + +1. **## 概述**: 简短描述文档内容,不要保留占位文本 +2. **## 正文**: 主要内容,详略得当,注重拓展进阶 +3. **## 关联笔记**: 相关笔记 Wiki-links,若无可移除 + +## 内容规范 + +- **代码示例**: 优先 Go (后端) + React/TS (前端) +- **图表**: 选择性使用 Mermaid +- **风格**: 详略得当,注重实用性 + +## Agent 执行步骤 + +1. 读取模板: `[[config/template/default-template.md]]` +2. 创建文件并填充 `create time` +3. 智能填充 `tags` +4. 替换占位文本为实际内容 +5. 添加相关 Wiki-links + +## 特殊情况 + +配置文件、开发文件等可灵活调整结构,但需保留: +- `tags` 字段 +- `create time` 记录 +- 清晰的组织结构 diff --git a/config/agent/learning-index.md b/config/agent/learning-index.md new file mode 100644 index 0000000..88f6d4e --- /dev/null +++ b/config/agent/learning-index.md @@ -0,0 +1,33 @@ +--- +tags: [index, learning, cs] +create time: 2026-04-17 +--- + +# 计算机科学学习索引 + +## 学习领域导航 + +### 计算机技术基础 +- [[CS/README|CS]] - 计算机科学总览 + - [[CS/OS/README|OS]] - 操作系统 + - [[CS/NET/README|NET]] - 计算机网络 + - [[CS/NET/Authorization]] - Authorization 授权机制 + - [[CS/DB/README|DB]] - 数据库 + - [[CS/TOOLS/README|TOOLS]] - 工具使用 + +### 开发实践 +- [[DEV/GO/README|GO]] - Go语言服务端开发 +- [[DEV/REACT/README|REACT]] - React前端开发 + +## 目录缩写说明 + +| 缩写 | 全称 | 描述 | +|------|------|------| +| CS | Computer Science | 计算机科学 | +| OS | Operating Systems | 操作系统 | +| NET | Network | 计算机网络 | +| DB | Database | 数据库 | +| TOOLS | Tools | 工具使用 | +| DEV | Development | 开发实践 | +| GO | Go Language | Go语言 | +| REACT | React Framework | React框架 | diff --git a/config/plugin/插件配置.md b/config/plugin/插件配置.md new file mode 100644 index 0000000..4b92c60 --- /dev/null +++ b/config/plugin/插件配置.md @@ -0,0 +1,55 @@ +# Obsidian 插件配置 + +> 最后更新: 2026-04-20 + +## 核心插件 + +| 插件名称 | 说明 | 状态 | +|---------|------|------| +| daily-notes | 每日笔记 | 启用 | +| file-explorer | 文件浏览器 | 启用 | +| global-search | 全局搜索 | 启用 | +| switcher | 快速切换 | 启用 | +| graph | 关系图谱 | 启用 | +| backlink | 反向链接 | 启用 | +| outline | 大纲视图 | 启用 | +| templates | 模板 | 启用 | +| word-count | 字数统计 | 启用 | + +## 社区插件 + +### Better Export PDF (v1.11.0) +- **ID**: better-export-pdf +- **作者**: l1xnan +- **说明**: 导出笔记为 PDF,支持预览、添加书签大纲和页眉页脚 +- **仅桌面**: 是 + +### Claudian (v2.0.3) +- **ID**: claudian +- **作者**: Yishen Tu +- **说明**: 将 Claude Code 嵌入为 AI 协作者,提供文件读写、搜索、命令和多步工作流能力 +- **仅桌面**: 是 + +### Git (v2.38.2) +- **ID**: obsidian-git +- **作者**: Vinzent03 +- **说明**: 集成 Git 版本控制,支持自动备份和高级功能 +- **仅桌面**: 否 + +### Style Settings (v1.0.9) +- **ID**: obsidian-style-settings +- **作者**: mgmeyers +- **说明**: 提供主题、插件和代码片段 CSS 变量的调整控件 +- **仅桌面**: 否 + +### Templater (v2.19.0) +- **ID**: templater-obsidian +- **作者**: SilentVoid13 +- **说明**: 创建和使用模板 +- **仅桌面**: 否 + +## 配置文件位置 + +- 核心插件配置: `.obsidian/core-plugins.json` +- 社区插件列表: `.obsidian/community-plugins.json` +- 插件数据目录: `.obsidian/plugins/` diff --git a/config/template/default-template.md b/config/template/default-template.md new file mode 100644 index 0000000..7c1e7f2 --- /dev/null +++ b/config/template/default-template.md @@ -0,0 +1,18 @@ +--- +tags: [] +create time: <% tp.date.now("YYYY-MM-DD HH:mm") %> +--- + +# <% tp.file.title %> + +## 概述 + +添加笔记的概述或简短描述。 + +## 正文 + +在这里写下你的笔记内容... + +## 关联笔记 + +- 添加关联笔记的链接 diff --git a/金山办公作业/Week05/Docker 命名卷 vs 挂载.md b/金山办公作业/Week05/Docker 命名卷 vs 挂载.md new file mode 100644 index 0000000..70cced2 --- /dev/null +++ b/金山办公作业/Week05/Docker 命名卷 vs 挂载.md @@ -0,0 +1,276 @@ +--- +tags: [docker, 存储, 容器技术, 数据持久化] +create time: 2026-04-18 23:59 +--- + +# Docker 命名卷 vs 挂载 + +## 概述 + +本文档详细对比 Docker 中两种主要的数据存储方式:**命名卷(Named Volumes)** 和 **绑定挂载(Bind Mounts)**,帮助理解它们的适用场景、优缺点及最佳实践。 + +## 正文 + +### 一、核心概念 + +#### 命名卷(Named Volumes) + +由 Docker 管理的存储卷,存储在 Docker 管理的目录中(Linux 默认 `/var/lib/docker/volumes/`)。 + +```bash +# 创建命名卷 +docker volume create my-data + +# 使用命名卷启动容器 +docker run -d --name myapp -v my-data:/app/data nginx + +# 列出所有卷 +docker volume ls + +# 查看卷详情 +docker volume inspect my-data + +# 删除卷 +docker volume rm my-data +``` + +#### 绑定挂载(Bind Mounts) + +将主机上的文件或目录直接挂载到容器中,提供绝对路径映射。 + +```bash +# 使用绑定挂载 +docker run -d --name myapp -v /host/path:/container/path nginx + +# Windows 路径示例 +docker run -d --name myapp -v C:\Users\data:/app/data nginx +``` + +### 二、对比表格 + +| 特性 | 命名卷 | 绑定挂载 | +|------|--------|----------| +| **管理方式** | Docker 管理 | 用户管理 | +| **存储位置** | Docker 默认目录 | 用户指定的任意位置 | +| **跨平台兼容性** | ✅ 良好 | ❌ 路径差异大 | +| **权限处理** | ✅ Docker 自动处理 | ⚠️ 需手动配置 | +| **安全性** | ✅ 隔离性好 | ⚠️ 直接访问主机 | +| **备份难度** | ⚠️ 需特殊命令 | ✅ 直接复制 | +| **性能** | ✅ 优化过 | ⚠️ 可能有额外开销 | +| **开发调试** | ⚠️ 不便 | ✅ 实时同步 | + +### 三、深入分析 + +#### 3.1 命名卷详解 + +**优点:** +- **跨平台一致性**:Windows、macOS、Linux 使用相同的卷名称 +- **自动初始化**:首次使用时自动创建并初始化容器镜像中的数据 +- **权限管理**:Docker 自动处理 SELinux/AppArmor 权限问题 +- **备份方便**:可通过 `docker run --volumes-from` 备份 + +```bash +# 备份命名卷 +docker run --rm -v my-data:/data -v $(pwd):/backup alpine \ + tar czf /backup/my-data-backup.tar.gz /data + +# 恢复命名卷 +docker run --rm -v my-data:/data -v $(pwd):/backup alpine \ + tar xzf /backup/my-data-backup.tar.gz -C / +``` + +**缺点:** +- **位置不透明**:需通过 `docker volume inspect` 查看实际路径 +- **开发不便**:无法直接编辑主机上的文件 +- **清理复杂**:未使用的卷需手动清理 + +**适用场景:** +- ✅ **生产环境**:应用的数据库文件、配置文件 +- ✅ **微服务架构**:服务间共享数据 +- ✅ **跨平台开发**:团队混合使用不同操作系统 + +#### 3.2 绑定挂载详解 + +**优点:** +- **开发体验极佳**:实时代码同步,IDE 直接编辑 +- **访问方便**:直接操作主机文件系统 +- **调试友好**:可快速查看和修改容器内文件 +- **简单直接**:无需额外命令管理 + +```bash +# 开发环境常用挂载 +docker run -d --name dev-server \ + -v $(pwd):/app \ + -v /app/node_modules \ # 避免覆盖 node_modules + -p 8080:8080 \ + node:18 npm run dev +``` + +**缺点:** +- **权限问题**:Linux 下可能遇到 UID/GID 不匹配 +- **路径依赖**:绝对路径在团队协作中易出错 +- **安全风险**:容器可越界访问主机文件 +- **跨平台复杂**:Windows/macOS 路径格式不同 + +**权限处理示例:** +```bash +# 方法1:使用 --user 参数 +docker run --user $(id -u):$(id -g) -v $(pwd):/workdir ... + +# 方法2:修复权限 +docker run -v $(pwd):/data alpine chown -R $(id -u):$(id -g) /data + +# 方法3:使用匿名卷组合 +docker run -v $(pwd):/app -v /app/node_modules nginx +``` + +**适用场景:** +- ✅ **本地开发**:代码热重载、实时调试 +- ✅ **配置注入**:注入主机上的配置文件 +- ✅ **日志收集**:将容器日志直接输出到主机 +- ✅ **CI/CD**:挂载源代码进行构建 + +### 四、决策流程图 + +```mermaid +graph TD + A[需要持久化数据?] -->|否| D[无需卷] + A -->|是| B{是否需要实时编辑?} + B -->|是| C[绑定挂载 Bind Mount] + B -->|否| E{环境类型?} + E -->|开发| F[绑定挂载优先] + E -->|生产| G{是否跨平台?} + G -->|是| H[命名卷 Named Volume] + G -->|否| I[绑定挂载] +``` + +### 五、混合使用策略 + +最佳实践是结合两者优势: + +```yaml +# docker-compose.yml 示例 +version: '3.8' +services: + webapp: + build: . + volumes: + # 开发代码实时同步 + - ./:/app + # 生产环境依赖分离 + - /app/node_modules + # 配置文件(主机路径) + - ./config:/app/config:ro + # 持久化数据(命名卷) + - app-data:/app/data + +volumes: + app-data: + driver: local +``` + +**分层策略:** +1. **代码层**:绑定挂载(开发环境)/ 匿名卷(生产环境) +2. **配置层**:绑定挂载(RO 只读模式) +3. **数据层**:命名卷 +4. **依赖层**:匿名卷(避免覆盖) + +### 六、进阶技巧 + +#### 6.1 卷驱动 + +```bash +# 使用云存储驱动 +docker volume create --driver driver-name cloud-volume + +# NFS 挂载 +docker volume create --driver local \ + -o type=nfs \ + -o device=:/path/to/dir \ + -o o=addr=nfs-server-ip,rw \ + nfs-volume +``` + +#### 6.2 临时文件系统 + +```bash +# 使用临时文件系统(速度最快,容器删除后消失) +docker run -d --tmpfs /tmp:rw,size=100m,mode=1777 nginx +``` + +#### 6.3 清理未使用资源 + +```bash +# 清理未使用的卷 +docker volume prune + +# 清理所有未使用资源(谨慎使用) +docker system prune -a --volumes +``` + +### 七、常见问题排查 + +#### 问题1:卷权限不足 + +```bash +# 查看卷的实际权限 +docker run --rm -v my-data:/data alpine ls -la /data + +# 临时容器修复 +docker run --rm -v my-data:/data alpine chown -R 1000:1000 /data +``` + +#### 问题2:卷有数据但容器看不到 + +```bash +# 检查卷内是否有内容 +docker run --rm -v my-data:/data alpine ls -la /data + +# 检查镜像是否有 /app/data 目录 +docker run --rm nginx ls -la /app/data + +# 使用 --workdir 验证 +docker run --rm -w /app -v my-data:/app/data nginx ls -la +``` + +#### 问题3:Windows 下绑定挂载失败 + +```bash +# 确保在 Docker Desktop 设置中共享了驱动器 + +# 验证路径格式 +docker run -v C:/Users/data:/data alpine ls /data # ✅ 正确 +docker run -v C:\Users\data:/data alpine ls /data # ❌ 错误 +``` + +### 八、最佳实践总结 + +✅ **选择命名卷的情况:** +- 生产环境数据持久化 +- 需要跨平台兼容性 +- 多个容器共享数据 +- 数据库文件存储 +- 敏感数据隔离 + +✅ **选择绑定挂载的情况:** +- 本地开发环境 +- 需要实时编辑代码 +- 注入配置文件(RO 模式) +- CI/CD 流程 +- 收集日志文件 + +✅ **通用建议:** +- 生产环境优先使用命名卷 +- 开发环境使用绑定挂载提高效率 +- 混合使用实现最佳效果 +- 定期清理未使用的卷 +- 为关键卷添加备份策略 +- 使用只读挂载保护配置文件 +- 文档化所有挂载策略 + +## 关联笔记 + +- [[金山办公作业/Week05/Docker 基础命令]] +- [[金山办公作业/Week05/Docker Compose 实践]] +- [[金山办公作业/Week05/Docker 网络管理]] +- [[金山办公作业/Week05/容器化最佳实践]] diff --git a/金山办公作业/Week05/Nginx.md b/金山办公作业/Week05/Nginx.md new file mode 100644 index 0000000..165a7a5 --- /dev/null +++ b/金山办公作业/Week05/Nginx.md @@ -0,0 +1,203 @@ +```nginx +user nginx; +worker_processes 1; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip 压缩 + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; + + server { + listen 80; + server_name localhost; + + # 前端静态文件 + root /usr/share/nginx/html; + index index.html; + + # 前端路由支持 + location / { + try_files $uri $uri/ /index.html; + } + + # 后端 API 代理 + location /api/ { + proxy_pass http://backend:8080; + proxy_http_version 1.1; + + # 代理请求头 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket 支持(如果需要) + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # 超时设置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + } +} +``` + +这是一个非常标准、功能完善、优化级别很高的 Nginx 配置,通常用于部署一个结合了**前端 SPA(单页应用)**和**后端 API**的现代 Web 应用。 + +我将从以下几个部分为您进行详细的解读: + +--- + +## 🚀 整体结构与功能总结 + +**该配置的职责是:** + +1. **作为反向代理(Reverse Proxy)**:它接收所有来自外部(`localhost:80`)的请求。 +2. **服务前端静态资源**:负责提供基于 `root` 目录的 HTML、CSS、JS 等文件,并支持前端路由(SPA)。 +3. **转发后端 API 请求**:将所有以 `/api/` 开头的请求,转发给指定的后端服务(`http://backend:8080`)。 +4. **优化性能**:通过设置 Gzip 压缩、HTTP 连接优化和定时器设置,确保传输效率。 + +--- + +## 🔍 详细配置解读(按代码块) + +### 1. 全局配置(Global Scope) + +这部分设置了 Nginx 整个运行环境的参数。 + +| 指令 | 含义 | 作用和解释 | +| :--- | :--- | :--- | +| `user nginx;` | **用户权限** | 指定 Nginx 使用 `nginx` 系统用户来运行,这是最佳实践,避免使用 `root`。 | +| `worker_processes 1;` | **工作进程** | 设置工作进程数量。`1` 表示使用单个进程。理想情况下,应设置为 `auto` 或 CPU 核数,以提高并发能力。 | +| `error_log ... warn;` | **错误日志** | 配置错误日志路径和级别。`warn` 级别表示只记录警告及以上级别的错误。 | +| `pid /var/run/nginx.pid;` | **进程ID文件** | 指定存储 Nginx 主进程 ID 的文件路径。 | +| `events { ... }` | **事件处理** | 负责管理网络连接(Connection)。 | +| `worker_connections 1024;` | **连接数限制** | 每个工作进程允许打开的最大并发连接数。1024 是一个常见的保守设置。 | + +### 2. HTTP 块配置(`http` Block) + +这块定义了所有 HTTP 服务通用的配置。 + +| 指令 | 含义 | 作用和解释 | +| :--- | :--- | :--- | +| `include /etc/nginx/mime.types;` | **文件类型包含** | 引入标准的 MIME 类型定义文件,使 Nginx 能够正确识别和发送文件的 Content-Type 头部。 | +| `default_type application/octet-stream;` | **默认类型** | 如果无法确定文件类型,则默认将其视为二进制流。 | +| `log_format main '...'` | **自定义日志格式** | 定义了访问日志的详细格式。它记录了请求来源 IP、时间、请求方式/路径、HTTP 状态码、传输字节数等,非常完整。 | +| `access_log ... main;` | **访问日志** | 指定使用前面定义的 `main` 格式,并将访问记录写入 `/var/log/nginx/access.log`。 | +| `sendfile on;` | **性能优化** | 启用 `sendfile` 功能,允许 Nginx 直接将内核缓冲区的内容发送给客户端,跳过用户空间的复制,极大地提升文件传输效率。 | +| `tcp_nopush on;` | **性能优化** | 当发送多个数据包时,确保这些数据包被一次性发送,避免网络延迟和不必要的包分割。 | +| `tcp_nodelay on;` | **性能优化** | 禁用发送数据包的缓冲机制,数据发送后立即离开,降低延迟,适用于需要低延迟的应用。 | +| `keepalive_timeout 65;` | **连接保持** | 设置客户端与服务器保持空闲连接的最大秒数(这里是 65 秒)。 | +| `gzip on;` | **压缩开启** | 启用 Gzip 压缩,减少传输数据量,提升速度。 | +| `gzip_vary on;` | **缓存优化** | 当启用压缩后,所有响应头都必须包含 `Vary: Accept-Encoding`,确保缓存服务器不会错误地缓存未压缩或不同压缩级别的内容。 | +| `gzip_proxied any;` | **压缩范围** | 无论请求是否来自代理,都进行压缩。 | +| `gzip_types ...` | **压缩类型** | 列出需要被 Gzip 压缩的文件类型(如文本、JS、CSS、JSON)。 | + +### 3. 服务器配置(`server` Block) + +这部分定义了一个虚拟主机(Virtual Host),决定了 Nginx 在哪个端口和哪个域名上监听请求。 + +| 指令 | 含义 | 作用和解释 | +| :--- | :--- | :--- | +| `listen 80;` | **监听端口** | 表示该服务器实例监听所有进来的 HTTP 流量(端口 80)。 | +| `server_name localhost;` | **虚拟域名** | 限制此配置只响应 `localhost` 的请求。 | + +### 4. 路径匹配配置(`location` Blocks) + +这是最核心的逻辑部分,决定了不同 URL 路径如何被处理。 + +#### 🅰️ 前端静态文件处理 (SPA 路由支持) + +```nginx +# 前端静态文件 +root /usr/share/nginx/html; +index index.html; + +# 前端路由支持 +location / { + try_files $uri $uri/ /index.html; +} +``` + +* **`root /usr/share/nginx/html;`**: 指定所有静态文件的根目录。 +* **`location /`**: 匹配所有路径(所有请求)。 +* **`try_files $uri $uri/ /index.html;`**: **这是支持单页应用(SPA)的关键指令。** + 1. **`$uri`**: Nginx 尝试从 `root` 目录查找请求的完整文件(例如:`/js/main.js`)。 + 2. **`$uri/`**: 如果找不到文件,则尝试查找是否是请求的一个目录。 + 3. **`/index.html`**: 如果前两者都失败(即,用户访问的是一个不存在的路径,但这个路径应该被 SPA 的前端路由接管,如 `/user/profile`),则内部重定向(Rewrite)请求到 `/index.html`。 + * **总结:** 确保所有的请求最终都会被 `index.html` 接收,让前端 JavaScript 框架(Vue/React/Angular)去判断路由,从而实现 SPA 的前端路由。 + +#### 🅱️ 后端 API 代理处理 + +```nginx +location /api/ { + proxy_pass http://backend:8080; + # ... 其他配置 ... +} +``` + +* **`location /api/`**: 匹配所有以 `/api/` 开头的请求(例如:`/api/users`)。 +* **`proxy_pass http://backend:8080;`**: 这是核心代理指令。它将所有匹配的请求,转发到内部网络服务 `backend` 的 `8080` 端口。 + +**代理请求头设置 (Headers)** + +这些指令至关重要,因为它们确保了后端服务(`backend`)能够获取到客户端的真实信息,而不是代理服务器本身的 IP 地址。 + +* `proxy_http_version 1.1;`: 强制代理使用 HTTP/1.1,确保高级特性(如 Connection 头)可用。 +* `proxy_set_header Host $host;`: 告诉后端服务客户端实际请求的 Host 名称。 +* `proxy_set_header X-Real-IP $remote_addr;`: 将客户端的真实 IP 地址传递给后端。 +* `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`: 记录所有代理跳过的 IP 地址链(用于安全审计和日志)。 +* `proxy_set_header X-Forwarded-Proto $scheme;`: 告知后端服务原始请求是通过 HTTP 还是 HTTPS 传输的。 + +**WebSocket 支持** + +* `proxy_set_header Upgrade $http_upgrade;` +* `proxy_set_header Connection "upgrade";`: 这两行代码是专门为了支持 **WebSocket** 连接的。WebSocket 需要特殊的 HTTP 升级(Upgrade)头,用于从传统的 HTTP 长连接模式切换到双向的 WS 协议。 + +**超时设置** + +* `proxy_connect_timeout 60s;`:连接后端服务时等待的最大时间。 +* `proxy_send_timeout 60s;`:将请求发送给后端时等待的最大时间。 +* `proxy_read_timeout 60s;`:从后端服务读取响应数据时等待的最大时间。 +* **目的:** 将所有超时设置设置为 60 秒,确保即使后端处理耗时较长(比如复杂的报告生成),Nginx也不会提前中断连接。 + +--- + +## 🌟 总结与最佳实践点 + +1. **职责分离完美体现:** 通过 `location /api/` 和 `location /` 的隔离,Nginx完美地将“站内服务(静态资源)”和“后端数据服务(API)”分离,使得架构清晰。 +2. **性能优化到位:** 包含了 `sendfile`、`tcp_nopush`、`tcp_nodelay` 以及 `gzip` 等一系列优化,配置的性能起点很高。 +3. **健壮性高:** `try_files` 的使用确保了即使是客户端路径错误,应用也能重定向到根页面,避免出现“文件找不到”的错误,保证了用户体验。 +4. **反向代理标准配置:** 后端代理部分的配置(特别是 `X-Real-IP` 和 `X-Forwarded-*` 头)是所有企业级反向代理的标配,非常专业。 + +**建议改进点(如果适用):** + +1. **Worker Processes:** 如果部署在服务器上,建议将 `worker_processes 1;` 改为 `worker_processes auto;` 或根据 CPU 核数设置,以充分利用服务器性能。 +2. **HTTPS/SSL:** 实际生产环境中,`server` 块应该增加 `listen 443 ssl;`,并在其中配置 SSL 证书,确保所有的通信都是加密的。 \ No newline at end of file diff --git a/金山办公作业/Week05/PLAN.md b/金山办公作业/Week05/PLAN.md new file mode 100644 index 0000000..35bb1bf --- /dev/null +++ b/金山办公作业/Week05/PLAN.md @@ -0,0 +1,512 @@ +# AI 智能单词本 - 开发实施方案 + +## 项目概述 + +本项目是一个前后端分离的英语学习应用,通过 AI 生成单词释义和例句,帮助用户构建个人单词本。 + +--- + +## 一、技术架构设计 + +### 1.1 整体架构图 + +```mermaid +graph TB + subgraph "客户端层" + User[用户] + end + + subgraph "应用层" + FE[Nginx / Vite] + BE[Go + Gin] + end + + subgraph "数据层" + DB[(MySQL 8.0)] + end + + subgraph "外部服务" + AI[AI大模型
DeepSeek / 通义千问] + end + + User --> FE + FE --> BE + BE --> AI + BE --> DB + + style FE fill:#e1f5ff + style BE fill:#ffe1e1 + style DB fill:#e1ffe1 + style AI fill:#ffe1ff +``` + +### 1.2 核心技术栈 + +| 层级 | 技术选型 | 说明 | +|------|----------|------| +| 前端 | Vite + Vue3/React/Vanilla | 现代化构建工具,快速开发 | +| 前端服务器 | Nginx | 生产环境静态资源服务与反向代理 | +| 后端 | Go 1.21+ + Gin | 高性能 Web 框架 | +| 数据库 | MySQL 8.0 | 关系型数据库 | +| ORM | GORM | Go 语言 ORM 框架 | +| 身份认证 | JWT | 无状态 token 认证 | +| 配置管理 | Viper | 配置文件管理 | +| 容器化 | Docker + Docker Compose | 统一部署环境 | + +### 1.3 跨域处理策略 + +```mermaid +graph LR + subgraph "开发环境" + FE[Vite
proxy配置] -->|代理| BE[后端:8080] + end + + subgraph "生产环境" + U[用户请求] --> FE2[Nginx:80] + FE2 -->|/api/* 代理转发| BE2[后端:8080] + FE2 -->|静态资源| SR[dist/] + end +``` + +**原则:后端代码严禁配置 CORS,统一通过代理解决跨域。** + +--- + +## 二、目录结构规划 + +``` +week05/homework/docker-gin +├── backend/ # 后端 Go 项目 +│ ├── Dockerfile # Go 多阶段构建镜像 +│ ├── main.go # 应用入口 +│ ├── .env.example # 环境变量示例文件 +│ ├── config/ # 配置管理 +│ │ └── config.go # Viper 配置加载 +│ ├── model/ # 数据模型层 +│ │ ├── user.go # 用户模型 +│ │ └── word.go # 单词模型 +│ ├── api/ # 路由与控制器层 +│ │ └── handler.go # 请求处理器 +│ ├── service/ # 业务逻辑层 +│ │ ├── auth.go # 认证服务 +│ │ ├── ai.go # AI 调用服务 +│ │ └── word.go # 单词业务 +│ ├── middleware/ # 中间件 +│ │ └── jwt.go # JWT 鉴权中间件 +│ ├── utils/ # 工具函数 +│ │ └── hash.go # 密码加密工具 +│ └── go.mod # Go 依赖管理 +│ +├── frontend/ # 前端项目 +│ ├── Dockerfile # Nginx 镜像构建 +│ ├── nginx.conf # 生产环境 Nginx 配置 +│ ├── package.json # 依赖管理 +│ ├── vite.config.js/ts # Vite 配置(开发代理) +│ ├── index.html # HTML 入口 +│ └── src/ +│ ├── main.js/tsx # 应用入口 +│ ├── api/ # API 请求封装 +│ ├── components/ # 页面组件 +│ │ ├── common/ # 公共组件 +│ │ ├── auth/ # 登录/注册 +│ │ └── word/ # 单词相关 +│ ├── router/ # 路由配置 +│ ├── store/ # 状态管理 +│ └── utils/ # 工具函数 +│ +├── docs/ # 项目文档 +│ ├── api.md # API 接口文档 +│ ├── db.md # 数据库设计文档 +│ └── init.sql # 数据库初始化脚本 +│ +├── docker-compose.yml # 容器编排配置 +└── README.md # 项目说明与运行指南 +``` + +--- + +## 三、数据库设计 + +### 3.1 ER 图 + +```mermaid +erDiagram + USER ||--|{ WORD : has + USER { + uuid id PK "用户ID" + string username UK "用户名" + string password "密码(hash)" + datetime created_at "创建时间" + datetime updated_at "更新时间" + datetime deleted_at "删除时间(软删除)" + } + WORD { + uuid id PK "单词记录ID" + uuid user_id FK "所属用户ID" + string word "单词" + text definition "释义" + json examples "例句列表" + string ai_provider "AI模型来源" + datetime created_at "创建时间" + datetime updated_at "更新时间" + datetime deleted_at "删除时间(软删除)" + } +``` + +### 3.2 表结构设计思路 + +1. **用户表 (users)** + - 使用 UUID 作为主键,安全性更高 + - 用户名设置唯一索引,防止重复注册 + - 密码字段存储 bcrypt 哈希值,严禁明文 + - 包含标准时间戳字段 + - 支持软删除(deleted_at) + +2. **单词表 (words)** + - 使用 UUID 作为主键 + - 通过 user_id 外键关联用户 + - 单词字段设为索引,配合 user_id 提高查询效率 + - 例句使用 JSON 类型存储 3 条数据 + - 记录 AI 来源以支持多模型切换 + - 支持软删除和分页查询 + +--- + +## 四、API 接口设计 + +### 4.1 接口总览 + +```mermaid +graph LR + subgraph "无需鉴权" + A[POST /api/auth/register] + B[POST /api/auth/login] + end + + subgraph "需要鉴权" + C[GET /api/words/query] + D[POST /api/words/save] + E[GET /api/words/list] + F[DELETE /api/words/:id] + end +``` + +### 4.2 接口设计要点 + +| 接口 | 方法 | 说明 | 关键参数 | +|------|------|------|----------| +| 注册 | POST | 用户名密码注册 | username, password | +| 登录 | POST | 返回 JWT Token | username, password | +| 查询单词 | GET | AI 生成释义和例句 | word, ai_provider | +| 保存单词 | POST | 持久化到数据库 | word, definition, examples, ai_provider | +| 单词列表 | GET | 分页获取用户单词 | page, page_size | +| 删除单词 | DELETE | 软删除单词记录 | 路径参数 id | + +--- + +## 五、核心功能实现步骤 + +### 5.1 数据库初始化流程 + +```mermaid +sequenceDiagram + participant DC as docker-compose + participant M as MySQL容器 + participant SQL as init.sql + + DC->>M: 启动容器 + DC->>M: 挂载 init.sql + M->>M: 初始化数据库 + M->>SQL: 执行建表语句 + SQL-->>M: 完成建表 + M-->>DC: 准备就绪 +``` + +**关键点:** +- 通过 docker-compose.yml 将 `docs/init.sql` 挂载到容器的 `/docker-entrypoint-initdb.d/` 目录 +- MySQL 容器首次启动时自动执行该目录下的 SQL 脚本 +- 严禁在代码中调用 GORM 的 AutoMigrate + +### 5.2 用户认证流程 + +```mermaid +sequenceDiagram + participant U as 用户 + participant FE as 前端 + participant BE as 后端 + + Note over U,BE: 注册流程 + U->>FE: 提交用户名密码 + FE->>BE: POST /api/auth/register + BE->>BE: 验证用户名重复 + BE->>BE: bcrypt 哈希密码 + BE->>BE: 存入数据库 + BE-->>FE: 注册成功 + + Note over U,BE: 登录流程 + U->>FE: 提交用户名密码 + FE->>BE: POST /api/auth/login + BE->>BE: 验证用户存在 + BE->>BE: bcrypt 验证密码 + BE->>BE: 生成 JWT Token + BE-->>FE: 返回 Token + FE->>FE: 存入 localStorage +``` + +### 5.3 智能查询单词流程 + +```mermaid +flowchart TD + A[接收查询请求
word + ai_provider] --> B{鉴权检查} + B -->|失败| C[返回 401 错误] + B -->|通过| D{检查数据库} + D -->|已保存| E[直接返回数据库记录] + D -->|未保存| F[调用 AI 接口] + F --> G[解析 AI 响应] + G --> H[返回 AI 结果至前端
不保存到数据库] + H --> I[前端展示查询结果] + I --> J{用户点击保存?} + J -->|是| K[前端调用保存接口] + J -->|否| L[结束] + K --> M[后端写入数据库] +``` + +### 5.4 AI 调用服务设计 + +```mermaid +graph TB + subgraph "AI 调用层" + Service[AI Service] + end + + subgraph "AI 提供商" + DS[DeepSeek] + QW[通义千问] + end + + Service -->|根据 ai_provider| DS + Service -->|根据 ai_provider| QW + + DS -->|返回结构化 JSON| Service + QW -->|返回结构化 JSON| Service + +``` + +--- + +## 六、开发环境配置 + +### 6.1 前端开发配置(Vite Proxy) + +在 `vite.config.js/ts` 中配置代理,解决开发环境跨域问题: + +```javascript +export default { + server: { + proxy: { + '/api': { + target: 'http://localhost:8080', // 后端服务地址 + changeOrigin: true, // 改变请求源 + } + } + } +} +``` + +### 6.2 后端配置管理 + +使用 Viper 管理配置,支持从 `.env` 文件加载: + +| 配置项 | 说明 | 示例 | +|--------|------|------| +| DB_HOST | 数据库地址 | db | +| DB_PORT | 数据库端口 | 3306 | +| DB_USER | 数据库用户 | root | +| DB_PASSWORD | 数据库密码 | password | +| DB_NAME | 数据库名 | wordbook | +| JWT_SECRET | JWT 签名密钥 | secret_key | +| DEEPSEEK_API_KEY | DeepSeek 密钥 | sk-xxx | +| QIANWEN_API_KEY | 通义千问密钥 | xxx | + +--- + +## 七、容器化部署设计 + +### 7.1 网络架构图 + +```mermaid +graph TB + subgraph "宿主机" + Host[宿主机] + Port80[端口 80] + end + + subgraph "Docker 网络" + Net[wordbook-network] + end + + subgraph "frontend 容器" + Nginx[Nginx:80] + end + + subgraph "backend 容器" + Gin[Gin:8080] + end + + subgraph "db 容器" + MySQL[MySQL:3306] + end + + Host --> Port80 + Port80 -->|外部访问| Nginx + Nginx --> Net + Gin --> Net + MySQL --> Net + + Nginx -.->|/api/* 代理| Gin + Gin -.->|数据库连接| MySQL + + style Host fill:#f0f0f0 + style Net fill:#e6f3ff +``` + +### 7.2 服务设计要点 + +| 服务 | 暴露端口 | 依赖 | 说明 | +|------|----------|------|------| +| frontend | 80:80 | backend | 对外唯一入口 | +| backend | 无 | db | 内部网络访问 | +| db | 无 | - | 内部网络访问 | + +**安全考虑:** +- 只有 frontend 暴露端口到宿主机 +- backend 和 db 仅在容器网络内通信 +- 通过容器名相互访问(`http://backend:8080`) + +### 7.3 Dockerfile 设计 + +**后端多阶段构建:** +```mermaid +graph LR + A[构建阶段
golang:alpine] -->|编译| B[二进制文件] + B -->|复制| C[运行阶段
alpine:latest] + C --> D[精简镜像] +``` + +**前端 Nginx 构建:** +```mermaid +graph LR + A[node:18-alpine
Vite Build] -->|dist 产物| B[nginx:alpine
复制 dist 并替换 nginx.conf] +``` + +--- + +## 八、开发实施顺序 + +### 阶段一:项目初始化 + +1. 创建项目目录结构 +2. 初始化后端项目(go mod init) +3. 初始化前端项目(npm create vite@latest) +4. 配置 docker-compose.yml 基础框架 + +### 阶段二:数据库设计 + +1. 编写 `docs/init.sql` 建表语句 +2. 编写 `docs/db.md` 数据库设计文档 +3. 在 docker-compose 中配置 MySQL 初始化挂载 + +### 阶段三:后端开发 + +1. **配置层**:Viper 环境变量加载 +2. **模型层**:定义 User 和 Word 结构体 +3. **认证模块**: + - 用户注册逻辑 + - 用户登录逻辑 + - JWT 生成与验证中间件 +4. **AI 调用模块**: + - DeepSeek 接口封装 + - 通义千问接口封装 + - Prompt 模板设计 +5. **单词业务模块**: + - 查询逻辑(数据库检查 + AI 调用) + - 保存逻辑 + - 列表查询(分页) + - 删除逻辑(软删除) +6. **API 路由**:注册所有接口 +7. **API 文档**:编写 `docs/api.md` + +### 阶段四:前端开发 + +1. **基础配置**: + - Vite proxy 配置 + - API 请求封装(axios) +2. **认证页面**: + - 登录表单 + - 注册表单 + - Token 存储与管理 +3. **单词学习页面**: + - 查询单词表单 + - AI 结果展示 + - 保存按钮 +4. **单词本页面**: + - 单词列表展示 + - 分页器 + - 删除功能 +5. **状态管理**:管理用户登录状态 + +### 阶段五:容器化部署 + +1. **后端 Dockerfile**:多阶段构建优化 +2. **前端 Dockerfile**:Nginx 配置 +3. **Nginx 反向代理**:配置 `/api/*` 路由 +4. **Docker Compose 编排**:网络、依赖、环境变量 +5. **本地测试**:完整流程验证 + +### 阶段六:文档编写 + +1. 编写 `README.md`:项目说明与运行指南 +2. 完善 `docs/api.md` +3. 完善 `docs/db.md` +4. 整理代码注释 + +--- + +## 九、注意事项与最佳实践 + +### 9.1 安全注意事项 + +- ⚠️ **严禁明文存储密码**:必须使用 bcrypt 等加密算法 +- ⚠️ **严禁后端配置 CORS**:统一通过代理解决跨域 +- ⚠️ **严禁使用 AutoMigrate**:数据库初始化必须通过 SQL 脚本 +- ⚠️ **敏感信息管理**:API Key 不应提交到代码仓库 + +### 9.2 开发规范 + +1. 代码风格遵循 Go 和各语言社区规范 +2. 所有接口必须有明确的参数说明和返回示例 +3. 数据库字段必须有清晰的注释 +4. 容器镜像应尽可能精简 + +### 9.3 调试建议 + +- 开发时可以先单独启动后端和前端测试 +- 使用 Docker Compose 日志查看:`docker-compose logs -f` +- 数据库连接问题检查网络配置和用户权限 + +--- + +## 十、验收标准 + +1. ✅ 项目目录结构符合要求 +2. ✅ 用户可正常注册和登录 +3. ✅ 可查询单词并获取 AI 生成的释义和例句 +4. ✅ 可保存单词到个人单词本 +5. ✅ 单词列表支持分页 +6. ✅ 可删除已保存的单词 +7. ✅ 使用 Docker Compose 一键启动 +8. ✅ 生产环境通过 Nginx 反向代理访问 +9. ✅ 三份文档完整且清晰 + +--- + diff --git a/金山办公作业/Week05/Slide.md b/金山办公作业/Week05/Slide.md new file mode 100644 index 0000000..b6ee239 --- /dev/null +++ b/金山办公作业/Week05/Slide.md @@ -0,0 +1,63 @@ + +| 算法类型 | 算法 | 密钥类型 | 特点 | +| ----- | ----- | ----- | ------------------- | +| HMAC | HS256 | 对称密钥 | 服务器签发和验证都 uses 同一密钥 | +| HMAC | HS512 | 对称密钥 | 更强的哈希,性能稍低 | +| RSA | RS256 | 非对称密钥 | 私钥签名,公钥验证 | +| RSA | RS512 | 非对称密钥 | 更强的签名算法 | +| ECDSA | ES256 | 椭圆曲线 | 比更短但同样安全 | + +```go +type JWTAuthenticator struct { + secretKey []byte // default config: Kingsoft-2026 +} + +func NewJWTAuthenticator(secretKey string) *JWTAuthenticator { + return &JWTAuthenticator{ + secretKey: []byte(secretKey), + } +} +``` + +|场景|推荐方案|理由| +|---|---|---| +|管理后台、商品列表|传统分页|需要跳页功能,数据量可控| +|社交媒体动态、无限滚动|游标分页|实时性强,只需向下加载| +|日志查看、历史订单|键集分页|大数据量,性能优先| +|搜索结果|混合方案|前10页传统 + 深度游标| +在 `vite.config.js/ts` 中配置代理,解决开发环境跨域问题: + +```javascript +export default { + server: { + proxy: { + '/api': { + target: 'http://localhost:8080', // 后端服务地址 + changeOrigin: true, // 改变请求源 + } + } + } +} +``` + +|维度|自增ID|UUID|雪花ID| +|---|---|---|---| +|**生成方式**|数据库自增|算法生成|时间戳+机器ID+序列| +|**有序性**|单机有序|完全无序|时间有序| +|**分布式友好度**|需要额外方案|✅ 原生支持|✅ 原生支持| +|**性能/TPS**|10万+|1万+|100万+| +|**ID长度**|8-10字节|36字符|19字符(十进制)| +|**冲突概率**|0(单机)|≈1/2^128|理论0| +|**索引友好度**|⭐⭐⭐⭐⭐|⭐⭐|⭐⭐⭐⭐| +|**可读性**|⭐⭐⭐⭐⭐|⭐⭐|⭐⭐⭐| + +|特性|命名卷|绑定挂载| +|---|---|---| +|**管理方式**|Docker 管理|用户管理| +|**存储位置**|Docker 默认目录|用户指定的任意位置| +|**跨平台兼容性**|✅ 良好|❌ 路径差异大| +|**权限处理**|✅ Docker 自动处理|⚠️ 需手动配置| +|**安全性**|✅ 隔离性好|⚠️ 直接访问主机| +|**备份难度**|⚠️ 需特殊命令|✅ 直接复制| +|**性能**|✅ 优化过|⚠️ 可能有额外开销| +|**开发调试**|⚠️ 不便|✅ 实时同步| diff --git a/金山办公作业/Week05/全栈开发实战:AI 智能单词本.md b/金山办公作业/Week05/全栈开发实战:AI 智能单词本.md new file mode 100644 index 0000000..f1640f5 --- /dev/null +++ b/金山办公作业/Week05/全栈开发实战:AI 智能单词本.md @@ -0,0 +1,105 @@ + +--- + +# 全栈开发实战:AI 智能单词本 + +## 一、项目背景 + +开发一个辅助英语学习的前后端分离 Web 应用。用户可以通过前端页面查询单词,后端系统会调用 AI 大模型(DeepSeek 或 通义千问)生成该单词的精准释义和 3 条例句。查询结果返回前端展示后,用户可以手动点击“保存”按钮,将该单词记录持久化到个人的单词本中,方便日后复习。 + +**本项目重点考察**:前后端分离架构、代理与跨域处理、第三方 AI 接口对接、关系型数据库设计,以及基于 Docker 的全栈工程化容器编排部署。 + +## 二、技术栈与核心架构要求 + +* **后端语言框架**:Go (>= 1.21) + Gin (`github.com/gin-gonic/gin`) +* **前端技术栈**:必须基于 Vite 构建(可搭配 Vue3、React 或原生 Vanilla JS),且必须掌握相应的工程化配置。 +* **数据库 & ORM**:MySQL 8.0 + GORM (`gorm.io/gorm`) +* **容器化部署**:Docker & Docker Compose +* **Web 服务器 (前端)**:Nginx +* **身份验证**:JWT (JSON Web Token) +* **配置管理**:Viper (或 godotenv) + +【特别要求:跨域处理规范】 + +* **严禁跨域配置**:本项目**严禁**在后端 Go 代码中配置任何允许跨域(CORS)的中间件! +* **开发环境**:前端必须通过配置 Vite 的 `proxy` 来解决跨域问题。 +* **生产环境**:必须通过 Nginx 反向代理 (`proxy_pass`) 统一路由,实现前端静态资源与后端 API 的同源访问。 + +## 三、项目目录结构要求 + +整个项目需要采用前后端分离的代码组织结构,并且包含完善的文档(请严格遵循以下目录规范): + +```text +week05/homework/docker-gin +├── backend/ # 后端 Go 代码目录 +│ ├── Dockerfile # 后端镜像构建文件 +│ ├── main.go +│ ├── .env # .env 文件示例 +│ └── ... # 其他分层目录如 api, service, model 等 +├── frontend/ # 前端项目目录(由 Vite 初始化) +│ ├── Dockerfile # 前端镜像构建文件(基于 Nginx) +│ ├── nginx.conf # Nginx 自定义配置文件(生产环境反向代理) +│ ├── vite.config.js/ts # Vite 配置文件(开发环境 Proxy) +│ └── ... # src, index.html 等 +├── docs/ # 项目文档目录 +│ ├── api.md # API 接口详细文档 +│ ├── db.md # 数据库设计文档 +│ └── init.sql # 数据库初始化脚本(包含建表语句) +├── docker-compose.yml # 统一部署文件 +└── README.md # 项目总体说明与运行指南 +``` + +## 四、核心功能需求 + +### 1. 用户认证模块 +* **用户注册**:前端提供表单,后端接收用户名密码,后端严禁明文存储密码(需进行 Hash 加密后存入 MySQL)。 +* **用户登录**:验证通过后返回 JWT Token,前端需要将 Token 存储(如 `localStorage`),并在后续请求的 Header 中携带 (`Authorization: Bearer `)。 + +### 2. 单词学习模块 (核心业务) +* **智能查询单词**: + * **接收参数**:`word`(单词),`ai_provider`(前端让用户下拉选择模型,如 DeepSeek/通义千问)。 + * **逻辑流程**: + 1. 鉴权通过后,后端先在数据库中检查当前用户是否已保存过该单词。 + 2. 如果已保存,直接从数据库读取并返回给前端。 + 3. 如果未保存,根据 `ai_provider` 调用对应 AI 接口。 + 4. 让 AI 返回格式化的 JSON 数据(包含:释义 + 3 条例句)。 + 5. 直接将 AI 结果返回给前端展示,**此时不在后端进行数据库保存**。 +* **手动保存单词**: + * **接收参数**:前端将上一步查询到的完整数据(单词、释义、例句列表、AI 来源)提交给后端。 + * **逻辑流程**:后端接收到数据后,将其写入 MySQL 数据库,并与当前 UserID 绑定。 +* **获取单词列表**:获取当前用户保存的所有单词记录(**必须支持分页**,前端提供分页器,后端接收 `page` 和 `page_size`)。 +* **删除单词**:根据单词 ID,从数据库中软删除某条记录。 + +## 五、部署与交付要求 (Docker & Nginx & Vite) + +### 1. 后端构建 (`backend/Dockerfile`) +* 使用多阶段构建编译 Go 应用,暴露出后端端口(如 8080)。镜像需尽可能精简。 + +### 2. 前端构建 (`frontend/Dockerfile`) 与环境配置 +* **【生产环境 - Nginx 统一入口】**:基础镜像使用 `nginx:alpine`,将 Vite build 后的产物拷贝到 Nginx 发布目录。 +* **必须替换自定义的 `nginx.conf`**:将 Nginx 作为整个应用的唯一外部访问入口。既要伺服前端静态页面,又要将后端接口请求(如 `/api/`)通过 `proxy_pass` 反向代理转发给内部 `backend` 容器。 + +### 3. 编排部署 (`docker-compose.yml`) +* 定义 3 个 Service:`db` (MySQL), `backend` (Go API), `frontend` (Nginx UI)。 +* 配置共享的网络。对外只需暴露 `frontend` (Nginx) 的 80/443 等端口,`backend` 和 `db` 的端口无需直接映射到宿主机,保障安全性。`frontend` 可以通过容器名访问 `backend`,`backend` 可以访问 `db`。 +* `backend` 服务需依赖于 `db` (`depends_on`)。 +* **数据库初始化要求**:为了符合企业级开发规范(DBA 审计与权限控制),**严禁**在代码中使用 GORM 的 `AutoMigrate` 等工具自动建表。**必须在 `docker-compose.yml` 中将包含建表语句的 `docs/init.sql` 挂载到 MySQL 容器的 `/docker-entrypoint-initdb.d/` 目录下进行初始化。**确保一键启动即可使用,无需人工干预数据库建表。 + +## 六、文档编写要求 (考察重点) + +本项目非常看重开发者的文档输出能力。除了提交能够正常运行的代码外,你必须编写并提交以下三份文档: + +### 1. `README.md` (项目说明与运行指南) +* **项目基本信息**:至少包含你的姓名、学校、学号。 +* **开发任务索引**:列出你完成的任务清单。 +* **项目简介**:包含项目简介与架构图(或架构说明)。 +* **运行指南(核心考点)**:必须极其清晰地写明如何从零启动该项目。包括前置依赖(Docker 等)、如何配置 AI 的 API Key(如环境变量或 `.env` 文件的创建)、一键启动命令(`docker-compose up -d`),以及启动后如何访问前端页面和后端服务。 + +### 2. `docs/api.md` (API 接口文档) +* 详细记录业务中的每一个接口。 +* 每一项应包含:接口路径、请求方法、鉴权说明、请求参数(Query 或 Body 结构)、成功的返回示例(JSON 格式)、失败的错误码及其含义。 + +### 3. `docs/db.md` (数据库设计文档) +* 详细阐述你的 MySQL 数据库表结构设计。 +* 需要列出每个表的字段名、数据类型、是否主/外键、索引设置、以及每个字段的具体业务含义。 +* 说明你对单词本表、用户表的关联关系设计。 \ No newline at end of file diff --git a/金山办公作业/Week05/分页.md b/金山办公作业/Week05/分页.md new file mode 100644 index 0000000..824f1a4 --- /dev/null +++ b/金山办公作业/Week05/分页.md @@ -0,0 +1,610 @@ +--- +tags: [go, pagination, backend, frontend, api, database, assignment] +create time: 2026-04-17 22:45 +--- + +# 分页实现指南 + +## 概述 + +分页是 Web 应用中处理大规模数据的核心技术。本文深入探讨分页的各种实现策略、性能优化及最佳实践,帮助构建高效的用户体验。 + +## 分页架构全景 + +### 核心流程 + +```mermaid +sequenceDiagram + participant C as Client + participant F as Frontend + participant S as Backend + participant D as Database + participant Cache as Redis + + C->>F: 访问列表页(第1页) + F->>S: GET /api/list?page=1&page_size=10 + + alt 缓存命中 + S->>Cache: get(pagination:1:10) + Cache-->>S: cached data + else 缓存未命中 + S->>D: SELECT ... LIMIT 10 OFFSET 0 + S->>D: SELECT COUNT(*) + D-->>S: data + total + S->>Cache: set(pagination:1:10, ttl:5m) + end + + S-->>F: {data, total, page, page_size, total_page} + F->>F: 计算分页信息 + F-->>C: 渲染数据 + 分页器 +``` + +### 三种分页模式对比 + +```mermaid +graph LR + A[分页需求] --> B{数据特征} + B -->|稳定数据
管理后台| C[传统分页
LIMIT/OFFSET] + B -->|实时数据
无限滚动| D[游标分页
Cursor-based] + B -->|历史数据
时间范围| E[键集分页
Keyset] + + C -.-> F[✅ 支持跳页
⚠️ 深度性能差] + D -.-> G[✅ 性能稳定
❌ 不支持跳页] + E -.-> H[✅ 最佳性能
⚠️ 需排序字段] +``` + +**选择指南**: + +| 场景 | 推荐方案 | 理由 | +|-----|---------|------| +| 管理后台、商品列表 | 传统分页 | 需要跳页功能,数据量可控 | +| 社交媒体动态、无限滚动 | 游标分页 | 实时性强,只需向下加载 | +| 日志查看、历史订单 | 键集分页 | 大数据量,性能优先 | +| 搜索结果 | 混合方案 | 前10页传统 + 深度游标 | + +## 前端分页器设计 + +### 核心接口设计 + +```typescript +interface PaginationRequest { + page: number; // 当前页码(从1开始) + page_size: number; // 每页大小 +} + +interface PaginationResponse { + data: T[]; + total: number; + page: number; + page_size: number; + total_page: number; +} +``` + +**关键点**: +- `page` 从 1 开始(用户直觉友好) +- 响应包含 `total` 用于前端计算总页数 +- 支持泛型 `T` 复用于不同数据类型 + +### 分页器组件要点 + +传统分页器的核心功能: +1. 页码导航(上一页、下一页、直接跳页) +2. 页码显示(智能压缩:"1 ... 5 6 7 ... 10") +3. 每页大小切换(10/20/50/100条) +4. 总数展示("共 100 条,共 10 页") + +```typescript +// 简化版展示核心逻辑 +const totalPages = Math.ceil(total / pageSize); +const startPage = Math.max(1, currentPage - 2); +const endPage = Math.min(totalPages, currentPage + 2); + +if (startPage > 1) showEllipsis = true; +if (endPage < totalPages) showEndEllipsis = true; +``` + +**进阶技巧**: +- **页码压缩**:页数 > 7 时显示省略号 +- **防抖处理**:快速点击时只执行最后一次请求 +- **地址栏同步**:URL 参数 `?page=2`, 支持前进/后退 +- **骨架屏**:加载时显示占位符,提升感知性能 + +### 无限滚动实现 + +使用 `Intersection Observer` API(性能优于 scroll 事件): + +```typescript +// 核心逻辑:监听列表最后一项进入视口 +const lastItemRef = useRef(); + +useEffect(() => { + const observer = new IntersectionObserver(([entry]) => { + if (entry.isIntersecting && hasMore && !loading) { + loadMore(); // 自动加载下一页 + } + }, { threshold: 0.1 }); + + if (lastItemRef.current) observer.observe(lastItemRef.current); + return () => observer.disconnect(); +}, [hasMore, loading]); +``` + +**注意事项**: +- 使用 `sticky` footer 显示加载状态 +- 返回顶部时考虑重新加载或保留状态 +- 批量追加数据避免频繁渲染 + +## 后端实现要点 + +### Go 核心结构 + +```go +// 分页请求 +type PaginationRequest struct { + Page int `form:"page"` // 默认 1 + PageSize int `form:"page_size"` // 默认 10,最大 100 +} + +// 分页响应 +type PaginationResponse struct { + Data []interface{} `json:"data"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPage int `json:"total_page"` +} +``` + +**关键逻辑**: + +```go +func GetPagination(r *http.Request) PaginationRequest { + p := PaginationRequest{Page: 1, PageSize: 10} + + // 参数解析 + 验证 + if page := r.URL.Query().Get("page"); page != "" { + if n, err := strconv.Atoi(page); err == nil && n > 0 { + p.Page = n + } + } + if size := r.URL.Query().Get("page_size"); size != "" { + if n, err := strconv.Atoi(size); err == nil && n > 0 { + p.PageSize = min(n, 100) // 限制最大值 + } + } + return p +} + +func ListHandler(w http.ResponseWriter, r *http.Request) { + p := GetPagination(r) + offset := (p.Page - 1) * p.PageSize + + // 查询数据 + items, _ := db.Query("SELECT ... LIMIT ? OFFSET ?", p.PageSize, offset) + total, _ := db.QueryInt("SELECT COUNT(*)") + + resp := PaginationResponse{ + Data: items, + Total: total, + Page: p.Page, + PageSize: p.PageSize, + TotalPage: (int(total) + p.PageSize - 1) / p.PageSize, + } + json.NewEncoder(w).Encode(resp) +} +``` + +### 三种分页查询策略 + +#### 1. 传统分页(LIMIT/OFFSET) + +```sql +-- 基础查询 +SELECT * FROM products ORDER BY id DESC LIMIT 10 OFFSET 0; -- 第1页 +``` + +**深度分页问题**: +- OFFSET 10000 需要扫描前 10001 条记录 +- 性能随页码线性下降:O(n) + +**优化方案**: +- 限制最大页码(如 1000 页) +- 预计算页码到 ID 的映射 +- 对热门数据使用缓存 + +#### 2. 游标分页(Cursor-based) + +```sql +-- 使用 WHERE 替代 OFFSET +SELECT * FROM products +WHERE id < {last_id} +ORDER BY id DESC +LIMIT 10; +``` + +**优势**: +- 性能稳定,复杂度 O(1) +- 支持实时数据(新增数据不影响) + +**实现要点**: +```go +type CursorResponse struct { + Data []Product `json:"data"` + Cursor string `json:"cursor"` // 最后一条的 ID + HasMore bool `json:"has_more"` +} + +// 前端保存 cursor,下次请求携带 +``` + +#### 3. 键集分页(Keyset Pagination) + +适用于有明确排序字段(如时间戳): + +```sql +-- 获取第N页(需要知道N-1页的最后值) +SELECT * FROM logs +WHERE created_at < '2026-04-17 10:00:00' -- 上一页最后的时间戳 +ORDER BY created_at DESC +LIMIT 10; +``` + +**性能最优**,但实现复杂,需要: +- 前端保存每页的最后值作为"书签" +- 支持双向导航(需要第一页、最后页的边界值) + +### 总数查询优化 + +传统方案需要两次查询(数据 + COUNT),优化策略: + +```go +// 策略1:缓存总数(适合数据变化不频繁) +total := cache.Get("total_products") + +// 策略2:近似总数(如每天更新一次) +if time.Since(lastUpdate) > 24*time.Hour { + total = db.QueryInt("SELECT COUNT(*)") +} + +// 策略3:渐进式加载(不显示总数,只显示"更多") +hasMore := len(items) == pageSize +``` + +## API 响应示例 + +### 标准分页响应格式 + +```json +{ + "data": [ + { + "id": 1, + "name": "Item 1", + "created_at": "2026-04-17T10:00:00Z" + }, + { + "id": 2, + "name": "Item 2", + "created_at": "2026-04-17T09:00:00Z" + } + ], + "total": 100, + "page": 1, + "page_size": 10, + "total_page": 10 +} +``` + +### 错误响应 + +```json +{ + "error": "Invalid pagination parameters", + "message": "page must be greater than 0" +} +``` + +## 性能优化 + +### 数据库索引策略 + +```sql +-- 基础索引:按排序字段 +CREATE INDEX idx_products_created ON products(created_at DESC); + +-- 复合索引:过滤条件 + 排序 +CREATE INDEX idx_products_status_created ON products(status, created_at DESC); + +-- 覆盖索引:避免回表 +CREATE INDEX idx_products_covering ON products(status, name, price); +-- 查询可以直接从索引获取,无需访问表数据 +SELECT name, price FROM products WHERE status = 1 ORDER BY created_at; +``` + +**索引设计原则**: +- WHERE 条件字段 → 等值匹配优先级更高 +- ORDER BY 字段 → 排序方向(ASC/DESC) +- 覆盖索引 → 避免回表,提升 50%+ 查询性能 + +### 缓存层次 + +```mermaid +graph TB + A[请求] --> B{缓存检查} + B -->|Hit| C[返回缓存数据] + B -->|Miss| D[数据库查询] + D --> E[写入缓存] + E --> C + + F[缓存策略] --> G[热门页
TTL: 10分钟] + F --> H[普通页
TTL: 5分钟] + F --> I[总数统计
TTL: 1小时] +``` + +**实现要点**: + +```go +// 分层缓存策略 +type CacheConfig struct { + HotPages map[int]time.Duration // 热门页长期缓存 + Normal time.Duration // 普通页短期缓存 + Total time.Duration // 总数统计超长期缓存 +} + +// 缓存键设计 +key := fmt.Sprintf("list:%d:%d", page, pageSize) +totalCountKey := "list:total" +``` + +并为了防止缓存雪崩,添加随机 TTL 偏移: + +```go +ttl := baseTTL + time.Duration(rand.Intn(60))*time.Second +``` + +### 深度分页优化方案 + +**问题场景**:100万条数据查询第10页 + +| 方案 | 查询时间 | 适用场景 | +|-----|---------|----------| +| LIMIT 10 OFFSET 100 | ~50ms | < 1000页 | +| WHERE id > last_id LIMIT 10 | ~5ms | > 1000页 | +| 预计算页码映射 | ~1ms | 数据稳定 | + +**混合策略**: + +```go +func QueryData(page, pageSize int) { + if page < 100 { + // 前页用 OFFSET + db.Query("SELECT ... LIMIT ? OFFSET ?", pageSize, (page-1)*pageSize) + } else { + // 深度页用游标(需要第99页的最后ID) + lastID := getPageLastID(99) + db.Query("SELECT ... WHERE id > ? LIMIT ?", lastID, pageSize * (page-99)) + } +} +``` + +## 前进话题 + +### 实时分页挑战 + +**问题场景**: +- 用户在第5页浏览 +- 其他用户删除了第5页的部分数据 +- 刷新后数据可能重复或遗漏 + +**解决方案**: + +1. **快照分页**(Snowflake/Slack模式) + ```go + type QueryID string // 每次查询生成唯一ID + cache.Store(queryKey, allData, 10m) // 缓存完整结果集 + // 后续请求基于快照 + ``` + +2. **游标 + 时间戳** + ```sql + WHERE (created_at, id) <= (:last_time, :last_id) + ORDER BY created_at DESC, id DESC + ``` + 使用复合游标保证顺序稳定 + +3. **接受不一致性**(社交媒体) + - 少量重复/遗漏可接受 + - 优先性能而非严格一致性 + +### 分布式分页 + +**问题**:分库分表后如何分页? + +**方案对比**: + +| 方案 | 复杂度 | 性能 | 适用场景 | +|-----|-------|------|----------| +| 全局聚合后分页 | 低 | 差(需扫描全量) | < 10万数据 | +| 按用户分片(user_id取模) | 中 | 好 | 私有数据 | +| 路由表(mapping表) | 高 | 较好 | 公共数据 | +| 基于ES的搜索分页 | 高 | 优秀 | 搜索功能 | + +**ES方案示例**: + +```json +GET /products/_search +{ + "from": 0, + "size": 10, + "sort": [{"created_at": "desc"}], + "query": {"match_all": {}} +} +``` + +ES 使用 `search_after` 替代 `from/size` 进行深度分页: + +```json +{ + "size": 10, + "sort": [{"created_at": "desc"}, {"_id": "desc"}], + "search_after": ["2026-04-17", "last_id"] +} +``` + +### GraphQL 分页 + +使用 **Relay规范** 实现连接(Connection): + +```graphql +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String +} + +type ProductEdge { + node: Product! + cursor: String! +} + +type ProductConnection { + edges: [ProductEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type Query { + products(first: Int, after: String): ProductConnection! +} +``` + +**客户端使用**: + +```typescript +// 查询第一页 +query { + products(first: 10) { + edges { node { name } cursor } + pageInfo { hasNextPage, endCursor } + } +} + +// 加载更多 +query { + products(first: 10, after: "cursor-from-first-page") { + // ... + } +} +``` + +## 最佳实践总结 + +### 前端 + +✅ **推荐** +- 使用 `Intersection Observer` 实现无限滚动 +- URL 同步分页参数(支持前进后退) +- 显示"加载中"骨架屏提升感知性能 +- 合理的防抖/节流策略 +- 分页器智能压缩(显示省略号) + +❌ **避免** +- 首次加载请求所有数据(前端分页) +- scroll 事件高频触发(用 Observer 替代) +- 前端计算总数(需要后端提供) +- 不处理空数据/单页边界 + +### 后端 + +✅ **推荐** +- 统一参数命名(`page` / `page_size`) +- 参数验证与默认值(page ≥ 1, page_size ≤ 100) +- 使用索引优化查询(重点优化排序字段) +- 多层缓存策略 +- 返回总数和总页数 +- 深度分页使用游标优化 + +❌ **避免** +- 不限制最大查询深度(导致性能问题) +- 不返回总数(前端无法展示) +- 每次查询 COUNT(可用缓存或近似值) +- SQL 注入风险(排序字段白名单验证) + +### API 响应标准 + +```json +{ + "data": [...], + "total": 1250, + "page": 2, + "page_size": 20, + "total_page": 63, + "has_more": true +} +``` + +**字段说明**: +- `has_more`:方便前端判断是否加载更多(适用于无限滚动) + +## 常见问题解答 + +### Q: 深度分页性能如何优化? + +**问题**:查询第10000页需要扫描100万条记录 + +**答案**: +1. **限制深度**:禁止查询超过1000页 +2. **混合方案**:前N页OFFSET,深度页游标 +3. **游标分页**:使用 `WHERE id > last_id` 替代 OFFSET +4. **预计算映射**:缓存页码到ID的映射关系 + +### Q: 数据变化时分页如何处理? + +**场景**:用户在第5页,删除操作后页面变空 + +**答案**: +```typescript +// 删除后检查并跳转 +if (currentData.length === 1 && currentPage > 1) { + loadData(currentPage - 1); // 跳到上一页 +} else { + loadData(currentPage); // 重新加载当前页 +} +``` + +### Q: 如何支持自定义排序? + +**答案**: +```go +// 白名单验证防止SQL注入 +allowedFields := []string{"name", "created_at", "price"} + +sortQuery := "ORDER BY " + allowedField(req.SortBy) + " " + + (req.SortDesc ? "DESC" : "ASC") +``` + +### Q: 为什么有时候返回重复数据? + +**原因**: +- 传统分页:数据插入/删除导致位移 +- 游标分页:使用复合字段解决 + +```sql +-- 使用复合游标保证稳定 +WHERE (created_at, id) <= (:last_time, :last_id) +ORDER BY created_at DESC, id DESC +``` + +## 学习资源 + +- **PostgreSQL文档**:[Pagination](https://www.postgresql.org/docs/current/queries-limit.html) +- **MySQL优化**:[Optimizing LIMIT Queries](https://dev.mysql.com/doc/refman/8.0/en/optimization-limit-optimization.html) +- **Relay规范**:[Cursor-based Pagination](https://relay.dev/graphql/connections.htm) + +## 关联笔记 + +- [[金山办公作业/Week05/用户认证.md]] - 用户认证授权机制 +- [[CS/DB/索引优化]] - 数据库索引设计与优化 +- [[CS/NET/RESTful API]] - API 设计最佳实践 diff --git a/金山办公作业/Week05/数据库设计.md b/金山办公作业/Week05/数据库设计.md new file mode 100644 index 0000000..8aad7a7 --- /dev/null +++ b/金山办公作业/Week05/数据库设计.md @@ -0,0 +1,197 @@ +# 数据库设计文档 + +## 一、数据库概览 + +| 项目 | 说明 | +|------|------| +| 数据库名称 | `wordbook` | +| 字符集 | `utf8mb4` | +| 排序规则 | `utf8mb4_unicode_ci` | +| 存储引擎 | `InnoDB` | + +## 二、ER 图 + +```mermaid +erDiagram + USER ||--|{ WORD : has + USER { + uuid id PK "用户ID" + string username UK "用户名" + string password "密码(hash)" + datetime created_at "创建时间" + datetime updated_at "更新时间" + datetime deleted_at "删除时间(软删除)" + } + WORD { + uuid id PK "单词记录ID" + uuid user_id FK "所属用户ID" + string word "单词" + text definition "释义" + json examples "例句列表" + string ai_provider "AI模型来源" + datetime created_at "创建时间" + datetime updated_at "更新时间" + datetime deleted_at "删除时间(软删除)" + } +``` + +## 三、数据表详情 + +### 3.1 用户表 (users) + +| 用户表名 | `users` | +|----------|---------| + +| 字段名 | 数据类型 | 约束 | 索引 | 说明 | +|--------|----------|------|------|------| +| `id` | CHAR(36) | PRIMARY KEY | - | 用户唯一标识符(UUID 格式) | +| `username` | VARCHAR(50) | NOT NULL, UNIQUE | UNIQUE KEY | 用户名,全局唯一,用于登录 | +| `password` | VARCHAR(255) | NOT NULL | - | 用户密码,存储 bcrypt 哈希值,严禁明文存储 | +| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | - | 账户创建时间 | +| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | - | 账户最后更新时间 | +| `deleted_at` | DATETIME | DEFAULT NULL | INDEX | 软删除标记,NULL 表示未删除 | + +**索引设计:** +- 主键索引:`id` +- 唯一索引:`username`(防止重复注册) + +### 3.2 单词记录表 (words) + +| 单词表名 | `words` | +|-----------|---------| + +| 字段名 | 数据类型 | 约束 | 索引 | 说明 | +| ------------- | ------------ | --------------------------- | ------------------------- | ------------------------------------------ | +| `id` | CHAR(36) | PRIMARY KEY | - | 单词记录唯一标识符(UUID 格式) | +| `user_id` | CHAR(36) | NOT NULL, FOREIGN KEY | - | 所属用户 ID,关联 users.id | +| `word` | VARCHAR(100) | NOT NULL | INDEX idx_word_search(20) | 单词文本,支持前缀搜索优化 | +| `definition` | TEXT | NOT NULL | - | AI 生成的单词释义 | +| `examples` | JSON | NOT NULL | - | 例句列表,存储 JSON 数组格式 | +| `ai_provider` | ENUM | NOT NULL | - | AI 模型来源标识(`deepseek`=DeepSeek,`qwen`=通义千问) | +| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | - | 单词记录创建时间 | +| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | - | 单词记录最后更新时间 | +| `deleted_at` | DATETIME | DEFAULT NULL | INDEX idx_user_word | 软删除标记 | + +**索引设计:** +- 主键索引:`id` +- 外键索引:`user_id`(自动创建) +- 唯一索引:`uk_user_word` (user_id, word(50)) - 防止同一用户保存重复单词 +- 复合索引:`idx_user_word` (user_id, deleted_at) - 优化用户单词列表分页查询 +- 前缀索引:`idx_word_search` (word(20)) - 支持单词前缀搜索优化 + +## 四、表关联关系 + +### 4.1 用户与单词的关系 + +``` +用户 (users) 1 ---- N 单词记录 (words) +``` + +- **关系类型**:一对多 +- **外键约束**:`words.user_id` → `users.id` +- **级联规则**:`ON DELETE CASCADE` + - 当用户被删除时,该用户的所有单词记录也会被自动删除 + +### 4.2 关联查询示例 + +```sql +-- 查询某用户的所有单词(排除已删除) +SELECT * FROM words +WHERE user_id = ? AND deleted_at IS NULL +ORDER BY created_at DESC +LIMIT ? OFFSET ?; + +-- 联表查询用户信息及单词数量 +SELECT u.id, u.username, COUNT(w.id) as word_count +FROM users u +LEFT JOIN words w ON u.id = w.user_id AND w.deleted_at IS NULL +WHERE u.deleted_at IS NULL +GROUP BY u.id; +``` + +### 4.2 唯一约束说明 + +`uk_user_word` 约束确保同一用户不能保存相同的单词两次: +- **触发场景**:在"手动保存单词"时 +- **业务逻辑**:前端应先调用"智能查询单词"接口检查是否已保存,避免触发重复错误 +- **不触发场景**:"智能查询单词"接口只查询不保存,不会触发此约束 + +## 五、核心业务 SQL 查询示例 + +### 5.1 智能查询单词 + +**业务逻辑**:先检查数据库,未命中则调用 AI(不保存) + +```sql +-- 查询:检查用户是否已保存该单词 +SELECT id, word, definition, examples, ai_provider +FROM words +WHERE user_id = ? AND word = ? AND deleted_at IS NULL; +``` + +### 5.2 手动保存单词 + +**业务逻辑**:将 AI 返回的结果写入数据库 + +```sql +-- 插入:保存单词记录(受 uk_user_word 唯一约束保护) +INSERT INTO words (id, user_id, word, definition, examples, ai_provider) +VALUES (?, ?, ?, ?, ?, ?); +``` + +### 5.3 获取单词列表(分页) + +**业务逻辑**:获取用户所有已保存的单词,支持分页 + +```sql +-- 查询:分页获取单词列表(按创建时间倒序) +SELECT id, word, definition, examples, ai_provider, created_at +FROM words +WHERE user_id = ? AND deleted_at IS NULL +ORDER BY created_at DESC +LIMIT ? OFFSET ?; + +-- 计算总数(用于分页器) +SELECT COUNT(*) as total +FROM words +WHERE user_id = ? AND deleted_at IS NULL; +``` + +### 5.4 删除单词(软删除) + +**业务逻辑**:根据单词 ID 软删除记录 + +```sql +-- 更新:软删除单词记录 +UPDATE words +SET deleted_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP +WHERE id = ? AND user_id = ? AND deleted_at IS NULL; +``` + +## 六、索引优化说明 + +### 6.1 查询场景分析 + +| 查询场景 | 涉及字段 | 索引策略 | +|----------|----------|----------| +| 用户登录 | username | UNIQUE KEY | +| 智能查询单词 | user_id, word, deleted_at | uk_user_word(唯一约束) | +| 用户单词列表(分页) | user_id, deleted_at | 复合索引 idx_user_word | +| 单词搜索 | word | 前缀索引 idx_word_search | +| 按用户 ID 查找单词 | user_id | 外键自动索引 | + +### 6.2 示例 JSON 数据结构 (examples 字段) + +```json +[ + "The word 'serendipity' means finding something good without looking for it.", + "It was pure serendipity that I met my best friend at the coffee shop.", + "Many scientific discoveries are the result of serendipity." +] +``` + +## 七、数据初始化 + +数据库初始化脚本位于 `docs/init.sql`,该脚本会在 Docker Compose 启动 MySQL 容器时自动执行。 + +**严禁使用 GORM 的 `AutoMigrate` 功能进行建表,必须通过此 SQL 脚本初始化数据库。** diff --git a/金山办公作业/Week05/用户ID.md b/金山办公作业/Week05/用户ID.md new file mode 100644 index 0000000..fee9f1c --- /dev/null +++ b/金山办公作业/Week05/用户ID.md @@ -0,0 +1,357 @@ +--- +tags: [database, distributed-system, architecture, week05] +create time: 2026-04-18 10:45 +--- + +# 用户ID + +## 概述 + +深入分析分布式系统中用户ID的三种主流生成方案:自增ID、UUID和雪花ID。本文从原理、性能、适用场景等多维度对比,帮助开发者在实际项目中做出明智的技术选型,并重点关注进阶问题和实战陷阱。 + +## 正文 + +### 核心对比 + +| 维度 | 自增ID | UUID | 雪花ID | +|------|--------|------|--------| +| **生成方式** | 数据库自增 | 算法生成 | 时间戳+机器ID+序列 | +| **有序性** | 单机有序 | 完全无序 | 时间有序 | +| **分布式友好度** | 需要额外方案 | ✅ 原生支持 | ✅ 原生支持 | +| **性能/TPS** | 10万+ | 1万+ | 100万+ | +| **ID长度** | 8-10字节 | 36字符 | 19字符(十进制) | +| **冲突概率** | 0(单机) | ≈1/2^128 | 理论0 | +| **索引友好度** | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | +| **可读性** | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | + +### 自增ID + +#### 原理与演进 + +在单机架构中,数据库维护一个递增计数器,每次插入时自动获取下一个可用的整数值。虽然简单,但随着业务发展到分布式场景,暴露出以下问题: + +```go +// MySQL 自增ID实现 +type AutoIncrementID struct { + mu sync.Mutex + nextID int64 +} + +func (a *AutoIncrementID) Next() int64 { + a.mu.Lock() + defer a.mu.Unlock() + a.nextID++ + return a.nextID +} +``` + +**分布式扩展方案**: + +1. **分段步长策略**:每台DB配置不同的起始值和步长 + - DB1: 1, 4, 7... (start=1, step=3) + - DB2: 2, 5, 8... (start=2, step=3) + - DB3: 3, 6, 9... (start=3, step=3) + +2. **Redis原子递增**:使用 `INCR` 命令实现分布式自增 + - 单线程保证原子性,性能受限于单机瓶颈 + - 集群模式下的分布式锁带来额外延迟 + +#### 深槽陷阱 + +**问题:ID泄露业务规模** + +攻击者可以通过观察用户ID推断出系统的用户增长速度和总规模。 + +**解决方案**: +- 在对外层添加混淆层(将真实ID映射到随机ID) +- 使用非连续的ID生成策略 +- 对主键ID对用户隐藏,使用生成的业务ID + +**问题:表级自增成为写入瓶颈** + +在高并发场景下,自增锁成为表级别的竞争点。 + +**优化思路**: +- 使用分段缓存(预分配1000个ID到应用层,减少数据库交互) +- 迁移到雪花ID等分布式方案 + +### UUID + +#### 标准解析 + +UUID(Universally Unique Identifier)是RFC 4122标准定义的128位标识符。 + +**版本对比**: + +```go +package main + +import ( + "fmt" + "github.com/google/uuid" +) + +func main() { + // UUID v1: 基于时间戳和MAC地址(有序但泄露信息) + uuidv1 := uuid.Must(uuid.NewUUID()) + fmt.Println("v1:", uuidv1) + + // UUID v4: 随机生成(完全无序,最常用) + uuidv4 := uuid.Must(uuid.NewRandom()) + fmt.Println("v4:", uuidv4) + + // UUID v5: 基于命名空间的SHA-1哈希(确定性) + namespace := uuid.Must(uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) + uuidv5 := uuid.NewSHA1(namespace, []byte("user@example.com")) + fmt.Println("v5:", uuidv5) +} +``` + +#### 深槽陷阱 + +**问题:B+树索引页分裂** + +B+树是有序的聚簇索引结构,当插入无序ID时,会导致频繁的页分裂,影响写入性能。 + +**可视化影响**: + +```mermaid +graph LR + A[插入有序ID] -->|顺序追加| B[B+树追加到末尾] + C[插入UUID] -->|随机位置| D[B+树页分裂重组] + B --> E[IO操作少
写入快] + D --> F[频繁磁盘IO
写入慢] +``` + +**优化策略**: + +1. **使用ULID(Universally Unique Lexicographically Sortable ID)**:兼容UUID格式但支持排序 + ```go + import "github.com/oklog/ulid/v2" + t := time.Unix(time.Now().Unix(), 0) + entropy := ulid.Monotonic(rand.New(rand.NewSource(t.UnixNano())), 0) + id := ulid.MustNew(ulid.Timestamp(t), entropy) + ``` + +2. **使用UUID v2/ULID版本**:基于时间戳的有序版本 + +3. **使用组合索引**:将UUID作为二级索引,有序ID作为聚簇索引 + +**问题:存储空间膨胀** + +UUID长度36字符,相比8字节自增ID存储空间增长4.5倍。 + +**优化措施**: +- 存储时去除连字符(16字节),应用层再格式化 +- 考虑使用Snowflake等更短的方案 + +### 雪花ID + +#### 算法原理 + +雪花ID是Twitter开源的分布式ID生成算法,采用64位整数结构: + +``` +位数:1 | 41 | 10 | 12 +含义:符号位 | 时间戳 | 机器ID | 序列号 +``` + +**时间戳**:41位,每个单位代表毫秒,可用年限 (2^41-1)/(1000×60×60×24×365) ≈ 69年 + +```go +// 雪花ID生成器实现 +type Snowflake struct { + mu sync.Mutex + machineID int64 // 10位机器ID,支持1024个节点 + epoch int64 // 起始时间戳 + sequence int64 // 12位序列号,单毫秒最多4096个 + lastTime int64 +} + +func NewSnowflake(machineID int64) *Snowflake { + return &Snowflake{ + machineID: machineID & 0x3FF, // 确保在10位范围内 + epoch: 1609459200000, // 2021-01-01 + } +} + +func (s *Snowflake) Generate() int64 { + s.mu.Lock() + defer s.mu.Unlock() + + now := time.Now().UnixNano() / 1e6 // 当前毫秒时间戳 + + if now < s.lastTime { + // 时钟回拨,抛出异常 + panic("时钟回拨,无法生成ID") + } + + if now == s.lastTime { + // 同一毫秒内,递增序列号 + s.sequence = (s.sequence + 1) & 0xFFF + if s.sequence == 0 { + // 序列号溢出,等待下一毫秒 + for now <= s.lastTime { + now = time.Now().UnixNano() / 1e6 + } + } + } else { + // 新的毫秒,序列号重置 + s.sequence = 0 + } + + s.lastTime = now + + // 计算:时间戳左移22位 + 机器ID左移12位 + 序列号 + id := ((now - s.epoch) << 22) | (s.machineID << 12) | s.sequence + return id +} +``` + +#### 进阶优化 + +**1. 机器ID动态分配** + +在容器化/云原生场景中,提前分配固定机器ID难以维护。改进方案: + +```go +// 使用Zookeeper动态注册 +func RegisterMachineID(zkClient *zk.Conn, basePath string) (int64, error) { + path := fmt.Sprintf("%s/node-", basePath) + zkPath, err := zkClient.Create(path, []byte{}, zk.FlagEphemeralSequential, zk.WorldACL(zk.PermAll)) + if err != nil { + return 0, err + } + + // 从临时有序节点中提取序号作为机器ID + nodeNum, _ := strconv.ParseInt(path[len(basePath)+1:], 10, 64) + return nodeNum % 1024, nil // 确保在1024范围内 +} +``` + +**2. 时钟回拨处理** + +时钟回拨会导致重复ID,需要特殊处理: + +```go +func (s *Snowflake) handleClockBackoff(now int64) int64 { + offset := s.lastTime - now + if offset < 10 { + // 小幅回拨,等待时钟同步 + time.Sleep(time.Duration(offset+10) * time.Millisecond) + return time.Now().UnixNano() / 1e6 + } else if offset < 1000 { + // 中度回拨,使用备用时间戳(增大序列号作为区分) + s.sequence = (s.sequence + 1) & 0xFFF + return s.lastTime + } else { + // 大幅回拨,告警并拒绝生成 + log.Printf("严重时钟回拨:回拨时长%dms", offset) + panic("时钟回拨超过容忍范围") + } +} +``` + +**3. 雪花ID的可读性优化** + +雪花ID的19位数字对用户不友好,应用层可做映射转换: + +```go +// 将雪花ID转换为更友好的格式 +func FriendlyID(snowflakeID int64) string { + // 使用Base58编码(去除易混淆字符) + alphabet := "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + return base58.Encode([]byte(strconv.FormatInt(snowflakeID, 10))) +} +``` + +#### 深槽陷阱 + +**问题:单机QPS受限** + +单机理论QPS = 1000ms × 4096 = 409万/秒,但实际受限于网络延迟、GC等。 + +**优化**: +- 使用本地缓存批量生成,减少锁竞争 +- 批量化ID生成接口(一次生成100个,供应用层使用) + +**问题:机器ID浪费** + +10位机器ID支持1024个节点,但实际业务可能只有几十个。 + +**思路**:将机器ID拆分为5位数据中心ID + 5位工作机ID,支持更多灵活部署 + +### 方案选型决策 + +根据业务特征进行技术选型: + +```mermaid +flowchart TD + A[开始选择ID方案] --> B{需要
分布式?} + B -->|否| C[自增ID
简单高效] + B -->|是| D{使用场景} + D --> E{主要需求} + E -->|高性能查询| F[雪花ID
有序高性能] + E -->|快速开发| G[UUID v4
即用即走] + E -->|用户可见
可读| H[自增ID+混淆
或自定义短ID] + + style C fill:#90EE90 + style F fill:#90EE90 + style G fill:#90EE90 + style H fill:#90EE90 +``` + +**实战建议**: + +| 场景 | 推荐方案 | 理由 | +|------|----------|------| +| 内部系统关联表ID | 雪花ID | 有序,查询性能好 | +| 用户可见的订单号 | 自增ID+混淆 | 用户友好,可控制格式 | +| 临时会话Token | UUID | 快速生成,无需持久化 | +| 日志/审计ID | UUID | 无冲突风险,生成简单 | +| 移动端离线ID | UUID | 中心化服务不依赖 | + +### 进阶思考 + +**1. 混合方案的可行性** + +能否在不同层级使用不同ID方案?例如:数据库层使用雪花ID(有序),对外接口使用UUID v5(业务ID)? + +``` +业务流程: +用户注册 → 生成UUID v5(业务ID) → 写入数据库(雪花ID为主键) +查询流程: +查询用户 → UUID v5 → 映射到雪花ID → 数据库查询 +``` + +**2. ID生成的可迁移性** + +如果需要从自增ID迁移到雪花ID,如何设计增量迁移策略? + +```go +// 双写+灰度方案 +func GenerateUserID() int64 { + // 灰度期:新用户用雪花ID,老用户保持自增ID + if isFeatureEnabled("snowflake-id") { + return snowflake.Generate() + } + return autoIncrement.Next() +} +``` + +**3. 跨IDC的雪花ID组网** + +多机房场景下,如何设计雪花ID的机器ID分配? + +``` +机房A: 0x00-0x0F (16个机器) +机房B: 0x10-0x1F (16个机器) +机房C: 0x20-0x2F (16个机器) +... +``` + +## 关联笔记 + +- [[金山办公作业/Week05/分页.md]] +- [[金山办公作业/Week05/用户认证.md]] +- [[CS/DB/数据库索引原理]] diff --git a/金山办公作业/Week05/用户认证.md b/金山办公作业/Week05/用户认证.md new file mode 100644 index 0000000..1c0c1d6 --- /dev/null +++ b/金山办公作业/Week05/用户认证.md @@ -0,0 +1,438 @@ +--- +tags: [go, auth, security, jwt, session, authorization, assignment] +create time: 2026-04-17 +--- + +# Go 语言用户认证实现指南 + +## 概述 + +用户认证是 Web 应用的核心安全机制。本文介绍在 Go 中实现用户认证的常见模式和最佳实践。 + +## 认证方式流程 + +### Session-Based 认证 + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + participant DB as Database + + C->>S: POST /login (username, password) + S->>DB: 验证凭证 + DB-->>S: 用户信息 + S->>S: 创建 Session + S-->>C: Set-Cookie: session_id=xxx + Note over C: 存储到浏览器 + + C->>S: GET /protected + Cookie + S->>DB: 根据 session_id 查询 + DB-->>S: Session 数据 + S-->>C: 返回受保护资源 +``` + +### JWT Token 认证 + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + participant Token as JWT Token + + C->>S: POST /login (credentials) + S->>S: 验证用户 + S->>S: 生成 JWT + Token->>S: eyJhbGciOiJIUzI1NiIs... + S-->>C: {access_token: "...", refresh_token: "..."} + + C->>S: GET /api + Authorization: Bearer {token} + S->>S: 验证 Token 签名和过期 + S-->>C: 返回 API 数据 +``` + +## 对比分析 + +| 特性 | Session | JWT | +|-----|--------|-----| +| 存储位置 | 服务器 | 客户端 | +| 状态性 | 有状态 | 无状态 | +| 分布式支持 | 需要共享机制 | 原生支持 | +| 可撤销性 | 容易 | 困难 | +| 适用场景 | 传统网页 | RESTful API | + +## OAuth 2.0 流程 + +```mermaid +sequenceDiagram + participant U as User + participant A as App + participant P as Provider (Google/GitHub) + + U->>A: 点击登录 + A->>P: 重定向到授权页 + U->>P: 同意授权 + P->>A: 重定向 + code + A->>P: 用 code 换取 access_token + P-->>A: {access_token, refresh_token} + A->>P: 使用 token 获取用户信息 + P-->>A: 用户信息 + A-->>U: 登录成功 +``` + +## Go 语言实现方案 + +### Session-Based 实现 + +#### 使用 gorilla/sessions + +```go +package main + +import ( + "net/http" + "github.com/gorilla/sessions" +) + +var store = sessions.NewCookieStore([]byte("secret-key")) + +func loginHandler(w http.ResponseWriter, r *http.Request) { + // 验证用户凭证 + if validateUser(r) { + session, _ := store.Get(r, "session-name") + session.Values["user_id"] = "123" + session.Values["authenticated"] = true + session.Save(r, w) + } +} + +func authMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session, _ := store.Get(r, "session-name") + if auth, ok := session.Values["authenticated"].(bool); !ok || !auth { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + next(w, r) + } +} +``` + +### JWT Implementation + +#### 标准流程 + +```go +package authenticator + +import ( + "time" + "github.com/golang-jwt/jwt/v5" +) + +type Claims struct { + UserID string `json:"user_id"` + Username string `json:"username"` + jwt.RegisteredClaims +} + +type JWTAuthenticator struct { + secretKey []byte +} + +func NewJWTAuthenticator(secretKey string) *JWTAuthenticator { + return &JWTAuthenticator{ + secretKey: []byte(secretKey), + } +} + +// 生成 Token +func (j *JWTAuthenticator) GenerateToken(userID, username string) (string, error) { + claims := Claims{ + UserID: userID, + Username: username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(j.secretKey) +} + +// 验证 Token +func (j *JWTAuthenticator) ValidateToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return j.secretKey, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, jwt.ErrSignatureInvalid +} + +// 中间件 +func (j *JWTAuthenticator) AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Missing authorization header", http.StatusUnauthorized) + return + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + claims, err := j.ValidateToken(tokenString) + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + // 将用户信息存入上下文 + ctx := context.WithValue(r.Context(), "user", claims) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +``` + +### OAuth 2.0 集成 + +```go +package oauth + +import ( + "context" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" +) + +var ( + googleOauthConfig = &oauth2.Config{ + RedirectURL: "http://localhost:8080/callback", + ClientID: "your-client-id", + ClientSecret: "your-client-secret", + Scopes: []string{ + "https://www.googleapis.com/auth/userinfo.email", + }, + Endpoint: google.Endpoint, + } +) + +func HandleGoogleLogin(w http.ResponseWriter, r *http.Request) { + url := googleOauthConfig.AuthCodeURL("state-token") + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +func HandleGoogleCallback(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + token, err := googleOauthConfig.Exchange(context.Background(), code) + if err != nil { + http.Error(w, "Failed to exchange token", http.StatusBadRequest) + return + } + + // 使用 token 获取用户信息 + // ... +} +``` + +## JWT 结构 + +```mermaid +graph LR + A[JWT Token] --> B[Header] + A --> C[Payload] + A --> D[Signature] + + B --> B1[alg: HS256] + B --> B2[typ: JWT] + + C --> C1[user_id] + C --> C2[exp: 时间戳] + C --> C3[iat: 颁发时间] + + D --> D1[Header + Payload] + D --> D2[Secret Key] +``` + +## Authorization(授权) + +**认证 vs 授权**: +- **认证(Authentication)**:你是谁?解决身份验证问题 +- **授权(Authorization)**:你能做什么?解决权限控制问题 + +### Authorization 头格式 + +| 方式 | 格式 | 示例 | +|-----|------|------| +| Bearer Token | `Bearer ` | `Bearer eyJhbGciOiJIUzI1NiIs...` | +| Basic Auth | `Basic ` | `Basic YWxhZGRpbjpvcGVuc2VzYW1l` | +| API Key | `ApiKey ` | `ApiKey abc123xyz` | + +### 在 Go 中处理 Authorization + +```go +// Bearer Token 解析 +func parseBearerToken(header string) (string, error) { + parts := strings.SplitN(header, " ", 2) + if parts[0] != "Bearer" || len(parts) < 2 { + return "", errors.New("invalid authorization format") + } + return parts[1], nil +} + +// Basic Auth 解析 +func parseBasicAuth(header string) (username, password string, err error) { + parts := strings.SplitN(header, " ", 2) + if parts[0] != "Basic" || len(parts) < 2 { + return "", "", errors.New("invalid basic auth format") + } + + decoded, err := base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + return "", "", err + } + + credentials := string(decoded) + idx := strings.Index(credentials, ":") + if idx == -1 { + return "", "", errors.New("invalid credentials format") + } + + return credentials[:idx], credentials[idx+1:], nil +} +``` + +**详细信息**:💡 详见 [[CS/NET/Authorization]] - Authorization 机制详解(RBAC、ABAC、策略引擎) + +## 安全最佳实践 + +### 密码安全 + +```go +package auth + +import "golang.org/x/crypto/bcrypt" + +// 加密密码 +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// 验证密码 +func CheckPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} +``` + +### 密钥管理 + +- ✅ 使用环境变量存储密钥 +- ✅ 使用专业密钥管理服务(HashiCorp Vault) +- ❌ 不将密钥写入代码库 +- ❌ 不在日志中打印密钥 + +### HTTPS 强制 + +```go +func RedirectHTTPS(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Scheme != "https" { + httpsURL := "https://" + r.Host + r.URL.RequestURI() + http.Redirect(w, r, httpsURL, http.StatusMovedPermanently) + return + } + next.ServeHTTP(w, r) + }) +} +``` + +## 常见安全威胁与防护 + +### CSRF 攻击流程与防护 + +```mermaid +sequenceDiagram + participant A as 攻击者 + participant U as 用户 + participant S as 服务器 + + %%% 受害场景 %%% + U->>U: 已登录网站 S,持有 Cookie + A->>U: 诱导点击恶意链接 + U->>S: POST /transfer (自动携带 Cookie) + S-->>U: 转账成功 ⚠️ + + %%% 防护方案 %%% + Note over S: CSRF Token 模式 + U->>S: GET /form + S-->>U: HTML + CSRF Token + U->>S: POST + Token in data + S->>S: 验证 Token + S-->>U: 请求通过 ✓ +``` + +### 威胁防护清单 + +| 威胁类型 | 防护措施 | +|---------|---------| +| SQL 注入 | 使用参数化查询、ORM | +| XSS 攻击 | 输入验证、输出编码 | +| CSRF 攻击 | CSRF Token、SameSite Cookie | +| 重放攻击 | Timestamp、Nonce 验证 | +| Token 窃取 | HTTPS、短期 Token、刷新机制 | + +## 性能优化 + +### JWT 优化 + +```go +// 1. 缓存已验证 Token +type CacheAuthenticator struct { + jwt *JWTAuthenticator + cache *cache.Cache +} + +// 2. 使用 Key ID 轮换密钥 +type KeyRotation struct { + currentKey []byte + oldKeys [][]byte +} + +// 3. 缩短 Token 生命周期,使用 Refresh Token +type TokenPair struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} +``` + +## 推荐库 + +| 库名 | 用途 | 特点 | +|-----|------|------| +| gorilla/sessions | Session 管理 | 功能完善,支持多种存储 | +| golang-jwt/jwt | JWT 处理 | 轻量级,标准实现 | +| golang.org/x/oauth2 | OAuth 2.0 | 官方实现,支持主流平台 | +| golang.org/x/crypto | 密码加密 | 包含 bcrypt、scrypt 等算法 | + +## 学习资源 + +- [JWT 官方文档](https://jwt.io/) +- [OWASP 认证备忘单](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) +- [Go Web Examples - Authentication](https://gowebexamples.com/routes/) + +## 相关笔记 + +- [[CS/NET/Authorization]] - Authorization 授权机制详解 +- [[CS/NET/HTTPS]] - HTTPS 原理 +- [[CS/OS/进程线程]] - 并发安全 +- [[CS/DB/SQL安全]] - SQL 注入防护