diff --git a/.gitignore b/.gitignore index 6a11c1e..26f8dcc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ temp/* CLAUDE.md MULTI_LANGUAGE_PLAN.md .trae +scripts/collapse_code_blocks.py +.gitignore +scripts/verify.sh diff --git a/docs-readme/zh-TW/README.md b/docs-readme/zh-TW/README.md index c90bd49..933ea46 100644 --- a/docs-readme/zh-TW/README.md +++ b/docs-readme/zh-TW/README.md @@ -89,19 +89,19 @@ #### 附錄:業務思維 -| 章節 | 關鍵內容 | 狀態 | -| :----------------------------------------------------------------------------------------- | :----------------------------------------- | :--- | -| [附錄A:產品思維與方案設計](../docs/zh-cn/stage-1/appendix-a-product-thinking/index.md) | 從零到一做產品需要考慮的思維框架 | ✅ | -| [附錄B:AI 行業應用場景參考 (B端)](../docs/zh-cn/stage-1/appendix-industry-scenarios/index.md) | 了解 AI 在不同產業的應用場景 | ✅ | -| [附錄C:AI 消費場景靈感參考 (C端)](../docs/zh-cn/stage-1/appendix-c-consumer-scenarios/index.md) | 探索 AI 在消費級產品中的應用場景 | ✅ | +| 章節 | 關鍵內容 | 狀態 | +| :----------------------------------------------------------------------------------------------- | :------------------------------- | :--- | +| [附錄A:產品思維與方案設計](../docs/zh-cn/stage-1/appendix-a-product-thinking/index.md) | 從零到一做產品需要考慮的思維框架 | ✅ | +| [附錄B:AI 行業應用場景參考 (B端)](../docs/zh-cn/stage-1/appendix-industry-scenarios/index.md) | 了解 AI 在不同產業的應用場景 | ✅ | +| [附錄C:AI 消費場景靈感參考 (C端)](../docs/zh-cn/stage-1/appendix-c-consumer-scenarios/index.md) | 探索 AI 在消費級產品中的應用場景 | ✅ | #### 附錄:技術方案 -| 章節 | 關鍵內容 | 狀態 | -| :-------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | :--- | -| [附錄D:寫代碼時遇到錯誤怎麼辦](../docs/zh-cn/stage-1/appendix-b-common-errors/index.md) | vibe coding 中的常見錯誤及排查方法 | ✅ | -| [附錄E:七款 AI 編程工具對比](../docs/zh-cn/stage-1/appendix-articles/example0-1/vibe-coding-tools-snake-game-tutorial.md) | 對比測試主流 AI 編程平台 | ✅ | -| [附錄F:用設計和編程 Agent 設計網站](../docs/zh-cn/stage-1/appendix-articles/example0-2/vibe-coding-tools-build-website-with-ai-coding-and-design-agents.md) | 學習如何使用 AI 智能體協同工作 | ✅ | +| 章節 | 關鍵內容 | 狀態 | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------- | :--- | +| [附錄D:寫代碼時遇到錯誤怎麼辦](../docs/zh-cn/stage-1/appendix-b-common-errors/index.md) | vibe coding 中的常見錯誤及排查方法 | ✅ | +| [附錄E:七款 AI 編程工具對比](../docs/zh-cn/stage-1/appendix-articles/example0-1/vibe-coding-tools-snake-game-tutorial.md) | 對比測試主流 AI 編程平台 | ✅ | +| [附錄F:用設計和編程 Agent 設計網站](../docs/zh-cn/stage-1/appendix-articles/example0-2/vibe-coding-tools-build-website-with-ai-coding-and-design-agents.md) | 學習如何使用 AI 智能體協同工作 | ✅ | ### 二、初中級開發工程師 diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index fd31874..b41667e 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -673,8 +673,14 @@ export default defineConfig({ { text: '系统缓存设计', link: '/zh-cn/appendix/cache-design' }, { text: '消息队列设计', link: '/zh-cn/appendix/queue-design' }, { text: '鉴权原理与实战', link: '/zh-cn/appendix/auth-design' }, - { text: '网关与反向代理', link: '/zh-cn/appendix/gateway-proxy' }, - { text: '负载均衡策略', link: '/zh-cn/appendix/load-balancing' }, + { + text: '网关与反向代理', + link: '/zh-cn/appendix/gateway-proxy' + }, + { + text: '负载均衡策略', + link: '/zh-cn/appendix/load-balancing' + }, { text: '埋点设计', link: '/zh-cn/appendix/tracking-design' }, { text: '线上运维', link: '/zh-cn/appendix/operations' } ] diff --git a/docs/.vitepress/theme/components/appendix/backend-languages/BackendLanguagesDemo.vue b/docs/.vitepress/theme/components/appendix/backend-languages/BackendLanguagesDemo.vue index 49789eb..7c1d50b 100644 --- a/docs/.vitepress/theme/components/appendix/backend-languages/BackendLanguagesDemo.vue +++ b/docs/.vitepress/theme/components/appendix/backend-languages/BackendLanguagesDemo.vue @@ -1,5 +1,15 @@ diff --git a/docs/.vitepress/theme/components/appendix/backend-languages/ConcurrencyModelDemo.vue b/docs/.vitepress/theme/components/appendix/backend-languages/ConcurrencyModelDemo.vue index 3b9e750..7badcdf 100644 --- a/docs/.vitepress/theme/components/appendix/backend-languages/ConcurrencyModelDemo.vue +++ b/docs/.vitepress/theme/components/appendix/backend-languages/ConcurrencyModelDemo.vue @@ -1,11 +1,16 @@ - - diff --git a/docs/.vitepress/theme/components/appendix/backend-languages/DeveloperEfficiencyDemo.vue b/docs/.vitepress/theme/components/appendix/backend-languages/DeveloperEfficiencyDemo.vue index 6d71aab..7c3515c 100644 --- a/docs/.vitepress/theme/components/appendix/backend-languages/DeveloperEfficiencyDemo.vue +++ b/docs/.vitepress/theme/components/appendix/backend-languages/DeveloperEfficiencyDemo.vue @@ -1,144 +1,50 @@ @@ -148,259 +54,153 @@ import { ref, computed } from 'vue' const selectedTask = ref('rest') -const tasks = [ - { id: 'rest', name: 'REST API 服务' }, - { id: 'web', name: 'Web 应用' }, - { id: 'script', name: '数据处理脚本' }, - { id: 'micro', name: '微服务' } -] - -const languages = [ - 'Python', - 'Ruby', - 'Go', - 'Node.js', - 'Java', - 'C#', - 'Rust', - 'C++' -] - -const taskMetrics = { - rest: { - Python: { lines: 50, time: 4, debug: 2 }, - Ruby: { lines: 45, time: 3.5, debug: 2.5 }, - Go: { lines: 80, time: 5, debug: 1.5 }, - 'Node.js': { lines: 60, time: 4.5, debug: 2 }, - Java: { lines: 150, time: 8, debug: 2 }, - 'C#': { lines: 120, time: 7, debug: 2 }, - Rust: { lines: 100, time: 10, debug: 3 }, - 'C++': { lines: 180, time: 12, debug: 5 }, - linesInsight: - 'Python 和 Ruby 用最少的代码实现 REST API,得益于简洁的语法和强大的框架(Flask、Sinatra)。Go 虽然语法简洁,但需要显式类型声明。Java 和 C# 的样板代码最多。', - timeInsight: - 'Ruby 和 Python 开发最快,适合快速迭代。Go 和 Node.js 居中,平衡了开发速度和性能。Java 和 C# 开发时间较长,但后期维护成本低。Rust 和 C++ 开发时间最长,主要受学习曲线和编译时间影响。', - debugInsight: - 'Go、Java、C# 的静态类型让调试更容易,大部分错误在编译时就能发现。Python 和 Ruby 虽然开发快,但运行时错误多,调试时间长。Rust 的借用检查器虽然学习曲线陡峭,但能提前发现大量 bug。' - }, - web: { - Python: { lines: 200, time: 10, debug: 5 }, - Ruby: { lines: 180, time: 9, debug: 5 }, - Go: { lines: 300, time: 12, debug: 4 }, - 'Node.js': { lines: 250, time: 11, debug: 5 }, - Java: { lines: 500, time: 20, debug: 6 }, - 'C#': { lines: 400, time: 18, debug: 6 }, - Rust: { lines: 350, time: 25, debug: 8 }, - 'C++': { lines: 600, time: 30, debug: 12 }, - linesInsight: - 'Rails 和 Django 的"约定优于配置"让 Web 开发极其高效,代码量最少。全栈的 Node.js 也表现不错。Go 需要 more boilerplate。Java 的 Spring Boot 虽然强大,但配置和样板代码较多。', - timeInsight: - 'Ruby (Rails) 和 Python (Django) 是 Web 开发的效率之王,内置 ORM、模板引擎、认证等功能,开箱即用。Node.js 的全栈特性让前后端统一,减少沟通成本。Go 和 Java 需要更多配置和 boilerplate。', - debugInsight: - '静态类型语言(Go、Java、C#)在大型 Web 项目中优势明显,IDE 支持更好,重构更安全。Python 和 Ruby 在小项目中调试很快,但随着项目增长,动态类型带来的维护成本会急剧上升。' - }, - script: { - Python: { lines: 20, time: 1, debug: 0.5 }, - Ruby: { lines: 18, time: 1, debug: 0.5 }, - Go: { lines: 40, time: 2, debug: 0.5 }, - 'Node.js': { lines: 25, time: 1.5, debug: 0.5 }, - Java: { lines: 80, time: 4, debug: 1 }, - 'C#': { lines: 70, time: 3.5, debug: 1 }, - Rust: { lines: 50, time: 4, debug: 1 }, - 'C++': { lines: 100, time: 5, debug: 2 }, - linesInsight: - 'Python 是脚本自动化的绝对王者,标准库丰富,第三方库如 Pandas、Requests 让数据处理极其简单。Ruby 也很优秀。其他语言对于简单脚本来说都太重量级了。', - timeInsight: - 'Python 和 Ruby 是脚本任务的首选,几行代码就能完成复杂的数据处理。Node.js 在处理 JSON 数据时也很方便。编译型语言(Go、Java、C++)对于简单脚本来说 overhead 太大。', - debugInsight: - 'Python 的交互式解释器(REPL)和丰富的调试工具(pdb、ipdb)让脚本调试极其高效。Ruby 的 Pry 也很强大。其他语言的编译/运行循环对于脚本开发来说太慢了。' - }, - micro: { - Python: { lines: 100, time: 6, debug: 3 }, - Ruby: { lines: 90, time: 5.5, debug: 3.5 }, - Go: { lines: 120, time: 7, debug: 2 }, - 'Node.js': { lines: 110, time: 6.5, debug: 3 }, - Java: { lines: 250, time: 15, debug: 4 }, - 'C#': { lines: 200, time: 13, debug: 4 }, - Rust: { lines: 140, time: 18, debug: 5 }, - 'C++': { lines: 300, time: 22, debug: 8 }, - linesInsight: - 'Go 是微服务的理想选择,单一二进制文件 + 内置 HTTP 服务器 + 强大的标准库。Node.js 的 Express/Koa 也很轻量。Python 的 FastAPI 表现不错,但性能和并发不如 Go。', - timeInsight: - 'Go 和 Node.js 在微服务开发中效率最高,启动快,部署简单。Python 和 Ruby 适合快速原型,但生产环境需要更多优化。Java 和 C# 的 Spring Cloud/.NET 虽然强大,但对于简单微服务来说太重量级。', - debugInsight: - 'Go 和 Rust 的类型系统和错误处理让微服务的调试和测试更容易。Java 和 C# 的成熟工具链(JUnit、NUnit)也很有优势。Python 和 Ruby 的动态类型在分布式系统中可能带来运行时错误。' - } +const taskData = { + rest: [ + { name: 'Python', time: 4 }, + { name: 'Ruby', time: 3.5 }, + { name: 'Go', time: 5 }, + { name: 'Node.js', time: 4.5 }, + { name: 'Java', time: 8 }, + { name: 'Rust', time: 10 } + ], + web: [ + { name: 'Ruby', time: 9 }, + { name: 'Python', time: 10 }, + { name: 'Node.js', time: 11 }, + { name: 'Go', time: 12 }, + { name: 'Java', time: 20 }, + { name: 'Rust', time: 25 } + ], + script: [ + { name: 'Python', time: 1 }, + { name: 'Ruby', time: 1 }, + { name: 'Node.js', time: 1.5 }, + { name: 'Go', time: 2 }, + { name: 'Java', time: 4 }, + { name: 'Rust', time: 4 } + ] } -const currentMetrics = computed(() => { - return taskMetrics[selectedTask.value] -}) - const sortedLanguages = computed(() => { - return languages - .map((lang) => ({ - name: lang, - ...currentMetrics.value[lang] - })) - .sort((a, b) => a.lines - b.lines) + return [...taskData[selectedTask.value]].sort((a, b) => a.time - b.time) }) -const getBarWidth = (value) => { - const max = Math.max( - ...Object.values(currentMetrics.value).flatMap((v) => [ - v.lines, - v.time * 20, - v.debug * 20 - ]) - ) - return (value / max) * 100 -} - -const getTaskDetail = (taskId) => { - return taskMetrics[taskId] -} - -const getRadarPosition = (langName) => { - const metrics = currentMetrics.value[langName] - const avgLines = - Object.values(currentMetrics.value).reduce((sum, v) => sum + v.lines, 0) / - languages.length - const avgTime = - Object.values(currentMetrics.value).reduce((sum, v) => sum + v.time, 0) / - languages.length - const avgDebug = - Object.values(currentMetrics.value).reduce((sum, v) => sum + v.debug, 0) / - languages.length - - // Normalize metrics (lower is better, so we invert) - const linesScore = 1 - metrics.lines / 300 // Max lines ~300 - const timeScore = 1 - metrics.time / 30 // Max time ~30 - const debugScore = 1 - metrics.debug / 12 // Max debug ~12 - - // Position in 2D space - // X: code efficiency (linesScore) vs ecosystem (hardcoded) - // Y: speed (timeScore) vs maintainability (debugScore) - const x = 50 + (linesScore - 0.5) * 80 - const y = 50 + (timeScore - 0.5) * 80 - - return { - left: `${x}%`, - top: `${y}%` - } -} - -const updateMetrics = () => { - // Trigger reactivity -} +const maxTime = computed(() => { + return Math.max(...taskData[selectedTask.value].map(l => l.time)) +}) diff --git a/docs/.vitepress/theme/components/appendix/backend-languages/LanguageComparisonDemo.vue b/docs/.vitepress/theme/components/appendix/backend-languages/LanguageComparisonDemo.vue index 077d1c9..d1928bc 100644 --- a/docs/.vitepress/theme/components/appendix/backend-languages/LanguageComparisonDemo.vue +++ b/docs/.vitepress/theme/components/appendix/backend-languages/LanguageComparisonDemo.vue @@ -1,202 +1,323 @@ diff --git a/docs/.vitepress/theme/components/appendix/backend-languages/LanguageEcosystemDemo.vue b/docs/.vitepress/theme/components/appendix/backend-languages/LanguageEcosystemDemo.vue index 4a15049..86e6c35 100644 --- a/docs/.vitepress/theme/components/appendix/backend-languages/LanguageEcosystemDemo.vue +++ b/docs/.vitepress/theme/components/appendix/backend-languages/LanguageEcosystemDemo.vue @@ -1,994 +1,202 @@ diff --git a/docs/.vitepress/theme/components/appendix/backend-languages/LanguageSelectorDemo.vue b/docs/.vitepress/theme/components/appendix/backend-languages/LanguageSelectorDemo.vue index 2c4f1d3..585862d 100644 --- a/docs/.vitepress/theme/components/appendix/backend-languages/LanguageSelectorDemo.vue +++ b/docs/.vitepress/theme/components/appendix/backend-languages/LanguageSelectorDemo.vue @@ -1,593 +1,370 @@ diff --git a/docs/.vitepress/theme/components/appendix/backend-languages/MemoryManagementDemo.vue b/docs/.vitepress/theme/components/appendix/backend-languages/MemoryManagementDemo.vue index 58068a0..e00552f 100644 --- a/docs/.vitepress/theme/components/appendix/backend-languages/MemoryManagementDemo.vue +++ b/docs/.vitepress/theme/components/appendix/backend-languages/MemoryManagementDemo.vue @@ -1,18 +1,40 @@ @@ -21,19 +43,19 @@ const models = [ { name: '垃圾回收 (GC)', icon: '♻️', - desc: '运行时自动回收不再使用的内存', + description: '运行时自动回收不再使用的内存', languages: ['Java', 'Go', 'Python', 'Node.js'] }, { name: '手动管理', icon: '🔧', - desc: '开发者显式申请和释放内存', + description: '开发者显式申请和释放内存', languages: ['C', 'C++'] }, { name: '所有权系统', icon: '🔒', - desc: '编译时通过规则保证内存安全', + description: '编译时通过规则保证内存安全', languages: ['Rust'] } ] @@ -43,41 +65,109 @@ const models = [ .memory-management-demo { border: 1px solid var(--vp-c-divider); border-radius: 8px; - padding: 20px; background: var(--vp-c-bg-soft); + padding: 1rem; + margin: 1rem 0; + max-height: 600px; + overflow-y: auto; } -.memory-models { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 16px; - margin-top: 20px; + +.demo-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.75rem; } -.model-card { - padding: 16px; + +.demo-header .icon { + font-size: 1.25rem; +} + +.demo-header .title { + font-weight: bold; + font-size: 1rem; +} + +.demo-header .subtitle { + color: var(--vp-c-text-2); + font-size: 0.85rem; + margin-left: 0.5rem; +} + +.intro-text { + font-size: 0.9rem; + color: var(--vp-c-text-2); + line-height: 1.6; + margin-bottom: 1rem; + padding: 0.75rem; background: var(--vp-c-bg); border-radius: 6px; +} + +.intro-text .highlight { + color: var(--vp-c-brand-1); + font-weight: 500; +} + +.models-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.75rem; + margin-bottom: 1rem; +} + +.model-card { + background: var(--vp-c-bg); + padding: 1rem; + border-radius: 6px; text-align: center; border: 1px solid var(--vp-c-divider); } + .model-icon { - font-size: 2em; - margin-bottom: 8px; + font-size: 2rem; + margin-bottom: 0.5rem; } + .model-name { - font-weight: bold; - margin-bottom: 4px; + font-weight: 600; + font-size: 0.9rem; + color: var(--vp-c-text-1); + margin-bottom: 0.25rem; } + .model-desc { - font-size: 0.9em; + font-size: 0.8rem; color: var(--vp-c-text-2); - margin-bottom: 12px; + margin-bottom: 0.75rem; + line-height: 1.4; } + +.model-languages { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + justify-content: center; +} + .lang-tag { - display: inline-block; - padding: 2px 6px; - margin: 2px; - background: var(--vp-c-bg-mute); + padding: 0.2rem 0.5rem; + background: var(--vp-c-bg-soft); border-radius: 4px; - font-size: 0.8em; + font-size: 0.7rem; + color: var(--vp-c-brand-1); + font-weight: 600; +} + +.info-box { + background: var(--vp-c-bg-alt); + padding: 0.75rem; + border-radius: 6px; + font-size: 0.85rem; + color: var(--vp-c-text-2); +} + +.info-box .icon { + margin-right: 0.25rem; } diff --git a/docs/.vitepress/theme/components/appendix/backend-languages/PerformanceBenchmarkDemo.vue b/docs/.vitepress/theme/components/appendix/backend-languages/PerformanceBenchmarkDemo.vue index e7cb980..4024373 100644 --- a/docs/.vitepress/theme/components/appendix/backend-languages/PerformanceBenchmarkDemo.vue +++ b/docs/.vitepress/theme/components/appendix/backend-languages/PerformanceBenchmarkDemo.vue @@ -1,45 +1,41 @@ @@ -67,56 +61,57 @@ import { ref, computed } from 'vue' const selectedScenario = ref('hello') const isRunning = ref(false) +const scenarios = [ + { id: 'hello', label: '🏁 简单 HTTP (Hello World)' }, + { id: 'json', label: '📦 JSON 序列化' }, + { id: 'db', label: '🗄️ 数据库查询' }, + { id: 'compute', label: '⚙️ CPU 密集计算' } +] + const benchmarkData = { - hello: { - 'C++': { rps: 1500000, time: 0.5 }, - Rust: { rps: 1200000, time: 0.6 }, - Go: { rps: 1000000, time: 0.7 }, - Java: { rps: 700000, time: 1.0 }, - 'Node.js': { rps: 800000, time: 0.9 }, - Python: { rps: 200000, time: 2.5 }, - Ruby: { rps: 150000, time: 3.0 }, - PHP: { rps: 250000, time: 2.0 } - }, - json: { - 'C++': { rps: 800000, time: 1.0 }, - Rust: { rps: 700000, time: 1.1 }, - Go: { rps: 600000, time: 1.2 }, - Java: { rps: 500000, time: 1.5 }, - 'Node.js': { rps: 450000, time: 1.6 }, - Python: { rps: 150000, time: 4.0 }, - Ruby: { rps: 120000, time: 5.0 }, - PHP: { rps: 180000, time: 3.5 } - }, - db: { - 'C++': { rps: 300000, time: 2.5 }, - Rust: { rps: 280000, time: 2.6 }, - Go: { rps: 250000, time: 3.0 }, - Java: { rps: 200000, time: 3.5 }, - 'Node.js': { rps: 220000, time: 3.2 }, - Python: { rps: 80000, time: 8.0 }, - Ruby: { rps: 70000, time: 9.0 }, - PHP: { rps: 90000, time: 7.5 } - }, - compute: { - 'C++': { rps: 500000, time: 1.5 }, - Rust: { rps: 480000, time: 1.6 }, - Go: { rps: 400000, time: 2.0 }, - Java: { rps: 350000, time: 2.3 }, - 'Node.js': { rps: 50000, time: 15.0 }, - Python: { rps: 30000, time: 25.0 }, - Ruby: { rps: 25000, time: 30.0 }, - PHP: { rps: 35000, time: 20.0 } - } + hello: [ + { language: 'C++', rps: 1500000 }, + { language: 'Rust', rps: 1200000 }, + { language: 'Go', rps: 1000000 }, + { language: 'Node.js', rps: 800000 }, + { language: 'Java', rps: 700000 }, + { language: 'Python', rps: 200000 }, + { language: 'Ruby', rps: 150000 } + ], + json: [ + { language: 'C++', rps: 800000 }, + { language: 'Rust', rps: 700000 }, + { language: 'Go', rps: 600000 }, + { language: 'Node.js', rps: 450000 }, + { language: 'Java', rps: 500000 }, + { language: 'Python', rps: 150000 }, + { language: 'Ruby', rps: 120000 } + ], + db: [ + { language: 'C++', rps: 300000 }, + { language: 'Rust', rps: 280000 }, + { language: 'Go', rps: 250000 }, + { language: 'Node.js', rps: 220000 }, + { language: 'Java', rps: 200000 }, + { language: 'Python', rps: 80000 }, + { language: 'Ruby', rps: 70000 } + ], + compute: [ + { language: 'C++', rps: 500000 }, + { language: 'Rust', rps: 480000 }, + { language: 'Go', rps: 400000 }, + { language: 'Java', rps: 350000 }, + { language: 'Node.js', rps: 50000 }, + { language: 'Python', rps: 30000 }, + { language: 'Ruby', rps: 25000 } + ] } const explanations = { - hello: - '简单的 Hello World HTTP 响应测试。C++ 和 Rust 在这个测试中展现出接近硬件的性能优势。Go 和 Node.js 表现也很优秀,因为它们的 HTTP 栈经过高度优化。Python 和 Ruby 由于解释器开销,性能相对较低。', - json: 'JSON 序列化/反序列化测试。这个测试考验语言的 JSON 处理能力。C++ 和 Rust 依然领先,但 Node.js 的表现也不错(V8 引擎优化)。Python 的标准库 json 模块性能尚可,但比编译型语言慢很多。', - db: '模拟数据库查询(连接池 + 查询)。这个测试更接近真实应用。性能差距缩小了,因为瓶颈主要在数据库 I/O 而非语言本身。但依然能看到编译型语言(C++、Rust、Go)的优势。', - compute: - 'CPU 密集型计算(斐波那契数列)。这个测试充分暴露了 Node.js 的短板:单线程 + V8 编译优化不如静态语言。Python 和 Ruby 表现最差,因为它们是解释型语言,且 GIL 限制了多线程性能。C++ 和 Rust 几乎是唯一选择。' + hello: '简单的 HTTP 响应测试。C++ 和 Rust 展现出接近硬件的性能优势。Go 和 Node.js 表现优秀(HTTP 栈经过高度优化)。Python 和 Ruby 由于解释器开销,性能相对较低。', + json: 'JSON 序列化测试。C++ 和 Rust 依然领先,Node.js 的 V8 引擎优化让它的表现也不错。Python 标准库 json 模块性能尚可,但比编译型语言慢很多。', + db: '模拟数据库查询。性能差距缩小,因为瓶颈主要在数据库 I/O。但编译型语言(C++、Rust、Go)的优势依然明显。', + compute: 'CPU 密集型计算(斐波那契)。Node.js 的短板暴露:单线程 + V8 优化不如静态语言。Python 和 Ruby 表现最差(解释型语言 + GIL 限制)。' } const currentResults = ref([]) @@ -129,16 +124,10 @@ const runBenchmark = () => { isRunning.value = true currentResults.value = [] - // 模拟测试延迟 setTimeout(() => { - const data = benchmarkData[selectedScenario.value] - currentResults.value = Object.entries(data).map(([language, stats]) => ({ - language, - rps: stats.rps, - time: stats.time - })) + currentResults.value = benchmarkData[selectedScenario.value] isRunning.value = false - }, 1000) + }, 800) } const getBarWidth = (rps) => { @@ -147,8 +136,8 @@ const getBarWidth = (rps) => { } const getBarClass = (rps) => { - if (rps >= 800000) return 'bar-high' - if (rps >= 300000) return 'bar-medium' + if (rps >= 500000) return 'bar-high' + if (rps >= 200000) return 'bar-medium' return 'bar-low' } @@ -162,69 +151,106 @@ const getCurrentExplanation = () => { return explanations[selectedScenario.value] } -// 初始运行一次 runBenchmark() diff --git a/docs/.vitepress/theme/components/appendix/backend-languages/SyntaxComparisonDemo.vue b/docs/.vitepress/theme/components/appendix/backend-languages/SyntaxComparisonDemo.vue index ebf017c..250dc91 100644 --- a/docs/.vitepress/theme/components/appendix/backend-languages/SyntaxComparisonDemo.vue +++ b/docs/.vitepress/theme/components/appendix/backend-languages/SyntaxComparisonDemo.vue @@ -1,80 +1,58 @@ @@ -86,13 +64,10 @@ const selectedLang = ref('Python') const languages = [ { name: 'Python', icon: '🐍' }, - { name: 'Ruby', icon: '💎' }, { name: 'Go', icon: '🐹' }, { name: 'Node.js', icon: '💚' }, { name: 'Java', icon: '☕' }, - { name: 'C#', icon: '💜' }, - { name: 'Rust', icon: '🦀' }, - { name: 'C++', icon: '⚡' } + { name: 'Rust', icon: '🦀' } ] const codes = { @@ -101,11 +76,6 @@ const codes = { filename: 'hello.py', complexity: '极简' }, - Ruby: { - code: `puts "Hello, World!"`, - filename: 'hello.rb', - complexity: '极简' - }, Go: { code: `package main @@ -131,175 +101,136 @@ func main() { filename: 'HelloWorld.java', complexity: '冗长' }, - 'C#': { - code: `using System; - -class Program { - static void Main() { - Console.WriteLine("Hello, World!"); - } -}`, - filename: 'Program.cs', - complexity: '冗长' - }, Rust: { code: `fn main() { println!("Hello, World!"); }`, filename: 'main.rs', complexity: '简洁' - }, - 'C++': { - code: `#include - -int main() { - std::cout << "Hello, World!" << std::endl; - return 0; -}`, - filename: 'hello.cpp', - complexity: '中等' } } -const analyses = { - Python: - 'Python 的语法极其简洁,只有一行代码。这也是为什么它被称为"伪代码语言"——读起来就像英语一样自然。没有任何样板代码,直接表达意图。', - Ruby: 'Ruby 受 Perl 影响,语法非常优雅。puts 是 "put string" 的缩写,字符串不需要括号(虽然可以加)。Ruby 哲学是"程序员快乐至上"。', - Go: 'Go 的语法虽然比 Python 冗长,但非常清晰。package main、import、func main() 都是必要的显式声明,这让代码更容易理解和维护。', - 'Node.js': - 'Node.js 使用 JavaScript,语法简单直接。console.log() 是浏览器和 Node.js 通用的输出方式。前端开发者零学习成本。', - Java: 'Java 是典型的"仪式感"语言。class、public static void main、String[] args 都是必须的样板代码。虽然冗长,但结构清晰,适合大型项目。', - 'C#': 'C# 和 Java 非常相似,同样需要 class 和 Main 方法。using System 类似 Java 的 import,但更现代一些。.NET Core 后跨平台能力大幅提升。', - Rust: 'Rust 的 fn main() 和 println! 宏看起来简洁,但 println! 后面的 ! 表示这是一个宏(不是函数)。Rust 的简洁来自于零成本抽象的设计哲学。', - 'C++': - 'C++ 需要 #include 头文件,std::cout 使用流操作符 <<,return 0 表示程序成功退出。虽然比 C 语言简洁(printf),但依然保留了很多底层细节。' -} - const getCode = (lang) => { - return codes[lang].code -} - -const getFileName = (lang) => { - return codes[lang].filename + return codes[lang] } const getLineCount = (lang) => { return codes[lang].code.split('\n').length } - -const getCharCount = (lang) => { - return codes[lang].code.replace(/\s/g, '').length -} - -const getComplexity = (lang) => { - return codes[lang].complexity -} - -const getAnalysis = (lang) => { - return analyses[lang] -} - -const getComplexityWidth = (lang) => { - const max = 10 // Java is the longest - const lines = getLineCount(lang) - return (lines / max) * 100 -} diff --git a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/CompositeDemo.vue b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/CompositeDemo.vue index a57e5c3..bc120fb 100644 --- a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/CompositeDemo.vue +++ b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/CompositeDemo.vue @@ -1,13 +1,58 @@ @@ -15,36 +60,214 @@ diff --git a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/DomToRenderTreeDemo.vue b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/DomToRenderTreeDemo.vue index 5b55f5a..1f47c83 100644 --- a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/DomToRenderTreeDemo.vue +++ b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/DomToRenderTreeDemo.vue @@ -1,50 +1,300 @@ diff --git a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/LayoutReflowDemo.vue b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/LayoutReflowDemo.vue index b58b068..d70c038 100644 --- a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/LayoutReflowDemo.vue +++ b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/LayoutReflowDemo.vue @@ -1,50 +1,283 @@ diff --git a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/MacroMicroTaskDemo.vue b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/MacroMicroTaskDemo.vue index 98bff19..3a751a6 100644 --- a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/MacroMicroTaskDemo.vue +++ b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/MacroMicroTaskDemo.vue @@ -1,13 +1,85 @@ @@ -15,36 +87,277 @@ diff --git a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/PaintLayerDemo.vue b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/PaintLayerDemo.vue index fd1790e..5448452 100644 --- a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/PaintLayerDemo.vue +++ b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/PaintLayerDemo.vue @@ -1,13 +1,57 @@ @@ -15,36 +59,272 @@ diff --git a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/RenderingPerformanceDemo.vue b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/RenderingPerformanceDemo.vue index 8789ed5..6fcc222 100644 --- a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/RenderingPerformanceDemo.vue +++ b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/RenderingPerformanceDemo.vue @@ -1,50 +1,298 @@ diff --git a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/RenderingPipelineDemo.vue b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/RenderingPipelineDemo.vue index 13cd313..00aad8f 100644 --- a/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/RenderingPipelineDemo.vue +++ b/docs/.vitepress/theme/components/appendix/browser-rendering-pipeline/RenderingPipelineDemo.vue @@ -1,80 +1,53 @@ @@ -82,322 +55,237 @@ diff --git a/docs/.vitepress/theme/components/appendix/cache-design/CacheArchitectureDemo.vue b/docs/.vitepress/theme/components/appendix/cache-design/CacheArchitectureDemo.vue index 6ca28c6..8698fdd 100644 --- a/docs/.vitepress/theme/components/appendix/cache-design/CacheArchitectureDemo.vue +++ b/docs/.vitepress/theme/components/appendix/cache-design/CacheArchitectureDemo.vue @@ -1,12 +1,14 @@ - @@ -99,9 +103,7 @@ const simulateRequest = () => { const hit = Math.random() * 100 < hitRate.value lastResult.value = { hit, - time: hit - ? Math.floor(Math.random() * 3) + 1 - : Math.floor(Math.random() * 20) + 40 + time: hit ? Math.floor(Math.random() * 3) + 1 : Math.floor(Math.random() * 20) + 40 } currentLayer.value = 'cache' @@ -119,26 +121,36 @@ const simulateRequest = () => { diff --git a/docs/.vitepress/theme/components/appendix/cache-design/CacheArchitectureOverview.vue b/docs/.vitepress/theme/components/appendix/cache-design/CacheArchitectureOverview.vue index caa4a6e..3cf7251 100644 --- a/docs/.vitepress/theme/components/appendix/cache-design/CacheArchitectureOverview.vue +++ b/docs/.vitepress/theme/components/appendix/cache-design/CacheArchitectureOverview.vue @@ -1,13 +1,126 @@ @@ -15,36 +128,186 @@ diff --git a/docs/.vitepress/theme/components/appendix/cache-design/CacheHierarchyDemo.vue b/docs/.vitepress/theme/components/appendix/cache-design/CacheHierarchyDemo.vue index d64f78d..80bcadf 100644 --- a/docs/.vitepress/theme/components/appendix/cache-design/CacheHierarchyDemo.vue +++ b/docs/.vitepress/theme/components/appendix/cache-design/CacheHierarchyDemo.vue @@ -1,13 +1,95 @@ @@ -15,36 +97,306 @@ diff --git a/docs/.vitepress/theme/components/appendix/cache-design/CacheMonitoringDashboardDemo.vue b/docs/.vitepress/theme/components/appendix/cache-design/CacheMonitoringDashboardDemo.vue index 5026a34..2ea1d51 100644 --- a/docs/.vitepress/theme/components/appendix/cache-design/CacheMonitoringDashboardDemo.vue +++ b/docs/.vitepress/theme/components/appendix/cache-design/CacheMonitoringDashboardDemo.vue @@ -1,51 +1,533 @@ - diff --git a/docs/.vitepress/theme/components/appendix/cache-design/CachePatternComparisonDemo.vue b/docs/.vitepress/theme/components/appendix/cache-design/CachePatternComparisonDemo.vue index 6f0c476..c560672 100644 --- a/docs/.vitepress/theme/components/appendix/cache-design/CachePatternComparisonDemo.vue +++ b/docs/.vitepress/theme/components/appendix/cache-design/CachePatternComparisonDemo.vue @@ -1,14 +1,177 @@ - @@ -16,36 +179,229 @@ diff --git a/docs/.vitepress/theme/components/appendix/cloud-iam/AccessKeyManagementDemo.vue b/docs/.vitepress/theme/components/appendix/cloud-iam/AccessKeyManagementDemo.vue index d530a33..dfba066 100644 --- a/docs/.vitepress/theme/components/appendix/cloud-iam/AccessKeyManagementDemo.vue +++ b/docs/.vitepress/theme/components/appendix/cloud-iam/AccessKeyManagementDemo.vue @@ -2,98 +2,93 @@

访问密钥(AK/SK)生命周期管理

-

模拟 AK/SK 的创建、使用和轮换流程

+

模拟 AK/SK 的创建、使用和轮换流程

-
- -
-
- {{ statusText }} - 已创建 {{ akAge }} 天 -
+
+
+ +
+
+ {{ statusText }} + 已创建 {{ akAge }} 天 +
-
-
- Access Key ID: -
- {{ maskedAK }} - +
+
+ Access Key ID: +
+ {{ maskedAK }} + +
+
+ +
+ Secret Access Key: +
+ {{ maskedSK }} + +
-
- Secret Access Key: -
- {{ maskedSK }} - +
+
+ {{ apiCalls }} + API 调用 +
+
+ {{ lastUsed }} + 最后使用
-
-
- {{ apiCalls }} - API 调用 -
-
- {{ lastUsed }} - 最后使用 -
+ +
+ + + + +
- -
- - - - - + +
+
+
+
+ {{ rotationStatus }}
- -
-
-
-
- {{ rotationStatus }} -
- - -
-
🔒 AK/SK 安全管理最佳实践
-
    -
  • - {{ tip.icon }} - {{ tip.text }} -
  • -
+
+ 💡 安全提示:访问密钥泄露是云安全事件的主要原因之一。建议优先使用 IAM 角色替代访问密钥,如果必须使用,请务必定期轮换。
@@ -198,129 +193,127 @@ function deleteKey() { alert('密钥已删除(演示模式)') } } - -// Security Tips -const securityTips = [ - { icon: '🔄', text: '每 90 天轮换一次访问密钥' }, - { icon: '🔒', text: '绝不将 AK/SK 硬编码在代码中' }, - { icon: '👁️', text: '定期审计和监控密钥使用情况' }, - { icon: '🗑️', text: '及时删除不再使用的访问密钥' }, - { icon: '🛡️', text: '优先使用 IAM 角色替代访问密钥' } -] diff --git a/docs/.vitepress/theme/components/appendix/cloud-iam/IdentityProviderDemo.vue b/docs/.vitepress/theme/components/appendix/cloud-iam/IdentityProviderDemo.vue index 28be77c..c39a2c8 100644 --- a/docs/.vitepress/theme/components/appendix/cloud-iam/IdentityProviderDemo.vue +++ b/docs/.vitepress/theme/components/appendix/cloud-iam/IdentityProviderDemo.vue @@ -2,37 +2,43 @@

身份提供商(IdP)集成流程

-

点击步骤查看 SSO 单点登录流程

+

点击步骤查看 SSO 单点登录流程

-
-
-
{{ index + 1 }}
-
- {{ step.title }} - {{ step.desc }} +
+
+
+
{{ index + 1 }}
+
+ {{ step.title }} + {{ step.desc }} +
+
+
+
+ +
+
{{ currentStepData.title }}
+

{{ currentStepData.detail }}

+ +
+
{{ currentStepData.code }}
+
+ +
+
+ {{ row.from.name }} + {{ row.action }} + {{ row.to.name }} +
-
-
-
{{ currentStepData.title }}
-

{{ currentStepData.detail }}

- -
-
{{ currentStepData.code }}
-
- -
-
- {{ row.from.name }} - {{ row.action }} - {{ row.to.name }} -
-
+
+ 💡 SSO 优势:通过企业 IdP 统一管理用户身份,避免在每个云平台单独创建账号,提高安全性和管理效率。
@@ -164,70 +170,86 @@ function goToStep(index) { diff --git a/docs/.vitepress/theme/components/appendix/cloud-iam/MfaSecurityDemo.vue b/docs/.vitepress/theme/components/appendix/cloud-iam/MfaSecurityDemo.vue index ca91878..b1d6cdb 100644 --- a/docs/.vitepress/theme/components/appendix/cloud-iam/MfaSecurityDemo.vue +++ b/docs/.vitepress/theme/components/appendix/cloud-iam/MfaSecurityDemo.vue @@ -2,59 +2,55 @@

MFA 多因素认证模拟

-

体验 MFA 双因素认证流程

+

体验 MFA 双因素认证流程

-
-
-
🔐
-
密码验证
-
-
-
-
📱
-
MFA 验证
-
-
-
-
-
登录成功
-
-
- -
-
请输入密码
- - -
- -
-
MFA 验证
-
- {{ totpCode }} -
-
+
+
+
+
🔐
+
密码验证
+
+
+
+
📱
+
MFA 验证
+
+
+
+
+
登录成功
- - + +
+
请输入密码
+ + +
+ +
+
MFA 验证
+
+ {{ totpCode }} +
+
+
+
+ + +
+ +
+
🎉
+
登录成功!
+

已通过 MFA 双因素认证

+ +
-
-
🎉
-
登录成功!
-

已通过 MFA 双因素认证

- -
- -
-
💡 MFA 安全提示
-
    -
  • 启用 MFA 可降低 99.9% 的账号被盗风险
  • -
  • 推荐使用 TOTP 应用(Google Authenticator、Microsoft Authenticator)
  • -
  • 硬件安全密钥(如 YubiKey)提供最高级别的安全性
  • -
  • 务必备份 MFA 恢复码,防止设备丢失无法登录
  • -
+
+ 💡 MFA 安全价值:启用 MFA 可降低 99.9% 的账号被盗风险。即使密码泄露,攻击者没有你的 MFA 设备也无法登录。
@@ -118,35 +114,41 @@ onUnmounted(() => { diff --git a/docs/.vitepress/theme/components/appendix/cloud-iam/PermissionHierarchyDemo.vue b/docs/.vitepress/theme/components/appendix/cloud-iam/PermissionHierarchyDemo.vue index f18dc28..ab956fc 100644 --- a/docs/.vitepress/theme/components/appendix/cloud-iam/PermissionHierarchyDemo.vue +++ b/docs/.vitepress/theme/components/appendix/cloud-iam/PermissionHierarchyDemo.vue @@ -2,60 +2,66 @@

权限层级结构

-

点击层级查看详细权限范围

+

点击层级查看详细权限范围

-
-
-
{{ level.icon }}
-
- {{ level.name }} - {{ level.scope }} +
+
+
+
{{ level.icon }}
+
+ {{ level.name }} + {{ level.scope }} +
+
+ + {{ perm }} + + + +{{ level.permissions.length - 3 }} + +
-
- - {{ perm }} - - - +{{ level.permissions.length - 3 }} - +
+ +
+
{{ selectedLevelData.name }} 详情
+
+ 权限范围: + {{ selectedLevelData.scope }} +
+
+ 典型场景: + {{ selectedLevelData.scenario }} +
+
+ 拥有权限: +
+ + {{ perm.name }} + +
-
-
{{ selectedLevelData.name }} 详情
-
- 权限范围: - {{ selectedLevelData.scope }} -
-
- 典型场景: - {{ selectedLevelData.scenario }} -
-
- 拥有权限: -
- - {{ perm.name }} - -
-
+
+ 💡 最小权限原则:始终授予用户完成工作所需的最小权限。从低权限开始,根据实际需求逐步提升,而不是一开始就授予高权限。
@@ -141,52 +147,59 @@ function selectLevel(index) { diff --git a/docs/.vitepress/theme/components/appendix/cloud-iam/RolePolicyDemo.vue b/docs/.vitepress/theme/components/appendix/cloud-iam/RolePolicyDemo.vue index 947463c..0c3b02f 100644 --- a/docs/.vitepress/theme/components/appendix/cloud-iam/RolePolicyDemo.vue +++ b/docs/.vitepress/theme/components/appendix/cloud-iam/RolePolicyDemo.vue @@ -2,79 +2,85 @@

角色与策略关系可视化

-

拖动查看角色如何关联多个策略

+

拖动查看角色如何关联多个策略

-
- -
-
-
🎭
-
- {{ roleName }} - {{ roleType }} +
+
+ +
+
+
🎭
+
+ {{ roleName }} + {{ roleType }} +
+
{{ showRoleDetails ? '▼' : '▶' }}
+
+ + +
+
+ 🔐 + 信任策略 (Trust Policy) +
+
+
+ {{ trust.principal }} + 可执行: {{ trust.action }} + 条件: {{ trust.condition }} +
+
-
{{ showRoleDetails ? '▼' : '▶' }}
- -
-
- 🔐 - 信任策略 (Trust Policy) -
-
-
- {{ trust.principal }} - 可执行: {{ trust.action }} - 条件: {{ trust.condition }} + + + + + + +
+
+
+ {{ policy.icon }} + {{ policy.name }} +
+ +
+
+ {{ perm.effect }} + {{ perm.action }} + {{ perm.resource }} +
+
- - - - - - -
-
-
- {{ policy.icon }} - {{ policy.name }} -
- -
-
- {{ perm.effect }} - {{ perm.action }} - {{ perm.resource }} -
-
-
-
+
+ 💡 策略叠加:一个角色可以附加多个策略,最终的权限是所有策略的叠加结果。Deny 策略优先级高于 Allow。
@@ -137,10 +143,6 @@ function selectPolicy(index) { selectedPolicy.value = index } -function selectFeature(platform, index) { - // For compatibility with other demos -} - function getPolicyPosition(index) { const positions = [ { top: '0%', right: '0%' }, @@ -151,7 +153,6 @@ function getPolicyPosition(index) { } function calculateConnections() { - // Simplified connection calculation connectionLines.value = attachedPolicies.value.map((_, index) => ({ x1: 50, y1: 50, @@ -177,30 +178,35 @@ onUnmounted(() => { diff --git a/docs/.vitepress/theme/components/appendix/component-state-management/EventBusDemo.vue b/docs/.vitepress/theme/components/appendix/component-state-management/EventBusDemo.vue index 9a2869c..5a6da30 100644 --- a/docs/.vitepress/theme/components/appendix/component-state-management/EventBusDemo.vue +++ b/docs/.vitepress/theme/components/appendix/component-state-management/EventBusDemo.vue @@ -1,330 +1,191 @@ diff --git a/docs/.vitepress/theme/components/appendix/component-state-management/MobxReactivityDemo.vue b/docs/.vitepress/theme/components/appendix/component-state-management/MobxReactivityDemo.vue index 2a903ba..918b092 100644 --- a/docs/.vitepress/theme/components/appendix/component-state-management/MobxReactivityDemo.vue +++ b/docs/.vitepress/theme/components/appendix/component-state-management/MobxReactivityDemo.vue @@ -1,121 +1,61 @@ @@ -123,7 +63,6 @@ diff --git a/docs/.vitepress/theme/components/appendix/component-state-management/PropsFlowDemo.vue b/docs/.vitepress/theme/components/appendix/component-state-management/PropsFlowDemo.vue index 64f9ce0..e64beb1 100644 --- a/docs/.vitepress/theme/components/appendix/component-state-management/PropsFlowDemo.vue +++ b/docs/.vitepress/theme/components/appendix/component-state-management/PropsFlowDemo.vue @@ -1,163 +1,103 @@ @@ -166,388 +106,214 @@ const emitUpdate = () => { .props-flow-demo { border: 1px solid var(--vp-c-divider); border-radius: 8px; - padding: 20px; background: var(--vp-c-bg-soft); + padding: 1rem; + margin: 1rem 0; + max-height: 600px; + overflow-y: auto; } .demo-header { - margin-bottom: 20px; -} - -.demo-header h4 { - margin: 0 0 8px 0; - color: var(--vp-c-text-1); -} - -.hint { - margin: 0; - font-size: 14px; - color: var(--vp-c-text-2); -} - -.flow-container { - display: grid; - grid-template-columns: 1fr auto 1fr; - gap: 16px; - margin-bottom: 20px; -} - -@media (max-width: 968px) { - .flow-container { - grid-template-columns: 1fr; - } - - .flow-animation { - flex-direction: row !important; - padding: 12px !important; - } - - .flow-line { - width: 100% !important; - height: 2px !important; - } -} - -.parent-component, -.child-component { - background: var(--vp-c-bg); - border: 2px solid var(--vp-c-divider); - border-radius: 8px; - padding: 16px; -} - -.component-header { display: flex; align-items: center; - justify-content: space-between; - margin-bottom: 12px; - padding-bottom: 8px; - border-bottom: 1px solid var(--vp-c-divider); + gap: 0.5rem; + margin-bottom: 0.75rem; } -.tag { - font-family: monospace; - font-size: 13px; - padding: 4px 8px; - background: var(--vp-c-brand-soft); - color: var(--vp-c-brand); - border-radius: 4px; +.demo-header .icon { + font-size: 1.25rem; } -.badge { - font-size: 11px; - padding: 2px 6px; - border-radius: 10px; +.demo-header .title { + font-weight: bold; + font-size: 1rem; } -.badge.blue { - background: #dbeafe; - color: #1e40af; -} - -.badge.green { - background: #dcfce7; - color: #166534; -} - -.data-box, -.props-box { - background: var(--vp-c-bg-soft); - border-radius: 6px; - padding: 12px; - margin-bottom: 12px; - font-family: monospace; - font-size: 13px; -} - -.data-title, -.props-title { - color: var(--vp-c-text-3); - margin-bottom: 4px; -} - -.data-item, -.prop-item { - padding: 4px 8px; - margin: 2px 0; - border-radius: 3px; - transition: all 0.3s ease; -} - -.data-item.changed { - background: #fef3c7; - animation: pulse 0.5s ease; -} - -@keyframes pulse { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.02); } -} - -.data-item .key { - color: var(--vp-c-brand); -} - -.data-item .value { +.demo-header .subtitle { color: var(--vp-c-text-2); + font-size: 0.85rem; + margin-left: 0.5rem; } -.prop-item.receiving { - background: #dcfce7; - animation: receive 0.5s ease; +.intro-text { + font-size: 0.9rem; + color: var(--vp-c-text-2); + line-height: 1.6; + margin-bottom: 1rem; + padding: 0.75rem; + background: var(--vp-c-bg); + border-radius: 6px; } -@keyframes receive { - 0% { transform: translateX(-10px); opacity: 0.5; } - 100% { transform: translateX(0); opacity: 1; } +.intro-text .highlight { + color: var(--vp-c-brand-1); + font-weight: 500; } +.demo-content { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.component-box { + background: var(--vp-c-bg); + border: 2px solid var(--vp-c-divider); + border-radius: 6px; + padding: 0.75rem; +} + +.component-label { + font-weight: 600; + color: var(--vp-c-brand); + margin-bottom: 0.5rem; + padding-bottom: 0.4rem; + border-bottom: 1px solid var(--vp-c-divider); + font-size: 0.85rem; +} + +.data-display, +.props-display { + margin-bottom: 0.5rem; +} + +.data-row, +.prop-item { + display: flex; + gap: 0.5rem; + padding: 0.2rem 0; + font-family: monospace; + font-size: 0.85rem; +} + +.key, .prop-name { color: var(--vp-c-brand); + font-weight: 500; } -.prop-type { - color: var(--vp-c-text-3); - font-size: 11px; - margin-left: 8px; -} - -.props-config { - margin-bottom: 12px; -} - -.config-title { - font-size: 12px; +.value, +.prop-value { color: var(--vp-c-text-2); - margin-bottom: 6px; +} + +.value.light, +.prop-value.light { + background: #fef3c7; + padding: 2px 6px; + border-radius: 3px; +} + +.value.dark, +.prop-value.dark { + background: #374151; + color: #f3f4f6; + padding: 2px 6px; + border-radius: 3px; +} + +.props-output { + display: flex; + gap: 0.5rem; + align-items: center; + font-size: 0.8rem; + color: var(--vp-c-text-2); +} + +.prop-tags { + display: flex; + gap: 0.25rem; } .prop-tag { - display: inline-block; + background: var(--vp-c-brand-soft); + color: var(--vp-c-brand); + padding: 2px 8px; + border-radius: 4px; font-family: monospace; - font-size: 12px; - padding: 4px 8px; - margin: 2px; - background: var(--vp-c-brand-soft); - color: var(--vp-c-brand); - border-radius: 4px; + font-size: 0.8rem; } -.flow-animation { +.flow-arrow { display: flex; flex-direction: column; align-items: center; - justify-content: center; - padding: 20px; -} - -.flow-line { - width: 2px; - height: 80px; - background: var(--vp-c-divider); - position: relative; + gap: 0.25rem; + padding: 0.4rem; transition: all 0.3s ease; } -.flow-line.active { - background: var(--vp-c-brand); - box-shadow: 0 0 10px var(--vp-c-brand); -} - -.flow-particles { - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; -} - -.particle { +.flow-arrow.active { color: var(--vp-c-brand); - font-size: 8px; - animation: flowDown 1s linear infinite; - opacity: 0; } -.particle:nth-child(1) { animation-delay: 0s; } -.particle:nth-child(2) { animation-delay: 0.2s; } -.particle:nth-child(3) { animation-delay: 0.4s; } -.particle:nth-child(4) { animation-delay: 0.6s; } -.particle:nth-child(5) { animation-delay: 0.8s; } - -@keyframes flowDown { - 0% { - opacity: 0; - transform: translateY(0); - } - 20% { - opacity: 1; - } - 80% { - opacity: 1; - } - 100% { - opacity: 0; - transform: translateY(60px); - } -} - -.flow-label { - margin-top: 12px; - font-size: 13px; +.arrow-body { + font-size: 1.3rem; color: var(--vp-c-text-3); - text-align: center; transition: all 0.3s ease; } -.flow-label.active { +.flow-arrow.active .arrow-body { + color: var(--vp-c-brand); + transform: scale(1.2); +} + +.flow-text { + font-size: 0.8rem; + color: var(--vp-c-text-3); +} + +.flow-arrow.active .flow-text { color: var(--vp-c-brand); font-weight: 600; } -.render-preview { - background: var(--vp-c-bg-soft); - border-radius: 6px; - padding: 12px; - margin-bottom: 12px; -} - -.preview-title { - font-size: 12px; - color: var(--vp-c-text-3); - margin-bottom: 8px; -} - -.preview-content { - background: var(--vp-c-bg); - border-radius: 4px; - padding: 12px; -} - -.user-card { - display: flex; - align-items: center; - gap: 12px; -} - -.avatar { - font-size: 32px; - width: 48px; - height: 48px; - display: flex; - align-items: center; - justify-content: center; - background: var(--vp-c-brand-soft); - border-radius: 50%; -} - -.user-info { - flex: 1; -} - -.user-name { - font-weight: 600; - color: var(--vp-c-text-1); - font-size: 14px; -} - -.user-meta { - display: flex; - gap: 8px; - margin-top: 4px; - font-size: 12px; -} - -.age { - color: var(--vp-c-text-2); -} - -.theme-badge { - padding: 2px 6px; - border-radius: 3px; - font-size: 11px; - text-transform: uppercase; -} - -.theme-badge.light { - background: #fef3c7; - color: #92400e; -} - -.theme-badge.dark { - background: #374151; - color: #f3f4f6; -} - -.emit-section { - padding: 12px; - background: var(--vp-c-bg-soft); - border-radius: 6px; -} - -.emit-title { - font-size: 12px; - color: var(--vp-c-text-3); - margin-bottom: 8px; -} - .emit-btn { width: 100%; - padding: 8px 12px; + padding: 0.5rem 1rem; background: var(--vp-c-brand); color: white; border: none; border-radius: 4px; - font-size: 13px; - font-family: monospace; cursor: pointer; + font-size: 0.85rem; transition: all 0.2s ease; } .emit-btn:hover { - background: var(--vp-c-brand-dark); + opacity: 0.9; transform: translateY(-1px); } -.interaction-panel { - margin-top: 20px; - padding: 16px; +.interaction-area { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); - border-radius: 8px; -} - -.panel-title { - font-weight: 600; - color: var(--vp-c-text-1); - margin-bottom: 12px; - font-size: 14px; + border-radius: 6px; + padding: 0.75rem; + margin-bottom: 0.75rem; } .control-group { - margin-bottom: 16px; + display: flex; + flex-direction: column; + gap: 0.5rem; } .control-group label { - display: block; - font-size: 13px; + font-size: 0.85rem; color: var(--vp-c-text-2); - margin-bottom: 8px; + font-weight: 500; } .control-group input, .control-group select { - width: 100%; - padding: 8px 12px; + padding: 0.4rem 0.6rem; border: 1px solid var(--vp-c-divider); border-radius: 4px; - font-size: 14px; background: var(--vp-c-bg); color: var(--vp-c-text-1); - margin-bottom: 8px; + font-size: 0.85rem; } .control-group input:focus, @@ -556,32 +322,15 @@ const emitUpdate = () => { border-color: var(--vp-c-brand); } -.checkbox { - display: inline-flex !important; - align-items: center; - gap: 6px; - margin-right: 16px; - cursor: pointer; -} - -.checkbox input { - width: auto !important; - margin: 0 !important; -} - -.flow-status { - padding: 10px 16px; - background: var(--vp-c-bg-soft); +.info-box { + background: var(--vp-c-bg-alt); + padding: 0.75rem; border-radius: 6px; - text-align: center; - font-size: 14px; + font-size: 0.85rem; color: var(--vp-c-text-2); - transition: all 0.3s ease; } -.flow-status.active { - background: var(--vp-c-brand-soft); - color: var(--vp-c-brand); - font-weight: 600; +.info-box .icon { + margin-right: 0.25rem; } diff --git a/docs/.vitepress/theme/components/appendix/component-state-management/ReduxFlowDemo.vue b/docs/.vitepress/theme/components/appendix/component-state-management/ReduxFlowDemo.vue index 4e7d9fe..73dbed0 100644 --- a/docs/.vitepress/theme/components/appendix/component-state-management/ReduxFlowDemo.vue +++ b/docs/.vitepress/theme/components/appendix/component-state-management/ReduxFlowDemo.vue @@ -1,165 +1,60 @@ @@ -167,53 +62,23 @@ diff --git a/docs/.vitepress/theme/components/appendix/component-state-management/ZustandJotaiDemo.vue b/docs/.vitepress/theme/components/appendix/component-state-management/ZustandJotaiDemo.vue index 026226c..fcb8207 100644 --- a/docs/.vitepress/theme/components/appendix/component-state-management/ZustandJotaiDemo.vue +++ b/docs/.vitepress/theme/components/appendix/component-state-management/ZustandJotaiDemo.vue @@ -1,461 +1,198 @@ diff --git a/docs/.vitepress/theme/components/appendix/database-intro/BPlusTreeDemo.vue b/docs/.vitepress/theme/components/appendix/database-intro/BPlusTreeDemo.vue index 87f0935..2b20601 100644 --- a/docs/.vitepress/theme/components/appendix/database-intro/BPlusTreeDemo.vue +++ b/docs/.vitepress/theme/components/appendix/database-intro/BPlusTreeDemo.vue @@ -1,13 +1,96 @@ @@ -15,36 +98,292 @@ diff --git a/docs/.vitepress/theme/components/appendix/database-intro/DatabaseEvolutionDemo.vue b/docs/.vitepress/theme/components/appendix/database-intro/DatabaseEvolutionDemo.vue index f2ea58e..0b8a99a 100644 --- a/docs/.vitepress/theme/components/appendix/database-intro/DatabaseEvolutionDemo.vue +++ b/docs/.vitepress/theme/components/appendix/database-intro/DatabaseEvolutionDemo.vue @@ -1,50 +1,365 @@ diff --git a/docs/.vitepress/theme/components/appendix/database-intro/DatabaseIndexDemo.vue b/docs/.vitepress/theme/components/appendix/database-intro/DatabaseIndexDemo.vue index 1d8a1ca..7916108 100644 --- a/docs/.vitepress/theme/components/appendix/database-intro/DatabaseIndexDemo.vue +++ b/docs/.vitepress/theme/components/appendix/database-intro/DatabaseIndexDemo.vue @@ -59,33 +59,60 @@ const startSearch = async () => { diff --git a/docs/.vitepress/theme/components/appendix/database-intro/DatabaseRelationDemo.vue b/docs/.vitepress/theme/components/appendix/database-intro/DatabaseRelationDemo.vue index 662e556..4a53a05 100644 --- a/docs/.vitepress/theme/components/appendix/database-intro/DatabaseRelationDemo.vue +++ b/docs/.vitepress/theme/components/appendix/database-intro/DatabaseRelationDemo.vue @@ -1,50 +1,320 @@ diff --git a/docs/.vitepress/theme/components/appendix/database-intro/QueryOptimizationDemo.vue b/docs/.vitepress/theme/components/appendix/database-intro/QueryOptimizationDemo.vue index 877c6f4..975364d 100644 --- a/docs/.vitepress/theme/components/appendix/database-intro/QueryOptimizationDemo.vue +++ b/docs/.vitepress/theme/components/appendix/database-intro/QueryOptimizationDemo.vue @@ -1,13 +1,63 @@ @@ -15,36 +65,301 @@ diff --git a/docs/.vitepress/theme/components/appendix/database-intro/RelationalDataDemo.vue b/docs/.vitepress/theme/components/appendix/database-intro/RelationalDataDemo.vue index 0bcfc0d..a43abeb 100644 --- a/docs/.vitepress/theme/components/appendix/database-intro/RelationalDataDemo.vue +++ b/docs/.vitepress/theme/components/appendix/database-intro/RelationalDataDemo.vue @@ -71,21 +71,31 @@ const setHover = (id) => { diff --git a/docs/.vitepress/theme/components/appendix/database-intro/SqlPlaygroundDemo.vue b/docs/.vitepress/theme/components/appendix/database-intro/SqlPlaygroundDemo.vue index 289c2f3..603f6ff 100644 --- a/docs/.vitepress/theme/components/appendix/database-intro/SqlPlaygroundDemo.vue +++ b/docs/.vitepress/theme/components/appendix/database-intro/SqlPlaygroundDemo.vue @@ -1,302 +1,372 @@ - - + + diff --git a/docs/.vitepress/theme/components/appendix/database-intro/TransactionACIDDemo.vue b/docs/.vitepress/theme/components/appendix/database-intro/TransactionACIDDemo.vue index 7507a90..b4b2850 100644 --- a/docs/.vitepress/theme/components/appendix/database-intro/TransactionACIDDemo.vue +++ b/docs/.vitepress/theme/components/appendix/database-intro/TransactionACIDDemo.vue @@ -1,50 +1,318 @@ diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentBackupDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentBackupDemo.vue new file mode 100644 index 0000000..b400fc9 --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentBackupDemo.vue @@ -0,0 +1,524 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentBuildDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentBuildDemo.vue new file mode 100644 index 0000000..f9aa56e --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentBuildDemo.vue @@ -0,0 +1,519 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentCdnDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentCdnDemo.vue new file mode 100644 index 0000000..01f50a2 --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentCdnDemo.vue @@ -0,0 +1,414 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentChecklistDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentChecklistDemo.vue new file mode 100644 index 0000000..117af6c --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentChecklistDemo.vue @@ -0,0 +1,474 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentCicdDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentCicdDemo.vue new file mode 100644 index 0000000..b31ebb2 --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentCicdDemo.vue @@ -0,0 +1,358 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentDnsDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentDnsDemo.vue new file mode 100644 index 0000000..961b130 --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentDnsDemo.vue @@ -0,0 +1,315 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentEnvironmentDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentEnvironmentDemo.vue new file mode 100644 index 0000000..4c26f25 --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentEnvironmentDemo.vue @@ -0,0 +1,395 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentHttpsDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentHttpsDemo.vue new file mode 100644 index 0000000..45b73bd --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentHttpsDemo.vue @@ -0,0 +1,344 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentLbDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentLbDemo.vue new file mode 100644 index 0000000..783749e --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentLbDemo.vue @@ -0,0 +1,465 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentMonitorDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentMonitorDemo.vue new file mode 100644 index 0000000..2267704 --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentMonitorDemo.vue @@ -0,0 +1,525 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentNginxDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentNginxDemo.vue new file mode 100644 index 0000000..d457843 --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentNginxDemo.vue @@ -0,0 +1,460 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentOverviewDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentOverviewDemo.vue new file mode 100644 index 0000000..d32e8aa --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentOverviewDemo.vue @@ -0,0 +1,300 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentSSHDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentSSHDemo.vue new file mode 100644 index 0000000..d4c99bd --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentSSHDemo.vue @@ -0,0 +1,561 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentServerDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentServerDemo.vue new file mode 100644 index 0000000..ad8562d --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentServerDemo.vue @@ -0,0 +1,426 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/deployment/DeploymentTroubleshootDemo.vue b/docs/.vitepress/theme/components/appendix/deployment/DeploymentTroubleshootDemo.vue new file mode 100644 index 0000000..f8e7272 --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/deployment/DeploymentTroubleshootDemo.vue @@ -0,0 +1,456 @@ + + + + + diff --git a/docs/.vitepress/theme/components/appendix/frontend-evolution/FrontendEvolutionDemo.vue b/docs/.vitepress/theme/components/appendix/frontend-evolution/FrontendEvolutionDemo.vue new file mode 100644 index 0000000..4a199df --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/frontend-evolution/FrontendEvolutionDemo.vue @@ -0,0 +1,484 @@ + + + + + + diff --git a/docs/.vitepress/theme/components/appendix/frontend-evolution/RenderingStrategyDemo.vue b/docs/.vitepress/theme/components/appendix/frontend-evolution/RenderingStrategyDemo.vue new file mode 100644 index 0000000..b9206d1 --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/frontend-evolution/RenderingStrategyDemo.vue @@ -0,0 +1,775 @@ + + + + + + diff --git a/docs/.vitepress/theme/components/appendix/frontend-evolution/RoutingModeDemo.vue b/docs/.vitepress/theme/components/appendix/frontend-evolution/RoutingModeDemo.vue new file mode 100644 index 0000000..b3a0b94 --- /dev/null +++ b/docs/.vitepress/theme/components/appendix/frontend-evolution/RoutingModeDemo.vue @@ -0,0 +1,767 @@ + + + + + + diff --git a/docs/.vitepress/theme/components/appendix/frontend-performance/PerformanceMetricsDemo.vue b/docs/.vitepress/theme/components/appendix/frontend-performance/PerformanceMetricsDemo.vue index 63238c6..896f41a 100644 --- a/docs/.vitepress/theme/components/appendix/frontend-performance/PerformanceMetricsDemo.vue +++ b/docs/.vitepress/theme/components/appendix/frontend-performance/PerformanceMetricsDemo.vue @@ -4,9 +4,10 @@ --> @@ -116,7 +95,6 @@ import { ref, computed } from 'vue' const loadTime = ref(2.5) -const isLoading = ref(false) const fcp = computed(() => (loadTime.value * 0.3).toFixed(1)) const lcp = computed(() => (loadTime.value * 0.7).toFixed(1)) @@ -152,249 +130,175 @@ const clsStatus = computed(() => { if (value <= 0.25) return { class: 'needs-improvement', text: '需改进' } return { class: 'poor', text: '差' } }) - -function startLoading() { - isLoading.value = true - setTimeout(() => { - isLoading.value = false - }, loadTime.value * 1000) -} diff --git a/docs/.vitepress/theme/components/appendix/frontend-performance/ReflowRepaintDemo.vue b/docs/.vitepress/theme/components/appendix/frontend-performance/ReflowRepaintDemo.vue index ab6b3c9..4bede70 100644 --- a/docs/.vitepress/theme/components/appendix/frontend-performance/ReflowRepaintDemo.vue +++ b/docs/.vitepress/theme/components/appendix/frontend-performance/ReflowRepaintDemo.vue @@ -4,21 +4,33 @@ --> @@ -114,46 +121,18 @@ @@ -276,54 +226,78 @@ function changeOpacity() { diff --git a/docs/.vitepress/theme/components/appendix/frontend-performance/VirtualScrollingDemo.vue b/docs/.vitepress/theme/components/appendix/frontend-performance/VirtualScrollingDemo.vue index 843dbd5..c57765d 100644 --- a/docs/.vitepress/theme/components/appendix/frontend-performance/VirtualScrollingDemo.vue +++ b/docs/.vitepress/theme/components/appendix/frontend-performance/VirtualScrollingDemo.vue @@ -1,14 +1,18 @@ + diff --git a/docs/.vitepress/theme/components/appendix/frontend-routing/MpaRoutingDemo.vue b/docs/.vitepress/theme/components/appendix/frontend-routing/MpaRoutingDemo.vue index 3fbd237..d7eb498 100644 --- a/docs/.vitepress/theme/components/appendix/frontend-routing/MpaRoutingDemo.vue +++ b/docs/.vitepress/theme/components/appendix/frontend-routing/MpaRoutingDemo.vue @@ -1,26 +1,21 @@ @@ -50,127 +82,147 @@ const comparisonData = [ { feature: 'URL 变化', mpa: '浏览器地址栏正常变化', spa: 'History API 控制 URL' }, { feature: '用户体验', mpa: '页面有白屏闪烁', spa: '过渡流畅无刷新' }, { feature: 'SEO 友好', mpa: '天生对搜索引擎友好', spa: '需要 SSR/预渲染优化' }, - { feature: '首屏时间', mpa: '较快(只加载当前页)', spa: '较慢(需加载完整应用)' }, - { feature: '服务端压力', mpa: '较高(每次请求都渲染)', spa: '较低(大部分逻辑在客户端)' }, - { feature: '开发复杂度', mpa: '简单,传统开发模式', spa: '较复杂,需理解前端路由' } + { feature: '首屏时间', mpa: '较快(只加载当前页)', spa: '较慢(需加载完整应用)' } ] - \ No newline at end of file diff --git a/docs/.vitepress/theme/components/appendix/frontend-routing/NestedRoutesDemo.vue b/docs/.vitepress/theme/components/appendix/frontend-routing/NestedRoutesDemo.vue index 82da03c..536cd8e 100644 --- a/docs/.vitepress/theme/components/appendix/frontend-routing/NestedRoutesDemo.vue +++ b/docs/.vitepress/theme/components/appendix/frontend-routing/NestedRoutesDemo.vue @@ -1,11 +1,16 @@ @@ -112,45 +114,6 @@ const routeConfig = [ path: ':id', name: 'UserDetail', component: 'UserDetail' - }, - { - path: ':id/edit', - name: 'UserEdit', - component: 'UserEdit' - } - ] - }, - { - path: 'products', - name: 'Products', - component: 'ProductLayout', - children: [ - { - path: '', - name: 'ProductList', - component: 'ProductList' - }, - { - path: 'category/:categoryId', - name: 'ProductCategory', - component: 'ProductCategory' - } - ] - }, - { - path: 'settings', - name: 'Settings', - component: 'Settings', - children: [ - { - path: 'profile', - name: 'ProfileSettings', - component: 'ProfileSettings' - }, - { - path: 'security', - name: 'SecuritySettings', - component: 'SecuritySettings' } ] } @@ -158,36 +121,29 @@ const routeConfig = [ } ] -// 扁平化路由,添加层级信息 const flattenRoutes = (routes, level = 0, parentPath = '') => { const result = [] - routes.forEach(route => { const fullPath = route.path ? `${parentPath}/${route.path}`.replace(/\/+/g, '/') : parentPath || '/' - const node = { ...route, fullPath, level, children: [] } - if (route.children?.length) { node.children = flattenRoutes(route.children, level + 1, fullPath) } - result.push(node) }) - return result } const treeData = computed(() => { const flatten = (routes, level = 0) => { const result = [] - routes.forEach(route => { const node = { name: route.name, @@ -199,10 +155,8 @@ const treeData = computed(() => { } result.push(node) }) - return result } - return flatten(flattenRoutes(routeConfig)) }) @@ -210,11 +164,9 @@ const activeRouteChain = computed(() => { const findChain = (routes, target, chain = []) => { for (const route of routes) { const currentChain = [...chain, route] - if (route.path === target || route.fullPath === target) { return currentChain } - if (route.children?.length) { const found = findChain(route.children, target, currentChain) if (found) return found @@ -222,7 +174,6 @@ const activeRouteChain = computed(() => { } return null } - return findChain(flattenRoutes(routeConfig), currentPath.value) || [] }) @@ -233,33 +184,6 @@ const breadcrumbs = computed(() => { })) }) -const routeConfigCode = computed(() => `const routes = [ - { - path: '/', - component: Layout, - children: [ - { path: 'dashboard', component: Dashboard }, - { - path: 'users', - component: UserLayout, - children: [ - { path: '', component: UserList }, - { path: ':id', component: UserDetail }, - { path: ':id/edit', component: UserEdit } - ] - }, - { - path: 'settings', - component: Settings, - children: [ - { path: 'profile', component: ProfileSettings }, - { path: 'security', component: SecuritySettings } - ] - } - ] - } -]`) - const selectNode = (node) => { currentPath.value = node.fullPath || node.path } @@ -271,44 +195,57 @@ const navigateTo = (path) => { diff --git a/docs/.vitepress/theme/components/appendix/frontend-routing/RouteMatchingDemo.vue b/docs/.vitepress/theme/components/appendix/frontend-routing/RouteMatchingDemo.vue index a1da6b5..a68056f 100644 --- a/docs/.vitepress/theme/components/appendix/frontend-routing/RouteMatchingDemo.vue +++ b/docs/.vitepress/theme/components/appendix/frontend-routing/RouteMatchingDemo.vue @@ -1,126 +1,74 @@ @@ -128,7 +76,7 @@ diff --git a/docs/.vitepress/theme/components/appendix/frontend-routing/RoutingModesDemo.vue b/docs/.vitepress/theme/components/appendix/frontend-routing/RoutingModesDemo.vue index 3af6e40..8ba38d2 100644 --- a/docs/.vitepress/theme/components/appendix/frontend-routing/RoutingModesDemo.vue +++ b/docs/.vitepress/theme/components/appendix/frontend-routing/RoutingModesDemo.vue @@ -1,8 +1,13 @@ \ No newline at end of file + diff --git a/docs/.vitepress/theme/components/appendix/frontend-routing/SpaNavigationDemo.vue b/docs/.vitepress/theme/components/appendix/frontend-routing/SpaNavigationDemo.vue index 8105bf7..6af19cc 100644 --- a/docs/.vitepress/theme/components/appendix/frontend-routing/SpaNavigationDemo.vue +++ b/docs/.vitepress/theme/components/appendix/frontend-routing/SpaNavigationDemo.vue @@ -1,12 +1,21 @@ diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js index 02a2dd2..f9c4f97 100644 --- a/docs/.vitepress/theme/index.js +++ b/docs/.vitepress/theme/index.js @@ -110,6 +110,21 @@ import CdnCacheDemo from './components/appendix/deployment/CdnCacheDemo.vue' import CicdPipelineDemo from './components/appendix/deployment/CicdPipelineDemo.vue' import RollbackSwitchDemo from './components/appendix/deployment/RollbackSwitchDemo.vue' import ObservabilityBackupDemo from './components/appendix/deployment/ObservabilityBackupDemo.vue' +import DeploymentOverviewDemo from './components/appendix/deployment/DeploymentOverviewDemo.vue' +import DeploymentBuildDemo from './components/appendix/deployment/DeploymentBuildDemo.vue' +import DeploymentServerDemo from './components/appendix/deployment/DeploymentServerDemo.vue' +import DeploymentSSHDemo from './components/appendix/deployment/DeploymentSSHDemo.vue' +import DeploymentEnvironmentDemo from './components/appendix/deployment/DeploymentEnvironmentDemo.vue' +import DeploymentNginxDemo from './components/appendix/deployment/DeploymentNginxDemo.vue' +import DeploymentLbDemo from './components/appendix/deployment/DeploymentLbDemo.vue' +import DeploymentMonitorDemo from './components/appendix/deployment/DeploymentMonitorDemo.vue' +import DeploymentBackupDemo from './components/appendix/deployment/DeploymentBackupDemo.vue' +import DeploymentTroubleshootDemo from './components/appendix/deployment/DeploymentTroubleshootDemo.vue' +import DeploymentChecklistDemo from './components/appendix/deployment/DeploymentChecklistDemo.vue' +import DeploymentDnsDemo from './components/appendix/deployment/DeploymentDnsDemo.vue' +import DeploymentHttpsDemo from './components/appendix/deployment/DeploymentHttpsDemo.vue' +import DeploymentCdnDemo from './components/appendix/deployment/DeploymentCdnDemo.vue' +import DeploymentCicdDemo from './components/appendix/deployment/DeploymentCicdDemo.vue' import CssBoxModel from './components/appendix/web-basics/CssBoxModel.vue' import CssFlexbox from './components/appendix/web-basics/CssFlexbox.vue' import CssLayoutDemo from './components/appendix/web-basics/CssLayoutDemo.vue' @@ -148,9 +163,13 @@ import ImperativeVsDeclarativeDemo from './components/appendix/web-basics/Impera import ComponentReusabilityDemo from './components/appendix/web-basics/ComponentReusabilityDemo.vue' // Frontend Evolution Components +import FrontendEvolutionTimelineDemo from './components/appendix/frontend-evolution/FrontendEvolutionDemo.vue' import EvolutionSliceRequestDemo from './components/appendix/frontend-evolution/SliceRequestDemo.vue' import EvolutionResponsiveGridDemo from './components/appendix/frontend-evolution/ResponsiveGridDemo.vue' import EvolutionJQueryVsStateDemo from './components/appendix/frontend-evolution/JQueryVsStateDemo.vue' +import EvolutionRoutingModeDemo from './components/appendix/frontend-evolution/RoutingModeDemo.vue' +import EvolutionRenderingStrategyDemo from './components/appendix/frontend-evolution/RenderingStrategyDemo.vue' +import EvolutionImperativeVsDeclarativeDemo from './components/appendix/frontend-evolution/ImperativeVsDeclarativeDemo.vue' import BackendEvolutionDemo from './components/appendix/backend-evolution/BackendEvolutionDemo.vue' import BackendQuickStartDemo from './components/appendix/backend-evolution/BackendQuickStartDemo.vue' @@ -262,6 +281,17 @@ import DependencyGraphDemo from './components/appendix/frontend-engineering/Depe import SourceMapDemo from './components/appendix/frontend-engineering/SourceMapDemo.vue' import AssetFingerprintDemo from './components/appendix/frontend-engineering/AssetFingerprintDemo.vue' +// Frontend Routing Components +import HashVsHistoryDemo from './components/appendix/frontend-routing/HashVsHistoryDemo.vue' +import DynamicRoutesDemo from './components/appendix/frontend-routing/DynamicRoutesDemo.vue' +import MpaRoutingDemo from './components/appendix/frontend-routing/MpaRoutingDemo.vue' +import NestedRoutesDemo from './components/appendix/frontend-routing/NestedRoutesDemo.vue' +import RouteGuardsDemo from './components/appendix/frontend-routing/RouteGuardsDemo.vue' +import RouteMatchingDemo from './components/appendix/frontend-routing/RouteMatchingDemo.vue' +import RouterArchitectureDemo from './components/appendix/frontend-routing/RouterArchitectureDemo.vue' +import RoutingModesDemo from './components/appendix/frontend-routing/RoutingModesDemo.vue' +import SpaNavigationDemo from './components/appendix/frontend-routing/SpaNavigationDemo.vue' + // Agent Intro Components import AgentWorkflowDemo from './components/appendix/agent-intro/AgentWorkflowDemo.vue' import AgentLevelDemo from './components/appendix/agent-intro/AgentLevelDemo.vue' @@ -431,6 +461,7 @@ import PaintLayerDemo from './components/appendix/browser-rendering-pipeline/Pai import CompositeDemo from './components/appendix/browser-rendering-pipeline/CompositeDemo.vue' import MacroMicroTaskDemo from './components/appendix/browser-rendering-pipeline/MacroMicroTaskDemo.vue' import RenderingPerformanceDemo from './components/appendix/browser-rendering-pipeline/RenderingPerformanceDemo.vue' +import RenderingPipelineDemo from './components/appendix/browser-rendering-pipeline/RenderingPipelineDemo.vue' // Cache Design Extra Components import CacheArchitectureOverview from './components/appendix/cache-design/CacheArchitectureOverview.vue' @@ -560,6 +591,21 @@ export default { app.component('CicdPipelineDemo', CicdPipelineDemo) app.component('RollbackSwitchDemo', RollbackSwitchDemo) app.component('ObservabilityBackupDemo', ObservabilityBackupDemo) + app.component('DeploymentOverviewDemo', DeploymentOverviewDemo) + app.component('DeploymentBuildDemo', DeploymentBuildDemo) + app.component('DeploymentServerDemo', DeploymentServerDemo) + app.component('DeploymentSSHDemo', DeploymentSSHDemo) + app.component('DeploymentEnvironmentDemo', DeploymentEnvironmentDemo) + app.component('DeploymentNginxDemo', DeploymentNginxDemo) + app.component('DeploymentLbDemo', DeploymentLbDemo) + app.component('DeploymentMonitorDemo', DeploymentMonitorDemo) + app.component('DeploymentBackupDemo', DeploymentBackupDemo) + app.component('DeploymentTroubleshootDemo', DeploymentTroubleshootDemo) + app.component('DeploymentChecklistDemo', DeploymentChecklistDemo) + app.component('DeploymentDnsDemo', DeploymentDnsDemo) + app.component('DeploymentHttpsDemo', DeploymentHttpsDemo) + app.component('DeploymentCdnDemo', DeploymentCdnDemo) + app.component('DeploymentCicdDemo', DeploymentCicdDemo) app.component('CssBoxModel', CssBoxModel) app.component('CssFlexbox', CssFlexbox) app.component('CssLayoutDemo', CssLayoutDemo) @@ -604,9 +650,13 @@ export default { app.component('ComponentReusabilityDemo', ComponentReusabilityDemo) // Frontend Evolution Components Registration + app.component('FrontendEvolutionDemo', FrontendEvolutionTimelineDemo) app.component('EvolutionSliceRequestDemo', EvolutionSliceRequestDemo) app.component('EvolutionResponsiveGridDemo', EvolutionResponsiveGridDemo) app.component('EvolutionJQueryVsStateDemo', EvolutionJQueryVsStateDemo) + app.component('RoutingModeDemo', EvolutionRoutingModeDemo) + app.component('RenderingStrategyDemo', EvolutionRenderingStrategyDemo) + app.component('ImperativeVsDeclarativeDemo', EvolutionImperativeVsDeclarativeDemo) app.component('BackendEvolutionDemo', BackendEvolutionDemo) app.component('BackendQuickStartDemo', BackendQuickStartDemo) @@ -713,6 +763,17 @@ export default { app.component('SourceMapDemo', SourceMapDemo) app.component('AssetFingerprintDemo', AssetFingerprintDemo) + // Frontend Routing Components Registration + app.component('HashVsHistoryDemo', HashVsHistoryDemo) + app.component('DynamicRoutesDemo', DynamicRoutesDemo) + app.component('MpaRoutingDemo', MpaRoutingDemo) + app.component('NestedRoutesDemo', NestedRoutesDemo) + app.component('RouteGuardsDemo', RouteGuardsDemo) + app.component('RouteMatchingDemo', RouteMatchingDemo) + app.component('RouterArchitectureDemo', RouterArchitectureDemo) + app.component('RoutingModesDemo', RoutingModesDemo) + app.component('SpaNavigationDemo', SpaNavigationDemo) + // Agent Intro Components Registration app.component('AgentWorkflowDemo', AgentWorkflowDemo) app.component('AgentLevelDemo', AgentLevelDemo) @@ -881,6 +942,7 @@ export default { app.component('CompositeDemo', CompositeDemo) app.component('MacroMicroTaskDemo', MacroMicroTaskDemo) app.component('RenderingPerformanceDemo', RenderingPerformanceDemo) + app.component('RenderingPipelineDemo', RenderingPipelineDemo) // Cache Design Extra Components Registration app.component('CacheArchitectureOverview', CacheArchitectureOverview) diff --git a/docs/zh-cn/appendix/backend-languages.md b/docs/zh-cn/appendix/backend-languages.md index fcae33e..2a3e520 100644 --- a/docs/zh-cn/appendix/backend-languages.md +++ b/docs/zh-cn/appendix/backend-languages.md @@ -1,32 +1,244 @@ # 后端编程语言选型指南:从问题出发做决策 -> 💡 **学习指南**:本章节无需编程基础,通过交互式演示带你全面了解主流后端编程语言的特点、应用场景和选择策略。我们将深入对比 Java、Python、Go、Node.js 等语言的优劣势。 +::: tip 🎯 核心问题 +**"我们后端该用什么语言?"** 这就像问:"我应该买什么工具?" 答案永远不是"最好的",而是"最适合你的"。本章将带你全面了解主流后端编程语言的特点、应用场景和选择策略,帮助你做出明智的决策。 +::: --- -## 0. 引言:为什么选语言这么难? +## 1. 为什么要了解后端语言? -想象一下这个场景: +### 1.1 从单一到多元:后端语言的演变 -> 你刚加入一家初创公司做 CTO,技术合伙人问你:"咱们后端用什么语言?" -> -> 你脑海中闪过无数个选择:Java 稳如老狗、Python 火遍 AI、Go 代表未来、Node.js 全栈爽歪歪... -> -> 最后你憋出一句:"先写 Python,不行再重构?" +在互联网早期,后端开发的选择非常有限。那时候大多用 Perl 或 CGI 脚本,一个网站的后端代码可能就几百行,部署方式简单直接——把文件上传到服务器的 CGI-BIN 目录就行。那是一个"一招鲜吃遍天"的时代, Perl、PHP、Java 几乎垄断了整个市场。 -**为什么后端编程语言的选择如此困难?** +但现代后端开发完全变了样。我们现在面临的选择有 Java、Python、Go、Node.js、Rust、PHP、C++ 等,每种语言都有其特定的适用场景和优势。云计算、微服务、AI/ML 等新技术的出现,让后端开发的边界不断扩展,语言选择也变得越来越多元化。 -因为**不同的时代有不同的需求**,不同的场景有不同的最优解。选语言不是选"最好的",而是选"最合适的"——就像选数据库、选架构一样,**没有银弹**。 +**这种多元化不是坏事,而是技术进步的必然结果。** 不同的场景有不同的需求,就像不同的工作需要不同的工具。你不会用瑞士军刀砍柴,也不会用斧子做精细雕刻。同样,后端语言的选择也必须基于具体场景。 + +
+
+ +**👴 二十年前** +- Perl/CGI 或 PHP 统治世界 +- 一个文件包含所有逻辑 +- 部署方式简单粗暴 +- 语言选择几乎不是问题 + +
+
+ +**🚀 现代开发** +- Java、Python、Go、Node.js、Rust 等多语言并存 +- 微服务架构,不同服务可用不同语言 +- 云原生部署,容器化成为标准 +- 语言选型直接影响开发效率和系统性能 + +
+
+### 1.2 一个真实的踩坑故事:为什么选对语言这么重要 + +你可能会说:"用 Python 什么都能写,为什么还要纠结?" 让我讲一个真实的故事,你就会明白为什么语言选型如此重要。 + +::: warning 老王的语言选型踩坑记 + +老王创业做了一个在线视频处理平台,后端用 Python Django 搭建。初期发展很快,用户量不多,系统运行良好。 + +但随着用户量增长,问题出现了:视频转码是 CPU 密集型任务,Python 的 GIL(全局解释器锁)导致多线程性能很差,一次只能转一个视频,用户排队等待时间越来越长。 + +老王试图用多进程解决,但每个进程占用内存几百 MB,服务器成本暴涨。最后他不得不痛下决心,用 Go 重写了整个转码服务。 + +结果呢?同样的服务器,Go 版本的并发处理能力是 Python 的 10 倍,用户等待时间从 30 分钟降到 3 分钟。但重写花了 3 个月时间,错过了业务黄金期。 + +**老王从此明白了一个道理:选错语言不致命,但会付出巨大代价。** + +::: + +::: info 💡 核心启示 +**没有最好的语言,只有最适合的语言。** Python 擅长快速开发和 AI/ML,但不是高性能计算的最优解;Go 性能强大且开发效率高,但 AI/ML 生态不如 Python。了解每种语言的优劣势,才能在选型时做出明智决策。 + +**关键不是学习所有语言,而是理解它们的设计哲学和适用场景,在需要时能快速选择合适的工具。** +::: + --- -## 1. 主流后端语言详解 +## 2. 核心概念:理解后端语言的基本特征 -在深入对比之前,让我们先逐一了解每种主流后端语言的特点、优势和典型应用场景。 +::: tip 🤔 这些概念和语言有什么关系? -### 1.1 Java:企业级应用的常青树 +就像买车时要看马力、油耗、载重量一样,选择后端语言时也要理解几个核心维度: + +1. **编译/解释**:影响启动速度和运行性能 +2. **类型系统**:影响开发效率和代码可靠性 +3. **并发模型**:影响系统能同时处理多少请求 +4. **内存管理**:影响性能和开发体验 + +理解这些概念,你就能看穿语言表象,抓住本质差异。 +::: + +在深入对比各种语言之前,我们需要先建立一些基础概念。这些概念就像语言的"DNA",决定了它们的特点和适用场景。 + +### 2.1 用工具比喻理解语言特征 + +想象你在装修房子,不同的装修工具就像不同的后端语言: + +| 概念 | 🔧 工具比喻 | 实际作用 | 具体例子 | +|------|-----------|----------|----------| +| **编译型语言** | 电动工具,插电即用,力量大但准备时间长 | 代码先编译成机器码再运行,启动慢但性能高 | Go、Rust、C++ | +| **解释型语言** | 手动工具,拿起来就能用,但效率相对低 | 代码边解释边运行,开发快但性能相对低 | Python、PHP、Ruby | +| **静态类型** | 严格按图纸施工,不容易出错但灵活性差 | 变量类型在编译时确定,错误提前发现 | Java、Go、Rust | +| **动态类型** | 自由发挥,灵活但容易出错 | 变量类型在运行时确定,开发快但风险高 | Python、JavaScript、PHP | +| **并发模型** | 同时干多少活的能力 | 决定了系统能同时处理多少请求 | 见下方详细解释 | + +### 2.2 编译 vs 解释:启动速度与运行性能的权衡 + +**编译型语言**(如 Go、Rust、C++)在运行前需要先编译成机器码,这个过程就像准备电动工具——插电、检查、调试,需要时间。但一旦准备好,使用时效率极高。 + +**解释型语言**(如 Python、PHP)不需要编译,直接运行。这就像手动工具,拿起来就能用,开发效率高。但运行时需要逐行解释,性能相对较低。 + +::: details 🔍 看看编译过程做了什么 + +**Go 代码(编译型):** +```go +// 源代码 main.go +package main +import "fmt" +func main() { + fmt.Println("Hello") +} +``` + +``` +编译过程: +go build main.go + ↓ +[编译器检查语法、类型检查、优化代码] + ↓ +生成可执行文件 main(机器码) + ↓ +./main ← 直接运行,速度极快 +``` + +**Python 代码(解释型):** +```python +# 源代码 main.py +print("Hello") +``` + +``` +运行过程: +python main.py + ↓ +[解释器逐行读取、解析、执行] + ↓ +每运行一次都要重新解析 +``` + +::: + +::: tip 💡 实际影响是什么? + +**编译型语言**:启动慢(需要先编译),但运行快。 +- 适合:长期运行的服务(API 服务器、微服务) +- 不适合:频繁重启的场景(如 Serverless 函数) + +**解释型语言**:启动快(直接运行),但运行相对慢。 +- 适合:快速开发、脚本、数据分析 +- 不适合:高性能计算、大规模并发服务 + +现代技术的发展让这个界限变得模糊:Java 既是编译型(编译成字节码),又是解释型(JVM 执行);JIT(即时编译)技术让 JavaScript 在浏览器中也能达到接近编译型语言的性能;Python 可以通过 C 扩展获得高性能。 + +::: + +### 2.3 并发模型:同时处理多少请求? + +并发是后端开发中最关键的概念之一,它决定了系统同时能处理多少请求。不同语言的并发模型差异巨大,这往往是选型的决定性因素。 + +::: tip 🤔 什么是并发? + +先区分两个容易混淆的概念: + +- **并发(Concurrency)**:同时处理多个任务的能力(看似同时) +- **并行(Parallelism)**:同时执行多个任务(真正同时) + +打个比方: +- **并发**:一个人同时应付三个客户的咨询(快速切换注意力) +- **并行**:三个人分别应付三个客户(真的同时进行) + +在单核 CPU 上,只能做到并发;在多核 CPU 上,才能做到并行。 +::: + +**主流语言的并发模型对比:** + +| 语言 | 并发模型 | 机制说明 | 资源消耗 | 适用场景 | +| :--- | :--- | :--- | :--- | :--- | +| **Java** | 操作系统线程 | 每个请求一个线程 | 1-2 MB/线程 | 传统企业应用 | +| **Go** | Goroutine 协程 | 用户态轻量级线程 | ~2 KB/协程 | 高并发、云原生 | +| **Node.js** | 事件循环 | 单线程 + 异步 I/O | 单线程 | I/O 密集型应用 | +| **Python** | 多进程 | 绕过 GIL 限制 | 进程级隔离 | 数据处理、脚本 | + +::: tip 📊 从表格中你能看到什么? + +**Java 的多线程**:每个线程占用 1-2 MB 内存,启动 1 万个线程就需要 10-20 GB 内存,成本很高。但 Java 的线程模型成熟稳定,适合传统企业应用。 + +**Go 的 Goroutine**:协程只占用 2 KB 内存,启动 100 万个协程只需要 2 GB 内存,成本极低。这就是为什么 Go 在云原生和微服务领域如此受欢迎。 + +**Node.js 的事件循环**:单线程模型意味着在处理大量并发 I/O 请求时效率很高(如实时聊天),但 CPU 密集型任务会阻塞整个事件循环,导致性能崩溃。 + +**Python 的多进程**:由于 GIL(全局解释器锁)的存在,Python 的多线程无法真正并行,只能用多进程。每个进程独立运行,内存隔离,但进程间通信开销大。 + +::: + +### 2.4 内存管理:谁来负责回收垃圾? + +内存管理是影响性能和开发体验的关键因素。不同语言采用了不同的策略,各有优劣。 + +| 语言 | 内存管理方式 | 实现机制 | 性能影响 | 开发体验 | +| :--- | :--- | :--- | :--- | :--- | +| **Java** | GC(垃圾回收) | 分代收集、并发标记 | 中等(有 STW 停顿) | 自动,无需关心 | +| **Python** | GC + 引用计数 | 自动回收 + 循环检测 | 较差(GIL 影响) | 自动,偶有泄漏 | +| **Go** | GC | 低延迟并发回收 | 良好 | 自动,性能优秀 | +| **Node.js** | GC(V8) | 分代回收 | 良好 | 自动,优化好 | +| **Rust** | 所有权系统 | 编译时检查,无 GC | 极佳 | 手动,学习陡峭 | +| **C++** | 手动管理 | new/delete 或智能指针 | 极佳(但风险高) | 完全手动,易出错 | + +::: tip 💡 什么是 GC(垃圾回收)? + +**GC = Garbage Collection,自动内存管理** + +想象你在打扫房间: +- **手动管理**(C++):自己记住哪里有垃圾,什么时候扔。效率高,但容易忘,导致内存泄漏。 +- **自动回收**(Java、Python、Go):有个保洁阿姨自动帮你清理,你只管用。省心,但阿姨工作时你可能需要等待(STW 停顿)。 +- **所有权系统**(Rust):用完立刻自动清理,不需要保洁阿姨。编译器保证不会出错,但学习成本高。 + +::: + +**什么是 STW(Stop-The-World)?** + +GC 在回收垃圾时,需要暂停应用线程,这个暂停就叫 STW。对于大多数应用,几十毫秒的停顿无感知;但对于高频交易系统,1 毫秒的停顿都可能造成损失。 + +--- + +## 3. 主流后端语言详解 + +现在我们已经掌握了基础概念,让我们逐一了解每种主流后端语言的特点、优势和典型应用场景。 + +### 3.1 Java:企业级应用的常青树 + +::: tip 🤔 什么是"企业级应用"? + +**企业级应用**指大型、复杂、对可靠性要求极高的系统,如: +- 银行核心系统(转账、记账) +- 电商平台(订单、库存、支付) +- ERP/CRM 系统(企业管理、客户关系) + +这类系统的特点:业务逻辑复杂、数据一致性要求高、不能挂、需要长期维护。 + +Java 在这个领域占据统治地位,就像瑞士军刀一样可靠。 +::: **历史与定位** @@ -34,41 +246,54 @@ Java 诞生于 1995 年,由 Sun 公司(后被 Oracle 收购)推出。它 **核心特点** -| 特性 | 说明 | -|------|------| -| **强类型静态语言** | 编译时就能发现大部分类型错误,代码更健壮 | -| **丰富的生态** | Spring、Spring Boot 等框架成熟完善 | -| **强大的工具链** | IntelliJ IDEA、Maven、Gradle 等开发工具成熟 | -| **多线程支持** | 内置并发库,适合高并发场景 | +| 特性 | 说明 | 为什么重要 | +|------|------|-----------| +| **强类型静态语言** | 编译时就能发现类型错误 | 减少运行时 bug,代码更健壮 | +| **丰富的生态** | Spring、Spring Boot 等框架成熟 | 不需要重复造轮子,开发效率高 | +| **强大的工具链** | IntelliJ IDEA、Maven、Gradle | 开发体验好,团队协作顺畅 | +| **多线程支持** | 内置并发库,成熟稳定 | 适合处理复杂并发场景 | **代码示例** +::: details 查看一个真实的 API 例子 ```java -// Java: 一个简单的 REST API 控制器 -import org.springframework.web.bind.annotation.*; - +// Java Spring Boot:用户注册 API @RestController -@RequestMapping("/users") +@RequestMapping("/api/users") public class UserController { - @GetMapping("/{id}") - public User getUser(@PathVariable Long id) { - return userService.findById(id); - } + @Autowired + private UserService userService; - @PostMapping - public User createUser(@RequestBody User user) { - return userService.save(user); + // 注册接口:POST /api/users/register + @PostMapping("/register") + public ResponseEntity register(@RequestBody RegisterRequest request) { + // 1. 参数校验(编译时就能发现类型错误) + if (request.getUsername() == null || request.getUsername().length() < 3) { + return ResponseEntity.badRequest().build(); + } + + // 2. 调用业务逻辑 + User user = userService.register(request); + + // 3. 返回结果 + return ResponseEntity.ok(user); } } ``` +**这段代码展示了 Java 的特点**: +- `@RestController` 等注解让代码结构清晰 +- 强类型系统让参数校验在编译时就进行 +- Spring 框架处理了大部分底层细节 +::: + **适用场景** -- 大型企业级应用(ERP、CRM、OA 系统) -- 金融系统(银行核心系统、支付平台) -- 电商平台后端 +- 大型企业级应用(银行、保险、电信) +- 电商平台后端(淘宝、京东的核心系统) - 大数据处理(Hadoop、Spark 生态) +- Android 开发(虽然 Google 推崇 Kotlin,但 Java 仍占很大比例) **优缺点分析** @@ -79,62 +304,141 @@ public class UserController { | 人才储备充足,招聘容易 | 学习曲线较陡峭 | | 工具链完善,开发体验好 | 版本更新快,需要持续学习 | -### 1.2 Python:快速开发与 AI 时代的宠儿 +**真实案例:阿里巴巴为什么选择 Java?** + +阿里巴巴的双11秒杀系统,峰值 QPS(每秒请求数)高达几十万,为什么用 Java 而不是性能更强的 Go? + +1. **团队背景**:阿里工程师大多熟悉 Java +2. **生态成熟**:中间件(Dubbo、RocketMQ)都是 Java 生态 +3. **可靠性**:Java 的类型系统和异常处理机制让大规模系统更稳定 +4. **性能足够**:经过 JVM 优化,Java 性能已经足够,不是瓶颈 + +**关键启示**:性能不是唯一标准,团队熟悉度和生态成熟度往往更重要。 + +--- + +### 3.2 Python:快速开发与 AI 时代的宠儿 + +::: tip 🤔 为什么 Python 在 AI 领域统治? + +**历史偶然 + 生态必然**: + +1. NumPy(2006)先奠定了数值计算基础 +2. 研究人员喜欢用 Python 写论文代码(简洁易读) +3. 深度学习框架(TensorFlow、PyTorch)都选择 Python 作为接口 +4. 形成了正向循环:用的人多 → 库越多 → 用的人更多 + +现在 AI/ML 领域几乎全是 Python,就像英语是科学界的通用语言。 +::: **历史与定位** -Python 由 Guido van Rossum 于 1991 年创建,设计哲学强调代码的可读性和简洁性。Python 的格言是"There should be one-- and preferably only one --obvious way to do it"。 +Python 由 Guido van Rossum 于 1991 年创建,设计哲学强调代码的可读性和简洁性。Python 的格言是"There should be one-- and preferably only one --obvious way to do it"(应该有一种—— preferably 只有一种——显而易见的方式做某事)。 **核心特点** -| 特性 | 说明 | -|------|------| -| **动态类型** | 无需声明变量类型,开发速度快 | -| **语法简洁** | 代码可读性极高,接近伪代码 | -| **胶水语言** | 可以轻松调用 C/C++ 代码 | -| **丰富的库** | NumPy、Pandas、Django、Flask 等 | +| 特性 | 说明 | 为什么重要 | +|------|------|-----------| +| **动态类型** | 无需声明变量类型 | 开发速度快,写代码像写伪代码 | +| **语法简洁** | 接近自然语言 | 可读性极高,新手友好 | +| **胶水语言** | 可以轻松调用 C/C++ 代码 | 性能不够?用 C 扩展补足 | +| **丰富的库** | NumPy、Pandas、Django、Flask | 不需要重复造轮子 | **代码示例** +::: details 查看一个真实的 API 例子 ```python -# Python: 使用 Flask 框架创建 REST API -from flask import Flask, jsonify, request +# Python FastAPI:用户注册 API +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel -app = Flask(__name__) +app = FastAPI() -@app.route('/users/', methods=['GET']) -def get_user(id): - user = find_user_by_id(id) - return jsonify(user) +class RegisterRequest(BaseModel): + username: str + password: str -@app.route('/users', methods=['POST']) -def create_user(): - data = request.get_json() - user = save_user(data) - return jsonify(user), 201 +@app.post("/api/users/register") +async def register(request: RegisterRequest): + # 1. 参数校验(自动进行,Pydantic 处理) + if len(request.username) < 3: + raise HTTPException(status_code=400, detail="用户名太短") -if __name__ == '__main__': - app.run(debug=True) + # 2. 调用业务逻辑 + user = await userService.register(request) + + # 3. 返回结果(自动转 JSON) + return user ``` +**这段代码展示了 Python 的特点**: +- 代码量只有 Java 的 1/3 +- 自动数据校验和 JSON 转换 +- 异步语法(`async/await`)简洁 +::: + **适用场景** - **Web 开发**:Django、Flask、FastAPI 等框架 -- **数据科学**:数据分析、可视化、机器学习 -- **AI/ML**:TensorFlow、PyTorch 等深度学习框架 +- **数据科学**:NumPy、Pandas、Matplotlib +- **AI/ML**:TensorFlow、PyTorch、Scikit-learn - **自动化运维**:脚本编写、DevOps 工具 -- **爬虫开发**:Scrapy、BeautifulSoup 等 +- **爬虫开发**:Scrapy、BeautifulSoup **优缺点分析** | 优点 | 缺点 | |------|------| -| 语法简洁,开发效率高 | GIL 限制,无法真正并行 | -| 丰富的第三方库,生态强大 | 执行速度较慢 | +| 语法简洁,开发效率高 | **GIL 限制**,无法真正并行 | +| 丰富的第三方库,生态强大 | 执行速度较慢(比编译型语言慢 10-100 倍)| | 学习曲线平缓,适合新手 | 动态类型,运行时才能发现错误 | | AI/ML 领域的首选语言 | 移动端和前端支持较弱 | -### 1.3 Node.js:JavaScript 的全栈革命 +::: tip 🤔 什么是 GIL? + +**GIL = Global Interpreter Lock,全局解释器锁** + +Python 的 GIL 就像一扇只能一个人通过的窄门:虽然你的代码看起来是多线程,但实际上同一时刻只能有一个线程在执行 Python 字节码。 + +**影响**:CPU 密集型任务无法利用多核 CPU,多线程反而比单线程慢。 + +**解决方案**: +1. 用多进程代替多线程(`multiprocessing` 库) +2. 用 C/C++ 扩展处理性能敏感部分 +3. 换语言(如 Go)处理高性能需求 +::: + +**真实案例:Instagram 为什么用 Python?** + +Instagram 是世界上最大的 Python 部署之一,每天处理几十亿请求。为什么他们不换性能更强的语言? + +1. **开发效率**:Python 让小团队能快速迭代 +2. **性能足够**:通过架构设计(缓存、异步、微服务)弥补语言短板 +3. **团队熟悉度**:工程师都是 Python 专家 +4. **不是瓶颈**:Instagram 的瓶颈在数据库和网络,不在 Python + +他们还开发了 Cython(Python 到 C 的编译器),把性能敏感的模块编译成 C 代码,获得接近 C 的性能。 + +--- + +### 3.3 Node.js:JavaScript 的全栈革命 + +::: tip 🤔 什么是"全栈"? + +**全栈 = 前端 + 后端都会** + +传统开发: +- 前端:JavaScript(浏览器) +- 后端:Java/Python/Go(服务器) +- 需要学两种语言 + +Node.js 全栈: +- 前端:JavaScript +- 后端:JavaScript(Node.js) +- 只需要学一种语言 + +这就是 Node.js 的最大价值:**语言统一**。 +::: **历史与定位** @@ -142,57 +446,102 @@ Node.js 由 Ryan Dahl 于 2009 年创建,它让 JavaScript 这门原本只能 **核心特点** -| 特性 | 说明 | -|------|------| -| **单线程事件循环** | 通过异步 I/O 处理大量并发连接 | -| **JavaScript 全栈** | 前后端使用同一种语言 | -| **npm 生态** | 世界上最大的开源库生态系统 | -| **快速启动** | 轻量级,适合微服务架构 | +| 特性 | 说明 | 为什么重要 | +|------|------|-----------| +| **单线程事件循环** | 通过异步 I/O 处理大量并发 | I/O 密集型应用性能极强 | +| **JavaScript 全栈** | 前后端使用同一种语言 | 减少语言切换,开发效率高 | +| **npm 生态** | 世界上最大的开源库生态系统 | 几乎任何功能都能找到现成的包 | +| **快速启动** | 轻量级,启动时间<1 秒 | 适合微服务和 Serverless | **代码示例** +::: details 查看一个真实的 API 例子 ```javascript -// Node.js: 使用 Express 框架创建 REST API +// Node.js Express:用户注册 API const express = require('express'); const app = express(); -app.use(express.json()); +app.use(express.json()); // 自动解析 JSON -// 获取用户 -app.get('/users/:id', async (req, res) => { - const user = await findUserById(req.params.id); - res.json(user); +app.post('/api/users/register', async (req, res) => { + try { + // 1. 参数校验 + const { username, password } = req.body; + if (!username || username.length < 3) { + return res.status(400).json({ error: '用户名太短' }); + } + + // 2. 调用业务逻辑(异步) + const user = await userService.register({ username, password }); + + // 3. 返回结果 + res.json(user); + } catch (err) { + res.status(500).json({ error: err.message }); + } }); -// 创建用户 -app.post('/users', async (req, res) => { - const user = await createUser(req.body); - res.status(201).json(user); -}); - -app.listen(3000, () => { - console.log('Server running on port 3000'); -}); +app.listen(3000); ``` +**这段代码展示了 Node.js 的特点**: +- `async/await` 异步语法简洁 +- 回调错误处理(try/catch) +- 与前端 JavaScript 代码风格一致 +::: + **适用场景** -- **实时应用**:聊天室、在线游戏、协作工具 +- **实时应用**:聊天室、在线游戏、协作工具(WebSocket 支持) - **API 服务**:RESTful API、GraphQL 服务 - **全栈 Web 应用**:Next.js、Nuxt.js 等框架 -- **微服务架构**:轻量级服务快速启动 +- **微服务架构**:轻量级服务,快速启动 - **Serverless 函数**:AWS Lambda、Vercel Functions **优缺点分析** | 优点 | 缺点 | |------|------| -| 前后端语言统一,全栈开发效率高 | 单线程,CPU 密集型任务表现差 | -| npm 生态丰富,包管理方便 | 回调地狱问题(已被 async/await 缓解)| -| 高并发 I/O 性能优秀 | 类型系统较弱(TypeScript 可缓解)| -| 启动速度快,适合微服务 | 生态质量参差不齐 | +| 前后端语言统一,全栈开发效率高 | **单线程**,CPU 密集型任务表现差 | +| npm 生态丰富,包管理方便 | 回调地狱(已被 async/await 缓解)| +| 高并发 I/O 性能优秀 | 类型系统较弱(可用 TypeScript 缓解)| +| 启动速度快,适合微服务 | 生态质量参差不齐,依赖管理混乱 | -### 1.4 Go:云原生时代的性能之选 +**真实踩坑案例:CPU 密集型任务的陷阱** + +某团队用 Node.js 做图片处理服务,用户上传图片后需要压缩、加水印、生成缩略图。 + +**问题**:这些操作都是 CPU 密集型,Node.js 的单线程模型导致处理一张图片时,整个事件循环被阻塞,其他请求全部等待。 + +**结果**:并发性能极差,3 个请求就能把服务打挂。 + +**解决方案**: +1. 用 Go 重写图片处理服务(终极方案) +2. 用子进程处理 CPU 密集型任务(临时方案) +3. 使用 sharp 库(底层用 C++ 实现)代替纯 JavaScript 库 + +**关键启示**:Node.js 擅长 I/O(读写数据库、调用 API),不擅长 CPU 计算(图像处理、加密解密)。选型时必须理解这个根本差异。 + +--- + +### 3.4 Go:云原生时代的性能之选 + +::: tip 🤔 什么是"云原生"? + +**云原生 = 为云环境设计的应用** + +特点: +- **容器化**:Docker 打包,到处运行 +- **微服务**:小而独立的服务 +- **动态编排**:Kubernetes 自动调度 + +Go 是云原生的首选语言,因为: +1. 编译成单一二进制文件,部署极简 +2. 启动快,适合容器环境 +3. 并发性能强,适合微服务 + +Docker 和 Kubernetes 都是用 Go 写的。 +::: **历史与定位** @@ -200,73 +549,108 @@ Go(又称 Golang)由 Google 的 Robert Griesemer、Rob Pike 和 Ken Thompson **核心特点** -| 特性 | 说明 | -|------|------| -| **Goroutine 协程** | 轻量级线程,百万级并发轻松实现 | -| **Channel 通道** | 基于 CSP 模型的通信机制,避免共享内存 | -| **快速编译** | 编译速度极快,接近解释型语言体验 | -| **静态链接** | 编译生成单二进制文件,部署简单 | +| 特性 | 说明 | 为什么重要 | +|------|------|-----------| +| **Goroutine 协程** | 轻量级线程,百万级并发轻松实现 | 高并发场景性价比最高 | +| **Channel 通道** | 基于 CSP 模型的通信机制 | 避免共享内存,代码更安全 | +| **快速编译** | 编译速度极快,接近解释型语言体验 | 开发效率高,反馈循环快 | +| **静态链接** | 编译生成单二进制文件,部署简单 | 一个文件搞定,无需依赖 | **代码示例** +::: details 查看一个真实的 API 例子 ```go -// Go: 使用 Gin 框架创建 REST API +// Go Gin:用户注册 API package main import ( - "net/http" "github.com/gin-gonic/gin" + "net/http" ) -type User struct { - ID int `json:"id"` - Name string `json:"name"` - Age int `json:"age"` +type RegisterRequest struct { + Username string `json:"username" binding:"required,min=3"` + Password string `json:"password" binding:"required"` +} + +func register(c *gin.Context) { + // 1. 参数绑定和校验(自动进行) + var req RegisterRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 2. 调用业务逻辑 + user, err := userService.Register(req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 3. 返回结果 + c.JSON(http.StatusOK, user) } func main() { r := gin.Default() - - // 获取用户 - r.GET("/users/:id", func(c *gin.Context) { - id := c.Param("id") - user := findUserByID(id) - c.JSON(http.StatusOK, user) - }) - - // 创建用户 - r.POST("/users", func(c *gin.Context) { - var user User - if err := c.ShouldBindJSON(&user); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - newUser := createUser(user) - c.JSON(http.StatusCreated, newUser) - }) - + r.POST("/api/users/register", register) r.Run(":3000") } ``` +**这段代码展示了 Go 的特点**: +- 结构体标签自动校验参数 +- 错误处理显式且清晰 +- 编译成单一可执行文件 +::: + **适用场景** -- **云原生基础设施**:Docker、Kubernetes 等云原生工具都用 Go 编写 +- **云原生基础设施**:Docker、Kubernetes、Prometheus - **微服务架构**:高性能、低延迟的分布式服务 - **网络编程**:高并发服务器、代理、网关 -- **命令行工具**:Docker、kubectl、Terraform 等 -- **区块链开发**:以太坊、Hyperledger Fabric 等项目 +- **命令行工具**:Docker、kubectl、Terraform +- **区块链开发**:以太坊、Hyperledger Fabric **优缺点分析** | 优点 | 缺点 | |------|------| -| 并发性能极强,Goroutine 轻量高效 | 泛型支持较晚(Go 1.18 才引入)| -| 编译速度快,开发效率高 | 错误处理较繁琐(显式 error 检查)| +| **并发性能极强**,Goroutine 轻量高效 | 泛型支持较晚(Go 1.18 才引入)| +| 编译速度快,开发效率高 | **错误处理繁琐**(`if err != nil` 到处都是)| | 部署简单,单二进制文件 | 缺少成熟的 GUI 框架 | | 垃圾回收性能优秀 | 生态相对年轻,某些领域库不够丰富 | -### 1.5 Rust:系统编程的新星 +**真实案例:Uber 为什么从 Node.js 迁移到 Go?** + +Uber 早期大量使用 Node.js,但随着业务增长,遇到了严重的性能问题:在高并发场景下,Node.js 的单线程模型无法充分利用多核 CPU,导致延迟波动大。 + +Uber 选择 Go 重写了部分核心服务(如定价、 ETA 计算),结果: +- 延迟降低了 10 倍 +- 硬件成本降低了 50% +- 系统稳定性大幅提升 + +**为什么 Go 比 Node.js 快这么多?** +1. **真正的并行**:Go 可以利用多核 CPU,Node.js 是单线程 +2. **编译优化**:Go 是编译型语言,性能接近 C++ +3. **GC 优化**:Go 的垃圾回收器延迟极低(<1ms) + +--- + +### 3.5 Rust:系统编程的新星 + +::: tip 🤔 什么是"系统编程"? + +**系统编程 = 编写操作系统、数据库、浏览器底层** + +特点: +- 对性能要求极高(毫秒级甚至微秒级) +- 对内存控制要求严格(不能泄漏) +- 对安全性要求极高(不能崩溃) + +这类程序通常用 C/C++ 编写,但 Rust 正在改变这个局面。 +::: **历史与定位** @@ -274,50 +658,45 @@ Rust 由 Mozilla 研究院的 Graydon Hoare 于 2006 年开始设计,2010 年 **核心特点** -| 特性 | 说明 | -|------|------| -| **所有权系统** | 编译时检查内存安全,无需 GC | -| **零成本抽象** | 高级特性不带来运行时开销 | -| **模式匹配** | 强大的 match 表达式,处理所有情况 | -| ** fearless concurrency** | 编译器保证线程安全 | +| 特性 | 说明 | 为什么重要 | +|------|------|-----------| +| **所有权系统** | 编译时检查内存安全,无需 GC | 保证无内存泄漏,性能极佳 | +| **零成本抽象** | 高级特性不带来运行时开销 | 既有安全性,又不牺牲性能 | +| **模式匹配** | 强大的 match 表达式 | 强制处理所有情况,减少 bug | +| **Fearless Concurrency** | 编译器保证线程安全 | 多线程编程不再害怕数据竞争 | **代码示例** +::: details 查看一个真实的 API 例子 ```rust -// Rust: 使用 Actix-web 框架创建 REST API -use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder}; +// Rust Actix-web:用户注册 API +use actix_web::{web, App, HttpResponse, HttpServer}; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] -struct User { - id: u32, - name: String, - age: u8, +#[derive(Deserialize, Serialize)] +struct RegisterRequest { + username: String, + password: String, } -// 获取用户 -#[get("/users/{id}")] -async fn get_user(path: web::Path) -> impl Responder { - let user = User { - id: path.into_inner(), - name: String::from("张三"), - age: 25, - }; - HttpResponse::Ok().json(user) -} +async fn register(req: web::Json) -> HttpResponse { + // 1. 参数校验 + if req.username.len() < 3 { + return HttpResponse::BadRequest().json(json!({"error": "用户名太短"})); + } -// 创建用户 -#[post("/users")] -async fn create_user(user: web::Json) -> impl Responder { - HttpResponse::Created().json(user.into_inner()) + // 2. 调用业务逻辑 + match user_service::register(&req).await { + Ok(user) => HttpResponse::Ok().json(user), + Err(err) => HttpResponse::InternalServerError().json(json!({"error": err.to_string()})), + } } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() - .service(get_user) - .service(create_user) + .route("/api/users/register", web::post().to(register)) }) .bind("127.0.0.1:3000")? .run() @@ -325,6 +704,12 @@ async fn main() -> std::io::Result<()> { } ``` +**这段代码展示了 Rust 的特点**: +- `Result` 类型强制错误处理 +- `match` 表达式覆盖所有情况 +- 编译时保证线程安全和内存安全 +::: + **适用场景** - **系统编程**:操作系统、文件系统、嵌入式开发 @@ -332,109 +717,101 @@ async fn main() -> std::io::Result<()> { - **WebAssembly**:浏览器端高性能计算 - **区块链**:加密货币、智能合约平台 - **游戏引擎**:高性能游戏开发 -- **命令行工具**:快速、安全的 CLI 工具 **优缺点分析** | 优点 | 缺点 | |------|------| -| 极致性能,媲美 C/C++ | 学习曲线极其陡峭 | -| 内存安全,无 dangling pointer | 编译时间较慢 | -| 线程安全,无数据竞争 | 生态相对年轻,某些领域库不够 | +| **极致性能**,媲美 C/C++ | **学习曲线极其陡峭**(最难学的语言之一)| +| **内存安全**,编译时保证无泄漏 | 编译时间较慢 | +| **线程安全**,编译时保证无数据竞争 | 生态相对年轻,某些领域库不够 | | 优秀的错误处理机制 | 开发效率相对较低 | -| 零成本抽象 | 招聘难度大,人才稀缺 | +| 零成本抽象 | **招聘难度大**,人才稀缺 | -### 1.6 PHP:Web 开发的老将 +**真实案例:Dropbox 为什么用 Rust 重写核心存储引擎?** + +Dropbox 的文件存储系统原来用 Python 编写,但随着用户量增长到 5 亿,遇到了严重的性能瓶颈:每个文件请求的 CPU 开销太大,服务器成本极高。 + +他们用 Rust 重写了存储引擎的核心部分(Block Server),结果: +- 单核性能提升了 10 倍 +- 内存占用降低了 50% +- 硬件成本节省了数百万美元 + +**为什么选择 Rust 而不是 C++?** +1. **内存安全**:Rust 编译器保证无内存泄漏,C++ 需要手动管理 +2. **并发安全**:Rust 编译时检查数据竞争,C++ 需要运行时调试 +3. **现代化工具链**:Cargo 包管理器、文档系统、测试框架都很完善 + +**代价**:开发周期变长了,因为 Rust 学习曲线陡峭,团队需要时间适应。 + +--- + +### 3.6 PHP:Web 开发的老将 + +::: tip 🤔 为什么 PHP 还没死? + +虽然 PHP 常被调侃("PHP 是世界上最好的语言"是梗),但它依然支撑着互联网的半壁江山: + +- **WordPress**:全球 40% 的网站用 WordPress +- **Facebook**:早期用 PHP,现在用 Hack(PHP 的改进版) +- **Wikipedia**:完全用 PHP 编写 +- **Slack**:早期后端用 PHP + +**原因**:部署简单、开发快、CMS 生态成熟。对于中小型网站,PHP 依然是性价比最高的选择。 +::: **历史与定位** -PHP(PHP: Hypertext Preprocessor)由 Rasmus Lerdorf 于 1994 年创建,最初只是一套简单的 CGI 脚本,用于跟踪他个人网站的访问者。后来经过不断演化,成为了世界上最流行的 Web 开发语言之一。Facebook、Wikipedia、WordPress 等大型网站都基于 PHP 构建。 +PHP(PHP: Hypertext Preprocessor)由 Rasmus Lerdorf 于 1994 年创建,最初只是一套简单的 CGI 脚本,用于跟踪他个人网站的访问者。后来经过不断演化,成为了世界上最流行的 Web 开发语言之一。 **核心特点** -| 特性 | 说明 | -|------|------| -| **专为 Web 设计** | 内置 HTTP 处理能力,部署极其简单 | -| **解释执行** | 无需编译,改完即生效 | -| **嵌入 HTML** | 可以直接在 HTML 中嵌入 PHP 代码 | -| **庞大的历史代码库** | 世界上 70%+ 的网站使用 PHP | +| 特性 | 说明 | 为什么重要 | +|------|------|-----------| +| **专为 Web 设计** | 内置 HTTP 处理能力 | 部署极其简单,改完即生效 | +| **解释执行** | 无需编译,直接运行 | 开发反馈快 | +| **嵌入 HTML** | 可以直接在 HTML 中写 PHP | 学习门槛低 | +| **庞大的生态** | WordPress、Laravel 等 | CMS 无人能敌 | **代码示例** +::: details 查看一个真实的 API 例子 ```php username) < 3) { + return new Response('用户名太短', 400); + } -// 路由处理 -$method = $_SERVER['REQUEST_METHOD']; -$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); - -// 获取用户 -if ($method === 'GET' && preg_match('/\/users\/(\d+)/', $path, $matches)) { - $userId = $matches[1]; - $user = [ - 'id' => $userId, - 'name' => '张三', - 'age' => 25 - ]; - echo json_encode($user); - exit; -} - -// 创建用户 -if ($method === 'POST' && $path === '/users') { - $data = json_decode(file_get_contents('php://input'), true); - $user = [ - 'id' => rand(1, 1000), - 'name' => $data['name'], - 'age' => $data['age'] - ]; - http_response_code(201); - echo json_encode($user); - exit; -} - -// 404 处理 -http_response_code(404); -echo json_encode(['error' => 'Not Found']); -``` - -**现代 PHP 的进化** - -PHP 8.0+ 带来了巨大改进: - -- **JIT 编译**:性能提升 2-3 倍 -- **类型系统**:支持联合类型、属性等现代特性 -- **Named Arguments**:函数参数可以按名传递 -- **Match 表达式**:更简洁的条件判断 -- **Attributes**:原生支持注解 - -```php -// PHP 8 的现代写法 -class User { - public function __construct( - private string $name, - private int $age - ) {} - - public function getName(): string { - return $this->name; + // 2. 调用业务逻辑 + try { + $user = $this->userService->register($req); + return new JsonResponse($user, 200); + } catch (\Exception $e) { + return new Response($e->getMessage(), 500); + } } } -// Match 表达式 -$result = match($status) { - 'pending' => '待处理', - 'completed' => '已完成', - default => '未知状态', -}; +// 路由配置 +$router->post('/api/users/register', [UserController::class, 'register']); ``` +**这段代码展示了 PHP 的特点**: +- 类型声明(PHP 8+ 支持强类型) +- 异常处理机制 +- 与前端 HTML/JavaScript 集成简单 +::: + **适用场景** - **内容管理系统**:WordPress、Drupal、Joomla -- **电商平台**:Magento、Shopify(部分) +- **电商平台**:Magento、WooCommerce - **中小型 Web 应用**:快速开发,快速上线 - **API 开发**:Laravel、Symfony 等现代框架 @@ -442,165 +819,101 @@ $result = match($status) { | 优点 | 缺点 | |------|------| -| 部署极其简单,改完即生效 | 历史上名声较差("PHP 是世界上最好的语言"梗)| +| **部署极其简单**,改完即生效 | 历史上名声较差("PHP 代码混乱"梗)| | 学习曲线平缓,入门简单 | 大型项目维护难度大 | -| 生态成熟,WordPress 等 CMS 流行 | 性能不如编译型语言 | -| 现代 PHP 8 性能大幅提升 | 异步编程支持不如 Node.js | +| **CMS 生态成熟**,WordPress 流行 | 性能不如编译型语言 | +| **现代 PHP 8** 性能大幅提升 | 异步编程支持不如 Node.js | + +**真实案例:Wikipedia 为什么坚持用 PHP?** + +Wikipedia 是全球第 5 大网站,每月访问量超过 15 亿次,完全用 PHP 编写。为什么不换性能更强的语言? + +1. **历史包袱**:2001 年上线时就是 PHP,代码量巨大 +2. **性能足够**:通过缓存(Memcached)、CDN、数据库优化,瓶颈不在 PHP +3. **开发效率**:PHP 让小团队能快速迭代 +4. **生态成熟**:MediaWiki(Wikipedia 的软件)本身就是 PHP 生态 + +**现代 PHP 的进化**: + +PHP 8 带来了巨大改进: +- **JIT 编译**:性能提升 2-3 倍 +- **类型系统**:支持类型声明、联合类型 +- **Match 表达式**:更简洁的条件判断 +- **属性**:原生支持注解 + +现代 PHP(8.0+)已经不再是当年被调侃的"玩具语言",性能和开发效率都达到了工业级水准。 --- -## 2. 核心维度对比:性能、效率、并发、生态 +## 4. 如何选择合适的语言:决策框架 -### 2.1 性能之争:谁跑得最快? +### 4.1 四步决策法 -**性能排行(大致,仅供参考)**: +### 第一步:明确你的场景类型 -``` -C++ ≈ Rust > Go > Java ≈ C# > Node.js > PHP > Python > Ruby -``` +| 场景类型 | 特征 | 推荐语言 | 不推荐 | +| :--- | :--- | :--- | :--- | +| **企业级核心业务** | 高可用、强事务、长生命周期 | Java、C# | Go(生态不够成熟)| +| **快速原型/MVP** | 快速验证、快速迭代 | Python、Ruby | Java(太慢)| +| **云原生基础设施** | 高并发、低延迟、微服务 | Go、Rust | Python(性能不够)| +| **全栈 Web 应用** | 前后端统一、实时交互 | Node.js、Go | Java(太重)| +| **AI/ML 项目** | 模型训练、数据处理 | Python | 其他所有 | +| **系统编程** | 极致性能、内存控制 | Rust、C++ | 其他所有 | -**但性能不是唯一标准!** +::: tip 📊 从表格中你能看到什么? -让我们用一张表理解性能差异的真实影响: +**企业级应用选 Java**:因为 Java 的类型系统、异常处理、事务支持让大规模系统更稳定。Spring 生态成熟,几乎不需要自己造轮子。 -| 场景 | 语言 | 单次请求耗时 | 1000 QPS 需要服务器 | 说明 | -| :--- | :--- | :--- | :--- | :--- | -| 简单 API | Go | 2ms | 2 台 | 语言差异几乎无感 | -| 简单 API | Python | 10ms | 10 台 | 可能需要更多机器 | -| 复杂计算 | Go | 100ms | 100 台 | CPU 密集型差距明显 | -| 复杂计算 | Python | 1000ms | 1000 台 | 10 倍差距 | +**快速开发选 Python**:代码量只有 Java 的 1/3,开发速度极快。适合 MVP 验证,但如果性能不够,后期可以用 Go 重写核心模块。 -**关键洞察**: +**云原生选 Go**:部署简单(单二进制文件)、启动快、并发强。Docker、Kubernetes 都是 Go 写的,生态成熟。 -> **对于大多数 Web 应用,瓶颈在数据库和网络,不是语言。** -> -> - **I/O 密集型**(CRUD 应用):Node.js、Go 表现优秀,Python 也完全够用 -> - **CPU 密集型**(图像处理、科学计算):Go、Java、C++、Rust 更适合 +**全栈选 Node.js**:前后端都用 JavaScript,减少语言切换成本。适合小团队快速开发。 -**性能对比实战:计算斐波那契数列** +**AI/ML 必须选 Python**:这不是选择,而是必然。整个 AI/ML 生态都是 Python。 +::: -让我们看一个实际的性能测试,计算第 35 个斐波那契数(CPU 密集型任务): +### 第二步:评估团队背景 -| 语言 | 实现方式 | 执行时间 | -|------|----------|----------| -| **Rust** | 递归 + 记忆化 | ~2 ms | -| **Go** | 递归 + 记忆化 | ~3 ms | -| **Java** | 递归 + 记忆化 | ~4 ms | -| **Node.js** | 递归 + 记忆化 | ~12 ms | -| **Python** | 递归 + 记忆化 | ~50 ms | -| **PHP** | 递归 + 记忆化 | ~80 ms | +**决策优先级:团队熟悉度 > 技术最优解** -**分析**: +| 团队背景 | 推荐路线 | 理由 | +| :--- | :--- | :--- | +| **Java 背景** | 继续 Java / 引入 Go | 生态迁移成本低,Go 可作为性能补充 | +| **前端背景** | Node.js → TypeScript → Go | 利用 JS 经验,逐步引入类型安全和后端语言 | +| **Python 背景** | Python + Go 混合 | Python 负责业务逻辑,Go 负责性能敏感模块 | +| **C/C++ 背景** | Rust / Go | Rust 替换 C++,Go 快速开发业务 | +| **全新人团队** | Go / Python | Go 培养工程思维,Python 快速产出 | -- **编译型语言(Rust、Go、Java)**:性能优势明显,适合 CPU 密集型任务 -- **JIT 编译(Node.js)**:V8 引擎优化后性能不错,但仍不如编译型语言 -- **解释型语言(Python、PHP)**:性能相对较低,但在 I/O 密集型场景中差距会缩小 +### 第三步:权衡性能与开发效率 -### 2.2 并发模型对比:谁能处理更多请求? +**决策矩阵**: -**线程 vs 协程 vs 异步:本质区别是什么?** +| 性能要求 | 开发周期 | 推荐语言 | 架构建议 | +| :--- | :--- | :--- | :--- | +| 极高(高频交易)| 长 | C++ / Rust | 专用硬件,定制化优化 | +| 高(高并发 API)| 中 | Go / Java | 微服务,水平扩展 | +| 中等(普通 Web)| 短 | Node.js / Python | 单体应用,快速迭代 | +| 低(内部工具)| 极短 | Python / Ruby | 脚本化,自动化优先 | -| 语言 | 并发模型 | 核心机制 | 资源消耗 | 适用场景 | -| :--- | :--- | :--- | :--- | :--- | -| Java | 操作系统线程 | Thread Pool | 1-2 MB/线程 | 传统企业应用 | -| Go | 用户态协程 | Goroutine + Channel | ~2 KB/协程 | 云原生、微服务 | -| Node.js | 事件循环 | Event Loop + Callback | 单线程 | I/O 密集型应用 | -| Python | 多进程 | Multiprocessing | 进程级隔离 | 数据处理(绕过 GIL)| -| Rust | 异步/并行 | Async/Await | 零成本抽象 | 系统编程 | +### 第四步:考虑长期维护成本 -**Go 的 Goroutine 为什么如此高效?** +**维护成本的隐藏项**: -```go -// Go: 启动 100 万个 Goroutine -for i := 0; i < 1000000; i++ { - go func() { - // 做一些工作 - }() -} -// 内存占用:约 2GB(100万 * 2KB) - -// Java: 启动 100 万个线程 -for (int i = 0; i < 1000000; i++) { - new Thread(() -> { - // 做一些工作 - }).start(); -} -// 结果:OOM(内存溢出),因为需要 1-2TB 内存 -``` - -**关键洞察**: - -> **高并发场景:Go 是性价比最高的选择,Java 适合企业级,Node.js 适合 I/O 密集型。** - -### 2.3 内存管理对比:谁来回收垃圾? - - - -**垃圾回收 vs 手动管理 vs 所有权系统** - -| 语言 | 内存管理 | 实现方式 | 性能影响 | 开发体验 | -| :--- | :--- | :--- | :--- | :--- | -| Java | GC (垃圾回收) | 分代收集、并发标记 | 中等(STW 停顿) | 自动,无需关心 | -| Python | GC + 引用计数 | 循环引用检测 | 较差(GIL 影响) | 自动,偶有内存泄漏 | -| Go | GC | 低延迟 GC (Go 1.20+) | 良好 | 自动,性能优秀 | -| Node.js | GC (V8) | 分代回收 | 良好 | 自动,V8 优化好 | -| Rust | 所有权系统 | 编译时检查,无 GC | 极佳 | 手动,学习曲线陡峭 | -| C++ | 手动管理 | new/delete 或智能指针 | 极佳(但风险高) | 完全手动,易出错 | - -**什么是 STW (Stop-The-World)?** - -``` -STW = 垃圾回收时的"世界暂停" - -Java GC 过程: -1. 标记阶段:标记所有存活对象 -2. 清理阶段:回收垃圾对象内存 - -问题: -- 在清理阶段,应用线程必须暂停(STW) -- 对于大内存堆,STW 可能持续数百毫秒 -- 这会导致应用卡顿(GC Pause) - -现代解决方案: -- G1 GC:分区回收,控制 STW 时间 -- ZGC/Shenandoah:亚毫秒级停顿 -``` - -**关键洞察**: - -> **追求极致性能选 Rust/C++,企业级应用选 Java/Go,快速开发选 Python/Node.js。** - -### 2.4 生态成熟度对比 - -语言生态的成熟度直接影响开发效率和项目维护成本。 - -**包管理器对比** - -| 语言 | 包管理器 | 包数量(估算) | 特点 | -|------|----------|----------------|------| -| **JavaScript** | npm / yarn / pnpm | 200万+ | 包数量最多,生态最活跃 | -| **Python** | pip / Poetry | 40万+ | 数据科学库最丰富 | -| **Java** | Maven / Gradle | 30万+ | 企业级库成熟稳定 | -| **Go** | go modules | 20万+ | 标准库强大,第三方库精简实用 | -| **Rust** | Cargo | 10万+ | 包质量高,安全性强 | -| **PHP** | Composer | 30万+ | Web 框架成熟,CMS 生态丰富 | - -**框架成熟度对比** - -| 语言 | Web 框架 | 特点 | -|------|----------|------| -| **Java** | Spring Boot | 企业级首选,功能全面,生态庞大 | -| **Python** | Django / Flask / FastAPI | Django 功能全,Flask 轻量,FastAPI 现代高性能 | -| **Node.js** | Express / NestJS / Koa | Express 简单,NestJS 企业级,Koa 轻量 | -| **Go** | Gin / Echo / Fiber | 都追求高性能和简洁 | -| **Rust** | Actix-web / Axum / Rocket | 极致性能,但学习曲线陡峭 | -| **PHP** | Laravel / Symfony | Laravel 现代优雅,Symfony 企业级 | +| 因素 | 影响 | 语言差异 | +| :--- | :--- | :--- | +| **人才招聘** | 影响团队扩张 | Java 人才最多,Rust 最难招 | +| **监控运维** | 影响故障排查 | Java 工具链最全,Go 轻量简单 | +| **版本升级** | 影响技术债务 | Python 2→3 痛苦,Go 向后兼容 | +| **安全更新** | 影响合规 | 主流语言都有安全团队支持 | --- -## 3. 案例研究:GitHub 的技术栈演进 +## 5. 真实案例:技术栈如何演进 -了解了技术维度后,让我们通过 GitHub 的真实案例,看看技术栈是如何演进的。 +了解了理论后,让我们通过真实案例,看看技术栈是如何在实际项目中演进的。 + +### 5.1 GitHub:从 Ruby 到多语言共存 **2008 年**:GitHub 上线,全部用 **Ruby on Rails** 开发。 @@ -617,7 +930,7 @@ Java GC 过程: **解决方案:渐进式重构** -GitHub 没有"推倒重来",而是采用了**绞杀者模式 (Strangler Fig Pattern)**: +GitHub 采用**绞杀者模式 (Strangler Fig Pattern)**: 1. **识别瓶颈**:找出最慢的功能模块(如代码搜索、通知系统) 2. **逐步替换**:用 Go 重写高性能服务 @@ -638,121 +951,78 @@ GitHub 没有"推倒重来",而是采用了**绞杀者模式 (Strangler Fig Pa > **技术栈演进不是革命,而是渐进式改良。选错语言不致命,但拒绝改进会致命。** ---- +### 5.2 Twitter:从 Ruby 到 Java -## 4. 如何选择合适的语言:决策框架 +**2006 年**:Twitter 上线,用 **Ruby on Rails** 开发。 -### 4.1 四步决策法 +**问题出现**: +- 用户快速增长,频繁宕机(著名的"Fail Whale"时代) +- Rails 无法处理高并发,每次推文都要查询数据库 +- 响应时间从 200ms 涨到 5 秒 -### 第一步:明确你的场景类型 +**演进过程**: +1. **2008 年**:引入 **Scala**(JVM 语言)处理消息队列 +2. **2010 年**:核心搜索功能迁移到 **Java**(Lucene) +3. **2011 年**:整个推文流处理迁移到 **Java** +4. **2017 年**:完全迁移到微服务架构,多语言共存 -| 场景类型 | 特征 | 推荐语言 | 不推荐 | -| :--- | :--- | :--- | :--- | -| **企业级核心业务** | 高可用、强事务、长生命周期 | Java、C# | Go(生态不够成熟)| -| **快速原型/MVP** | 快速验证、快速迭代 | Python、Ruby | Java(太慢)| -| **云原生基础设施** | 高并发、低延迟、微服务 | Go、Rust | Python(性能不够)| -| **全栈 Web 应用** | 前后端统一、实时交互 | Node.js、Go | Java(太重)| -| **AI/ML 项目** | 模型训练、数据处理 | Python | 其他所有 | -| **系统编程** | 极致性能、内存控制 | Rust、C++ | 其他所有 | +**今天的 Twitter 技术栈**: +- **前端**:React + JavaScript +- **后端服务**:Java、Scala、Go、Python 混合 +- **消息队列**:Kafka(Scala/Java) +- **存储**:HDFS、Cassandra、Redis -### 4.2 第二步:评估团队背景 +**关键启示**: -**决策优先级:团队熟悉度 > 技术最优解** - -| 团队背景 | 推荐路线 | 理由 | -| :--- | :--- | :--- | -| **Java 背景** | 继续 Java / 引入 Go | 生态迁移成本低,Go 可作为性能补充 | -| **前端背景** | Node.js -> TypeScript -> Go | 利用 JS 经验,逐步引入类型安全和后端语言 | -| **Python 背景** | Python + Go 混合 | Python 负责业务逻辑,Go 负责性能敏感模块 | -| **C/C++ 背景** | Rust / Go | Rust 替换 C++,Go 快速开发业务 | -| **全新人团队** | Go / Python | Go 培养工程思维,Python 快速产出 | - -### 4.3 第三步:权衡性能与开发效率 - -**决策矩阵**: - -| 性能要求 | 开发周期 | 推荐语言 | 架构建议 | -| :--- | :--- | :--- | :--- | -| 极高(高频交易)| 长 | C++ / Rust | 专用硬件,定制化优化 | -| 高(高并发 API)| 中 | Go / Java | 微服务,水平扩展 | -| 中等(普通 Web)| 短 | Node.js / Python | 单体应用,快速迭代 | -| 低(内部工具)| 极短 | Python / Ruby | 脚本化,自动化优先 | - -### 4.4 第四步:考虑长期维护成本 - -**维护成本的隐藏项**: - -| 因素 | 影响 | 语言差异 | -| :--- | :--- | :--- | -| **人才招聘** | 影响团队扩张 | Java 人才最多,Rust 最难招 | -| **监控运维** | 影响故障排查 | Java 工具链最全,Go 轻量简单 | -| **版本升级** | 影响技术债务 | Python 2->3 痛苦,Go 向后兼容 | -| **安全更新** | 影响合规 | 主流语言都有安全团队支持 | - -### 4.5 语言选型决策树 - -为了帮助你更直观地做出选择,以下是一个简化的决策流程: - -``` -开始选型 - │ - ├── 是否需要极致性能(高频交易、游戏引擎)? - │ ├── 是 → Rust / C++ - │ └── 否 → 继续 - │ - ├── 是否做 AI/ML/数据科学? - │ ├── 是 → Python - │ └── 否 → 继续 - │ - ├── 是否已有 Java 技术栈的团队? - │ ├── 是 → Java / Kotlin - │ └── 否 → 继续 - │ - ├── 是否需要快速开发 MVP? - │ ├── 是 → Python / Node.js / PHP - │ └── 否 → 继续 - │ - ├── 是否构建云原生/微服务? - │ ├── 是 → Go - │ └── 否 → 继续 - │ - └── 推荐默认选择:Go - (现代、平衡、未来趋势) -``` +> **不要推倒重来,要渐进式迁移。Twitter 用了 5 年时间才完成技术栈转型。** --- -## 5. 快速参考:语言特性速查表 +## 6. 常见误区与真相 -### 5.1 核心特性对比 +### 误区 1:"XX 语言性能最好,所以应该用它" -| 语言 | 类型系统 | 编译/解释 | 内存管理 | 并发模型 | 主要应用领域 | -| :--- | :--- | :--- | :--- | :--- | :--- | -| **Java** | 静态强类型 | 编译(JVM)| GC | 多线程 | 企业级应用、Android | -| **Python** | 动态强类型 | 解释 | GC + 引用计数 | 多进程(GIL)| AI/ML、数据分析、脚本 | -| **Go** | 静态强类型 | 编译 | GC | Goroutine(协程)| 云原生、微服务、基础设施 | -| **Node.js** | 动态弱类型 | 解释(V8)| GC | Event Loop | 全栈 Web、实时应用 | -| **Rust** | 静态强类型 | 编译 | 所有权系统 | Async/Await | 系统编程、区块链 | -| **C++** | 静态强类型 | 编译 | 手动管理 | 多线程/异步 | 游戏、高频交易、系统软件 | +**真相**:性能不是唯一标准,甚至往往不是最重要的标准。 -### 5.2 性能与资源占用 +对于大多数 Web 应用,瓶颈在: +1. **数据库查询**(占 70% 以上时间) +2. **网络 I/O**(调用外部 API) +3. **缓存策略**(Redis、Memcached) -| 语言 | 执行速度 | 内存占用 | 启动时间 | 并发能力 | -| :--- | :--- | :--- | :--- | :--- | -| **C++** | ★★★★★ | 极低 | 极快 | 极高 | -| **Rust** | ★★★★★ | 极低 | 快 | 极高 | -| **Go** | ★★★★ | 极低 | 极快 | 极高(百万级协程)| -| **Java** | ★★★★ | 高 | 慢(JVM 启动)| 高 | -| **Node.js** | ★★★ | 中 | 快 | 高(I/O 密集)/ 低(CPU 密集)| -| **Python** | ★★ | 中 | 快 | 低(GIL 限制)| +语言本身的性能差异只占很小一部分。通过架构优化(缓存、异步、水平扩展),Python 也能支撑百万级并发。 + +**例子**:Instagram 用 Python 支撑 5 亿用户,通过缓存和异步架构弥补了语言性能短板。 + +### 误区 2:"学了 XX 语言,其他语言就不需要学了" + +**真相**:现代系统往往是多语言混合架构。 + +**典型的微服务架构**: +- **API 网关**:Go(高性能) +- **业务逻辑**:Java 或 Python(开发效率高) +- **AI/ML 服务**:Python(生态成熟) +- **实时推送**:Node.js(WebSocket 支持好) +- **高性能计算**:Rust 或 C++(极致性能) + +**建议**:精通一门,了解多门。主语言要深入,其他语言要理解设计哲学和适用场景。 + +### 误区 3:"新语言一定比旧语言好" + +**真相**:语言没有好坏,只有适合与否。 + +**Python(1991)**:比 Go(2009)老,但在 AI/ML 领域无人能敌。 +**Java(1995)**:比 Go(2009)老,但在企业级应用依然统治。 +**PHP(1994)**:被嘲笑了 20 年,但依然支撑着互联网半壁江山。 + +**关键不是语言的年龄,而是生态成熟度和团队熟悉度。** --- -## 6. 总结:没有银弹,只有权衡 +## 7. 总结:没有银弹,只有权衡 -### 6.1 核心观点回顾 +### 7.1 核心观点回顾 1. **语言选择是工程决策,不是宗教战争** - 每个语言都有其设计哲学和适用场景 @@ -769,7 +1039,7 @@ GitHub 没有"推倒重来",而是采用了**绞杀者模式 (Strangler Fig Pa - 微服务、缓存、异步处理等架构策略影响远大于语言 - 不要指望换语言解决所有问题 -### 6.2 给不同阶段工程师的建议 +### 7.2 给不同阶段工程师的建议 **初级工程师(0-2 年)**: - 先精通一门语言(推荐 Python 或 Go) @@ -788,9 +1058,9 @@ GitHub 没有"推倒重来",而是采用了**绞杀者模式 (Strangler Fig Pa --- -## 7. 更多学习资源 +## 8. 更多学习资源 -### 7.1 官方文档推荐 +### 8.1 官方文档推荐 | 语言 | 官方文档 | 推荐入门教程 | |------|----------|--------------| @@ -801,27 +1071,16 @@ GitHub 没有"推倒重来",而是采用了**绞杀者模式 (Strangler Fig Pa | **Rust** | [doc.rust-lang.org](https://doc.rust-lang.org/) | The Rust Book | | **PHP** | [php.net/docs](https://www.php.net/docs.php) | Laravel 官方文档 | -### 7.2 在线练习平台 +### 8.2 在线练习平台 - **LeetCode**: 算法练习,支持所有主流语言 - **HackerRank**: 编程挑战和面试准备 - **Exercism**: 免费编程练习,有导师评审 - **Codewars**: 游戏化编程练习 -### 7.3 社区与论坛 - -| 语言 | Reddit 社区 | Stack Overflow 标签 | Discord 服务器 | -|------|-------------|---------------------|----------------| -| Java | r/java | java | The Coding Den | -| Python | r/Python | python | Python Discord | -| Node.js | r/node | node.js | Node.js Discord | -| Go | r/golang | go | Gophers | -| Rust | r/rust | rust | Rust Programming | -| PHP | r/PHP | php | PHP Community | - --- -## 8. 名词速查表 (Glossary) +## 9. 名词速查表 (Glossary) | 名词 | 全称 | 解释 | | :--- | :--- | :--- | @@ -831,23 +1090,14 @@ GitHub 没有"推倒重来",而是采用了**绞杀者模式 (Strangler Fig Pa | **Goroutine** | - | Go 语言的轻量级线程(协程)| | **NPM** | Node Package Manager | Node.js 的包管理器,世界最大的包仓库 | | **Pip** | Pip Installs Packages | Python 的包管理器 | -| **Maven** | - | Java 的项目管理和构建工具 | | **ORM** | Object-Relational Mapping | 对象关系映射,用面向对象方式操作数据库 | | **STW** | Stop-The-World | 垃圾回收时的暂停时间 | | **JIT** | Just-In-Time Compilation | 即时编译,提高运行时性能 | | **Type Safety** | - | 类型安全,编译时检查类型错误 | -| **Memory Safe** | - | 内存安全,编译时保证无内存泄漏 | | **Concurrency** | - | 并发,同时处理多个任务 | | **Parallelism** | - | 并行,真正同时执行多个任务 | -| **Async/Await** | - | 异步编程语法,简化异步代码编写 | -| **Event Loop** | - | 事件循环,Node.js 的并发模型 | | **I/O Bound** | - | I/O 密集型,瓶颈在网络/磁盘操作 | | **CPU Bound** | - | CPU 密集型,瓶颈在计算 | -| **Jitter** | - | 延迟抖动,网络或 GC 导致的时间波动 | -| **Throughput** | - | 吞吐量,单位时间处理的请求数 | -| **Latency** | - | 延迟,请求响应时间 | - ---- --- @@ -908,6 +1158,6 @@ Rust 很酷,但如果你的团队只有 PHP 经验,强行切换可能带来 --- -*最后更新:2024年12月* +*最后更新:2025年1月* *本文档基于各语言的最新稳定版本(Java 21、Python 3.12、Go 1.23、Node.js 22、Rust 1.83、PHP 8.4)编写,特性描述可能随版本更新而变化。* diff --git a/docs/zh-cn/appendix/browser-rendering-pipeline.md b/docs/zh-cn/appendix/browser-rendering-pipeline.md index 722740f..a2e3465 100644 --- a/docs/zh-cn/appendix/browser-rendering-pipeline.md +++ b/docs/zh-cn/appendix/browser-rendering-pipeline.md @@ -1,558 +1,681 @@ -# 浏览器渲染管线与事件循环可视化 +# 浏览器渲染管线与事件循环 -> 💡 **学习指南**:浏览器是你最亲密的"同事",但你真的了解它是怎么干活的吗?本文将带你深入浏览器的"车间",看看它是如何把一堆HTML、CSS、JavaScript变成你眼前的像素画的。本章节会围绕一个问题展开:**为什么有些网页流畅如丝,有些却卡成PPT?** - -在开始之前,建议你先补两块"基础砖": - -- **DOM是什么**:可以先阅读 [Web开发基础](./web-basics/) 的相关内容。 -- **JavaScript异步基础**:如果你对Promise、async/await还不熟悉,可以先了解相关概念。 +::: tip 🎯 核心问题 +**为什么有些网页流畅如丝,有些却卡成PPT?** 浏览器是怎么把一堆HTML、CSS、JavaScript代码变成你眼前看到的网页的?本章将带你深入浏览器的"车间",理解它的工作流程,从而写出性能更好的网页。 +::: --- -## 0. 引言:为什么我的网页卡成PPT? +## 1. 为什么要理解"渲染管线"? + +### 1.1 从"能跑"到"跑得快":前端开发的进阶之路 + +刚开始学前端时,我们只关心代码"能不能跑"——页面能显示出来,按钮能点击,就算成功了。但随着项目变大,用户变多,你很快会发现一个残酷的现实:**同样的功能,有人写的页面丝般顺滑,有人写的却卡顿到用户想摔鼠标**。 + +这就像学开车。新手只关心"车能不能开动",但老司机会关心"什么时候该换挡、什么时候该刹车、怎么开最省油"。浏览器就是你开的那辆"车",理解它的"工作习性",你才能开得又快又稳。 + +
+
+ +**🐢 新手思维(只关注功能)** +- 只要页面能显示就行 +- 卡顿是浏览器的问题 +- 性能优化是后期才考虑的事 + +
+
+ +**🚀 进阶思维(关注体验)** +- 流畅度是用户体验的核心 +- 理解浏览器工作流程 +- 写代码时就考虑性能 + +
+
+ +**理解渲染管线,就是从"能跑"到"跑得快"的关键一步。** + +### 1.2 一个真实的踩坑故事:为什么"优化"后反而更卡了? + +::: warning 小张的性能踩坑记 +小张是一家电商公司的前端工程师,负责优化商品详情页。这个页面展示商品信息时卡得要死,用户投诉不断。 + +小张想:"页面卡应该是因为DOM太多了,我先用`display:none`隐藏起来,修改完再显示,这样浏览器就不会重复渲染了吧?" + +于是他写了这样的代码: + +```javascript +// 你以为的"优化" +const container = document.getElementById('list') +container.style.display = 'none' // 先隐藏,应该不会触发渲染了吧? + +for (let i = 0; i < 1000; i++) { + const item = document.createElement('div') + item.style.width = Math.random() * 100 + 'px' // 随机宽度 + container.appendChild(item) +} + +container.style.display = 'block' // 最后显示,一次性渲染 +``` + +结果测试后发现,页面**更卡了**!小张懵了:明明已经"优化"了,为什么反而更慢? + +后来前端负责人看了代码,点出问题所在:**虽然元素被隐藏了,但你每次修改`style.width`仍然会触发浏览器的样式计算和布局标记,浏览器在后台做了大量无用功**。 + +正确的做法是用`DocumentFragment`在内存中批量操作,最后一次性插入DOM,只触发一次渲染。 +::: + +::: info 💡 核心启示 +不了解浏览器的工作流程,你可能会"自作聪明"地写出一堆"优化代码",结果反而让性能更差。**理解渲染管线,你才知道哪些操作是昂贵的、哪些是廉价的,从而避免在错误的地方用力。** +::: + +--- + +## 2. 核心概念:什么是"渲染管线"? + +::: tip 🤔 什么是"渲染"? +**渲染(Rendering)**,简单说就是浏览器把代码"画"成你看到的网页的过程。 + +你可以把它想象成**印刷厂印书**: +- **HTML** = 书稿内容(文字、图片、章节) +- **CSS** = 排版要求(字体大小、颜色、间距) +- **JavaScript** = 动态修改(作者临时改稿、调整排版) + +浏览器拿到这些"材料"后,要经过一道道"工序",最后才能"印刷"出你看到的网页。这一系列工序,就是**渲染管线(Rendering Pipeline)**。 +::: + +为了帮你更好地理解,我们用一家**面包店**来比喻浏览器的渲染流程。 + +### 2.1 用面包店比喻理解渲染管线 + +想象你在经营一家面包店,每天要为顾客制作各种面包。这个过程中涉及到的环节,与浏览器的渲染流程惊人地相似: + +| 阶段 | 🥖 面包店比喻 | 浏览器实际工作 | 具体例子 | +|------|-------------|--------------|----------| +| **1. 准备食材** | 整理原料清单(面粉、鸡蛋、奶油...) | **构建DOM树**:把HTML解析成树形结构 | 你写`

Hello

`,浏览器解析成`div→p→"Hello"`的树 | +| **2. 准备配方** | 整理配方卡(每种面包的配料比例) | **构建CSSOM树**:把CSS解析成规则树 | 你写`.title { color: red }`,浏览器记录"`.title`的文字是红色" | +| **3. 制定计划** | 根据原料和配方,决定今天要做什么面包 | **构建渲染树**:合并DOM和CSSOM,只保留可见元素 | ` + + +
+

可见内容

+
+
+

隐藏内容(display:none)

+
+ + ``` -浏览器构建CSSOM时需要计算每个元素的最终样式,复杂的选择器会增加计算量。 +**DOM树会包含所有元素**: +- ``、``、`<style>`、`<script>`(这些不显示) +- `display: none`的div(也不显示) + +但**渲染树只包含"要画到屏幕上"的元素**: +- 去掉`<head>`及其子元素 +- 去掉`display: none`的div + +### 4.2 渲染树的构建规则 + +浏览器在构建渲染树时,会遵循一套规则: + +| 场景 | 处理方式 | 示例 | 性能影响 | +|------|---------|------|----------| +| `display: none` | **完全排除**出渲染树 | 元素及其子元素都不可见 | ✅ 减少渲染工作量 | +| `visibility: hidden` | **包含在渲染树中**,但不绘制 | 占据空间,但完全透明 | ⚠️ 仍需布局计算 | +| `opacity: 0` | **包含在渲染树中**,但透明 | 可交互(能点击),但看不见 | ⚠️ 仍需布局计算 | +| 不在视口内 | **包含在渲染树中**,暂不绘制 | 滚动到视口时才绘制 | ⚠️ 但仍在渲染树中 | + +::: tip 📊 从表格中你能看到什么? +**关键发现**:`display: none`是唯一"真正省性能"的隐藏方式,因为元素完全不在渲染树里,浏览器不会为它做任何布局和绘制工作。 + +而`visibility: hidden`和`opacity: 0`虽然"看不见",但仍在渲染树中,浏览器仍需计算它们的布局(占据空间)。如果你需要"隐藏但不影响布局"(比如做淡入淡出动画),可以用`opacity`;如果需要"完全隐藏且不占空间",用`display: none`。 +::: + +### 4.3 踩坑实录:为什么设置了display:none,页面还是卡? + +::: danger ❌ 常见误区:以为display:none的元素"不存在" +很多人以为设置`display: none`后,元素就"消失"了,怎么操作都不会影响性能。这是**错误**的! + +虽然`display: none`的元素不在渲染树中,但你通过JavaScript修改它的属性时,浏览器仍需要: +1. **重新计算样式**(匹配CSS规则) +2. **跟踪变化**(为未来显示做准备) + +看下面这个"优化"例子: +::: + +::: details 查看"无效优化"的代码 +```javascript +// ❌ 你以为的"优化":先隐藏,修改完再显示 +const container = document.getElementById('list') +container.style.display = 'none' + +// 疯狂操作DOM +for (let i = 0; i < 1000; i++) { + const item = document.createElement('div') + item.style.width = Math.random() * 100 + 'px' // 改变宽度! + item.textContent = `Item ${i}` + container.appendChild(item) +} + +container.style.display = 'block' + +// 问题:每次修改style.width,浏览器都要重新计算样式, +// 即使元素是display:none! +``` + +**✅ 正确的优化姿势:** +```javascript +// 使用DocumentFragment批量操作 +const container = document.getElementById('list') +const fragment = document.createDocumentFragment() // 虚拟容器 + +// 所有操作都在内存中的fragment上进行 +for (let i = 0; i < 1000; i++) { + const item = document.createElement('div') + item.style.width = Math.random() * 100 + 'px' + item.textContent = `Item ${i}` + fragment.appendChild(item) // 不影响真实DOM +} + +// 一次性插入真实DOM,只触发一次渲染 +container.appendChild(fragment) +``` +::: + +--- + +## 5. 第三阶段:布局与重排 + +### 5.1 什么是"布局"? + +::: tip 🤔 什么是布局(Layout)? +**布局**,也叫**回流(Reflow)**,是浏览器计算渲染树中每个元素"在什么位置、占多大空间"的过程。 + +你可以把它想象成**装修设计师测量房间**: +- 先测量每个房间的长宽 +- 决定家具摆在哪里 +- 算出每个家具的坐标 + +**为什么布局很"贵"?** 因为一个元素的变化可能影响其他元素。比如你把一个div变宽了,它旁边的div可能被挤下去,导致整个页面重新计算。 +::: + +### 5.2 触发重排的"雷区" + +以下是常见的会触发重排的操作,**建议收藏并背诵**: + +| 类别 | 属性/操作 | 性能影响 | 替代方案 | +|------|----------|----------|----------| +| **尺寸** | `width`, `height`, `min/max-width/height` | 💀💀💀 | 用`transform: scale()`代替 | +| **位置** | `top`, `right`, `bottom`, `left` | 💀💀💀 | 用`transform: translate()`代替 | +| **边距** | `margin`, `padding` | 💀💀 | 用`transform`或`gap`代替 | +| **边框** | `border-width` | 💀💀 | 尽量避免频繁修改 | +| **内容** | 文字内容变化、图片加载 | 💀💀 | 预留空间,避免布局抖动 | +| **字体** | `font-size`, `line-height` | 💀💀💀 | 尽量避免频繁修改 | +| **显示** | `display`值改变 | 💀💀💀 | 用`visibility`或`opacity`代替(如不需要完全隐藏) | +| **查询** | `offsetWidth`, `offsetHeight`等 | 💀💀💀💀💀 | **批量读取,避免布局抖动** | + +::: tip 📊 从表格中你能看到什么? +**关键发现**: +1. **几何属性(宽高位置)最昂贵**:它们会触发完整的布局计算 +2. **查询属性比修改更危险**:读取`offsetWidth`会**强制同步布局**(详见5.4节) +3. **transform和opacity是性能最好的**:它们不触发重排,只触发合成 +::: + +### 5.3 踩坑实录:为什么我的动画卡成PPT? + +**坑:用width做动画** + +::: details 查看性能差的动画代码 +```css +/* ❌ 坏的动画:触发重排 */ +.box { + width: 100px; + transition: width 0.3s; +} + +.box:hover { + width: 200px; /* 改变宽度会触发重排! */ +} +``` + +每一帧动画都会触发重排,浏览器需要: +1. 重新计算宽度 +2. 重新计算位置(可能影响其他元素) +3. 重新绘制 + +**✅ 好的动画:用transform** +```css +/* ✅ 好的动画:只触发合成 */ +.box { + width: 100px; + transform: scaleX(1); + transition: transform 0.3s; +} + +.box:hover { + transform: scaleX(2); /* 缩放不触发重排! */ +} +``` + +`transform`直接由GPU处理,不会触发重排和重绘,动画丝般顺滑。 +::: + +### 5.4 性能杀手:强制同步布局 + +::: danger 💀 最危险的性能问题:布局抖动 +**强制同步布局(Forced Synchronous Layout)**,也叫**布局抖动(Layout Thrashing)**,是最常见也是最严重的性能问题。 + +它的原因是:**JavaScript在读取布局属性(如`offsetWidth`)时,浏览器必须立即执行布局计算,才能返回准确值。** + +如果你"读写交替",就会导致浏览器反复"布局→读取→布局→读取",形成恶性循环。 +::: + +::: details 查看布局抖动的代码 +```javascript +// ❌ 极坏:读写交替,导致布局抖动 +const elements = document.querySelectorAll('.item') + +for (let i = 0; i < elements.length; i++) { + const height = elements[i].offsetHeight // 读取 → 强制布局 + elements[i].style.width = (height * 2) + 'px' // 写入 → 标记需要重排 + // 下一次循环的读取又会强制布局...恶性循环! +} + +// 如果有100个元素,就会触发100次布局计算! +``` + +**✅ 正确的优化姿势:读写分离** +```javascript +const elements = document.querySelectorAll('.item') + +// 第一步:批量读取(先全部读完) +const heights = [] +for (let i = 0; i < elements.length; i++) { + heights.push(elements[i].offsetHeight) // 只触发一次布局 +} + +// 第二步:批量写入(再全部写) +requestAnimationFrame(() => { + for (let i = 0; i < elements.length; i++) { + elements[i].style.width = (heights[i] * 2) + 'px' // 只触发一次重排 + } +}) +``` +::: + +<LayoutReflowDemo /> + +--- + +## 6. 第四阶段:绘制与重绘 + +### 6.1 什么是"绘制"? + +::: tip 🤔 什么是绘制(Paint)? +**绘制**,是浏览器把"布局计算好"的元素真正"画"到屏幕上的过程。 + +你可以把它想象成**给房间刷漆**: +- 布局阶段 = 量尺寸、画线 +- 绘制阶段 = 真正刷漆、贴壁纸 + +**绘制没有布局那么昂贵,但也不便宜。** 频繁绘制仍会影响性能,尤其是复杂元素(阴影、渐变等)。 +::: + +### 6.2 触发重绘的信号 + +与重排不同,重绘只涉及"外观"的改变,不涉及"几何"的改变: + +| 类别 | 属性 | 性能影响 | 备注 | +|------|------|----------|------| +| **颜色** | `color`, `background-color` | 💀 | 最常见的重绘触发者 | +| **背景** | `background-image`, `background-position` | 💀💀 | 图片比纯色慢 | +| **边框** | `border-color`, `border-style` | 💀 | 改变边框颜色/样式 | +| **文字** | `text-decoration`, `text-shadow` | 💀💀 | 阴影比纯文字慢 | +| **盒阴影** | `box-shadow` | 💀💀💀 | 复杂的阴影很慢 | +| **圆角** | `border-radius` | 💀 | 改变圆角大小 | +| **透明度** | `opacity` | ✅ | **特殊:不触发重绘,只触发合成** | + +::: tip 📊 从表格中你能看到什么? +**关键发现**:`opacity`是特殊的!它和`transform`一样,不会触发重绘,而是直接触发合成阶段。这就是为什么用`opacity`做淡入淡出动画性能最好的原因。 + +另外,**阴影和渐变比重绘更昂贵**,因为它们需要复杂的像素计算。如果你的页面有很多`box-shadow`,考虑用伪元素或图片代替。 +::: + +### 6.3 踩坑实录:为什么我的hover效果卡? + +**坑:用box-shadow做hover动画** + +::: details 查看性能差的hover效果 +```css +/* ❌ 坏的hover效果:box-shadow动画很慢 */ +.card { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: box-shadow 0.3s; +} + +.card:hover { + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); /* 阴影很慢! */ +} +``` + +`box-shadow`需要逐像素计算,动画时会卡顿。 + +**✅ 好的做法:用transform或伪元素** +```css +/* ✅ 好的hover效果:用transform */ +.card { + transform: translateY(0); + transition: transform 0.3s, box-shadow 0.3s; +} + +.card:hover { + transform: translateY(-4px); /* 只在hover时改阴影,不做动画 */ + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +} +``` +::: <PaintLayerDemo /> --- -## 4. 第二阶段:构建渲染树(Render Tree) +## 7. 第五阶段:合成与GPU加速 -### 4.1 为什么需要"渲染树"? +### 7.1 什么是"合成"? -DOM树和CSSOM树构建完成后,浏览器需要把它们"合并"成一棵新的树——**渲染树(Render Tree)**。 +::: tip 🤔 什么是合成(Composite)? +**合成**,是现代浏览器的"魔法",它把页面的不同部分分成多个**层(Layer)**,然后利用**GPU(图形处理器)**来并行合成最终的画面。 -为什么要多此一举?因为: -- **DOM树包含了所有节点**,包括那些不可见的(如`<head>`、`<script>`、`display:none`的元素); -- **渲染树只包含可见节点**,它是实际需要绘制到屏幕上的内容的集合。 +你可以把它想象成**Photoshop的图层**: +- 传统方式 = 所有东西画在一层上(CPU串行,慢) +- 合成方式 = 分层画,最后合并(GPU并行,快) -``` -DOM树 CSSOM树 - │ │ - ▼ ▼ -┌─────────┐ ┌──────────┐ -│ html │ │ body │ -│ │ │ │ │ │ -│ body │ │ ├── width: 100% -│ │ │ │ └── div -│ div │ │ ├── color: red -│ │ │ │ └── display: none -│ span │ └──────────┘ -│ (隐藏) │ -└─────────┘ - │ - ▼ - 合并计算 - │ - ▼ - ┌─────────────┐ - │ 渲染树 │ - │ │ - │ body │ - │ │ │ - │ div │ ← span被排除,因为CSS指定了display:none - │ (最终样式) │ - └─────────────┘ +**为什么合成快?** 因为GPU擅长处理"图像合成"这种并行任务,比CPU快几十倍。 +::: + +### 7.2 哪些元素会被提升到"合成层"? + +浏览器会自动将某些元素提升到独立的合成层。以下是常见的触发条件: + +| 触发条件 | CSS属性/值 | 性能影响 | 注意事项 | +|---------|-----------|----------|----------| +| **3D变换** | `transform: translate3d()`, `rotate3d()` | ✅✅✅ | 动画性能最佳 | +| **硬件加速hack** | `transform: translateZ(0)` | ✅✅ | 俗称"强制GPU加速" | +| **透明度动画** | `opacity`变化(配合动画) | ✅✅✅ | 不触发重绘 | +| **固定定位** | `position: fixed` | ✅ | 避免滚动时重复布局 | +| **Will-Change** | `will-change: transform, opacity` | ✅✅ | 提前创建层,注意内存 | +| **Canvas/WebGL** | `<canvas>`, WebGL内容 | ✅✅ | 天然在独立层中 | +| **Video** | `<video>` | ✅✅ | 独立层,防止相互影响 | + +::: tip 📊 从表格中你能看到什么? +**关键发现**:`transform`和`opacity`是性能最好的动画属性,因为它们不触发重排和重绘,直接触发合成。这就是为什么性能优化指南总是说"用transform和opacity做动画"。 + +但要注意:**每个合成层都要占用GPU内存**,滥用`translateZ(0)`会导致内存爆炸(详见7.4节)。 +::: + +### 7.3 踩坑实录:合成层太多反而卡? + +::: danger 💀 过度优化的陷阱 +有人听说"GPU加速快",就给所有元素都加`transform: translateZ(0)`,结果页面反而更卡了。 + +**问题原因**: +每个合成层需要在GPU中存储一份"纹理"(位图),占用内存。如果一个页面有100个合成层,GPU内存可能被撑爆,导致低端设备崩溃或降级到CPU渲染。 +::: + +::: details 查看"过度优化"的代码 +```css +/* ❌ 错误做法:给所有元素都开启GPU加速 */ +.card { transform: translateZ(0); } +.button { transform: translateZ(0); } +.icon { transform: translateZ(0); } +/* ... 100个元素都加 ... */ + +/* 结果:GPU内存爆炸,页面卡死 */ ``` -### 4.2 渲染树的构建规则 - -浏览器在构建渲染树时,会遵循以下规则: - -| 场景 | 处理方式 | 示例 | -|------|---------|------| -| `display: none` | **完全排除**出渲染树 | 元素及其所有子元素都不可见 | -| `visibility: hidden` | **包含在渲染树中**,但标记为不可见 | 占据空间,但完全透明 | -| `opacity: 0` | **包含在渲染树中**,但完全透明 | 可交互,但看不见 | -| 不在视口内 | **包含在渲染树中**,暂不绘制 | 滚动到视口时才绘制 | - -### 4.3 踩坑实录:为什么设置了display:none,页面还是卡? - -**坑:以为display:none的元素"不存在"** - -```javascript -// 你以为的优化:先隐藏,修改完再显示 -const container = document.getElementById('list') -container.style.display = 'none' // "这步应该很快吧?" - -// 疯狂操作DOM -for (let i = 0; i < 1000; i++) { - const item = document.createElement('div') - item.textContent = `Item ${i}` - item.style.width = `${Math.random() * 100}px` // 改变宽度! - container.appendChild(item) +**✅ 正确的做法:按需使用** +```css +/* 策略1:只给真正需要动画的元素开启 */ +.card { + transition: transform 0.3s ease; } -container.style.display = 'block' // "这下应该一次渲染了吧?" -``` - -**残酷的现实**: - -即使设置了`display:none`,当你修改元素的样式(尤其是影响布局的属性如`width`、`height`、`margin`等)时,浏览器仍然需要: - -1. **重新计算样式**(Recalculate Style):计算每个元素匹配哪些CSS规则; -2. **构建/更新渲染树**(Update Render Tree):因为`display:none`的元素虽然不在渲染树中,但它们的子元素可能会被JavaScript引用并修改; -3. **标记需要重排的节点**:即使父节点`display:none`,浏览器也需要跟踪这些变化,以便在显示时正确渲染。 - -**正确的优化姿势**: - -```javascript -// 真正的优化:使用DocumentFragment批量操作 -const container = document.getElementById('list') -const fragment = document.createDocumentFragment() // 创建一个"虚拟容器" - -// 所有操作都在内存中的fragment上进行,不影响真实DOM -for (let i = 0; i < 1000; i++) { - const item = document.createElement('div') - item.textContent = `Item ${i}` - item.style.width = `${Math.random() * 100}px` - fragment.appendChild(item) // 操作的是fragment,不是真实DOM! +.card:hover { + transform: translateY(-5px); /* 自动创建合成层 */ } -// 一次性把fragment的内容插入真实DOM,只触发一次重排和重绘 -container.appendChild(fragment) +/* 策略2:用will-change提示浏览器 */ +.card { + will-change: transform; /* 提前创建层 */ +} + +/* 策略3:动画结束后移除 */ +.card:not(:hover) { + will-change: auto; /* 释放GPU内存 */ +} ``` +::: <CompositeDemo /> --- -## 5. 第三阶段:布局(Layout)与重排(Reflow) +## 8. 事件循环:JavaScript的"分身术" -### 5.1 为什么浏览器要"算"布局? +::: tip 🤔 什么是事件循环? +**事件循环(Event Loop)**,是JavaScript实现"异步"的机制。因为JavaScript是**单线程**的(一次只能做一件事),但它又要处理用户点击、网络请求、定时器等多种任务,所以需要一套"调度系统"来管理这些任务。 -渲染树构建完成后,浏览器知道了页面上有哪些可见元素,以及它们的样式。但还不知道它们**具体在什么位置、占多大空间**。 +你可以把它想象成**快递分拣中心**: +- **Call Stack(调用栈)** = 当前正在处理的快递 +- **Web APIs** = 外部合作仓库(定时器、网络请求等) +- **Callback Queue(回调队列)** = 待处理的快递架 +- **Event Loop(事件循环)** = 分拣机器人(不断检查"是否可以处理下一个任务") +::: -这就像你拿到了家具的清单(渲染树),知道要买哪些家具、什么颜色,但还没设计家具的摆放位置。布局阶段就是做这件事的。 +### 8.1 宏任务与微任务 -浏览器会: -1. **从渲染树的根节点开始遍历**; -2. **计算每个节点的几何信息**:宽度、高度、位置(x, y坐标); -3. **处理嵌套关系**:父节点的尺寸会影响子节点,子节点的尺寸也可能影响父节点(视布局方式而定)。 - -### 5.2 重排(Reflow)的"脾气" - -布局计算很"贵",因为它通常是**同步**的、**阻塞**的。也就是说,当你通过JavaScript修改了一个影响布局的属性,浏览器必须: - -1. **立即停止当前的所有工作**; -2. **重新计算样式**(可能涉及复杂的CSS选择器匹配); -3. **重新构建/更新渲染树**; -4. **重新执行布局计算**(从根节点开始,可能涉及成千上万节点的几何计算); -5. **完成后才能继续执行你的JavaScript代码**。 - -这就是**重排(Reflow)**,也称为**回流**或**重新布局(Relayout)**。 - -### 5.3 触发重排的"雷区" - -以下是常见的会触发重排的属性和操作,建议**背诵并收藏**: - -| 类别 | 属性/操作 | 说明 | -|------|----------|------| -| **尺寸** | `width`, `height`, `min/max-width/height` | 改变元素的宽高会触发重排 | -| **位置** | `top`, `right`, `bottom`, `left` | 定位元素的位置变化会触发重排 | -| **边距** | `margin`, `padding` | 内外边距的改变会触发重排 | -| **边框** | `border-width`, `border-style`(某些情况) | 边框改变可能影响布局 | -| **内容** | 文字内容变化、图片加载 | 内容改变可能导致尺寸变化 | -| **字体** | `font-size`, `font-weight`, `line-height` | 字体变化影响文本布局 | -| **显示** | `display`(值改变时) | `none`↔`block`等切换会触发重排 | -| **布局模式** | `position`(值改变时) | `static`↔`absolute`等切换会触发重排 | -| **查询** | `offsetWidth`, `offsetHeight`, `offsetTop`等 | **读取**这些值会强制浏览器立即执行重排!| - -**重点注意最后一项**:读取布局属性会**强制同步布局(Forced Synchronous Layout)**,这是性能杀手中的战斗机! - -### 5.4 踩坑实录:为什么我的"优化"反而更卡? - -**坑:强制同步布局的"死亡循环"** - -```javascript -// 你想做的:给所有卡片设置相同高度 -const cards = document.querySelectorAll('.card') - -// 你觉得这样很"聪明":先获取最高卡片的高度 -let maxHeight = 0 -for (let i = 0; i < cards.length; i++) { - const height = cards[i].offsetHeight // 触发强制同步布局! - maxHeight = Math.max(maxHeight, height) -} - -// 然后统一设置 -for (let i = 0; i < cards.length; i++) { - cards[i].style.height = maxHeight + 'px' // 再次触发重排! -} -``` - -**问题分析**: - -这段代码触发了**两次强制同步布局**: - -1. **读取`offsetHeight`时**:浏览器为了确保返回的是最新的高度值,必须立即执行一次完整的布局计算(重排)。如果有100个卡片,这个动作会执行100次! - -2. **设置`height`时**:这又触发了新一轮的重排。 - -更糟糕的是,如果在读取和设置之间还有其他DOM操作,浏览器可能不得不进行**布局抖动(Layout Thrashing)**——反复地读取→重排→写入→重排→读取...形成恶性循环。 - -**正确的优化姿势**: - -```javascript -const cards = document.querySelectorAll('.card') - -// 第一步:批量读取(先全部读完) -const heights = [] -for (let i = 0; i < cards.length; i++) { - heights.push(cards[i].offsetHeight) // 读取操作集中在一起 -} - -// 计算最大值 -const maxHeight = Math.max(...heights) - -// 第二步:批量写入(再全部写) -// 使用 requestAnimationFrame 确保在下一次重绘前执行 -requestAnimationFrame(() => { - for (let i = 0; i < cards.length; i++) { - cards[i].style.height = maxHeight + 'px' - } -}) -``` - -**优化原理**: - -1. **读写分离**:先集中完成所有读取操作(获取offsetHeight),再集中完成所有写入操作(设置height)。这样浏览器可以在读取阶段一次性完成布局计算,而不是每读一个就重排一次。 - -2. **requestAnimationFrame**:将写入操作放在下一次重绘之前执行。这样可以确保: - - 批量处理DOM修改,减少重排次数; - - 与浏览器的渲染周期同步,避免不必要的计算。 - -<EventLoopDemo /> - ---- - -## 6. 第四阶段:绘制(Paint)与合成(Composite) - -### 6.1 从"设计图"到"真墙壁":绘制阶段 - -布局完成后,浏览器已经知道了每个元素的位置和大小,就像装修师傅已经量好了尺寸、画好了线。接下来就是"刷墙"——把元素的样式真正"画"出来。 - -绘制阶段,浏览器会: -1. **遍历渲染树**,为每个可见节点创建绘制记录(Paint Record); -2. **调用图形API**,将元素的背景、边框、文字、阴影等绘制到内存中的位图(Bitmap)上; -3. **分层绘制**,不同的元素可能绘制到不同的"层"(Layer)上,方便后续处理。 - -### 6.2 触发重绘(Repaint)的"信号" - -与重排不同,重绘只涉及"外观"的改变,不涉及"几何"的改变。以下属性会触发重绘: - -| 类别 | 属性 | 说明 | -|------|------|------| -| **颜色** | `color`, `background-color` | 文字和背景颜色变化 | -| **背景** | `background-image`, `background-position` | 背景图片和位置 | -| **边框样式** | `border-color`, `border-style`(颜色部分) | 边框外观变化 | -| **文字** | `text-decoration`, `text-shadow` | 文字装饰和阴影 | -| **盒阴影** | `box-shadow` | 元素阴影 | -| **可见性** | `visibility`(非`none`值之间切换) | 元素可见性变化 | -| **圆角** | `border-radius` | 圆角大小 | -| **透明度** | `opacity` | 元素透明度 | - -**注意**:`opacity`和`transform`是两个特殊的属性,它们不会触发重绘,而是直接触发**合成**阶段!这是它们性能优异的原因。 - -### 6.3 合成(Composite):GPU的"魔法" - -传统的前三个阶段(构建渲染树、布局、绘制)都是在**CPU**上执行的。而**合成(Composite)**阶段,是现代浏览器引入的一项重要优化——它把页面的不同部分分成多个**层(Layer)**,然后利用**GPU(图形处理器)**来并行合成最终的画面。 - -你可以这样理解: -- **传统方式**:画家一笔一笔在画布上画完整个画面(CPU串行执行); -- **合成方式**:把画面分成多层(背景层、人物层、前景层),分别画好,然后用相机一次性拍在一起(GPU并行合成)。 - -### 6.4 哪些因素会创建新的合成层? - -浏览器会自动将某些元素提升到独立的合成层。以下是常见的触发条件: - -| 触发条件 | CSS属性/值 | 说明 | -|---------|-----------|------| -| **3D变换** | `transform: translate3d()`, `rotate3d()`, `scale3d()` | 任何3D变换都会创建合成层 | -| **硬件加速的2D变换** | `transform: translateZ(0)` | 俗称"GPU hack",强制创建层 | -| **透明度动画** | `opacity`变化(配合动画) | 避免重绘,直接合成 | -| **固定定位** | `position: fixed` | 避免滚动时重复布局 | -| **Will-Change** | `will-change: transform, opacity` | 显式提示浏览器提前创建层 | -| **Canvas/WebGL** | `<canvas>`, WebGL内容 | 天然在独立层中 | -| **Video/iframe** | `<video>`, `<iframe>` | 独立层,防止相互影响 | -| **Backface-Visibility** | `backface-visibility: hidden` | 3D相关,创建层 | -| **CSS滤镜** | `filter`(某些浏览器) | 可能创建层 | -| **Mask/Clip** | `clip-path`, `mask`(某些浏览器) | 可能创建层 | - -### 6.5 踩坑实录:合成层太多反而卡? - -**坑:滥用`translateZ(0)`导致GPU内存爆炸** - -```css -/* 你以为的优化:给所有动画元素都开启GPU加速 */ -.card { transform: translateZ(0); } -.button { transform: translateZ(0); } -.icon { transform: translateZ(0); } -/* ... 100个元素都加 ... */ -``` - -**问题分析**: - -每个合成层都需要GPU内存来存储: -- **层的内容**:像素数据(位图),大小取决于元素的尺寸; -- **层的元数据**:位置、变换矩阵、透明度等信息。 - -如果一个页面的合成层太多,会导致: -1. **GPU内存占用过高**:低端设备(尤其是手机)可能会崩溃或降级到CPU渲染; -2. **合成阶段耗时增加**:层越多,GPU需要处理的纹理越多,合成时间线性增长; -3. **额外的内存带宽消耗**:每一层都需要从CPU传输到GPU。 - -**正确的优化姿势**: - -```css -/* 策略1:只给真正需要动画的元素开启GPU加速 */ -.card { - /* 默认不使用GPU加速 */ - transition: transform 0.3s ease; -} - -.card:hover { - /* 只在需要动画时临时开启,动画结束后可移除 */ - transform: translateY(-5px); - will-change: transform; /* 或者使用will-change */ -} - -/* 策略2:使用CSS containment减少影响范围 */ -.card { - contain: layout style paint; /* 告诉浏览器这个元素是"独立"的 */ -} - -/* 策略3:对于复杂列表,使用虚拟滚动 */ -/* 只渲染视口内的元素,DOM节点数量固定 */ -``` - -**优化原理**: - -1. **精准使用GPU加速**:只在真正需要动画的元素上使用`transform`或`will-change`,避免"一刀切"地给所有元素加`translateZ(0)`。 - -2. **CSS Containment**:`contain`属性告诉浏览器"这个元素的变化不会影响外部",浏览器可以据此进行优化,比如: - - `contain: layout`:元素的布局变化不影响外部; - - `contain: paint`:元素的绘制不会溢出边界; - - `contain: style`:CSS计数器等不会影响外部。 - -3. **虚拟滚动**:对于长列表,与其让所有元素都参与渲染管线的各个阶段,不如只渲染视口内的元素。这样无论列表多长,DOM节点数量都是固定的。 - -<MacroMicroTaskDemo /> - ---- - -## 7. 第五阶段:事件循环与JavaScript执行机制 - -### 7.1 为什么JavaScript需要"事件循环"? - -前面的四个阶段(DOM/CSSOM构建、渲染树构建、布局、绘制、合成)都是浏览器的工作。但网页不是静态的图片,它需要响应用户的点击、输入、滚动,需要动态更新内容。这些动态行为的指挥官,就是**JavaScript**。 - -但JavaScript有一个"先天缺陷":**它是单线程的**。这意味着它同一时间只能做一件事。如果JavaScript在执行一个耗时任务(比如计算100万以内的所有质数),那么这段时间里,它就不能响应用户的点击,不能更新页面,整个浏览器就会"假死"。 - -这显然是不可接受的。为了解决这个问题,浏览器为JavaScript设计了一套"分身术":**事件循环(Event Loop)**。 - -### 7.2 事件循环的核心组件 - -你可以把事件循环想象成一个快递分拣中心,有几个核心"工种"在协同工作: - -| 组件 | 类比 | 作用 | -|------|------|------| -| **Call Stack(调用栈)** | 当前正在处理的快递 | 记录当前正在执行的JavaScript代码。当一个函数被调用,它就被"压入"栈顶;执行完就"弹出"。 | -| **Web APIs** | 外部合作仓库 | 浏览器提供的"外挂"功能,比如`setTimeout`、DOM事件、AJAX请求。这些操作不会阻塞调用栈,完成后会把回调放入任务队列。 | -| **Callback Queue(回调队列)** | 待处理的快递架 | 存放那些已经"准备好"执行,但还在等待调用栈清空的回调函数。 | -| **Event Loop(事件循环)** | 分拣机器人 | 不断检查调用栈是否为空。如果为空,就把回调队列中的第一个任务推入调用栈执行。 | - -<EventLoopDemo /> - -### 7.3 宏任务(Macro Task)与微任务(Micro Task) - -早期的JavaScript只有一套任务队列(就是上面的回调队列)。但随着异步编程越来越复杂,浏览器引入了两类任务:**宏任务(Macro Task)**和**微任务(Micro Task)**。 +早期的JavaScript只有一套任务队列。但随着异步编程变复杂,浏览器引入了两类任务: | 类型 | 常见来源 | 优先级 | 执行时机 | -|------|---------|--------|---------| -| **宏任务** | `setTimeout`/`setInterval`、I/O操作、UI渲染、`<script>`标签 | 低 | 每个事件循环周期执行一个 | -| **微任务** | `Promise.then/catch/finally`、`MutationObserver`、`queueMicrotask` | 高 | 当前宏任务结束后,下一个宏任务开始前,清空所有微任务 | +|------|---------|--------|----------| +| **宏任务** | `setTimeout`/`setInterval`、I/O操作、UI渲染 | 低 | 每个事件循环周期执行一个 | +| **微任务** | `Promise.then`、`MutationObserver` | 高 | 当前宏任务结束后,立即清空所有微任务 | **执行顺序的"口诀"**: ``` -1. 执行当前宏任务(比如<script>整体、setTimeout回调) +1. 执行当前宏任务(比如<script>整体) 2. 执行过程中产生的所有微任务(Promise.then等) ↳ 微任务可以产生新的微任务,全部清空后才继续 3. 如果有需要,进行UI渲染(重排/重绘) 4. 开启下一轮事件循环,执行下一个宏任务 ``` -**关键理解**: +### 8.2 踩坑实录:Promise比setTimeout快? -微任务的设计目的是让异步操作尽可能快地执行,但又不会阻塞当前正在执行的同步代码。它比宏任务"更急",因为宏任务之间可能要等待很长时间(比如`setTimeout`的延迟),而微任务保证在当前操作"告一段落"后立即执行。 +::: danger ❌ 常见误解:setTimeout(fn, 0)会"立即"执行 +很多人以为`setTimeout(fn, 0)`是"0毫秒后立即执行",这是**错误**的理解。 -<MacroMicroTaskDemo /> - -### 7.4 踩坑实录:Promise比setTimeout快? - -**坑:以为setTimeout(fn, 0)会"立即"执行** +实际上,`setTimeout(fn, 0)`的含义是:**"至少等待0毫秒后,将回调加入宏任务队列"**。但它需要等待当前调用栈清空、微任务队列清空、可能的UI渲染完成后,才能执行。 +::: +::: details 查看执行顺序 ```javascript console.log('1. Start') @@ -579,17 +702,10 @@ console.log('4. End') // 2. setTimeout callback ``` -**问题分析**: - -1. **`setTimeout(fn, 0)`的真正含义**:不是"0毫秒后立即执行",而是"**至少**等待0毫秒后,将回调加入宏任务队列"。实际上,由于事件循环的工作机制,它通常需要等待当前调用栈清空、微任务队列清空、可能的UI渲染完成后,才能执行。 - -2. **Promise.then的本质**:`Promise.then`注册的是微任务。根据事件循环的规则,**微任务在当前宏任务结束后立即执行,优先级高于下一个宏任务**。 - -3. **执行流程图解**: - +**执行流程图解:** ``` -调用栈(Call Stack) 宏任务队列(Macrotask Queue) 微任务队列(Microtask Queue) - [setTimeout callback] [Promise.then callback] +调用栈(Call Stack) 宏任务队列 微任务队列 + [setTimeout callback] [Promise.then callback] 1. console.log('1. Start') → 输出: 1. Start @@ -598,7 +714,7 @@ console.log('4. End') → 将回调加入宏任务队列 ← [setTimeout callback] 3. Promise.resolve().then() - → 将回调加入微任务队列 ← [Promise.then callback] + → 将回调加入微任务队列 ← [Promise.then callback] 4. console.log('4. End') → 输出: 4. End @@ -616,37 +732,37 @@ console.log('4. End') → 执行: console.log('2. setTimeout callback') → 输出: 2. setTimeout callback ``` +::: -**正确的认知**: +::: tip 💡 核心启示 +**微任务比宏任务"更急"**。如果你希望某个操作在"当前代码块结束后、但UI更新前"尽快执行,用`Promise.then`或`queueMicrotask`。 -1. **微任务比宏任务"更急"**:如果你希望某个操作在当前代码块"结束后、但UI更新前"尽快执行,用`Promise.then`或`queueMicrotask`。 +`setTimeout(0)`不保证立即执行,它至少会被延迟到当前调用栈清空、微任务队列清空之后。 +::: -2. **setTimeout(0)不保证立即执行**:它至少会被延迟到当前调用栈清空、微任务队列清空之后。如果需要"尽快但不必立即",或者需要兼容旧浏览器,可以用它。 +<EventLoopDemo /> -3. **requestAnimationFrame的特殊性**:`requestAnimationFrame`(rAF)也是一种宏任务,但它与渲染周期紧密绑定,通常会在下一次重绘前执行。如果你需要在"下一次UI更新前"做一些计算或准备,rAF是更好的选择。 - -<RenderingPerformanceDemo /> +<MacroMicroTaskDemo /> --- -## 8. 性能优化实战:让你的网页"飞"起来 +## 9. 性能优化实战:让你的网页"飞"起来 -### 8.1 黄金法则:避免强制同步布局 +理解了渲染管线的工作流程后,我们来看看如何优化。以下是五个最实用的优化技巧。 -我们已经知道,读取布局属性(如`offsetWidth`、`clientHeight`等)会强制浏览器立即执行布局计算。最坏的情况是**交替进行读取和写入**: +### 9.1 黄金法则:避免强制同步布局 +**问题**:交替读取和写入布局属性,导致布局抖动。 + +::: details 查看优化前后对比 ```javascript -// ❌ 极坏:读写交替,导致布局抖动(Layout Thrashing) +// ❌ 极坏:读写交替,导致布局抖动 for (let i = 0; i < elements.length; i++) { const height = elements[i].offsetHeight // 读取 → 强制布局 elements[i].style.height = (height * 2) + 'px' // 写入 → 标记需要重排 // 下一次循环的读取又会强制布局...恶性循环! } -``` -**优化方案:批量读写分离** - -```javascript // ✅ 极好:先全部读取,再全部写入 // 第一步:批量读取 const heights = [] @@ -655,65 +771,52 @@ for (let i = 0; i < elements.length; i++) { } // 第二步:批量写入 -for (let i = 0; i < elements.length; i++) { - elements[i].style.height = (heights[i] * 2) + 'px' -} -// 只需要两次布局计算(读取时一次,实际修改后一次) +requestAnimationFrame(() => { + for (let i = 0; i < elements.length; i++) { + elements[i].style.height = (heights[i] * 2) + 'px' + } +}) ``` +::: -### 8.2 使用transform和opacity做动画 +### 9.2 使用transform和opacity做动画 -前面多次提到,`transform`和`opacity`是性能最好的动画属性,因为它们:**不触发重排、不触发重绘,直接触发合成阶段**! +**问题**:用`width`、`height`、`left`、`top`做动画会触发重排。 +::: details 查看优化前后对比 ```css -/* ❌ 坏的动画属性(触发重排) */ +/* ❌ 坏的动画:触发重排 */ .box { - transition: width 0.3s, height 0.3s, left 0.3s, top 0.3s; + transition: width 0.3s, left 0.3s; +} +.box.moving { + width: 200px; + left: 100px; } -/* ✅ 好的动画属性(仅触发合成) */ +/* ✅ 好的动画:只触发合成 */ .box { - transition: transform 0.3s, opacity 0.3s; -} -.box:hover { - transform: translateX(100px) scale(1.2); /* 移动+缩放 */ - opacity: 0.8; -} -``` - -**实用技巧:用transform模拟其他属性变化** - -```css -/* 模拟从width: 0到width: auto的展开效果 */ -.accordion { - transform: scaleX(0); - transform-origin: left; transition: transform 0.3s; } -.accordion.open { - transform: scaleX(1); -} - -/* 模拟margin-top的移动 */ -.slider { - transform: translateY(0); - transition: transform 0.3s; -} -.slider.down { - transform: translateY(100px); /* 替代 margin-top: 100px */ +.box.moving { + transform: translateX(100px) scaleX(2); } ``` +::: -### 8.3 虚拟滚动:解决大数据列表 +### 9.3 虚拟滚动:解决大数据列表 -当列表项数量达到数千甚至上万时,无论你怎么优化,DOM节点的数量本身就是一个性能瓶颈。这时,**虚拟滚动(Virtual Scrolling)**是终极解决方案。 +**问题**:列表项数量达到数千时,DOM节点数量过多导致性能问题。 **核心思想**:只渲染视口内可见的列表项(加上少量缓冲),DOM节点数量固定,与数据总量无关。 +<RenderingPerformanceDemo /> + +::: details 查看虚拟滚动的实现 ```vue <template> - <div class="virtual-list-container" ref="container" @scroll="handleScroll"> - <!-- 占位元素,用于撑起滚动条 --> + <div class="virtual-list" @scroll="handleScroll"> + <!-- 占位元素,撑起滚动条 --> <div class="phantom" :style="{ height: totalHeight + 'px' }"></div> <!-- 实际渲染的列表项 --> @@ -721,7 +824,7 @@ for (let i = 0; i < elements.length; i++) { <div v-for="item in visibleItems" :key="item.id" - class="list-item" + class="item" :style="{ height: itemHeight + 'px' }" > {{ item.name }} @@ -734,150 +837,136 @@ for (let i = 0; i < elements.length; i++) { import { ref, computed } from 'vue' const props = defineProps({ - items: Array, // 所有数据 - itemHeight: { type: Number, default: 50 } // 每项高度 + items: Array, + itemHeight: { type: Number, default: 50 } }) -const container = ref(null) const scrollTop = ref(0) +const buffer = 5 // 缓冲数量 // 可视区域能显示多少项 -const visibleCount = computed(() => { - if (!container.value) return 10 - return Math.ceil(container.value.clientHeight / props.itemHeight) -}) - -// 缓冲数量(上下各多渲染几项,避免快速滚动时白屏) -const buffer = 5 +const visibleCount = computed(() => 10) // 起始索引 -const startIndex = computed(() => { - return Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - buffer) -}) +const startIndex = computed(() => + Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - buffer) +) // 结束索引 -const endIndex = computed(() => { - return Math.min( - props.items.length, - startIndex.value + visibleCount.value + buffer * 2 - ) -}) +const endIndex = computed(() => + Math.min(props.items.length, startIndex.value + visibleCount.value + buffer * 2) +) // 当前可视的数据 -const visibleItems = computed(() => { - return props.items.slice(startIndex.value, endIndex.value) -}) +const visibleItems = computed(() => + props.items.slice(startIndex.value, endIndex.value) +) -// 总高度(用于撑起滚动条) -const totalHeight = computed(() => { - return props.items.length * props.itemHeight -}) +// 总高度 +const totalHeight = computed(() => props.items.length * props.itemHeight) -// 偏移量(让可视内容位于正确的位置) -const offsetY = computed(() => { - return startIndex.value * props.itemHeight -}) +// 偏移量 +const offsetY = computed(() => startIndex.value * props.itemHeight) -// 滚动事件处理 -const handleScroll = () => { - scrollTop.value = container.value.scrollTop +const handleScroll = (e) => { + scrollTop.value = e.target.scrollTop } </script> - -<style scoped> -.virtual-list-container { - position: relative; - height: 400px; /* 固定高度 */ - overflow-y: auto; -} - -.phantom { - position: absolute; - left: 0; - top: 0; - right: 0; - z-index: -1; -} - -.content { - position: absolute; - left: 0; - right: 0; - top: 0; -} - -.list-item { - display: flex; - align-items: center; - padding: 0 16px; - border-bottom: 1px solid #eee; -} -</style> ``` +::: -**虚拟滚动的核心优势**: +### 9.4 防抖与节流:减少事件触发频率 -| 场景 | 传统列表(10000项) | 虚拟滚动(10000项) | -|------|-------------------|-------------------| -| DOM节点数 | 10000+ | 20-30(可视区域+缓冲) | -| 内存占用 | 高(每个节点都占内存) | 低(节点数固定) | -| 初始渲染时间 | 慢(要创建所有节点) | 快(只创建少量节点) | -| 滚动性能 | 卡(大量节点参与重排/重绘) | 流畅(仅数据更新,DOM复用) | -| 实现复杂度 | 简单 | 较复杂 | +**问题**:频繁触发的事件(如scroll、resize)会导致性能问题。 -**适用场景**: -- 数据量大的列表(通常>100条就有优化价值) -- 列表项高度固定或可以预估 -- 对滚动性能有较高要求 +::: details 查看防抖与节流的实现 +```javascript +// 防抖(Debounce):延迟执行,如果在延迟时间内再次触发,则重新计时 +function debounce(fn, delay) { + let timer = null + return function (...args) { + clearTimeout(timer) + timer = setTimeout(() => fn.apply(this, args), delay) + } +} -**不适用场景**: -- 数据量很小(<50条) -- 列表项高度极不规律且无法预估 -- 需要全量DOM操作(如全选、全量导出等) +// 节流(Throttle):固定时间间隔执行 +function throttle(fn, interval) { + let lastTime = 0 + return function (...args) { + const now = Date.now() + if (now - lastTime >= interval) { + lastTime = now + fn.apply(this, args) + } + } +} + +// 使用示例 +window.addEventListener('scroll', debounce(handleScroll, 200)) +window.addEventListener('resize', throttle(handleResize, 100)) +``` +::: + +### 9.5 懒加载:延迟加载非关键资源 + +**问题**:首屏加载太多资源导致页面打开慢。 + +::: details 查看懒加载的实现 +```javascript +// 图片懒加载 +const lazyImages = document.querySelectorAll('img[data-src]') + +const imageObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const img = entry.target + img.src = img.dataset.src // 加载真实图片 + img.removeAttribute('data-src') + observer.unobserve(img) // 停止观察 + } + }) +}) + +lazyImages.forEach(img => imageObserver.observe(img)) +``` +::: --- -## 9. 总结:渲染管线优化的本质 +## 10. 总结:渲染管线优化的本质 通过本文的学习,我们可以得出以下核心结论: **从实践来看**:不是优化越多越好,而是优化越"对位"越好。理解浏览器的渲染管线,才能知道在哪里用力、在哪里放手。 **从成本视角看**: -- 大部分性能浪费来自对布局属性的**频繁读写交替**,需要通过读写分离、批量处理来解决; -- 复杂的动画效果如果触发了重排和重绘,往往源于使用了"错误的属性",需要通过`transform`和`opacity`来解决; -- 面对大量数据的列表渲染,单纯依靠虚拟DOM的diff算法已经不够,必须结合**虚拟滚动**等技术。 +- 大部分性能浪费来自对布局属性的**频繁读写交替**,需要通过读写分离、批量处理来解决 +- 复杂的动画效果如果触发了重排和重绘,往往源于使用了"错误的属性",需要通过`transform`和`opacity`来解决 +- 面对大量数据的列表渲染,单纯依靠虚拟DOM已经不够,必须结合**虚拟滚动**等技术 -目标是:在给定的浏览器和硬件条件下,让每一个渲染步骤的投入都具备明确的性能收益。 +**目标是:在给定的浏览器和硬件条件下,让每一个渲染步骤的投入都具备明确的性能收益。** --- -## 10. 名词对照表 +## 11. 名词对照表 | 英文术语 | 中文对照 | 解释 | | :--- | :--- | :--- | -| **DOM** | 文档对象模型 | Document Object Model,浏览器将HTML文档解析后形成的树形结构,JavaScript可以通过DOM API操作页面元素。 | -| **CSSOM** | CSS对象模型 | CSS Object Model,浏览器将CSS解析后形成的树形结构,与DOM结合用于计算最终样式。 | -| **Render Tree** | 渲染树 | 由DOM树和CSSOM树合并而成,只包含可见节点,用于后续的布局计算和绘制。 | -| **Layout** | 布局 | 计算渲染树中每个节点的几何信息(位置、大小)的过程,也称为Reflow(重排)。 | -| **Reflow** | 重排/回流 | 当元素的尺寸、位置等几何属性发生变化时,浏览器需要重新计算布局的过程。 | -| **Paint** | 绘制/重绘 | 将布局计算后的元素样式(颜色、背景、边框等)绘制到屏幕上的过程。 | -| **Repaint** | 重绘 | 当元素的外观属性(如颜色、背景)变化但不影响几何属性时,触发的绘制更新。 | -| **Composite** | 合成 | 将多个绘制层(Layer)合并为最终屏幕图像的过程,通常在GPU上执行。 | -| **Layer** | 层/合成层 | 浏览器为了优化渲染而创建的独立绘制表面,可以单独变换和合成。 | -| **Event Loop** | 事件循环 | JavaScript的异步执行机制,负责调度宏任务和微任务的执行。 | -| **Call Stack** | 调用栈 | 记录当前正在执行的JavaScript函数的数据结构。 | -| **Macro Task** | 宏任务 | 事件循环中优先级较低的任务类型,如setTimeout、setInterval、I/O操作等。 | -| **Micro Task** | 微任务 | 事件循环中优先级较高的任务类型,如Promise.then、MutationObserver等。 | -| **Forced Synchronous Layout** | 强制同步布局 | 在JavaScript中交替读取和写入布局属性,导致浏览器被迫立即执行布局计算的性能问题。 | -| **Layout Thrashing** | 布局抖动 | 频繁的强制同步布局导致的性能急剧下降现象。 | -| **Virtual Scrolling** | 虚拟滚动 | 只渲染视口内可见列表项的技术,用于优化大数据列表的性能。 | -| **RAF (requestAnimationFrame)** | 请求动画帧 | 浏览器提供的API,用于在下一次重绘前执行动画相关的JavaScript代码。 | -| **RAIL** | 响应、动画、空闲、加载 | Google提出的性能模型,关注响应(Response)、动画(Animation)、空闲(Idle)、加载(Load)四个维度。 | -| **Critical Rendering Path** | 关键渲染路径 | 浏览器将HTML、CSS、JavaScript转换为屏幕上像素所经历的一系列步骤。 | -| **FP (First Paint)** | 首次绘制 | 浏览器首次将像素绘制到屏幕上的时间点。 | -| **FCP (First Contentful Paint)** | 首次内容绘制 | 浏览器首次绘制来自DOM的内容(文本、图片等)的时间点。 | -| **LCP (Largest Contentful Paint)** | 最大内容绘制 | 浏览器绘制视口内最大内容元素的时间点,是Core Web Vitals指标之一。 | -| **TTI (Time to Interactive)** | 可交互时间 | 页面完全可交互(主线程空闲)的时间点。 | -| **TBT (Total Blocking Time)** | 总阻塞时间 | FCP到TTI之间,主线程被阻塞超过50ms的总时间。 | -| **CLS (Cumulative Layout Shift)** | 累积布局偏移 | 页面生命周期内发生的所有意外布局偏移的分数总和,是Core Web Vitals指标之一。 | +| **DOM** | 文档对象模型 | 浏览器将HTML文档解析后形成的树形结构,JavaScript可以通过DOM API操作页面元素 | +| **CSSOM** | CSS对象模型 | 浏览器将CSS解析后形成的树形结构,与DOM结合用于计算最终样式 | +| **Render Tree** | 渲染树 | 由DOM树和CSSOM树合并而成,只包含可见节点,用于后续的布局计算和绘制 | +| **Layout** | 布局 | 计算渲染树中每个节点的几何信息(位置、大小)的过程,也称为Reflow(重排) | +| **Reflow** | 重排/回流 | 当元素的尺寸、位置等几何属性发生变化时,浏览器需要重新计算布局的过程 | +| **Paint** | 绘制/重绘 | 将布局计算后的元素样式(颜色、背景、边框等)绘制到屏幕上的过程 | +| **Repaint** | 重绘 | 当元素的外观属性(如颜色、背景)变化但不影响几何属性时,触发的绘制更新 | +| **Composite** | 合成 | 将多个绘制层(Layer)合并为最终屏幕图像的过程,通常在GPU上执行 | +| **Layer** | 层/合成层 | 浏览器为了优化渲染而创建的独立绘制表面,可以单独变换和合成 | +| **Event Loop** | 事件循环 | JavaScript的异步执行机制,负责调度宏任务和微任务的执行 | +| **Call Stack** | 调用栈 | 记录当前正在执行的JavaScript函数的数据结构 | +| **Macro Task** | 宏任务 | 事件循环中优先级较低的任务类型,如setTimeout、setInterval、I/O操作等 | +| **Micro Task** | 微任务 | 事件循环中优先级较高的任务类型,如Promise.then、MutationObserver等 | +| **Forced Synchronous Layout** | 强制同步布局 | 在JavaScript中交替读取和写入布局属性,导致浏览器被迫立即执行布局计算的性能问题 | +| **Layout Thrashing** | 布局抖动 | 频繁的强制同步布局导致的性能急剧下降现象 | +| **Virtual Scrolling** | 虚拟滚动 | 只渲染视口内可见列表项的技术,用于优化大数据列表的性能 | +| **RAF** | 请求动画帧 | 浏览器提供的API,用于在下一次重绘前执行动画相关的JavaScript代码 | diff --git a/docs/zh-cn/appendix/cache-design.md b/docs/zh-cn/appendix/cache-design.md index 2206bdc..aba82fd 100644 --- a/docs/zh-cn/appendix/cache-design.md +++ b/docs/zh-cn/appendix/cache-design.md @@ -1,402 +1,924 @@ -# 缓存系统设计:从淘宝商品详情页看高性能架构 +# 缓存系统设计:从零到高性能架构 -> 💡 **学习指南**:为什么用户点击淘宝商品详情页只需要 50 毫秒,而直接查数据库要 500 毫秒?这背后是一个精心设计的缓存架构在发挥作用。本章节将带你深入理解缓存的本质、模式与实战技巧。 +::: tip 🎯 核心问题 +**为什么有些网站打开只需 50 毫秒,而有些却要等 5 秒?** 这就像问:为什么从书包拿书只要 1 秒,而要去图书馆找书要 10 分钟?答案就是——缓存。本章将带你深入理解缓存的核心原理、设计模式和实战技巧,让你的系统性能提升 100 倍。 +::: --- -## 0. 引言:为什么系统越来越慢? +## 1. 为什么要"缓存"? -### 0.1 一个真实的性能危机 +### 1.1 从"每次都查"到"记住常用数据"的演变 -2020 年双十一,某电商平台的数据库 CPU 使用率突然飙到 95%,订单查询响应时间从 100ms 暴涨到 8 秒。问题根源很快查明:一个新上线的促销页面,每次加载都要查询 50+ 次数据库,且没有缓存。 +在计算机世界的早期,程序员每次需要数据时都会去硬盘或数据库查询。这就像你每次做数学题都要翻书查公式,虽然准确,但效率很低。随着系统规模增大,这种"每次都查"的方式开始暴露出严重的问题:数据库 CPU 飙升到 95%,响应时间从 100 毫秒暴涨到 8 秒,最终整个系统崩溃。 -**核心问题暴露**——当流量激增时,数据库成为整个系统的瓶颈。 +这就像一个学生每天上课都要从宿舍跑到图书馆查资料,一天跑 50 次,最后累瘫在半路。解决方案很简单:在书包里放一本常用公式手册,需要时直接翻书包,不用每次都跑图书馆。缓存就是计算机系统的"公式手册",它把常用数据存储在快速访问的地方,让系统不用每次都去"图书馆"(数据库)。 -| 操作类型 | 响应时间 | 单节点 QPS | 瓶颈分析 | -| :------- | :------- | :--------- | :------- | -| L1 Cache 读取 | ~0.5 ns | 数十亿级 | CPU 内置,无瓶颈 | -| 内存读取 | ~100 ns | 百万级 | 内存带宽 | -| Redis 查询 | ~1 ms | 10万级 | 网络延迟 | -| MySQL 查询 | ~10 ms | 数千级 | 磁盘 IO | -| 跨机房查询 | ~100 ms | 百级 | 网络距离 | +<div style="display: flex; gap: 20px; margin: 20px 0;"> +<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;"> -**性能差距触目惊心**:内存操作比 MySQL 查询快 100,000 倍。 +**🐌 没有缓存** +- 每次请求都查数据库 +- 数据库 CPU 使用率 95% +- 响应时间 5-8 秒 +- 系统容易崩溃 -### 0.2 缓存的本质定义 +</div> +<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;"> -缓存不是简单地把数据存到内存,而是一种**基于局部性原理的数据访问优化技术**。 +**🚀 有缓存** +- 95% 请求直接返回 +- 数据库 CPU 使用率 < 20% +- 响应时间 50 毫秒 +- 系统稳定运行 -**严格定义**:缓存是位于计算单元与慢速存储之间的高速数据存储层,通过存储最近或频繁访问数据的副本,减少访问延迟、提升系统吞吐量。 +</div> +</div> -**与原始存储的核心区别**: -- 原始存储(数据库):容量大、持久化、查询慢 -- 缓存:容量有限、易失性、查询极快 -- 缓存内容永远是原始数据的**副本**,而非主数据 +**这就是"缓存"要解决的核心问题:通过存储常用数据的副本,减少对慢速存储(数据库)的访问,让系统更快、更稳定。** -### 0.3 淘宝商品详情页的缓存实战 +<CachePerformanceComparisonDemo /> -让我们拆解一个真实的电商商品详情页,看看缓存是如何层层发挥作用的。 +### 1.2 一个真实的踩坑故事:为什么缓存是救命稻草 -**场景**:用户打开商品 ID 为 12345 的详情页。 +你可能会想:"我的系统现在还行,为什么要提前设计缓存?"让我讲一个真实的故事,你就会明白为什么缓存不是"可选项",而是"必选项"。 -**访问链路**(从上到下,层层穿透): +::: warning 阿强的数据库崩溃记 +阿强是一个创业公司的全栈工程师,公司做了一个社交 App。早期用户少(几百人),系统运行正常,阿强觉得没必要搞缓存,直接查数据库就行。 -| 层级 | 存储位置 | 查询耗时 | 数据示例 | 命中率 | -| :--- | :------- | :------- | :------- | :----- | -| L1 | 浏览器本地缓存 | ~0ms | 静态图片、CSS、JS | 95%+ | -| L2 | CDN 边缘节点 | ~20ms | 商品主图、详情图 | 90%+ | -| L3 | Nginx 本地缓存 | ~1ms | 商品基础信息 JSON | 80%+ | -| L4 | 应用进程本地缓存 (Caffeine) | ~0.1ms | 热销商品详情 | 90%+ | -| L5 | Redis 集群 | ~2ms | 所有商品信息、库存 | 99%+ | -| L6 | MySQL 主从集群 | ~10ms | 全量商品数据 | 100% | +半年后,用户增长到 10 万人,某天有个明星在 App 上发了一条动态,瞬间涌来 10 万用户访问。结果数据库直接撑爆了:CPU 100%,响应时间从 100ms 变成 30 秒,最后整个 App 崩溃,用户大量流失。 -**最终效果**: -- 95% 的请求在前 4 层就被拦截,根本不会到达数据库 -- 整体响应时间:P99 < 100ms -- 数据库 QPS 从理论上的 100万+ 下降到实际 5000 以下 +事后复盘:如果当时有一个简单的缓存层(比如 Redis),把热门动态缓存起来,数据库压力至少能降低 95%,系统完全能撑住这次流量洪峰。 -<CacheArchitectureOverview /> +阿强从此明白了一个道理:**缓存不是锦上添花,而是高并发系统的保命符。不加缓存,就像开车不系安全带——平时没事,出事就晚了。** +::: + +::: info 💡 核心启示 +缓存的价值不只是"更快",更重要的是"保护"。它保护数据库不被压垮,保护系统在高流量下依然稳定运行。当你设计系统时,不要等到出事才想起缓存,要从一开始就把它作为核心架构的一部分。 +::: --- -## 1. 缓存的底层原理:局部性 +## 2. 核心概念:什么是缓存? -缓存之所以有效,根植于计算机科学中一个被反复验证的观察:**局部性原理**。 +::: tip 🤔 缓存到底是什么? +简单来说,**缓存就是数据副本的存储空间**。就像你在书桌前贴了一张便利贴,记着常用电话号码,这样就不需要每次都翻手机通讯录。 -### 1.1 时间局部性 (Temporal Locality) +**三个关键点**: +1. **副本**:缓存里的数据是原始数据(数据库)的副本,不是主数据 +2. **快速访问**:缓存通常在内存中,读取速度比硬盘快 10 万倍 +3. **有限容量**:缓存空间有限,只能存储最常用的数据 -**定义**:如果一个数据项被访问,那么在不久的将来它很可能再次被访问。 +所以,**缓存就是用空间换时间**——牺牲一些内存空间,换取极快的数据访问速度。 +::: -**技术解释**:程序在执行过程中,往往会对某些变量或数据对象进行反复读取或修改。这是因为: -- 循环结构会重复访问相同的计数器变量 -- 函数调用会重复访问相同的参数和局部变量 -- 热点数据(如用户会话、配置项)会被频繁查询 +在深入具体技术之前,我们需要先搞清楚几个核心概念。为了帮助你理解,我们用一个"学生的书包"来类比缓存系统。 -**实例**:用户登录后,系统需要反复查询该用户的权限信息。第一次查询后将结果缓存,后续几十次请求都可以直接从缓存获取,直到用户登出或权限变更。 +### 2.1 用"书包比喻"理解缓存的核心概念 -### 1.2 空间局部性 (Spatial Locality) +想象你是一个学生,每天需要查各种资料。这个过程和缓存系统惊人地相似: -**定义**:如果一个数据项被访问,那么与它地址相邻的数据项也很可能被访问。 +| 概念 | 🎒 书包比喻 | 技术含义 | 真实例子 | +|------|-----------|----------|----------| +| **缓存命中 (Cache Hit)** | 你要找的公式正好在便利贴上 | 请求的数据在缓存中找到 | 查询用户信息,Redis 中有,直接返回 | +| **缓存未命中 (Cache Miss)** | 便利贴上没有,得翻书 | 请求的数据不在缓存中 | 查询用户信息,Redis 中没有,需要查数据库 | +| **命中率 (Hit Ratio)** | 100 次查公式中,有 95 次在便利贴上 | 缓存命中的比例 | 命中率 95%,说明 95% 的请求不用查数据库 | +| **TTL (Time To Live)** | 便利贴写上"3 天后撕掉" | 缓存的过期时间 | 设置用户信息缓存 30 分钟后自动失效 | +| **淘汰 (Eviction)** | 书包装满了,把最旧的一张便利贴扔掉 | 缓存满时删除旧数据 | Redis 内存满了,自动删除最少使用的数据 | -**技术解释**:计算机存储和访问数据的方式天然具有连续性: -- 内存以缓存行(通常 64 字节)为单位加载数据 -- 数组、列表等数据结构在内存中连续存储 -- 磁盘读取以块(通常 4KB)为单位 -- 业务数据往往按时间、分类等维度聚簇存储 +### 2.2 缓存命中 vs 缓存未命中 -**实例**:加载商品列表时,如果缓存了第 1-10 条商品数据,当用户翻页到第 11-20 条时,这些相邻的数据很可能已经被批量加载到缓存中,实现快速响应。 +缓存命中和未命中的性能差异是巨大的。让我们看看具体的数据: -### 1.3 缓存的生命周期 +| 操作类型 | 响应时间 | 相对速度 | 适合场景 | +|---------|---------|----------|----------| +| **CPU L1 缓存** | ~0.5 纳秒 | 极快(基准) | CPU 内部运算 | +| **内存读取** | ~100 纳秒 | 快 200 倍 | 本地缓存(如 Caffeine) | +| **Redis 查询** | ~1 毫秒 | 慢 200 万倍 | 分布式缓存 | +| **MySQL 查询** | ~10 毫秒 | 慢 2000 万倍 | 硬盘数据库查询 | -一个缓存条目从创建到销毁,经历完整的生命周期: +::: tip 📊 从表格中你能看到什么? +**性能差距触目惊心**:内存操作比 MySQL 查询快 10 万倍!这就像从书桌拿书(1 秒)和去图书馆找书(10 万秒,约 28 小时)的差距。 -``` -写入 (Write) → 命中/未命中 (Hit/Miss) → 过期 (Expiration) → 淘汰 (Eviction) +**三层性能阶梯**: +1. **本地缓存(内存)**:最快,但容量小,适合热点数据 +2. **Redis 缓存**:中等速度,容量大,适合分布式场景 +3. **数据库**:最慢,但容量无限,是数据的最终来源 + +**实战启示**:你的系统应该让 95% 以上的请求在缓存层就返回,只有不到 5% 的请求需要查数据库。这样数据库压力小,系统整体性能就会大幅提升。 +::: + +::: details 🔍 看看一次"缓存命中"和"缓存未命中"的真实代码 +让我们用代码对比这两种情况: + +```javascript +// 场景:查询用户信息 + +// ===== 缓存命中 (Cache Hit) ===== +// 1. 先查 Redis 缓存 +const userFromCache = await redis.get('user:123') +if (userFromCache) { + // 命中!直接返回,耗时约 1 毫秒 + return JSON.parse(userFromCache) +} + +// ===== 缓存未命中 (Cache Miss) ===== +// 2. 缓存没有,查数据库 +const userFromDB = await db.query('SELECT * FROM users WHERE id = 123') +// 未命中!需要查数据库,耗时约 10 毫秒,慢了 10 倍 + +// 3. 查到后写入缓存,下次命中 +await redis.set('user:123', JSON.stringify(userFromDB), 'EX', 1800) +return userFromDB ``` -**写入策略**: -- **主动写入**:系统启动时预加载热点数据(缓存预热) -- **懒加载**:首次访问时从数据库加载并写入缓存 -- **异步更新**:后台线程定期刷新即将过期的数据 +**关键点**: +- 缓存命中:1 毫秒返回,用户体验极佳 +- 缓存未命中:10 毫秒返回,用户体验稍差 +- **缓存的价值**:把未命中变成命中,性能提升 10 倍 +::: -**过期策略**: -- **TTL (Time To Live)**:设置固定生存时间,到期自动失效 -- **滑动过期**:每次访问后重置过期时间 -- **绝对过期**:指定具体过期时间点 +### 2.3 缓存的生命周期 -**淘汰策略**(缓存满时): -- **LRU (Least Recently Used)**:淘汰最近最少使用(最常用) -- **LFU (Least Frequently Used)**:淘汰访问频率最低 -- **FIFO (First In First Out)**:先进先出 -- **Random**:随机淘汰 +一个缓存条目从创建到销毁,会经历完整的生命周期。理解这个过程对设计缓存系统至关重要。 + +**四个阶段**: + +**阶段一:写入 (Write)** +- **主动写入**:系统启动时,预先把热点数据加载到缓存(缓存预热) +- **懒加载**:首次访问时从数据库加载并写入缓存(最常用) + +**阶段二:命中/未命中 (Hit/Miss)** +- 每次请求都会先查缓存 +- 命中则直接返回,未命中则查数据库 + +**阶段三:过期 (Expiration)** +- **TTL (Time To Live)**:设置缓存存活时间(如 30 分钟) +- 到期后缓存自动失效,下次访问需要重新加载 + +**阶段四:淘汰 (Eviction)** +- 缓存空间有限,满了之后需要删除旧数据 +- 常见淘汰策略: + - **LRU (Least Recently Used)**:删除最久没有被使用的数据(最常用) + - **LFU (Least Frequently Used)**:删除访问频率最低的数据 + - **FIFO (First In First Out)**:删除最早写入的数据 + +👇 **动手看看**: +下面这个演示展示了缓存的生命周期。点击"新增缓存",观察缓存如何经历写入、命中、过期、淘汰的全过程: + +<CacheLifecycleDemo /> --- -## 2. 缓存架构选型 +## 3. 缓存的演进之路:从单机到分布式 -### 2.1 本地缓存 (Local Cache) +::: tip 🤔 为什么需要不同类型的缓存? +就像你学习时会在不同地方放资料:书桌上放最常用的(便利贴),书包里放常用的(笔记本),图书馆放所有资料(书库)。 -**定义**:与应用进程共享内存空间的缓存,无需网络访问。 +**缓存系统也一样**: +- **本地缓存(书桌)**:最快,容量小,放超级热点数据 +- **分布式缓存(公共储物柜)**:较快,容量大,放常用数据 +- **数据库(图书馆)**:最慢,容量无限,放所有数据 -**技术特点**: -- **访问延迟**:~100 纳秒(纯内存访问,无网络开销) -- **容量限制**:受限于单机内存(通常几百 MB 到几 GB) -- **一致性挑战**:多实例部署时,各节点缓存独立,数据可能不一致 -- **进程绑定**:应用重启,缓存丢失 +**为什么要分层?** 因为不同层次的性能和成本不同,合理组合才能达到最优效果。 +::: -**主流实现**: +讲了这么多概念,让我们看一个真实的案例:某电商系统是如何从"没有缓存"一步步进化到"多级缓存架构"的。通过这个案例,你会更直观地理解缓存设计的重要性。 -| 语言 | 库/框架 | 核心特性 | -| :--- | :------ | :------- | -| Java | Caffeine | 高性能,W-TinyLFU 淘汰算法,近乎完美的命中率 | -| Java | Guava Cache | 功能全面,但性能略逊于 Caffeine | -| Go | Ristretto | 高性能,高命中率,支持过期 | -| Go | BigCache | 专为大量 entry 设计,零 GC 压力 | -| Python | cachetools | 多种缓存策略,TTL 支持 | +### 3.1 阶段一:无缓存时代——数据库裸奔 -### 2.2 分布式缓存 (Distributed Cache) +**背景**:早期系统用户少(几百人),所有请求直接查数据库,没有任何缓存层。 -**定义**:作为独立服务部署的缓存系统,通过网络协议访问,多实例共享。 - -**技术特点**: -- **访问延迟**:~1-5 毫秒(网络开销) -- **容量扩展**:支持集群部署,容量可达数百 GB 甚至 TB -- **一致性保证**:所有应用实例访问同一份数据,天然一致 -- **高可用**:支持主从复制、哨兵、集群模式 - -**主流实现对比**: - -| 特性 | Redis | Memcached | -| :--- | :---- | :---------- | -| 数据结构 | String, Hash, List, Set, ZSet, Stream 等 | 仅 String | -| 持久化 | RDB, AOF 支持 | 不支持 | -| 集群 | Redis Cluster, Codis | 客户端分片 | -| 单线程/多线程 | 单线程(命令执行)| 多线程 | -| 内存管理 | 支持内存淘汰策略 | LRU 淘汰 | -| 适用场景 | 复杂数据结构、持久化需求 | 纯缓存、简单 KV | - -### 2.3 多级缓存架构 - -在真实的生产环境中,单一缓存层往往无法满足性能和成本的双重需求。多级缓存架构通过在不同层级部署缓存,形成层层防护,最大化性能收益。 - -**典型多级缓存架构**: +**技术栈**: +- 数据库:MySQL +- 无缓存:没有 Redis,没有本地缓存 +**系统架构**: ``` -┌─────────────────────────────────────────────────────────────────┐ -│ 用户请求 │ -└─────────────────────────┬───────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ L1: 浏览器缓存 (Browser Cache) │ -│ - 存储:静态资源 (CSS/JS/图片) │ -│ - 控制:Cache-Control, ETag, Last-Modified │ -│ - 延迟:~0ms (本地磁盘/内存) │ -└─────────────────────────┬───────────────────────────────────────┘ - ↓ (未命中) -┌─────────────────────────────────────────────────────────────────┐ -│ L2: CDN 缓存 (Content Delivery Network) │ -│ - 存储:静态资源、部分 API 响应 │ -│ - 节点:全球分布,就近访问 │ -│ - 延迟:~20-50ms │ -└─────────────────────────┬───────────────────────────────────────┘ - ↓ (未命中) -┌─────────────────────────────────────────────────────────────────┐ -│ L3: 反向代理缓存 (Nginx/Varnish) │ -│ - 存储:完整 HTTP 响应、聚合数据 │ -│ - 延迟:~1-5ms │ -└─────────────────────────┬───────────────────────────────────────┘ - ↓ (未命中) -┌─────────────────────────────────────────────────────────────────┐ -│ L4: 应用本地缓存 (Caffeine/Guava) │ -│ - 存储:热点对象、配置、用户会话 │ -│ - 延迟:~0.1ms │ -└─────────────────────────┬───────────────────────────────────────┘ - ↓ (未命中) -┌─────────────────────────────────────────────────────────────────┐ -│ L5: 分布式缓存 (Redis Cluster) │ -│ - 存储:业务数据、会话、计算结果 │ -│ - 延迟:~1-5ms │ -└─────────────────────────┬───────────────────────────────────────┘ - ↓ (未命中) -┌─────────────────────────────────────────────────────────────────┐ -│ L6: 数据库 (MySQL/PostgreSQL) │ -│ - 存储:全量数据 │ -│ - 延迟:~10-50ms │ -└─────────────────────────────────────────────────────────────────┘ +用户请求 → 应用服务器 → MySQL 数据库 ``` -<CacheHierarchyDemo /> +**这个阶段的特点**: +- ✅ **优点**:架构简单,开发快速 +- ❌ **缺点**:数据库压力大,性能差,用户量上千就崩 + +::: details 查看当时的代码和遇到的问题 +**代码示例**(每次都查数据库): + +```javascript +// 获取商品详情——每次都查数据库 +async function getProduct(productId) { + // 直接查数据库,没有任何缓存 + const product = await db.query( + 'SELECT * FROM products WHERE id = ?', + [productId] + ) + return product +} +``` + +**遇到的问题**: +1. **数据库 CPU 飙升**:每次请求都查数据库,CPU 使用率 80%+ +2. **响应慢**:复杂查询要 50-100 毫秒,用户体验差 +3. **并发能力差**:数据库 QPS(每秒查询数)上限只有 2000,再多就崩溃 +4. **热点商品问题**:热门商品详情页被频繁查询,数据库成为瓶颈 + +**当时的临时解决方案**: +- 买更贵的服务器(加 CPU、内存)——成本高,效果有限 +- 数据库读写分离 —— 能缓解读压力,但写压力依然存在 +- SQL 优化 —— 能提升 20-30%,但无法解决根本问题 +::: + +这种"裸奔"模式在用户量 < 1000 时还能应付,但随着用户增长到 1 万、10 万,数据库开始频繁崩溃,团队迫切需要引入缓存。 + +### 3.2 阶段二:引入 Redis 缓存——性能提升 10 倍 + +**背景**:用户增长到 1 万人,数据库撑不住了,团队决定引入 Redis 作为缓存层。 + +**技术栈**: +- 数据库:MySQL +- 缓存:Redis(单机版) + +**系统架构**: +``` +用户请求 → 应用服务器 → Redis 缓存(未命中才查) → MySQL 数据库 +``` + +**这个阶段的特点**: +- ✅ **优点**:性能提升 10 倍,数据库压力降低 90% +- ❌ **缺点**:Redis 单点故障,缓存和数据库可能不一致 + +::: details 查看 Redis 缓存的实现代码 +**代码示例**(增加 Redis 缓存): + +```javascript +// 获取商品详情——先查 Redis,没有再查数据库 +async function getProduct(productId) { + // 1. 先查 Redis 缓存 + const cacheKey = `product:${productId}` + const cached = await redis.get(cacheKey) + + if (cached) { + // 缓存命中!直接返回,约 1 毫秒 + return JSON.parse(cached) + } + + // 2. 缓存未命中,查数据库 + const product = await db.query( + 'SELECT * FROM products WHERE id = ?', + [productId] + ) + + // 3. 查到后写入 Redis,设置 30 分钟过期 + await redis.setex( + cacheKey, + 1800, // 30 分钟 = 1800 秒 + JSON.stringify(product) + ) + + return product +} +``` + +**性能提升对比**: + +| 场景 | 无缓存 | 有 Redis 缓存 | 提升倍数 | +|------|-------|--------------|---------| +| 普通商品查询 | 50ms | 5ms(缓存命中时) | **10 倍** | +| 热门商品查询 | 80ms | 1ms(命中率 95%) | **80 倍** | +| 数据库 QPS | 2000(满载) | 200(缓存拦截 90%) | **数据库压力降低 10 倍** | +| 系统最大并发 | 2000 用户 | 20000 用户 | **10 倍** | + +**带来的改善**: +1. **响应速度**:缓存命中时,响应时间从 50ms 降到 1-5ms +2. **并发能力**:系统能支撑的用户量从 2000 提升到 20000 +3. **数据库压力**:90% 的请求被 Redis 拦截,数据库 CPU 从 80% 降到 20% +4. **用户体验**:页面加载速度明显提升,用户投诉减少 + +**新的挑战**: +1. **缓存一致性问题**:商品价格变了,数据库更新了,但缓存还是旧的 +2. **缓存穿透**:有人恶意查询不存在的商品 ID(如 id=-1),每次都穿透到数据库 +3. **缓存雪崩**:系统重启后,所有缓存同时失效,瞬间大量请求打到数据库 +4. **Redis 单点故障**:Redis 宕机,所有请求直接打到数据库,系统可能崩溃 + +**解决方案**: +- **缓存一致性**:更新数据库时,同步删除缓存 +- **缓存穿透**:对不存在的数据也在 Redis 中缓存(value 为空,TTL 设置短一些,如 5 分钟) +- **缓存雪崩**:给缓存过期时间加随机值,避免同时失效 +::: + +引入 Redis 后,系统性能大幅提升,但新问题也随之而来。团队开始研究如何解决这些缓存相关问题。 + +### 3.3 阶段三:多级缓存架构——性能再提升 5 倍 + +**背景**:用户增长到 10 万人,即使是 Redis 缓存也开始成为瓶颈(单机 Redis QPS 上限约 10 万),团队决定引入多级缓存。 + +**技术栈**: +- L1 缓存:应用本地缓存(Caffeine) +- L2 缓存:Redis 集群 +- 数据库:MySQL 主从集群 + +**系统架构**: +``` +用户请求 → CDN 缓存(静态资源) → 应用服务器 + ↓ + L1: 本地缓存(Caffeine) → 未命中 → L2: Redis → 未命中 → MySQL +``` + +**这个阶段的特点**: +- ✅ **优点**:极致性能(本地缓存只需 0.1 毫秒),高可用(Redis 宕机不影响热点数据) +- ❌ **缺点**:架构复杂,多级缓存的一致性难以保证 + +::: details 查看多级缓存的实现代码 +**代码示例**(本地缓存 + Redis 两级缓存): + +```javascript +// 使用 Caffeine 本地缓存 +const caffeine = require('caffeine') +const localCache = new caffeine.Cache({ + max: 1000, // 最多缓存 1000 条 + ttl: 30, // 30 秒过期 +}) + +// 获取商品详情——两级缓存 +async function getProduct(productId) { + const cacheKey = `product:${productId}` + + // L1: 先查本地缓存(最快,约 0.1 毫秒) + const localCached = localCache.get(cacheKey) + if (localCached) { + console.log('L1 命中') + return localCached + } + + // L2: 本地缓存未命中,查 Redis(较快,约 1 毫秒) + const redisCached = await redis.get(cacheKey) + if (redisCached) { + console.log('L2 命中,回填 L1') + const product = JSON.parse(redisCached) + // 回填本地缓存 + localCache.set(cacheKey, product) + return product + } + + // L3: Redis 也未命中,查数据库(最慢,约 10 毫秒) + console.log('L3 命中,回填 L2 和 L1') + const product = await db.query( + 'SELECT * FROM products WHERE id = ?', + [productId] + ) + + // 回填 Redis(30 分钟过期) + await redis.setex(cacheKey, 1800, JSON.stringify(product)) + // 回填本地缓存 + localCache.set(cacheKey, product) + + return product +} +``` + +**多级缓存性能对比**: + +| 缓存层级 | 响应时间 | 命中率 | 适合存储的数据 | +|---------|---------|--------|--------------| +| **L1: 本地缓存** | ~0.1 毫秒 | 70%(超级热点) | 热门商品、系统配置、用户会话 | +| **L2: Redis 缓存** | ~1 毫秒 | 25%(一般热点) | 大部分商品数据、评论聚合 | +| **L3: 数据库** | ~10 毫秒 | 5%(冷数据) | 所有商品的全量数据 | + +**整体性能提升**: +- **平均响应时间**:5ms(阶段二) → 1ms(阶段三),**再提升 5 倍** +- **系统最大并发**:2 万用户(阶段二) → 10 万用户(阶段三),**提升 5 倍** +- **数据库 QPS**:200(阶段二) → 50(阶段三),**再降低 4 倍** + +**这个阶段解决的新问题**: +1. **本地缓存一致性**:多个应用实例的本地缓存可能不一致(A 实例缓存了旧价格,B 实例是新价格) + - **解决**:本地缓存 TTL 设置短一些(30 秒),让不一致的时间窗口变小 +2. **缓存预热**:系统重启后,本地缓存是空的,大量请求会穿透到 Redis + - **解决**:系统启动时,主动加载热点数据到本地缓存 +::: + +多级缓存架构在大型互联网公司(如淘宝、京东)广泛应用,它能支撑百万级 QPS 的访问。 + +### 3.4 缓存架构演进全景图 + +| 阶段 | 架构 | 响应时间 | 最大并发 | 核心变化 | +|------|------|---------|---------|---------| +| **阶段一:无缓存** | 应用 → 数据库 | 50ms | 2000 用户 | 数据库裸奔,性能差 | +| **阶段二:单级缓存** | 应用 → Redis → 数据库 | 5ms | 20000 用户 | 引入 Redis,性能提升 10 倍 | +| **阶段三:多级缓存** | 应用 → 本地缓存 → Redis → 数据库 | 1ms | 100000 用户 | 本地缓存 + Redis,性能再提升 5 倍 | + +::: tip 📊 从表格中你能看到什么? +**阶段一 → 阶段二**:质的飞跃。引入 Redis 后,性能提升 10 倍,数据库压力降低 90%。这是从"能用"到"够用"的关键一步。 + +**阶段二 → 阶段三**:极致优化。引入本地缓存后,性能再提升 5 倍。这是从"够用"到"极致"的进阶,适合超大流量场景。 + +**实战建议**: +- **用户量 < 1 万**:阶段一(无缓存)够用,但建议引入 Redis(阶段二) +- **用户量 1-10 万**:阶段二(Redis 缓存)是最佳选择 +- **用户量 > 10 万**:考虑阶段三(多级缓存),但要注意一致性复杂度 + +**总结一下**:缓存架构演进不只是"加更多缓存层",而是**根据流量规模选择合适的架构**——过度设计会增加复杂度,设计不足会导致性能瓶颈。 +::: --- -## 3. 缓存设计模式 +## 4. 缓存的三大经典问题:穿透、击穿、雪崩 -当引入缓存层后,应用程序需要确定如何与缓存和数据库交互。业界形成了四种经典的设计模式,每种模式在一致性、性能和复杂度之间做出不同的权衡。 +在实战中,缓存会引入三类经典问题。如果不了解它们,你的系统可能在某个时刻突然崩溃。让我们用生活化的比喻来理解这些问题。 -### 3.1 Cache-Aside (旁路缓存) — 最常用 +### 4.1 缓存穿透:查询不存在数据 -**模式定义**:应用程序负责直接管理缓存,显式地从缓存读取、写入数据。缓存对数据库完全透明,不知道数据库的存在。 +**问题定义**:查询一个**不存在的数据**(如 id=-1),缓存中没有(因为没有存过),数据库中也没有,导致每次请求都直接穿透到数据库。 -**读取流程**: -``` -应用程序 ─┬─→ 查询缓存 ──→ 命中?─┬─是→ 返回数据 - │ │ - │ └─否→ 查询数据库 - │ ↓ - │ 写入缓存 - │ ↓ - └──────────────────────── 返回数据 -``` +::: tip 🤔 用"查书"比喻缓存穿透 +想象你在图书馆查一本书,你问管理员:"有没有《不存在之书》?" -**更新流程**(关键!): -``` -应用程序 ──→ 更新数据库 ──→ 删除缓存(不是更新缓存!) -``` +**正常流程**: +- 管理员查目录:"没有这本书" +- 你离开 -### 3.2 Read-Through (读穿透) +**缓存穿透场景**: +- 你第 1 次来问,管理员查数据库:"没有",告诉你 +- 你第 2 次来问,管理员又查一遍数据库:"没有" +- 你第 100 次来问,管理员还是查数据库:"没有" -**模式定义**:应用程序只与缓存交互,缓存层负责在缺失时自动从数据库加载数据。对应用程序完全透明。 +**问题**:管理员(数据库)被烦死了,每次都要查数据库,即使答案永远是"没有"。 -**工作原理**: -``` -应用程序 ──→ 查询缓存 ──→ 未命中 ──→ 缓存服务自动加载数据库数据 - ↓ - 返回给应用 -``` +**解决**:管理员记住"《不存在之书》不存在",下次你问,直接说"没有",不用查数据库。这就是**缓存空对象**。 +::: -**优缺点对比**: - -| 维度 | Read-Through | Cache-Aside | -| :--- | :----------- | :------------ | -| 代码复杂度 | 低(业务代码无缓存逻辑) | 中(显式管理缓存) | -| 灵活性 | 低(受限于缓存库实现) | 高(完全控制) | -| 首次加载延迟 | 较高(穿透到库) | 可控(可预热) | -| 适用场景 | 标准化缓存需求 | 复杂业务逻辑 | - -### 3.3 Write-Through (写穿透) - -**模式定义**:应用程序写入缓存,缓存服务同步将数据写入数据库,确保缓存和数据库强一致。 - -**工作原理**: -``` -应用程序 ──→ 写入缓存 ──→ 缓存服务同步写入数据库 ──→ 返回成功 -``` - -**特点**: -- **强一致性**:缓存和数据库始终一致 -- **写入延迟高**:需等待数据库写入完成 -- **吞吐量受限**:受限于数据库写入能力 - -**适用场景**: -- 对数据一致性要求极高的场景(金融交易、库存扣减) -- 写操作相对较少的场景 - -<CachePatternComparisonDemo /> - -### 3.4 Write-Behind (异步写回) - -**模式定义**:应用程序只写入缓存,缓存服务异步批量将数据写入数据库。 - -**工作原理**: -``` -应用程序 ──→ 写入缓存(立即返回) - ↓ - 后台异步批量写入数据库 -``` - -**核心优势**: -- **写入极快**:~1ms 延迟,10万+ QPS -- **批量写入**:减少数据库 IO,提升吞吐量 -- **削峰填谷**:将突发流量平滑化 - -**核心风险**: -- **数据丢失风险**:缓存宕机,未写入数据库的数据丢失 -- **数据不一致窗口**:异步写入期间,缓存与数据库不一致 - ---- - -## 4. 缓存的三大经典问题 - -在实际生产环境中,缓存可能引入三类严重问题,需要系统性解决方案。 - -### 4.1 缓存穿透 (Cache Penetration) - -**问题定义**:查询一个**不存在的数据**,缓存中没有(因为没有),数据库中也没有,导致每次请求都直接打到数据库。 - -**攻击场景**: +**真实场景**: - 恶意攻击者构造大量不存在的 ID 进行查询(如 id=-1, id=999999999) -- 爬虫遍历不存在的资源路径 +- 爬虫遍历不存在的资源路径(如 /api/products/invalid-id) - 业务逻辑错误导致查询无效数据 -**解决方案 1:布隆过滤器 (Bloom Filter)** +**解决方案 1:缓存空对象** -**原理**:在缓存之前加一层概率型数据结构,快速判断"这个 key **肯定不存在**或**可能存在**"。 +```javascript +async function getProduct(productId) { + const cacheKey = `product:${productId}` -**特性**: -- 100% 判断不存在(绝对不会误判为存在) -- 可能误判存在(实际不存在,但过滤器说可能存在,概率可调) + // 1. 先查缓存 + const cached = await redis.get(cacheKey) + if (cached !== null) { + // 注意:cached 可能是字符串 "null" + if (cached === 'null') { + // 缓存的是"空对象",说明数据库中没有这个数据 + return null + } + return JSON.parse(cached) + } -### 4.2 缓存击穿 (Cache Breakdown) + // 2. 查数据库 + const product = await db.query( + 'SELECT * FROM products WHERE id = ?', + [productId] + ) -**问题定义**:某个**热点数据**在缓存中过期(TTL 到期),此时大量并发请求同时到达,都去查询数据库,导致数据库压力骤增。 + // 3. 即使数据库没有,也缓存"null",TTL 设置短一些(如 5 分钟) + if (!product) { + await redis.setex(cacheKey, 300, 'null') + return null + } -**典型场景**: -- 微博热搜榜过期瞬间 -- 明星八卦新闻缓存失效 -- 秒杀活动开始时的库存数据 -- 热门商品的缓存同时过期 + // 4. 查到数据,正常缓存 + await redis.setex(cacheKey, 1800, JSON.stringify(product)) + return product +} +``` + +**解决方案 2:布隆过滤器 (Bloom Filter)** + +布隆过滤器是一个"快速判断数据是否存在"的工具,它像一个"超级索引": + +::: tip 📖 布隆过滤器是什么? +想象你有一个"神奇的黑盒": +- 你问它:"ID 为 123 的商品存在吗?" +- 它说:"**肯定不存在**" → 那就真不存在,不用查数据库 +- 它说:"**可能存在**" → 那就去查数据库确认 + +**特点**: +- **绝对不会漏判**:如果它说不存在,那就真不存在 +- **可能误判**:如果它说可能存在,有可能实际不存在(概率很低,可调) + +**价值**:布隆过滤器能在查缓存之前,就把 99% 的"不存在"请求拦截掉,保护数据库。 +::: + +```javascript +// 使用布隆过滤器 +const { BloomFilter } = require('bloom-filters') + +// 初始化布隆过滤器(假设最多有 100 万个商品 ID) +const bloomFilter = new BloomFilter(1000000, 0.01) // 误判率 1% + +// 系统启动时,把所有商品 ID 加入布隆过滤器 +async function initBloomFilter() { + const allIds = await db.query('SELECT id FROM products') + allIds.forEach(row => { + bloomFilter.add(row.id) + }) +} + +// 查询商品前,先用布隆过滤器判断 +async function getProduct(productId) { + // 1. 先用布隆过滤器判断 + if (!bloomFilter.has(productId)) { + // 肯定不存在,直接返回 null,不用查数据库 + console.log('布隆过滤器拦截:商品不存在') + return null + } + + // 2. 布隆过滤器说"可能存在",查缓存 + const cached = await redis.get(`product:${productId}`) + if (cached) { + return JSON.parse(cached) + } + + // 3. 缓存未命中,查数据库 + const product = await db.query( + 'SELECT * FROM products WHERE id = ?', + [productId] + ) + + if (!product) { + // 布隆过滤器误判(概率很低),实际不存在 + await redis.setex(`product:${productId}`, 300, 'null') + return null + } + + // 4. 查到数据,写入缓存 + await redis.setex(`product:${productId}`, 1800, JSON.stringify(product)) + return product +} +``` + +### 4.2 缓存击穿:热点数据过期 + +**问题定义**:某个**热点数据**(如热门商品、热搜新闻)在缓存中过期(TTL 到期),此时大量并发请求同时到达,都去查询数据库,导致数据库压力骤增。 + +::: tip 🤔 用"抢书"比喻缓存击穿 +想象图书馆有本《哈利波特》,超热门,100 个人都想借。 + +**正常情况**: +- 图书馆把《哈利波特》放在"借阅台"(缓存) +- 大家直接从借阅台拿,不用去书架找 + +**缓存击穿场景**: +- 借阅台的《哈利波特》到期了(被还回书架) +- 100 个人同时来借,发现借阅台没有 +- 100 个人都冲去书架找(数据库) +- 书架管理员(数据库)被挤爆了 + +**问题**:不是"不存在的书",而是"超热门的书"突然从缓存消失了,导致瞬间大量请求打到数据库。 +::: + +**真实场景**: +- 微博热搜榜过期瞬间,几万人同时访问 +- 明星八卦新闻缓存失效,粉丝疯狂访问 +- 秒杀活动开始时的库存数据过期 **解决方案 1:互斥锁 (Mutex Lock)** -**原理**:当缓存失效时,只允许一个线程去查询数据库并重建缓存,其他线程等待或重试。 +```javascript +async function getProduct(productId) { + const cacheKey = `product:${productId}` + + // 1. 先查缓存 + const cached = await redis.get(cacheKey) + if (cached) { + return JSON.parse(cached) + } + + // 2. 缓存未命中,获取分布式锁 + const lockKey = `lock:${productId}` + const lock = await redis.set(lockKey, '1', 'NX', 'EX', 10) // 锁 10 秒 + + if (lock === 'OK') { + // 3. 获取到锁,查数据库 + console.log('获取锁成功,查询数据库') + const product = await db.query( + 'SELECT * FROM products WHERE id = ?', + [productId] + ) + + // 4. 写入缓存 + await redis.setex(cacheKey, 1800, JSON.stringify(product)) + + // 5. 释放锁 + await redis.del(lockKey) + return product + } else { + // 6. 没获取到锁,等待 50ms 后重试 + console.log('获取锁失败,等待后重试') + await new Promise(resolve => setTimeout(resolve, 50)) + return getProduct(productId) // 递归重试 + } +} +``` **解决方案 2:逻辑过期 (Logical Expiration)** -**原理**:不设置物理过期时间(TTL),而是在缓存 value 中嵌入逻辑过期时间字段。发现逻辑过期时,异步重建缓存,同时返回旧数据(永不过期)。 +```javascript +async function getProduct(productId) { + const cacheKey = `product:${productId}` -### 4.3 缓存雪崩 (Cache Avalanche) + // 1. 查缓存 + const cached = await redis.get(cacheKey) + if (cached) { + const data = JSON.parse(cached) + + // 2. 检查逻辑过期时间 + if (Date.now() < data.expireTime) { + // 未过期,直接返回 + return data.product + } else { + // 3. 逻辑过期,异步重建缓存,同时返回旧数据 + console.log('逻辑过期,异步重建缓存') + rebuildCacheAsync(productId) // 异步重建 + return data.product // 返回旧数据 + } + } + + // 4. 缓存不存在(首次加载),同步查数据库 + const product = await db.query( + 'SELECT * FROM products WHERE id = ?', + [productId] + ) + + // 5. 写入缓存(包含逻辑过期时间) + const cacheData = { + product: product, + expireTime: Date.now() + 30 * 60 * 1000 // 30 分钟后逻辑过期 + } + await redis.set(cacheKey, JSON.stringify(cacheData)) + + return product +} + +// 异步重建缓存 +async function rebuildCacheAsync(productId) { + const lockKey = `rebuild:${productId}` + const lock = await redis.set(lockKey, '1', 'NX', 'EX', 10) + + if (lock === 'OK') { + console.log('异步重建缓存开始') + const product = await db.query( + 'SELECT * FROM products WHERE id = ?', + [productId] + ) + + const cacheData = { + product: product, + expireTime: Date.now() + 30 * 60 * 1000 + } + await redis.set(`product:${productId}`, JSON.stringify(cacheData)) + await redis.del(lockKey) + console.log('异步重建缓存完成') + } +} +``` + +### 4.3 缓存雪崩:大量数据同时过期 **问题定义**:大量缓存数据在**同一时间点集中过期**(或 Redis 宕机),导致所有请求同时穿透到数据库,瞬间压垮数据库。 -**典型触发场景**: -- 系统重启后,所有缓存从 0 开始重建,同时设置相同 TTL +::: tip 🤔 用"图书馆批量还书"比喻缓存雪崩 +想象图书馆的"借阅台"(缓存)有 1000 本书。 + +**正常情况**: +- 这些书的还书时间是分散的:有的今天还,有的明天还,有的后天还 +- 每天只有几十本书到期,管理员(数据库)能轻松处理 + +**缓存雪崩场景**: +- 系统重启后,管理员把 1000 本书都设置"30 天后到期" +- 30 天后,这 1000 本书同时到期 +- 1000 个人同时来借书,发现借阅台没有 +- 1000 个人都冲去书架找 +- 书架管理员(数据库)瞬间被挤爆 + +**问题**:不是一本书的问题,而是**大量数据同时过期**,导致数据库瞬间压力暴增。 +::: + +**真实场景**: +- 系统重启后,所有缓存从 0 开始重建,同时设置相同 TTL(如 30 分钟) - 定时任务批量刷新缓存,设置相同的过期时间 - 缓存服务(Redis)宕机或网络分区 -- 大量热点数据同时达到过期时间 -**解决方案 1:随机 TTL (Time To Live)** +**解决方案 1:随机 TTL** -**原理**:在基础过期时间上增加随机偏移量,打散过期时间点。 +```javascript +async function getProduct(productId) { + const cacheKey = `product:${productId}` + + const cached = await redis.get(cacheKey) + if (cached) { + return JSON.parse(cached) + } + + const product = await db.query( + 'SELECT * FROM products WHERE id = ?', + [productId] + ) + + // 关键:在基础 TTL(30 分钟)上加随机值(±5 分钟) + const baseTTL = 1800 // 30 分钟 + const randomOffset = Math.floor(Math.random() * 600) - 300 // -5 到 +5 分钟 + const finalTTL = baseTTL + randomOffset + + console.log(`缓存 TTL: ${finalTTL} 秒(${Math.floor(finalTTL / 60)} 分钟)`) + await redis.setex(cacheKey, finalTTL, JSON.stringify(product)) + + return product +} +``` **解决方案 2:缓存预热 (Cache Preheating)** -**原理**:系统启动或定时任务主动将热点数据加载到缓存,避免冷启动时集中回源。 +```javascript +// 系统启动时,主动加载热点数据到缓存 +async function cacheWarmup() { + console.log('开始缓存预热...') + + // 1. 查询最热门的 1000 个商品(根据访问量排序) + const hotProducts = await db.query(` + SELECT * FROM products + ORDER BY view_count DESC + LIMIT 1000 + `) + + // 2. 批量写入 Redis + for (const product of hotProducts) { + const cacheKey = `product:${product.id}` + const ttl = 1800 + Math.floor(Math.random() * 600) // 30 分钟 ± 5 分钟 + await redis.setex(cacheKey, ttl, JSON.stringify(product)) + } + + console.log(`缓存预热完成,已加载 ${hotProducts.length} 个热门商品`) +} + +// 应用启动时执行 +cacheWarmup() +``` **解决方案 3:熔断降级 (Circuit Breaker)** -**原理**:当数据库压力过大时,暂时拒绝部分请求或返回降级数据,保护数据库不被压垮。 +```javascript +// 使用熔断器保护数据库 +const CircuitBreaker = require('opossum') + +// 设置熔断器 +const dbQueryBreaker = new CircuitBreaker( + async (productId) => { + return await db.query('SELECT * FROM products WHERE id = ?', [productId]) + }, + { + timeout: 3000, // 3 秒超时 + errorThresholdPercentage: 50, // 错误率超过 50% 时熔断 + resetTimeout: 30000 // 30 秒后尝试恢复 + } +) + +// 熔断后的降级处理 +dbQueryBreaker.fallback(() => { + console.log('数据库熔断,返回降级数据') + return { + id: productId, + name: '服务繁忙,请稍后重试', + status: 'degraded' + } +}) + +async function getProduct(productId) { + const cacheKey = `product:${productId}` + + const cached = await redis.get(cacheKey) + if (cached) { + return JSON.parse(cached) + } + + // 通过熔断器查数据库 + const product = await dbQueryBreaker.fire(productId) + + if (product.status === 'degraded') { + return product // 返回降级数据 + } + + await redis.setex(cacheKey, 1800, JSON.stringify(product)) + return product +} +``` + +👇 **动手看看**: +下面这个演示对比了缓存穿透、击穿、雪崩三种问题的场景和解决方案: + +<CacheProblemsDemo /> --- -## 5. 缓存一致性策略 +## 5. 缓存一致性策略:如何让缓存和数据库保持同步 -缓存的本质是数据的副本,副本与主数据(数据库)之间必然存在不一致的时间窗口。如何控制这个时间窗口,是缓存设计的核心挑战。 +缓存的本质是数据的副本,副本和原始数据(数据库)之间必然存在不一致的时间窗口。如何控制这个时间窗口,是缓存设计的核心挑战。 -### 5.1 为什么不一致? +### 5.1 为什么缓存和数据库会不一致? -**并发写入场景**: +::: tip 🤔 用"便利贴和书"比喻不一致 +想象你在便利贴上记着:"小明电话:123456",这是你通讯录(数据库)的副本。 -| 时间 | 线程 A(更新 age=25) | 线程 B(查询) | 数据库 | 缓存 | -| :--- | :-------------------- | :------------- | :----- | :--- | -| T1 | 更新 DB age=25 | - | 25 | 20(旧值) | -| T2 | - | 查询缓存(命中旧值) | 25 | 20 ❌ | -| T3 | 删除缓存 | - | 25 | - | -| T4 | - | - | 25 | 从 DB 加载 25 ✅ | +**不一致的场景**: +- 你更新通讯录,把小明电话改成 "7654321" +- 但你忘记更新便利贴 +- 下次你查电话,看便利贴,还是旧的 "123456" -**问题**:在 T2 时刻,线程 B 读取到了缓存中的旧值 20,而此时数据库已经是 25。 +**问题**:便利贴(缓存)和通讯录(数据库)不一致了。 + +**原因**:更新了原始数据,但没有同步更新副本。在计算机系统中,这是因为"更新数据库"和"更新缓存"是两个独立的操作,中间有时间窗口,可能被其他操作打乱。 +::: + +**真实的并发场景**: + +| 时间 | 线程 A(更新用户年龄) | 线程 B(查询用户) | 数据库 | 缓存 | +|------|---------------------|------------------|--------|------| +| T1 | 开始更新数据库 | - | age=20 | age=20 | +| T2 | 数据库更新为 age=25 | 查询缓存,命中 age=20 | age=25 | age=20 ❌ | +| T3 | 删除缓存 | - | age=25 | - | +| T4 | - | - | age=25 | 从 DB 加载 age=25 ✅ | + +**问题**:在 T2 时刻,线程 B 读到了缓存中的旧值 20,而数据库已经是 25。这就是**缓存不一致**。 ### 5.2 最佳实践:先更新数据库,再删除缓存 -**原理**:利用数据库的行锁机制,保证"更新"操作的排他性,最大限度减少不一致窗口。 +::: tip 🤔 为什么是"删除"而不是"更新"缓存? +你可能会想:为什么不直接"更新缓存",而是"删除缓存"? -**流程**: +**更新缓存的问题**: +- 并发更新时,可能出现 A 线程先更新缓存,B 线程后更新数据库但缓存没更新 +- 更新缓存的成本可能很高(比如需要聚合多个表的数据) +- 如果更新后数据又被删除了,白费力气 -``` -写请求 ──→ 开启数据库事务 ──→ 更新数据库记录 ──→ 提交事务 ──→ 删除缓存 +**删除缓存的优势**: +- 下次查询时自动从数据库加载最新数据(懒加载) +- 避免并发更新导致的脏数据 +- 简单可靠,是业界最佳实践 +::: + +**标准流程**: + +```javascript +// 更新商品信息 +async function updateProduct(productId, updateData) { + // 1. 先更新数据库 + await db.query( + 'UPDATE products SET name = ?, price = ? WHERE id = ?', + [updateData.name, updateData.price, productId] + ) + + // 2. 再删除缓存(不是更新缓存!) + await redis.del(`product:${productId}`) + + // 3. 下次查询时,缓存未命中,自动从数据库加载最新数据 + console.log('更新完成,缓存已删除') +} ``` -**为什么这个顺序最优?** +::: details 查看为什么"先更新 DB,再删缓存"是最优方案 +对比三种更新策略: -1. **数据库锁保护**:更新操作会获取行锁,其他读写操作必须等待,天然排他 -2. **删除缓存是异步操作**:不需要等待缓存删除成功,数据库提交后即可返回 -3. **极端情况仍可接受**:即使缓存删除失败,只是下次读取会回源,不会导致脏数据长期存在 +**策略 1:先更新缓存,再更新数据库** ❌ 不推荐 +```javascript +// 问题:如果更新数据库失败,缓存是新值,数据库是旧值,不一致 +await redis.set('product:1', newProduct) // 缓存更新成功 +await db.query('UPDATE products SET ...') // 数据库更新失败! +// 结果:缓存是新值,数据库是旧值,永久不一致! +``` -### 5.3 延迟双删 (Delayed Double Deletion) +**策略 2:先删除缓存,再更新数据库** ❌ 不推荐 +```javascript +// 问题:删除和更新之间,有其他线程查询,会加载旧数据到缓存 +await redis.del('product:1') // 缓存删除 +// 此时线程 B 来查询,发现缓存没有,查数据库(还是旧值),写入缓存 +await db.query('UPDATE products SET ...') // 更新数据库 +// 结果:缓存是旧值,数据库是新值,不一致! +``` -**场景**:先删缓存、再写 DB 的方案在极端并发下仍有不一致风险。延迟双删通过两次删除,最大限度保证一致性。 +**策略 3:先更新数据库,再删除缓存** ✅ 推荐 +```javascript +// 优点:数据库更新时加行锁,其他线程必须等待,避免脏数据 +await db.query('UPDATE products SET ...') // 更新数据库(加行锁) +await redis.del('product:1') // 删除缓存 +// 即使删除缓存失败,只是下次查询会回源,不会导致脏数据长期存在 +``` + +**为什么策略 3 最优?** +1. **数据库锁保护**:更新操作会获取行锁,其他读写操作必须等待 +2. **删除失败影响小**:即使删除缓存失败,只是下次读取会回源,不会导致脏数据 +3. **简单可靠**:不需要额外的复杂逻辑 +::: + +### 5.3 延迟双删:极端场景的一致性保障 + +**场景**:在高并发场景下,即使是"先更新 DB,再删缓存",仍有极小概率出现不一致。延迟双删通过两次删除,最大限度保证一致性。 **流程**: - ``` 1. 删除缓存 2. 更新数据库 @@ -404,188 +926,167 @@ 4. 再次删除缓存 ``` +```javascript +async function updateProduct(productId, updateData) { + const cacheKey = `product:${productId}` + + // 1. 第一次删除缓存 + await redis.del(cacheKey) + + // 2. 更新数据库 + await db.query( + 'UPDATE products SET name = ?, price = ? WHERE id = ?', + [updateData.name, updateData.price, productId] + ) + + // 3. 等待 500ms(让其他线程的查询完成) + await new Promise(resolve => setTimeout(resolve, 500)) + + // 4. 第二次删除缓存(删除可能被其他线程加载的旧数据) + await redis.del(cacheKey) + + console.log('延迟双删完成,数据已同步') +} +``` + **三种一致性策略对比**: | 策略 | 一致性级别 | 性能影响 | 复杂度 | 适用场景 | -| :--- | :--------- | :--------- | :------- | :------- | -| 先更新 DB,再删缓存 | 最终一致 | 低 | 低 | 大多数场景,推荐作为默认方案 | -| 延迟双删 | 强最终一致 | 中(延迟) | 中 | 对一致性要求较高的场景 | -| 先删缓存,再更新 DB | 弱 | 低 | 低 | 不推荐,易出现不一致 | +|------|-----------|---------|--------|---------| +| **先更新 DB,再删缓存** | 最终一致(不一致窗口 < 100ms) | 低 | 低 | 大多数场景,推荐作为默认方案 | +| **延迟双删** | 强最终一致(不一致窗口 < 10ms) | 中(延迟 500ms) | 中 | 对一致性要求较高的场景(如金融、库存) | +| **先删缓存,再更新 DB** | 弱(不一致窗口大) | 低 | 低 | ❌ 不推荐,易出现不一致 | + +👇 **动手看看**: +下面这个演示对比了三种一致性策略的效果。点击"更新数据",观察缓存和数据库的一致性变化: + +<CacheConsistencyDemo /> --- -## 6. 实战:构建高性能缓存系统 +## 6. 实战:构建一个完整的缓存系统 -### 6.1 场景分析:电商商品详情页 +讲了这么多原理,让我们看一个真实案例:如何为一个电商商品详情页设计完整的缓存系统。 -**业务特点**: -- 读多写少:100:1 的读写比 -- 热点集中:20% 的商品贡献 80% 的流量 -- 数据复杂度:商品基础信息 + 价格 + 库存 + 评价聚合 -- 一致性要求:价格、库存强一致,其他可最终一致 +### 6.1 业务场景分析 + +**需求**:用户访问商品详情页,需要展示商品基础信息、价格、库存、评价等数据。 + +**特点**: +- **读多写少**:100 次查询,1 次更新(读写比 100:1) +- **热点集中**:20% 的商品贡献 80% 的流量 +- **数据复杂**:商品基础信息 + 价格 + 库存 + 评价聚合 +- **一致性要求**:价格、库存强一致,其他可最终一致 **性能指标**: -- P99 响应时间 < 100ms +- P99 响应时间 < 100ms(99% 的请求在 100ms 内返回) - 数据库 QPS 峰值 < 5000 - 缓存命中率 > 95% -<CacheProblemsDemo /> - ### 6.2 架构设计 +**多级缓存架构**: + ``` -┌─────────────────────────────────────────────────────────┐ -│ 客户端请求 │ -└───────────────────────┬─────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────┐ -│ CDN 缓存层 │ -│ - 商品主图、详情图 │ -│ - Cache-Control: max-age=86400 │ -└───────────────────────┬─────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────┐ -│ Nginx 缓存层 │ -│ - 商品基础信息聚合(包含价格、库存) │ -│ - proxy_cache_valid 5m │ -└───────────────────────┬─────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────┐ -│ 应用层 │ -│ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ 本地缓存 │ │ 分布式锁 │ │ -│ │ (Caffeine) │ │ (Redisson) │ │ -│ │ - 热点商品 │ │ - 防击穿 │ │ -│ │ - 配置信息 │ │ - 库存扣减 │ │ -│ └─────────────────┘ └─────────────────┘ │ -└───────────────────────┬─────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────┐ -│ Redis 集群 │ -│ - 商品详情 Hash │ -│ - 库存计数器 String │ -│ - 热点商品列表 ZSet │ -│ - 分布式锁 │ -└───────────────────────┬─────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────┐ -│ MySQL 集群 │ -│ - 商品主表 │ -│ - 库存表(行锁控制并发) │ -│ - 订单表 │ -└─────────────────────────────────────────────────────────┘ +用户请求 + ↓ +CDN 缓存(静态资源:图片、CSS、JS) + ↓ 未命中 +Nginx 本地缓存(商品基础信息聚合) + ↓ 未命中 +应用服务器 + ↓ + ├─ L1: 本地缓存(Caffeine,热点商品) + │ ↓ 未命中 + ├─ L2: Redis 缓存(所有商品数据) + │ ↓ 未命中 + └─ L3: MySQL 数据库(全量数据) ``` ### 6.3 核心代码实现 -**完整的多级缓存实现(Java + Spring Boot)**: +**完整的多级缓存实现(简化版)**: -```java -@Component -public class MultiLevelCache { +```javascript +const caffeine = require('caffeine') - // 本地缓存:Caffeine - private final Cache<String, Object> localCache; +// L1: 本地缓存(30 秒过期) +const localCache = new caffeine.Cache({ + max: 1000, + ttl: 30, +}) - @Autowired - private StringRedisTemplate redisTemplate; +// 获取商品详情(多级缓存) +async function getProduct(productId) { + const cacheKey = `product:${productId}` - @Autowired - private RedissonClient redissonClient; + // L1: 本地缓存(约 0.1 毫秒) + const localCached = localCache.get(cacheKey) + if (localCached) { + console.log('L1 命中') + return localCached + } - // 缓存层级配置 - private static final long LOCAL_TTL_SECONDS = 30; - private static final long REDIS_TTL_SECONDS = 300; + // L2: Redis 缓存(约 1 毫秒) + const redisCached = await redis.get(cacheKey) + if (redisCached) { + console.log('L2 命中,回填 L1') + const product = JSON.parse(redisCached) + localCache.set(cacheKey, product) + return product + } - public MultiLevelCache() { - this.localCache = Caffeine.newBuilder() - .maximumSize(10_000) - .expireAfterWrite(Duration.ofSeconds(LOCAL_TTL_SECONDS)) - .recordStats() - .build(); + // L3: 数据库(约 10 毫秒,带分布式锁防击穿) + const lockKey = `lock:${productId}` + const lock = await redis.set(lockKey, '1', 'NX', 'EX', 10) + + if (lock === 'OK') { + console.log('L3 命中,查询数据库') + const product = await db.query( + 'SELECT * FROM products WHERE id = ?', + [productId] + ) + + if (product) { + // 写入 Redis(30 分钟 + 随机 TTL) + const ttl = 1800 + Math.floor(Math.random() * 600) - 300 + await redis.setex(cacheKey, ttl, JSON.stringify(product)) + // 回填本地缓存 + localCache.set(cacheKey, product) } - /** - * 多级缓存读取(L1: 本地缓存 -> L2: Redis -> L3: 数据库) - */ - public <T> T get(String key, Class<T> type, Supplier<T> dbLoader) { - // L1: 本地缓存 - Object localValue = localCache.getIfPresent(key); - if (localValue != null) { - return type.cast(localValue); - } + await redis.del(lockKey) + return product + } else { + // 获取锁失败,等待后重试 + await new Promise(resolve => setTimeout(resolve, 50)) + return getProduct(productId) + } +} - // L2: Redis 缓存(带分布式锁防击穿) - String redisKey = "cache:" + key; - String redisValue = redisTemplate.opsForValue().get(redisKey); +// 更新商品信息(先更新 DB,再删除缓存) +async function updateProduct(productId, updateData) { + const cacheKey = `product:${productId}` - if (StrUtil.isNotBlank(redisValue)) { - T value = JSON.parseObject(redisValue, type); - // 回填本地缓存 - localCache.put(key, value); - return value; - } + // 1. 更新数据库 + await db.query( + 'UPDATE products SET name = ?, price = ? WHERE id = ?', + [updateData.name, updateData.price, productId] + ) - // L3: 数据库(带分布式锁) - String lockKey = "lock:" + key; - RLock lock = redissonClient.getLock(lockKey); + // 2. 删除本地缓存 + localCache.del(cacheKey) - try { - boolean acquired = lock.tryLock(100, 10, TimeUnit.MILLISECONDS); - if (!acquired) { - // 获取锁失败,等待后重试 - Thread.sleep(50); - return get(key, type, dbLoader); - } + // 3. 删除 Redis 缓存 + await redis.del(cacheKey) - // 双重检查 - String doubleCheck = redisTemplate.opsForValue().get(redisKey); - if (StrUtil.isNotBlank(doubleCheck)) { - return JSON.parseObject(doubleCheck, type); - } - - // 查询数据库 - T value = dbLoader.get(); - - if (value != null) { - // 写入 Redis(带随机 TTL) - long ttl = REDIS_TTL_SECONDS + RandomUtil.randomInt(-30, 30); - redisTemplate.opsForValue().set( - redisKey, - JSON.toJSONString(value), - ttl, - TimeUnit.SECONDS - ); - - // 回填本地缓存 - localCache.put(key, value); - } - - return value; - - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("获取锁被中断", e); - } finally { - if (lock.isHeldByCurrentThread()) { - lock.unlock(); - } - } - } - - /** - * 删除缓存(更新时调用) - */ - public void evict(String key) { - // 删除本地缓存 - localCache.invalidate(key); - - // 删除 Redis 缓存 - redisTemplate.delete("cache:" + key); - } + console.log('更新完成,缓存已删除') } ``` +👇 **动手看看**: +下面这个演示展示了多级缓存系统的完整工作流程。点击"查询商品",观察请求如何在各级缓存中流转: + <EcommerceCacheArchitectureDemo /> --- @@ -594,88 +1095,47 @@ public class MultiLevelCache { ### 7.1 核心知识点回顾 -| 知识点 | 关键概念 | 实战要点 | -| :----- | :------- | :------- | -| **局部性原理** | 时间局部性、空间局部性 | 根据访问模式设计缓存 key 和预热策略 | -| **多级缓存** | L1-L6 层级 | 浏览器 → CDN → Nginx → 本地 → Redis → DB | -| **缓存模式** | Cache-Aside、Read-Through、Write-Through、Write-Behind | Cache-Aside 最常用,Write-Behind 用于高并发写入 | -| **缓存穿透** | 查询不存在数据 | 布隆过滤器 + 缓存空对象 | -| **缓存击穿** | 热点数据过期 | 互斥锁 + 逻辑过期 | -| **缓存雪崩** | 大量数据同时过期 | 随机 TTL + 缓存预热 + 熔断降级 | -| **一致性策略** | 先更新 DB 再删缓存、延迟双删 | Cache-Aside + 延迟双删应对极端并发 | +| 知识点 | 一句话解释 | 解决的问题 | 实战要点 | +|--------|-----------|-----------|----------| +| **缓存命中** | 数据在缓存中找到 | 性能提升 10-100 倍 | 命中率目标 > 95% | +| **缓存穿透** | 查询不存在数据,每次都查数据库 | 数据库被恶意查询拖垮 | 布隆过滤器 + 缓存空对象 | +| **缓存击穿** | 热点数据过期,大量请求打到数据库 | 数据库瞬间压力暴增 | 互斥锁 + 逻辑过期 | +| **缓存雪崩** | 大量数据同时过期 | 数据库被压垮 | 随机 TTL + 缓存预热 | +| **多级缓存** | 本地缓存 + Redis + 数据库 | 性能极致优化 | L1 本地缓存命中率 70%,L2 Redis 命中率 25% | +| **缓存一致性** | 缓存和数据库同步 | 数据准确性 | 先更新 DB,再删除缓存 | +| **延迟双删** | 更新前后各删除一次缓存 | 极端场景的一致性 | 等待 500ms 后再删除 | ### 7.2 学习路径建议 **阶段 1:理解原理(1-2 天)** -- 掌握局部性原理(时间、空间) -- 理解缓存生命周期(写入 → 命中 → 过期 → 淘汰) -- 了解不同存储介质的性能差异(CPU Cache → 内存 → SSD → HDD) +- 掌握缓存的本质(数据副本,用空间换时间) +- 理解缓存命中率、TTL、淘汰等核心概念 +- 了解不同存储介质的性能差异(内存 vs 硬盘) -**阶段 2:掌握基础模式(2-3 天)** -- 实现 Cache-Aside 模式(读取、更新) -- 使用 Redis 做分布式缓存 +**阶段 2:掌握基础(2-3 天)** +- 学会使用 Redis 做缓存(SET、GET、SETEX 命令) +- 实现简单的缓存读写逻辑(先查缓存,未命中再查数据库) - 理解为什么"更新时删除缓存而不是更新缓存" -**阶段 3:多级缓存实战(3-5 天)** -- 实现本地缓存(Caffeine/Guava)+ Redis 的两级架构 -- 解决多级缓存的一致性问题 -- 实现缓存统计和监控 +**阶段 3:解决经典问题(1 周)** +- 解决缓存穿透:实现布隆过滤器或缓存空对象 +- 解决缓存击穿:实现互斥锁或逻辑过期 +- 解决缓存雪崩:实现随机 TTL 和缓存预热 -**阶段 4:解决经典问题(1 周)** -- 缓存穿透:实现布隆过滤器 -- 缓存击穿:实现互斥锁和逻辑过期 -- 缓存雪崩:实现随机 TTL、缓存预热、熔断降级 +**阶段 4:多级缓存(1-2 周)** +- 引入本地缓存(Caffeine/Guava) +- 设计本地缓存 + Redis 的两级架构 +- 处理多级缓存的一致性问题 -**阶段 5:一致性保障(1-2 周)** -- 深入理解 Cache-Aside 模式的并发问题 -- 实现延迟双删 -- 了解 Binlog 订阅方案(Canal/Debezium) - -**阶段 6:生产级实战(持续)** +**阶段 5:生产级实战(持续)** - 设计完整的商品详情页缓存系统 -- 搭建 Prometheus + Grafana 监控体系 +- 搭建监控(缓存命中率、响应时间) - 进行压测验证和性能调优 -### 7.3 推荐资源 +::: info 💡 写在最后 +缓存是高并发系统的基石。从淘宝的商品详情页到微博的热搜榜,从微信的朋友圈到抖音的视频流,所有高性能系统背后都有一套精心设计的缓存架构。 -**书籍**: -- 《Redis 设计与实现》(黄健宏)—— 深入理解 Redis 内部机制 -- 《高性能 MySQL》(第 5 章:缓存策略) +理解缓存,不只是学会一个技术,更是理解**用空间换时间、用副本保护主数据**的架构思想。当你真正掌握缓存,你的系统性能将从"能用"跨越到"好用",最终达到"极致"。 -**文章**: -- Martin Fowler: *Patterns of Distributed Systems* -- Redis 官方文档:https://redis.io/docs/ -- Google: *Designing a Cache System* - -**开源项目**: -- Caffeine(Java 本地缓存) -- Redisson(Java Redis 客户端,提供分布式锁等) -- JetCache(阿里开源的多级缓存框架) - -<CacheMonitoringDashboardDemo /> - ---- - -## 8. 名词速查表 (Glossary) - -| 名词 | 全称 | 解释 | -| :--- | :--- | :--- | -| **Cache** | - | **缓存**。存储数据副本的快速存储层,用于加速访问。 | -| **Hit** | - | **缓存命中**。请求的数据在缓存中找到,无需访问数据库。 | -| **Miss** | - | **缓存未命中**。请求的数据不在缓存中,需要回源查询。 | -| **Hit Ratio** | - | **命中率**。缓存命中的请求数占总请求数的比例(目标: > 95%)。 | -| **TTL** | Time To Live | **生存时间**。缓存条目的过期时间。 | -| **Eviction** | - | **淘汰**。缓存满了时,删除旧数据为新数据腾空间。 | -| **LRU** | Least Recently Used | **最近最少使用**。常见的缓存淘汰策略。 | -| **LFU** | Least Frequently Used | **最不经常使用**。按访问频率淘汰的策略。 | -| **Cache Penetration** | - | **缓存穿透**。查询不存在数据,导致请求直接打到数据库。 | -| **Cache Breakdown** | - | **缓存击穿**。热点数据过期,瞬间大量请求打到数据库。 | -| **Cache Avalanche** | - | **缓存雪崩**。大量缓存同时过期,数据库压力骤增。 | -| **Bloom Filter** | - | **布隆过滤器**。空间效率高的概率型数据结构,用于判断元素是否可能存在。 | -| **Cache-Aside** | - | **旁路缓存**。应用代码直接操作缓存和数据库的模式。 | -| **Read-Through** | - | **读穿透**。缓存库自动从数据库加载数据。 | -| **Write-Through** | - | **写穿透**。写入缓存时同步写入数据库。 | -| **Write-Behind** | - | **异步写回**。写入缓存后异步批量写数据库。 | -| **Local Cache** | - | **本地缓存**。与应用在同一进程内的缓存(如 Caffeine)。 | -| **Distributed Cache** | - | **分布式缓存**。独立服务,通过网络访问(如 Redis)。 | -| **Consistent Hashing** | - | **一致性哈希**。分布式缓存中用于数据分片和节点扩缩容的算法。 | +希望这篇文章能帮助你建立起对缓存系统的完整认知。当你在实际项目中遇到性能问题时,能够想到:"是否可以用缓存来解决?" +::: diff --git a/docs/zh-cn/appendix/component-state-management.md b/docs/zh-cn/appendix/component-state-management.md index ba24cb0..de9aa59 100644 --- a/docs/zh-cn/appendix/component-state-management.md +++ b/docs/zh-cn/appendix/component-state-management.md @@ -1,703 +1,632 @@ # 组件化与状态管理模式总览 -> **学习指南**:组件化解决的是"代码该怎么拆",状态管理解决的是"数据该怎么流"。本章节会围绕一个问题展开:**当应用规模越来越大,组件之间该如何优雅地共享和同步数据?** - -在开始之前,建议你先补两块"基础砖": - -- **Vue/React 基础组件**:如果你还不熟悉组件的 props、events、生命周期等概念,建议先回顾一下 [前端框架入门](../stage-1/)。 -- **JavaScript 模块化**:了解 ES Module 和 CommonJS 的基本用法,有助于理解状态管理库的设计哲学。 +::: tip 🎯 核心问题 +**当应用越来越大,组件之间该如何优雅地共享和同步数据?** 你可能会遇到这样的困境:用户在商品页添加了购物车,但头部的购物车数量没更新;两个不相关的组件需要同一份数据,却不知道该怎么传递。本章将带你从"混乱的数据传递"进化到"清晰的状态管理"。 +::: --- -## 0. 引言:从"一盘散沙"到"井然有序" +## 1. 为什么要"组件化与状态管理"? + +### 1.1 从小作坊到工厂:前端开发的演变 + +在正式开始之前,先问你一个问题:**你有没有试过在厨房里做一顿大餐?** + +如果你只是给自己煮一碗面,那很简单——一个锅、一把面、一点调料,十秒钟搞定。但如果你要开一家餐厅,每天服务几百个顾客,就不能再"想做什么做什么"了。你需要标准化的菜谱、明确的分工、统一的采购流程,这样才能保证每道菜的质量稳定、出餐效率高。 + +前端开发也一样。一个人写小项目,代码随便放哪里都行。但当团队变大、项目变复杂后,就需要一套系统的方法来组织代码和管理数据。这就是**组件化与状态管理**要解决的问题。 + +::: tip 🤔 什么是"组件"和"状态"? +在继续之前,先解释两个核心术语: + +**组件(Component)**:就像乐高积木,每个积木是一个独立的部分,有自己的形状、颜色、功能。你可以把多个积木拼在一起,搭建出复杂的城堡。在前端开发中,一个按钮、一个表单、一个导航栏,都可以是一个组件。 + +**状态(State)**:就是组件的"记忆"。比如一个按钮,它"记住"了自己是"禁用"还是"启用"状态;一个购物车组件,它"记住"了里面有哪些商品。状态会变化,而状态变化会触发界面更新。 + +**组件化 + 状态管理 = 有组织的代码 + 清晰的数据流** +::: + +<div style="display: flex; gap: 20px; margin: 20px 0;"> +<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;"> + +**🏠 小作坊模式** +- 代码写在一个文件里,像在一口锅里煮所有菜 +- 数据到处传递,像服务员端着盘子在餐厅乱跑 +- 改一处可能影响其他地方,像盐放多了整道菜都毁了 + +</div> +<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;"> + +**🏭 工厂模式** +- 代码拆分成组件,像餐厅分成前厅、后厨、采购部 +- 数据集中管理,像有统一的仓库和配送系统 +- 改动影响范围清晰,像换个菜不会影响整个餐厅 + +</div> +</div> + +### 1.2 一个真实的踩坑故事:为什么你需要了解状态管理 + +你可能会说:"我用的不是 Vue/React 吗?它们不是已经有状态管理了吗?" 让我讲一个真实的故事,你就会明白为什么系统性地理解组件化和状态管理如此重要。 + +::: warning 小美的踩坑记 +小美是某电商公司的产品经理转前端开发,刚接手公司的购物车功能重构。她之前用的是 jQuery 时代的老项目,现在要用 Vue 3 改造。 + +小美想:"购物车逻辑很简单,存个数组就行了。" 于是她开始写代码: +- 在商品详情页组件里,用一个数组 `cart` 存储购物车数据 +- 在购物车页面组件里,又定义了一个 `cartItems` 数组 +- 在头部导航栏组件里,还有一个 `cartCount` 变量 + +问题很快出现了: +1. **数据不同步**:用户在商品详情页添加了商品,但购物车页面的数据没更新 +2. **重复代码**:小美不得不写了好几个"添加到购物车"的函数,分别放在不同的组件里 +3. **维护困难**:运营说要加一个"清空购物车"功能,小美发现要改三个地方 + +后来她请教前端架构师阿强,阿强看了一眼代码就说:"你犯了状态管理的大忌——同一份数据在多个地方存储。" + +解决方案很简单:用 Pinia 创建一个全局的购物车状态管理,所有组件都从同一个地方读写数据。这样改动之后,所有问题迎刃而解。 + +小美从此明白了一个道理:**不理解组件化和状态管理,你会写出难以维护的"意大利面条代码"。** +::: + +::: info 💡 核心启示 +组件化和状态管理不是框架的"附加功能",而是现代前端开发的基石。理解它们,你才能设计出清晰的架构、写出可维护的代码、在团队协作中游刃有余。 +::: + +--- + +## 2. 核心概念:理解组件化的本质 + +::: tip 🤔 什么是"组件化思维"? +组件化思维,就是一种把复杂界面拆分成独立、可复用、职责单一的代码单元的方法。 + +打个比方:想象你在组装一台电脑。你会把 CPU、内存、硬盘、显卡这些部件分别买回来,然后组装在一起。每个部件都有明确的功能,你可以随时替换某个部件,而不影响其他部分。 + +组件化就是让前端代码也能这样"模块化"——每个组件负责自己的事情,通过明确的接口和其他组件协作。 +::: + +### 2.1 用餐厅比喻理解组件化 + +让我们用餐厅的比喻来理解组件化的核心思想: + +| 概念 | 🍽️ 餐厅比喻 | 实际作用 | 具体例子 | +|------|-------------|----------|----------| +| **组件** | 餐厅的各个部门(前厅、后厨、采购部) | 每个部门负责自己的事情 | 按钮组件负责点击,表单组件负责输入 | +| **Props(属性)** | 顾客给服务员点的菜单 | 父组件给子组件传递数据 | 父组件把"用户名"传给头像组件 | +| **Events(事件)** | 服务员通知后厨"有新订单" | 子组件通知父组件发生了什么 | 按钮组件告诉父组件"我被点击了" | +| **State(状态)** | 后厨的"当前订单列表" | 组件内部存储的数据 | 购物车组件记住里面有哪些商品 | + +::: tip 📊 从表格中你能看到什么? +让我们逐行解读这张表: + +**组件**:就像餐厅有不同的部门,前端页面也由不同的组件组成。每个组件是一个独立的部分,有自己的职责。 + +**Props**:这是父组件给子组件"传递数据"的方式。就像顾客点菜时告诉服务员要吃什么,父组件也可以通过 props 把数据(比如用户名、商品信息)传给子组件。注意:props 是"单向"的,只能从父传给子,不能反向传递。 + +**Events**:当子组件需要通知父组件时(比如按钮被点击、表单提交),就会触发事件。就像服务员接到订单后通知后厨"开始做菜"。这样保持了数据流的单向性——子组件不能直接修改父组件的数据,只能"发消息"。 + +**State**:这是组件内部的"记忆"。就像后厨要记住当前有哪些订单,组件也需要记住自己的状态(比如购物车有哪些商品、按钮是否被禁用)。状态变化时,组件会自动更新界面。 +::: <ComponentHierarchyDemo /> -很多前端开发者在项目初期都会经历这样的阶段: +### 2.2 Props 和 Events:父子组件的"官方通道" -- 页面功能少的时候,数据直接放组件里,props 传来传去还能应付; -- 业务复杂了,组件层级越来越深,props drilling(属性钻取)像打地鼠一样令人崩溃; -- 两个不相干的组件需要共享同一份数据,开始搞事件总线、全局变量,结果代码像意大利面一样纠缠不清。 - -**直觉上,我们会以为是:"这个框架不够强大"。** 但大多数时候,问题并不在于工具,而在于我们**没有设计好组件的职责边界和数据流向**。 - -面对这些挑战,单纯依靠"写更多代码"已经捉襟见肘。我们需要一套系统的方法论,来在复杂的组件树中优雅地管理共享状态。这正是**组件化与状态管理**试图解决的问题。 - ---- - -## 1. 什么是"组件化思维"?(定义 + 场景) - -先给一个简短的工作定义,再看几个典型场景。 - -> 组件化思维,是一种将用户界面拆分为独立、可复用、职责单一的代码单元的工程方法,每个组件封装自己的结构(HTML)、表现(CSS)和行为(JS),并通过明确的接口与其他组件通信。 - -你可以简单地把它理解成三件事:**高内聚、低耦合、可复用**。 -常见会用到它的场景包括: - -- **UI 组件库开发**(如 Element Plus、Ant Design) -- **大型单页应用(SPA)**的页面拆分 -- **跨项目复用**的业务组件封装 - -接下来,我们就从一个真实团队的"血泪教训"出发,看看他们是怎么一点点从"面条代码"进化到"组件化架构"的。 - ---- - -## 2. 从"血泪教训"说起:某电商团队的组件化重构 - -### 2.1 初始阶段:一团乱麻的"大泥球" - -某电商团队在 2019 年启动了一个促销活动后台管理系统。初期为了快速上线,采用了传统的 jQuery + 服务端渲染模式。 - -**代码结构大概长这样:** - -```html -<!-- promotion-edit.html --> -<div id="page"> - <div class="header">...</div> - - <!-- 活动基本信息表单 --> - <form id="basic-info"> - <input name="activityName" /> - <input name="startTime" type="datetime-local" /> - <!-- 200+ 行的表单字段... --> - </form> - - <!-- 商品选择弹窗(隐藏在页面里) --> - <div id="product-modal" style="display:none"> - <!-- 商品列表、搜索、分页... --> - </div> - - <!-- 优惠券规则配置(另一个弹窗) --> - <div id="coupon-modal" style="display:none"> - <!-- 复杂的规则配置表单... --> - </div> - - <script> - // 1000+ 行的 jQuery 代码,处理各种交互 - $(function() { - // 初始化日期选择器 - $('#startTime').datetimepicker({...}); - - // 打开商品弹窗 - $('#select-product-btn').click(function() { - $('#product-modal').show(); - loadProductList(); - }); - - // 提交表单(200+ 行的表单验证和提交逻辑) - $('#submit-btn').click(function() { - // ... - }); - }); - </script> -</div> -``` - -**当时的痛点:** - -| 问题 | 表现 | 影响 | -| :--- | :--- | :--- | -| **代码冗余** | 商品选择弹窗在 5 个页面复制粘贴 | 修改一次要改 5 处,经常漏改 | -| **耦合严重** | 表单验证逻辑散落在 HTML、JS、CSS 中 | 改一个字段可能引发连锁 Bug | -| **难以测试** | 所有逻辑都挂在 jQuery 回调里 | 无法单元测试,只能靠人工点 | -| **团队协作难** | 5 个前端同时改一个文件 | Git 冲突频发,合并痛苦 | - -### 2.2 第一次重构:Vue 2 的曙光 - -2020 年,团队决定用 Vue 2 重构系统。这次重构的核心目标是:**按页面维度拆分组件**。 - -**重构后的代码结构:** - -```vue -<!-- ActivityEdit.vue --> -<template> - <div class="activity-edit"> - <PageHeader title="编辑活动" /> - - <BasicInfoForm v-model="activityData" /> - - <ProductSelector - v-model="activityData.productIds" - @open="showProductModal = true" - /> - - <CouponRules - v-model="activityData.coupons" - @open="showCouponModal = true" - /> - - <div class="actions"> - <el-button @click="save">保存</el-button> - <el-button @click="publish">发布</el-button> - </div> - - <!-- 弹窗组件 --> - <ProductModal - v-model:visible="showProductModal" - @select="onProductSelect" - /> - <CouponModal - v-model:visible="showCouponModal" - @confirm="onCouponConfirm" - /> - </div> -</template> - -<script> -export default { - components: { - PageHeader, - BasicInfoForm, - ProductSelector, - CouponRules, - ProductModal, - CouponModal - }, - data() { - return { - activityData: { - name: '', - startTime: null, - productIds: [], - coupons: [] - }, - showProductModal: false, - showCouponModal: false - } - }, - methods: { - save() { - // 保存逻辑 - }, - publish() { - // 发布逻辑 - } - } -} -</script> -``` - -**这次重构带来的改变:** - -- **组件复用**:商品选择弹窗从 5 个页面复制粘贴,变成了 1 个 `<ProductModal>` 组件到处复用 -- **职责分离**:每个组件只负责自己的逻辑,表单验证拆到 `<BasicInfoForm>` 内部 -- **团队协作**:5 个前端可以并行开发不同组件,Git 冲突大幅减少 - -但很快,新的问题又出现了... - -### 2.3 第二次重构:状态管理的觉醒 - -随着业务复杂度增加,组件之间的通信变得越来越复杂。 - -**典型场景:购物车状态同步** - -用户在一个页面把商品加入购物车,购物车图标上的数字要实时更新。但购物车状态散落在多个组件中: - -```vue -<!-- Header.vue --> -<template> - <header> - <div class="cart-icon"> - <i class="icon-cart"></i> - <span class="badge">{{ cartCount }}</span> - </div> - </header> -</template> - -<script> -export default { - data() { - return { - cartCount: 0 // 自己的购物车数量 - } - }, - created() { - // 方案1:事件总线 - this.$bus.$on('cart-updated', (count) => { - this.cartCount = count - }) - - // 方案2:localStorage 轮询 - setInterval(() => { - this.cartCount = JSON.parse(localStorage.getItem('cart')).length - }, 1000) - } -} -</script> -``` - -**问题爆发:** - -| 反模式 | 代码表现 | 后果 | -| :--- | :--- | :--- | -| **事件总线地狱** | `$bus.$emit` 满天飞,不知道谁监听谁 | 调试困难,内存泄漏 | -| **Props Drilling** | 数据层层传递,中间组件只是"搬运工" | 中间组件被迫耦合 | -| **LocalStorage 滥用** | 把状态存 localStorage 然后轮询 | 性能差,数据不一致 | -| **全局变量** | `window.sharedState` 直接修改 | 无法追踪变化,Bug 难定位 | - -**最终解决方案:Vuex + 组件化设计规范** - -团队引入了 Vuex 作为集中式状态管理,并制定了组件设计规范: - -```javascript -// store/modules/cart.js -export default { - namespaced: true, - state: { - items: [], - selectedIds: [] - }, - getters: { - totalCount: state => state.items.reduce((sum, item) => sum + item.quantity, 0), - totalPrice: state => state.items.reduce((sum, item) => sum + item.price * item.quantity, 0) - }, - mutations: { - ADD_ITEM(state, item) { - const existing = state.items.find(i => i.id === item.id) - if (existing) { - existing.quantity += item.quantity - } else { - state.items.push(item) - } - }, - REMOVE_ITEM(state, itemId) { - state.items = state.items.filter(i => i.id !== itemId) - } - }, - actions: { - async addToCart({ commit }, product) { - // 可以在这里调用 API - commit('ADD_ITEM', { ...product, quantity: 1 }) - } - } -} -``` - -重构后的组件代码: - -```vue -<!-- Header.vue --> -<template> - <header> - <div class="cart-icon" @click="goToCart"> - <i class="icon-cart"></i> - <span v-if="cartCount > 0" class="badge">{{ cartCount }}</span> - </div> - </header> -</template> - -<script> -import { mapGetters } from 'vuex' - -export default { - computed: { - ...mapGetters('cart', ['totalCount']) - } -} -</script> -``` - -通过这两次重构,团队总结出了组件化开发的**核心心法**: - -1. **组件化是手段,不是目的**:不要为了拆而拆,组件的边界应该对应业务的边界。 -2. **状态往上提,事件往下传**:共享状态尽量放在共同的父组件或状态管理库中。 -3. **单向数据流是底线**:不要直接修改 props,不要跨组件直接修改状态。 - ---- - -## 3. 组件通信的"七种武器" - -在深入状态管理库之前,我们先搞清楚组件之间有哪些通信方式,以及它们各自的适用场景。 - -### 3.1 Props / Emit:父子组件的"官方通道" - -这是 Vue/React 中最基础、最推荐的父子通信方式。 - -<PropsFlowDemo /> +在前端框架(Vue、React)中,**Props 和 Events 是父子组件通信的标准方式**。 **Vue 示例:** ```vue -<!-- Parent.vue --> +<!-- Parent.vue - 父组件 --> <template> <div> + <!-- 像给服务员递菜单一样,通过 props 传递数据 --> <Child - :user="currentUser" - :theme="darkMode" - @update:profile="handleProfileUpdate" - @delete="handleDelete" + :user-name="currentUser.name" + :is-admin="currentUser.isAdmin" + @delete-user="handleDelete" /> </div> </template> -<script> -export default { - data() { - return { - currentUser: { id: 1, name: '张三' }, - darkMode: true - } - }, - methods: { - handleProfileUpdate(newProfile) { - this.currentUser = { ...this.currentUser, ...newProfile } - }, - handleDelete(userId) { - console.log('删除用户:', userId) - } - } +<script setup> +import { ref } from 'vue' +import Child from './Child.vue' + +const currentUser = ref({ + name: '张三', + isAdmin: true +}) + +const handleDelete = (userId) => { + console.log('删除用户:', userId) + // 处理删除逻辑 } </script> ``` ```vue -<!-- Child.vue --> +<!-- Child.vue - 子组件 --> <template> - <div :class="['user-card', theme ? 'dark' : 'light']"> - <h3>{{ user.name }}</h3> - <input v-model="localProfile.bio" placeholder="个人简介" /> - <button @click="saveProfile">保存</button> - <button @click="requestDelete">删除</button> + <div class="user-card"> + <h3>{{ userName }}</h3> + <span v-if="isAdmin" class="badge">管理员</span> + <button @click="requestDelete">删除用户</button> </div> </template> -<script> -export default { - props: { - user: { type: Object, required: true }, - theme: { type: Boolean, default: false } - }, - data() { - return { - localProfile: { bio: '' } - } - }, - watch: { - user: { - immediate: true, - handler(newVal) { - this.localProfile = { ...newVal } - } - } - }, - methods: { - saveProfile() { - // 通过事件通知父组件更新 - this.$emit('update:profile', this.localProfile) - }, - requestDelete() { - this.$emit('delete', this.user.id) - } - } +<script setup> +// 接收父组件传来的数据 +const props = defineProps({ + userName: { type: String, required: true }, + isAdmin: { type: Boolean, default: false } +}) + +// 定义可以触发的事件 +const emit = defineEmits(['delete-user']) + +const requestDelete = () => { + // 通过事件通知父组件 + emit('delete-user', props.userName) } </script> ``` -**最佳实践:** +::: tip 💡 核心原则 +**Props 向下,Events 向上**——这是组件通信的黄金法则。 -1. **Props 向下传递,Events 向上传递**:保持单向数据流 -2. **Props 尽量只读**:不要在子组件直接修改 props -3. **事件命名要语义化**:`update:xxx` 表示更新,`delete`、`submit` 表示动作 +- 父组件通过 **props** 把数据传给子组件(像给下属分配任务) +- 子组件通过 **events** 通知父组件发生了什么(像下属汇报工作) -### 3.2 Event Bus:跨组件通信的"小道消息" +这样保持了数据流的清晰和单向性,避免了"谁都可以改数据"的混乱局面。 +::: -当两个没有直接父子关系的组件需要通信时,Event Bus(事件总线)是一种简单的方案。 +<PropsFlowDemo /> + +### 2.3 单向数据流:为什么不能直接修改 props? + +很多初学者会犯一个错误:在子组件里直接修改 props 的值。 + +```vue +<!-- ❌ 错误做法 --> +<script setup> +const props = defineProps({ + count: { type: Number, default: 0 } +}) + +// 直接修改 props - 这是被禁止的! +props.count = 10 // 会报错 +</script> +``` + +**为什么不能直接修改 props?** + +想象一下:你从图书馆借了一本书(props),然后在书上乱涂乱画(修改 props)。其他借这本书的人(其他组件)也会看到你的涂鸦,这会导致混乱。正确的做法是:如果你需要修改数据,应该让父组件来改,子组件只是"请求修改"。 + +```vue +<!-- ✅ 正确做法 --> +<script setup> +const props = defineProps({ + count: { type: Number, default: 0 } +}) + +const emit = defineEmits(['update-count']) + +// 通过事件请求父组件修改 +const increment = () => { + emit('update-count', props.count + 1) +} +</script> +``` + +--- + +## 3. 从"混沌"到"有序":组件通信的演进之路 + +::: tip 🤔 为什么需要演进? +随着项目变大,组件之间的通信会变得越来越复杂。让我们看看一个真实团队是如何一步步进化出清晰的状态管理方案的。 + +这不仅仅是"工具升级",而是**整个思维方式的变化**——从"随意传递数据"到"设计清晰的数据流"。 +::: + +### 3.1 演进的全景图 + +下面这张表展示了组件通信方式演进的四个阶段,你可以看到问题是如何一步步被解决的: + +| 阶段 | 通信方式 | 典型问题 | 核心变化 | +|------|---------|----------|----------| +| **阶段一:自由传递** | 直接修改、全局变量 | 数据不同步、难以调试 | 没有规范,怎么传都行 | +| **阶段二:Props/Events** | 父子组件标准通信 | Props Drilling(层层传递) | 有了规范,但深层嵌套很麻烦 | +| **阶段三:状态管理库** | Vuex/Redux/Pinia | 学习成本、样板代码 | 数据集中管理,调试方便 | +| **阶段四:现代化方案** | 组合式函数/原子化 | 需要理解新概念 | 更灵活、更简洁 | <EventBusDemo /> -**实现方式:** +::: tip 📊 从表格中你能看到什么? +让我们逐行解读这张表: + +**阶段一 → 阶段二**:从"没有规范"到"有规范"。这是质的飞跃——你开始用标准的 props/events 通信,数据流变得清晰。但代价是当组件层级很深时,数据要一层层传递,很麻烦(这就是 Props Drilling)。 + +**阶段二 → 阶段三**:从"分散管理"到"集中管理"。你开始用 Vuex/Redux 这样的状态管理库,把共享数据放在一个全局的"仓库"里,所有组件都从这里读写数据。这样解决了 Props Drilling,但学习成本变高了。 + +**阶段三 → 阶段四**:从"重量级"到"轻量级"。新的方案(如 Vue 3 的 Composition API、React 的 Hooks)让状态管理更灵活、更简洁。你不再一定要用全局的 store,可以按需组合小的状态单元。 + +**总结一下**:演进不只是"换了更好的工具",而是**整个思维方式的升级**——从随意传递数据,到设计清晰的数据流。 +::: + +### 3.2 阶段一:自由传递——混乱的开始 + +为什么叫"自由传递"?因为这个阶段没有任何规范,数据想怎么传就怎么传——全局变量、直接修改、事件总线满天飞。 + +**典型场景:购物车数据分散在各处** ```javascript -// eventBus.js -import { createApp } from 'vue' - -// 创建一个空的 Vue 实例作为事件总线 -const EventBus = createApp({}) - -export default EventBus -``` - -**使用示例:** - -```vue -<!-- 组件 A:发送消息 --> -<template> - <button @click="notifyUserLoggedIn">用户登录</button> -</template> - -<script> -import EventBus from './eventBus' - -export default { - methods: { - notifyUserLoggedIn() { - // 发送事件,带上用户数据 - EventBus.$emit('user:login', { - userId: 123, - username: '张三', - timestamp: Date.now() - }) - } - } -} -</script> -``` - -```vue -<!-- 组件 B:接收消息 --> -<template> - <div class="notification-bar"> - <span v-if="lastLoginUser">欢迎回来,{{ lastLoginUser.username }}!</span> - </div> -</template> - -<script> -import EventBus from './eventBus' - +// 商品详情页组件 export default { data() { return { - lastLoginUser: null + localCart: [] // 自己维护一份购物车数据 } }, - created() { - // 监听登录事件 - EventBus.$on('user:login', this.handleUserLogin) - }, - beforeUnmount() { - // 重要:组件销毁时取消监听,防止内存泄漏 - EventBus.$off('user:login', this.handleUserLogin) - }, methods: { - handleUserLogin(userData) { - console.log('收到登录事件:', userData) - this.lastLoginUser = userData - - // 3秒后清空提示 - setTimeout(() => { - this.lastLoginUser = null - }, 3000) + addToCart(product) { + this.localCart.push(product) + // 试图同步到其他组件 + window.cart = this.localCart // ❌ 全局变量! } } } -</script> + +// 购物车页面组件 +export default { + data() { + return { + cartItems: [] // 又一份购物车数据 + } + }, + mounted() { + // 试图从全局变量读取 + this.cartItems = window.cart || [] // ❌ 不可靠! + } +} + +// 头部导航组件 +export default { + data() { + return { + cartCount: 0 // 还有第三份数据! + } + }, + mounted() { + // 轮询检查变化(多么荒谬) + setInterval(() => { + this.cartCount = window.cart?.length || 0 + }, 1000) // ❌ 性能差! + } +} ``` -**Event Bus 的优缺点:** +**这个阶段的特点:** +- ✅ **优点**:简单直接,没有任何学习成本 +- ❌ **缺点**:数据分散、难以同步、调试困难、一团乱麻 -| 优点 | 缺点 | -| :--- | :--- | -| 实现简单,无需额外依赖 | 难以追踪事件流向,调试困难 | -| 适合小范围、临时性的通信 | 容易形成"事件 spaghetti" | -| 解耦发送方和接收方 | 必须手动管理订阅/取消订阅,容易内存泄漏 | +### 3.3 阶段二:Props/Events——规范的建立 -**什么时候用,什么时候不用:** +自由传递的混乱让团队意识到:**我们需要规范**。于是开始使用框架提供的标准通信方式:props 和 events。 -- **可以用**:两个距离较远、但逻辑上有关联的组件,且通信频率不高 -- **不要用**:组件层级简单、可以用 props/emit 解决;或者通信逻辑复杂、需要状态持久化 - -### 3.3 Provide / Inject:跨层级传值的"秘密通道" - -当数据需要从祖先组件传递给深层嵌套的后代组件时,逐层传递 props 会非常繁琐。Vue 的 Provide / Inject 机制可以解决这个问题。 - -**使用场景:** - -- 主题配置(深色/浅色模式) -- 用户信息(当前登录用户) -- 国际化配置 -- 表单控件之间的通信 - -**代码示例:** +**典型场景:Props Drilling(属性钻取)** ```vue <!-- 祖先组件:App.vue --> <template> - <div :class="['app', theme]"> - <!-- 深层嵌套的组件树 --> - <Layout> - <Sidebar> - <Menu> - <MenuItem v-for="item in menuItems" :key="item.id" /> - </Menu> - </Sidebar> - <MainContent> - <RouterView /> - </MainContent> - </Layout> + <div class="app"> + <!-- 层层传递用户信息 --> + <Layout :user-name="userName" /> </div> </template> -<script> -import { provide, ref } from 'vue' +<script setup> +import { ref } from 'vue' +import Layout from './Layout.vue' -export default { - setup() { - // 响应式的主题配置 - const theme = ref('light') - const userInfo = ref({ - id: 123, - name: '张三', - role: 'admin' - }) +const userName = ref('张三') +</script> +``` - // 切换主题的方法 - const toggleTheme = () => { - theme.value = theme.value === 'light' ? 'dark' : 'light' +```vue +<!-- 中间层:Layout.vue --> +<template> + <div class="layout"> + <Header :user-name="userName" /> <!-- 只是传递,不使用 --> + <Main> + <Page :user-name="userName" /> <!-- 只是传递,不使用 --> + </Main> + </div> +</template> + +<script setup> +const props = defineProps({ + userName: String +}) +</script> +``` + +```vue +<!-- 真正需要的地方:Header.vue --> +<template> + <header> + <span>{{ userName }}</span> <!-- 终于用到了 --> + </header> +</template> + +<script setup> +const props = defineProps({ + userName: String +}) +</script> +``` + +**这个阶段的特点:** +- ✅ **优点**:数据流清晰、单向流动、易于理解 +- ❌ **缺点**:Props Drilling(层层传递很麻烦)、跨组件通信困难 + +::: tip 🤔 什么是 Props Drilling? +Props Drilling 指的是:**数据要通过很多中间组件,一层层往下传,但这些中间组件并不真正使用这些数据**。 + +就像你要给住在五楼的人送快递,但规定必须每一层楼都要签收一次。一二三四楼的人只是帮你"传快递",他们并不需要这个快递,但必须参与进来。这显然很麻烦。 +::: + +### 3.4 阶段三:状态管理库——集中式管理 + +Props Drilling 的痛点催生了状态管理库(Vuex、Redux、Pinia)。它们的核心思想是:**把共享数据放在一个全局的"仓库"里,所有组件都从这里读写数据**。 + +**典型场景:用 Pinia 管理购物车** + +```javascript +// stores/cart.js - 全局购物车状态 +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +export const useCartStore = defineStore('cart', () => { + // 所有购物车数据集中在这里 + const items = ref([]) + + // 计算属性:商品数量 + const itemCount = computed(() => + items.value.reduce((sum, item) => sum + item.quantity, 0) + ) + + // 方法:添加商品 + const addItem = (product) => { + const existing = items.value.find(item => item.id === product.id) + if (existing) { + existing.quantity++ + } else { + items.value.push({ ...product, quantity: 1 }) } - - // 提供给后代组件 - provide('theme', theme) - provide('userInfo', userInfo) - provide('toggleTheme', toggleTheme) - - return { theme } } + + return { + items, + itemCount, + addItem + } +}) +``` + +```vue +<!-- 商品详情页组件 --> +<script setup> +import { useCartStore } from '@/stores/cart' + +const cart = useCartStore() + +const addToCart = (product) => { + cart.addItem(product) // 直接调用,无需层层传递 } </script> ``` ```vue -<!-- 深层后代组件:MenuItem.vue(可能是第 5+ 层嵌套) --> +<!-- 头部导航组件 --> <template> - <div :class="['menu-item', theme]"> - <span class="icon">{{ icon }}</span> - <span class="label">{{ label }}</span> + <header> + <span>购物车 ({{ cart.itemCount }})</span> + </header> +</template> - <!-- 管理员才能看到设置按钮 --> - <button - v-if="userInfo?.role === 'admin'" - class="settings-btn" - @click="openSettings" - > - 设置 - </button> +<script setup> +import { useCartStore } from '@/stores/cart' + +const cart = useCartStore() // 直接读取,自动同步 +</script> +``` + +**这个阶段的特点:** +- ✅ **优点**:数据集中管理、解决 Props Drilling、调试工具强大 +- ❌ **缺点**:学习成本、需要写额外代码(样板代码)、对简单项目可能过度设计 + +### 3.5 阶段四:现代化方案——灵活与简洁 + +状态管理库虽然强大,但也有"大炮打蚊子"的问题。对于中小型项目,更灵活、更轻量的方案出现了。 + +**典型场景:用 Composable/Hooks 复用状态逻辑** + +```javascript +// composables/useCart.js - 可复用的购物车逻辑 +import { ref, computed } from 'vue' + +export function useCart() { + const items = ref([]) + + const itemCount = computed(() => + items.value.reduce((sum, item) => sum + item.quantity, 0) + ) + + const addItem = (product) => { + const existing = items.value.find(item => item.id === product.id) + if (existing) { + existing.quantity++ + } else { + items.value.push({ ...product, quantity: 1 }) + } + } + + return { + items, + itemCount, + addItem + } +} +``` + +```vue +<!-- 在任何组件中使用 --> +<script setup> +import { useCart } from '@/composables/useCart' + +// 每次调用都会创建一个新的状态实例 +// 适合组件内部的局部状态 +const { items, itemCount, addItem } = useCart() +</script> +``` + +**这个阶段的特点:** +- ✅ **优点**:灵活、轻量、可组合、按需使用 +- ❌ **缺点**:需要理解组合式思维、跨组件共享需要额外处理 + +--- + +## 4. 状态管理库详解:Vuex vs Pinia vs Redux + +::: tip 🤔 如何选择状态管理库? +面对不同的状态管理库,你可能会困惑:到底该选哪一个? + +其实没有"最好"的库,只有"最适合"的。选择时考虑这些因素: +- **你用什么框架?** Vue 用 Pinia,React 用 Redux/Zustand +- **项目多大?** 小项目用 Composable,大项目用状态管理库 +- **团队经验?** 选团队熟悉的,或学习成本低的 + +接下来的内容会详细介绍主流状态管理库的特点和使用场景。 +::: + +### 4.1 主流状态管理库对比 + +| 特性 | Redux | Vuex | Pinia | Zustand | +| :--- | :--- | :--- | :--- | :--- | +| **适用框架** | React | Vue | Vue | React | +| **学习曲线** | 陡峭 | 中等 | 平缓 | 平缓 | +| **样板代码** | 多 | 中等 | 少 | 极少 | +| **TypeScript** | 良好 | 良好 | 优秀 | 优秀 | +| **调试工具** | 强大 | 良好 | 优秀 | 良好 | +| **适用场景** | 大型项目 | Vue 2/3 中大型项目 | Vue 3 新项目 | React 中小型项目 | + +::: tip 📊 从表格中你能看到什么? +让我们逐行解读这张表: + +**Redux**:React 生态的老牌状态管理库。优点是规范严格、调试工具强大,但缺点是样板代码多、学习曲线陡峭。适合大型项目和需要严格规范的团队。 + +**Vuex**:Vue 2 时代的官方状态管理库。设计理念类似 Redux,但更贴合 Vue 的响应式系统。现在仍然可以用,但新项目推荐用 Pinia。 + +**Pinia**:Vue 3 官方推荐的新一代状态管理库。语法简洁、TypeScript 支持好、学习成本低。**这是 Vue 3 项目的首选**。 + +**Zustand**:React 生态的轻量级状态管理库。API 极简、几乎无样板代码。适合中小型 React 项目。 +::: + +<StateManagementComparisonDemo /> + +### 4.2 Pinia 实战:Vue 3 的推荐选择 + +Pinia 是 Vue 团队官方推荐的状态管理库,专为 Vue 3 设计。它比 Vuex 更简洁、更易用。 + +**为什么叫 Pinia?** + +Pinia 是西班牙语"菠萝"的意思。菠萝是一种由很多小花组成的水果,每个小花都很独立,但整体上又是一个统一的整体。这正好比喻了 Pinia 的设计理念——**每个 store 是独立的,但可以组合使用**。 + +**核心概念:** + +::: details 查看完整代码示例 +```javascript +// stores/user.js - 用户状态管理 +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +export const useUserStore = defineStore('user', () => { + // 1. State:存储数据 + const userInfo = ref(null) + const isLoggedIn = computed(() => !!userInfo.value) + + // 2. Actions:修改数据的方法 + const login = async (username, password) => { + const response = await fetch('/api/login', { + method: 'POST', + body: JSON.stringify({ username, password }) + }) + const user = await response.json() + userInfo.value = user // 直接修改,Pinia 会处理响应式 + } + + const logout = () => { + userInfo.value = null + } + + // 3. Getters:计算属性 + const displayName = computed(() => { + return userInfo.value?.name || '游客' + }) + + return { + userInfo, + isLoggedIn, + login, + logout, + displayName + } +}) +``` +::: + +**在组件中使用:** + +```vue +<template> + <div class="user-panel"> + <span v-if="user.isLoggedIn">欢迎,{{ user.displayName }}</span> + <button v-if="user.isLoggedIn" @click="user.logout">退出登录</button> + <button v-else @click="showLoginDialog">登录</button> </div> </template> -<script> -import { inject } from 'vue' +<script setup> +import { useUserStore } from '@/stores/user' -export default { - props: { - icon: String, - label: String - }, - setup() { - // 注入祖先提供的数据 - const theme = inject('theme', 'light') // 提供默认值 - const userInfo = inject('userInfo', {}) +// 直接获取 store,所有内容都是响应式的 +const user = useUserStore() - const openSettings = () => { - console.log('打开设置,当前主题:', theme.value) - } - - return { theme, userInfo, openSettings } - } +const showLoginDialog = () => { + // 显示登录对话框... } </script> ``` -**Provide / Inject 的特点:** +**Pinia 的优势:** -| 优点 | 缺点 | -| :--- | :--- | -| 解决跨层级通信问题,无需逐层传递 props | 破坏了组件的封装性,难以追踪数据来源 | -| 适合提供全局配置、主题、用户上下文等 | 过度使用会导致组件间耦合严重 | -| 与响应式系统配合良好 | 不适合频繁变化的数据(如表单输入) | +| 优势 | 说明 | 对比 Vuex | +|------|------|----------| +| **简洁的 API** | 不需要 mutations,直接修改 state | Vuex 需要 mutations 和 actions 分开 | +| **TypeScript 友好** | 原生类型推导,不需要额外配置 | Vuex 需要复杂的类型定义 | +| **自动模块化** | 每个 store 文件自动成为模块 | Vuex 需要手动配置 namespaced | +| **更小的体积** | 打包后约 1KB | Vuex 约 3KB | -**使用建议:** +<VuexPiniaDemo /> -- **适合**:主题、语言、当前用户、全局配置等相对稳定的数据 -- **不适合**:频繁变化的业务数据、组件间复杂的交互逻辑 +### 4.3 Redux 实战:React 的经典选择 ---- +Redux 是 React 生态中最经典的状态管理库,以严格的单向数据流著称。 -## 4. 状态管理的"进化论" +**为什么叫 Redux?** -前面我们讲了组件之间的通信方式,但当应用规模进一步扩大,单纯依靠组件自身的机制已经不够用了。这时候就需要专门的状态管理方案。 - -### 4.1 为什么需要专门的状态管理? - -<StateManagementComparisonDemo /> - -让我们看一个典型的购物车场景: - -**没有状态管理时的问题:** - -```javascript -// 问题1:状态分散在各个组件 -// Header.vue 有自己的 cartCount -// CartPage.vue 有自己的 cartItems -// ProductDetail.vue 也有自己的 localCart - -// 问题2:同步困难 -// 在 ProductDetail 添加商品到购物车 -// Header 的购物车数量不会自动更新 - -// 问题3:数据不一致 -// 用户在 CartPage 删除了商品 -// 但 ProductDetail 的"已加入购物车"按钮还是选中状态 - -// 问题4:难以持久化 -// 用户刷新页面,购物车数据丢失 -``` - -**有状态管理时的好处:** - -```javascript -// 1. 单一数据源 -// 整个应用只有一份购物车状态 -const store = { - cart: { - items: [], - selectedIds: [] - } -} - -// 2. 响应式同步 -// 任何地方修改购物车,所有相关组件自动更新 - -// 3. 状态可追溯 -// 每次修改都有记录,可以调试、回滚 - -// 4. 易于持久化 -// 可以方便地保存到 localStorage 或服务器 -``` - -### 4.2 状态管理的核心概念 - -无论你选择哪种状态管理方案,以下概念都是通用的: - -| 概念 | 解释 | 类比 | -| :--- | :--- | :--- | -| **State** | 应用的状态数据 | 数据库中的数据 | -| **Getter** | 从 state 派生的计算属性 | SQL 查询视图 | -| **Mutation** | 修改 state 的唯一方式(同步) | 数据库事务 | -| **Action** | 提交 mutation 的方法(可异步) | 业务逻辑层 | -| **Store** | 容纳以上所有内容的对象 | 数据库实例 | - -### 4.3 主流状态管理库对比 - -<StateManagementComparisonDemo /> - -| 特性 | Redux | Vuex | Pinia | MobX | Zustand | Jotai | -| :--- | :--- | :--- | :--- | :--- | :--- | :--- | -| **框架** | React | Vue | Vue | React/Vue | React | React | -| **学习曲线** | 陡峭 | 中等 | 平缓 | 中等 | 平缓 | 平缓 | -| **样板代码** | 多 | 中等 | 少 | 少 | 极少 | 极少 | -| **TypeScript** | 良好 | 良好 | 优秀 | 良好 | 优秀 | 优秀 | -| **中间件支持** | 丰富 | 有 | 无 | 无 | 无 | 无 | -| **适用场景** | 大型应用 | 中大型 Vue 应用 | 中小型 Vue 应用 | 中大型应用 | 中小型应用 | 原子化状态 | - ---- - -## 5. 各框架状态管理详解 - -### 5.1 Redux:严格而强大的状态管理 - -<ReduxFlowDemo /> - -Redux 是 React 生态中最经典的状态管理方案,以严格的单向数据流著称。 +Redux 是 "Reduced Flux" 的缩写。Flux 是 Facebook 早期提出的应用架构模式,Redux 简化了 Flux 的概念,所以叫 "Reduced Flux"。 **核心原则:** @@ -705,14 +634,13 @@ Redux 是 React 生态中最经典的状态管理方案,以严格的单向数 2. **State 只读**:唯一改变 state 的方法是触发 action 3. **使用纯函数修改**:Reducer 必须是纯函数 -**代码示例:** - +::: details 查看完整代码示例 ```javascript -// 1. Action Types +// 1. 定义 Action Types const ADD_TODO = 'ADD_TODO' const TOGGLE_TODO = 'TOGGLE_TODO' -// 2. Action Creators +// 2. 定义 Action Creators const addTodo = (text) => ({ type: ADD_TODO, payload: { id: Date.now(), text, completed: false } @@ -723,10 +651,9 @@ const toggleTodo = (id) => ({ payload: { id } }) -// 3. Reducer +// 3. 定义 Reducer(纯函数) const initialState = { - todos: [], - filter: 'all' // all, active, completed + todos: [] } const todoReducer = (state = initialState, action) => { @@ -750,15 +677,22 @@ const todoReducer = (state = initialState, action) => { } } -// 4. Store +// 4. 创建 Store import { createStore } from 'redux' const store = createStore(todoReducer) +``` +::: -// 5. 在 React 中使用 +**在 React 中使用:** + +```jsx import { useSelector, useDispatch } from 'react-redux' function TodoList() { + // 读取 state const todos = useSelector(state => state.todos) + + // 获取 dispatch 函数 const dispatch = useDispatch() return ( @@ -783,711 +717,96 @@ function TodoList() { | :--- | :--- | | 严格的数据流,易于调试 | 样板代码多,学习曲线陡峭 | | 时间旅行调试(Time Travel) | 简单的状态也需要写很多代码 | -| 丰富的中间件生态(redux-thunk, redux-saga) | 不适合小型项目 | +| 丰富的中间件生态 | 不适合小型项目 | | 可预测的状态更新 | 需要理解函数式编程概念 | -### 5.2 Vuex 与 Pinia:Vue 生态的状态管理双雄 - -<VuexPiniaDemo /> - -#### 5.2.1 Vuex:经典的选择 - -Vuex 是 Vue 2 时代的官方状态管理库,设计理念与 Redux 类似,但更贴合 Vue 的响应式系统。 - -**核心概念:** - -```javascript -// store/index.js -import { createStore } from 'vuex' - -export default createStore({ - // State: 存储应用的状态数据 - state: { - user: null, - cart: { - items: [], - selectedIds: [] - }, - theme: 'light' - }, - - // Getters: 从 state 派生的计算属性 - getters: { - isLoggedIn: state => !!state.user, - cartTotal: state => { - return state.cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0) - }, - cartItemCount: state => { - return state.cart.items.reduce((count, item) => count + item.quantity, 0) - } - }, - - // Mutations: 修改 state 的唯一方式(必须是同步的) - mutations: { - SET_USER(state, user) { - state.user = user - }, - ADD_TO_CART(state, product) { - const existing = state.cart.items.find(item => item.id === product.id) - if (existing) { - existing.quantity++ - } else { - state.cart.items.push({ ...product, quantity: 1 }) - } - }, - REMOVE_FROM_CART(state, productId) { - state.cart.items = state.cart.items.filter(item => item.id !== productId) - }, - SET_THEME(state, theme) { - state.theme = theme - } - }, - - // Actions: 提交 mutation 的方法(可以包含异步操作) - actions: { - // 用户登录 - async login({ commit }, credentials) { - try { - const response = await fetch('/api/login', { - method: 'POST', - body: JSON.stringify(credentials) - }) - const user = await response.json() - commit('SET_USER', user) - return { success: true } - } catch (error) { - return { success: false, error: error.message } - } - }, - - // 添加到购物车(可能涉及 API 调用) - async addToCart({ commit, state }, product) { - // 先乐观更新 UI - commit('ADD_TO_CART', product) - - try { - // 同步到服务器 - await fetch('/api/cart', { - method: 'POST', - body: JSON.stringify({ - productId: product.id, - quantity: 1 - }) - }) - } catch (error) { - // 如果失败,回滚状态 - commit('REMOVE_FROM_CART', product.id) - throw error - } - } - }, - - // Modules: 将 store 分割成模块 - modules: { - user: { - namespaced: true, - state: () => ({ - profile: null, - preferences: {} - }), - mutations: { - SET_PROFILE(state, profile) { - state.profile = profile - } - } - }, - cart: { - namespaced: true, - state: () => ({ - items: [], - selectedIds: [] - }), - getters: { - total: state => state.items.reduce((sum, item) => sum + item.price * item.quantity, 0) - } - } - } -}) -``` - -**在组件中使用 Vuex:** - -```vue -<template> - <div class="cart"> - <span>购物车 ({{ cartItemCount }})</span> - <span>总计: ¥{{ cartTotal }}</span> - <button @click="addItem">添加商品</button> - </div> -</template> - -<script> -import { mapState, mapGetters, mapActions } from 'vuex' - -export default { - computed: { - // 方式1:使用辅助函数 - ...mapState(['user']), - ...mapGetters(['cartTotal', 'cartItemCount']), - - // 方式2:使用命名空间模块 - ...mapState('cart', ['items']), - - // 方式3:直接访问(适合简单场景) - userName() { - return this.$store.state.user?.name - } - }, - methods: { - ...mapActions(['addToCart']), - - async addItem() { - await this.addToCart({ id: 1, name: '商品A', price: 100 }) - } - } -} -</script> -``` - -#### 5.2.2 Pinia:新一代的优雅之选 - -Pinia 是 Vue 团队官方推荐的新一代状态管理库,专为 Vue 3 设计,但也支持 Vue 2。 - -**Pinia 相比 Vuex 的优势:** - -| 特性 | Vuex | Pinia | -| :--- | :--- | :--- | -| 语法 | 选项式 API | 组合式 API(更现代) | -| 类型支持 | 需要额外配置 | 原生 TypeScript 支持 | -| 代码量 | 需要 mutations、actions 分开 | 更简洁,直接修改 state | -| 模块化 | 需要 namespaced | 自动模块化 | -| 开发工具 | 支持 | 支持,且体验更好 | - -**Pinia 代码示例:** - -```javascript -// stores/counter.js -import { defineStore } from 'pinia' - -// 方式1:选项式 API(类似 Vuex) -export const useCounterStore = defineStore('counter', { - state: () => ({ - count: 0, - name: '计数器' - }), - getters: { - doubleCount: (state) => state.count * 2, - // 可以访问其他 getter - doubleCountPlusOne() { - return this.doubleCount + 1 - } - }, - actions: { - increment() { - this.count++ - }, - async fetchCount() { - const response = await fetch('/api/count') - const data = await response.json() - this.count = data.count - } - } -}) - -// 方式2:组合式 API(推荐,更符合 Vue3 风格) -import { ref, computed } from 'vue' - -export const useCartStore = defineStore('cart', () => { - // State - const items = ref([]) - const selectedIds = ref([]) - - // Getters - const totalCount = computed(() => - items.value.reduce((sum, item) => sum + item.quantity, 0) - ) - - const totalPrice = computed(() => - items.value.reduce((sum, item) => sum + item.price * item.quantity, 0) - ) - - // Actions - function addItem(product) { - const existing = items.value.find(item => item.id === product.id) - if (existing) { - existing.quantity++ - } else { - items.value.push({ ...product, quantity: 1 }) - } - } - - function removeItem(productId) { - items.value = items.value.filter(item => item.id !== productId) - } - - async function checkout() { - // 调用支付 API - const response = await fetch('/api/checkout', { - method: 'POST', - body: JSON.stringify({ items: items.value }) - }) - if (response.ok) { - items.value = [] // 清空购物车 - } - } - - return { - items, - selectedIds, - totalCount, - totalPrice, - addItem, - removeItem, - checkout - } -}) -``` - -**在组件中使用 Pinia:** - -```vue -<template> - <div class="cart-page"> - <h2>购物车 ({{ cart.totalCount }})</h2> - - <div v-for="item in cart.items" :key="item.id" class="cart-item"> - <span>{{ item.name }}</span> - <span>¥{{ item.price }} x {{ item.quantity }}</span> - <button @click="cart.removeItem(item.id)">删除</button> - </div> - - <div class="cart-summary"> - <p>总计: ¥{{ cart.totalPrice }}</p> - <button @click="cart.checkout" :disabled="cart.items.length === 0"> - 结算 - </button> - </div> - </div> -</template> - -<script setup> -import { useCartStore } from '@/stores/cart' - -// 直接获取 store 实例,所有属性和方法都是响应式的 -const cart = useCartStore() -</script> -``` - -**Vuex vs Pinia 的选择建议:** - -- **新项目**:直接选择 Pinia,官方推荐,体验更好 -- **Vue 2 老项目**:可以继续使用 Vuex,或者迁移到 Pinia(Pinia 支持 Vue 2) -- **大型项目需要中间件**:如果依赖 Redux 生态的中间件(如 redux-saga),可以考虑 Redux - -### 5.3 MobX:响应式编程的魔法 +<ReduxFlowDemo /> <MobxReactivityDemo /> -MobX 是一个基于响应式编程(Reactive Programming)的状态管理库,通过自动追踪依赖来实现状态更新。 - -**核心概念:** - -```javascript -import { makeAutoObservable } from 'mobx' - -class TodoStore { - todos = [] - filter = 'all' // all, active, completed - - constructor() { - // 自动将所有属性和方法转为响应式 - makeAutoObservable(this) - } - - // 计算属性(自动追踪依赖) - get filteredTodos() { - switch (this.filter) { - case 'active': - return this.todos.filter(t => !t.completed) - case 'completed': - return this.todos.filter(t => t.completed) - default: - return this.todos - } - } - - get stats() { - return { - total: this.todos.length, - active: this.todos.filter(t => !t.completed).length, - completed: this.todos.filter(t => t.completed).length - } - } - - // Action:修改状态 - addTodo(text) { - this.todos.push({ - id: Date.now(), - text, - completed: false - }) - } - - toggleTodo(id) { - const todo = this.todos.find(t => t.id === id) - if (todo) { - todo.completed = !todo.completed - } - } - - removeTodo(id) { - this.todos = this.todos.filter(t => t.id !== id) - } - - setFilter(filter) { - this.filter = filter - } -} - -// 创建 store 实例 -const todoStore = new TodoStore() -export default todoStore -``` - -**在 React 中使用:** - -```jsx -import { observer } from 'mobx-react-lite' -import todoStore from './TodoStore' - -// 使用 observer 包裹组件,使其自动追踪使用的状态 -const TodoList = observer(() => { - return ( - <div> - <div className="filters"> - {['all', 'active', 'completed'].map(filter => ( - <button - key={filter} - onClick={() => todoStore.setFilter(filter)} - className={todoStore.filter === filter ? 'active' : ''} - > - {filter} - </button> - ))} - </div> - - <ul> - {todoStore.filteredTodos.map(todo => ( - <li key={todo.id}> - <input - type="checkbox" - checked={todo.completed} - onChange={() => todoStore.toggleTodo(todo.id)} - /> - <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}> - {todo.text} - </span> - <button onClick={() => todoStore.removeTodo(todo.id)}>删除</button> - </li> - ))} - </ul> - - <div className="stats"> - 总计: {todoStore.stats.total} | - 进行中: {todoStore.stats.active} | - 已完成: {todoStore.stats.completed} - </div> - </div> - ) -}) -``` - -**MobX 的优缺点:** - -| 优点 | 缺点 | -| :--- | :--- | -| 自动追踪依赖,无需手动优化 | 魔法般的响应式可能让人困惑 | -| 代码量少,接近原生 JavaScript | 装饰器语法需要额外配置 | -| 面向对象风格,易于理解 | 调试工具不如 Redux 强大 | -| 性能优秀,只更新需要的组件 | 大型团队需要约定规范 | - -### 5.4 Zustand 与 Jotai:轻量级的新星 - <ZustandJotaiDemo /> -#### 5.4.1 Zustand:极简主义的选择 - -Zustand(德语"状态")是一个轻量级的状态管理库,核心代码只有几百行。 - -**基本用法:** - -```javascript -import { create } from 'zustand' - -// 创建 store -const useStore = create((set, get) => ({ - // State - bears: 0, - fish: 100, - - // Actions - increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), - removeAllBears: () => set({ bears: 0 }), - - // 使用 get 访问当前状态 - eatFish: () => { - const { fish, bears } = get() - if (fish > 0) { - set({ fish: fish - 1, bears: bears + 0.1 }) - } - }, - - // 异步 action - fetchBears: async () => { - const response = await fetch('/api/bears') - const bears = await response.json() - set({ bears }) - } -})) - -// 在 React 组件中使用 -function BearCounter() { - // 只订阅需要的字段,实现细粒度更新 - const bears = useStore((state) => state.bears) - const increase = useStore((state) => state.increasePopulation) - - return ( - <div> - <h1>{bears} bears around here...</h1> - <button onClick={increase}>Add bear</button> - </div> - ) -} -``` - -**Zustand 的优势:** - -1. **极简 API**:核心只有 `create`、`set`、`get` 三个概念 -2. **无需 Provider**:没有 Context 的包裹问题 -3. **细粒度订阅**:组件只订阅需要的状态片段 -4. **中间件支持**:持久化、日志、 immer 等 - -```javascript -// 使用中间件 -import { persist, createJSONStorage } from 'zustand/middleware' - -const useStore = create( - persist( - (set, get) => ({ - bears: 0, - increase: () => set((state) => ({ bears: state.bears + 1 })) - }), - { - name: 'bear-storage', // localStorage 的 key - storage: createJSONStorage(() => localStorage) - } - ) -) -``` - -#### 5.4.2 Jotai:原子化的状态管理 - -Jotai 采用"原子(Atom)"的概念来管理状态,灵感来自 Recoil。 - -**核心概念:** - -```javascript -import { atom, useAtom } from 'jotai' - -// 定义原子状态 -const countAtom = atom(0) - -// 派生原子(类似 computed) -const doubledCountAtom = atom((get) => get(countAtom) * 2) - -// 可写派生原子 -const incrementAtom = atom( - (get) => get(countAtom), - (get, set, update) => { - set(countAtom, get(countAtom) + update) - } -) - -// 在组件中使用 -function Counter() { - const [count, setCount] = useAtom(countAtom) - const [doubled] = useAtom(doubledCountAtom) - const [, increment] = useAtom(incrementAtom) - - return ( - <div> - <p>Count: {count}</p> - <p>Doubled: {doubled}</p> - <button onClick={() => setCount(c => c + 1)}>+1</button> - <button onClick={() => increment(5)}>+5</button> - </div> - ) -} -``` - -**Jotai 的特点:** - -1. **原子化**:状态拆分成独立的小单元,按需组合 -2. **细粒度更新**:只有订阅的原子变化时才重渲染 -3. **派生状态**:类似 computed,自动追踪依赖 -4. **异步支持**:内置对异步原子的支持 - -```javascript -// 异步原子 -const userAtom = atom(null) - -const fetchUserAtom = atom( - (get) => get(userAtom), - async (get, set, userId) => { - const response = await fetch(`/api/users/${userId}`) - const user = await response.json() - set(userAtom, user) - } -) - -// 使用 -function UserProfile({ userId }) { - const [user, fetchUser] = useAtom(fetchUserAtom) - - useEffect(() => { - fetchUser(userId) - }, [userId]) - - if (!user) return <div>Loading...</div> - return <div>Hello, {user.name}</div> -} -``` - --- -## 6. 如何选择适合你的状态管理方案? +## 5. 实战指南:如何设计状态管理? -面对这么多选择,你可能会感到困惑。以下是一些实用的决策建议: +::: tip 🤔 什么时候需要状态管理库? +不是所有项目都需要状态管理库。在引入之前,先问自己几个问题: -### 6.1 决策树 +1. **有多少组件需要共享这份数据?** + - 如果只有 2-3 个组件,用 props/events 就够了 + - 如果有 5+ 个组件,考虑状态管理库 -``` -你的项目规模? -├── 小型项目 (< 10 个组件) -│ └── 使用组件自带的状态管理即可 -│ (useState / ref) -│ -├── 中型项目 (10-50 个组件) -│ ├── 使用 Vue? -│ │ └── Pinia(推荐)或 Vuex -│ └── 使用 React? -│ ├── 需要严格的数据流?→ Redux Toolkit -│ └── 追求简洁?→ Zustand -│ -└── 大型项目 (> 50 个组件,多团队协作) - ├── 使用 Vue? - │ ├── 需要严格规范?→ Vuex + Modules - │ └── 追求开发效率?→ Pinia - ├── 使用 React? - │ ├── 企业级应用?→ Redux + Redux Toolkit - │ ├── 需要细粒度控制?→ Recoil / Jotai - │ └── 追求简洁?→ Zustand - └── 跨框架需求? - └── 考虑 RxJS 或原生事件机制 -``` +2. **这份数据会经常变化吗?** + - 如果几乎不变(如用户信息),用 Provide/Inject + - 如果经常变化(如购物车),用状态管理库 -### 6.2 各方案适用场景速查表 +3. **团队规模多大?** + - 个人或小团队:简单的方案就行 + - 大团队:需要严格的规范和强大的调试工具 -| 如果你... | 推荐方案 | 理由 | -| :--- | :--- | :--- | -| 刚开始学 Vue 3 | Pinia | 官方推荐,语法简单,TypeScript 支持好 | -| 维护 Vue 2 老项目 | Vuex | 稳定成熟,生态丰富 | -| 做企业级 React 项目 | Redux Toolkit | 规范严格,调试工具强大 | -| 做中小型 React 项目 | Zustand | 极简,无样板代码 | -| 需要原子化状态管理 | Jotai / Recoil | 细粒度控制,派生状态强大 | -| 喜欢面向对象风格 | MobX | 自动追踪依赖,代码直观 | -| 追求极致性能 | 原生 Context + useReducer | 零依赖,完全可控 | +**记住:从简单开始,按需升级。** +::: -### 6.3 避坑指南 +### 5.1 状态设计的原则 -**不要这样用:** +无论你选择哪种状态管理方案,都应该遵循以下原则: + +**原则一:单一数据源** + +同一份数据只应该在一个地方存储。不要在多个组件里重复定义相同的数据。 ```javascript -// ❌ 错误:直接修改 state -store.state.user.name = '李四' +// ❌ 错误:数据分散在各处 +const ProductDetail = { cart: [] } +const CartPage = { items: [] } +const Header = { count: 0 } -// ❌ 错误:在组件外修改 state -setTimeout(() => { - store.count++ -}, 1000) - -// ❌ 错误:在 getter 中修改 state -getters: { - doubleCount(state) { - state.count *= 2 // 副作用! - return state.count - } -} - -// ❌ 错误:不取消事件监听 -export default { - created() { - EventBus.$on('event', this.handler) - } - // 忘记在 beforeUnmount 中取消订阅 -} +// ✅ 正确:数据集中管理 +const cartStore = { items: [] } // 唯一的数据源 ``` -**推荐这样用:** +**原则二:不可变性** + +修改状态时,应该创建新对象,而不是直接修改原对象。 ```javascript -// ✅ 正确:通过 mutation/action 修改 state -store.commit('SET_USER_NAME', '李四') -// 或 Pinia: -store.userName = '李四' // Pinia 允许直接修改,因为它底层已经处理了响应式 +// ❌ 错误:直接修改 +state.items.push(newItem) -// ✅ 正确:在 action 中处理异步 -actions: { - async fetchUser({ commit }) { - const user = await api.getUser() - commit('SET_USER', user) - } -} - -// ✅ 正确:getter 是纯函数 -getters: { - doubleCount: (state) => state.count * 2 -} - -// ✅ 正确:及时取消订阅 -export default { - created() { - this.unsubscribe = store.$subscribe((mutation, state) => { - console.log(mutation, state) - }) - }, - beforeUnmount() { - this.unsubscribe() - } -} +// ✅ 正确:创建新对象 +state.items = [...state.items, newItem] ``` ---- +**原则三:状态往上提,事件往下传** -## 7. 实战:电商购物车状态管理设计 +共享状态应该放在最近的公共祖先组件或全局 store 中,而不是分散在各个子组件里。 -让我们综合运用前面的知识,设计一个电商购物车系统的状态管理方案。 +```vue +<!-- ❌ 错误:状态在子组件中 --> +<Parent> + <Child :data="childData" @update="childData = $event" /> +</Parent> -### 7.1 需求分析 +<!-- ✅ 正确:状态在父组件中 --> +<Parent> + <Child :data="parentData" @update="parentData = $event" /> +</Parent> +``` + +### 5.2 实战案例:电商购物车状态设计 + +让我们综合运用前面的知识,设计一个电商购物车的状态管理方案。 + +**需求分析:** - 商品列表页可以添加商品到购物车 - 购物车页面可以查看、修改数量、删除商品 - 头部导航显示购物车商品数量 - 支持选择/取消选择商品,计算选中商品总价 -- 支持全选/取消全选 - 数据持久化到 localStorage -### 7.2 状态设计(Pinia) +**状态设计(Pinia):** ```javascript // stores/cart.js @@ -1495,11 +814,9 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useCartStore = defineStore('cart', () => { - // ============ State ============ - const items = ref([]) - const selectedIds = ref([]) - const isLoading = ref(false) - const error = ref(null) + // ============ State(状态)============ + const items = ref([]) // 购物车商品列表 + const selectedIds = ref([]) // 选中的商品 ID // 从 localStorage 恢复数据 const initFromStorage = () => { @@ -1510,7 +827,7 @@ export const useCartStore = defineStore('cart', () => { items.value = data.items || [] selectedIds.value = data.selectedIds || [] } catch (e) { - console.error('Failed to parse cart data:', e) + console.error('读取购物车数据失败:', e) } } } @@ -1523,7 +840,7 @@ export const useCartStore = defineStore('cart', () => { })) } - // ============ Getters ============ + // ============ Getters(计算属性)============ const itemCount = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0) ) @@ -1532,7 +849,6 @@ export const useCartStore = defineStore('cart', () => { items.value.reduce((sum, item) => sum + item.price * item.quantity, 0) ) - // 选中商品相关计算 const selectedItems = computed(() => items.value.filter(item => selectedIds.value.includes(item.id)) ) @@ -1541,11 +857,7 @@ export const useCartStore = defineStore('cart', () => { selectedItems.value.reduce((sum, item) => sum + item.price * item.quantity, 0) ) - const isAllSelected = computed(() => - items.value.length > 0 && selectedIds.value.length === items.value.length - ) - - // ============ Actions ============ + // ============ Actions(方法)============ const addItem = (product) => { const existing = items.value.find(item => item.id === product.id) if (existing) { @@ -1587,21 +899,6 @@ export const useCartStore = defineStore('cart', () => { persist() } - const toggleSelectAll = () => { - if (isAllSelected.value) { - selectedIds.value = [] - } else { - selectedIds.value = items.value.map(item => item.id) - } - persist() - } - - const clearCart = () => { - items.value = [] - selectedIds.value = [] - persist() - } - // 初始化 initFromStorage() @@ -1609,64 +906,33 @@ export const useCartStore = defineStore('cart', () => { // State items, selectedIds, - isLoading, - error, // Getters itemCount, totalPrice, selectedItems, selectedTotalPrice, - isAllSelected, // Actions addItem, updateQuantity, removeItem, - toggleSelection, - toggleSelectAll, - clearCart + toggleSelection } }) ``` -### 7.3 组件中使用 +**在组件中使用:** ```vue -<!-- CartIcon.vue --> +<!-- 商品详情页:ProductDetail.vue --> <template> - <div class="cart-icon" @click="goToCart"> - <i class="icon-cart"></i> - <span v-if="cart.itemCount > 0" class="badge"> - {{ cart.itemCount }} - </span> - </div> -</template> - -<script setup> -import { useCartStore } from '@/stores/cart' - -const cart = useCartStore() - -const goToCart = () => { - router.push('/cart') -} -</script> -``` - -```vue -<!-- ProductCard.vue --> -<template> - <div class="product-card"> - <img :src="product.image" :alt="product.name" /> - <h3>{{ product.name }}</h3> + <div class="product-detail"> + <h2>{{ product.name }}</h2> <p class="price">¥{{ product.price }}</p> - <button @click="addToCart" :disabled="isInCart"> - {{ isInCart ? '已加入' : '加入购物车' }} - </button> + <button @click="addToCart">加入购物车</button> </div> </template> <script setup> -import { computed } from 'vue' import { useCartStore } from '@/stores/cart' const props = defineProps({ @@ -1675,63 +941,227 @@ const props = defineProps({ const cart = useCartStore() -const isInCart = computed(() => - cart.items.some(item => item.id === props.product.id) -) - const addToCart = () => { cart.addItem({ id: props.product.id, name: props.product.name, - price: props.product.price, - image: props.product.image + price: props.product.price }) } </script> ``` +```vue +<!-- 头部导航:Header.vue --> +<template> + <header class="header"> + <div class="logo">我的商店</div> + <nav> + <RouterLink to="/">首页</RouterLink> + <RouterLink to="/cart"> + 购物车 ({{ cart.itemCount }}) + </RouterLink> + </nav> + </header> +</template> + +<script setup> +import { useCartStore } from '@/stores/cart' + +const cart = useCartStore() // 直接使用,自动响应变化 +</script> +``` + --- -## 8. 名词对照表 +## 6. 常见踩坑与避坑指南 -| 英文术语 | 中文对照 | 解释 | +::: warning ⚠️ 这些坑,90% 的初学者都会踩 +在状态管理的实践中,有些错误特别常见。让我总结一下最常见的坑,以及如何避免它们。 +::: + +### 6.1 坑一:直接修改 Props 或 State + +**错误代码:** + +```javascript +// ❌ 直接修改 props +props.user.name = '李四' + +// ❌ 直接修改 Vuex 的 state +store.state.user.name = '李四' + +// ❌ 直接修改数组元素 +state.items[0].name = '新名称' +``` + +**为什么这样不行?** + +前端框架(Vue/React)需要"追踪"数据的变化,才能自动更新界面。如果你直接修改对象或数组,框架可能无法检测到变化,导致界面不更新。 + +**正确做法:** + +```javascript +// ✅ Vue 3 / Pinia:直接修改顶层属性 +store.user.name = '李四' // Pinia 会自动处理响应式 + +// ✅ Vue 2 / Vuex:通过 mutation +mutations: { + UPDATE_USER_NAME(state, newName) { + state.user.name = newName + } +} + +// ✅ 修改数组:创建新数组 +state.items = state.items.map((item, index) => + index === 0 ? { ...item, name: '新名称' } : item +) +``` + +### 6.2 坑二:在 Getter 中修改状态 + +**错误代码:** + +```javascript +// ❌ 在 getter 中修改状态 +getters: { + doubleCount(state) { + state.count *= 2 // 副作用! + return state.count + } +} +``` + +**为什么这样不行?** + +Getter 应该是"纯函数",只负责计算和返回值,不应该有任何副作用(修改状态)。如果在 getter 中修改状态,会导致无限循环、难以调试的问题。 + +**正确做法:** + +```javascript +// ✅ Getter 只计算,不修改 +getters: { + doubleCount(state) { + return state.count * 2 + } +} + +// ✅ 如果需要修改,用 action +actions: { + doubleCountAndSave({ commit }) { + commit('SET_DOUBLE_COUNT') + } +} +``` + +### 6.3 坑三:忘记清理事件监听 + +**错误代码:** + +```javascript +// ❌ 忘记取消订阅 +export default { + created() { + EventBus.$on('cart-updated', this.handleCartUpdate) + } + // 组件销毁了,但监听还在! +} +``` + +**为什么这样不行?** + +如果组件销毁了但事件监听还在,会导致内存泄漏(占用的内存无法释放)。在单页应用中,用户不断切换页面,这些未清理的监听器会越积越多,最终导致页面卡顿。 + +**正确做法:** + +```javascript +// ✅ 及时取消订阅 +export default { + created() { + EventBus.$on('cart-updated', this.handleCartUpdate) + }, + beforeUnmount() { // Vue 3 用 beforeUnmount,Vue 2 用 beforeDestroy + EventBus.$off('cart-updated', this.handleCartUpdate) + } +} +``` + +### 6.4 坑四:过度使用状态管理 + +**错误代码:** + +```javascript +// ❌ 把所有状态都放进 store +const store = useStore() +store.inputValue = '用户输入' +store.isModalOpen = true +store.currentTab = 'profile' +``` + +**为什么这样不行?** + +不是所有状态都需要放进全局 store。如果一个状态只在一个组件中使用(如输入框的值、模态框的开关),放在组件内部就行。过度使用状态管理会让代码变得复杂。 + +**正确做法:** + +```javascript +// ✅ 局部状态用组件内部管理 +const inputValue = ref('') + +// ✅ 只有需要共享的状态才放 store +const userInfo = useUserStore() // 多个组件需要用户信息 +const cart = useCartStore() // 多个组件需要购物车数据 +``` + +--- + +## 7. 总结与建议 + +### 7.1 核心知识点回顾 + +让我们用一张表格来回顾组件化与状态管理的核心概念: + +| 概念 | 一句话解释 | 解决的问题 | 典型工具 | +|------|-----------|-----------|----------| +| **组件化** | 把界面拆成独立的、可复用的部分 | 代码复用、职责分离 | Vue/React 组件 | +| **Props** | 父组件给子组件传递数据 | 父子通信 | Vue/React 内置 | +| **Events** | 子组件通知父组件发生了什么 | 子父通信 | Vue/React 内置 | +| **State** | 组件内部存储的数据 | 记忆组件的状态 | Vue/React 内置 | +| **状态管理库** | 集中管理全局共享状态 | 跨组件通信、Props Drilling | Pinia、Redux、Zustand | +| **单一数据源** | 同一份数据只在一个地方存储 | 数据不一致、同步困难 | 状态管理库的核心原则 | + +### 7.2 不同场景的选择建议 + +| 场景 | 推荐方案 | 理由 | | :--- | :--- | :--- | -| **State** | 状态 | 应用的数据存储,是状态管理的核心。 | -| **Props** | 属性 | 父组件向子组件传递数据的方式。 | -| **Event/Action** | 事件/动作 | 组件通知父组件或触发状态变更的方式。 | -| **Store** | 存储/仓库 | 集中管理状态的对象。 | -| **Getter** | 获取器 | 从 state 派生的计算属性。 | -| **Mutation** | 变更 | Vuex 中修改 state 的唯一方式,必须是同步的。 | -| **Reducer** | 归约器 | Redux 中处理 state 更新的纯函数。 | -| **Dispatch** | 派发 | 触发 action 或 mutation 的操作。 | -| **Subscribe** | 订阅 | 监听状态变化的操作。 | -| **Reactive** | 响应式 | 数据变化自动触发更新的机制。 | -| **Computed** | 计算属性 | 基于其他状态派生的状态。 | -| **Module** | 模块 | 将 store 分割成多个子模块的方式。 | -| **Namespaced** | 命名空间 | 模块内部的 action/mutation 自动带命名前缀。 | -| **Hydration** | 注水 | SSR 中将服务端状态同步到客户端的过程。 | -| **Time Travel** | 时间旅行 | Redux DevTools 中回溯状态变化的功能。 | +| **父子组件通信** | Props + Events | 框架内置,简单直接 | +| **跨层级传值** | Provide / Inject | 避免层层传递 | +| **组件内局部状态** | ref / useState | 简单,不需要额外工具 | +| **中型 Vue 项目** | Pinia | 官方推荐,学习成本低 | +| **中型 React 项目** | Zustand | 极简,无样板代码 | +| **大型 Vue 项目** | Pinia + 规范 | 灵活且可扩展 | +| **大型 React 项目** | Redux Toolkit | 规范严格,生态丰富 | +| **跨组件复用逻辑** | Composable / Hooks | 灵活,可组合 | ---- +### 7.3 学习建议 -## 总结 +**对于初学者:** -状态管理是前端开发中永恒的话题。从简单的 props 传递到复杂的全局状态管理,没有银弹,只有适合当前场景的方案。 +1. **先掌握基础**:理解 props、events、state 这些基本概念 +2. **从小项目开始**:不要一开始就上状态管理库 +3. **多写代码**:理论学再多,不如动手实践 + +**对于进阶者:** + +1. **读源码**:理解 Pinia/Redux 的工作原理 +2. **学模式**:了解常见的设计模式(如观察者模式、发布订阅模式) +3. **关注生态**:学习相关的工具(如 DevTools、中间件) **记住这些核心原则:** -1. **从简单开始**:不要过早引入复杂的状态管理库,props 和 emit 能解决大部分问题。 -2. **状态往上提**:共享状态尽量放在共同的父组件或状态管理库中。 -3. **单一数据源**:避免同一份数据在多个地方存储。 -4. **不可变性**:修改状态时创建新对象,而不是直接修改原对象。 -5. **按需选择**:根据团队技术栈和项目规模选择合适的方案。 +1. **从简单开始**:不要过早引入复杂的状态管理库 +2. **单一数据源**:避免同一份数据在多个地方存储 +3. **不可变性**:修改状态时创建新对象,而不是直接修改 +4. **按需选择**:根据项目规模和团队情况选择合适的方案 -**不同场景的选择建议:** - -- **小型项目**:props / emit + Provide / Inject -- **中型 Vue 项目**:Pinia -- **中型 React 项目**:Zustand 或 React Query -- **大型项目**:Redux Toolkit、Vuex/Pinia + 严格规范 -- **需要细粒度控制**:MobX、Jotai、Recoil - -希望这篇指南能帮助你在面对复杂的状态管理问题时,做出明智的选择。 +希望这篇文章能帮助你建立起对组件化与状态管理的整体认知。当你在实际项目中遇到复杂的数据流问题时,能够知道从哪里入手、如何设计、怎样实现。 diff --git a/docs/zh-cn/appendix/database-intro.md b/docs/zh-cn/appendix/database-intro.md index 3227589..10d8f59 100644 --- a/docs/zh-cn/appendix/database-intro.md +++ b/docs/zh-cn/appendix/database-intro.md @@ -1,297 +1,371 @@ # 数据库原理入门:为什么淘宝能在 0.01 秒内找到你的订单? -> 💡 **学习指南**:本章节无需编程基础。我们将从一个你熟悉的场景出发——当你在淘宝搜索订单时,系统如何在 10 亿条记录中瞬间定位到你购买的那件 T 恤?答案藏在数据库的底层原理里:B+ 树、索引、事务……我们会用真实的业务案例(淘宝、微信、12306)一步步拆解这些概念。 +::: tip 🎯 核心问题 +**为什么你的 Excel 查询要 10 秒,而淘宝搜索只要 0.01 秒?** 当数据从"几千条"变成"十亿条",从"单人使用"变成"千万人同时访问",Excel 就不够用了。数据库就是为解决这个问题而生的——它是专门处理海量数据、高并发访问的"超级 Excel"。本章将带你从零开始理解数据库的核心原理。 +::: --- -## 0. 引言:当你的 Excel 打不开时 +## 1. 为什么要"数据库"? -想象一下: +### 1.1 从小书店到淘宝:数据规模的演变 -- **场景 A**:你是淘宝的订单系统负责人,今天是大促,1 秒钟有 100 万笔新订单涌入。你的 Excel 还在转圈…… -- **场景 B**:你是微信的工程师,用户正在加载朋友圈,需要瞬间从百亿条动态里找到他好友的 20 条。你的代码还在循环遍历…… -- **场景 C**:你是 12306 的架构师,春运当天,几千万人同时抢票,系统必须保证同一张票不会被两个人同时买到。你的数据库连接池已经耗尽…… - -这三个场景的共同点是:**数据规模已经从"千条"变成了"百亿条",用户并发从"几人"变成了"千万人"**。 - -这时候,Excel 已经完全无法胜任,你需要的是**数据库 (Database)**。 - -本教程将带你从零开始,理解数据库这座大厦是如何构建的: - -1. **存储革命**:数据是如何从 Excel 进化到数据库的? -2. **关系模型**:表、行、列、主键、外键到底是什么? -3. **查询语言**:如何用 SQL 与数据库对话? -4. **性能核心**:为什么数据库能毫秒级查询?(揭秘 B+ 树索引) -5. **事务安全**:如何保证数据不丢、不乱、不冲突? - -<DatabaseEvolutionDemo /> - ---- - -## 1. 为什么 Excel 不够用了?从记事本到数据库的进化 - -### 1.1 当数据只有 100 条时:记事本时代 - -假设你开了一家小书店,每天卖出几本书。你随手记在笔记本上: +想象你开了一家小书店,每天卖出几本书。你随手在笔记本上记下: ``` 2024-01-15:张三买了《百年孤独》,59元 2024-01-16:李四买了《活着》,39元 ``` -**优点**:零门槛,拿笔就能写。 +这时候,笔记本完全够用。但当你的书店变成了"亚马逊",每天有百万订单涌入,问题就出现了: -**缺点**: -- 想知道"上个月一共卖了多少钱?"——你得一页页翻,按着计算器算半天。 -- 想知道"哪本书卖得最好?"——你可能需要手动数每本书出现的次数。 +- **数据量大**:不是几十行,而是几亿行 +- **并发访问**:不是一个人在查,而是几千万人同时访问 +- **数据关联**:订单关联用户、商品、库存、物流……复杂关系需要高效管理 +- **数据安全**:不能因为断电就丢失所有订单 -这就是**无结构化数据**的困境:数据是死的,想要获得洞察,需要人工大量加工。 +<div style="display: flex; gap: 20px; margin: 20px 0;"> +<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;"> -### 1.2 当数据增长到 1 万条时:Excel 时代 +**📓 Excel/笔记本** +- 适合个人或小团队 +- 数据量:几千到几万行 +- 单人使用,顺序访问 +- 手动查找,速度慢 -生意好了,你开始用 **Excel**。 +</div> +<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;"> -你建了一张表,列出了:`订单号`、`书名`、`价格`、`购买者`、`购买日期`。 +**🗄️ 数据库** +- 适合企业级应用 +- 数据量:亿级以上 +- 千万人同时在线访问 +- 毫秒级查询速度 -| 订单号 | 书名 | 价格 | 购买者 | 日期 | -|--------|------|------|--------|------| -| 001 | 百年孤独 | 59 | 张三 | 2024-01-15 | -| 002 | 活着 | 39 | 李四 | 2024-01-16 | +</div> +</div> -**优点**: -- 可以**自动求和**(SUM 函数)。 -- 可以**排序**(按价格从高到低)。 -- 可以**筛选**(只看张三的购买记录)。 +**这就是"数据库"要解决的问题:如何高效存储、快速查询、安全地管理海量数据?** -**缺点**: -- **容量有限**:当你有 100 万行数据时,Excel 打开都要几分钟,甚至直接卡死。 -- **难以协作**:你和店员不能同时修改同一个文件,否则会冲突(你得等同事保存关闭后才能编辑)。 -- **数据不安全**:不小心删了一行,Ctrl+Z 可能救不回来。如果硬盘坏了,数据可能永久丢失。 -- **数据冗余**:如果张三买了 100 本书,你得在每一行重复写张三的地址和电话。如果张三换了电话,你得修改 100 行。 +### 1.2 一个真实的踩坑故事:为什么不能用 Excel 存用户数据 -这就是**单机文件型数据**的瓶颈:它只适合个人或小团队处理中等规模的数据。 +你可能会说:"我的项目才几万用户,Excel 不就够用了吗?" 让我讲一个真实的故事。 -### 1.3 当数据达到 1 亿条时:数据库时代 +::: warning 小林的创业踩坑记 +小林创业做了一个社交应用,刚开始用户不多,他用 Excel 存储用户信息(姓名、手机、注册时间等)。每天导出 Excel 统计用户增长,一切正常。 -当你的书店变成了"亚马逊",你需要处理亿级的订单,成千上万的用户同时访问。这时,你就需要**数据库**。 +当用户突破 10 万时,问题开始出现了: +- Excel 打开要等 5 分钟 +- 筛选"北京的用户"要卡顿半天 +- 有一次 Excel 文件损坏,几千个用户数据永久丢失 -**数据库,本质上就是一个"超级 Excel"**,但它专为**海量数据**、**高并发访问**和**数据安全**而设计。 +最致命的是,他想要实现"查看某个用户的所有订单"这个功能——但用户信息和订单分别在不同的 Excel 表里,他只能手动复制粘贴,每次都要花半小时。 -**核心优势对比**: +后来他请教师兄,师兄看了一眼就笑了:"你需要的不是 Excel,而是数据库。" -| 特性 | 记事本 | Excel | 数据库 | -|------|--------|-------|--------| -| 数据量 | 极少 | 中等 (百万级) | 海量 (亿级+) | -| 并发访问 | 单人 | 单人/顺序 | 千人/万人在线 | -| 数据安全 | 低 | 中 | 高 (备份、事务) | -| 数据关系 | 无 | 弱 | 强 (关系型) | -| 查询速度 | 极慢 | 中等 | 极快 (毫秒级) | +改用数据库后,一切都变了: +- 查询"北京的用户"只需要 0.01 秒 +- 通过"关系"自动关联用户和订单,一个 SQL 语句搞定 +- 数据自动备份,再也不怕文件损坏 + +小林从此明白了一个道理:**数据量小的时候,什么都能用;但数据一大,Excel 就是灾难。** +::: + +::: info 💡 核心启示 +数据库不是"更复杂的 Excel",而是完全不同的设计理念: +- **Excel**:为小数据、单人使用设计 +- **数据库**:为大数据、高并发、复杂关联设计 + +选择合适的工具,能让你的系统性能提升成千上万倍。 +::: --- -## 2. 数据库长什么样?从图书馆的视角理解关系模型 +## 2. 核心概念:表、行、列、主键 -最流行的数据库类型是**关系型数据库 (Relational Database)**,比如 MySQL、PostgreSQL。它们的样子其实和 Excel 非常像,但概念更加严谨。 +::: tip 🤔 这些概念和数据库有什么关系? +表、行、列、主键就是数据库的"积木块"。 -### 2.1 核心概念:图书馆的比喻 +想象你要盖房子: +- **表** = 一个房间(存放一类数据) +- **行** = 房间里的一个箱子(一条完整记录) +- **列** = 箱子上的标签(姓名、年龄等) +- **主键** = 箱子的唯一编号(绝对不会重复) -想象你是一个**图书馆管理员**。 +理解这些基础概念,你才能知道数据是如何组织的。 +::: -| 数据库概念 | 图书馆类比 | 解释 | -|------------|------------|------| -| **数据库 (Database)** | 整座图书馆 | 存放所有数据的容器 | -| **表 (Table)** | 一个书架 | 存放同一类数据的集合,比如"用户书架"、"图书书架" | -| **列 (Column)** | 书脊上的标签 | 数据的属性,比如"书名"、"作者"、"出版日期" | -| **行 (Row)** | 书架上的每一本书 | 一条具体的数据记录,比如"《百年孤独》,马尔克斯,1967" | -| **主键 (Primary Key)** | 每本书的 ISBN 编号 | 唯一标识每一行的 ID,绝对不会重复 | +在深入学习数据库之前,我们需要先搞清楚这几个核心概念。为了帮助你理解,我们用图书馆的比喻来类比。 -**举个例子**: +### 2.1 用图书馆比喻理解数据库结构 + +想象你走进一座图书馆,里面的组织和数据库惊人地相似: + +| 概念 | 📚 图书馆比喻 | 实际作用 | 具体例子 | +|------|-------------|----------|----------| +| **数据库 (Database)** | 整座图书馆 | 存放所有数据的容器 | 一个电商网站的数据库 | +| **表 (Table)** | 一个书架 | 存放同一类数据的集合 | 用户表、商品表、订单表 | +| **列 (Column)** | 书脊上的标签 | 数据的属性(字段) | 姓名、年龄、手机号 | +| **行 (Row)** | 书架上的每一本书 | 一条具体的数据记录 | "张三,25岁,北京" | +| **主键 (Primary Key)** | 每本书的 ISBN 编号 | 唯一标识每一行的 ID | user_id = 1001 | + +**看个真实例子**:用户表 (users) + +| user_id (主键) | name | age | city | email | +|:-------------:|------|-----|------|-------| +| 1001 | 张三 | 25 | 北京 | zhangsan@example.com | +| 1002 | 李四 | 30 | 上海 | lisi@example.com | +| 1003 | 王五 | 28 | 北京 | wangwu@example.com | + +- **表**:`users`(存放所有用户数据) +- **列**:`user_id`、`name`、`age`、`city`、`email`(每个用户的属性) +- **行**:每一行是一个用户(如"张三,25岁,北京") +- **主键**:`user_id`(1001、1002、1003,永不重复) + +### 2.2 主键 (Primary Key):数据的"身份证号" + +::: tip 📖 什么是主键? +**主键**就是表中每一行的唯一标识,就像身份证号一样。 + +**关键特点**: +- **唯一性**:绝对不会重复(没有两个人有相同的身份证号) +- **非空**:必须有值(不可能有"没有身份证号"的人) +- **不变性**:一旦设定,不会修改(你的身份证号不会变) + +**常见的做法**: +- 使用自增整数:1、2、3、4... +- 使用 UUID(全球唯一标识符):`550e8400-e29b-41d4-a716-446655440000` +::: + +为什么需要主键?想象一下没有主键的世界: + +**场景**:你想修改"张三"的年龄,但表里有 3 个"张三",系统该改哪一个? + +```sql +-- 没有主键,这会同时修改所有叫"张三"的人! +UPDATE users SET age = 26 WHERE name = '张三'; + +-- 有主键,精确修改 +UPDATE users SET age = 26 WHERE user_id = 1001; +``` + +**主键的黄金法则**:每个表都应该有一个主键,而且永远不要修改它。 + +### 2.3 外键 (Foreign Key):连接表的桥梁 + +这是数据库比 Excel 强大的关键——**表之间可以建立关系**。 + +::: tip 📖 什么是外键? +**外键**是指向另一张表主键的列,用来建立表与表之间的关联。 + +**简单理解**: +- 主键 = 我的身份证号 +- 外键 = 我引用的别人的身份证号 + +**举个例子**:订单表里的 `user_id` 就是外键,它指向用户表的主键。 +::: + +看一个真实的例子: **用户表 (users)**: -| user_id (主键) | name | age | email | -|----------------|------|-----|-------| -| 1 | 张三 | 25 | zhangsan@example.com | -| 2 | 李四 | 30 | lisi@example.com | -| 3 | 王五 | 28 | wangwu@example.com | - -- **表**:`users`(用户书架) -- **列**:`user_id`、`name`、`age`、`email`(书脊标签) -- **行**:每一行是一个用户(书架上的每本书) -- **主键**:`user_id`(ISBN 编号,1、2、3 永不重复) - -<DatabaseRelationDemo /> - -### 2.2 关系 (Relation):数据库的灵魂 - -这是数据库比 Excel 强大的关键。 - -**Excel 的问题:数据冗余** - -在 Excel 中,如果你要记录订单,可能会这样写: - -| 订单号 | 书名 | 价格 | 购买者 | 购买者电话 | 购买者地址 | -|--------|------|------|--------|------------|------------| -| 001 | 百年孤独 | 59 | 张三 | 138xxxx | 北京 | -| 002 | 活着 | 39 | 张三 | 138xxxx | 北京 | -| 003 | 三体 | 99 | 张三 | 138xxxx | 北京 | - -**问题**: -- 张三买了 100 本书,你得重复写 100 次他的电话和地址。 -- 如果张三换了电话,你得修改 100 行,漏改一行就数据不一致了。 -- 数据量爆炸,浪费存储空间。 - -**数据库的解决方案:拆表 + 关联** - -数据库会把数据拆开,存到不同的表里,通过**关系**(外键)把它们连起来。 - -**用户表 (users)**: - -| user_id (主键) | name | phone | address | -|----------------|------|-------|---------| -| 101 | 张三 | 138xxxx | 北京 | -| 102 | 李四 | 139xxxx | 上海 | +| user_id (主键) | name | phone | +|:-------------:|------|-------| +| 1001 | 张三 | 138xxxx | +| 1002 | 李四 | 139xxxx | **订单表 (orders)**: -| order_id (主键) | book_name | price | user_id (外键) | -|-----------------|-----------|-------|----------------| -| 001 | 百年孤独 | 59 | 101 | -| 002 | 活着 | 39 | 101 | -| 003 | 三体 | 99 | 101 | -| 004 | 百年孤独 | 59 | 102 | +| order_id (主键) | product_name | price | user_id (外键) | +|:--------------:|-------------|-------|:-------------:| +| 5001 | iPhone 15 | 5999 | 1001 | +| 5002 | MacBook | 14999 | 1001 | +| 5003 | AirPods | 1999 | 1002 | -**关系解释**: -- `orders` 表里的 `user_id` 列,指向 `users` 表的 `user_id` 主键。 -- 当我们要查看"订单 001 是谁买的"时,数据库会去 `users` 表里查找 `user_id = 101` 的行,发现是"张三"。 -- 这种通过**外键 (Foreign Key)** 建立表与表之间联系的方式,就是**关系 (Relation)** 的含义。 +**关键理解**: +- 订单表里的 `user_id = 1001` 指向用户表里的 `user_id = 1001`(张三) +- 当你要查"订单 5001 是谁买的",数据库会自动去用户表查找 `user_id = 1001` 的用户 **好处**: -- **节省空间**:张三的信息只存一次,不管他买多少本书。 -- **数据一致**:张三换电话,只需要改 `users` 表一行,所有订单关联的电话自动更新。 -- **灵活查询**:可以轻松回答复杂问题,比如"统计每个用户的总消费金额"。 +- **数据不重复**:张三买 100 单商品,他的信息也只在用户表存一次 +- **易于维护**:张三换手机号,只改用户表,所有订单自动关联新手机号 +- **灵活查询**:可以轻松回答"每个用户的总消费是多少"这类复杂问题 + +<DatabaseRelationDemo /> --- -## 3. 如何和数据库说话?SQL 入门与实战 +## 3. 如何和数据库对话?SQL 入门与实战 -你不能直接用鼠标去点数据库(虽然有图形化工具,但本质也是转换成命令),你需要用一种特殊的、标准化的语言来指挥数据库工作。 +你不能直接用鼠标"点"数据库(虽然有图形化工具,但本质也是转换成命令),你需要用一种特殊的语言来指挥数据库工作。 这种语言就是 **SQL (Structured Query Language,结构化查询语言)**。 -好消息是,SQL 非常接近自然英语,读起来就像一句话。 +好消息是:SQL 非常接近自然英语,读起来就像在说话。 -### 3.1 SQL 的核心命令:CRUD +### 3.1 SQL 的核心操作:CRUD 大部分时候,你只需要掌握四种操作,江湖人称 **CRUD**: -| 操作 | 英文 | SQL 关键字 | 类比 Excel | -|------|------|------------|------------| -| **C**reate | 创建/插入 | `INSERT` | 在末尾新增一行 | -| **R**ead | 读取/查询 | `SELECT` | 筛选、查找 | -| **U**pdate | 更新/修改 | `UPDATE` | 修改单元格内容 | -| **D**elete | 删除 | `DELETE` | 删除一行 | +| 操作 | 英文 | SQL 关键字 | 通俗理解 | +|------|------|------------|----------| +| **C**reate | 创建 | `INSERT` | 新增一条数据 | +| **R**ead | 读取 | `SELECT` | 查询数据 | +| **U**pdate | 更新 | `UPDATE` | 修改数据 | +| **D**elete | 删除 | `DELETE` | 删除数据 | -### 3.2 实战示例:淘宝订单系统 +::: tip 📊 从表格中你能看到什么? +这四个操作覆盖了数据处理的全部场景: +- **Create**:用户注册时,插入一条新用户记录 +- **Read**:用户登录时,查询用户名和密码 +- **Update**:用户修改个人资料时,更新表中的数据 +- **Delete**:用户注销账号时,删除用户数据 -假设我们有以下两张表: +记住这四个,你就掌握了 80% 的日常 SQL 操作。 +::: -**用户表 (users)**: +### 3.2 查询数据 (SELECT):数据库最常用的操作 -| user_id | name | age | city | -|---------|------|-----|------| -| 1 | 张三 | 25 | 北京 | -| 2 | 李四 | 30 | 上海 | -| 3 | 王五 | 28 | 北京 | +查询是数据库最重要的功能,也是性能优化的关键。 -**商品表 (products)**: - -| product_id | name | price | stock | -|------------|------|-------|-------| -| 101 | iPhone 15 | 5999 | 1000 | -| 102 | MacBook Pro | 14999 | 500 | -| 103 | AirPods Pro | 1999 | 2000 | - -#### 查询数据 (Read) - -**示例 1**:查找所有年龄大于 25 岁的用户 +**示例 1**:查找所有北京的用户 ```sql -SELECT name, age FROM users WHERE age > 25; +SELECT name, age FROM users WHERE city = '北京'; ``` -**逐词翻译**: +**逐词理解**: - `SELECT name, age`:选择 name 和 age 这两列 - `FROM users`:从 users 这张表 -- `WHERE age > 25`:在 age 大于 25 的条件下 +- `WHERE city = '北京'`:在 city 等于"北京"的条件下 **返回结果**: | name | age | |------|-----| -| 李四 | 30 | +| 张三 | 25 | | 王五 | 28 | **示例 2**:查找价格在 5000 到 15000 之间的商品 ```sql -SELECT name, price FROM products WHERE price BETWEEN 5000 AND 15000; +SELECT name, price FROM products +WHERE price BETWEEN 5000 AND 15000; ``` -#### 插入数据 (Create) +**示例 3**:模糊搜索(查找名字包含"张"的用户) + +```sql +SELECT name FROM users WHERE name LIKE '%张%'; +``` + +::: warning ⚠️ 性能陷阱:LIKE 的使用 +`LIKE '%张%'` 会导致**全表扫描**,数据量大时非常慢。 + +**优化建议**: +- ❌ 不要用 `LIKE '%张%'`(前后都有 %) +- ✅ 可以用 `LIKE '张%'`(只有后面有 %) + +因为 `LIKE '张%'` 可以利用索引,而 `LIKE '%张%'` 无法使用索引。 +::: + +### 3.3 插入数据 (INSERT):新增记录 **示例**:新增一个用户 ```sql -INSERT INTO users (user_id, name, age, city) -VALUES (4, '赵六', 35, '广州'); +INSERT INTO users (user_id, name, age, city, email) +VALUES (1004, '赵六', 35, '广州', 'zhaoliu@example.com'); ``` -**逐词翻译**: +**逐词理解**: - `INSERT INTO users`:插入到 users 表 -- `(user_id, name, age, city)`:这几列 -- `VALUES (4, '赵六', 35, '广州')`:值分别是... +- `(user_id, name, age, city, email)`:指定要插入的列 +- `VALUES (1004, '赵六', ...)`:对应的值 -#### 更新数据 (Update) - -**示例**:给所有北京的用户年龄加 1 岁 +**批量插入**(更高效): ```sql -UPDATE users -SET age = age + 1 -WHERE city = '北京'; +INSERT INTO users (name, age, city) VALUES +('小明', 25, '北京'), +('小红', 28, '上海'), +('小刚', 30, '广州'); ``` -**⚠️ 重要警告**:如果你忘记写 `WHERE city = '北京'`,这条命令会把**所有用户**的年龄都加 1!在生产环境中,这是一个极其危险的错误。 +### 3.4 更新数据 (UPDATE):修改记录 -#### 删除数据 (Delete) - -**示例**:删除用户 ID 为 4 的用户 +**示例**:给所有北京的用户年龄加 1 ```sql -DELETE FROM users WHERE user_id = 4; +UPDATE users SET age = age + 1 WHERE city = '北京'; ``` -**⚠️ 重要警告**:和 UPDATE 一样,如果忘记写 `WHERE`,你会删除整张表的所有数据!这在生产环境中是灾难性的。 +::: danger ❌ 非常危险:别忘了 WHERE! +如果你忘记写 `WHERE` 子句,会修改**所有行**! -### 3.3 多表查询:JOIN 的力量 +```sql +-- 危险!会把所有用户的年龄都改成 26 +UPDATE users SET age = 26; -还记得我们讲过的"关系"吗?SQL 最强大的地方在于可以一次性查询多张关联的表。 +-- 正确:只修改 user_id = 1001 的用户 +UPDATE users SET age = 26 WHERE user_id = 1001; +``` -**示例场景**:查询"张三购买过的所有商品" +**真实教训**:2012 年,某知名公司因为工程师忘记写 WHERE,导致生产环境数百万用户数据被错误更新,系统瘫痪 4 小时,损失巨大。 +::: -假设我们还有一张订单表 (orders): +### 3.5 删除数据 (DELETE):删除记录 -| order_id | user_id | product_id | quantity | order_date | -|----------|---------|--------------|----------|------------| -| 1001 | 1 | 101 | 1 | 2024-01-15 | -| 1002 | 1 | 103 | 2 | 2024-01-16 | -| 1003 | 2 | 101 | 1 | 2024-01-17 | +**示例**:删除 user_id = 1004 的用户 + +```sql +DELETE FROM users WHERE user_id = 1004; +``` + +::: danger ❌ 双重危险:DELETE 更需要 WHERE! +```sql +-- 危险!会删除整张表的所有数据! +DELETE FROM users; + +-- 正确:只删除指定行 +DELETE FROM users WHERE user_id = 1004; +``` + +**最佳实践**: +1. 删除前先用 SELECT 确认数据 +2. 在重要系统中,使用"软删除"(添加 `is_deleted` 字段标记删除) +3. 生产环境操作前先备份数据 +::: + +### 3.6 多表查询 (JOIN):数据库的魔法时刻 + +还记得我们讲过的"外键"吗?SQL 最强大的地方在于可以一次性查询多张关联的表。 + +**场景**:查询"张三买过的所有商品" + +假设我们有三张表: + +**用户表 (users)**: +| user_id | name | +|---------|------| +| 1001 | 张三 | + +**商品表 (products)**: +| product_id | name | price | +|------------|------|-------| +| 201 | iPhone 15 | 5999 | +| 202 | MacBook | 14999 | + +**订单表 (orders)**: +| order_id | user_id | product_id | quantity | +|----------|---------|------------|----------| +| 5001 | 1001 | 201 | 1 | +| 5002 | 1001 | 202 | 2 | **SQL 查询**: ```sql -SELECT u.name, p.name AS product_name, o.quantity, o.order_date +SELECT u.name, p.name AS product_name, p.price, o.quantity FROM orders o JOIN users u ON o.user_id = u.user_id JOIN products p ON o.product_id = p.product_id @@ -300,22 +374,26 @@ WHERE u.name = '张三'; **返回结果**: -| name | product_name | quantity | order_date | -|------|--------------|----------|------------| -| 张三 | iPhone 15 | 1 | 2024-01-15 | -| 张三 | AirPods Pro | 2 | 2024-01-16 | +| name | product_name | price | quantity | +|------|--------------|-------|----------| +| 张三 | iPhone 15 | 5999 | 1 | +| 张三 | MacBook | 14999 | 2 | -通过 `JOIN`,我们把三张表的数据关联在了一起,得到了完整的答案。 +**理解 JOIN 的过程**: +1. `FROM orders o`:从订单表开始 +2. `JOIN users u ON o.user_id = u.user_id`:通过 user_id 关联用户表 +3. `JOIN products p ON o.product_id = p.product_id`:通过 product_id 关联商品表 +4. `WHERE u.name = '张三'`:筛选张三的订单 <SqlPlaygroundDemo /> --- -## 4. 为什么数据库这么快?索引原理与 B+ 树揭秘 +## 4. 为什么数据库这么快?索引原理揭秘 这是数据库最神奇的地方,也是面试中最爱问的问题。 -如果你在 Excel 里找"所有姓张的人",Excel 可能需要从第一行扫到最后一行。这就是**全表扫描 (Full Table Scan)**——数据越多,速度越慢。 +如果你在 Excel 里查找"所有姓张的人",Excel 需要从第一行扫到最后一行。这就是**全表扫描**——数据越多,速度越慢。 但在数据库里,即使有 10 亿行数据,查找也只需要几毫秒。 @@ -323,76 +401,82 @@ WHERE u.name = '张三'; ### 4.1 直观理解:字典的启示 -想象你要在一本没有目录、没有页码的 1000 页书里找一个词。你该怎么办? +想象你要在一本没有目录的 1000 页书里找一个词。你该怎么办? -**只能一页一页翻**——这就是全表扫描。 +**只能一页一页翻**——这就是全表扫描,平均需要翻 500 页。 -但现在,你手里有一本**字典**。字典有一个按字母排序的**索引**。 +但如果这本书记有**拼音索引**呢? 你要找"数据库"这个词: -1. 翻到"数"字开头的区域(快速定位)。 -2. 在"数"字区域内,按第二个字"据"的顺序找。 -3. 很快,你就定位到了"数据库"这个词所在的页码。 +1. 翻到索引,找到"数"字开头的区域 +2. 在"数"字区域内,找"据"字 +3. 索引告诉你:在第 256 页 -这就是**索引查找**——不需要翻完整本书,只需要查索引,直接跳转到目标位置。 +你只需要翻 3 次就能找到!这就是**索引查找**。 -### 4.2 全表扫描 vs 索引查找 +**数据库的索引就像书的目录**: +- 没有索引:逐行扫描(10 亿行 = 数分钟) +- 有索引:直接跳转(10 亿行 = 3 次磁盘 I/O = 几毫秒) -让我们通过一个具体的例子来感受两者的差异。 +### 4.2 全表扫描 vs 索引查找:速度对比 -假设我们有一张用户表,里面有 1000 万条用户记录。 +假设我们有一张用户表,有 1000 万条记录。 -**场景:查找 ID = 5,555,555 的用户** +**场景**:查找 `user_id = 5,555,555` 的用户 -| 方式 | 过程 | 需要检查的行数 | 耗时(估算) | -|------|------|----------------|--------------| -| **全表扫描** | 从第 1 行开始,一行一行看,直到找到 ID = 5,555,555 | 平均 500 万行 | 数秒 ~ 数十秒 | -| **索引查找** | 查索引树,直接跳到目标位置 | 约 3-4 次比较 | 数毫秒 | +| 方式 | 过程 | 需要检查的行数 | 耗时估算 | +|------|------|----------------|----------| +| **全表扫描** | 从第 1 行开始,一行一行看 | 平均 500 万行 | 5-30 秒 | +| **索引查找** | 查索引树,直接跳到目标位置 | 3-4 次比较 | 0.003 秒 | -**速度差距**:数千倍甚至数万倍! +**速度差距:数千倍!** + +::: tip 💡 核心启示 +索引不是银弹,它有代价: +- **占用空间**:索引需要额外的存储空间 +- **降低写入速度**:每次 INSERT/UPDATE/DELETE 都要更新索引 + +**什么时候建索引?** +- 经常用来查询的列(WHERE、JOIN 的条件) +- 数据量大(几千行以下不需要) + +**什么时候不建索引?** +- 很少查询的列 +- 频繁更新的列 +- 数据量小的表 +::: ### 4.3 底层数据结构:B+ 树 -真实的索引并不是简单的"字母排序列表",而是一种精心设计的数据结构,叫做 **B+ 树 (B+ Tree)**。 +真实的索引不是简单的"字母列表",而是一种精心设计的数据结构,叫做 **B+ 树 (B+ Tree)**。 -#### 为什么是"树"? +::: tip 📖 什么是 B+ 树? +**B+ 树**是一种"矮胖"的树形数据结构: -想象一棵倒过来的树: -- **根节点 (Root)**:在最顶层,像树干。 -- **中间节点 (Internal Nodes)**:在树干和树叶之间,像树枝。 -- **叶子节点 (Leaf Nodes)**:在最底层,像树叶,存储着真正的数据或数据地址。 - -#### B+ 树的特点:矮胖 - -B+ 树有一个非常聪明的设计:**它非常"矮胖"**。 - -- **矮**:从根到叶子,通常只有 3-4 层。 -- **胖**:每个节点可以存储很多个键值(比如几百个)。 +- **矮**:从根到叶子通常只有 3-4 层 +- **胖**:每个节点可以存储几百个键值 **为什么要"矮胖"?** -因为数据库的数据最终是存储在**磁盘**(硬盘或 SSD)上的。 +因为数据存储在磁盘上,每次读取磁盘(I/O)都非常慢(比内存慢几千倍)。B+ 树的设计目标就是**尽量减少磁盘 I/O 次数**。 -每次从磁盘读取数据都需要**一次 I/O 操作**(磁盘寻道),这个操作相对于内存计算来说,非常**慢**(慢几千倍)。 +- 3-4 层高度 = 最多 3-4 次磁盘读取 +- 每层存大量数据 = 保证树不会太高 +::: -所以,B+ 树的设计目标是:**尽量减少磁盘 I/O 次数**。 - -- **矮**:只有 3-4 层,意味着最多只需要 3-4 次磁盘读取就能找到数据。 -- **胖**:每一层能容纳大量数据,保证树不会变高。 - -**实际例子**: +**真实例子**: 假设一棵 B+ 树的每个节点可以存储 1000 个键值: -- 根节点:1000 个键值 → 指向 1000 个中间节点 -- 中间节点:每个存 1000 个键值 → 指向 1000 个叶子节点 -- 叶子节点:每个存 1000 条真实数据 +- **根节点**:1000 个键值 → 指向 1000 个子节点 +- **中间节点**:每个存 1000 个键值 → 指向 1000 个叶子节点 +- **叶子节点**:每个存 1000 条真实数据 **总数据量** = 1000 × 1000 × 1000 = **10 亿条数据** **树的高度** = **3 层** -这意味着,在一个存储了 10 亿条数据的 B+ 树索引中,找到任意一条数据,**只需要 3 次磁盘 I/O**! +这意味着:在 10 亿条数据中查找任意一条,只需要 **3 次磁盘 I/O**! 这就是数据库查询飞快的秘密。 @@ -400,135 +484,185 @@ B+ 树有一个非常聪明的设计:**它非常"矮胖"**。 --- -## 5. 事务:当多人同时抢票时,系统如何保证不重复售票? +## 5. 事务:如何保证数据不丢、不乱? -想象一下春运期间的 12306: +想象一下春运抢票的场景: -- 时间 T1:用户 A 查询,发现"G1234 次列车还剩 1 张票"。 -- 时间 T2:用户 B 也查询,也发现"还剩 1 张票"。 -- 时间 T3:用户 A 点击"购买",系统减库存,票卖给了 A。 -- 时间 T4:用户 B 点击"购买"——如果没有保护机制,系统会再次减库存,把同一张票卖给 B! +- 时间 T1:用户 A 查询,发现"G1234 次列车还剩 1 张票" +- 时间 T2:用户 B 也查询,也发现"还剩 1 张票" +- 时间 T3:用户 A 点击"购买",系统扣库存,票卖给了 A +- 时间 T4:用户 B 点击"购买"——如果没有保护机制,系统会再次扣库存,把同一张票卖给 B! 这就是典型的**并发冲突**问题。 ### 5.1 什么是事务 (Transaction)? -**事务**是数据库的一组操作,这些操作要么全部成功,要么全部失败,不会出现"做了一半"的情况。 +**事务**是数据库的一组操作,这些操作**要么全部成功,要么全部失败**,不会出现"做了一半"的情况。 + +::: tip 🤖 生活中的例子 +**银行转账**就是一个典型的事务: + +1. 从账户 A 扣除 100 元 +2. 给账户 B 增加 100 元 + +如果第 1 步成功了,但第 2 步失败了(比如断电),会发生什么? +- **没有事务**:账户 A 的钱没了,账户 B 没收到钱,钱凭空消失了 +- **有事务**:系统发现第 2 步失败,自动回滚第 1 步,两个账户都恢复原状 + +这就是事务的**原子性**:要么全做,要么全不做。 +::: + +### 5.2 事务的四大特性 (ACID) 事务有四大特性,简称 **ACID**: -| 特性 | 英文 | 含义 | 12306 的例子 | +| 特性 | 英文 | 含义 | 银行转账的例子 | |------|------|------|--------------| -| **A**tomicity | 原子性 | 操作要么全做,要么全不做 | 买票时扣款和出票必须同时成功,不能只扣钱不出票 | -| **C**onsistency | 一致性 | 数据始终保持合法状态 | 票卖完了,库存必须是 0,不能是负数 | -| **I**solation | 隔离性 | 多个事务互不影响 | A 在买票时,B 看到的结果应该是"已售罄"或"还剩 1 张",不会看到中间状态 | -| **D**urability | 持久性 | 一旦提交,数据永久保存 | 订单成功后,即使服务器宕机,已售出的票也不会丢失 | +| **A**tomicity | 原子性 | 要么全做,要么全不做 | 扣款和入账必须同时成功,不能只扣钱不入账 | +| **C**onsistency | 一致性 | 数据始终保持合法状态 | 转账前后,两个账户的总金额应该不变 | +| **I**solation | 隔离性 | 多个事务互不影响 | A 在转账时,B 看到的应该是"转账前"或"转账后"的余额,不能看到中间状态 | +| **D**urability | 持久性 | 一旦提交,数据永久保存 | 转账成功后,即使断电,账户余额也不会变回去 | -### 5.2 事务的隔离级别:鱼与熊掌的权衡 +::: tip 📊 从表格中你能看到什么? +这四个特性保证了数据的安全性: -理论上,我们希望事务完全隔离(最高的隔离性)。但在实际系统中,**完全隔离 = 性能极差**(因为需要大量加锁,其他事务只能等待)。 +- **原子性**:防止"做一半"(扣了钱但没到账) +- **一致性**:防止数据不合理(转账后总金额变了) +- **隔离性**:防止并发冲突(两个人同时修改同一数据) +- **持久性**:防止数据丢失(提交后断电也不影响) -因此,数据库提供了四种**隔离级别**,让开发者根据业务场景权衡: +没有这些保证,银行系统根本无法运行。 +::: -| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 适用场景 | -|----------|------|------------|------|----------| -| **读未提交** | 可能 | 可能 | 可能 | 几乎不用(数据可能错误) | -| **读已提交** | 不可能 | 可能 | 可能 | 普通业务(Oracle 默认) | -| **可重复读** | 不可能 | 不可能 | 可能 | 银行转账(MySQL 默认) | -| **串行化** | 不可能 | 不可能 | 不可能 | 极端严格场景(极少用) | +### 5.3 事务的隔离级别:权衡安全与性能 -**名词解释**: -- **脏读**:读到了其他事务还没提交的数据(可能回滚)。 -- **不可重复读**:同一个事务里,两次读同一个数据,结果不一样(因为被其他事务修改了)。 -- **幻读**:同一个事务里,两次查询,结果集的行数不一样(因为其他事务插入或删除了数据)。 +理论上,我们希望事务完全隔离。但**完全隔离 = 性能极差**(因为需要大量加锁,其他事务只能等待)。 + +因此,数据库提供了四种**隔离级别**: + +| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 | 适用场景 | +|----------|------|------------|------|------|----------| +| **读未提交** | 可能 | 可能 | 可能 | 最快 | 几乎不用(数据可能错误) | +| **读已提交** | 不可能 | 可能 | 可能 | 较快 | 普通业务(Oracle 默认) | +| **可重复读** | 不可能 | 不可能 | 可能 | 中等 | 银行转账(MySQL 默认) | +| **串行化** | 不可能 | 不可能 | 不可能 | 最慢 | 极端严格场景(极少用) | + +::: tip 📖 三个"读"是什么意思? +- **脏读**:读到了其他事务还没提交的数据(可能回滚,数据不准确) +- **不可重复读**:同一事务里,两次读同一数据,结果不一样(被其他事务修改了) +- **幻读**:同一事务里,两次查询,结果集的行数不一样(其他事务插入/删除了数据) + +**通俗例子**(银行查余额): +- **脏读**:你查到余额 1000 元,但对方事务回滚了,实际只有 100 元 +- **不可重复读**:你第一次查余额 1000 元,第二次查变成 800 元(被扣款了) +- **幻读**:你第一次查到 5 笔交易,第二次查变成 6 笔(新增了一笔) +::: <TransactionACIDDemo /> --- -## 6. 性能优化:如何让你的查询快 1000 倍? +## 6. 性能优化:让查询快 1000 倍的实战技巧 -现在你已经理解了索引、事务这些核心概念。但在真实项目中,你可能会遇到这样的问题: - -- "明明建了索引,为什么查询还是很慢?" -- "这条 SQL 昨天还很快,今天怎么突然卡死了?" -- "并发一高,数据库就挂了,怎么办?" +现在你已经理解了索引、事务这些核心概念。但在真实项目中,你可能会遇到各种性能问题。 本节将给出**可直接落地的优化策略**。 ### 6.1 索引使用避坑指南 -**坑 1:在索引列上使用函数,导致索引失效** +::: warning ⚠️ 常见错误:索引失效的坑 +很多时候,你明明建了索引,但查询还是很慢——因为索引**失效**了。 + +**导致索引失效的常见原因**: +1. 在索引列上使用函数 +2. 隐式类型转换 +3. LIKE 查询以 % 开头 +4. OR 条件(部分情况) +5. 复合索引不满足最左前缀原则 +::: + +**坑 1:在索引列上使用函数** ```sql --- 错误:对索引列使用函数,无法使用索引 +-- ❌ 错误:对索引列使用函数,无法使用索引 SELECT * FROM users WHERE YEAR(created_at) = 2024; --- 正确:改写为范围查询,可以使用索引 -SELECT * FROM users WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'; +-- ✅ 正确:改写为范围查询,可以使用索引 +SELECT * FROM users +WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'; ``` -**坑 2:隐式类型转换,导致索引失效** +**坑 2:隐式类型转换** ```sql -- 假设 user_id 是 int 类型 --- 错误:传字符串,可能导致隐式转换 +-- ❌ 错误:传字符串,导致隐式转换,无法使用索引 SELECT * FROM users WHERE user_id = '123'; --- 正确:传对应类型 +-- ✅ 正确:传对应类型 SELECT * FROM users WHERE user_id = 123; ``` -**坑 3:LIKE 查询以 % 开头,无法使用索引** +**坑 3:LIKE 以 % 开头** ```sql --- 错误:以 % 开头,无法使用索引 +-- ❌ 错误:以 % 开头,无法使用索引 SELECT * FROM users WHERE name LIKE '%张三%'; --- 正确:以固定前缀开头,可以使用索引 +-- ✅ 正确:以固定前缀开头,可以使用索引 SELECT * FROM users WHERE name LIKE '张三%'; + +-- ✅ 或者使用全文索引(适用于文本搜索) +SELECT * FROM users WHERE MATCH(name) AGAINST('张三'); ``` -### 6.2 SQL 优化技巧模板 +### 6.2 SQL 优化实战模板 **模板 1:分页优化(深分页问题)** +::: details 查看问题与解决方案 ```sql --- 问题:当 OFFSET 很大时,查询会越来越慢 -SELECT * FROM orders ORDER BY created_at DESC LIMIT 10 OFFSET 1000000; - --- 优化方案 1:使用覆盖索引 +-- ❌ 问题:OFFSET 很大时,查询越来越慢 SELECT * FROM orders -WHERE created_at < '上次查询的最小时间戳' -ORDER BY created_at DESC LIMIT 10; +ORDER BY created_at DESC +LIMIT 10 OFFSET 1000000; --- 优化方案 2:使用主键范围查询 +-- ✅ 优化方案 1:使用上次查询的时间戳作为游标 SELECT * FROM orders -WHERE order_id > 上次查询的最大order_id -ORDER BY order_id LIMIT 10; +WHERE created_at < '2024-01-15 12:00:00' +ORDER BY created_at DESC +LIMIT 10; + +-- ✅ 优化方案 2:使用主键范围查询 +SELECT * FROM orders +WHERE order_id > 1000000 +ORDER BY order_id +LIMIT 10; ``` +::: **模板 2:批量插入优化** ```sql --- 低效:多次单条插入 +-- ❌ 低效:多次单条插入(网络往返多次) INSERT INTO users (name, age) VALUES ('张三', 25); INSERT INTO users (name, age) VALUES ('李四', 30); +INSERT INTO users (name, age) VALUES ('王五', 28); --- 高效:单条 SQL 批量插入 +-- ✅ 高效:单条 SQL 批量插入(只需一次网络往返) INSERT INTO users (name, age) VALUES ('张三', 25), ('李四', 30), ('王五', 28); ``` -**模板 3:避免 SELECT ** +**模板 3:避免 SELECT *** ```sql --- 低效:返回所有列 +-- ❌ 低效:返回所有列(包括不需要的大字段) SELECT * FROM users WHERE user_id = 1; --- 高效:只返回需要的列 +-- ✅ 高效:只返回需要的列 SELECT user_id, name, email FROM users WHERE user_id = 1; ``` @@ -536,10 +670,18 @@ SELECT user_id, name, email FROM users WHERE user_id = 1; | 场景 | 问题 | 解决方案 | |------|------|----------| -| 热点数据 | 某行数据被频繁读写,导致锁竞争 | 缓存 + 读写分离;或分段锁 | -| 秒杀场景 | 瞬间高并发扣减库存 | 乐观锁 + 库存预热 + 队列削峰 | -| 慢查询 | 复杂查询拖垮数据库 | 索引优化 + 查询拆分 + 读写分离 | -| 连接数耗尽 | 太多并发请求导致连接池耗尽 | 连接池优化 + 限流 + 服务降级 | +| **热点数据** | 某行数据被频繁读写,导致锁竞争 | 使用缓存(Redis)+ 读写分离 | +| **秒杀场景** | 瞬间高并发扣减库存 | 乐观锁 + 库存预热 + 消息队列削峰 | +| **慢查询** | 复杂查询拖垮数据库 | 索引优化 + 查询拆分 + 读写分离 | +| **连接数耗尽** | 太多并发请求导致连接池耗尽 | 连接池优化 + 限流 + 服务降级 | + +::: tip 💡 核心启示 +性能优化的基本原则: +1. **先测量,后优化**:用 `EXPLAIN` 分析查询计划,找到真正的瓶颈 +2. **索引优先**:80% 的性能问题都可以通过优化索引解决 +3. **减少数据库压力**:能用缓存就用缓存,能异步就异步 +4. **分而治之**:大表拆分成小表,大查询拆分成小查询 +::: <QueryOptimizationDemo /> @@ -547,43 +689,26 @@ SELECT user_id, name, email FROM users WHERE user_id = 1; ## 7. 总结与学习路线 -现在你已经打通了从"Excel 表格"到"B+ 树索引"的任督二脉: +让我们用一张表格来回顾数据库的核心概念: -1. **数据库的本质**:处理海量数据的"超级 Excel",专为高并发、高安全、高性能而设计。 -2. **数据的组织**:通过**表**、**列**、**行**、**主键**组织数据,通过**关系**(外键)连接多张表,消除冗余。 -3. **SQL 语言**:使用 `SELECT`、`INSERT`、`UPDATE`、`DELETE` 等命令与数据库对话,通过 `JOIN` 实现多表查询。 -4. **索引原理**:使用 **B+ 树**作为底层数据结构,通过"矮胖"的树形结构,将磁盘 I/O 次数降至最低,实现毫秒级查询。 -5. **事务安全**:通过 **ACID 特性**和**隔离级别**,保证数据的一致性、完整性和并发安全。 -6. **性能优化**:通过合理的索引设计、SQL 优化和高并发策略,让查询效率提升 1000 倍。 +| 概念 | 一句话解释 | 解决的问题 | 关键点 | +|------|-----------|-----------|--------| +| **表、行、列** | 数据的组织方式 | 如何存储结构化数据 | 表 = Excel 工作表,行 = 记录,列 = 字段 | +| **主键** | 每行的唯一标识 | 如何精确找到一行数据 | 唯一、非空、不变 | +| **外键** | 连接表的桥梁 | 如何关联不同表的数据 | 指向另一张表的主键 | +| **SQL** | 和数据库对话的语言 | 如何增删改查数据 | SELECT、INSERT、UPDATE、DELETE | +| **索引** | 加速查询的数据结构 | 如何快速找到数据 | B+ 树,减少磁盘 I/O | +| **事务** | 保证数据安全的机制 | 如何防止并发冲突和丢失数据 | ACID:原子性、一致性、隔离性、持久性 | -**下一步建议**: +::: info 写在最后 +数据库是一个博大精深的主题,本文只是入门。如果你想继续深入学习,建议按以下路线: -- 如果你想动手实践,可以尝试安装 **MySQL** 或 **PostgreSQL**,亲手创建几张表,插入数据,体验 SQL 的强大。 -- 如果你对后端开发感兴趣,可以学习如何使用 **ORM**(如 SQLAlchemy、Prisma)在代码中操作数据库,而不需要手写 SQL。 -- 如果你想深入底层,可以研究 **InnoDB 存储引擎**的原理,了解事务、锁、MVCC 等高级概念。 +**下一步学习**: +1. **动手实践**:安装 MySQL 或 PostgreSQL,创建表、插入数据、写 SQL 查询 +2. **ORM 框架**:学习如何在代码中使用数据库(如 SQLAlchemy、Prisma、TypeORM) +3. **索引优化**:深入研究复合索引、覆盖索引、索引下推等高级主题 +4. **事务原理**:了解 MVCC(多版本并发控制)、锁机制、隔离级别实现 +5. **分布式数据库**:学习分库分表、读写分离、主从复制等架构 ---- - -## 8. 名词速查表 (Glossary) - -| 名词 | 英文 | 解释 | -|------|------|------| -| **数据库** | Database | 存储和管理数据的系统,专为海量数据、高并发访问而设计 | -| **关系型数据库** | Relational Database | 基于关系模型组织数据的数据库,如 MySQL、PostgreSQL | -| **表** | Table | 数据库中存储同一类数据的集合,由行和列组成 | -| **列** | Column | 表的垂直维度,代表数据的一个属性(如"姓名"、"年龄") | -| **行** | Row | 表的水平维度,代表一条具体的数据记录 | -| **主键** | Primary Key | 唯一标识表中每一行的列,值不能重复 | -| **外键** | Foreign Key | 建立表与表之间关联的列,指向另一张表的主键 | -| **关系** | Relation | 表与表之间通过主键和外键建立的关联 | -| **SQL** | Structured Query Language | 结构化查询语言,用于与数据库通信的标准语言 | -| **索引** | Index | 加速数据查询的数据结构,类似于书的目录 | -| **B+ 树** | B+ Tree | 数据库索引常用的数据结构,具有矮胖、有序、支持范围查询的特点 | -| **全表扫描** | Full Table Scan | 不通过索引,逐行扫描整张表的查询方式,效率低 | -| **磁盘 I/O** | Disk I/O | 从磁盘读取或写入数据的操作,相对于内存操作非常慢 | -| **事务** | Transaction | 一组数据库操作,要么全部成功,要么全部失败 | -| **ACID** | Atomicity, Consistency, Isolation, Durability | 事务的四大特性:原子性、一致性、隔离性、持久性 | -| **隔离级别** | Isolation Level | 事务并发时的隔离程度,分为读未提交、读已提交、可重复读、串行化 | -| **脏读** | Dirty Read | 读到了其他事务未提交的数据 | -| **不可重复读** | Non-repeatable Read | 同一事务内两次读取同一数据,结果不同 | -| **幻读** | Phantom Read | 同一事务内两次查询,结果集的行数不同 | +记住:**理论 + 实践 = 真正的掌握**。 +::: diff --git a/docs/zh-cn/appendix/deployment.md b/docs/zh-cn/appendix/deployment.md index 2c105f7..719b48a 100644 --- a/docs/zh-cn/appendix/deployment.md +++ b/docs/zh-cn/appendix/deployment.md @@ -1,263 +1,592 @@ -# 部署与上线 (Deployment & Release) +# 服务上线之旅:从代码到用户眼中的网页 -> 💡 **学习指南**:本章节不讲复杂的服务器运维,而是通过“交互式体验”,带你从零看懂一个网站是如何从你的电脑“跑”到用户手机里的。 - -## 0. 全局概览:一个请求的“奇幻漂流” - -你有没有想过,当你在浏览器输入网址并回车的那一瞬间,到底发生了什么? -其实,这就像是一次**快递配送**的过程。 - -不过,根据你要“送”的东西不同,配送路线也会稍微有点不一样。 - -**为什么会有这些区别呢?** -就像在现实生活中,不同的东西有不同的送法: - -1. **发传单(静态网页)**: - - _场景_:公司的介绍页、博客文章。 - - _特点_:内容是**死**的,印好了就不变了。 - - _做法_:直接把“传单”贴在离用户最近的宣传栏(CDN)上。谁来看都一样,速度最快,成本最低。 - -2. **送乐高积木(SPA 单页应用)**: - - _场景_:类似飞书、Notion 这种复杂的网页软件。 - - _特点_:交互特别多,像个**软件**。 - - _做法_:先给你寄一个“空盒子”和一大堆“零件”(JS代码),你的浏览器收到后,自己在本地把页面“拼”出来。 - - _好处_:一旦加载完,点哪里都很快,因为不需要再找服务器要页面了,只需要要数据。 - -3. **送热披萨(SSR 服务端渲染)**: - - _场景_:股票大盘、新闻头条、个性化推荐。 - - _特点_:数据**实时变动**,或者需要**千人千面**。 - - _做法_:必须在用户下单的那一秒,由办事员(服务器)现场查数据、现场组装好页面,再热乎乎地交给你。 - - _好处_:数据绝对新鲜,而且搜索引擎(爬虫)最喜欢这种完整的页面。 - -下面这个互动图,带你体验这三种不同的“配送路线”: - -<DeploymentArchitecture /> - -不管你选择哪种模式,一个**完整的请求**通常都要经过以下这些关卡。 -让我们看看它们都是干什么的,以及**为什么**我们缺不了它们: - -1. **用户 (User) —— 寄件人** - - _动作_:在浏览器输入网址,回车。 - - _人话_:就像是你发出了一个“我想看网页”的请求包裹。 - -2. **DNS (域名解析) —— 查号台** - - _为什么需要?_ 电脑只认识数字(IP地址),人脑只记得住单词(域名)。 - - _人话_:它负责告诉你“baidu.com”这个名字对应的**门牌号 (IP地址)** 到底是哪一家。 - -3. **CDN (内容分发) —— 家门口的快递柜** - - _为什么需要?_ 服务器可能在地球另一边,传一张图片过来太慢了。 - - _人话_:如果你要的东西(图片、视频)在楼下的快递柜(CDN节点)里正好有备份,直接给你,**不用跑远路**去总仓库取。 - -4. **WAF (防火墙) —— 小区保安** - - _为什么需要?_ 互联网上有很多坏人(黑客)想搞破坏。 - - _人话_:它站在门口,把带炸弹的、发传单的(恶意攻击)都**拦在外面**,只让正常的客人进去。 - -5. **LB (负载均衡) —— 大堂经理** - - _为什么需要?_ 访问的人太多了,一台服务器累死也干不完。 - - _人话_:它看着后面开着的 10 个窗口(服务器),把你引导到那个**最空闲**的窗口去办理业务,不让你干等。 - -6. **Server (服务器) —— 办事员** - - _为什么需要?_ 总得有人真正干活,处理你的订单、计算金额。 - - _人话_:他是真正**处理业务**的人,负责把网页内容拼好,或者把数据算出来给你。 - -7. **DB (数据库) —— 档案室** - - _为什么需要?_ 办事员脑子记不住那么多数据,而且断电了不能忘。 - - _人话_:这里**永久保存**着你的账号、密码、历史订单等核心机密数据。 - -弄懂了这个流程,部署就不难了。部署无非就是把这些环节一个个打通。 +> **学习指南**:本章节带你完整体验"一个服务上线"的全过程。我们不讲复杂的运维术语,而是通过小明的咖啡店网站上线故事,让你看懂代码是怎么变成用户能访问的网站的。 --- -## 1. 域名与 DNS:给你的网站起个名 +## 0. 引言:小明的咖啡店网站要上线了 -你想让别人访问你的网站,总不能给人家一串冷冰冰的数字(IP 地址:`123.45.67.89`)吧? -你需要一个好记的名字,比如 `my-cool-site.com`。这就是**域名**。 +小明是个前端开发,他用 Vue 写了一个漂亮的咖啡店网站:可以看菜单、在线下单、查看订单状态。 -而 **DNS**,就是负责把这个“好记的名字”翻译成“机器能懂的 IP”的系统。 +网站在**小明的电脑上**跑得好好的,但是问题来了: -### 1.1 常见的记录类型 +> **怎么让全世界的人都能访问这个网站?** -在配置域名时,你会看到很多选项,别晕,最常用的就这俩: +这就像小明在家做了一桌子菜,现在要开餐厅让所有人来吃。这可不是简单地"把菜端出去"那么简单,他要经历一整套完整的流程。 -- **A 记录**:最直接的。告诉 DNS,“`my-site.com` 的 IP 是 `1.2.3.4`”。 -- **CNAME 记录**:起别名。告诉 DNS,“`www.my-site.com` 也就是 `my-site.com`”。这在接入 CDN 时特别常用。 +<DeploymentOverviewDemo /> -### 1.2 避坑指南 +**服务上线就是一场"搬家+开业"的大工程**: -- **生效慢**:DNS 修改不是立即生效的,全球同步可能需要几分钟到几小时(受 TTL 影响)。 -- **配错了**:如果不小心配错了 IP,用户就真的找不到你了。 +1. **打包行李** → 把代码打包成服务器能懂的格式 +2. **找房子** → 选择服务器(云服务器/VPS) +3. **搬家** → 把代码部署到服务器 +4. **装修** → 配置运行环境 +5. **挂牌** → 配置域名和DNS +6. **安防** → 安装HTTPS证书 +7. **招服务员** → 配置负载均衡 +8. **开分店** → 配置CDN加速 +9. **自动化** → 建立CI/CD流程 +10. **守夜人** → 监控和备份 -<DnsFlowDemo /> +让我们跟着小明,一步一步完成这场"上线之旅"! --- -## 2. 服务器:你的网站“住”在哪? +## 1. 打包行李:把代码变成"可携带的包裹" -代码写好了,得找个地方跑起来。这个地方就是**服务器**。 -你可以把它想象成一台**永不关机、连着公网的电脑**。 +小明的网站代码在他电脑上是这样散落的: -### 2.1 怎么选配置? +``` +my-coffee-shop/ +├── src/ # 源代码 +│ ├── App.vue # Vue 组件 +│ ├── main.js # 入口文件 +│ └── assets/ # 图片、样式 +├── package.json # 依赖清单 +└── vite.config.js # 构建配置 +``` -新手最容易犯两个错: +这些源代码浏览器**看不懂**。浏览器只认识: +- HTML 文件(网页骨架) +- CSS 文件(网页样式) +- JS 文件(网页逻辑) -1. **买太小**:1核1G 的机器,跑个 Hello World 还行,稍微装点东西就卡死。 -2. **买太大**:一上来就买 8核16G,结果每天只有 10 个人访问,纯属浪费钱。 +### 1.1 为什么要"构建"? -**建议**:先买个入门款(比如 2核4G),不够了再随时升级。云服务器的好处就是可以弹性伸缩。 +想象小明做了一桌子菜,现在要打包外卖: -### 2.2 最小上云脚本 (Ubuntu) +<DeploymentBuildDemo /> -买好服务器后,通常是空的。你需要装一些基础软件。 -把下面这段话复制到服务器终端里运行,就能装好 Nginx(Web服务器)和 Node.js(运行环境): +**构建(Build)就是"打包外卖"的过程**: + +1. **翻译**:把 Vue 组件翻译成浏览器懂的 HTML/CSS/JS +2. **压缩**:把代码体积缩小(省流量、加载快) +3. **合并**:把几十个小文件合成几个大包(减少请求) +4. **哈希**:给文件名加上指纹(`app.abc123.js`),浏览器缓存友好 + +运行构建命令: ```bash -# 1. 更新系统软件库 +npm run build +``` + +构建完成后会生成一个 `dist` 文件夹: + +``` +dist/ +├── index.html # 主页面 +├── assets/ +│ ├── app.abc123.js # 打包后的JS(278KB) +│ ├── app.abc123.css # 打包后的CSS(45KB) +│ └── logo.789xyz.png # 优化后的图片 +``` + +这个 `dist` 文件夹,就是小明的"行李箱",里面装着所有要搬去服务器的东西。 + +--- + +## 2. 找房子:选择服务器 + +代码打包好了,现在要找个"房子"住。这个房子就是**服务器**。 + +### 2.1 服务器是什么? + +服务器 = **一台永远不关机、连着互联网的电脑** + +<DeploymentServerDemo /> + +### 2.2 怎么选服务器? + +小明有几个选择: + +| 方案 | 类比 | 优点 | 缺点 | 价格 | +|------|------|------|------|------| +| **虚拟主机** | 租个床位 | 便宜、简单 | 性能差、不能随便装软件 | ¥50-200/年 | +| **云服务器** | 租整套公寓 | 自由度高、可扩展 | 需要自己配置 | ¥100-1000/年 | +| **容器服务** | 租豪华酒店 | 自动化管理 | 价格高 | ¥500+/月 | +| **Serverless** | 租用会议室 | 用多少付多少 | 冷启动慢 | 按量计费 | + +**小明的选择**:云服务器(2核4G,约¥500/年) + +选配置的误区: + +- ❌ **太小了**:1核1G,跑个Hello World还行,稍微多点人就卡死 +- ❌ **太大了**:一上来就8核16G,每天10个访问,纯属浪费 +- ✅ **刚刚好**:2核4G起步,不够了再升级(云服务器支持弹性伸缩) + +### 2.3 主流云厂商 + +| 厂商 | 特点 | 适合人群 | +|------|------|---------| +| **阿里云** | 国内访问快、文档多 | 国内业务首选 | +| **腾讯云** | 价格亲民、微信生态 | 小程序开发者 | +| **AWS** | 全球覆盖、功能最强 | 国际业务 | +| **Vercel** | 免费额度、零配置 | 前端项目快速上线 | + +--- + +## 3. 搬家:把代码放到服务器上 + +服务器租好了,现在要把代码"搬"过去。 + +### 3.1 怎么连接服务器? + +服务器在遥远的机房,怎么操作?用 **SSH(远程连接工具)**: + +```bash +# 连接到服务器 +ssh root@123.45.67.89 + +# 输入密码后,你就"进入"服务器了 +# 后面敲的命令,都是在服务器上执行 +``` + +<DeploymentSSHDemo /> + +### 3.2 部署方式对比 + +| 方式 | 类比 | 优点 | 缺点 | +|------|------|------|------| +| **FTP上传** | 自己扛箱子搬家 | 简单直观 | 容易漏传文件、慢 | +| **Git拉取** | 让快递公司送货 | 快、可追溯 | 需要配置Git | +| **Docker容器** | 用搬家集装箱 | 环境一致、易迁移 | 需要学习Docker | + +**推荐方式**:Git + Build Script + +小明用 Git 把代码推送到 GitHub,然后让服务器自己拉取最新代码: + +```bash +# 服务器上执行的脚本 +cd /var/www/coffee-shop +git pull origin main # 拉取最新代码 +npm install # 安装依赖 +npm run build # 构建项目 +pm2 restart all # 重启服务 +``` + +--- + +## 4. 装修:配置运行环境 + +代码搬过去了,但是服务器还只是个"空房子",需要"装修"才能住人。 + +### 4.1 需要装什么? + +<DeploymentEnvironmentDemo /> + +**最小化安装脚本**(Ubuntu系统): + +```bash +# 1. 更新系统 sudo apt update && sudo apt upgrade -y -# 2. 安装 Nginx (Web服务器) 和 Git (拉代码用) -sudo apt install -y nginx git curl +# 2. 安装 Nginx(Web服务器,负责接待客人) +sudo apt install -y nginx -# 3. 安装 Node.js (这里选了版本 18) +# 3. 安装 Node.js(运行JavaScript代码) curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt install -y nodejs -# 4. 安装 PM2 (这是个好东西,能帮你把网站进程“守”住,崩溃了自动重启) -sudo npm i -g pm2 +# 4. 安装 PM2(进程管家,防止程序崩溃) +sudo npm install -g pm2 + +# 5. 安装 Git(拉代码用) +sudo apt install -y git ``` -<ServerSizerDemo /> +### 4.2 Nginx 反向代理是什么? + +小明的程序跑在 `3000` 端口,但用户习惯访问 `80`(HTTP)或 `443`(HTTPS)端口。 + +**Nginx 就像个"门童"**: +- 守在 80/443 端口接待客人 +- 把请求转给后端的 3000 端口 +- 把结果返回给用户 + +<DeploymentNginxDemo /> + +**Nginx 配置示例**: + +```nginx +server { + listen 80; + server_name coffee.example.com; + + # 所有请求都转发给 3000 端口 + location / { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} +``` --- -## 3. HTTPS:给传输管道“加个盖” +## 5. 挂牌:配置域名和 DNS -以前的网站(HTTP),数据是在网线上“裸奔”的。谁要是拿个工具在中间截获一下,你的密码、聊天记录全都被看见了。 -现在的标准是 **HTTPS**,多出来的这个 **S** 就是 **Secure (安全)**。它给数据加了一层加密壳,别人截获了也看不懂。 +房子装修好了,现在要挂个牌子,让客人能找到你。这个牌子就是**域名**。 -### 3.1 反向代理 (Reverse Proxy) +### 5.1 域名是什么? -这是个听起来很高级,其实很简单的概念。 -你的 Node.js 程序跑在 `3000` 端口,但用户习惯访问 `80` (HTTP) 或 `443` (HTTPS) 端口。 -你不能让用户自己在网址后面输 `:3000` 吧? +域名 = **网站的门牌号** -这时候就需要 **Nginx** 出场了。它守在 `80/443` 端口,把用户的请求“接”进来,再“转”给你的 `3000` 端口程序。这就叫**反向代理**。 +- IP地址(123.45.67.89)太难记 +- 域名(coffee.example.com)好记 -### 3.2 怎么搞定 HTTPS? +**DNS(域名解析)** = **电话本**,负责把域名翻译成IP -不用花钱买证书!现在有免费的工具叫 **Certbot**。 -它能自动帮你申请证书,还自动帮你改 Nginx 配置。 +<DeploymentDnsDemo /> -<HttpsNginxDemo /> +### 5.2 域名配置步骤 + +1. **买域名**:在阿里云/腾讯云/Namecheap购买(¥50-100/年) +2. **配置DNS记录**:告诉世界"我的网站在这个IP" + +常用记录类型: + +| 记录类型 | 用途 | 示例 | +|---------|------|------| +| **A记录** | 直接指向IP | `coffee.example.com` → `123.45.67.89` | +| **CNAME** | 指向另一个域名 | `www.coffee.example.com` → `coffee.example.com` | + +**注意事项**: +- ⏰ DNS生效慢:全球同步需要几分钟到几小时 +- 📝 TTL值:设置小一点(如600秒),修改后生效快 --- -## 4. CDN:让网站“飞”到用户家门口 +## 6. 安防:安装 HTTPS 证书 -如果你的服务器在**北京**,而用户在**纽约**。 -每一次请求都要跨越半个地球,光是光纤传输就要好几百毫秒,肯定慢。 +房子好了,牌子挂了,现在要装门锁,保证安全。 -**CDN (内容分发网络)** 就是解决这个问题的。 -它在全球各地建了无数个“小仓库”。当你把图片、CSS、JS 文件放到 CDN 上: +### 6.1 为什么需要 HTTPS? -- 北京用户访问,CDN 就近从北京节点给他。 -- 纽约用户访问,CDN 就近从纽约节点给他。 +<DeploymentHttpsDemo /> -**结果**:你的服务器轻松了(流量少了),用户也爽了(速度快了)。 +**HTTP 的问题**:数据"裸奔",谁都能看见 -<CdnCacheDemo /> +**HTTPS 的好处**:数据加密,黑客看见也是乱码 + +### 6.2 怎么搞定 HTTPS? + +不用花钱买证书!用 **Let's Encrypt(免费证书)** + **Certbot(自动工具)** + +```bash +# 1. 安装 Certbot +sudo apt install -y certbot python3-certbot-nginx + +# 2. 自动申请证书并配置 Nginx +sudo certbot --nginx -d coffee.example.com + +# 3. 证书自动续期(Certbot会自动设置) +sudo certbot renew --dry-run +``` + +完成后,访问网站会看到小锁🔒,HTTPS 就搞定了! --- -## 5. CI/CD:告别“手工搬砖” +## 7. 招服务员:负载均衡 -以前发布网站是这样的: +小明的咖啡店火了,一个人接待不过来,怎么办?招更多服务员! -1. 在本地改代码。 -2. 用 FTP 软件把文件上传到服务器。 -3. SSH 连上去重启服务。 -4. 哎呀,上传漏了一个文件,报错了!赶紧修... +### 7.1 什么是负载均衡? -这太累,而且容易出错。 -现在我们用 **CI/CD (持续集成/持续部署)**。 +<DeploymentLbDemo /> -### 5.1 它是怎么工作的? +**负载均衡器(Load Balancer)** = **餐厅领班** -你只需要做一件事:**把代码推送到 Git 仓库**。 -剩下的事情,流水线(Pipeline)自动帮你做: +- 看着后面N个服务器(服务员) +- 把客人分配到最空闲的那个 +- 某个服务器挂了,自动把流量分给其他的 -1. **检测**:自动发现有新代码了。 -2. **安装**:自动在新环境里装依赖 (`npm install`)。 -3. **构建**:自动打包 (`npm run build`)。 -4. **部署**:自动把打好的包发到服务器,并重启服务。 +### 7.2 什么时候需要负载均衡? -如果中间任何一步出错了(比如测试没过),它会立刻停下来报警,绝不会把烂代码发到线上。 - -<CicdPipelineDemo /> - -### 5.2 进阶:如何“丝滑”回滚? - -万一新版本上线后发现有重大 Bug,怎么办? -**蓝绿部署** 或 **滚动更新** 可以帮你。简单说,就是新老版本共存一小会儿,没问题了再全切过去。有问题?一键切回老版本,用户甚至感觉不到。 - -<RollbackSwitchDemo /> +| 访问量 | 服务器配置 | 是否需要LB | +|--------|-----------|-----------| +| <100/天 | 1核2G | ❌ 不需要 | +| 1000-10000/天 | 2核4G | ❌ 不需要 | +| >10000/天 | 4核8G | ✅ 建议配置 | --- -## 6. 监控与备份:做个“心里有数”的管理员 +## 8. 开分店:CDN 加速 -网站上线了,不是结束,只是开始。 -你不能等用户打电话骂你“网站打不开了”,你才知道出事了。 +小明在全国都有客人,北京的服务器响应纽约的请求太慢了。怎么办?开分店! -### 6.1 监控 (Observability) +### 8.1 什么是 CDN? -你需要给服务器装上“摄像头”和“报警器”: +**CDN(内容分发网络)** = **全球连锁仓库** -- **日志 (Logs)**:记录程序运行的每一句话。报错了查日志,一查一个准。 -- **指标 (Metrics)**:CPU 用了多少?内存剩多少?QPS(每秒请求数)是多少? -- **告警 (Alerts)**:当 CPU 飙到 90%,或者错误率突然升高时,直接发短信/钉钉告诉你。 +<DeploymentCdnDemo /> -### 6.2 备份 (Backup) +**CDN 的工作原理**: -**数据是无价的**。 -一定要设置**自动备份**。哪怕服务器炸了、被黑客删库了,只要有备份,你就能在半小时内“起死回生”。 +1. 你把图片、CSS、JS等"不变的东西"上传到CDN +2. CDN把这些文件复制到全球各地的节点 +3. 用户访问时,CDN自动从最近的节点给文件 -<ObservabilityBackupDemo /> +**效果**: +- 北京用户 → 北京节点(10ms) +- 纽约用户 → 纽约节点(15ms) +- 伦敦用户 → 伦敦节点(20ms) + +### 8.2 怎么配置CDN? + +1. **开通CDN服务**:阿里云CDN/腾讯云CDN/Cloudflare +2. **添加域名**:填写你的网站域名 +3. **配置源站**:告诉CDN你的服务器IP +4. **刷新缓存**:文件更新后,手动刷新CDN缓存 --- -## 7. 遇到问题怎么办?(故障速查) +## 9. 自动化:建立 CI/CD 流程 -别慌,按这个表排查: +每次更新代码都要手动SSH到服务器、拉代码、构建、重启,太累!自动化吧! -| 现象 | 可能原因 | 怎么办? | -| :------------- | :--------------------------------------- | :------------------------------------------------------------------------- | -| **打不开网站** | 域名没解析好?服务器挂了?防火墙拦住了? | 1. `ping 域名` 看看通不通。<br>2. 检查云厂商防火墙是不是没开 80/443 端口。 | -| **HTTPS 报错** | 证书过期了?没配置好? | 运行 `certbot renew` 试着续期一下。 | -| **改了没生效** | 浏览器缓存?CDN 缓存? | 强制刷新浏览器 (Ctrl+F5);去 CDN 控制台点“刷新缓存”。 | -| **发布失败** | 依赖装不上?代码写错了? | 去看 CI/CD 的日志,最后几行通常就是原因。 | +### 9.1 什么是 CI/CD? + +<DeploymentCicdDemo /> + +**CI(持续集成)** = 自动化测试 + +- 每次提交代码自动运行测试 +- 保证代码质量 + +**CD(持续部署)** = 自动化上线 + +- 代码通过测试后自动部署 +- 一键上线,安全可靠 + +### 9.2 怎么实现 CI/CD? + +**推荐:GitHub Actions** + +在项目根目录创建 `.github/workflows/deploy.yml`: + +```yaml +name: Deploy to Production + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Deploy to server + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SERVER_HOST }} + username: root + key: ${{ secrets.SSH_KEY }} + script: | + cd /var/www/coffee-shop + git pull origin main + npm install + npm run build + pm2 restart all +``` + +**工作流程**: +1. 小明提交代码到 GitHub +2. GitHub Actions 自动触发 +3. 自动构建 + 自动部署 +4. 几分钟后,新版本就上线了! --- -## 8. 名词速查表 (Glossary) +## 10. 守夜人:监控和备份 -| 缩写 | 全称 | 人话解释 | -| :-------- | :---------------------------------- | :------------------------------------------------- | -| **DNS** | Domain Name System | **域名解析**。把网址变成 IP。 | -| **CDN** | Content Delivery Network | **加速网络**。把资源存到离用户最近的地方。 | -| **HTTPS** | HyperText Transfer Protocol Secure | **安全协议**。给数据传输加把锁。 | -| **CI/CD** | Continuous Integration / Deployment | **自动发布**。提交代码后自动跑完测试和上线流程。 | -| **PM2** | Process Manager 2 | **进程管家**。Node.js 的保姆,负责让程序一直跑着。 | +网站上线了,但工作还没完。就像开店后需要保安和账房,网站也需要**监控**和**备份**。 + +### 10.1 监控:当个好管家 + +<DeploymentMonitorDemo /> + +**需要监控什么?** + +| 指标 | 说明 | 正常范围 | +|------|------|---------| +| **CPU使用率** | 服务器"脑子"有多忙 | <70% | +| **内存使用率** | 服务器"记忆"占多少 | <80% | +| **磁盘空间** | 硬盘还剩多少 | <80% | +| **响应时间** | 网页加载多慢 | <2秒 | +| **错误率** | 有多少请求失败 | <1% | + +**监控工具推荐**: +- **基础监控**:云厂商自带的监控(阿里云云监控/腾讯云云监控) +- **APM监控**:New Relic / Datadog(收费,功能强大) +- **开源方案**:Prometheus + Grafana(免费,需自建) + +### 10.2 备份:最后的安全网 + +**数据是无价的**!一定要定期备份。 + +<DeploymentBackupDemo /> + +**备份三要素**: + +1. **定期备份**:每天凌晨自动备份(用户访问少的时候) +2. **多地存储**:本地 + 异地(防止单点故障) +3. **定期恢复测试**:备份了要确保能恢复 + +**备份策略**: + +```bash +# 每天自动备份数据库 +0 2 * * * /usr/bin/mysqldump -u root -p密码 coffee_shop > /backup/db_$(date +\%Y\%m\%d).sql + +# 保留最近7天的备份 +find /backup -name "db_*.sql" -mtime +7 -delete + +# 自动上传到云存储(如阿里云OSS) +/usr/bin/ossutil cp /backup/db_$(date +\%Y\%m\%d).sql oss://my-backup/db/ +``` --- -## 9. 上线前的最后检查 (Checklist) +## 11. 故障排查:遇到问题怎么办? -在按下“发布”按钮前,心里默念一遍: +网站出问题了,别慌,按这个流程排查: -- [ ] **域名**:解析通了吗?IP 对吗? -- [ ] **安全**:HTTPS 绿锁有了吗? -- [ ] **加速**:CDN 配好了吗?静态资源快吗? -- [ ] **流程**:自动发布跑通了吗?能一键回滚吗? -- [ ] **后路**:监控报警开了吗?数据库备份了吗? +### 11.1 排查流程图 -如果都 OK,恭喜你,你的产品正式面世了!🚀 +<DeploymentTroubleshootDemo /> + +### 11.2 常见问题速查表 + +| 现象 | 可能原因 | 怎么办 | +|------|---------|--------| +| **网站打不开** | 域名没解析?服务器挂了? | `ping 域名` 看通不通<br>`ssh` 连不上就是服务器挂了 | +| **404 Not Found** | Nginx配置错了?文件路径不对? | 检查 `nginx -t` 配置<br>看看 `root` 路径对不对 | +| **502 Bad Gateway** | 后端服务挂了?端口没开? | `pm2 list` 看服务在不在<br>`netstat -tlnp` 看端口监听 | +| **HTTPS证书报错** | 证书过期了?域名不匹配? | `certbot renew` 续期<br>检查证书域名是否正确 | +| **更新不生效** | 浏览器缓存?CDN缓存? | Ctrl+F5 强制刷新<br>去CDN控制台"刷新缓存" | +| **很慢** | 服务器性能不够?CDN没配置? | 查监控看CPU/内存<br>静态资源上CDN | + +--- + +## 12. 上线前最后检查 + +小明终于要正式开业了!在按下"发布"按钮前,再检查一遍: + +<DeploymentChecklistDemo /> + +### 最终检查清单 + +**基础检查**: +- [ ] 代码已经构建(`npm run build`) +- [ ] 服务器环境配置完成(Nginx + Node.js) +- [ ] 域名解析正确(能ping通) +- [ ] HTTPS证书正常(有🔒小锁) + +**性能检查**: +- [ ] 首屏加载时间 <2秒 +- [ ] 图片已经压缩优化 +- [ ] CDN配置完成 +- [ ] 开启了Gzip压缩 + +**安全检查**: +- [ ] 数据库密码不是弱密码 +- [ ] 敏感信息没写在代码里 +- [ ] 开启了防火墙(只开必要的端口) +- [ ] 配置了SQL注入防护 + +**运维检查**: +- [ ] 监控告警配置完成 +- [ ] 日志正常记录 +- [ ] 自动备份已设置 +- [ ] CI/CD流程测试通过 + +**应急预案**: +- [ ] 准备了回滚方案 +- [ ] 有故障联系机制 +- [ ] 备份恢复测试通过 + +--- + +## 13. 总结:服务上线的关键点 + +小明的咖啡店网站终于上线了!让我们回顾一下整个过程: + +### 核心流程 + +1. **构建**:把代码打包成浏览器懂的格式 +2. **部署**:把代码放到服务器上 +3. **配置**:Nginx、域名、HTTPS +4. **优化**:CDN、负载均衡 +5. **自动化**:CI/CD解放双手 +6. **保障**:监控和备份守住底线 + +### 关键原则 + +| 原则 | 说明 | +|------|------| +| **小步快跑** | 先上线MVP(最小可用产品),再逐步完善 | +| **自动化优先** | 能自动的别手动,减少人为失误 | +| **监控先行** | 先建监控,再上功能 | +| **备份为王** | 数据无价,备份是最后的防线 | +| **安全第一** | HTTPS、防火墙、弱密码检查,一个都不能少 | + +### 学习路线 + +**入门**(第1天): +- 用 Vercel/Netlify 免费部署一个静态网页 + +**进阶**(第1周): +- 租一台云服务器 +- 手动部署一个 Node.js 应用 +- 配置域名和 HTTPS + +**实战**(第2-4周): +- 搭建完整的 CI/CD 流程 +- 配置 CDN 加速 +- 建立监控和备份体系 + +**深入**(持续): +- 学习 Docker 容器化部署 +- 研究 Kubernetes 集群管理 +- 探索微服务架构 + +--- + +## 名词速查表 + +| 名词 | 英文 | 人话解释 | +|------|------|---------| +| **部署** | Deployment | 把代码放到服务器上让人能访问 | +| **构建** | Build | 把源代码翻译打包成浏览器懂的格式 | +| **服务器** | Server | 一台永远不关机、连着互联网的电脑 | +| **域名** | Domain Name | 网站的好记名字(如 baidu.com) | +| **DNS** | Domain Name System | 域名解析系统,把域名翻译成IP | +| **IP地址** | IP Address | 电脑在互联网上的门牌号 | +| **HTTP** | HyperText Transfer Protocol | 网页传输协议(不安全) | +| **HTTPS** | HTTP Secure | 安全的网页传输协议(加密) | +| **Nginx** | Engine X | 一个高性能的Web服务器(门童) | +| **反向代理** | Reverse Proxy | 转发请求到后端服务 | +| **SSH** | Secure Shell | 远程连接服务器的工具 | +| **CDN** | Content Delivery Network | 内容分发网络,全球加速 | +| **负载均衡** | Load Balancing | 把流量分到多台服务器 | +| **CI/CD** | Continuous Integration/Deployment | 持续集成/持续部署(自动化) | +| **监控** | Monitoring | 盯着服务器看有没有问题 | +| **备份** | Backup | 复份数据,防丢失 | diff --git a/docs/zh-cn/appendix/frontend-performance.md b/docs/zh-cn/appendix/frontend-performance.md index f19a7ea..17dcc04 100644 --- a/docs/zh-cn/appendix/frontend-performance.md +++ b/docs/zh-cn/appendix/frontend-performance.md @@ -1,181 +1,653 @@ -# 前端性能优化 (Frontend Performance) +# 前端性能优化 -> 💡 **学习指南**:本章节无需深入的算法背景,通过交互式演示带你掌握前端性能优化的核心逻辑。我们将从最直观的页面加载讲起,一直到浏览器底层的渲染机制和缓存策略。 +::: tip 🎯 核心问题 +**为什么你的网页加载很慢,用户还在疯狂抱怨卡顿?** 这就像是问:为什么餐厅上菜慢、顾客等得不耐烦?本章将带你深入理解前端性能优化的核心概念,让你的网页"飞"起来。 +::: + +--- + +## 1. 为什么要"性能优化"? + +### 1.1 从能用到好用:性能优化的演变 + +十年前的网页非常简单,一个页面可能就几 KB,加载速度几乎感觉不到延迟。那时的我们根本不需要考虑性能优化——因为问题还没出现。 + +但现在完全不同了。现代网页的复杂度呈指数级增长:一个电商首页可能有几十张高清图片,一个社交平台可能同时加载上千条动态,一个管理后台可能包含几十个交互组件。这些"丰富"的功能背后,是庞大的代码量和资源体积,如果不好好优化,用户体验就会一塌糊涂。 + +<div style="display: flex; gap: 20px; margin: 20px 0;"> +<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;"> + +**👴 十年前的网页** +- 单个页面只有几 KB 到几十 KB +- 只有文字和少量图片 +- 用户几乎感觉不到加载延迟 +- 不需要任何性能优化 + +</div> +<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;"> + +**🚀 现代的网页** +- 单个页面可能几 MB 甚至更大 +- 有高清图片、视频、交互组件 +- 加载慢、滚动卡、点击反应迟钝 +- 必须做性能优化才能用 + +</div> +</div> + +**这就是"性能优化"要解决的问题:让用户等待的时间更短,让操作更流畅。** + +### 1.2 一个真实的踩坑故事:为什么你需要了解性能优化 + +你可能会说:"现在的网络这么快,设备这么好,还需要考虑性能优化吗?" 让我讲一个真实的故事,你就会明白为什么这些知识如此重要。 + +::: warning 小王的性能踩坑记 +小王是一个刚入职的前端工程师,负责开发公司的电商首页。他用了最新的 Vue 3、最流行的 UI 库,功能做得非常完善,自己在公司的高性能电脑上测试时一切正常。 + +但上线后第二天,客服部门就炸锅了——大量用户投诉说"网站太卡了"、"图片加载不出来"、"点击按钮半天没反应"。小王打开自己的开发机测试,一切都很流畅啊,他完全不理解问题出在哪里。 + +后来请师傅帮忙定位,师傅让他用一台普通的笔记本电脑,连上普通的 4G 网络,然后再测试自己的网站。小王这才傻眼了:首页加载要等十几秒,滚动列表时卡得像 PPT,点击按钮后要等好几秒才有反应。 + +原来小王的开发环境是顶配的 MacBook Pro + 千兆光纤,而大多数用户用的是普通设备 + 移动网络。他写的代码里有几十张未压缩的高清图片,引入了整个 UI 库但只用了几个组件,还在渲染时做了大量同步计算。 + +解决方案其实不复杂:压缩图片、按需引入组件、把计算放到后台线程、使用虚拟列表。这样改动之后,首页加载时间从十几秒变成了 2 秒,滚动也非常流畅,用户投诉立刻消失了。 + +小王从此明白了一个道理:**不了解性能优化,你写出来的代码在自己电脑上跑得飞快,但在用户设备上可能根本没法用。** +::: + +::: info 💡 核心启示 +性能优化不是可选项,而是必备技能。你要站在用户的视角思考问题——他们用的是普通设备、普通网络,如果你的代码在他们设备上跑不动,那就说明你需要优化了。 +::: + +--- + +## 2. 核心概念:加载、渲染、交互 + +::: tip 🤔 这些概念和性能有什么关系? +加载、渲染、交互就是用户访问网页的三个核心环节,每个环节都可能成为性能瓶颈。 + +当用户访问你的网页时,会依次经历: +1. **加载** → 把 HTML/CSS/JS/图片 从服务器下载到浏览器 +2. **渲染** → 把下载的内容"画"成用户能看到的页面 +3. **交互** → 响应用户的点击、滚动等操作 + +所以,**性能优化就是让这三个环节都快起来**。理解它们,你才能知道性能瓶颈出在哪里,该用什么方法优化。 +::: + +在深入学习具体优化技巧之前,我们需要先搞清楚这几个核心概念。为了帮助你更好地理解,我们用餐厅的比喻来类比它们之间的关系。 + +### 2.1 用餐厅比喻理解三个环节 + +想象你去一家餐厅吃饭,这个过程和访问网页惊人地相似: + +| 环节 | 🍽️ 餐厅比喻 | 实际作用 | 具体例子 | +|------|-------------|----------|----------| +| **加载** | 把食材从仓库运送到厨房 | 把 HTML/CSS/JS/图片 从服务器下载到浏览器 | 用户打开网页,浏览器开始下载各种资源 | +| **渲染** | 厨师把食材加工成菜肴 | 浏览器把代码转换成用户能看到的页面 | 浏览器解析 HTML、计算布局、绘制页面 | +| **交互** | 服务员响应顾客的需求 | 浏览器响应点击、滚动等操作 | 用户点击按钮,页面做出反馈 | + +### 2.2 加载(Loading):食材运送 + +加载是指把网页所需的各种资源(HTML、CSS、JavaScript、图片、字体等)从服务器下载到浏览器的过程。这个过程就像把食材从仓库运送到厨房,如果运送慢或者食材太多,厨房就得干等着。 + +**为什么加载会慢?** 主要有三个原因:首先,资源体积太大——一张未压缩的高清图片可能就有 5MB,相当于下载一本小说;其次,网络延迟——如果服务器在国外,或者用户用移动网络,每个请求都要等很久;最后,请求太多——浏览器同时下载的资源数量有限,太多资源就要排队。 + +::: details 🔍 看看加载阶段都做了什么 +当用户在浏览器地址栏输入网址并按下回车后,会依次发生: + +1. **DNS 解析**:把域名(如 `www.example.com`)转换成 IP 地址(如 `192.168.1.1`),就像通过电话簿查找餐厅地址 +2. **TCP 连接**:浏览器和服务器建立连接,就像打电话前要先拨号 +3. **TLS 握手**:建立安全连接(HTTPS),就像确认对方身份 +4. **请求资源**:浏览器向服务器请求 HTML 文件 +5. **解析 HTML**:浏览器解析 HTML,发现需要 CSS、JS、图片等资源,继续请求 +6. **下载资源**:把所有需要的资源下载到本地 +7. **开始渲染**:下载完成后,开始渲染页面 + +前面的 1-4 步叫"首字节时间"(TTFB),后面的 5-7 步是真正的资源下载时间。 +::: + +**常见的加载优化手段:** + +- **压缩资源**:把文件变小(Gzip、Brotli 压缩) +- **使用 CDN**:把文件存在离用户更近的服务器上 +- **懒加载**:只加载用户看得到的内容,剩下的等用户滚动时再加载 +- **代码分割**:把大文件拆成小文件,按需加载 + +### 2.3 渲染(Rendering):厨师做菜 + +渲染是指浏览器把下载的 HTML、CSS、JavaScript 转换成用户能看到的页面的过程。这个过程就像厨师把食材加工成菜肴,如果工序复杂、步骤多,上菜就会慢。 + +::: tip 📖 什么是"渲染"? +你可能听说过"渲染"这个词,它到底是什么? + +**简单来说,渲染就是把代码变成画面的过程。** + +浏览器要做的事情包括: +1. **解析 HTML** → 生成 DOM 树(页面的结构) +2. **解析 CSS** → 生成 CSSOM 树(页面的样式) +3. **合并** → 生成渲染树(结构和样式的结合) +4. **布局** → 计算每个元素的位置和大小 +5. **绘制** → 把元素画出来 +6. **合成** → 把多个图层合并成最终画面 + +这个过程非常复杂,任何一个环节出问题,都会导致页面卡顿。 +::: + +**为什么渲染会慢?** 主要有两个原因:首先,页面太复杂——如果一个页面有上万个 DOM 节点,浏览器计算布局和绘制就会非常耗时;其次,频繁修改页面——如果 JavaScript 代码频繁修改 DOM,会导致浏览器反复重新布局和绘制,消耗大量性能。 + +::: details 📁 看看渲染阶段都做了什么 +**渲染的完整流程**: + +``` +HTML (字符串) + ↓ +[解析 HTML] → 生成 DOM 树 + ↓ +DOM 树 (页面结构) + +CSS (样式表) + ↓ +[解析 CSS] → 生成 CSSOM 树 + ↓ +CSSOM 树 (页面样式) + +DOM 树 + CSSOM 树 + ↓ +[合并] → 生成渲染树 + ↓ +渲染树 (要渲染的元素) + ↓ +[布局 Layout] → 计算每个元素的位置和大小 + ↓ +[绘制 Paint] → 填充颜色、绘制文字 + ↓ +[合成 Composite] → 合并多个图层 + ↓ +最终画面 +``` + +**关键渲染路径(Critical Rendering Path)**:浏览器要尽快把第一屏内容渲染出来,让用户觉得"网站很快"。这叫"关键渲染路径优化"。 +::: + +👇 **动手看看**: +下面这个演示展示了浏览器是如何渲染页面的。点击"下一步",观察渲染的各个阶段: <PerformanceOverviewDemo /> +**常见的渲染优化手段:** + +- **减少重排和重绘**:避免频繁修改 DOM,使用 `transform` 和 `opacity` 代替 `top` 和 `width` +- **虚拟列表**:只渲染可见区域的内容,大量数据时性能提升明显 +- **CSS 动画**:用 CSS 动画代替 JavaScript 动画,性能更好 + +### 2.4 交互(Interaction):服务员响应 + +交互是指浏览器响应用户操作(点击、滚动、输入等)的过程。这个过程就像服务员响应顾客的需求,如果服务员忙不过来,顾客就得等。 + +**为什么交互会卡?** 主要原因是**主线程被阻塞了**。浏览器的 JavaScript 是单线程的,如果代码在执行复杂的计算,就没法响应用户的操作,导致页面卡顿。 + +::: tip 🤔 什么是"主线程"? +浏览器有多个线程,但负责执行 JavaScript、渲染页面、响应用户操作的只有一个——**主线程**。 + +你可以把主线程想象成一个**忙碌的服务员**,他要做很多事情: +- 执行 JavaScript 代码(计算数据、调用 API) +- 渲染页面(布局、绘制) +- 响应用户操作(点击按钮、滚动页面) + +问题来了:**他只有一个人**。如果他在执行复杂的 JavaScript 计算(比如处理一万条数据),这时候用户点击了按钮,他是没法立即响应的,必须等计算完才行。这就是**卡顿**的根源。 + +**解决方案**: +- 把复杂的计算放到 Web Worker(后台线程) +- 使用时间切片,把大任务拆成小任务 +- 避免同步的复杂操作,改用异步 +::: + +👇 **动手试试看**: +下面这个演示对比了同步计算和 Web Worker 的区别。点击"开始计算",观察页面是否卡顿: + <PerformanceMetricsDemo /> -## 0. 引言:从 "能用" 到 "好用" +**常见的交互优化手段:** -如果把访问网页比作去餐厅**吃饭**,那么: - -- **加载 (Loading)** 就是食材(HTML/CSS/JS/图片)从仓库(服务器)运送到厨房(浏览器)的过程。 -- **渲染 (Rendering)** 就是厨师(浏览器引擎)把食材加工成美味菜肴(页面)的过程。 -- **交互 (Interaction)** 就是服务员响应你的需求(点击、滚动)。 - -**前端性能优化**的本质,就是为了让这三个过程**更快、更顺畅**。 - -它的核心任务只有一个:**最大限度地减少用户的等待时间。** - -为了实现这个目标,我们需要解决三个核心挑战: - -1. **传输**:怎么把“食材”运得更快?(压缩、CDN、懒加载) -2. **渲染**:怎么让“厨师”做得更快?(关键渲染路径、重排重绘) -3. **记忆**:怎么避免重复劳动?(缓存策略) - -本教程将带你一步步拆解这些优化技巧。 +- **防抖和节流**:限制事件的触发频率(比如滚动事件、输入事件) +- **Web Worker**:把复杂计算放到后台线程,不阻塞主线程 +- **时间切片**:把大任务拆成小任务,让浏览器有机会响应用户操作 --- -## 1. 第一步:传输 (Loading) +## 3. 实战:一个团队的性能优化演进之路 -在厨师开始做饭之前,首先得有食材。如果运送食材的卡车堵在路上了,厨房里再厉害的大厨也得干瞪眼。 +讲了这么多概念,让我们看一个真实的案例:某创业公司是如何从"完全没考虑性能"一步步进化到"系统化性能优化"的。通过这个案例,你会更直观地理解性能优化到底解决了什么问题。 -### 1.1 为什么网速快了,网页还是很慢? +### 3.1 演进的全景图 -你可能会疑惑:现在的 5G 和光纤这么快,为什么有些网页打开还是很慢? +下面这张表展示了性能优化的四个阶段,你可以看到优化手段、工具、指标是如何一步步进化的: -原因通常有两个: -1. **东西太多**:一张高清大图可能就有 5MB,相当于下载一本书。 -2. **路太堵**:浏览器同时下载的资源数量是有限的(通常 6 个),就像只有 6 辆小卡车在运货,多出来的得排队。 +| 阶段 | 优化手段 | 监控工具 | 核心指标 | 核心变化 | +|------|---------|---------|---------|----------| +| **阶段一:原始时代** | 无(没考虑) | 无(凭感觉) | 无 | 完全没性能意识,能跑就行 | +| **阶段二:手动优化** | 压缩图片、减少请求 | 浏览器 Network 面板 | 页面加载时间 | 开始有意识,但方法原始 | +| **阶段三:系统化优化** | 代码分割、懒加载、虚拟列表 | Lighthouse、Performance 面板 | FCP、LCP、TBT | 用专业工具,有明确的优化目标 | +| **阶段四:持续优化** | 性能预算、CI/CD 检查 | RUM、Lighthouse CI | INP、CLS、全链路监控 | 把性能纳入开发流程 | -### 1.2 解决方案:瘦身与偷懒 +::: tip 📊 从表格中你能看到什么? +让我们逐行解读这张表: -为了解决这个问题,我们主要用两招:**压缩(瘦身)**和**懒加载(偷懒)**。 +**阶段一 → 阶段二**:从"没意识"到"有意识"。这是关键的一步——开发者开始意识到性能是个问题,并且尝试优化。但优化手段比较原始,主要靠感觉和经验。 -#### 瘦身:图片与代码压缩 +**阶段二 → 阶段三**:从"手动"到"系统化"。这是质的飞跃——开始使用专业工具(Lighthouse、Performance 面板)来诊断性能问题,用科学的方法(代码分割、懒加载)来优化,而不是凭感觉。 -图片通常是网页里最大的“胖子”。 -现代的图片格式(如 WebP, AVIF)就像是采用了高科技压缩技术的压缩包,在画质几乎不变的情况下,体积能减小 30%-70%。 +**阶段三 → 阶段四**:从"一次性优化"到"持续优化"。当性能优化成为开发流程的一部分后,就需要建立监控体系(RUM、真实用户监控),在开发阶段就设置性能预算,防止退化。 -<ImageOptimizationDemo /> +**总结一下**:性能优化演进不只是"用了更多技术",而是**整个思维方式的升级**——从被动响应到主动预防,从凭感觉到数据驱动,从单次优化到持续改进。 +::: -#### 偷懒:懒加载 (Lazy Loading) +### 3.2 阶段一:原始时代——完全没考虑 -“偷懒”在这里是个褒义词。 -如果用户只在看第一屏的内容,为什么要把底下第十屏的图片也下载下来呢? +为什么叫"原始时代"?因为这个阶段完全没考虑性能问题——能跑就行。团队只有 3 个人,做一个简单的企业官网,项目很小,看起来没什么问题。 -**懒加载**的策略是:**只加载用户看得到的内容**。当用户滚动页面,图片快要出现时,再去下载它。 +但随着项目变大、用户增多,问题开始暴露出来。 -<LazyLoadingDemo /> +**开发方式**: +- **优化手段**:无,直接开发,没考虑性能 +- **监控工具**:无,凭感觉判断快慢 +- **核心指标**:无 -**关键点**:永远不要让用户下载他们不需要(或者暂时不需要)的资源。 +**这个阶段的特点**: +- ✅ **优点**:开发快,没有额外的学习成本 +- ❌ **缺点**:用户体验差,网速慢时根本没法用 ---- +::: details 查看当时的问题 +**遇到的具体问题**: -## 2. 核心难题:渲染 (Rendering) +1. **图片太大**:产品经理上传了一张 5MB 的首页 Banner 图,移动网络用户打开网页要等 1 分钟 +2. **没有压缩**:CSS 和 JS 文件完全没有压缩,体积是压缩后的 3 倍 +3. **没有缓存**:每次访问都要重新下载所有资源,老用户也要等 +4. **同步加载**:所有 JS 文件都在 `<head>` 中同步加载,阻塞页面渲染 -食材运到了,接下来压力给到了厨师(浏览器)。 +**用户的反馈**: +- "你们网站怎么打不开?" +- "图片半天加载不出来,就是空白" +- "点击按钮没反应,是不是网站坏了?" -### 2.1 浏览器的“单线程”困境 - -浏览器里的大厨(主线程)非常忙,他不仅要负责**画页面**(布局、绘制),还要负责**响应用户**(点击事件、JS 逻辑)。 -最糟糕的是,他只有**一个人**(单线程)。 - -如果你让他在切菜(运行复杂的 JS 计算)的时候,顾客(用户)想点菜(点击按钮),他是没法理你的。这就导致了**卡顿**。 - -### 2.2 关键渲染路径 (Critical Rendering Path) - -为了让用户尽快看到东西,浏览器制定了一套标准的工作流程,我们叫它**关键渲染路径**: - -1. **HTML -> DOM**:把菜谱读懂,列出食材清单。 -2. **CSS -> CSSOM**:搞清楚每种食材怎么处理(颜色、大小)。 -3. **Render Tree**:把清单和处理方法结合,决定最后上桌的菜。 -4. **Layout (排版)**:决定每个菜摆在盘子的哪个位置。 -5. **Paint (绘制)**:最后淋上酱汁,上色。 - -<CriticalRenderingPathDemo /> - -### 2.3 避坑指南:重排 (Reflow) 与重绘 (Repaint) - -在这个流程中,最累人的步骤是 **Layout (排版)**。 - -- **重排 (Reflow)**:如果你改变了元素的大小或位置,浏览器通过重新计算所有元素的位置。这就像因为桌子移了一下,整个餐厅的椅子都要重新摆一遍。**非常消耗性能!** -- **重绘 (Repaint)**:如果你只是改变了颜色,浏览器只需要重新上色。这就像给桌布换个颜色,简单多了。 - -<ReflowRepaintDemo /> - -**优化原则**: -- 尽量避免**重排**(比如不要频繁修改 `width`, `top`)。 -- 尽量使用只会触发**合成**(Composite)的属性(如 `transform`, `opacity`),这相当于让 GPU(帮厨)来干活,不占用主厨的时间。 - ---- - -## 3. 进阶:处理海量数据 - -如果你的网页需要展示 10,000 条聊天记录,或者 5,000 个商品列表,该怎么办? - -### 3.1 为什么不能直接 `v-for`? - -如果直接在页面上生成 10,000 个 `<div>`,浏览器的内存会瞬间爆炸,渲染树会变得巨大无比,每动一下都会卡死。 -这就好比餐厅里只有 10 张桌子,你却非要一次性接待 10,000 个客人,结果就是谁也吃不上饭。 - -### 3.2 解决方案:虚拟列表 (Virtual Scrolling) - -聪明的工程师想出了**虚拟列表**。 -它的核心思想是:**欺骗眼睛**。 - -既然屏幕只能显示 10 条数据,那我就只渲染这 10 条(加上前后一点缓冲)。当用户滚动时,我快速地把移出屏幕的 DOM 销毁,把新进入屏幕的数据填进去。 -用户感觉他在滚一个无限长的列表,但实际上浏览器里永远只有几十个 DOM 节点。 - -<VirtualScrollingDemo /> - -**关键点**:DOM 节点是昂贵的,能省则省。 - ---- - -## 4. 脚本执行优化 (Script Execution) - -JavaScript 的执行是阻塞主线程的,优化 JS 执行效率对于保持页面流畅至关重要。 - -### 4.1 代码压缩 (Minification) - -**移除不必要的字符**: - -- 空格、换行、注释 -- 缩短变量名 -- 移除无用代码 - -**工具**: - -- **Terser**:JavaScript 压缩工具 -- **ESBuild**:极快的打包工具 -- **Vite**:开发环境使用 ESBuild,生产环境使用 Rollup - -**示例**: - -```javascript -// 原始代码 -function calculateTotal(price, quantity) { - return price * quantity -} - -// 压缩后 -function calculateTotal(a, b) { - return a * b -} +**当时的临时解决方案**: +```html +<!-- 用 loading 遮罩"欺骗"用户 --> +<div id="loading">加载中...</div> +<script> + // 页面加载完成后才移除遮罩 + window.onload = function() { + document.getElementById('loading').style.display = 'none' + } +</script> ``` -### 4.2 防抖与节流 +这完全是在"自欺欺人"——页面还是很慢,只是用户看不到而已。 +::: -**防抖 (Debounce)**:事件触发后,等待一段时间再执行 +### 3.3 阶段二:手动优化——开始有意识 -```javascript -// 搜索框输入:停止输入 300ms 后才搜索 -const debouncedSearch = debounce((keyword) => { - searchAPI(keyword) -}, 300) +原始时代的问题积累到一定程度,团队终于决定开始做性能优化。这是一个重要的转折点——从"完全不考虑"到"有意识地优化"。 -input.addEventListener('input', (e) => { - debouncedSearch(e.target.value) +但这个阶段的优化比较原始,主要靠压缩图片、合并文件等简单手段。 + +**开发方式**: +- **优化手段**:手动压缩图片、合并 CSS/JS 文件、减少 HTTP 请求 +- **监控工具**:浏览器 Network 面板、简单的计时日志 +- **核心指标**:页面加载时间(手动用秒表计时) + +**这个阶段的特点**: +- ✅ **优点**:有明显改善,用户不再疯狂投诉 +- ❌ **缺点**:优化不系统,容易反复,缺少量化指标 + +::: details 查看手动优化的具体做法 +**手动优化手段**: + +1. **手动压缩图片**: + - 用 Photoshop 把每张图片手动"另存为 Web 格式" + - 把 PNG 转 JPEG(有损压缩,但体积小很多) + - 缩小图片尺寸(比如 2000px 宽的图缩小到 800px) + +2. **手动合并文件**: + ```html + <!-- 优化前:10 个 JS 文件 = 10 个请求 --> + <script src="utils.js"></script> + <script src="api.js"></script> + <script src="component-a.js"></script> + <script src="component-b.js"></script> + ...(还有 6 个) + + <!-- 优化后:1 个合并的 JS 文件 = 1 个请求 --> + <script src="all.js"></script> + ``` + +3. **把 CSS/JS 移到页面底部**: + ```html + <body> + <!-- 页面内容 --> + <h1>欢迎访问</h1> + + <!-- 优化:把 CSS/JS 放在最后 --> + <link rel="stylesheet" href="style.css"> + <script src="app.js"></script> + </body> + ``` + +**带来的改善**: +- 图片体积从 5MB 减小到 500KB(减少 90%) +- HTTP 请求数从 30 个减少到 5 个 +- 页面加载时间从 30 秒减少到 8 秒 + +**新的痛点**: +1. **手动工作量大**:每次更新都要手动压缩图片、合并文件 +2. **容易忘记**:新人不知道要优化,直接上传原图 +3. **缺少量化**:只知道"快了一些",但不知道具体快多少 +::: + +### 3.4 阶段三:系统化优化——用工具和数据说话 + +阶段二的问题(手动工作量大、缺少量化)困扰了团队很久。直到后来,团队发现了 Lighthouse、Performance 面板等专业工具,进入了系统化优化时代。 + +这个阶段的核心是**用数据驱动优化**——先用工具诊断问题,找到性能瓶颈,再有针对性地优化。 + +**开发方式**: +- **优化手段**:代码分割、懒加载、虚拟列表、图片自动压缩 +- **监控工具**:Lighthouse、Chrome Performance 面板、WebPageTest +- **核心指标**:FCP(首屏时间)、LCP(最大内容绘制)、TBT(总阻塞时间) + +::: details 系统化优化的具体做法 +**使用 Lighthouse 诊断问题**: + +Lighthouse 是 Google 开发的自动化性能测试工具,可以给出全面的性能报告和优化建议。 + +```bash +# 使用 Lighthouse 测试网页 +lighthouse https://www.example.com --view +``` + +Lighthouse 会给出: +- **性能评分**(0-100 分) +- **核心指标**(FCP、LCP、CLS、TBT、INP) +- **优化建议**(比如"启用文本压缩"、"移除未使用的 JavaScript") + +**关键指标解读**: + +| 指标 | 全称 | 含义 | 理想值 | +|------|------|------|--------| +| **FCP** | First Contentful Paint | 首次内容绘制时间(用户看到第一块内容的时间) | <1.8s | +| **LCP** | Largest Contentful Paint | 最大内容绘制时间(主要内容加载完成的时间) | <2.5s | +| **TBT** | Total Blocking Time | 总阻塞时间(主线程被阻塞的总时间) | <200ms | +| **CLS** | Cumulative Layout Shift | 累积布局偏移(页面元素乱跳的程度) | <0.1 | + +::: + +**这个阶段的特点**: +- ✅ **优点**:优化有针对性,效果好,有量化指标 +- ❌ **缺点**:需要学习工具和指标,有一定门槛 + +::: details 查看系统化优化的具体技术 +**1. 代码分割(Code Splitting)**: + +把大文件拆成小文件,按需加载。比如用户访问首页时,只加载首页需要的代码,等到点击"关于我们"时,再去加载关于页面的代码。 + +```js +// 优化前:所有代码都在一个文件,一次性加载 +import About from './views/About.vue' +import Contact from './views/Contact.vue' +// ... 还有 10 个页面 + +// 优化后:懒加载,访问时才加载 +const About = () => import('./views/About.vue') +const Contact = () => import('./views/Contact.vue') +``` + +**效果**:首页加载的代码量减少 70%,首屏时间从 5 秒降到 1.5 秒。 + +**2. 图片懒加载(Lazy Loading)**: + +只加载用户看得到的图片,滚动到可视区域时再加载其他图片。 + +```html +<!-- 现代浏览器支持原生的懒加载 --> +<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" /> +``` + +**效果**:首页加载的图片数量从 20 张减少到 3 张,节省 80% 的带宽。 + +**3. 虚拟列表(Virtual Scrolling)**: + +如果要渲染 10,000 条数据,不要真的创建 10,000 个 DOM 节点,而是只渲染可见区域的 20 条,滚动时动态替换。 + +```vue +<!-- 使用 vue-virtual-scroller 组件 --> +<RecycleScroller + :items="items" + :item-size="50" + key-field="id" +> + <template #default="{ item }"> + <div>{{ item.name }}</div> + </template> +</RecycleScroller> +``` + +**效果**:10,000 条数据从"卡死"变成"流畅滚动",内存占用减少 95%。 +::: + +### 3.5 阶段四:持续优化——把性能纳入开发流程 + +当工具和方法成熟后,团队开始关注更深层次的问题:如何防止性能退化?如何让性能成为开发流程的一部分? + +这个阶段的核心是**建立性能监控和预算体系**——不是上线后再优化,而是在开发阶段就预防性能问题。 + +**开发方式**: +- **优化手段**:性能预算(Performance Budget)、Lighthouse CI、真实用户监控(RUM) +- **监控工具**:Lighthouse CI、WebPageTest API、Google Analytics +- **核心指标**:INP(交互延迟)、CLS(布局偏移)、全链路监控 + +::: details 持续优化的具体做法 +**1. 设置性能预算**: + +在打包配置中设置限制,超过就报错,防止"无意中引入大文件"。 + +```js +// vite.config.js +export default defineConfig({ + build: { + rollupOptions: { + output: { + // 限制单个文件不超过 200KB + chunkFileNames: 'js/[name]-[hash].js', + } + }, + // 超过 200KB 时发出警告 + chunkSizeWarningLimit: 200 + } }) ``` -**节流 (Throttle)**:限制函数执行频率 +**2. Lighthouse CI**: -```javascript -// 滚动事件:最多每 100ms 执行一次 +每次提交代码时,自动运行 Lighthouse 测试,如果性能分数下降,就阻止合并。 + +```yaml +# .github/workflows/lighthouse.yml +name: Lighthouse CI +on: [pull_request] +jobs: + lighthouse: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Run Lighthouse CI + uses: treosh/lighthouse-ci-action@v9 + with: + urls: | + https://staging.example.com + budgetPath: ./budget.json +``` + +**3. 真实用户监控(RUM)**: + +在真实用户浏览器中收集性能数据,而不是只在开发环境测试。 + +```js +// 发送性能数据到服务器 +const perfData = performance.getEntriesByType('navigation')[0] +const lcp = performance.getEntriesByType('largest-contentful-paint')[0] + +fetch('/api/perf', { + method: 'POST', + body: JSON.stringify({ + fcp: perfData.loadEventEnd - perfData.fetchStart, + lcp: lcp.renderTime || lcp.loadTime, + url: window.location.href + }) +}) +``` + +**效果**: +- 能及时发现性能退化(比如某次提交导致 LCP 从 2 秒变成 5 秒) +- 能了解真实用户的体验(而不是开发环境的"理想状态") +- 能针对性地优化最慢的那 10% 用户 +::: + +**这个阶段会做什么?** + +1. **性能预算**:限制文件大小、请求数量,超过就报警 +2. **CI/CD 检查**:每次提交代码自动测试性能,退化就阻止合并 +3. **真实用户监控**:收集真实用户的性能数据,持续改进 +4. **定期性能报告**:每周/每月生成性能报告,跟踪趋势 + +--- + +## 4. 常见性能瓶颈与解决方案 + +讲了这么多理论,让我们看看实际开发中最常见的性能问题,以及如何解决。 + +### 4.1 图片加载慢 + +**问题表现**:图片半天加载不出来,或者加载过程中页面跳动。 + +**原因**: +- 图片体积太大(高清原图) +- 图片尺寸太大(2000px 宽的图显示为 200px) +- 没有懒加载(一次性加载所有图片) + +**解决方案**: + +1. **使用现代图片格式**(WebP、AVIF): + +```html +<!-- 现代:WebP 格式,体积小 30-70% --> +<picture> + <source srcset="image.webp" type="image/webp"> + <img src="image.jpg" alt="图片"> +</picture> +``` + +2. **响应式图片**(根据设备大小加载不同尺寸): + +```html +<!-- 小设备加载小图,大设备加载大图 --> +<img + src="image-800.jpg" + srcset="image-400.jpg 400w, + image-800.jpg 800w, + image-1200.jpg 1200w" + sizes="(max-width: 600px) 400px, + (max-width: 1200px) 800px, + 1200px" + alt="响应式图片"> +``` + +3. **懒加载**(用户滚动到时再加载): + +```html +<!-- 现代:原生懒加载 --> +<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" /> +``` + +👇 **动手试试看**: +下面这个演示对比了懒加载和不懒加载的区别。观察网络请求: + +<ImageOptimizationDemo /> + +### 4.2 首屏加载慢 + +**问题表现**:用户打开网页,白屏时间很长。 + +**原因**: +- 加载了太多不必要的代码 +- 关键渲染路径被阻塞 +- 没有做代码分割 + +**解决方案**: + +1. **代码分割**(Code Splitting): + +```js +// 路由懒加载:访问时才加载 +const routes = [ + { + path: '/about', + component: () => import('./views/About.vue') // 访问 /about 时才加载 + } +] +``` + +2. **预加载关键资源**(Preload): + +```html +<!-- 提前告知浏览器:这些资源很重要,优先加载 --> +<link rel="preload" href="critical.css" as="style"> +<link rel="preload" href="hero-image.jpg" as="image"> +``` + +3. **内联关键 CSS**: + +```html +<!-- 把首屏需要的 CSS 直接内嵌在 HTML 中 --> +<style> + /* 首屏关键样式 */ + .hero { background: #000; color: #fff; } +</style> +``` + +### 4.3 滚动卡顿 + +**问题表现**:页面滚动时一卡一卡的,不流畅。 + +**原因**: +- 渲染了太多 DOM 节点(比如 10,000 条数据) +- 滚动事件监听器中有复杂计算 +- 频繁触发布局计算 + +**解决方案**: + +1. **虚拟列表**(Virtual Scrolling): + +```vue +<!-- 只渲染可见区域的内容 --> +<RecycleScroller + :items="10000" + :item-size="50" +> + <template #default="{ item }"> + <div>{{ item.name }}</div> + </template> +</RecycleScroller> +``` + +👇 **动手看看**: +下面这个演示对比了普通列表和虚拟列表的性能差异: + +<VirtualScrollingDemo /> + +2. **节流滚动事件**(Throttle): + +```js +// 限制滚动事件的触发频率(最多每 100ms 触发一次) const throttledScroll = throttle(() => { updatePosition() }, 100) @@ -183,24 +655,48 @@ const throttledScroll = throttle(() => { window.addEventListener('scroll', throttledScroll) ``` -### 4.3 Web Workers +3. **使用 CSS `will-change`**: -**Web Workers**:在后台线程运行 JavaScript,不阻塞主线程 +```css +/* 提前告知浏览器:这个元素会变化,请做好准备 */ +.scroll-container { + will-change: transform; +} +``` -**使用场景**: +### 4.4 点击反应慢 -- 大数据计算 -- 图片/视频处理 -- 复杂算法 +**问题表现**:点击按钮后,要等好几秒才有反应。 -**示例**: +**原因**: +- 点击事件处理器中有复杂计算(阻塞主线程) +- 没有使用防抖(用户快速点击多次,触发多次计算) -```javascript +**解决方案**: + +1. **防抖点击事件**(Debounce): + +```js +// 用户停止点击 300ms 后才执行 +const debouncedClick = debounce(() => { + submitForm() +}, 300) + +button.addEventListener('click', debouncedClick) +``` + +2. **使用 Web Worker**(把计算放到后台线程): + +```js // 主线程 const worker = new Worker('calculator.js') -worker.postMessage({ data: largeData }) +button.addEventListener('click', () => { + worker.postMessage({ data: largeData }) +}) + worker.onmessage = (e) => { - console.log('计算结果:', e.data.result) + // 计算完成,显示结果 + showResult(e.data.result) } // calculator.js (Worker 线程) @@ -210,350 +706,123 @@ self.onmessage = (e) => { } ``` -### 4.4 避免长任务 - -**长任务(Long Task)**:执行时间超过 50ms 的任务 - -**问题**:长任务会阻塞主线程,导致页面卡顿 - -**解决方案**: - -- **时间切片**:把大任务拆成小任务 -- **使用 requestIdleCallback**:在浏览器空闲时执行 -- **Web Workers**:移到后台线程 - -**示例**(时间切片): - -```javascript -async function processLargeArray(items) { - for (let i = 0; i < items.length; i++) { - processItem(items[i]) - // 每 100 个项目暂停一次,让浏览器处理其他任务 - if (i % 100 === 0) { - await new Promise((resolve) => setTimeout(resolve, 0)) - } - } -} -``` - -### 5.3 压缩与裁剪 - -**使用工具压缩图片**: - -- **ImageOptim**(Mac) -- **TinyPNG**(在线) -- **Squoosh**(Google 开源) - -**使用 CDN 实时裁剪**: - -```html -<!-- 使用 CDN 裁剪为 800x600 --> -<img src="https://cdn.example.com/image.jpg?w=800&h=600&q=80" /> -``` - --- -## 6. 字体优化 +## 5. 性能监控工具 -字体也会影响性能,不当的字体加载会导致 FOUT/FOIT。 +性能优化不是一次性工作,需要持续监控。下面介绍常用的工具。 -### 6.1 Web Font 优化 +### 5.1 浏览器开发者工具 -**问题**:使用 Web Font 时,浏览器可能: +**Chrome DevTools** 是最常用的性能分析工具: -- **FOUT**(Flash of Unstyled Text):先显示系统字体,然后切换到 Web Font -- **FOIT**(Flash of Invisible Text):文字隐藏,等 Web Font 加载完才显示 +- **Network 面板**:查看资源加载情况 +- **Performance 面板**:分析运行时性能(FPS、主线程活动) +- **Lighthouse**:一键生成性能报告 -### 6.2 Font Display 策略 +::: tip 如何使用 Performance 面板 +1. 打开 Chrome DevTools(F12) +2. 切换到 Performance 面板 +3. 点击"Record"按钮 +4. 操作网页(滚动、点击等) +5. 点击"Stop"停止录制 +6. 分析结果:看 FPS(帧率)、主线程活动、长任务等 +::: -```css -@font-face { - font-family: 'MyFont'; - src: url('myfont.woff2') format('woff2'); - font-display: swap; /* 立即显示系统字体,Web Font 加载完再切换 */ -} -``` +### 5.2 Lighthouse -**`font-display` 值**: - -- `auto`:浏览器默认 -- `swap`:立即显示文本,Web Font 加载后替换(推荐) -- `fallback`:短时间隐藏,超时后显示系统字体 -- `optional`:如果 Web Font 加载慢,就不使用它 - -### 6.3 字体子集化 - -**只包含用到的字符**: - -- 中文字体很大(几 MB),但通常只用几百个字 -- 使用工具提取子集 - -**工具**: - -- **Fontmin**(中文字体子集化) -- **glyphhanger**(提取页面实际使用的字符) - -**示例**: +**Lighthouse** 是 Google 开发的自动化性能测试工具: ```bash -# 只提取常用的 500 个汉字 -fontmin input.ttf output/ --text='常用的五百个汉字...' +# 命令行使用 +lighthouse https://www.example.com --view + +# 或者在 Chrome DevTools 中使用 +# 打开 DevTools → Lighthouse → 点击 "Analyze page load" ``` -**结果**: +Lighthouse 会给出: +- 性能评分(0-100 分) +- 核心指标(FCP、LCP、CLS、TBT、INP) +- 优化建议(按影响排序) -- 原始字体:5 MB -- 子集化后:200 KB -- 减少 96% +### 5.3 WebPageTest + +**WebPageTest** 是在线性能测试工具,可以从多个地点、多种设备测试: + +```bash +# 访问 https://www.webpagetest.org +# 输入网址,选择测试地点和设备,点击 "Start Test" +``` + +WebPageTest 会给出: +- 瀑布图(Waterfall):每个资源加载的时间线 +- 视频对比:优化前后的加载过程视频 +- 优化建议 --- -## 7. 缓存策略 +## 6. 性能优化清单 -缓存是性能优化的"银弹",用好了能极大提升性能。 +下面是一个实用的性能优化清单,你可以按照这个顺序优化你的网页: -### 7.1 HTTP 缓存 +### 6.1 加载优化 -**强缓存(Strong Cache)**: +- ✅ **压缩图片**:使用 WebP 格式,压缩质量 80-85% +- ✅ **响应式图片**:根据设备大小加载不同尺寸的图片 +- ✅ **懒加载**:图片和组件懒加载,只加载可见内容 +- ✅ **代码分割**:按路由分割代码,按需加载 +- ✅ **压缩代码**:启用 Gzip/Brotli 压缩 +- ✅ **使用 CDN**:把静态资源放到 CDN,加速下载 +- ✅ **预加载关键资源**:使用 `<link rel="preload">` -```nginx -# 静态资源缓存 1 年 -location ~* \.(jpg|png|css|js)$ { - expires: 1y; - add_header Cache-Control: public, immutable; -} -``` +### 6.2 渲染优化 -**协商缓存(Conditional Cache)**: +- ✅ **减少重排重绘**:使用 `transform` 和 `opacity` 代替 `top` 和 `width` +- ✅ **虚拟列表**:大量数据时使用虚拟滚动 +- ✅ **CSS 动画**:优先使用 CSS 动画,而不是 JavaScript 动画 +- ✅ **优化关键渲染路径**:内联关键 CSS,延迟加载非关键 CSS +- ✅ **避免 @import**:`@import` 会阻塞渲染,改用 `<link>` -```nginx -# 使用 ETag -location / { - etag on; -} -``` +### 6.3 交互优化 -**最佳实践**: +- ✅ **防抖和节流**:滚动、输入、resize 事件使用防抖/节流 +- ✅ **Web Worker**:复杂计算放到后台线程 +- ✅ **时间切片**:大任务拆成小任务,避免长任务 +- ✅ **避免同步布局**:不要在循环中读取布局属性(如 `offsetHeight`) -- 带哈希的文件名(如 `app.abc123.js`):永久缓存 -- 不带哈希的文件:协商缓存 +### 6.4 缓存优化 -### 7.2 Service Worker +- ✅ **HTTP 缓存**:配置 Cache-Control 和 ETag +- ✅ **Service Worker**:缓存静态资源,实现离线访问 +- ✅ **LocalStorage**:缓存 API 数据,减少请求 +- ✅ **内存缓存**:使用 `Map`/`Object` 缓存计算结果 -**Service Worker**:在浏览器后台运行的脚本,可以拦截网络请求 +### 6.5 监控优化 -**核心能力**: - -- 离线访问 -- 资源缓存 -- 后台同步 - -**示例**(使用 Workbox): - -```javascript -// 注册 Service Worker -if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('/sw.js') -} - -// sw.js (Service Worker 脚本) -workbox.routing.registerRoute( - /\.(?:png|jpg|jpeg|svg|gif)$/, - new workbox.strategies.CacheFirst({ - cacheName: 'images', - plugins: [ - new workbox.expiration.ExpirationPlugin({ - maxEntries: 60, - maxAgeSeconds: 30 * 24 * 60 * 60 // 30 天 - }) - ] - }) -) -``` - -### 7.3 LocalStorage / IndexedDB - -**LocalStorage**:存储简单数据(5-10 MB) - -```javascript -// 缓存 API 数据 -localStorage.setItem('cache_key', JSON.stringify(data)) -const cached = JSON.parse(localStorage.getItem('cache_key')) -``` - -**IndexedDB**:存储大量结构化数据 - -```javascript -// 存储离线数据 -const db = await openDB('mydb', 1, { - upgrade(db) { - db.createObjectStore('posts') - } -}) -await db.put('posts', postData, 'post-1') -``` +- ✅ **Lighthouse CI**:每次提交代码自动测试性能 +- ✅ **真实用户监控**:收集真实用户的性能数据 +- ✅ **性能预算**:设置文件大小限制,超过报警 +- ✅ **定期性能报告**:每周/每月生成性能趋势报告 --- -## 8. 监控与持续优化 +## 7. 总结 -性能优化不是一次性的工作,需要持续监控和改进。 +让我们用一张表格来回顾前端性能优化的核心概念: -### 8.1 Real User Monitoring (RUM) +| 概念 | 一句话解释 | 解决的问题 | 常用手段 | +|------|-----------|-----------|----------| +| **加载优化** | 让资源下载更快 | 首屏慢、等待时间长 | 压缩图片、CDN、代码分割、懒加载 | +| **渲染优化** | 让页面"画"得更快 | 滚动卡、点击慢 | 虚拟列表、减少重排重绘、CSS 动画 | +| **交互优化** | 让响应更快 | 点击没反应、操作卡顿 | 防抖节流、Web Worker、时间切片 | +| **缓存优化** | 避免重复下载 | 重复访问慢 | HTTP 缓存、Service Worker、LocalStorage | +| **监控优化** | 持续发现问题 | 性能退化 | Lighthouse、RUM、性能预算 | -**RUM**:收集真实用户的性能数据 +::: info 写在最后 +性能优化是一个持续演进的话题,工具会变,但核心理念不变:**站在用户的角度思考问题,让等待时间更短、让操作更流畅**。 -**工具**: +理解了这些基本原理,无论技术如何更新换代,你都能快速上手、从容应对。 -- **Google Analytics**:免费,基础数据 -- **Cloudflare Web Analytics**:免费,注重隐私 -- **SpeedCurve**:付费,专业级 - -**关键指标**: - -- 首屏时间(FCP、LCP) -- 交互时间(TTI) -- 转化率与性能的关系 - -### 8.2 Synthetic Monitoring - -**合成监控**:用模拟用户定期测试 - -**工具**: - -- **Lighthouse CI**:每次提交代码自动测试 -- **WebPageTest**:定期测试关键页面 -- **Pingdom**:简单易用的监控服务 - -### 8.3 性能预算 - -**设置预算并强制执行**: - -```javascript -// vite.config.js -import { defineConfig } from 'vite' - -export default defineConfig({ - build: { - rollupOptions: { - output: { - manualChunks: { - vendor: ['vue', 'vue-router'], - ui: ['element-plus'] - } - } - } - } -}) -``` - -**使用 Lighthouse CI 检查预算**: - -```json -// lighthouserc.json -{ - "ci": { - "assert": { - "preset": "desktop", - "assertions": { - "first-contentful-paint": ["warn", { "maxNumericValue": 2000 }], - "interactive": ["error", { "maxNumericValue": 5000 }] - } - } - } -} -``` - ---- - -## 9. 实战案例 - -### 9.1 案例 1:新闻列表页优化 - -**问题**:首屏加载慢,滚动卡顿 - -**优化**: - -1. **图片**:WebP + 懒加载 -2. **列表**:虚拟列表(只渲染可见的 10 项) -3. **数据**:分页加载 - -**结果**:LCP 2.5s -> 0.8s - -### 9.2 案例 2:数据可视化大屏 - -**问题**:渲染大量节点卡死 - -**优化**: - -1. **渲染**:Canvas 代替 DOM -2. **计算**:Web Worker 处理数据 - -**结果**:FPS 10 -> 60 - -### 9.3 案例 3:移动端活动页 - -**问题**:白屏时间长 - -**优化**: - -1. **资源**:预加载 (Preload) 关键图 -2. **体验**:骨架屏 (Skeleton) - -**结果**:白屏减少 60% - ---- - -## 10. 总结与最佳实践 - -### 10.1 性能优化清单 - -**加载优化**: - -- ✅ 启用 Gzip/Brotli 压缩 -- ✅ 使用 CDN 加速静态资源 -- ✅ 实施代码分割和懒加载 -- ✅ 压缩和优化图片 - -**渲染优化**: - -- ✅ 减少重排和重绘 -- ✅ 优化关键渲染路径 -- ✅ 使用 CSS 动画代替 JS 动画 - -**执行优化**: - -- ✅ 使用 Web Workers 处理重计算 -- ✅ 避免长任务(Long Tasks) -- ✅ 合理使用防抖和节流 - -**缓存优化**: - -- ✅ 配置 HTTP 强缓存和协商缓存 -- ✅ 考虑使用 Service Worker - -### 10.2 持续学习 - -前端性能优化是一个不断发展的领域,新的标准(如 INP)和新的工具(如 Vite, Turbopack)层出不穷。保持好奇心,多看 Performance 面板,是你最好的老师。 - ---- - -## 11. 名词速查表 (Glossary) - -| 名词 | 全称 | 解释 | -| :--- | :--- | :--- | -| **FP / FCP** | First Paint / First Contentful Paint | **首屏时间**。用户看到页面第一个像素/第一块内容的时间。 | -| **LCP** | Largest Contentful Paint | **最大内容绘制**。页面主要内容加载完成的时间(衡量加载速度的核心指标)。 | -| **INP** | Interaction to Next Paint | **交互到下一次绘制**。衡量页面响应速度的新指标(替代 FID),关注点击后的反馈延迟。 | -| **CLS** | Cumulative Layout Shift | **累积布局偏移**。页面加载时元素乱跳的程度(衡量视觉稳定性)。 | -| **TTFB** | Time to First Byte | **首字节时间**。从发出请求到接收到服务器第一个字节的时间(衡量后端响应速度)。 | -| **TBT** | Total Blocking Time | **总阻塞时间**。主线程被长任务阻塞的总时间(衡量页面交互流畅度)。 | -| **Reflow** | Reflow (Layout) | **重排**。浏览器重新计算元素位置和大小的过程。成本高,应避免。 | -| **Repaint** | Repaint | **重绘**。浏览器重新绘制元素外观(如颜色)的过程。成本中等。 | -| **CDN** | Content Delivery Network | **内容分发网络**。把文件存在离用户最近的服务器上,加速下载。 | -| **SSR** | Server-Side Rendering | **服务端渲染**。在服务器端生成 HTML,加快首屏显示,利于 SEO。 | -| **CSR** | Client-Side Rendering | **客户端渲染**。在浏览器端通过 JS 生成 HTML,交互体验好,但首屏慢。 | -| **SSG** | Static Site Generation | **静态站点生成**。构建时生成静态 HTML,访问速度极快。 | -| **Tree Shaking** | Tree Shaking | **摇树优化**。构建时移除未使用的代码,减小包体积。 | -| **Code Splitting** | Code Splitting | **代码分割**。将代码拆分成小块,按需加载。 | -| **Preload / Prefetch** | Preload / Prefetch | **预加载/预获取**。提前告知浏览器加载关键资源或未来可能用到的资源。 | +希望这篇文章能帮助你建立起对前端性能优化的整体认知。当你在实际项目中遇到性能问题时,能够知道从哪里入手、如何定位、怎样解决。 +::: diff --git a/docs/zh-cn/appendix/frontend-routing.md b/docs/zh-cn/appendix/frontend-routing.md index 33fa150..f5803a0 100644 --- a/docs/zh-cn/appendix/frontend-routing.md +++ b/docs/zh-cn/appendix/frontend-routing.md @@ -1,466 +1,453 @@ -# 前端路由与导航机制 +# 前端路由:单页应用的导航系统 -> **学习指南**:页面跳转不刷新?URL变化但页面没白屏?这就是前端路由的魔法。本文会带你从"传统多页面跳转"的惯性思维,切换到"单页面应用路由"的新世界。 - -在阅读前,建议你先具备以下基础: - -- **SPA概念**:了解什么是单页面应用(Single Page Application) -- **浏览器History API**:知道 `pushState` 和 `popstate` 事件的存在 -- **基础正则**:能读懂 `:id`、`*` 这类路由参数写法 +::: tip 🎯 核心问题 +**为什么有些网站切换页面时不会白屏刷新,像 App 一样流畅?** 这就是前端路由的魔法。本章将带你从传统网站的"翻书式跳转",进入到单页应用的"幻灯片切换"世界,理解前端路由如何让用户体验提升一个档次。 +::: --- -## 0. 引言:为什么需要前端路由? +## 1. 为什么要"前端路由"? -还记得传统网站的体验吗?点击一个链接,页面白一下,然后整个页面重新加载。如果网络慢,你还要盯着加载圈发呆几秒。 +### 1.1 从传统网站到单页应用:用户体验的质变 -**前端路由的出现,彻底改变了这种体验。** +回顾早期的网站浏览体验,每次点击链接都是一次"完整翻页"的过程:页面白屏一下、加载圈转动、整个页面重新渲染。如果网络慢,你还要盯着加载圈发呆几秒。这种体验在今天看来已经过时了,但当时这就是标准做法。 -### 从一个电商网站的演进说起 +现代前端开发完全改变了这种模式。我们使用前端路由技术,让页面切换像手机 App 一样流畅——没有白屏、没有加载圈、用户几乎感觉不到"跳转"的过程。这种体验的提升不是魔法,而是前端路由系统的功劳。 -2015年,某电商网站(我们叫它"买得多")还是传统的多页面架构: +<div style="display: flex; gap: 20px; margin: 20px 0;"> +<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;"> -``` -首页 → 点击商品 → 商品详情页 → 点击购买 → 订单确认页 -(刷新) (刷新) (刷新) (刷新) -``` +**📖 传统网站(MPA)** +- 点击链接 → 整页刷新 +- 每个页面是独立的 HTML 文件 +- 浏览器重新下载所有资源 +- 体验像"翻书",有明显的翻页过程 -**用户吐槽**:"每次跳转都要等,感觉好卡!" +</div> +<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;"> -2016年,他们决定升级到SPA架构,引入前端路由: +**📱 单页应用(SPA)** +- 点击链接 → 无刷新切换 +- 只有一个 HTML 入口文件 +- 只下载需要的数据 +- 体验像"幻灯片",流畅自然 -``` -首页 → 商品详情 → 订单确认 -(无刷新) (无刷新) (无刷新) -``` +</div> +</div> -**用户反馈**:"哇,好流畅!像App一样!" +**这就是"前端路由"要解决的核心问题:在不刷新页面的情况下,实现视图的切换和 URL 的同步更新。** <RouteMatchingDemo /> +### 1.2 一个真实的踩坑故事:为什么你需要理解路由模式 + +你可能会说:"我用 Vue Router 或者 React Router,配置一下就能用,为什么还需要了解这些底层原理?" 让我讲一个真实的故事,你就会明白为什么这些知识如此重要。 + +::: warning 小李的部署踩坑记 +小李是一个前端新人,刚入职就负责开发一个基于 Vue 的单页应用。在本地开发时一切正常,路由跳转丝般顺滑。但是当他把项目部署到测试服务器后,问题出现了:用户直接访问某个路由(如 `example.com/user/123`)或者在详情页刷新页面时,会看到 **404 Not Found** 错误。 + +小李懵了:明明本地能正常访问,为什么部署后就 404 了?他排查了很久,甚至怀疑是服务器配置问题。 + +后来他请教师兄,师兄一眼就看出了问题:小李用的是 History 模式,但服务器没有配置 fallback。当用户直接访问 `/user/123` 时,服务器会去查找这个路径对应的文件,但 SPA 的所有路由其实都指向同一个 `index.html`。解决方案很简单:配置服务器让所有路由都回退到 `index.html`,让前端路由接管后续处理。 + +小李从此明白了一个道理:**不理解路由模式的原理和服务器配置要求,你连为什么报错都不知道,更别提解决问题了。** +::: + +::: info 💡 核心启示 +前端路由不是"黑魔法",理解它的工作原理能让你在遇到部署、性能、SEO 问题时快速定位、精准解决。更重要的是,它能在项目架构设计时帮你做出更明智的选择——什么时候用 Hash 模式、什么时候用 History 模式、如何避免常见的坑。 +::: + --- -## 1. 核心概念:SPA、路由、导航 +## 2. 核心概念:路由、模式、导航 -### 1.1 什么是SPA? +在深入具体实现之前,我们需要先搞清楚几个核心概念。为了帮助你更好地理解,我们用一个图书馆的比喻来类比它们之间的关系。 -**SPA(Single Page Application,单页面应用)** 是指在浏览器中运行的应用程序,它在首次加载时将所有必要的HTML、CSS和JavaScript下载到本地,之后的页面切换都通过JavaScript动态更新DOM实现,**不会触发完整的页面刷新**。 +::: tip 🤔 这些概念和路由有什么关系? +路由、模式、导航就是前端路由系统的三大支柱。 -**类比理解**: +当你使用 Vue Router 或 React Router 时,框架会帮你处理: +1. **路由映射** → 定义 URL 和组件的对应关系 +2. **模式选择** → 决定用 Hash 还是 History 模式 +3. **导航控制** → 处理页面跳转、浏览器前进后退 -> 传统多页面应用(MPA)就像**翻书**——每看一页都要翻到新的一页。 -> 单页面应用(SPA)就像**幻灯片**——所有内容都在一个屏幕上,只是切换显示区域。 +所以,**理解这三个概念,你才能知道路由系统到底在做什么,为什么有时候需要特殊配置,为什么部署时会出问题。** +::: -<SpaNavigationDemo /> +### 2.1 用图书馆比喻理解路由系统 -### 1.2 什么是前端路由? +想象你在图书馆里找书,这个过程与前端路由的工作原理惊人地相似: -**前端路由**是SPA中负责管理"当前显示哪个视图"的机制。它通过监听URL的变化,决定渲染哪个组件,同时保证浏览器的前进/后退按钮能正常工作。 +| 概念 | 📚 图书馆比喻 | 实际作用 | 具体例子 | +|------|-------------|----------|----------| +| **路由(Route)** | 书架编号和书籍的对应关系 | 定义 URL 和页面组件的映射关系 | `/user/123` 路径对应 `UserDetail.vue` 组件 | +| **路由器(Router)** | 图书馆的指引系统和定位服务 | 管理所有路由、处理导航行为的核心模块 | Vue Router、React Router 就是路由器 | +| **路由模式** | 索引方式(卡片目录 vs 电子系统) | 决定 URL 的形式和底层实现方式 | Hash 模式用 `#`、History 模式用普通路径 | +| **导航** | 从一个书架走到另一个书架 | 在不同页面之间切换的行为 | 点击链接、编程式跳转、浏览器前进后退 | -**核心职责**: +::: tip 📊 从表格中你能看到什么? +让我们逐行解读这张表: -1. **URL ↔ 视图的映射**:定义什么样的URL对应什么样的页面组件 -2. **导航控制**:处理点击链接、浏览器前进后退等导航行为 -3. **状态保持**:在URL变化时保持必要的应用状态 +**路由**:只是一个"配置",告诉系统"什么 URL 对应什么页面"。就像图书馆的书号对应一本书的位置。 -**类比理解**: +**路由器**:是"管理者",负责根据当前的 URL 找到对应的组件并渲染。就像图书馆员根据你提供的书号帮你找到书。 -> 前端路由就像是**剧院的节目单和舞台切换系统**: -> - 节目单(路由配置)告诉你每个节目(URL)对应什么表演(组件) -> - 舞台切换系统(路由器)负责在观众不注意的时候换布景(无刷新切换) +**路由模式**:是"实现方式",决定了 URL 长什么样、底层用什么技术实现。就像图书馆可以用纸质目录,也可以用电子查询系统。 -<RouterArchitectureDemo /> +**导航**:是"行为",是用户触发页面切换的动作。就像你在图书馆里从 A 区走到 B 区。 -### 1.3 路由模式:Hash vs History +理解这四者的区别非常重要:**路由是静态配置,路由器是动态管理者,模式是技术选型,导航是用户行为。** +::: -前端路由的实现主要有两种模式,它们在URL表现形式和底层实现上有本质区别。 +### 2.2 路由(Route):URL 与组件的映射契约 + +路由,本质上就是一个"契约",它规定了访问某个 URL 时应该显示什么内容。在 Vue Router 中,一个典型的路由配置长这样: + +```javascript +const routes = [ + { + path: '/', // URL 路径 + component: Home // 对应的组件 + }, + { + path: '/user/:id', // 带参数的动态路由 + component: UserDetail, + children: [ // 嵌套路由 + { path: 'profile', component: UserProfile }, + { path: 'posts', component: UserPosts } + ] + } +] +``` + +**你可能会有疑问:为什么不直接用 `<a>` 标签跳转,非要用路由?** + +答案在于"单页应用"的本质:SPA 只有一个 HTML 页面,所有的页面切换其实都是在同一个页面内替换组件。如果你用传统的 `<a href="/user/123">`,浏览器会真的去请求 `/user/123` 这个路径,导致页面刷新或 404 错误。路由的作用就是拦截这些跳转行为,用 JavaScript 动态替换组件,从而实现无刷新切换。 + +::: details 🔧 路由配置的几种常见模式 +**静态路由**(最简单): +```javascript +{ path: '/home', component: Home } +{ path: '/about', component: About } +``` + +**动态路由**(带参数): +```javascript +{ path: '/user/:id', component: UserDetail } +// 可以匹配 /user/123、/user/abc 等 +// 组件内可以通过 route.params.id 获取参数 +``` + +**嵌套路由**(父子关系): +```javascript +{ + path: '/user/:id', + component: UserLayout, // 父组件 + children: [ + { path: 'profile', component: UserProfile }, // 实际路径 /user/:id/profile + { path: 'posts', component: UserPosts } // 实际路径 /user/:id/posts + ] +} +``` + +**通配符路由**(404 页面): +```javascript +{ path: '/:pathMatch(.*)*', component: NotFound } +// 匹配所有未定义的路由 +``` +::: + +### 2.3 路由模式:Hash vs History 的本质区别 + +前端路由有两种主流的实现模式:Hash 模式和 History 模式。它们在 URL 表现形式、底层实现、兼容性等方面有本质区别。 + +::: tip 🤔 为什么需要两种模式? +这其实是历史原因和技术权衡的结果。 + +**Hash 模式**是最早的前端路由实现方式,它利用 URL 中的 hash 部分(即 `#` 后面的内容)。hash 的变化不会触发页面刷新,而且兼容性极好(连 IE8 都支持)。 + +**History 模式**是 HTML5 推出后的"标准做法",它利用 History API 提供的 `pushState` 和 `replaceState` 方法,可以让 URL 变得更"正常"(没有 `#`),但需要服务端配合配置。 + +打个比方:Hash 模式就像"给房间门口贴个便利贴"(不影响房间结构),History 模式就像"重新给房间编号"(需要更新门牌系统)。 +::: | 特性 | Hash 模式 | History 模式 | |------|-----------|--------------| -| URL 示例 | `/#/user/123` | `/user/123` | -| 实现原理 | 监听 `hashchange` 事件 | 使用 History API | -| 服务端配置 | 不需要 | 需要配置 fallback | -| 浏览器兼容性 | IE8+ | IE10+ | -| SEO 友好度 | 较差 | 良好 | +| **URL 示例** | `https://example.com/#/user/123` | `https://example.com/user/123` | +| **实现原理** | 监听 `hashchange` 事件 | 使用 History API (`pushState`、`replaceState`) | +| **服务端配置** | 不需要(hash 不被发送到服务器) | **必须配置 fallback 到 index.html** | +| **浏览器兼容性** | IE8+(几乎全部浏览器) | IE10+(现代浏览器) | +| **SEO 友好度** | 较差(搜索引擎可能忽略 hash) | 良好(URL 结构清晰) | +| **用户体验** | URL 有 `#`,看起来像"锚点跳转" | URL 美观,接近传统网站 | +| **部署难度** | 低,无需特殊配置 | 高,需要正确配置服务器 | <HashVsHistoryDemo /> ---- +::: tip 📊 从表格中你能看到什么? +让我们逐行解读这张表: -## 2. 案例分析:某SaaS平台的路由演进 +**URL 示例**:Hash 模式的 URL 中有明显的 `#`,用户会一眼看出这是个"单页应用";History 模式的 URL 和传统网站一样,看起来更"专业"。 -### 2.1 初期:简单的扁平路由 +**实现原理**:Hash 模式监听的是 `hashchange` 事件(hash 变化时触发);History 模式用的是 HTML5 的 History API,可以"假装"页面跳转了,但实际不刷新。 -2019年,"云管家"SaaS平台刚上线时,只有简单的几个页面: +**服务端配置**:这是最容易踩坑的地方!Hash 模式的 `#` 后面的内容不会发送到服务器,所以服务器不需要知道路由的存在;但 History 模式的完整路径会发送到服务器,如果服务器没配置好,会返回 404。 -```javascript -// router.js - 第一版 -const routes = [ - { path: '/', component: Home }, - { path: '/dashboard', component: Dashboard }, - { path: '/settings', component: Settings }, - { path: '/profile', component: Profile } -] -``` +**SEO 友好度**:搜索引擎爬虫通常不会执行 JavaScript,Hash 模式的 URL 可能被忽略;History 模式的 URL 结构清晰,更容易被收录。 -**问题出现**:随着功能增加,路由文件迅速膨胀到200+行,维护困难。 - -### 2.2 发展期:按模块拆分 - -2020年,团队决定将路由按业务模块拆分: - -```javascript -// router/index.js -import dashboardRoutes from './modules/dashboard' -import userRoutes from './modules/user' -import projectRoutes from './modules/project' - -const routes = [ - { path: '/', component: Home }, - ...dashboardRoutes, - ...userRoutes, - ...projectRoutes, - { path: '/:path(.*)*', component: NotFound } -] -``` - -```javascript -// router/modules/project.js -export default [ - { - path: '/projects', - component: ProjectList, - meta: { title: '项目列表', requiresAuth: true } - }, - { - path: '/projects/:id', - component: ProjectDetail, - meta: { title: '项目详情' }, - children: [ - { path: '', component: ProjectOverview }, - { path: 'tasks', component: ProjectTasks }, - { path: 'members', component: ProjectMembers } - ] - } -] -``` - -**好处**:每个模块独立维护,新增功能只需修改对应模块。 - -### 2.3 成熟期:动态权限路由 - -2021年,平台引入RBAC权限系统,需要根据不同用户角色动态生成路由: - -```javascript -// 后端返回的菜单/路由配置 -const serverRouteConfig = [ - { - path: '/admin', - name: 'Admin', - component: 'Layout', - meta: { icon: 'setting', roles: ['admin', 'super_admin'] }, - children: [ - { path: 'users', component: 'UserManagement', meta: { title: '用户管理' } }, - { path: 'roles', component: 'RoleManagement', meta: { title: '角色管理' } } - ] - }, - { - path: '/finance', - name: 'Finance', - component: 'Layout', - meta: { icon: 'money', roles: ['finance', 'admin'] }, - children: [ - { path: 'invoices', component: 'InvoiceList', meta: { title: '发票管理' } }, - { path: 'reports', component: 'FinanceReport', meta: { title: '财务报表' } } - ] - } -] - -// 路由生成器 -function generateRoutes(config, userRoles) { - return config - .filter(route => { - // 检查用户是否有权限访问该路由 - const requiredRoles = route.meta?.roles || [] - return requiredRoles.some(role => userRoles.includes(role)) - }) - .map(route => ({ - ...route, - component: () => import(`@/views/${route.component}.vue`), - children: route.children ? generateRoutes(route.children, userRoles) : undefined - })) -} - -// 在路由守卫中动态添加 -router.beforeEach(async (to, from, next) => { - const userStore = useUserStore() - - if (!userStore.hasGeneratedRoutes) { - const userRoles = userStore.roles - const accessRoutes = generateRoutes(serverRouteConfig, userRoles) - - accessRoutes.forEach(route => router.addRoute(route)) - userStore.hasGeneratedRoutes = true - - // 重新导航到目标路由 - next({ ...to, replace: true }) - } else { - next() - } -}) -``` - -**演进总结**: - -| 阶段 | 特点 | 解决问题 | -|------|------|----------| -| 初期 | 扁平路由 | 快速上线 | -| 发展期 | 模块拆分 | 维护性 | -| 成熟期 | 动态权限 | 安全性 | +**部署难度**:Hash 模式"开箱即用",History 模式需要运维知识(Nginx、Apache 等)。这也是为什么很多个人项目默认用 Hash 模式的原因。 +::: --- -## 3. 原理深入:路由工作原理 +## 3. 演进之路:从传统网站到现代路由 -### 3.1 Hash 模式的实现原理 +讲了这么多概念,让我们看一个真实的案例:某电商网站是如何从"传统多页面"一步步进化到"现代单页应用路由"的。通过这个案例,你会更直观地理解前端路由解决了什么问题。 -Hash 模式的核心是利用 URL 中的 `hash` 部分(即 `#` 后面的内容)。hash 的变化不会触发页面刷新,但会产生历史记录。 +::: tip 📖 背景知识:MPA、SPA、SSR 是什么? +在开始案例之前,先简单介绍一下这些名词: -**工作流程**: +- **MPA(Multi-Page Application)**:**多页面应用**,传统网站的开发方式。每个页面是独立的 HTML 文件,页面跳转会刷新整个页面。 +- **SPA(Single-Page Application)**:**单页面应用**,现代前端的主流方式。只有一个 HTML 入口,页面切换通过 JavaScript 动态替换组件,无刷新。 +- **SSR(Server-Side Rendering)**:**服务端渲染**,在服务器端生成完整的 HTML。结合了 SPA 和 MPA 的优点,首屏渲染快、SEO 好。 +**简单理解**:MPA 是"每次翻页都重新画",SPA 是"在同一张纸上擦了再画",SSR 是"提前在纸上画好再给你"。 +::: + +### 3.1 演进的全景图 + +下面这张表展示了前端应用的四个演进阶段,你可以看到路由技术是如何一步步发展的: + +| 阶段 | 应用类型 | 路由实现 | 核心特点 | 用户体验 | +|------|---------|---------|---------|---------| +| **阶段一:传统多页** | MPA | 服务端路由 | 每个页面独立 HTML 文件 | 每次跳转都刷新 | +| **阶段二:早期 SPA** | SPA(Hash 模式) | Hash 路由 | URL 带 `#`,兼容性好 | 无刷新,但 URL 不美观 | +| **阶段三:现代 SPA** | SPA(History 模式) | History 路由 | URL 美观,需服务端配置 | 流畅,URL 接近传统网站 | +| **阶段四:混合渲染** | SPA + SSR | 同构路由 | 首屏服务端渲染,后续前端路由 | 首屏快、SEO 好、体验流畅 | + +::: tip 📊 从表格中你能看到什么? +让我们逐行解读这张表: + +**阶段一 → 阶段二**:从"有刷新"到"无刷新",这是质的飞跃。用户第一次体验到了"像 App 一样"的流畅感,但代价是 URL 中带着 `#`,看起来不太专业。 + +**阶段二 → 阶段三**:从"能用"到"好用"。History 模式让 URL 变得美观,更接近传统网站,但代价是增加了部署复杂度(需要配置服务器)。 + +**阶段三 → 阶段四**:从"体验好"到"体验好 + SEO 好"。SSR 解决了 SPA 的 SEO 问题,首屏渲染速度也更快,但实现复杂度大幅提升。 + +**总结一下**:前端路由演进不只是"切换变快了",而是**整个应用架构的升级**——从服务端主导到前端主导,再到前后端结合,每一步都在平衡用户体验、开发成本、SEO 等多个维度。 +::: + +### 3.2 阶段一:传统多页应用——每次都刷新 + +为什么叫"传统多页应用"?因为这个阶段每个页面都是独立的 HTML 文件,页面跳转时浏览器会重新下载所有资源(HTML、CSS、JS)。这是最早的 Web 开发方式,现在很多传统网站仍然这样运作。 + +在这个阶段,电商网站"买得多"用的是典型的 MPA 架构: + +**开发方式**: +- **路由实现**:服务端路由,每个页面对应服务器上的一个 HTML 文件 +- **页面跳转**:使用 `<a href="/products/123">`,触发完整的页面刷新 +- **状态管理**:每次跳转都会丢失之前的页面状态(滚动位置、表单内容等) + +**这个阶段的特点**: +- ✅ **优点**:实现简单,对搜索引擎友好(SEO 好),浏览器前进后退开箱即用 +- ❌ **缺点**:每次跳转都刷新,用户体验差,服务器压力大(重复加载相同资源) + +::: details 查看当时的项目结构和访问流程 +**项目结构**(服务端渲染的典型结构): ``` -┌─────────────────────────────────────────────────────────────┐ -│ Hash 模式工作流程 │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ 1. 初始状态 │ -│ URL: https://example.com/#/home │ -│ 当前 hash: #/home │ -│ │ -│ 2. 用户点击导航链接 │ -│ 链接: <a href="#/user/123">用户中心</a> │ -│ │ -│ 3. hashchange 事件触发 │ -│ 浏览器自动更新 URL: │ -│ https://example.com/#/user/123 │ -│ │ -│ 4. 路由处理器执行 │ -│ ┌─────────────────────┐ │ -│ │ 1. 解析 hash 值 │ │ -│ │ → 提取 /user/123 │ │ -│ │ │ │ -│ │ 2. 匹配路由配置 │ │ -│ │ → 匹配 /user/:id │ │ -│ │ │ │ -│ │ 3. 提取参数 │ │ -│ │ → { id: "123" } │ │ -│ │ │ │ -│ │ 4. 渲染组件 │ │ -│ │ → UserDetail.vue │ │ -│ └─────────────────────┘ │ -│ │ -│ 5. 浏览器历史栈 │ -│ history: ["/home", "/user/123"] │ -│ 用户可点击后退按钮回到 /home │ -│ │ -└─────────────────────────────────────────────────────────────┘ +server/ +├── views/ # HTML 模板 +│ ├── index.html # 首页模板 +│ ├── products.html # 商品列表页模板 +│ └── product.html # 商品详情页模板 +├── public/ # 静态资源 +│ ├── css/ +│ ├── js/ +│ └── images/ +└── server.js # 服务器入口 ``` -**核心代码实现**: +**页面跳转流程**: +``` +1. 用户点击链接 <a href="/products/123"> + ↓ +2. 浏览器发送 GET 请求到服务器 + ↓ +3. 服务器渲染 product.html,插入数据 + ↓ +4. 返回完整的 HTML 页面 + ↓ +5. 浏览器解析 HTML、下载 CSS/JS、渲染页面 + ↓ +6. 用户看到页面(这个过程通常需要 1-3 秒) +``` +**用户的痛点**: +- 点击链接后页面白屏,等待时间长 +- 每次跳转都重新下载相同的 CSS/JS 文件 +- 浏览器前进后退会重新加载页面 +- 无法保存复杂的页面状态(如筛选条件、滚动位置) +::: + +这种开发方式在小网站还能接受,但随着网站规模变大、用户对体验要求提高,这些问题开始严重影响用户留存和转化率。 + +### 3.3 阶段二:早期单页应用——Hash 路由的时代 + +传统多页应用的问题积累到一定程度,"买得多"团队决定引入前端路由,升级到单页应用架构。这是一个重要的转折点——从"服务端主导"进入"前端主导"。 + +但这个阶段也有代价:URL 中带着 `#`,看起来不够专业,搜索引擎收录也有问题。 + +**开发方式**: +- **路由实现**:Hash 路由,利用 URL 中的 `#` 部分 +- **页面跳转**:JavaScript 拦截链接点击,动态替换组件 +- **状态管理**:页面状态在客户端保持,不需要重新加载 + +**这个阶段的特点**: +- ✅ **优点**:无刷新切换,用户体验流畅,服务器压力减小 +- ❌ **缺点**:URL 带 `#`,SEO 不友好,首次加载较慢 + +::: details 查看 Hash 路由的实现方式 +**项目结构**(早期 SPA 的典型结构): +``` +project/ +├── index.html # 唯一的 HTML 入口文件 +├── css/ +│ └── app.css # 所有样式打包在一个文件 +├── js/ +│ ├── router.js # 简单的路由实现 +│ ├── views/ # 页面组件 +│ │ ├── Home.js +│ │ ├── ProductList.js +│ │ └── ProductDetail.js +│ └── app.js # 应用入口 +└── server.js # 简单的静态文件服务器 +``` + +**Hash 路由的核心代码**: ```javascript +// router.js - 简化的 Hash 路由实现 class HashRouter { constructor(routes) { this.routes = routes - this.currentPath = '' + this.currentPath = null - // 初始化时解析当前 hash - this.parseHash() - - // 监听 hashchange 事件 + // 监听 hash 变化 window.addEventListener('hashchange', () => { - this.parseHash() + this.matchRoute() }) + + // 初始化 + this.matchRoute() } - parseHash() { - // 获取 hash,去掉开头的 # + matchRoute() { + // 获取当前 hash(去掉 #) const hash = window.location.hash.slice(1) || '/' - this.navigate(hash) - } - - navigate(path) { - this.currentPath = path - const route = this.matchRoute(path) + const route = this.routes.find(r => r.path === hash) if (route) { - this.render(route.component, route.params) + this.render(route.component) } else { this.render(NotFoundComponent) } } - matchRoute(path) { - for (const route of this.routes) { - const match = this.parseRoute(route.path, path) - if (match) { - return { ...route, params: match.params } - } - } - return null - } - - parseRoute(routePath, actualPath) { - // 将 /user/:id 转换为正则表达式 - const paramNames = [] - const regexPath = routePath.replace(/:([^/]+)/g, (match, name) => { - paramNames.push(name) - return '([^/]+)' - }) - - const regex = new RegExp(`^${regexPath}$`) - const match = actualPath.match(regex) - - if (!match) return null - - // 提取参数 - const params = {} - paramNames.forEach((name, index) => { - params[name] = match[index + 1] - }) - - return { params } - } - - render(component, params = {}) { - // 实际的DOM渲染逻辑 + render(component) { const app = document.getElementById('app') - app.innerHTML = '' - const instance = new component({ params }) - app.appendChild(instance.mount()) + app.innerHTML = component.template() + component.mount?.(app) } - push(path) { + navigate(path) { window.location.hash = path } } -// 使用示例 +// 使用 const router = new HashRouter([ { path: '/', component: Home }, - { path: '/user', component: UserList }, - { path: '/user/:id', component: UserDetail }, - { path: '/products/:category/:id', component: ProductDetail } + { path: '/products', component: ProductList }, + { path: '/products/:id', component: ProductDetail } ]) -// 编程式导航 -router.push('/user/123') +// 导航 +router.navigate('/products/123') ``` -### 3.2 History 模式的实现原理 +**URL 形式**: +- 首页:`https://example.com/#/` +- 商品列表:`https://example.com/#/products` +- 商品详情:`https://example.com/#/products/123` -History 模式利用 HTML5 History API(主要是 `pushState` 和 `replaceState`)来实现 URL 的改变,同时不会触发页面刷新。 +**带来的改善**: +1. **用户体验提升**:页面切换无刷新,流畅自然 +2. **服务器压力减小**:只加载一次 HTML/CSS/JS,后续只请求数据 +3. **状态保持**:滚动位置、表单内容等状态可以在页面切换时保持 +4. **离线友好**:配合 Service Worker 可以实现离线访问 -**与 Hash 模式的核心区别**: +**新的痛点**: +1. **URL 不美观**:`#` 让 URL 看起来像"锚点跳转",不够专业 +2. **SEO 问题**:搜索引擎爬虫可能忽略 hash 后的内容,导致页面无法被收录 +3. **首次加载慢**:需要一次性加载所有 JavaScript,首屏时间较长 +::: -| 特性 | Hash 模式 | History 模式 | -|------|-----------|--------------| -| URL 变化 | 修改 `#` 部分 | 修改完整路径 | -| 浏览器事件 | `hashchange` | `popstate` | -| 服务端感知 | 不感知 hash | 会收到请求 | -| SEO | 较差 | 良好 | +### 3.4 阶段三:现代单页应用——History 路由成为主流 -**工作流程**: +Hash 路由的痛点(URL 不美观、SEO 差)困扰了开发者很多年。随着 HTML5 的普及和浏览器兼容性的提升,History 路由逐渐成为主流。 +History 路由利用 HTML5 History API,可以让 URL 变得"正常"(没有 `#`),但代价是需要服务端配合配置。 + +**开发方式**: +- **路由实现**:History 路由,使用 `pushState` 和 `replaceState` +- **路由库**:Vue Router、React Router 等成熟路由库 +- **服务端配置**:需要配置服务器将所有路由回退到 `index.html` + +**这个阶段的特点**: +- ✅ **优点**:URL 美观,SEO 友好,用户体验流畅 +- ❌ **缺点**:部署需要特殊配置,服务器端必须配合 + +::: details History 路由的实现和部署配置 +**项目结构**(现代 SPA 的典型结构): ``` -┌─────────────────────────────────────────────────────────────┐ -│ History 模式工作流程 │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ 1. 初始状态 │ -│ URL: https://example.com/home │ -│ 浏览器历史栈: ["/home"] │ -│ │ -│ 2. 用户点击导航链接 │ -│ 链接: <a href="/user/123" data-nav>用户中心</a> │ -│ │ -│ 3. 拦截导航行为 │ -│ ┌──────────────────────────┐ │ -│ │ 阻止默认行为 │ │ -│ │ event.preventDefault() │ │ -│ └──────────────────────────┘ │ -│ │ -│ 4. 调用 History API │ -│ ┌────────────────────────────┐ │ -│ │ history.pushState( │ │ -│ │ { userId: 123 }, │ // state 数据 │ -│ │ "用户中心", │ // 页面标题 │ -│ │ "/user/123" │ // 新 URL │ -│ │ ) │ │ -│ └────────────────────────────┘ │ -│ │ -│ URL 更新为: https://example.com/user/123 │ -│ ⚠️ 注意:此时页面不会刷新! │ -│ │ -│ 5. 路由匹配与渲染 │ -│ ┌─────────────────────────┐ │ -│ │ 1. 解析路径 /user/123 │ │ -│ │ │ │ -│ │ 2. 匹配路由配置 │ │ -│ │ /user/:id │ │ -│ │ │ │ -│ │ 3. 提取参数 │ │ -│ │ { id: "123" } │ │ -│ │ │ │ -│ │ 4. 渲染组件 │ │ -│ │ UserDetail.vue │ │ -│ │ │ │ -│ │ 5. 更新页面标题 │ │ -│ │ document.title │ │ -│ └─────────────────────────┘ │ -│ │ -│ 6. 浏览器历史栈 │ -│ history: ["/home", "/user/123"] │ -│ │ -│ 用户可以: │ -│ - 点击后退 → 回到 /home │ -│ - 点击前进 → 回到 /user/123 │ -│ - 直接修改URL访问 │ -│ │ -│ 7. 处理浏览器前进/后退 │ -│ ┌────────────────────────────────┐ │ -│ │ window.addEventListener( │ │ -│ │ 'popstate', │ │ -│ │ (event) => { │ │ -│ │ // 获取 state 数据 │ │ -│ │ const state = event.state │ │ -│ │ │ │ -│ │ // 根据当前 URL 重新渲染 │ │ -│ │ const path = location.pathname │ │ -│ │ router.match(path) │ │ -│ │ } │ │ -│ │ ) │ │ -│ └────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ +project/ +├── public/ +│ └── index.html # 唯一的 HTML 入口 +├── src/ +│ ├── router/ +│ │ └── index.js # 路由配置 +│ ├── views/ # 页面组件 +│ │ ├── Home.vue +│ │ ├── ProductList.vue +│ │ └── ProductDetail.vue +│ ├── App.vue +│ └── main.js +├── package.json +└── vite.config.js # 构建配置 ``` -**服务端配置的关键作用**: +**Vue Router 配置示例**: +```javascript +// src/router/index.js +import { createRouter, createWebHistory } from 'vue-router' -History 模式的最大陷阱在于**服务端配置**。当用户直接访问 `https://example.com/user/123` 或刷新页面时,浏览器会向服务端发送请求。 +const router = createRouter({ + history: createWebHistory(), // History 模式 + routes: [ + { path: '/', component: () => import('@/views/Home.vue') }, + { path: '/products', component: () => import('@/views/ProductList.vue') }, + { path: '/products/:id', component: () => import('@/views/ProductDetail.vue') }, + { path: '/:pathMatch(.*)*', component: () => import('@/views/NotFound.vue') } + ] +}) -``` -用户直接访问 /user/123 - ↓ -浏览器发送 GET /user/123 到服务器 - ↓ -服务器查找 /user/123 对应的文件 - ↓ -❌ 找不到!返回 404 +export default router ``` -**正确的服务端配置**(以 Nginx 为例): +**URL 形式**: +- 首页:`https://example.com/` +- 商品列表:`https://example.com/products` +- 商品详情:`https://example.com/products/123` +**关键:Nginx 配置**(部署时必须配置): ```nginx server { listen 80; @@ -475,101 +462,492 @@ server { } ``` +**为什么需要这个配置?** + ``` -用户直接访问 /user/123 +场景:用户直接访问 https://example.com/products/123 + +❌ 没有配置的情况: +1. 浏览器向服务器请求 /products/123 +2. Nginx 在文件系统中查找 /products/123 +3. 找不到这个文件,返回 404 + +✅ 配置了 try_files 的情况: +1. 浏览器向服务器请求 /products/123 +2. Nginx 尝试查找文件 → 不存在 +3. 回退到 /index.html(根据 try_files 规则) +4. 浏览器加载 index.html +5. Vue Router 接管,解析 /products/123 +6. 渲染 ProductDetail 组件 +7. 页面正常显示! +``` + +**对比 Hash 模式的差异**: +| 对比项 | Hash 模式 | History 模式 | +|--------|----------|-------------| +| URL | `/#/products/123` | `/products/123` | +| 服务端配置 | 不需要 | **必须配置** | +| 直接访问 | ✅ 正常工作 | ❌ 需要服务端支持 | +| SEO | ⚠️ 较差 | ✅ 良好 | +::: + +### 3.5 阶段四:混合渲染——SPA + SSR 的终极方案 + +当 History 路由成熟后,团队开始关注更深层次的问题:如何既保留 SPA 的流畅体验,又解决 SEO 和首屏加载慢的问题? + +这个阶段的核心是"同构渲染"——首屏在服务端渲染(SEO 好、加载快),后续交互在前端路由(体验流畅)。 + +**开发方式**: +- **框架选择**:Next.js(React)、Nuxt.js(Vue) +- **渲染策略**:服务端渲染 + 客户端水合(Hydration) +- **路由模式**:History 模式(服务端已配置好) + +**这个阶段的特点**: +- ✅ **优点**:首屏快、SEO 好、后续交互流畅 +- ❌ **缺点**:实现复杂度高,需要服务端运行环境 + +::: details 混合渲染的工作原理 +**页面加载流程**: +``` +1. 用户访问 /products/123 ↓ -浏览器发送 GET /user/123 到服务器 +2. 服务端接收到请求 ↓ -Nginx 尝试查找 /user/123 文件 → 不存在 +3. 服务端渲染 ProductDetail 组件 → 生成完整 HTML ↓ -Nginx 回退到 /index.html +4. 返回 HTML 到浏览器(包含了完整的内容) ↓ -浏览器加载 SPA,前端路由接管 +5. 浏览器快速显示内容(首屏渲染快) ↓ -前端路由解析 /user/123 → 渲染 UserDetail 组件 +6. 加载 JavaScript,执行"水合"(Hydration) ↓ -✅ 页面正常显示! +7. 后续页面切换由前端路由接管(无刷新) +``` + +**传统 SPA vs SSR 的首屏对比**: + +| 对比项 | 传统 SPA | SSR | +|--------|---------|-----| +| 首屏内容 | 白屏 → 加载 JS → 渲染 | 立即显示内容 | +| SEO | 爬虫可能看不到内容 | 爬虫能看到完整 HTML | +| 首屏时间 | 较慢(需要加载 JS) | 较快(HTML 已包含内容) | +| 后续交互 | 流畅(前端路由) | 流畅(前端路由) | +::: + +--- + +## 4. 原理深入:路由是如何工作的? + +了解了实际案例后,让我们深入看看前端路由的工作原理,理解 Hash 和 History 两种模式到底有什么不同。 + +<RouterArchitectureDemo /> + +### 4.1 Hash 模式的工作原理 + +Hash 模式的核心是利用 URL 中的 `hash` 部分(即 `#` 后面的内容)。hash 有两个重要特性: + +1. **hash 的变化不会触发页面刷新** +2. **hash 的变化会记录在浏览器历史栈中** + +这意味着我们可以在不刷新页面的情况下改变 URL,同时浏览器的前进/后退按钮也能正常工作。 + +**工作流程**: + +``` +用户点击链接 <a href="#/user/123"> + ↓ +浏览器更新 URL(不刷新页面) +https://example.com/#/user/123 + ↓ +触发 hashchange 事件 + ↓ +路由监听器捕获事件 + ↓ +解析 hash 值 → /user/123 + ↓ +匹配路由配置 → 找到 UserDetail 组件 + ↓ +渲染组件到页面 +``` + +**核心代码实现**: + +```javascript +class HashRouter { + constructor(routes) { + this.routes = routes + + // 监听 hash 变化 + window.addEventListener('hashchange', () => { + this.loadRoute() + }) + + // 初始化加载 + this.loadRoute() + } + + loadRoute() { + // 获取当前 hash,去掉开头的 # + const hash = window.location.hash.slice(1) || '/' + const route = this.matchRoute(hash) + + if (route) { + this.render(route.component) + } + } + + matchRoute(path) { + return this.routes.find(r => r.path === path) + } + + render(component) { + document.getElementById('app').innerHTML = component.template() + } + + push(path) { + window.location.hash = path + } +} +``` + +::: tip 💡 Hash 模式的优点 +- **兼容性好**:IE8+ 都支持,几乎适用于所有浏览器 +- **部署简单**:不需要服务端配置,开箱即用 +- **实现简单**:只需要监听 `hashchange` 事件 +::: + +### 4.2 History 模式的工作原理 + +History 模式利用 HTML5 History API,提供了 `pushState`、`replaceState` 等方法,可以改变 URL 而不刷新页面。 + +**核心 API**: + +```javascript +// 添加新的历史记录 +history.pushState(state, title, url) +// 示例:history.pushState({id: 123}, '用户详情', '/user/123') + +// 替换当前历史记录 +history.replaceState(state, title, url) + +// 监听历史记录变化(前进/后退按钮) +window.addEventListener('popstate', (event) => { + // event.state 包含 pushState 时传入的 state +}) +``` + +**工作流程**: + +``` +用户点击链接 <a href="/user/123"> + ↓ +JavaScript 拦截点击事件 +event.preventDefault() + ↓ +调用 history.pushState +history.pushState({id: 123}, '用户详情', '/user/123') + ↓ +URL 更新(不刷新页面) +https://example.com/user/123 + ↓ +路由匹配并渲染组件 + ↓ +用户点击浏览器后退按钮 + ↓ +触发 popstate 事件 + ↓ +路由监听器捕获事件 + ↓ +根据新 URL 渲染对应组件 +``` + +**核心代码实现**: + +```javascript +class HistoryRouter { + constructor(routes) { + this.routes = routes + + // 拦截所有链接点击 + document.addEventListener('click', (e) => { + const link = e.target.closest('a') + if (link && link.getAttribute('href').startsWith('/')) { + e.preventDefault() + this.push(link.getAttribute('href')) + } + }) + + // 监听浏览器前进/后退 + window.addEventListener('popstate', () => { + this.loadRoute() + }) + + // 初始化加载 + this.loadRoute() + } + + loadRoute() { + const path = window.location.pathname + const route = this.matchRoute(path) + + if (route) { + this.render(route.component) + } + } + + push(path) { + history.pushState({}, '', path) + this.loadRoute() + } + + render(component) { + document.getElementById('app').innerHTML = component.template() + } +} +``` + +::: warning ⚠️ History 模式的陷阱 +History 模式最大的问题在于:**当用户直接访问某个 URL 或刷新页面时,浏览器会向服务器发送请求**。 + +如果服务器没有正确配置,会返回 404。解决方案是配置服务器让所有路由都回退到 `index.html`,让前端路由接管后续处理。 +::: + +--- + +## 5. 路由配置实战指南 + +理论讲得差不多了,下面是实际项目中常用的路由配置模式和最佳实践。 + +### 5.1 基础路由配置 + +::: details Vue Router 完整配置示例 + +```javascript +// src/router/index.js +import { createRouter, createWebHistory } from 'vue-router' +import Home from '@/views/Home.vue' +import NotFound from '@/views/NotFound.vue' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'Home', + component: Home + }, + { + path: '/user/:id', + name: 'UserDetail', + component: () => import('@/views/UserDetail.vue'), + props: true // 将路由参数作为 props 传递 + }, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: NotFound + } + ], + scrollBehavior(to, from, savedPosition) { + // 滚动行为:返回时保持滚动位置,否则滚动到顶部 + if (savedPosition) { + return savedPosition + } else { + return { top: 0 } + } + } +}) + +export default router +``` + +::: + +### 5.2 路由懒加载:提升首屏性能 + +路由懒加载是指只在访问某个路由时才加载对应的组件,而不是一次性加载所有组件。这可以显著减少首屏加载时间。 + +```javascript +// ❌ 一次性加载所有组件(首屏慢) +import Home from '@/views/Home.vue' +import About from '@/views/About.vue' +import User from '@/views/User.vue' + +const routes = [ + { path: '/', component: Home }, + { path: '/about', component: About }, + { path: '/user', component: User } +] + +// ✅ 懒加载(首屏快) +const routes = [ + { path: '/', component: () => import('@/views/Home.vue') }, + { path: '/about', component: () => import('@/views/About.vue') }, + { path: '/user', component: () => import('@/views/User.vue') } +] +``` + +<CodeSplittingDemo /> + +::: tip 💡 懒加载的原理 +当你使用 `import('@/views/Home.vue')` 时,Webpack/Vite 会把这个组件打包成单独的文件。只有当用户访问这个路由时,才会下载对应的文件。 + +打个比方:懒加载就像"按需点菜",而不是一次性把所有菜都端上来。这样可以减少首屏加载时间,提升用户体验。 +::: + +### 5.3 路由守卫:权限控制与导航拦截 + +路由守卫可以在路由跳转前后执行逻辑,常用于权限验证、页面标题设置、数据预加载等场景。 + +```javascript +// 全局前置守卫 +router.beforeEach(async (to, from, next) => { + // 设置页面标题 + document.title = to.meta.title || 'My App' + + // 权限验证 + if (to.meta.requiresAuth) { + const isAuthenticated = await checkAuth() + if (!isAuthenticated) { + next('/login') + return + } + } + + next() +}) + +// 全局后置钩子 +router.afterEach((to, from) => { + // 页面访问统计 + analytics.trackPageView(to.path) +}) + +// 路由级守卫 +const routes = [ + { + path: '/admin', + component: Admin, + meta: { requiresAuth: true, roles: ['admin'] }, + beforeEnter: (to, from, next) => { + // 这个路由的专属逻辑 + if (hasPermission()) { + next() + } else { + next('/403') + } + } + } +] +``` + +::: tip 💡 路由守卫的常见用途 +- **权限验证**:检查用户是否有权限访问某个页面 +- **页面标题**:动态设置 document.title +- **数据预加载**:在进入页面前提前获取数据 +- **进度条**:显示页面切换的进度条 +- **访问统计**:记录页面访问情况 +::: + +--- + +## 6. 常见问题与解决方案 + +### 6.1 部署后刷新 404 + +**问题**:本地开发正常,部署到服务器后,直接访问某个路由或刷新页面会显示 404。 + +**原因**:History 模式下,服务器会将 URL 当作文件路径去查找,但 SPA 的所有路由其实都指向 `index.html`。 + +**解决方案**:配置服务器 fallback。 + +```nginx +# Nginx 配置 +location / { + try_files $uri $uri/ /index.html; +} +``` + +```apache +# Apache 配置(.htaccess) +<IfModule mod_rewrite.c> + RewriteEngine On + RewriteBase / + RewriteRule ^index\.html$ - [L] + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule . /index.html [L] +</IfModule> +``` + +### 6.2 路由参数丢失 + +**问题**:页面刷新后,路由参数 `$route.params` 丢失。 + +**原因**:路由参数只在路由跳转时存在,刷新后需要从 URL 中重新解析。 + +**解决方案**: + +```javascript +// ❌ 错误做法:只在 created 时获取参数 +created() { + const userId = this.$route.params.id + this.fetchUser(userId) +} + +// ✅ 正确做法:监听路由变化 +watch: { + '$route.params.id': { + immediate: true, + handler(newId) { + this.fetchUser(newId) + } + } +} +``` + +### 6.3 页面切换时滚动位置异常 + +**问题**:页面切换后,滚动位置没有重置,或者返回时没有保持之前的位置。 + +**解决方案**:配置路由的 `scrollBehavior`。 + +```javascript +const router = createRouter({ + scrollBehavior(to, from, savedPosition) { + // 返回时保持滚动位置 + if (savedPosition) { + return savedPosition + } + // 跳转到锚点 + if (to.hash) { + return { el: to.hash } + } + // 否则滚动到顶部 + return { top: 0 } + } +}) ``` --- -## 4. 总结与学习建议 +## 7. 总结 +让我们用一张表格来回顾前端路由的核心概念: + +| 概念 | 一句话解释 | 解决的问题 | 代表方案 | +|------|-----------|-----------|----------| +| **路由** | URL 和组件的映射关系 | 访问不同 URL 显示不同内容 | Vue Router、React Router | +| **Hash 模式** | 利用 URL hash 实现路由 | 兼容性好、部署简单 | Vue Router Hash 模式 | +| **History 模式** | 利用 History API 实现路由 | URL 美观、SEO 好 | Vue Router History 模式 | +| **路由懒加载** | 按需加载路由组件 | 减少首屏加载时间 | `() => import('./Page.vue')` | +| **路由守卫** | 路由跳转前后的钩子函数 | 权限控制、数据预加载 | `beforeEach`、`beforeEnter` | +| **动态路由** | 带参数的路由 | 匹配一类路径而非单个 | `/user/:id` | + +::: info 写在最后 前端路由是现代单页应用的核心技术之一。从早期的 Hash 模式到现在主流的 History 模式,路由技术在不断进化,为用户提供更流畅的浏览体验。 -### 核心要点回顾 +理解路由的原理和模式,能让你在遇到部署、性能、SEO 问题时快速定位、精准解决。更重要的是,它能在项目架构设计时帮你做出更明智的选择——什么时候用 Hash、什么时候用 History、如何避免常见的坑。 -1. **理解两种路由模式的本质区别**:Hash 模式利用 URL hash 特性,History 模式利用 HTML5 History API -2. **服务端配置至关重要**:使用 History 模式必须正确配置服务端 fallback 到 index.html -3. **路由设计体现架构思维**:扁平化 vs 嵌套、静态 vs 动态,都反映了对业务的理解 -4. **权限路由要谨慎处理**:前后端都需要验证,不能依赖单一端做权限控制 - -### 学习路线图 - -``` -初级阶段 -├── 理解 SPA 与传统 MPA 的区别 -├── 掌握 Hash 和 History 模式的基本原理 -└── 能够使用 Vue Router / React Router 完成基础配置 - -进阶阶段 -├── 深入理解 History API 的底层实现 -├── 能够手写一个简单的前端路由库 -├── 掌握路由守卫、懒加载、滚动行为等高级特性 -└── 理解服务端配置原理,能够独立部署 SPA - -高级阶段 -├── 设计复杂的路由架构(微前端、嵌套路由等) -├── 实现基于权限的动态路由系统 -├── 路由性能优化(预加载、按需加载策略) -└── 多端路由方案设计(Web、小程序、App 统一路由) -``` - -### 实践建议 - -1. **动手实现一个迷你路由库**:不依赖框架,用原生 JS 实现 Hash 和 History 两种模式,这是理解原理的最佳方式。 - -2. **阅读源码**:Vue Router 和 React Router 的源码都相对易读,从中可以学到很多工程化实践经验。 - -3. **关注真实项目中的路由设计**:分析知名开源项目(如 GitLab、Jira、各种 Admin 系统)的路由结构,学习它们的组织方式。 - -4. **解决实际问题**:尝试在你的项目中实现以下功能: - - 面包屑导航自动生成 - - 页面切换动画 - - 路由级权限控制 - - 页面标题和 meta 信息动态更新 - -记住:**路由不只是"页面跳转",它反映了整个应用的信息架构**。一个好的路由设计,能让用户更容易理解你的产品,也能让代码更易维护。 - ---- - -## 5. 名词速查表 (Glossary) - -| 名词 | 英文全称 | 解释 | -| :--- | :--- | :--- | -| **SPA** | Single Page Application | **单页应用**。整个应用只有一个 HTML 页面,通过动态更新 DOM 实现页面切换,无需整页刷新。 | -| **MPA** | Multi-Page Application | **多页应用**。每个页面对应独立的 HTML 文件,页面跳转会触发完整的浏览器刷新。 | -| **Router** | - | **路由器/路由库**。负责管理 URL 与页面组件的映射关系,处理导航逻辑的库或模块。 | -| **Route** | - | **路由**。URL 路径与组件的映射配置,定义了访问某个路径时应该渲染什么内容。 | -| **Hash Mode** | - | **Hash 模式**。前端路由的一种实现方式,利用 URL 中的 hash(#)部分,不会触发页面刷新。 | -| **History Mode** | - | **History 模式**。前端路由的一种实现方式,利用 HTML5 History API,URL 更美观但需要服务端配合。 | -| **History API** | HTML5 History API | **历史记录 API**。浏览器提供的接口,允许在不刷新页面的情况下操作浏览器历史记录。 | -| **pushState** | - | **压入状态**。History API 的方法,将指定状态添加到历史记录栈,改变 URL 但不刷新页面。 | -| **replaceState** | - | **替换状态**。History API 的方法,修改当前历史记录条目,不会创建新记录。 | -| **popstate** | - | **历史变化事件**。当用户点击前进/后退按钮或调用 history.back/forward 时触发的事件。 | -| **hashchange** | - | **Hash 变化事件**。当 URL 中的 hash 部分发生变化时触发的事件。 | -| **Nested Route** | - | **嵌套路由**。在一个路由内定义子路由,形成层级结构,对应页面的嵌套布局。 | -| **Dynamic Route** | - | **动态路由**。包含参数的路由,如 `/user/:id`,可以匹配多个具体的 URL。 | -| **Route Parameter** | - | **路由参数**。URL 中的动态部分,如 `:id`、`:name`,可以在组件中获取使用。 | -| **Wildcard Route** | - | **通配符路由**。匹配任意路径的路由,通常用于 404 页面,如 `*` 或 `(.*)*`。 | -| **Lazy Loading** | - | **懒加载/按需加载**。只在需要时才加载路由对应的组件,减少首屏加载时间。 | -| **Route Guard** | - | **路由守卫**。在路由跳转前后执行的钩子函数,用于权限验证、日志记录等。 | -| **Navigation** | - | **导航**。在应用中切换页面的行为,可以通过链接点击或编程方式触发。 | -| **Programmatic Navigation** | - | **编程式导航**。通过代码而非点击链接来触发路由跳转,如 `router.push()`。 | -| **Fallback** | - | **回退/兜底**。当请求的资源不存在时返回的默认内容,如 SPA 的 `index.html` 回退。 | -| **SEO** | Search Engine Optimization | **搜索引擎优化**。提升网站在搜索引擎中排名的技术和方法。 | -| **SSR** | Server-Side Rendering | **服务端渲染**。在服务器端生成 HTML 内容,有利于 SEO 和首屏加载。 | -| **TTFB** | Time To First Byte | **首字节时间**。从发起请求到接收到服务器第一个字节的时间。 | -| **Breadcrumb** | - | **面包屑导航**。显示当前页面在网站层级结构中位置的导航元素。 | -| **Active Link** | - | **激活链接**。当前匹配路由的导航链接,通常有特殊样式标识。 | -<|tool_calls_section_begin|><|tool_call_begin|>functions.Write:16<|tool_call_argument_begin|>{ \ No newline at end of file +希望这篇文章能帮助你建立起对前端路由的整体认知。当你在实际项目中遇到路由相关的问题时,能够知道从哪里入手、如何定位、怎样解决。 +::: diff --git a/package.json b/package.json index 515373a..91f88e8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "vitepress dev docs", "build": "vitepress build docs", "preview": "vitepress preview docs", - "format": "prettier --write ." + "format": "prettier --write .", + "verify": "bash scripts/verify.sh" }, "keywords": [ "easy-vibe",