docs: 重构 README 附录展示 & 新增多个附录交互组件

README 更新:
- 移除顶部 header.png 横幅图片
- 新增「附录知识库」板块,以 3×3 网格展示 9 大知识领域精选内容
- 附录链接指向部署版网站 (datawhalechina.github.io)
- 阶段表格新增「附录」行,突出 80+ 交互式专题
- 章节标题「新手入门 & PM」简化为「零基础入门」
- News 新增 2026-02-25 附录知识库更新条目

新增交互组件:
- 异步任务队列 (async-task-queues) 演示组件
- 文件存储 (file-storage) 演示组件
- 项目架构 (project-architecture) 演示组件
- 限流与背压 (rate-limiting) 演示组件
- 搜索引擎 (search-engines) 演示组件
- 计算机基础: AppLaunch/BiosUefi/OSBoot 等启动流程演示组件

新增附录文档:
- 前端项目架构 (frontend-project-architecture.md)
- 后端项目架构 (backend-project-architecture.md)

内容优化:
- 算法思维、数据结构、编程语言、调试艺术等多篇附录内容更新
- HTML/CSS 布局、请求旅程等前后端文档完善
- 附录索引页 (index.md) 同步更新
This commit is contained in:
sanbuphy
2026-02-25 12:22:49 +08:00
parent f44c842fe7
commit df51f84ab5
36 changed files with 8535 additions and 393 deletions
+95 -4
View File
@@ -1,6 +1,5 @@
<!-- trigger vercel build -->
<div align="center">
<img src="assets/header.png" width="100%" />
<pre style="font-family: 'Courier New', monospace; font-size: 16px; color: #000000; margin: 0; padding: 0; line-height: 1.2; transform: skew(-1deg, 0deg); display: block;">
███████╗ █████╗ ███████╗██╗ ██╗ ██╗ ██╗██╗██████╗ ███████╗
@@ -115,9 +114,11 @@ Easy-Vibe 通过以下几个阶段,带你从 0 到 1:
| **第一阶段** | AI 编程入门、产品思维、原型设计 | 互动小游戏、Web 应用原型(新手入门 & PM) |
| **第二阶段** | 全栈开发、AI 集成、数据库 | 完整的全栈 AI 应用 |
| **第三阶段** | claude code 进阶、小程序安卓开发 | 生产级多平台应用 |
| **附录** | 帮你理解计算机、人工智能基础概念 | 9 大知识领域、80+ 交互式专题 |
## 🔥 News
- **[2026-02-25]** 更新[附录知识库](https://datawhalechina.github.io/easy-vibe/zh-cn/appendix/),涵盖 9 大知识领域、80+ 交互式专题。
- **[2026-01-27]** 新增 Android 和 iOS 平台应用开发教程。
- **[2026-01-19]** 发布 Prompt Engineering、AI 演进史、鉴权设计、Git 原理等一系列交互式演示组件,大幅提升可视化学习体验。
@@ -136,11 +137,101 @@ Easy-Vibe 通过以下几个阶段,带你从 0 到 1:
<img src="assets/readme-image1.png" alt="Learning Map" width="70%" style="border-radius: 10px; box-shadow: 0 8px 20px rgba(45,55,72,0.3); margin: 15px 0;"/>
</div>
### 总附录
### 📚 附录知识库
[AI 能力词典:常见 AI 核心概念与名词、场景解释](docs/zh-cn/appendix/ai-capability-dictionary.md)
> 涵盖 **9 大知识领域**、**80+ 交互式专题**,以动画和可视化组件帮助你直观理解从计算机底层到 AI 前沿的核心概念
>
> 👉 [查看完整附录](https://datawhalechina.github.io/easy-vibe/zh-cn/appendix/) · [AI 能力词典](https://datawhalechina.github.io/easy-vibe/zh-cn/appendix/8-artificial-intelligence/ai-capability-dictionary)
### 一、新手入门 & PM - 从游戏到 Web 应用原型
<table>
<tr>
<td valign="top" width="33%">
<strong>💻 计算机基础</strong><br><br>
• <a href="docs/zh-cn/appendix/1-computer-fundamentals/transistor-to-cpu.md">从晶体管到 CPU</a><br>
• <a href="docs/zh-cn/appendix/1-computer-fundamentals/operating-systems.md">操作系统</a><br>
• <a href="docs/zh-cn/appendix/1-computer-fundamentals/data-encoding-storage.md">数据的编码、存储与传输</a><br>
• <a href="docs/zh-cn/appendix/1-computer-fundamentals/computer-networks.md">网络:两台电脑如何对话</a><br>
• <a href="docs/zh-cn/appendix/1-computer-fundamentals/data-structures.md">数据结构</a><br>
• <a href="docs/zh-cn/appendix/1-computer-fundamentals/algorithm-thinking.md">算法思维入门</a>
</td>
<td valign="top" width="33%">
<strong>🔧 开发工具</strong><br><br>
• <a href="docs/zh-cn/appendix/2-development-tools/git-version-control.md">Git:代码的时光机</a><br>
• <a href="docs/zh-cn/appendix/2-development-tools/command-line-shell.md">命令行与 Shell 脚本</a><br>
• <a href="docs/zh-cn/appendix/2-development-tools/package-managers.md">包管理器</a><br>
• <a href="docs/zh-cn/appendix/2-development-tools/debugging-art/">调试的艺术</a><br>
• <a href="docs/zh-cn/appendix/2-development-tools/regex.md">正则表达式</a><br>
• <a href="docs/zh-cn/appendix/2-development-tools/environment-path.md">环境变量与 PATH</a>
</td>
<td valign="top" width="33%">
<strong>🌐 浏览器与前端</strong><br><br>
• <a href="docs/zh-cn/appendix/3-browser-and-frontend/javascript-deep-dive.md">JavaScript 语言深入</a><br>
• <a href="docs/zh-cn/appendix/3-browser-and-frontend/browser-as-os-rendering.md">浏览器渲染管道</a><br>
• <a href="docs/zh-cn/appendix/3-browser-and-frontend/frontend-frameworks.md">前端框架对比</a><br>
• <a href="docs/zh-cn/appendix/3-browser-and-frontend/graphics-animation.md">图形与动画</a><br>
• <a href="docs/zh-cn/appendix/3-browser-and-frontend/web-performance.md">网页性能的度量与优化</a><br>
• <a href="docs/zh-cn/appendix/3-browser-and-frontend/frontend-engineering.md">前端工程化全貌</a>
</td>
</tr>
<tr>
<td valign="top" width="33%">
<strong>🖥️ 服务器与后端</strong><br><br>
• <a href="docs/zh-cn/appendix/4-server-and-backend/http-protocol.md">HTTP 协议</a><br>
• <a href="docs/zh-cn/appendix/4-server-and-backend/api-design.md">API 设计哲学</a><br>
• <a href="docs/zh-cn/appendix/4-server-and-backend/auth-authorization.md">认证与授权体系</a><br>
• <a href="docs/zh-cn/appendix/4-server-and-backend/concurrency-async.md">并发、异步与多线程</a><br>
• <a href="docs/zh-cn/appendix/4-server-and-backend/message-queues.md">消息队列与事件驱动</a><br>
• <a href="docs/zh-cn/appendix/4-server-and-backend/backend-layered-architecture.md">后端分层架构</a>
</td>
<td valign="top" width="33%">
<strong>📊 数据</strong><br><br>
• <a href="docs/zh-cn/appendix/5-data/sql.md">SQL</a><br>
• <a href="docs/zh-cn/appendix/5-data/database-fundamentals.md">数据库原理</a><br>
• <a href="docs/zh-cn/appendix/5-data/data-tracking.md">数据埋点与用户行为采集</a><br>
• <a href="docs/zh-cn/appendix/5-data/data-analysis.md">数据分析基础</a><br>
• <a href="docs/zh-cn/appendix/5-data/ab-testing.md">A/B 测试与实验驱动</a><br>
• <a href="docs/zh-cn/appendix/5-data/data-visualization.md">数据可视化与仪表盘</a>
</td>
<td valign="top" width="33%">
<strong>🏗️ 架构与系统设计</strong><br><br>
• <a href="docs/zh-cn/appendix/6-architecture-and-system-design/monolith-to-microservices.md">从单体到微服务的演进</a><br>
• <a href="docs/zh-cn/appendix/6-architecture-and-system-design/distributed-systems.md">分布式系统的挑战</a><br>
• <a href="docs/zh-cn/appendix/6-architecture-and-system-design/high-availability.md">高可用与容灾</a><br>
• <a href="docs/zh-cn/appendix/6-architecture-and-system-design/system-design-methodology.md">系统设计方法论</a>
</td>
</tr>
<tr>
<td valign="top" width="33%">
<strong>☁️ 基础设施与运维</strong><br><br>
• <a href="docs/zh-cn/appendix/7-infrastructure-and-operations/docker-containers.md">Docker 容器化</a><br>
• <a href="docs/zh-cn/appendix/7-infrastructure-and-operations/kubernetes.md">Kubernetes 编排</a><br>
• <a href="docs/zh-cn/appendix/7-infrastructure-and-operations/ci-cd.md">CI / CD 自动化</a><br>
• <a href="docs/zh-cn/appendix/7-infrastructure-and-operations/dns-https.md">域名、DNS 与 HTTPS</a><br>
• <a href="docs/zh-cn/appendix/7-infrastructure-and-operations/monitoring-logging.md">监控、日志与告警</a><br>
• <a href="docs/zh-cn/appendix/7-infrastructure-and-operations/infrastructure-as-code.md">基础设施即代码</a>
</td>
<td valign="top" width="33%">
<strong>🤖 人工智能</strong><br><br>
• <a href="docs/zh-cn/appendix/8-artificial-intelligence/llm-principles.md">大语言模型的工作原理</a><br>
• <a href="docs/zh-cn/appendix/8-artificial-intelligence/transformer-attention.md">Transformer 与注意力机制</a><br>
• <a href="docs/zh-cn/appendix/8-artificial-intelligence/rag.md">RAG 架构</a><br>
• <a href="docs/zh-cn/appendix/8-artificial-intelligence/ai-agents.md">AI Agent 与工具调用</a><br>
• <a href="docs/zh-cn/appendix/8-artificial-intelligence/prompt-engineering.md">提示词工程</a><br>
• <a href="docs/zh-cn/appendix/8-artificial-intelligence/image-generation.md">图像生成原理</a>
</td>
<td valign="top" width="33%">
<strong>🎯 工程素养</strong><br><br>
• <a href="docs/zh-cn/appendix/9-engineering-excellence/code-quality-refactoring.md">代码质量与重构</a><br>
• <a href="docs/zh-cn/appendix/9-engineering-excellence/testing-strategies.md">测试策略</a><br>
• <a href="docs/zh-cn/appendix/9-engineering-excellence/design-patterns.md">设计模式</a><br>
• <a href="docs/zh-cn/appendix/9-engineering-excellence/security-thinking.md">安全思维与攻防基础</a><br>
• <a href="docs/zh-cn/appendix/9-engineering-excellence/technical-writing.md">技术文档写作</a><br>
• <a href="docs/zh-cn/appendix/9-engineering-excellence/open-source-collaboration.md">开源协作</a>
</td>
</tr>
</table>
### 一、零基础入门
| 章节 | 关键内容 | 状态 |
| :----------------------------------------------------------------------------------------------- | :------------------------------------------------ | :--- |
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

+12 -4
View File
@@ -613,13 +613,13 @@ export default defineConfig({
text: '编程语言图谱',
link: '/zh-cn/appendix/1-computer-fundamentals/programming-languages'
},
{
text: '类型系统入门',
link: '/zh-cn/appendix/1-computer-fundamentals/type-systems'
},
{
text: '编译原理入门',
link: '/zh-cn/appendix/1-computer-fundamentals/compilers'
},
{
text: '类型系统入门',
link: '/zh-cn/appendix/1-computer-fundamentals/type-systems'
}
]
},
@@ -725,6 +725,10 @@ export default defineConfig({
text: '前端工程化全貌',
link: '/zh-cn/appendix/3-browser-and-frontend/frontend-engineering'
},
{
text: '前端项目架构设计',
link: '/zh-cn/appendix/3-browser-and-frontend/frontend-project-architecture'
},
{
text: '无障碍与国际化',
link: '/zh-cn/appendix/3-browser-and-frontend/a11n-i18n'
@@ -807,6 +811,10 @@ export default defineConfig({
text: '后端分层架构',
link: '/zh-cn/appendix/4-server-and-backend/backend-layered-architecture'
},
{
text: '后端项目架构设计',
link: '/zh-cn/appendix/4-server-and-backend/backend-project-architecture'
},
{
text: '领域特定语言(DSL',
link: '/zh-cn/appendix/4-server-and-backend/domain-specific-languages'
@@ -0,0 +1,143 @@
<!--
AsyncComparisonDemo.vue
异步任务框架对比演示
-->
<template>
<div class="comparison-demo">
<div class="header">
<div class="title">主流异步任务框架对比</div>
<div class="subtitle">点击查看各框架详情</div>
</div>
<div class="framework-grid">
<div
v-for="fw in frameworks"
:key="fw.name"
:class="['fw-card', { active: selected === fw.name }]"
@click="selected = fw.name"
>
<div class="fw-name">{{ fw.name }}</div>
<div class="fw-lang">{{ fw.lang }}</div>
<div class="fw-stars">
<span v-for="n in 5" :key="n" :class="n <= fw.rating ? 'star-filled' : 'star-empty'"></span>
</div>
</div>
</div>
<div v-if="currentFw" class="detail-panel">
<div class="detail-header">
<span class="detail-name">{{ currentFw.name }}</span>
<span class="detail-lang-tag">{{ currentFw.lang }}</span>
</div>
<div class="detail-desc">{{ currentFw.desc }}</div>
<div class="detail-features">
<div class="feature-title">核心特性</div>
<div class="feature-list">
<span v-for="f in currentFw.features" :key="f" class="feature-tag">{{ f }}</span>
</div>
</div>
<div class="detail-usecase">
<div class="usecase-title">典型场景</div>
<div class="usecase-text">{{ currentFw.usecase }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selected = ref('Celery')
const frameworks = [
{
name: 'Celery',
lang: 'Python',
rating: 5,
desc: 'Python 生态最流行的分布式任务队列,支持多种消息中间件(RabbitMQ、Redis),功能全面且社区活跃。',
features: ['定时任务', '任务链', '结果存储', '自动重试', '优先级队列', '任务路由'],
usecase: '数据处理管道、邮件发送、报表生成、机器学习训练任务'
},
{
name: 'Sidekiq',
lang: 'Ruby',
rating: 5,
desc: 'Ruby 生态的高性能后台任务处理器,基于 Redis,使用多线程模型,内存效率极高。',
features: ['多线程', 'Web UI', '定时任务', '批量处理', '速率限制', '唯一任务'],
usecase: 'Rails 应用的邮件、通知、数据导入导出'
},
{
name: 'Bull',
lang: 'Node.js',
rating: 4,
desc: 'Node.js 生态最成熟的任务队列库,基于 Redis,支持优先级、延迟任务、重复任务等。BullMQ 是其下一代版本。',
features: ['优先级', '延迟任务', '速率限制', '并发控制', '事件驱动', 'Dashboard'],
usecase: 'API 后台处理、文件转换、爬虫任务、通知推送'
},
{
name: 'RQ',
lang: 'Python',
rating: 3,
desc: '轻量级 Python 任务队列,基于 Redis,API 简洁易用。适合不需要 Celery 全部功能的中小项目。',
features: ['简洁 API', '任务依赖', 'Worker 管理', '失败重试', 'Dashboard'],
usecase: '中小型 Web 应用的后台任务处理'
},
{
name: 'Kafka Streams',
lang: 'Java/JVM',
rating: 4,
desc: '基于 Kafka 的流处理框架,适合高吞吐量的实时数据处理场景,天然支持分布式和容错。',
features: ['流处理', '精确一次语义', '状态存储', '窗口操作', '高吞吐', '容错'],
usecase: '实时数据管道、事件驱动架构、日志聚合分析'
}
]
const currentFw = computed(() => frameworks.find(f => f.name === selected.value))
</script>
<style scoped>
.comparison-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header { margin-bottom: 1rem; }
.title { font-weight: 700; font-size: 1.1rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.framework-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.5rem; margin-bottom: 1rem;
}
.fw-card {
padding: 0.75rem; border-radius: 8px; background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider); cursor: pointer; text-align: center;
transition: all 0.2s;
}
.fw-card:hover { border-color: var(--vp-c-brand); }
.fw-card.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
.fw-name { font-weight: 700; font-size: 0.95rem; }
.fw-lang { font-size: 0.8rem; color: var(--vp-c-text-2); margin: 0.25rem 0; }
.fw-stars { font-size: 0.85rem; }
.star-filled { color: #f59e0b; }
.star-empty { color: var(--vp-c-divider); }
.detail-panel {
padding: 1rem; border-radius: 10px; background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
}
.detail-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
.detail-name { font-weight: 700; font-size: 1rem; }
.detail-lang-tag {
padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem;
background: rgba(var(--vp-c-brand-rgb), 0.1); color: var(--vp-c-brand);
}
.detail-desc { font-size: 0.9rem; color: var(--vp-c-text-2); margin-bottom: 0.75rem; line-height: 1.6; }
.feature-title, .usecase-title { font-weight: 600; font-size: 0.85rem; margin-bottom: 0.4rem; }
.feature-list { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.75rem; }
.feature-tag {
padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.75rem;
background: var(--vp-c-bg-soft); border: 1px solid var(--vp-c-divider);
}
.usecase-text { font-size: 0.85rem; color: var(--vp-c-text-2); }
</style>
@@ -0,0 +1,197 @@
<!--
AsyncTaskFlowDemo.vue
异步任务流程演示展示同步 vs 异步处理的对比
-->
<template>
<div class="async-task-demo">
<div class="header">
<div class="title">同步 vs 异步处理对比</div>
<div class="subtitle">点击按钮观察两种模式的差异</div>
</div>
<div class="mode-tabs">
<button
:class="['tab', { active: mode === 'sync' }]"
@click="mode = 'sync'; reset()"
>同步模式</button>
<button
:class="['tab', { active: mode === 'async' }]"
@click="mode = 'async'; reset()"
>异步模式</button>
</div>
<div class="flow-area">
<div class="user-side">
<div class="label">用户请求</div>
<button class="action-btn" @click="startProcess" :disabled="running">
{{ running ? '处理中...' : '提交订单' }}
</button>
<div :class="['response-box', { success: responseReady }]">
<template v-if="!running && !responseReady">等待提交</template>
<template v-else-if="running && mode === 'sync'">
用户等待中... ({{ elapsed }}s)
</template>
<template v-else-if="running && mode === 'async' && responseReady">
已返回 ({{ asyncResponseTime }}ms)
</template>
<template v-else-if="running && mode === 'async'">
等待响应...
</template>
<template v-else>
完成 ({{ mode === 'sync' ? syncTime + 'ms' : asyncResponseTime + 'ms' }})
</template>
</div>
</div>
<div class="arrow"></div>
<div class="server-side">
<div class="label">服务端处理</div>
<div class="tasks">
<div
v-for="(task, i) in tasks"
:key="i"
:class="['task-item', task.status]"
>
<span class="task-icon">{{ task.status === 'done' ? '✅' : task.status === 'running' ? '⏳' : '⬜' }}</span>
<span>{{ task.name }}</span>
<span class="task-time">{{ task.time }}ms</span>
</div>
</div>
</div>
</div>
<div class="summary" v-if="!running && responseReady">
<template v-if="mode === 'sync'">
<div class="summary-bad">同步模式用户等待了 {{ syncTime }}ms所有任务串行完成后才返回响应</div>
</template>
<template v-else>
<div class="summary-good">异步模式用户仅等待 {{ asyncResponseTime }}ms耗时任务在后台异步处理</div>
</template>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const mode = ref('sync')
const running = ref(false)
const responseReady = ref(false)
const elapsed = ref(0)
const syncTime = ref(0)
const asyncResponseTime = ref(200)
const tasks = ref([
{ name: '扣减库存', time: 50, status: 'pending' },
{ name: '创建订单', time: 100, status: 'pending' },
{ name: '发送确认邮件', time: 800, status: 'pending' },
{ name: '更新推荐系统', time: 600, status: 'pending' },
{ name: '记录审计日志', time: 300, status: 'pending' }
])
let timer = null
function reset() {
running.value = false
responseReady.value = false
elapsed.value = 0
syncTime.value = 0
tasks.value.forEach(t => t.status = 'pending')
if (timer) clearInterval(timer)
}
async function sleep(ms) {
return new Promise(r => setTimeout(r, Math.min(ms, 1500)))
}
async function startProcess() {
reset()
running.value = true
if (mode.value === 'sync') {
timer = setInterval(() => { elapsed.value = (elapsed.value + 0.1).toFixed(1) }, 100)
let total = 0
for (const task of tasks.value) {
task.status = 'running'
await sleep(task.time)
task.status = 'done'
total += task.time
}
syncTime.value = total
responseReady.value = true
running.value = false
clearInterval(timer)
} else {
// 异步模式:只等核心任务
tasks.value[0].status = 'running'
await sleep(tasks.value[0].time)
tasks.value[0].status = 'done'
tasks.value[1].status = 'running'
await sleep(tasks.value[1].time)
tasks.value[1].status = 'done'
responseReady.value = true
// 后台任务继续
for (let i = 2; i < tasks.value.length; i++) {
tasks.value[i].status = 'running'
await sleep(tasks.value[i].time)
tasks.value[i].status = 'done'
}
running.value = false
}
}
</script>
<style scoped>
.async-task-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header { margin-bottom: 1rem; }
.title { font-weight: 700; font-size: 1.1rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.mode-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.tab {
padding: 0.5rem 1rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); cursor: pointer; font-size: 0.9rem;
}
.tab.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
.flow-area { display: flex; align-items: flex-start; gap: 1rem; margin-bottom: 1rem; }
.arrow { font-size: 2rem; color: var(--vp-c-text-3); padding-top: 2rem; }
.user-side, .server-side { flex: 1; }
.label { font-weight: 600; margin-bottom: 0.5rem; font-size: 0.9rem; }
.action-btn {
padding: 0.5rem 1.5rem; border-radius: 6px; border: none;
background: var(--vp-c-brand); color: #fff; cursor: pointer; font-size: 0.9rem;
margin-bottom: 0.75rem; width: 100%;
}
.action-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.response-box {
padding: 0.75rem; border-radius: 8px; background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider); font-size: 0.85rem; text-align: center;
}
.response-box.success { border-color: #22c55e; background: rgba(34,197,94,0.05); }
.tasks { display: flex; flex-direction: column; gap: 0.5rem; }
.task-item {
display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem;
border-radius: 6px; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
font-size: 0.85rem;
}
.task-item.running { border-color: #f59e0b; background: rgba(245,158,11,0.05); }
.task-item.done { border-color: #22c55e; background: rgba(34,197,94,0.05); }
.task-icon { font-size: 0.9rem; }
.task-time { margin-left: auto; color: var(--vp-c-text-3); font-family: var(--vp-font-family-mono); font-size: 0.8rem; }
.summary { margin-top: 0.75rem; padding: 0.75rem; border-radius: 8px; font-size: 0.9rem; }
.summary-bad { background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.3); border-radius: 8px; padding: 0.75rem; }
.summary-good { background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.3); border-radius: 8px; padding: 0.75rem; }
@media (max-width: 640px) {
.flow-area { flex-direction: column; }
.arrow { transform: rotate(90deg); align-self: center; padding: 0; }
}
</style>
@@ -0,0 +1,168 @@
<!--
TaskRetryDemo.vue
任务重试机制演示展示失败重试和退避策略
-->
<template>
<div class="retry-demo">
<div class="header">
<div class="title">任务重试与退避策略</div>
<div class="subtitle">模拟任务失败后的重试过程</div>
</div>
<div class="strategy-tabs">
<button
v-for="s in strategies"
:key="s.key"
:class="['tab', { active: strategy === s.key }]"
@click="strategy = s.key; reset()"
>{{ s.label }}</button>
</div>
<div class="retry-area">
<button class="start-btn" @click="startRetry" :disabled="running">
{{ running ? '重试中...' : '执行任务(模拟失败)' }}
</button>
<div class="attempts">
<div
v-for="(attempt, i) in attempts"
:key="i"
:class="['attempt', attempt.status]"
>
<div class="attempt-header">
<span class="attempt-num"> {{ i + 1 }} {{ i === 0 ? '执行' : '重试' }}</span>
<span :class="['status-badge', attempt.status]">
{{ attempt.status === 'success' ? '成功' : attempt.status === 'fail' ? '失败' : attempt.status === 'waiting' ? '等待中' : '执行中' }}
</span>
</div>
<div class="attempt-detail">
<span v-if="attempt.delay > 0">等待 {{ attempt.delay }}s 后重试</span>
<span v-if="attempt.error" class="error-msg">{{ attempt.error }}</span>
</div>
</div>
</div>
</div>
<div class="strategy-info">
<div class="info-title">{{ currentStrategy.label }}</div>
<div class="info-desc">{{ currentStrategy.desc }}</div>
<div class="info-formula">
延迟公式<code>{{ currentStrategy.formula }}</code>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const strategy = ref('fixed')
const running = ref(false)
const attempts = ref([])
const strategies = [
{ key: 'fixed', label: '固定间隔', desc: '每次重试等待相同的时间,简单但可能造成"重试风暴"', formula: 'delay = 2s' },
{ key: 'exponential', label: '指数退避', desc: '每次重试等待时间翻倍,有效避免服务端过载', formula: 'delay = 2^n 秒 (1s, 2s, 4s, 8s...)' },
{ key: 'jitter', label: '指数退避+抖动', desc: '在指数退避基础上加随机偏移,防止多个客户端同时重试', formula: 'delay = 2^n + random(0, 1s)' }
]
const currentStrategy = computed(() => strategies.find(s => s.key === strategy.value))
function reset() {
running.value = false
attempts.value = []
}
function getDelay(n) {
if (strategy.value === 'fixed') return 2
if (strategy.value === 'exponential') return Math.pow(2, n)
return Math.pow(2, n) + Math.random().toFixed(1) * 1
}
async function sleep(ms) {
return new Promise(r => setTimeout(r, ms))
}
async function startRetry() {
reset()
running.value = true
const maxRetries = 4
const failUntil = 2 + Math.floor(Math.random() * 2) // succeed on 3rd or 4th attempt
for (let i = 0; i <= maxRetries; i++) {
const delay = i === 0 ? 0 : getDelay(i - 1)
const attempt = { status: 'waiting', delay, error: '' }
attempts.value.push(attempt)
if (delay > 0) {
await sleep(Math.min(delay * 500, 2000))
}
attempt.status = 'running'
await sleep(500)
if (i < failUntil) {
attempt.status = 'fail'
attempt.error = ['连接超时', '服务不可用', '网络错误'][i % 3]
} else {
attempt.status = 'success'
running.value = false
return
}
}
running.value = false
}
</script>
<style scoped>
.retry-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header { margin-bottom: 1rem; }
.title { font-weight: 700; font-size: 1.1rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.strategy-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.tab {
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
}
.tab.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.start-btn {
padding: 0.5rem 1.5rem; border-radius: 6px; border: none;
background: var(--vp-c-brand); color: #fff; cursor: pointer; font-size: 0.9rem;
margin-bottom: 1rem;
}
.start-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.attempts { display: flex; flex-direction: column; gap: 0.5rem; }
.attempt {
padding: 0.6rem 0.75rem; border-radius: 8px; background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
}
.attempt.fail { border-color: rgba(239,68,68,0.4); }
.attempt.success { border-color: #22c55e; background: rgba(34,197,94,0.05); }
.attempt.running { border-color: var(--vp-c-brand); }
.attempt-header { display: flex; justify-content: space-between; align-items: center; }
.attempt-num { font-weight: 600; font-size: 0.85rem; }
.status-badge { font-size: 0.75rem; padding: 0.15rem 0.5rem; border-radius: 4px; }
.status-badge.fail { background: rgba(239,68,68,0.1); color: #ef4444; }
.status-badge.success { background: rgba(34,197,94,0.1); color: #22c55e; }
.status-badge.running { background: rgba(var(--vp-c-brand-rgb),0.1); color: var(--vp-c-brand); }
.status-badge.waiting { background: var(--vp-c-bg-soft); color: var(--vp-c-text-3); }
.attempt-detail { font-size: 0.8rem; color: var(--vp-c-text-2); margin-top: 0.25rem; }
.error-msg { color: #ef4444; margin-left: 0.5rem; }
.strategy-info {
margin-top: 1rem; padding: 0.75rem; border-radius: 8px;
background: rgba(var(--vp-c-brand-rgb),0.05); border: 1px solid var(--vp-c-brand);
}
.info-title { font-weight: 700; font-size: 0.9rem; margin-bottom: 0.25rem; }
.info-desc { font-size: 0.85rem; color: var(--vp-c-text-2); margin-bottom: 0.5rem; }
.info-formula { font-size: 0.85rem; }
.info-formula code {
padding: 0.15rem 0.4rem; background: var(--vp-c-bg); border-radius: 4px;
font-family: var(--vp-font-family-mono); font-size: 0.8rem;
}
</style>
@@ -0,0 +1,190 @@
<!--
TaskWorkerDemo.vue
Worker 工作池演示展示任务分发和消费过程
-->
<template>
<div class="worker-demo">
<div class="header">
<div class="title">Worker 工作池模型</div>
<div class="subtitle">观察任务如何被分发到不同 Worker 处理</div>
</div>
<div class="controls">
<button class="ctrl-btn" @click="addTask" :disabled="running">添加任务</button>
<button class="ctrl-btn primary" @click="startProcessing" :disabled="running || queue.length === 0">开始处理</button>
<button class="ctrl-btn" @click="resetAll">重置</button>
<div class="worker-count">
Worker 数量
<button class="small-btn" @click="workerCount = Math.max(1, workerCount - 1)" :disabled="running">-</button>
<span>{{ workerCount }}</span>
<button class="small-btn" @click="workerCount = Math.min(5, workerCount + 1)" :disabled="running">+</button>
</div>
</div>
<div class="pool-layout">
<div class="queue-section">
<div class="section-title">任务队列 ({{ queue.length }})</div>
<div class="queue-list">
<div v-for="task in queue" :key="task.id" class="queue-item">
{{ task.name }}
</div>
<div v-if="queue.length === 0" class="empty">队列为空</div>
</div>
</div>
<div class="arrow-section"></div>
<div class="workers-section">
<div class="section-title">Workers</div>
<div class="workers-grid">
<div v-for="w in workers" :key="w.id" :class="['worker-card', w.status]">
<div class="worker-name">Worker {{ w.id }}</div>
<div class="worker-status">
<template v-if="w.status === 'idle'">💤 空闲</template>
<template v-else> {{ w.currentTask }}</template>
</div>
<div class="worker-count-label">已完成: {{ w.completed }}</div>
</div>
</div>
</div>
<div class="arrow-section"></div>
<div class="done-section">
<div class="section-title">已完成 ({{ doneList.length }})</div>
<div class="done-list">
<div v-for="task in doneList" :key="task.id" class="done-item">
{{ task.name }}
</div>
<div v-if="doneList.length === 0" class="empty">暂无</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const workerCount = ref(3)
const running = ref(false)
let taskId = 0
const taskTypes = ['发送邮件', '生成报表', '图片压缩', '数据同步', '推送通知', '日志归档', 'PDF 导出', '缓存预热']
const queue = ref([])
const doneList = ref([])
const workers = computed(() => {
const arr = []
for (let i = 1; i <= workerCount.value; i++) {
arr.push(workerState.value[i] || { id: i, status: 'idle', currentTask: '', completed: 0 })
}
return arr
})
const workerState = ref({})
function addTask() {
const name = taskTypes[taskId % taskTypes.length]
queue.value.push({ id: ++taskId, name: `${name} #${taskId}` })
}
function resetAll() {
running.value = false
queue.value = []
doneList.value = []
workerState.value = {}
taskId = 0
}
async function sleep(ms) {
return new Promise(r => setTimeout(r, ms))
}
async function startProcessing() {
running.value = true
// Initialize worker states
for (let i = 1; i <= workerCount.value; i++) {
workerState.value[i] = { id: i, status: 'idle', currentTask: '', completed: 0 }
}
const workerPromises = []
for (let i = 1; i <= workerCount.value; i++) {
workerPromises.push(runWorker(i))
}
await Promise.all(workerPromises)
running.value = false
}
async function runWorker(wid) {
while (queue.value.length > 0) {
const task = queue.value.shift()
if (!task) break
workerState.value = {
...workerState.value,
[wid]: { ...workerState.value[wid], status: 'busy', currentTask: task.name }
}
await sleep(600 + Math.random() * 800)
doneList.value.push(task)
workerState.value = {
...workerState.value,
[wid]: { ...workerState.value[wid], status: 'idle', currentTask: '', completed: workerState.value[wid].completed + 1 }
}
}
}
</script>
<style scoped>
.worker-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header { margin-bottom: 1rem; }
.title { font-weight: 700; font-size: 1.1rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.controls { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin-bottom: 1rem; }
.ctrl-btn {
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
}
.ctrl-btn.primary { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
.ctrl-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.small-btn {
width: 24px; height: 24px; border-radius: 4px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
}
.small-btn:disabled { opacity: 0.5; }
.worker-count { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; margin-left: auto; }
.pool-layout { display: flex; gap: 0.75rem; align-items: flex-start; }
.arrow-section { font-size: 1.5rem; color: var(--vp-c-text-3); padding-top: 2rem; flex-shrink: 0; }
.queue-section, .done-section { flex: 1; min-width: 0; }
.workers-section { flex: 1.5; min-width: 0; }
.section-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 0.5rem; }
.queue-list, .done-list { display: flex; flex-direction: column; gap: 0.25rem; max-height: 200px; overflow-y: auto; }
.queue-item {
padding: 0.4rem 0.6rem; background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.3);
border-radius: 4px; font-size: 0.8rem;
}
.done-item {
padding: 0.4rem 0.6rem; background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.2);
border-radius: 4px; font-size: 0.8rem;
}
.empty { color: var(--vp-c-text-3); font-size: 0.8rem; padding: 0.5rem; }
.workers-grid { display: flex; flex-direction: column; gap: 0.5rem; }
.worker-card {
padding: 0.5rem 0.75rem; border-radius: 8px; background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
}
.worker-card.busy { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
.worker-name { font-weight: 600; font-size: 0.85rem; }
.worker-status { font-size: 0.8rem; color: var(--vp-c-text-2); margin: 0.25rem 0; }
.worker-count-label { font-size: 0.75rem; color: var(--vp-c-text-3); }
@media (max-width: 640px) {
.pool-layout { flex-direction: column; }
.arrow-section { transform: rotate(90deg); align-self: center; padding: 0; }
}
</style>
@@ -0,0 +1,337 @@
<template>
<div class="launch-demo">
<div class="demo-header">
<span class="demo-icon">🌐</span>
<span class="demo-title">浏览器启动过程</span>
<span class="demo-hint">点击每一步查看详情</span>
</div>
<div class="timeline">
<div
v-for="(step, i) in steps"
:key="i"
class="timeline-item"
:class="{ active: active === i, done: active > i }"
@click="active = active === i ? -1 : i"
>
<div class="marker-col">
<div class="dot">
<span v-if="active > i" class="check"></span>
<span v-else>{{ i + 1 }}</span>
</div>
<div v-if="i < steps.length - 1" class="line"></div>
</div>
<div class="card">
<div class="card-header">
<span class="step-icon">{{ step.icon }}</span>
<div class="card-titles">
<div class="step-name">{{ step.name }}</div>
<div class="step-brief">{{ step.brief }}</div>
</div>
<span class="expand-icon">{{ active === i ? '▾' : '▸' }}</span>
</div>
<transition name="slide">
<div v-if="active === i" class="card-detail">
<div class="detail-desc">{{ step.detail }}</div>
<div class="detail-visual">
<div
v-for="(item, j) in step.items"
:key="j"
class="visual-item"
>
<span class="vi-icon">{{ item.icon }}</span>
<div class="vi-text">
<span class="vi-label">{{ item.label }}</span>
<span class="vi-desc">{{ item.desc }}</span>
</div>
</div>
</div>
<div v-if="step.analogy" class="analogy">
<span class="analogy-icon">💡</span>
<span class="analogy-text">{{ step.analogy }}</span>
</div>
</div>
</transition>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const active = ref(-1)
const steps = [
{
icon: '👆',
name: '双击图标',
brief: '用户触发启动请求,操作系统开始响应',
detail: '你双击桌面上的浏览器图标时,操作系统的窗口管理器捕获这个鼠标事件,通过文件关联表查找该图标对应的可执行文件路径。',
items: [
{ icon: '🖱️', label: '鼠标事件捕获', desc: '窗口管理器检测到双击动作,识别点击目标' },
{ icon: '🔗', label: '快捷方式解析', desc: '读取 .lnkWindows)或 .desktopLinux)文件中的目标路径' },
{ icon: '📂', label: '文件关联查找', desc: '在注册表或 MIME 数据库中找到对应的可执行文件' }
],
analogy: '就像你按下遥控器的开机键,电视先要识别你按的是哪个按钮,再决定执行什么操作。'
},
{
icon: '🔍',
name: '查找可执行文件',
brief: '根据文件关联,找到浏览器的 .exe 或可执行文件',
detail: '操作系统根据路径在硬盘上定位浏览器的可执行文件(如 chrome.exe),验证文件完整性和权限,准备加载。',
items: [
{ icon: '📋', label: '路径解析', desc: '将快捷方式中的路径转换为硬盘上的实际文件位置' },
{ icon: '🔒', label: '权限检查', desc: '验证当前用户是否有执行该文件的权限' },
{ icon: '✅', label: '签名验证', desc: '检查数字签名确认文件未被篡改(Windows UAC' }
],
analogy: '好比你要找一本书,先查图书馆目录(路径),确认你有借阅权限(权限检查),再确认书没有被损坏(签名验证)。'
},
{
icon: '📋',
name: '创建浏览器进程',
brief: '为浏览器创建一个新的进程,分配进程 ID',
detail: '操作系统内核调用 fork()+exec()Linux)或 CreateProcess()Windows),在进程表中创建新条目,分配唯一的 PID,建立进程控制块(PCB)。',
items: [
{ icon: '🆔', label: '分配 PID', desc: '为新进程分配唯一的进程标识符' },
{ icon: '📊', label: '创建 PCB', desc: '记录进程状态、优先级、寄存器上下文等元信息' },
{ icon: '🧠', label: '分配虚拟地址空间', desc: '为进程创建独立的 4GB(32位)虚拟内存空间' },
{ icon: '📑', label: '初始化文件描述符', desc: '打开 stdin/stdout/stderr 三个标准 I/O 通道' }
],
analogy: '就像新生儿出生要办户口——分配身份证号(PID)、建立档案(PCB)、分配住房(内存空间)。'
},
{
icon: '💾',
name: '加载代码到内存',
brief: '把浏览器的程序代码从硬盘读取到内存中',
detail: '操作系统的加载器(Loader)解析可执行文件格式(PE/ELF),将代码段、数据段映射到虚拟内存,并加载所需的动态链接库(DLL/SO)。',
items: [
{ icon: '📦', label: '解析文件格式', desc: '读取 PEWindows)或 ELFLinux)文件头,确定各段位置' },
{ icon: '🗺️', label: '内存映射', desc: '将 .text(代码)、.data(数据)、.bss 段映射到虚拟地址' },
{ icon: '🔗', label: '动态链接', desc: '加载 DLL/SO 共享库,解析函数符号引用' },
{ icon: '📍', label: '重定位', desc: '修正代码中的绝对地址引用,适配实际加载位置' }
],
analogy: '好比搬家——把家具(代码)从仓库(硬盘)搬到新房(内存),还要接通水电(链接库)。'
},
{
icon: '🚀',
name: '初始化各模块',
brief: '启动主线程、渲染引擎、网络引擎、JS 引擎等',
detail: '浏览器的 main() 函数开始执行,依次初始化多进程架构中的各个核心模块:Browser 主进程、GPU 进程、网络进程等。',
items: [
{ icon: '🧵', label: '主线程启动', desc: '初始化消息循环(Event Loop),处理 UI 事件和任务调度' },
{ icon: '🎨', label: '渲染引擎', desc: '初始化 Blink/Gecko 引擎,准备解析 HTML/CSS' },
{ icon: '🌐', label: '网络模块', desc: '启动网络栈,初始化 DNS 缓存、连接池、Cookie 管理' },
{ icon: '⚡', label: 'JS 引擎', desc: '初始化 V8/SpiderMonkey,编译内置 JavaScript 代码' }
],
analogy: '就像一家餐厅开业前——厨房(渲染)、前台(UI)、外卖(网络)、收银(JS)各部门同时准备就绪。'
},
{
icon: '🖼️',
name: '显示浏览器窗口',
brief: '所有模块就绪,浏览器界面呈现在屏幕上',
detail: '浏览器向操作系统请求创建窗口,GPU 进程完成界面的合成与光栅化,最终将像素数据提交给显卡,浏览器窗口出现在屏幕上。',
items: [
{ icon: '🪟', label: '创建窗口', desc: '调用系统 API 创建原生窗口,设置大小和位置' },
{ icon: '🎨', label: 'UI 绘制', desc: '渲染地址栏、标签页、工具栏等浏览器 Chrome 界面' },
{ icon: '🖥️', label: 'GPU 合成', desc: '将各图层合成为最终画面,提交给显卡输出' },
{ icon: '✨', label: '加载首页', desc: '打开新标签页或恢复上次会话,浏览器进入可用状态' }
],
analogy: '幕布拉开,灯光亮起——舞台(窗口)搭好了,演员(界面元素)就位,等待观众(你)的第一次操作。'
}
]
</script>
<style scoped>
.launch-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
background: var(--vp-c-bg-soft);
padding: 1.2rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.demo-icon { font-size: 1.2rem; }
.demo-title {
font-size: 0.85rem;
font-weight: 700;
color: var(--vp-c-text-1);
}
.demo-hint {
font-size: 0.65rem;
color: var(--vp-c-text-3);
margin-left: auto;
}
.timeline { display: flex; flex-direction: column; }
.timeline-item {
display: flex;
gap: 0.8rem;
cursor: pointer;
}
.marker-col {
display: flex;
flex-direction: column;
align-items: center;
width: 2rem;
flex-shrink: 0;
}
.dot {
width: 1.8rem;
height: 1.8rem;
border-radius: 50%;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
color: var(--vp-c-text-3);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 700;
transition: all 0.3s;
}
.timeline-item.active .dot {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
transform: scale(1.15);
box-shadow: 0 0 0 4px var(--vp-c-brand-soft);
}
.timeline-item.done .dot {
background: #10b981;
border-color: #10b981;
color: white;
}
.check { font-size: 0.65rem; }
.line {
flex: 1;
width: 2px;
background: var(--vp-c-divider);
min-height: 0.8rem;
transition: background 0.3s;
}
.timeline-item.done .line { background: #10b981; opacity: 0.5; }
.timeline-item.active .line { background: var(--vp-c-brand); opacity: 0.4; }
.card {
flex: 1;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.7rem 0.9rem;
margin-bottom: 0.5rem;
transition: all 0.25s;
}
.timeline-item.active .card {
border-color: var(--vp-c-brand);
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.step-icon { font-size: 1.2rem; }
.card-titles { flex: 1; }
.step-name {
font-size: 0.78rem;
font-weight: 700;
color: var(--vp-c-text-1);
}
.step-brief {
font-size: 0.68rem;
color: var(--vp-c-text-3);
margin-top: 0.1rem;
line-height: 1.4;
}
.expand-icon {
font-size: 0.7rem;
color: var(--vp-c-text-3);
transition: transform 0.2s;
}
.card-detail {
margin-top: 0.7rem;
padding-top: 0.7rem;
border-top: 1px dashed var(--vp-c-divider);
}
.detail-desc {
font-size: 0.72rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-bottom: 0.6rem;
}
.detail-visual {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.4rem;
}
.visual-item {
display: flex;
align-items: flex-start;
gap: 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 0.45rem 0.55rem;
}
.vi-icon { font-size: 0.9rem; flex-shrink: 0; margin-top: 0.05rem; }
.vi-text { display: flex; flex-direction: column; }
.vi-label {
font-size: 0.68rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.vi-desc {
font-size: 0.62rem;
color: var(--vp-c-text-3);
line-height: 1.4;
margin-top: 0.1rem;
}
.analogy {
display: flex;
align-items: flex-start;
gap: 0.4rem;
margin-top: 0.6rem;
padding: 0.5rem 0.6rem;
background: var(--vp-c-brand-soft);
border-radius: 6px;
}
.analogy-icon { font-size: 0.85rem; flex-shrink: 0; }
.analogy-text {
font-size: 0.66rem;
color: var(--vp-c-text-2);
line-height: 1.5;
font-style: italic;
}
.slide-enter-active, .slide-leave-active {
transition: all 0.3s ease;
overflow: hidden;
}
.slide-enter-from, .slide-leave-to {
opacity: 0;
max-height: 0;
margin-top: 0;
padding-top: 0;
}
.slide-enter-to, .slide-leave-from {
opacity: 1;
max-height: 30rem;
}
@media (max-width: 640px) {
.detail-visual { grid-template-columns: 1fr; }
.demo-hint { display: none; }
}
</style>
@@ -0,0 +1,376 @@
<template>
<div class="bios-demo">
<div class="demo-header">
<span class="demo-icon">📟</span>
<span class="demo-title">BIOS/UEFI 工作流程</span>
<span class="demo-hint">点击每一步查看详情</span>
</div>
<div class="timeline">
<div
v-for="(step, i) in steps"
:key="i"
class="timeline-item"
:class="{ active: active === i, done: active > i }"
@click="active = active === i ? -1 : i"
>
<div class="marker-col">
<div class="dot">
<span v-if="active > i" class="check"></span>
<span v-else>{{ i + 1 }}</span>
</div>
<div v-if="i < steps.length - 1" class="line"></div>
</div>
<div class="card">
<div class="card-header">
<span class="step-icon">{{ step.icon }}</span>
<div class="card-titles">
<div class="step-name">{{ step.name }}</div>
<div class="step-brief">{{ step.brief }}</div>
</div>
<span class="expand-icon">{{ active === i ? '▾' : '▸' }}</span>
</div>
<transition name="slide">
<div v-if="active === i" class="card-detail">
<div class="detail-desc">{{ step.detail }}</div>
<div class="detail-visual">
<div
v-for="(item, j) in step.items"
:key="j"
class="visual-item"
:class="{ 'error-item': item.error }"
>
<span class="vi-icon">{{ item.icon }}</span>
<div class="vi-text">
<span class="vi-label">{{ item.label }}</span>
<span class="vi-desc">{{ item.desc }}</span>
</div>
</div>
</div>
<div v-if="step.analogy" class="analogy">
<span class="analogy-icon">💡</span>
<span class="analogy-text">{{ step.analogy }}</span>
</div>
</div>
</transition>
</div>
</div>
</div>
<div class="beep-note">
<span class="beep-icon">🔔</span>
<div class="beep-content">
<div class="beep-title">蜂鸣声错误码</div>
<div class="beep-desc">如果 POST 发现问题主板会发出蜂鸣声不同次数代表不同错误</div>
<div class="beep-codes">
<div v-for="code in beepCodes" :key="code.beeps" class="beep-code">
<span class="beep-count">{{ code.beeps }}</span>
<span class="beep-meaning">{{ code.meaning }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const active = ref(-1)
const steps = [
{
icon: '🔍',
name: '硬件自检(POST',
brief: '检查内存、显卡、键盘等部件是否正常',
detail: 'Power-On Self-Test 是开机后执行的第一段程序。BIOS/UEFI 固件逐一检测关键硬件,确保它们能正常工作,任何故障都会在这一步被发现。',
items: [
{ icon: '🧠', label: '内存检测', desc: '向内存写入测试数据并读回验证,确认每个内存条工作正常' },
{ icon: '🎮', label: '显卡检测', desc: '初始化显卡,尝试输出画面;如果失败,屏幕会保持黑屏' },
{ icon: '⌨️', label: '键盘/鼠标检测', desc: '扫描 PS/2 或 USB 端口,检测输入设备是否连接并响应' },
{ icon: '💾', label: '存储设备检测', desc: '识别硬盘、SSD、光驱等存储设备,读取设备信息' },
{ icon: '❌', label: '错误报告', desc: '检测失败时通过蜂鸣声或屏幕错误码告知用户具体问题', error: true }
],
analogy: '就像飞机起飞前的安全检查——机长逐项确认引擎、仪表、燃油都正常,有任何问题就不能起飞。'
},
{
icon: '⚙️',
name: '初始化硬件',
brief: '设置硬件工作模式,配置中断向量表',
detail: '自检通过后,BIOS/UEFI 开始配置各硬件的工作参数:设置 CPU 频率、内存时序、配置中断控制器,建立硬件与软件之间的通信桥梁。',
items: [
{ icon: '🔧', label: '设置工作模式', desc: '配置 CPU 运行频率、内存时序(CAS Latency)等参数' },
{ icon: '📋', label: '中断向量表', desc: '建立中断号与处理程序的映射表,让硬件事件能被正确响应' },
{ icon: '🔌', label: 'PCI 设备枚举', desc: '扫描 PCI/PCIe 总线,为显卡、网卡、声卡分配资源' },
{ icon: '🕐', label: '时钟初始化', desc: '读取 CMOS 中的实时时钟(RTC),同步系统时间' }
],
analogy: '好比乐队演出前的调音——每件乐器(硬件)都要调到正确的音高(工作模式),指挥(中断控制器)要能指挥每个声部。'
},
{
icon: '🔎',
name: '寻找启动设备',
brief: '按启动顺序查找可启动设备,读取启动扇区',
detail: 'BIOS/UEFI 按照用户设定的启动顺序(Boot Order),依次检查硬盘、U 盘、网络等设备,找到第一个包含有效引导记录的设备,读取其启动扇区并将控制权交出。',
items: [
{ icon: '📑', label: '读取启动顺序', desc: '从 CMOS/NVRAM 中读取用户设定的设备优先级列表' },
{ icon: '💿', label: '检查启动扇区', desc: '读取设备第一个扇区,验证末尾的 0x55AA 魔数签名' },
{ icon: '🔀', label: '多设备尝试', desc: '第一个设备无法启动时,自动尝试下一个(硬盘→U盘→网络)' },
{ icon: '🚀', label: '跳转执行', desc: '将启动扇区代码加载到内存 0x7C00,CPU 跳转到该地址执行' }
],
analogy: '就像你早上出门找交通工具——先看车库有没有车(硬盘),没有就看门口有没有共享单车(U盘),再不行就叫网约车(网络启动)。'
}
]
const beepCodes = [
{ beeps: '1 短', meaning: '正常启动,一切 OK' },
{ beeps: '1 长 2 短', meaning: '显卡错误或未插好' },
{ beeps: '1 长 3 短', meaning: '内存错误或未插好' },
{ beeps: '持续长鸣', meaning: '内存未检测到' },
{ beeps: '持续短鸣', meaning: '电源供电异常' }
]
</script>
<style scoped>
.bios-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
background: var(--vp-c-bg-soft);
padding: 1.2rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.demo-icon { font-size: 1.2rem; }
.demo-title {
font-size: 0.85rem;
font-weight: 700;
color: var(--vp-c-text-1);
}
.demo-hint {
font-size: 0.65rem;
color: var(--vp-c-text-3);
margin-left: auto;
}
.timeline { display: flex; flex-direction: column; }
.timeline-item {
display: flex;
gap: 0.8rem;
cursor: pointer;
}
.marker-col {
display: flex;
flex-direction: column;
align-items: center;
width: 2rem;
flex-shrink: 0;
}
.dot {
width: 1.8rem;
height: 1.8rem;
border-radius: 50%;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
color: var(--vp-c-text-3);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 700;
transition: all 0.3s;
}
.timeline-item.active .dot {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
transform: scale(1.15);
box-shadow: 0 0 0 4px var(--vp-c-brand-soft);
}
.timeline-item.done .dot {
background: #10b981;
border-color: #10b981;
color: white;
}
.check { font-size: 0.65rem; }
.line {
flex: 1;
width: 2px;
background: var(--vp-c-divider);
min-height: 0.8rem;
transition: background 0.3s;
}
.timeline-item.done .line { background: #10b981; opacity: 0.5; }
.timeline-item.active .line { background: var(--vp-c-brand); opacity: 0.4; }
.card {
flex: 1;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.7rem 0.9rem;
margin-bottom: 0.5rem;
transition: all 0.25s;
}
.timeline-item.active .card {
border-color: var(--vp-c-brand);
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.step-icon { font-size: 1.2rem; }
.card-titles { flex: 1; }
.step-name {
font-size: 0.78rem;
font-weight: 700;
color: var(--vp-c-text-1);
}
.step-brief {
font-size: 0.68rem;
color: var(--vp-c-text-3);
margin-top: 0.1rem;
line-height: 1.4;
}
.expand-icon {
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.card-detail {
margin-top: 0.7rem;
padding-top: 0.7rem;
border-top: 1px dashed var(--vp-c-divider);
}
.detail-desc {
font-size: 0.72rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-bottom: 0.6rem;
}
.detail-visual {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.4rem;
}
.visual-item {
display: flex;
align-items: flex-start;
gap: 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 0.45rem 0.55rem;
}
.visual-item.error-item {
background: rgba(239, 68, 68, 0.08);
border: 1px solid rgba(239, 68, 68, 0.15);
}
.vi-icon { font-size: 0.9rem; flex-shrink: 0; margin-top: 0.05rem; }
.vi-text { display: flex; flex-direction: column; }
.vi-label {
font-size: 0.68rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.vi-desc {
font-size: 0.62rem;
color: var(--vp-c-text-3);
line-height: 1.4;
margin-top: 0.1rem;
}
.analogy {
display: flex;
align-items: flex-start;
gap: 0.4rem;
margin-top: 0.6rem;
padding: 0.5rem 0.6rem;
background: var(--vp-c-brand-soft);
border-radius: 6px;
}
.analogy-icon { font-size: 0.85rem; flex-shrink: 0; }
.analogy-text {
font-size: 0.66rem;
color: var(--vp-c-text-2);
line-height: 1.5;
font-style: italic;
}
.beep-note {
display: flex;
gap: 0.6rem;
margin-top: 0.8rem;
padding: 0.7rem 0.8rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
border-left: 3px solid #f59e0b;
}
.beep-icon { font-size: 1.1rem; flex-shrink: 0; }
.beep-content { flex: 1; }
.beep-title {
font-size: 0.75rem;
font-weight: 700;
color: var(--vp-c-text-1);
margin-bottom: 0.2rem;
}
.beep-desc {
font-size: 0.66rem;
color: var(--vp-c-text-3);
margin-bottom: 0.5rem;
}
.beep-codes {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.beep-code {
display: flex;
align-items: center;
gap: 0.35rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
padding: 0.25rem 0.5rem;
}
.beep-count {
font-size: 0.62rem;
font-weight: 700;
color: #f59e0b;
white-space: nowrap;
}
.beep-meaning {
font-size: 0.62rem;
color: var(--vp-c-text-2);
white-space: nowrap;
}
.slide-enter-active, .slide-leave-active {
transition: all 0.3s ease;
overflow: hidden;
}
.slide-enter-from, .slide-leave-to {
opacity: 0;
max-height: 0;
margin-top: 0;
padding-top: 0;
}
.slide-enter-to, .slide-leave-from {
opacity: 1;
max-height: 30rem;
}
@media (max-width: 640px) {
.detail-visual { grid-template-columns: 1fr; }
.demo-hint { display: none; }
.beep-codes { flex-direction: column; }
}
</style>
@@ -0,0 +1,599 @@
<template>
<div class="bios-demo">
<div class="demo-header">
<span class="demo-title">BIOS/UEFI 工作流程</span>
</div>
<div class="main-layout">
<!-- 左侧模拟屏幕 -->
<div class="screen-panel">
<div class="monitor">
<div class="monitor-bezel">
<div class="screen" :class="'stage-' + stage">
<!-- Stage 0: 介绍 -->
<div v-if="stage === 0" class="screen-intro">
<div class="intro-icon">📟</div>
<div class="intro-title">BIOS/UEFI</div>
<div class="intro-desc">点击开始了解<br>固件启动流程</div>
</div>
<!-- Stage 1: POST 自检 -->
<div v-if="stage === 1" class="screen-post">
<div class="post-header">POST - Power On Self Test</div>
<div class="post-list">
<div v-for="(item, i) in postItems" :key="i" class="post-item" :class="{ checking: currentCheck === i, done: currentCheck > i }">
<span class="post-icon">{{ currentCheck > i ? '✓' : (currentCheck === i ? '◐' : '○') }}</span>
<span class="post-name">{{ item.name }}</span>
</div>
</div>
<div v-if="currentCheck >= postItems.length" class="post-result">
<span class="result-ok"> 所有硬件检测通过</span>
</div>
</div>
<!-- Stage 2: 初始化硬件 -->
<div v-if="stage === 2" class="screen-init">
<div class="init-header">初始化硬件配置</div>
<div class="init-visual">
<div class="hardware-grid">
<div v-for="(hw, i) in hardwareItems" :key="i" class="hw-item" :class="{ active: activeHw === i }">
<span class="hw-icon">{{ hw.icon }}</span>
<span class="hw-name">{{ hw.name }}</span>
</div>
</div>
<div class="init-progress">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: hwProgress + '%' }"></div>
</div>
<div class="progress-text">{{ hwProgress }}%</div>
</div>
</div>
</div>
<!-- Stage 3: 寻找启动设备 -->
<div v-if="stage === 3" class="screen-boot">
<div class="boot-header">寻找启动设备</div>
<div class="boot-order">
<div class="order-label">启动顺序</div>
<div class="device-list">
<div v-for="(dev, i) in bootDevices" :key="i" class="device-item" :class="{ checking: currentDevice === i, found: foundDevice === i, skipped: foundDevice > i || (foundDevice === -1 && currentDevice > i) }">
<span class="device-num">{{ i + 1 }}</span>
<span class="device-icon">{{ dev.icon }}</span>
<span class="device-name">{{ dev.name }}</span>
<span class="device-status">{{ getDeviceStatus(i) }}</span>
</div>
</div>
</div>
<div v-if="foundDevice >= 0" class="boot-result">
<span class="boot-ok">🚀 {{ bootDevices[foundDevice].name }} 启动</span>
</div>
</div>
</div>
</div>
</div>
<!-- 进度指示 -->
<div class="stage-dots">
<div
v-for="(s, i) in stages"
:key="i"
class="stage-dot"
:class="{ active: stage === i, done: stage > i }"
>
<span class="dot-label">{{ s.short }}</span>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button class="ctrl-btn" :disabled="stage <= 0" @click="prev"> 上一步</button>
<button class="ctrl-btn primary" v-if="stage === 0" @click="next">开始 </button>
<button class="ctrl-btn primary" v-else-if="stage < 3" @click="next">下一步 </button>
<button class="ctrl-btn" v-else @click="reset"> 重新开始</button>
</div>
</div>
<!-- 右侧详细信息 -->
<div class="info-panel">
<div class="info-stage-header">
<span class="info-stage-icon">{{ currentStage.icon }}</span>
<div>
<div class="info-stage-name">{{ currentStage.name }}</div>
<div class="info-stage-desc">{{ currentStage.desc }}</div>
</div>
</div>
<div class="info-operations">
<div
v-for="(op, i) in currentStage.operations"
:key="i"
class="op-card"
:class="{ expanded: expandedOp === i }"
@click="expandedOp = expandedOp === i ? -1 : i"
>
<div class="op-header">
<span class="op-num">{{ i + 1 }}</span>
<span class="op-icon">{{ op.icon }}</span>
<span class="op-name">{{ op.name }}</span>
<span class="op-toggle">{{ expandedOp === i ? '▾' : '▸' }}</span>
</div>
<transition name="expand">
<div v-if="expandedOp === i" class="op-detail">
<div class="op-what">{{ op.what }}</div>
<div v-if="op.details" class="op-details">
<div v-for="(d, j) in op.details" :key="j" class="op-detail-item">
<span class="od-dot"></span>
<span>{{ d }}</span>
</div>
</div>
</div>
</transition>
</div>
</div>
<div v-if="currentStage.analogy" class="info-analogy">
<span class="analogy-icon">💡</span>
<span>{{ currentStage.analogy }}</span>
</div>
<!-- 蜂鸣声错误码 -->
<div v-if="stage === 1" class="beep-codes">
<div class="beep-header">
<span class="beep-icon">🔔</span>
<span class="beep-title">蜂鸣声错误码</span>
</div>
<div class="beep-list">
<div v-for="code in beepCodes" :key="code.beeps" class="beep-item">
<span class="beep-count">{{ code.beeps }}</span>
<span class="beep-meaning">{{ code.meaning }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const stage = ref(0)
const expandedOp = ref(-1)
const currentCheck = ref(0)
const activeHw = ref(0)
const hwProgress = ref(0)
const currentDevice = ref(0)
const foundDevice = ref(-1)
const postItems = [
{ name: '内存检测', icon: '🧠' },
{ name: '显卡检测', icon: '🎮' },
{ name: '键盘/鼠标', icon: '⌨️' },
{ name: '存储设备', icon: '💾' }
]
const hardwareItems = [
{ name: 'CPU', icon: '🧠' },
{ name: '内存', icon: '💾' },
{ name: '显卡', icon: '🎮' },
{ name: '网卡', icon: '🌐' },
{ name: '声卡', icon: '🔊' },
{ name: 'USB', icon: '🔌' }
]
const bootDevices = [
{ name: '硬盘', icon: '💿' },
{ name: 'U盘', icon: '🔌' },
{ name: '网络', icon: '🌐' }
]
const beepCodes = [
{ beeps: '1 短', meaning: '正常启动' },
{ beeps: '1 长 2 短', meaning: '显卡错误' },
{ beeps: '1 长 3 短', meaning: '内存错误' },
{ beeps: '持续长鸣', meaning: '内存未检测' },
{ beeps: '持续短鸣', meaning: '电源异常' }
]
const stages = [
{
short: '介绍',
icon: '📟',
name: '什么是 BIOS/UEFI',
desc: 'BIOS 是电脑启动后第一个运行的程序,存储在主板的只读芯片中。UEFI 是 BIOS 的升级版,更安全、更现代。',
operations: [
{
icon: '💾', name: 'BIOS(传统)',
what: 'Basic Input/Output System1980年代开始使用的固件接口。',
details: ['存储在主板 ROM 芯片中', '16位实模式运行', '最大支持 2.2TB 硬盘', '蓝色文本界面']
},
{
icon: '✨', name: 'UEFI(现代)',
what: 'Unified Extensible Firmware InterfaceBIOS 的现代化替代品。',
details: ['支持 32/64位模式', '支持超过 2.2TB 的大硬盘', '图形化设置界面', '安全启动(Secure Boot']
}
],
analogy: 'BIOS/UEFI 就像是电脑的"守门人"——它第一个醒来,检查一切是否正常,然后决定让谁(操作系统)进来。'
},
{
short: 'POST',
icon: '🔍',
name: '硬件自检(POST',
desc: 'Power-On Self-Test,逐一检测关键硬件,确保它们能正常工作。',
operations: [
{
icon: '🧠', name: '内存检测',
what: '向内存写入测试数据并读回验证,确认每个内存条工作正常。',
details: ['逐字节写入/读取测试', '检测内存容量和速度', '失败会发出蜂鸣声(1长3短)']
},
{
icon: '🎮', name: '显卡检测',
what: '初始化显卡,尝试输出画面。如果失败,屏幕会保持黑屏。',
details: ['加载显卡 BIOS', '设置基本显示模式', '失败蜂鸣:1长2短']
},
{
icon: '⌨️', name: '外设检测',
what: '扫描 USB/PS2 端口,检测键盘、鼠标等输入设备。',
details: ['枚举 USB 设备', '检测键盘响应', '非关键设备,缺失不影响启动']
},
{
icon: '💾', name: '存储设备检测',
what: '识别硬盘、SSD、光驱等存储设备,读取设备信息。',
details: ['检测 SATA/NVMe 设备', '读取设备型号和容量', '为后续启动做准备']
}
],
analogy: '就像飞机起飞前的安全检查——机长逐项确认引擎、仪表、燃油都正常,有任何问题就不能起飞。'
},
{
short: '初始化',
icon: '⚙️',
name: '初始化硬件',
desc: '自检通过后,配置各硬件的工作参数,建立硬件与软件之间的通信桥梁。',
operations: [
{
icon: '🔧', name: '设置工作模式',
what: '配置 CPU 运行频率、内存时序(CAS Latency)等参数。',
details: ['读取 CMOS 中的用户设置', '应用超频配置(如果有)', '设置电源管理模式']
},
{
icon: '📋', name: '中断向量表',
what: '建立中断号与处理程序的映射表,让硬件事件能被正确响应。',
details: ['配置中断控制器(PIC/APIC', '分配 IRQ 中断号', '设置中断处理程序入口']
},
{
icon: '🔌', name: 'PCI 设备枚举',
what: '扫描 PCI/PCIe 总线,为显卡、网卡、声卡分配资源。',
details: ['发现所有 PCI 设备', '分配内存映射 I/O 地址', '分配中断资源']
},
{
icon: '🕐', name: '时钟初始化',
what: '读取 CMOS 中的实时时钟(RTC),同步系统时间。',
details: ['读取硬件时钟', '校验时间有效性', '为操作系统提供初始时间']
}
],
analogy: '好比乐队演出前的调音——每件乐器(硬件)都要调到正确的音高(工作模式),指挥(中断控制器)要能指挥每个声部。'
},
{
short: '启动',
icon: '🔎',
name: '寻找启动设备',
desc: '按照启动顺序查找可启动设备,读取启动扇区,把控制权交给操作系统。',
operations: [
{
icon: '📑', name: '读取启动顺序',
what: '从 CMOS/NVRAM 中读取用户设定的设备优先级列表。',
details: ['硬盘 → U盘 → 网络(默认顺序)', '用户可在 BIOS 设置中修改', '保存到非易失性存储器']
},
{
icon: '💿', name: '检查启动扇区',
what: '读取设备第一个扇区,验证末尾的 0x55AA 魔数签名。',
details: ['读取第 0 扇区(512字节)', '检查 510-511 字节是否为 0x55AA', '验证引导代码有效性']
},
{
icon: '🔀', name: '多设备尝试',
what: '第一个设备无法启动时,自动尝试下一个。',
details: ['硬盘无系统 → 尝试 U盘', 'U盘不存在 → 尝试网络启动', '全部失败 → 显示错误信息']
},
{
icon: '🚀', name: '跳转执行',
what: '将启动扇区代码加载到内存 0x7C00,CPU 跳转到该地址执行。',
details: ['加载 512 字节引导代码', '跳转到 0x7C00 执行', '控制权交给引导程序']
}
],
analogy: '就像你早上出门找交通工具——先看车库有没有车(硬盘),没有就看门口有没有共享单车(U盘),再不行就叫网约车(网络启动)。'
}
]
const currentStage = computed(() => stages[stage.value])
function getDeviceStatus(i) {
if (foundDevice.value === i) return '✓ 可启动'
if (foundDevice.value > i || (foundDevice.value === -1 && currentDevice.value > i)) return '✗ 跳过'
if (currentDevice.value === i) return '检查中...'
return '等待'
}
// POST 自检动画
watch(() => stage.value, (newStage) => {
if (newStage === 1) {
currentCheck.value = 0
const interval = setInterval(() => {
if (currentCheck.value < postItems.length) {
currentCheck.value++
} else {
clearInterval(interval)
}
}, 600)
}
})
// 硬件初始化动画
watch(() => stage.value, (newStage) => {
if (newStage === 2) {
activeHw.value = 0
hwProgress.value = 0
const interval = setInterval(() => {
if (hwProgress.value < 100) {
hwProgress.value += 5
activeHw.value = Math.floor(hwProgress.value / 20) % hardwareItems.length
} else {
clearInterval(interval)
}
}, 100)
}
})
// 启动设备搜索动画
watch(() => stage.value, (newStage) => {
if (newStage === 3) {
currentDevice.value = 0
foundDevice.value = -1
let device = 0
const interval = setInterval(() => {
if (device < bootDevices.length) {
currentDevice.value = device
// 假设第一个设备(硬盘)可启动
if (device === 0) {
setTimeout(() => {
foundDevice.value = device
}, 400)
clearInterval(interval)
}
device++
} else {
clearInterval(interval)
}
}, 800)
}
})
function next() {
if (stage.value < 3) {
stage.value++
expandedOp.value = -1
}
}
function prev() {
if (stage.value > 0) {
stage.value--
expandedOp.value = -1
}
}
function reset() {
stage.value = 0
expandedOp.value = -1
currentCheck.value = 0
activeHw.value = 0
hwProgress.value = 0
currentDevice.value = 0
foundDevice.value = -1
}
</script>
<style scoped>
.bios-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
background: var(--vp-c-bg-soft);
padding: 1.2rem;
margin: 1rem 0;
}
.demo-header { margin-bottom: 1rem; }
.demo-title { font-size: 0.9rem; font-weight: 700; color: var(--vp-c-text-1); }
/* 主布局 */
.main-layout { display: flex; gap: 1rem; }
/* ===== 左侧屏幕 ===== */
.screen-panel { flex: 0 0 280px; display: flex; flex-direction: column; gap: 0.6rem; }
.monitor { background: #222; border-radius: 10px; padding: 3px; }
.monitor-bezel { background: #111; border-radius: 8px; overflow: hidden; }
.screen {
width: 100%; aspect-ratio: 4/3; display: flex;
align-items: center; justify-content: center;
font-family: 'Courier New', monospace; transition: background 0.5s;
overflow: hidden; position: relative;
}
/* 介绍 */
.stage-0 { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); }
.screen-intro { text-align: center; color: #fff; }
.intro-icon { font-size: 2.5rem; margin-bottom: 0.3rem; }
.intro-title { font-size: 0.9rem; font-weight: 700; margin-bottom: 0.3rem; }
.intro-desc { font-size: 0.6rem; color: #94a3b8; line-height: 1.5; }
/* POST */
.stage-1 { background: #000; flex-direction: column; padding: 0.6rem; align-items: flex-start; }
.screen-post { width: 100%; }
.post-header { color: #4ade80; font-size: 0.55rem; margin-bottom: 0.5rem; font-weight: 700; }
.post-list { display: flex; flex-direction: column; gap: 0.3rem; }
.post-item {
display: flex; align-items: center; gap: 0.4rem;
color: #64748b; font-size: 0.6rem;
transition: all 0.3s;
}
.post-item.checking { color: #fbbf24; }
.post-item.done { color: #4ade80; }
.post-icon { width: 1rem; text-align: center; }
.post-result { margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid #333; }
.result-ok { color: #4ade80; font-size: 0.6rem; }
/* 初始化 */
.stage-2 { background: #0f172a; flex-direction: column; padding: 0.6rem; }
.screen-init { width: 100%; }
.init-header { color: #60a5fa; font-size: 0.55rem; margin-bottom: 0.5rem; font-weight: 700; }
.hardware-grid {
display: grid; grid-template-columns: repeat(3, 1fr);
gap: 0.4rem; margin-bottom: 0.6rem;
}
.hw-item {
display: flex; flex-direction: column; align-items: center;
padding: 0.4rem; background: rgba(255,255,255,0.05);
border-radius: 6px; transition: all 0.3s;
}
.hw-item.active { background: rgba(96, 165, 250, 0.3); transform: scale(1.05); }
.hw-icon { font-size: 1.2rem; margin-bottom: 0.1rem; }
.hw-name { font-size: 0.5rem; color: #94a3b8; }
.init-progress { display: flex; align-items: center; gap: 0.4rem; }
.progress-bar {
flex: 1; height: 4px; background: #333; border-radius: 2px; overflow: hidden;
}
.progress-fill {
height: 100%; background: linear-gradient(90deg, #60a5fa, #3b82f6);
transition: width 0.1s linear;
}
.progress-text { color: #60a5fa; font-size: 0.55rem; width: 2rem; text-align: right; }
/* 启动 */
.stage-3 { background: #1e1b4b; flex-direction: column; padding: 0.6rem; align-items: flex-start; }
.screen-boot { width: 100%; }
.boot-header { color: #a78bfa; font-size: 0.55rem; margin-bottom: 0.4rem; font-weight: 700; }
.order-label { color: #94a3b8; font-size: 0.5rem; margin-bottom: 0.3rem; }
.device-list { display: flex; flex-direction: column; gap: 0.25rem; }
.device-item {
display: flex; align-items: center; gap: 0.3rem;
padding: 0.3rem 0.4rem; background: rgba(255,255,255,0.05);
border-radius: 4px; font-size: 0.55rem; color: #64748b;
transition: all 0.3s;
}
.device-item.checking { color: #fbbf24; background: rgba(251, 191, 36, 0.1); }
.device-item.found { color: #4ade80; background: rgba(74, 222, 128, 0.1); }
.device-item.skipped { opacity: 0.5; }
.device-num {
width: 1rem; height: 1rem; border-radius: 50%;
background: rgba(255,255,255,0.1); display: flex;
align-items: center; justify-content: center; font-size: 0.5rem;
}
.device-icon { font-size: 0.8rem; }
.device-name { flex: 1; }
.device-status { font-size: 0.5rem; }
.boot-result { margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid rgba(167, 139, 250, 0.3); }
.boot-ok { color: #4ade80; font-size: 0.6rem; }
/* 进度点 */
.stage-dots { display: flex; justify-content: center; gap: 0.3rem; }
.stage-dot {
padding: 0.15rem 0.4rem; border-radius: 10px;
font-size: 0.55rem; color: var(--vp-c-text-3);
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
transition: all 0.3s;
}
.stage-dot.active {
background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand);
}
.stage-dot.done { background: #10b981; color: white; border-color: #10b981; }
.dot-label { white-space: nowrap; }
/* 控制按钮 */
.controls { display: flex; gap: 0.4rem; justify-content: center; }
.ctrl-btn {
padding: 0.35rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); color: var(--vp-c-text-2); font-size: 0.68rem;
cursor: pointer; transition: all 0.2s;
}
.ctrl-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.ctrl-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.ctrl-btn.primary {
background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand);
}
.ctrl-btn.primary:hover { opacity: 0.9; }
/* ===== 右侧信息 ===== */
.info-panel { flex: 1; min-width: 0; }
.info-stage-header { display: flex; align-items: flex-start; gap: 0.5rem; margin-bottom: 0.7rem; }
.info-stage-icon { font-size: 1.4rem; }
.info-stage-name { font-size: 0.82rem; font-weight: 700; color: var(--vp-c-text-1); }
.info-stage-desc { font-size: 0.68rem; color: var(--vp-c-text-3); margin-top: 0.1rem; line-height: 1.4; }
/* 操作卡片 */
.info-operations { display: flex; flex-direction: column; gap: 0.35rem; }
.op-card {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 6px; padding: 0.5rem 0.6rem; cursor: pointer;
transition: all 0.2s;
}
.op-card.expanded { border-color: var(--vp-c-brand); box-shadow: 0 1px 8px rgba(0,0,0,0.05); }
.op-header { display: flex; align-items: center; gap: 0.4rem; }
.op-num {
width: 1.2rem; height: 1.2rem; border-radius: 50%;
background: var(--vp-c-brand-soft); color: var(--vp-c-brand);
display: flex; align-items: center; justify-content: center;
font-size: 0.58rem; font-weight: 700; flex-shrink: 0;
}
.op-icon { font-size: 0.9rem; }
.op-name { flex: 1; font-size: 0.72rem; font-weight: 600; color: var(--vp-c-text-1); }
.op-toggle { font-size: 0.65rem; color: var(--vp-c-text-3); }
.op-detail { margin-top: 0.4rem; padding-top: 0.4rem; border-top: 1px dashed var(--vp-c-divider); }
.op-what { font-size: 0.66rem; color: var(--vp-c-text-2); line-height: 1.6; margin-bottom: 0.3rem; }
.op-details { display: flex; flex-direction: column; gap: 0.15rem; }
.op-detail-item {
display: flex; align-items: flex-start; gap: 0.3rem;
font-size: 0.62rem; color: var(--vp-c-text-3); line-height: 1.4;
}
.od-dot { color: var(--vp-c-brand); flex-shrink: 0; }
/* 类比 */
.info-analogy {
display: flex; align-items: flex-start; gap: 0.4rem;
margin-top: 0.6rem; padding: 0.5rem 0.6rem;
background: var(--vp-c-brand-soft); border-radius: 6px;
font-size: 0.64rem; color: var(--vp-c-text-2);
line-height: 1.5; font-style: italic;
}
.analogy-icon { font-size: 0.85rem; flex-shrink: 0; }
/* 蜂鸣声错误码 */
.beep-codes {
margin-top: 0.6rem; padding: 0.5rem 0.6rem;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.beep-header {
display: flex; align-items: center; gap: 0.3rem;
margin-bottom: 0.4rem;
}
.beep-icon { font-size: 0.9rem; }
.beep-title { font-size: 0.7rem; font-weight: 600; color: var(--vp-c-text-1); }
.beep-list { display: flex; flex-direction: column; gap: 0.2rem; }
.beep-item {
display: flex; align-items: center; gap: 0.5rem;
font-size: 0.62rem;
}
.beep-count {
padding: 0.1rem 0.3rem; background: var(--vp-c-brand-soft);
border-radius: 4px; color: var(--vp-c-brand); font-weight: 600;
min-width: 3rem; text-align: center;
}
.beep-meaning { color: var(--vp-c-text-2); }
/* 展开动画 */
.expand-enter-active, .expand-leave-active { transition: all 0.25s ease; overflow: hidden; }
.expand-enter-from, .expand-leave-to { opacity: 0; max-height: 0; }
.expand-enter-to, .expand-leave-from { opacity: 1; max-height: 20rem; }
@media (max-width: 720px) {
.main-layout { flex-direction: column; }
.screen-panel { flex: none; width: 100%; }
}
</style>
@@ -1,15 +1,129 @@
<template>
<div class="boot-demo">
<div class="demo-title">操作系统启动流程</div>
<div class="timeline">
<div v-for="(step, i) in steps" :key="step.name" class="timeline-item">
<div class="marker">
<span class="dot">{{ i + 1 }}</span>
<span v-if="i < steps.length - 1" class="line"></span>
<div class="demo-header">
<span class="demo-title">从开机到桌面</span>
</div>
<div class="main-layout">
<!-- 左侧模拟屏幕 -->
<div class="screen-panel">
<div class="monitor">
<div class="monitor-bezel">
<div class="screen" :class="'stage-' + stage">
<!-- Stage 0: 关机 -->
<div v-if="stage === 0" class="screen-off">
<div class="power-icon"></div>
<div class="off-text">按下电源键开始</div>
</div>
<!-- Stage 1: BIOS 自检 -->
<div v-if="stage === 1" class="screen-bios">
<div class="bios-line" v-for="(line, i) in biosLines" :key="i">{{ line }}</div>
<div class="bios-cursor">_</div>
</div>
<!-- Stage 2: 内核加载 -->
<div v-if="stage === 2" class="screen-kernel">
<div class="kernel-logo">🐧</div>
<div class="kernel-text">Loading kernel...</div>
<div class="kernel-bar-wrap">
<div class="kernel-bar"></div>
</div>
<div class="kernel-modules">
<div v-for="m in kernelModules" :key="m">[ OK ] {{ m }}</div>
</div>
</div>
<!-- Stage 3: 服务启动 -->
<div v-if="stage === 3" class="screen-services">
<div class="svc-header">Starting system services...</div>
<div class="svc-list">
<div v-for="s in services" :key="s.name" class="svc-item">
<span class="svc-status" :class="s.ok ? 'ok' : ''">{{ s.ok ? '●' : '○' }}</span>
<span>{{ s.name }}</span>
</div>
</div>
</div>
<!-- Stage 4: 桌面 -->
<div v-if="stage === 4" class="screen-desktop">
<div class="desktop-icons">
<div class="desktop-icon" v-for="ic in desktopIcons" :key="ic.label">
<span class="icon-emoji">{{ ic.icon }}</span>
<span class="icon-label">{{ ic.label }}</span>
</div>
</div>
<div class="taskbar">
<span class="taskbar-menu"></span>
<span class="taskbar-time">09:57</span>
</div>
</div>
</div>
</div>
</div>
<div class="content">
<div class="step-name">{{ step.name }}</div>
<div class="step-desc">{{ step.desc }}</div>
<!-- 进度指示 -->
<div class="stage-dots">
<div
v-for="(s, i) in stages"
:key="i"
class="stage-dot"
:class="{ active: stage === i, done: stage > i }"
>
<span class="dot-label">{{ s.short }}</span>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button class="ctrl-btn" :disabled="stage <= 0" @click="prev"> 上一步</button>
<button class="ctrl-btn primary" v-if="stage === 0" @click="next"> 开机</button>
<button class="ctrl-btn primary" v-else-if="stage < 4" @click="next">下一步 </button>
<button class="ctrl-btn" v-else @click="reset"> 重新开始</button>
</div>
</div>
<!-- 右侧详细信息 -->
<div class="info-panel">
<div class="info-stage-header">
<span class="info-stage-icon">{{ currentStage.icon }}</span>
<div>
<div class="info-stage-name">{{ currentStage.name }}</div>
<div class="info-stage-desc">{{ currentStage.desc }}</div>
</div>
</div>
<div class="info-operations">
<div
v-for="(op, i) in currentStage.operations"
:key="i"
class="op-card"
:class="{ expanded: expandedOp === i }"
@click="expandedOp = expandedOp === i ? -1 : i"
>
<div class="op-header">
<span class="op-num">{{ i + 1 }}</span>
<span class="op-icon">{{ op.icon }}</span>
<span class="op-name">{{ op.name }}</span>
<span class="op-toggle">{{ expandedOp === i ? '▾' : '▸' }}</span>
</div>
<transition name="expand">
<div v-if="expandedOp === i" class="op-detail">
<div class="op-what">{{ op.what }}</div>
<div v-if="op.details" class="op-details">
<div v-for="(d, j) in op.details" :key="j" class="op-detail-item">
<span class="od-dot"></span>
<span>{{ d }}</span>
</div>
</div>
</div>
</transition>
</div>
</div>
<div v-if="currentStage.analogy" class="info-analogy">
<span class="analogy-icon">💡</span>
<span>{{ currentStage.analogy }}</span>
</div>
</div>
</div>
@@ -17,66 +131,367 @@
</template>
<script setup>
const steps = [
{ name: '引导程序 Bootloader', desc: '从硬盘启动扇区读取引导代码,找到操作系统内核的位置' },
{ name: '内核加载 Kernel', desc: '将内核载入内存,接管 CPU、内存、设备的控制权' },
{ name: '系统服务启动', desc: '启动网络、安全、音频等后台服务(Windows 服务 / Linux systemd' },
{ name: '桌面环境显示', desc: '加载显卡驱动 → 启动显示服务 → 渲染桌面背景和图标' }
import { ref, computed } from 'vue'
const stage = ref(0)
const expandedOp = ref(-1)
const biosLines = [
'American Megatrends BIOS v2.20',
'CPU: Intel Core i7 @ 3.60GHz ... OK',
'Memory: 16384 MB ... OK',
'GPU: NVIDIA GeForce RTX ... OK',
'Keyboard ... OK',
'Detecting drives ...',
'SATA0: Samsung SSD 512GB',
'Boot from Hard Disk ...'
]
const kernelModules = [
'Started Memory Manager',
'Started Process Scheduler',
'Loaded disk driver',
'Mounted root filesystem'
]
const services = [
{ name: 'Network Manager', ok: true },
{ name: 'Firewall (iptables)', ok: true },
{ name: 'Audio Service', ok: true },
{ name: 'SSH Server', ok: true },
{ name: 'Display Manager', ok: true },
{ name: 'System Logger', ok: true }
]
const desktopIcons = [
{ icon: '📁', label: '文件' },
{ icon: '🌐', label: '浏览器' },
{ icon: '⚙️', label: '设置' },
{ icon: '🗑️', label: '回收站' }
]
const stages = [
{
short: '关机',
icon: '⏻',
name: '准备就绪',
desc: '电脑处于关机状态,按下电源键即可开始启动流程',
operations: [
{
icon: '🔌', name: '电源供电',
what: '按下电源键后,电源(PSU)将交流电转换为直流电,为主板、CPU、内存等供电。',
details: ['220V 交流电 → 12V/5V/3.3V 直流电', '主板收到 Power Good 信号后开始工作']
},
{
icon: '⚡', name: 'CPU 复位',
what: 'CPU 收到复位信号,清空所有寄存器,跳转到固定地址(0xFFFFFFF0)执行第一条指令。',
details: ['所有寄存器归零', '指令指针指向 BIOS/UEFI 固件入口']
}
],
analogy: '就像你按下汽车的启动按钮——电池通电,发动机准备点火。'
},
{
short: 'BIOS 自检',
icon: '📟',
name: 'BIOS/UEFI 自检',
desc: '固件程序逐一检测硬件,确保一切正常后寻找启动设备',
operations: [
{
icon: '🧠', name: '内存检测(POST',
what: '向内存写入测试数据并读回验证,确认每根内存条都能正常工作。',
details: ['逐字节写入/读取测试', '检测内存容量和速度', '失败会发出蜂鸣声(1长3短 = 内存错误)']
},
{
icon: '🎮', name: '显卡检测',
what: '初始化显卡,尝试输出画面。如果显卡故障,屏幕会保持黑屏。',
details: ['加载显卡 BIOS', '设置基本显示模式', '失败蜂鸣:1长2短']
},
{
icon: '⌨️', name: '外设检测',
what: '扫描 USB/PS2 端口,检测键盘、鼠标等输入设备。',
details: ['枚举 USB 设备', '检测键盘响应', '非关键设备,缺失不影响启动']
},
{
icon: '💾', name: '寻找启动设备',
what: '按照启动顺序(Boot Order)依次检查硬盘、U盘、网络,找到可启动设备。',
details: ['读取 CMOS 中的启动顺序设置', '检查设备第一扇区的 0x55AA 签名', '找到后将引导代码加载到内存 0x7C00']
}
],
analogy: '好比飞机起飞前的安全检查——机长逐项确认引擎、仪表、燃油,有问题就不能起飞。'
},
{
short: '内核加载',
icon: '⚙️',
name: '操作系统内核加载',
desc: '引导程序找到内核文件,将其加载到内存,内核接管整台计算机',
operations: [
{
icon: '📀', name: '引导程序(Bootloader',
what: '硬盘第一扇区的引导程序(如 GRUB、bootmgr)读取分区表,找到内核文件位置。',
details: ['Windows: bootmgr → 读取 BCD 配置', 'Linux: GRUB → 显示系统选择菜单', 'macOS: boot.efi → 直接加载 XNU 内核']
},
{
icon: '📦', name: '内核解压与加载',
what: '内核通常是压缩存储的,引导程序将其解压并复制到内存的指定位置。',
details: ['解压 vmlinuzLinux)或加载 ntoskrnl.exeWindows', '内核大小通常 5-15 MB']
},
{
icon: '🧠', name: '初始化内存管理',
what: '建立虚拟内存页表,划分内核空间和用户空间,让每个程序以为自己独占内存。',
details: ['建立页表映射', '内核空间:高地址区域', '用户空间:低地址区域,程序运行在这里']
},
{
icon: '📁', name: '挂载根文件系统',
what: '将硬盘分区挂载为根目录(/),从此系统可以读写文件。',
details: ['识别文件系统类型(NTFS/ext4/APFS', '挂载为 /Linux)或 C:\\Windows', '加载设备驱动程序']
}
],
analogy: '内核就像公司的 CEO 上任——接管所有部门(硬件),安排人事(进程)、财务(内存)、后勤(设备)各就各位。'
},
{
short: '服务启动',
icon: '🔧',
name: '系统服务启动',
desc: '内核拉起第一个用户进程,按依赖顺序启动各种后台服务',
operations: [
{
icon: '🚀', name: '初始化进程启动',
what: '内核启动第一个用户态进程(PID=1),它是所有其他进程的"祖先"。',
details: ['Linux: systemd 或 init', 'Windows: smss.exe → csrss.exe → wininit.exe', '负责按配置文件拉起后续服务']
},
{
icon: '🌐', name: '网络服务',
what: '初始化网卡驱动,通过 DHCP 获取 IP 地址,启动 DNS 解析。',
details: ['加载网卡驱动', '发送 DHCP 请求获取 IP', '配置 DNS 服务器地址']
},
{
icon: '🔒', name: '安全服务',
what: '启动防火墙、用户认证系统,确保系统安全。',
details: ['Linux: iptables/nftables 防火墙', 'Windows: Windows Defender、安全中心', '加载登录管理器,准备用户认证']
},
{
icon: '🔊', name: '多媒体与其他服务',
what: '启动音频服务、打印服务、日志服务等,让系统功能完整。',
details: ['音频混合器(PulseAudio/PipeWire', '系统日志(journald/Event Log', '定时任务(cron/Task Scheduler']
}
],
analogy: '就像商场开门营业前——保安到岗(安全)、空调开启(后台服务)、收银上线(网络),一切就绪迎接顾客。'
},
{
short: '桌面就绪',
icon: '🖥️',
name: '桌面环境显示',
desc: '图形界面启动完成,你熟悉的桌面出现了',
operations: [
{
icon: '🎮', name: '显卡驱动加载',
what: '初始化 GPU,设置屏幕分辨率、刷新率和色彩深度。',
details: ['加载 NVIDIA/AMD/Intel 驱动', '设置分辨率(如 1920×1080', '启用硬件加速']
},
{
icon: '🪟', name: '显示服务器启动',
what: '窗口管理系统启动,负责管理所有窗口的绘制、层叠和交互。',
details: ['Windows: Desktop Window Manager (DWM)', 'Linux: X Server 或 Wayland', 'macOS: WindowServer']
},
{
icon: '🎨', name: '桌面环境渲染',
what: '绘制壁纸、桌面图标、任务栏、系统托盘等界面元素。',
details: ['Windows: explorer.exe 渲染桌面', 'Linux: GNOME/KDE/XFCE 桌面环境', 'macOS: Finder + Dock']
},
{
icon: '👆', name: '等待用户操作',
what: '鼠标光标出现,键盘就绪,系统进入完全可交互状态。',
details: ['加载用户配置和偏好设置', '恢复上次会话(如果设置了)', '自启动程序开始运行']
}
],
analogy: '幕布拉开,灯光亮起——舞台(窗口)搭好,演员(图标)就位,等待观众(你)的第一次操作。'
}
]
const currentStage = computed(() => stages[stage.value])
function next() {
if (stage.value < 4) {
stage.value++
expandedOp.value = -1
}
}
function prev() {
if (stage.value > 0) {
stage.value--
expandedOp.value = -1
}
}
function reset() {
stage.value = 0
expandedOp.value = -1
}
</script>
<style scoped>
.boot-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
border-radius: 10px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
padding: 1.2rem;
margin: 1rem 0;
}
.demo-title {
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.8rem;
.demo-header { margin-bottom: 1rem; }
.demo-title { font-size: 0.9rem; font-weight: 700; color: var(--vp-c-text-1); }
/* 主布局 */
.main-layout { display: flex; gap: 1rem; }
/* ===== 左侧屏幕 ===== */
.screen-panel { flex: 0 0 280px; display: flex; flex-direction: column; gap: 0.6rem; }
.monitor { background: #222; border-radius: 10px; padding: 3px; }
.monitor-bezel { background: #111; border-radius: 8px; overflow: hidden; }
.screen {
width: 100%; aspect-ratio: 4/3; display: flex;
align-items: center; justify-content: center;
font-family: 'Courier New', monospace; transition: background 0.5s;
overflow: hidden; position: relative;
}
.timeline { display: flex; flex-direction: column; }
.timeline-item { display: flex; gap: 0.7rem; }
.marker {
display: flex;
flex-direction: column;
align-items: center;
width: 1.6rem;
flex-shrink: 0;
/* 关机 */
.stage-0 { background: #000; }
.screen-off { text-align: center; color: #555; }
.power-icon { font-size: 2.5rem; margin-bottom: 0.3rem; }
.off-text { font-size: 0.6rem; }
/* BIOS */
.stage-1 { background: #000; align-items: flex-start; justify-content: flex-start; padding: 0.5rem; flex-direction: column; }
.screen-bios { width: 100%; }
.bios-line { color: #aaa; font-size: 0.5rem; line-height: 1.5; }
.bios-cursor { color: #fff; animation: blink 1s infinite; font-size: 0.55rem; }
/* 内核 */
.stage-2 { background: #1a1a2e; flex-direction: column; padding: 0.6rem; }
.screen-kernel { text-align: center; width: 100%; }
.kernel-logo { font-size: 1.8rem; margin-bottom: 0.3rem; }
.kernel-text { color: #ccc; font-size: 0.55rem; margin-bottom: 0.4rem; }
.kernel-bar-wrap {
width: 70%; height: 4px; background: #333; border-radius: 2px;
margin: 0 auto 0.5rem; overflow: hidden;
}
.dot {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 600;
.kernel-bar {
width: 100%; height: 100%;
background: linear-gradient(90deg, #4ade80, #22d3ee);
animation: loading 2s ease-in-out infinite;
}
.line {
flex: 1;
width: 2px;
background: var(--vp-c-brand);
opacity: 0.3;
min-height: 1.2rem;
.kernel-modules { text-align: left; width: 100%; }
.kernel-modules div { color: #4ade80; font-size: 0.45rem; line-height: 1.6; }
/* 服务 */
.stage-3 { background: #0f172a; flex-direction: column; align-items: flex-start; padding: 0.6rem; }
.screen-services { width: 100%; }
.svc-header { color: #94a3b8; font-size: 0.55rem; margin-bottom: 0.4rem; }
.svc-list { display: flex; flex-direction: column; gap: 0.15rem; }
.svc-item { color: #cbd5e1; font-size: 0.48rem; display: flex; align-items: center; gap: 0.3rem; }
.svc-status { font-size: 0.5rem; color: #475569; }
.svc-status.ok { color: #4ade80; }
/* 桌面 */
.stage-4 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); flex-direction: column; justify-content: space-between; padding: 0; }
.screen-desktop { flex: 1; display: flex; flex-direction: column; justify-content: space-between; width: 100%; }
.desktop-icons {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: 0.3rem; padding: 0.8rem 0.5rem; justify-items: center;
}
.content { flex: 1; padding-bottom: 0.8rem; }
.step-name {
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-1);
.desktop-icon { display: flex; flex-direction: column; align-items: center; gap: 0.1rem; }
.icon-emoji { font-size: 1.3rem; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); }
.icon-label { font-size: 0.45rem; color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
.taskbar {
background: rgba(0,0,0,0.6); backdrop-filter: blur(8px);
display: flex; justify-content: space-between; align-items: center;
padding: 0.25rem 0.5rem;
}
.step-desc {
font-size: 0.68rem;
color: var(--vp-c-text-3);
margin-top: 0.15rem;
line-height: 1.5;
.taskbar-menu { color: white; font-size: 0.7rem; }
.taskbar-time { color: white; font-size: 0.5rem; }
/* 进度点 */
.stage-dots { display: flex; justify-content: center; gap: 0.3rem; }
.stage-dot {
padding: 0.15rem 0.4rem; border-radius: 10px;
font-size: 0.55rem; color: var(--vp-c-text-3);
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
transition: all 0.3s;
}
.stage-dot.active {
background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand);
}
.stage-dot.done { background: #10b981; color: white; border-color: #10b981; }
.dot-label { white-space: nowrap; }
/* 控制按钮 */
.controls { display: flex; gap: 0.4rem; justify-content: center; }
.ctrl-btn {
padding: 0.35rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); color: var(--vp-c-text-2); font-size: 0.68rem;
cursor: pointer; transition: all 0.2s;
}
.ctrl-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.ctrl-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.ctrl-btn.primary {
background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand);
}
.ctrl-btn.primary:hover { opacity: 0.9; }
/* ===== 右侧信息 ===== */
.info-panel { flex: 1; min-width: 0; }
.info-stage-header { display: flex; align-items: flex-start; gap: 0.5rem; margin-bottom: 0.7rem; }
.info-stage-icon { font-size: 1.4rem; }
.info-stage-name { font-size: 0.82rem; font-weight: 700; color: var(--vp-c-text-1); }
.info-stage-desc { font-size: 0.68rem; color: var(--vp-c-text-3); margin-top: 0.1rem; line-height: 1.4; }
/* 操作卡片 */
.info-operations { display: flex; flex-direction: column; gap: 0.35rem; }
.op-card {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 6px; padding: 0.5rem 0.6rem; cursor: pointer;
transition: all 0.2s;
}
.op-card.expanded { border-color: var(--vp-c-brand); box-shadow: 0 1px 8px rgba(0,0,0,0.05); }
.op-header { display: flex; align-items: center; gap: 0.4rem; }
.op-num {
width: 1.2rem; height: 1.2rem; border-radius: 50%;
background: var(--vp-c-brand-soft); color: var(--vp-c-brand);
display: flex; align-items: center; justify-content: center;
font-size: 0.58rem; font-weight: 700; flex-shrink: 0;
}
.op-icon { font-size: 0.9rem; }
.op-name { flex: 1; font-size: 0.72rem; font-weight: 600; color: var(--vp-c-text-1); }
.op-toggle { font-size: 0.65rem; color: var(--vp-c-text-3); }
.op-detail { margin-top: 0.4rem; padding-top: 0.4rem; border-top: 1px dashed var(--vp-c-divider); }
.op-what { font-size: 0.66rem; color: var(--vp-c-text-2); line-height: 1.6; margin-bottom: 0.3rem; }
.op-details { display: flex; flex-direction: column; gap: 0.15rem; }
.op-detail-item {
display: flex; align-items: flex-start; gap: 0.3rem;
font-size: 0.62rem; color: var(--vp-c-text-3); line-height: 1.4;
}
.od-dot { color: var(--vp-c-brand); flex-shrink: 0; }
/* 类比 */
.info-analogy {
display: flex; align-items: flex-start; gap: 0.4rem;
margin-top: 0.6rem; padding: 0.5rem 0.6rem;
background: var(--vp-c-brand-soft); border-radius: 6px;
font-size: 0.64rem; color: var(--vp-c-text-2);
line-height: 1.5; font-style: italic;
}
.analogy-icon { font-size: 0.85rem; flex-shrink: 0; }
/* 展开动画 */
.expand-enter-active, .expand-leave-active { transition: all 0.25s ease; overflow: hidden; }
.expand-enter-from, .expand-leave-to { opacity: 0; max-height: 0; }
.expand-enter-to, .expand-leave-from { opacity: 1; max-height: 20rem; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
@keyframes loading { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }
@media (max-width: 720px) {
.main-layout { flex-direction: column; }
.screen-panel { flex: none; width: 100%; }
}
</style>
@@ -0,0 +1,763 @@
<template>
<div class="os-boot-demo">
<div class="demo-header">
<span class="demo-title">操作系统启动流程</span>
</div>
<div class="main-layout">
<!-- 左侧模拟屏幕 -->
<div class="screen-panel">
<div class="monitor">
<div class="monitor-bezel">
<div class="screen" :class="'stage-' + stage">
<!-- Stage 0: 操作系统介绍 -->
<div v-if="stage === 0" class="screen-intro">
<div class="intro-icon">🖥</div>
<div class="intro-title">操作系统</div>
<div class="intro-desc">管理硬件和软件资源<br>计算机的"大管家"</div>
<div class="os-icons">
<div v-for="os in osList" :key="os.name" class="os-item">
<span class="os-icon">{{ os.icon }}</span>
<span class="os-name">{{ os.name }}</span>
</div>
</div>
</div>
<!-- Stage 1: 引导程序 -->
<div v-if="stage === 1" class="screen-bootloader">
<div class="bl-header">Bootloader</div>
<div class="bl-flow">
<div v-for="(step, i) in blSteps" :key="i" class="bl-step" :class="{ active: blStep >= i }">
<span class="bl-num">{{ i + 1 }}</span>
<span class="bl-text">{{ step }}</span>
</div>
</div>
<div class="bl-code">
<div class="code-line" v-for="(line, i) in blCode" :key="i" :class="{ highlight: blCodeLine === i }">
{{ line }}
</div>
</div>
</div>
<!-- Stage 2: 内核加载 -->
<div v-if="stage === 2" class="screen-kernel">
<div class="kernel-header">Kernel Loading</div>
<div class="kernel-logo"></div>
<div class="kernel-name">{{ kernelName }}</div>
<div class="kernel-bar-wrap">
<div class="kernel-bar" :style="{ width: kernelProgress + '%' }"></div>
</div>
<div class="kernel-modules">
<div v-for="(m, i) in kernelModules" :key="i" class="k-module" :class="{ loaded: kernelProgress > (i + 1) * 20 }">
<span class="k-status">{{ kernelProgress > (i + 1) * 20 ? '✓' : '○' }}</span>
<span class="k-name">{{ m }}</span>
</div>
</div>
</div>
<!-- Stage 3: 系统服务 -->
<div v-if="stage === 3" class="screen-services">
<div class="svc-header">System Services</div>
<div class="svc-grid">
<div v-for="(svc, i) in services" :key="i" class="svc-item" :class="{ started: svcProgress > i * 15 }">
<span class="svc-icon">{{ svc.icon }}</span>
<span class="svc-name">{{ svc.name }}</span>
<span class="svc-status">{{ svcProgress > i * 15 ? '●' : '○' }}</span>
</div>
</div>
<div class="svc-progress-bar">
<div class="svc-progress-fill" :style="{ width: svcProgress + '%' }"></div>
</div>
</div>
<!-- Stage 4: 桌面显示 -->
<div v-if="stage === 4" class="screen-desktop">
<div class="desktop-bg">
<div class="desktop-icons">
<div class="desktop-icon" v-for="(ic, i) in desktopIcons" :key="i">
<span class="d-icon">{{ ic.icon }}</span>
<span class="d-label">{{ ic.label }}</span>
</div>
</div>
<div class="taskbar">
<span class="taskbar-start">🪟</span>
<span class="taskbar-apps">
<span v-for="(app, i) in taskbarApps" :key="i" class="taskbar-app">{{ app }}</span>
</span>
<span class="taskbar-time">{{ currentTime }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 进度指示 -->
<div class="stage-dots">
<div
v-for="(s, i) in stages"
:key="i"
class="stage-dot"
:class="{ active: stage === i, done: stage > i }"
>
<span class="dot-label">{{ s.short }}</span>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button class="ctrl-btn" :disabled="stage <= 0" @click="prev"> 上一步</button>
<button class="ctrl-btn primary" v-if="stage === 0" @click="next">开始 </button>
<button class="ctrl-btn primary" v-else-if="stage < 4" @click="next">下一步 </button>
<button class="ctrl-btn" v-else @click="reset"> 重新开始</button>
</div>
</div>
<!-- 右侧详细信息 -->
<div class="info-panel">
<div class="info-stage-header">
<span class="info-stage-icon">{{ currentStage.icon }}</span>
<div>
<div class="info-stage-name">{{ currentStage.name }}</div>
<div class="info-stage-desc">{{ currentStage.desc }}</div>
</div>
</div>
<div class="info-operations">
<div
v-for="(op, i) in currentStage.operations"
:key="i"
class="op-card"
:class="{ expanded: expandedOp === i }"
@click="expandedOp = expandedOp === i ? -1 : i"
>
<div class="op-header">
<span class="op-num">{{ i + 1 }}</span>
<span class="op-icon">{{ op.icon }}</span>
<span class="op-name">{{ op.name }}</span>
<span class="op-toggle">{{ expandedOp === i ? '▾' : '▸' }}</span>
</div>
<transition name="expand">
<div v-if="expandedOp === i" class="op-detail">
<div class="op-what">{{ op.what }}</div>
<div v-if="op.details" class="op-details">
<div v-for="(d, j) in op.details" :key="j" class="op-detail-item">
<span class="od-dot"></span>
<span>{{ d }}</span>
</div>
</div>
</div>
</transition>
</div>
</div>
<div v-if="currentStage.analogy" class="info-analogy">
<span class="analogy-icon">💡</span>
<span>{{ currentStage.analogy }}</span>
</div>
<!-- 操作系统对比表 -->
<div v-if="stage === 0" class="os-comparison">
<div class="os-comp-header">
<span class="os-comp-icon">📊</span>
<span class="os-comp-title">常见操作系统</span>
</div>
<div class="os-comp-table">
<div class="os-comp-row os-comp-header-row">
<span class="os-comp-cell">系统</span>
<span class="os-comp-cell">特点</span>
<span class="os-comp-cell">典型设备</span>
</div>
<div v-for="os in osList" :key="os.name" class="os-comp-row">
<span class="os-comp-cell os-name-cell">
<span class="os-comp-icon-small">{{ os.icon }}</span>
{{ os.name }}
</span>
<span class="os-comp-cell">{{ os.feature }}</span>
<span class="os-comp-cell">{{ os.device }}</span>
</div>
</div>
</div>
<!-- 启动流程对比 -->
<div v-if="stage === 1" class="boot-flow-comparison">
<div class="bf-header">
<span class="bf-icon">🔄</span>
<span class="bf-title">Windows vs Linux 启动流程</span>
</div>
<div class="bf-content">
<div class="bf-col">
<div class="bf-os-name">🪟 Windows</div>
<div class="bf-flow">
<div class="bf-step" v-for="(step, i) in windowsFlow" :key="i">
<span class="bf-arrow" v-if="i > 0"></span>
<span class="bf-step-text">{{ step }}</span>
</div>
</div>
</div>
<div class="bf-col">
<div class="bf-os-name">🐧 Linux</div>
<div class="bf-flow">
<div class="bf-step" v-for="(step, i) in linuxFlow" :key="i">
<span class="bf-arrow" v-if="i > 0"></span>
<span class="bf-step-text">{{ step }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
const stage = ref(0)
const expandedOp = ref(-1)
const blStep = ref(-1)
const blCodeLine = ref(-1)
const kernelProgress = ref(0)
const svcProgress = ref(0)
const currentTime = ref('09:41')
const osList = [
{ name: 'Windows', icon: '🪟', feature: '生态丰富,兼容性好', device: '桌面电脑、笔记本' },
{ name: 'macOS', icon: '🍎', feature: '苹果生态,流畅稳定', device: 'Mac 电脑' },
{ name: 'Linux', icon: '🐧', feature: '开源免费,服务器首选', device: '服务器、嵌入式' },
{ name: 'Android', icon: '🤖', feature: '移动端 Linux', device: '手机、平板' },
{ name: 'iOS', icon: '📱', feature: '苹果移动端', device: 'iPhone、iPad' }
]
const blSteps = ['读取分区表', '找到系统分区', '加载内核到内存', '跳转到内核入口']
const blCode = [
'mov ax, 0x07C0',
'mov ds, ax',
'read_sector:',
' mov ah, 0x02',
' int 0x13',
'jmp 0x0000:0x7C00'
]
const kernelName = ref('ntoskrnl.exe')
const kernelModules = ['进程管理', '内存管理', '文件系统', '设备驱动']
const services = [
{ name: '网络服务', icon: '🌐' },
{ name: '音频服务', icon: '🔊' },
{ name: '安全中心', icon: '🛡️' },
{ name: '打印服务', icon: '🖨️' },
{ name: '图形界面', icon: '🎨' },
{ name: '系统日志', icon: '📝' }
]
const desktopIcons = [
{ icon: '📁', label: '文件' },
{ icon: '🌐', label: '浏览器' },
{ icon: '📧', label: '邮件' },
{ icon: '⚙️', label: '设置' }
]
const taskbarApps = ['🌐', '📁', '📝']
const windowsFlow = ['BIOS', 'MBR', 'bootmgr', 'winload.exe', 'ntoskrnl.exe', '系统服务', '桌面']
const linuxFlow = ['BIOS', 'GRUB', 'vmlinuz', 'systemd', '系统服务', '桌面环境']
const stages = [
{
short: '介绍',
icon: '🖥️',
name: '什么是操作系统?',
desc: '操作系统(OS)是管理计算机硬件和软件资源的程序集合,就像一个"大管家"。',
operations: [
{
icon: '🏢', name: '资源管理',
what: '操作系统负责管理 CPU、内存、硬盘、网络等所有硬件资源。',
details: ['进程管理 - 调度程序运行', '内存管理 - 分配和回收内存', '文件系统 - 管理文件存储', '设备管理 - 控制硬件设备']
},
{
icon: '🎮', name: '提供接口',
what: '为应用程序提供统一的接口,让程序不需要直接操作硬件。',
details: ['系统调用接口(API', '图形用户界面(GUI', '命令行界面(CLI', '驱动程序接口']
},
{
icon: '🔒', name: '安全保护',
what: '保护系统资源不被非法访问,确保多用户环境下的隔离。',
details: ['用户权限管理', '进程地址空间隔离', '文件访问控制', '网络安全防护']
}
],
analogy: '操作系统就像一座大楼的物业管理——负责水电供应(硬件资源)、分配房间(内存)、管理仓库(文件系统)、维护安全(权限控制),让住户(应用程序)可以安心生活。'
},
{
short: '引导程序',
icon: '🚀',
name: '引导程序(Bootloader',
desc: '硬盘第一个扇区存放着引导程序,它的任务是把操作系统内核加载到内存。',
operations: [
{
icon: '📀', name: '读取分区表',
what: '引导程序首先读取硬盘的分区表,找到操作系统所在的分区。',
details: ['读取 MBR(主引导记录)', '解析分区表结构', '定位活动分区', 'Windows: bootmgr / Linux: GRUB']
},
{
icon: '🔍', name: '定位内核',
what: '在系统分区中找到操作系统内核文件的位置。',
details: ['Windows: 读取 BCD 配置', 'Linux: 显示系统选择菜单', '支持多系统启动', '加载文件系统驱动']
},
{
icon: '💾', name: '加载到内存',
what: '将内核文件从硬盘读取到内存的指定位置。',
details: ['解压压缩的内核镜像', '复制到内存 0x100000 以上', 'Windows: ntoskrnl.exe', 'Linux: vmlinuz']
},
{
icon: '➡️', name: '跳转执行',
what: '设置好初始环境后,跳转到内核入口点,把控制权交给内核。',
details: ['设置 CPU 保护模式', '初始化页表', '跳转至内核入口', '内核开始执行']
}
],
analogy: '引导程序就像剧场的报幕员——他先上台确认场地(检查硬件)、找到剧本(定位内核)、把道具摆好(加载到内存),然后宣布:"演出开始!"(跳转执行)'
},
{
short: '内核加载',
icon: '⚙️',
name: '操作系统内核(Kernel',
desc: '内核是操作系统的核心,负责管理内存、CPU、进程等核心功能。',
operations: [
{
icon: '🧠', name: '进程管理',
what: '创建第一个用户进程,建立进程调度机制。',
details: ['创建 init/systemd 进程', '建立进程控制块(PCB', '初始化调度器', '设置进程优先级']
},
{
icon: '💾', name: '内存管理',
what: '建立虚拟内存系统,划分内核空间和用户空间。',
details: ['初始化页表', '建立物理内存映射', '设置内存保护', '启用虚拟内存']
},
{
icon: '📁', name: '文件系统',
what: '挂载根文件系统,初始化 VFS 层。',
details: ['识别文件系统类型', '挂载根分区(/', '初始化 inode 缓存', '建立文件描述符表']
},
{
icon: '🔌', name: '设备驱动',
what: '加载核心设备驱动,初始化硬件抽象层。',
details: ['加载磁盘驱动', '初始化显示驱动', '加载键盘鼠标驱动', '枚举 PCI 设备']
}
],
analogy: '内核就像公司的 CEO 上任——接管所有部门(硬件),安排人事(进程)、财务(内存)、后勤(设备)各就各位,建立公司的基本运作框架。'
},
{
short: '服务启动',
icon: '🔧',
name: '系统服务启动',
desc: '内核拉起第一个用户进程,按依赖顺序启动各种后台服务。',
operations: [
{
icon: '🚀', name: '初始化进程',
what: '启动第一个用户态进程(PID=1),它是所有其他进程的"祖先"。',
details: ['Linux: systemd 或 init', 'Windows: smss.exe → csrss.exe', '读取服务配置文件', '按依赖关系排序']
},
{
icon: '🌐', name: '网络服务',
what: '初始化网卡驱动,配置网络连接。',
details: ['加载网卡驱动', 'DHCP 获取 IP 地址', '配置 DNS 服务器', '启动防火墙']
},
{
icon: '🔒', name: '安全服务',
what: '启动用户认证和安全监控服务。',
details: ['启动登录管理器', '初始化权限系统', '启动杀毒软件', '配置安全策略']
},
{
icon: '🔊', name: '多媒体服务',
what: '启动音频、显示等多媒体相关服务。',
details: ['启动音频服务', '初始化显示管理器', '加载主题和字体', '准备用户界面']
}
],
analogy: '就像商场开门营业前——保安到岗(安全)、空调开启(后台服务)、收银上线(网络),一切就绪迎接顾客(用户)。'
},
{
short: '桌面就绪',
icon: '🖥️',
name: '显示桌面',
desc: '图形界面启动完成,用户熟悉的桌面环境呈现出来。',
operations: [
{
icon: '🎮', name: '显卡驱动',
what: '初始化 GPU,设置屏幕分辨率和色彩。',
details: ['加载显卡驱动', '设置分辨率(如 1920×1080', '启用硬件加速', '配置多显示器']
},
{
icon: '🪟', name: '窗口系统',
what: '启动窗口管理器,负责窗口的绘制和交互。',
details: ['Windows: DWM', 'Linux: X11/Wayland', 'macOS: WindowServer', '管理窗口层叠关系']
},
{
icon: '🎨', name: '桌面环境',
what: '绘制壁纸、桌面图标、任务栏等界面元素。',
details: ['加载桌面壁纸', '显示桌面图标', '渲染任务栏', '加载系统托盘']
},
{
icon: '👆', name: '用户交互',
what: '鼠标光标出现,系统进入完全可交互状态。',
details: ['显示鼠标指针', '响应键盘输入', '加载用户配置', '启动自启动程序']
}
],
analogy: '幕布拉开,灯光亮起——舞台(窗口)搭好,演员(图标)就位,等待观众(你)的第一次操作。'
}
]
const currentStage = computed(() => stages[stage.value])
let timeInterval = null
onMounted(() => {
timeInterval = setInterval(() => {
const now = new Date()
currentTime.value = now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}, 1000)
})
onUnmounted(() => {
if (timeInterval) clearInterval(timeInterval)
})
// 引导程序动画
watch(() => stage.value, (newStage) => {
if (newStage === 1) {
blStep.value = -1
blCodeLine.value = -1
let step = 0
const interval = setInterval(() => {
if (step < blSteps.length) {
blStep.value = step
blCodeLine.value = step + 1
step++
} else {
clearInterval(interval)
}
}, 600)
}
})
// 内核加载动画
watch(() => stage.value, (newStage) => {
if (newStage === 2) {
kernelProgress.value = 0
kernelName.value = Math.random() > 0.5 ? 'ntoskrnl.exe' : 'vmlinuz'
const interval = setInterval(() => {
if (kernelProgress.value < 100) {
kernelProgress.value += 4
} else {
clearInterval(interval)
}
}, 80)
}
})
// 服务启动动画
watch(() => stage.value, (newStage) => {
if (newStage === 3) {
svcProgress.value = 0
const interval = setInterval(() => {
if (svcProgress.value < 100) {
svcProgress.value += 3
} else {
clearInterval(interval)
}
}, 100)
}
})
function next() {
if (stage.value < 4) {
stage.value++
expandedOp.value = -1
}
}
function prev() {
if (stage.value > 0) {
stage.value--
expandedOp.value = -1
}
}
function reset() {
stage.value = 0
expandedOp.value = -1
blStep.value = -1
blCodeLine.value = -1
kernelProgress.value = 0
svcProgress.value = 0
}
</script>
<style scoped>
.os-boot-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
background: var(--vp-c-bg-soft);
padding: 1.2rem;
margin: 1rem 0;
}
.demo-header { margin-bottom: 1rem; }
.demo-title { font-size: 0.9rem; font-weight: 700; color: var(--vp-c-text-1); }
/* 主布局 */
.main-layout { display: flex; gap: 1rem; }
/* ===== 左侧屏幕 ===== */
.screen-panel { flex: 0 0 280px; display: flex; flex-direction: column; gap: 0.6rem; }
.monitor { background: #222; border-radius: 10px; padding: 3px; }
.monitor-bezel { background: #111; border-radius: 8px; overflow: hidden; }
.screen {
width: 100%; aspect-ratio: 4/3; display: flex;
align-items: center; justify-content: center;
font-family: 'Courier New', monospace; transition: background 0.5s;
overflow: hidden; position: relative;
}
/* 介绍 */
.stage-0 { background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%); }
.screen-intro { text-align: center; color: #fff; width: 100%; padding: 0.5rem; }
.intro-icon { font-size: 2rem; margin-bottom: 0.2rem; }
.intro-title { font-size: 0.8rem; font-weight: 700; margin-bottom: 0.2rem; }
.intro-desc { font-size: 0.55rem; color: #94a3b8; margin-bottom: 0.4rem; line-height: 1.4; }
.os-icons {
display: grid; grid-template-columns: repeat(5, 1fr);
gap: 0.2rem; padding: 0 0.3rem;
}
.os-item { display: flex; flex-direction: column; align-items: center; }
.os-icon { font-size: 1rem; }
.os-name { font-size: 0.4rem; color: #94a3b8; margin-top: 0.1rem; }
/* Bootloader */
.stage-1 { background: #0f172a; flex-direction: column; padding: 0.5rem; align-items: flex-start; }
.screen-bootloader { width: 100%; }
.bl-header { color: #fbbf24; font-size: 0.55rem; margin-bottom: 0.4rem; font-weight: 700; }
.bl-flow { display: flex; flex-direction: column; gap: 0.2rem; margin-bottom: 0.4rem; }
.bl-step {
display: flex; align-items: center; gap: 0.3rem;
color: #64748b; font-size: 0.55rem;
transition: all 0.3s;
}
.bl-step.active { color: #fbbf24; }
.bl-num {
width: 1rem; height: 1rem; border-radius: 50%;
background: rgba(255,255,255,0.1); display: flex;
align-items: center; justify-content: center; font-size: 0.5rem;
}
.bl-step.active .bl-num { background: #fbbf24; color: #000; }
.bl-code {
background: rgba(0,0,0,0.5); border-radius: 4px;
padding: 0.3rem; font-size: 0.45rem; color: #64748b;
font-family: monospace;
}
.code-line { line-height: 1.4; padding: 0 0.2rem; }
.code-line.highlight { color: #fbbf24; background: rgba(251, 191, 36, 0.1); border-radius: 2px; }
/* Kernel */
.stage-2 { background: #1a1a2e; flex-direction: column; padding: 0.6rem; }
.screen-kernel { text-align: center; width: 100%; }
.kernel-header { color: #60a5fa; font-size: 0.55rem; margin-bottom: 0.4rem; font-weight: 700; }
.kernel-logo { font-size: 2rem; margin-bottom: 0.2rem; }
.kernel-name { color: #fff; font-size: 0.6rem; margin-bottom: 0.4rem; }
.kernel-bar-wrap {
width: 80%; height: 6px; background: #333; border-radius: 3px;
margin: 0 auto 0.4rem; overflow: hidden;
}
.kernel-bar {
height: 100%; background: linear-gradient(90deg, #60a5fa, #3b82f6);
transition: width 0.1s linear;
}
.kernel-modules { display: flex; flex-direction: column; gap: 0.15rem; }
.k-module {
display: flex; align-items: center; gap: 0.3rem;
color: #64748b; font-size: 0.55rem;
}
.k-module.loaded { color: #4ade80; }
.k-status { width: 1rem; text-align: center; }
/* Services */
.stage-3 { background: #0f172a; flex-direction: column; padding: 0.5rem; }
.screen-services { width: 100%; }
.svc-header { color: #a78bfa; font-size: 0.55rem; margin-bottom: 0.4rem; font-weight: 700; }
.svc-grid {
display: grid; grid-template-columns: repeat(2, 1fr);
gap: 0.3rem; margin-bottom: 0.4rem;
}
.svc-item {
display: flex; align-items: center; gap: 0.2rem;
padding: 0.25rem; background: rgba(255,255,255,0.05);
border-radius: 4px; font-size: 0.5rem; color: #64748b;
transition: all 0.3s;
}
.svc-item.started { color: #a78bfa; background: rgba(167, 139, 250, 0.1); }
.svc-icon { font-size: 0.7rem; }
.svc-name { flex: 1; }
.svc-status { font-size: 0.5rem; }
.svc-progress-bar {
height: 4px; background: #333; border-radius: 2px; overflow: hidden;
}
.svc-progress-fill {
height: 100%; background: linear-gradient(90deg, #a78bfa, #8b5cf6);
transition: width 0.1s linear;
}
/* Desktop */
.stage-4 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 0; }
.screen-desktop { width: 100%; height: 100%; }
.desktop-bg {
width: 100%; height: 100%;
display: flex; flex-direction: column; justify-content: space-between;
}
.desktop-icons {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: 0.3rem; padding: 0.6rem 0.4rem;
}
.desktop-icon {
display: flex; flex-direction: column; align-items: center;
gap: 0.1rem;
}
.d-icon { font-size: 1.2rem; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); }
.d-label { font-size: 0.45rem; color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
.taskbar {
background: rgba(0,0,0,0.6); backdrop-filter: blur(8px);
display: flex; align-items: center; gap: 0.4rem;
padding: 0.25rem 0.4rem;
}
.taskbar-start { font-size: 0.9rem; cursor: pointer; }
.taskbar-apps { display: flex; gap: 0.2rem; flex: 1; }
.taskbar-app { font-size: 0.8rem; opacity: 0.8; cursor: pointer; }
.taskbar-time { color: white; font-size: 0.5rem; }
/* 进度点 */
.stage-dots { display: flex; justify-content: center; gap: 0.3rem; }
.stage-dot {
padding: 0.15rem 0.4rem; border-radius: 10px;
font-size: 0.55rem; color: var(--vp-c-text-3);
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
transition: all 0.3s;
}
.stage-dot.active {
background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand);
}
.stage-dot.done { background: #10b981; color: white; border-color: #10b981; }
.dot-label { white-space: nowrap; }
/* 控制按钮 */
.controls { display: flex; gap: 0.4rem; justify-content: center; }
.ctrl-btn {
padding: 0.35rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); color: var(--vp-c-text-2); font-size: 0.68rem;
cursor: pointer; transition: all 0.2s;
}
.ctrl-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.ctrl-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.ctrl-btn.primary {
background: var(--vp-c-brand); color: white; border-color: var(--vp-c-brand);
}
.ctrl-btn.primary:hover { opacity: 0.9; }
/* ===== 右侧信息 ===== */
.info-panel { flex: 1; min-width: 0; }
.info-stage-header { display: flex; align-items: flex-start; gap: 0.5rem; margin-bottom: 0.7rem; }
.info-stage-icon { font-size: 1.4rem; }
.info-stage-name { font-size: 0.82rem; font-weight: 700; color: var(--vp-c-text-1); }
.info-stage-desc { font-size: 0.68rem; color: var(--vp-c-text-3); margin-top: 0.1rem; line-height: 1.4; }
/* 操作卡片 */
.info-operations { display: flex; flex-direction: column; gap: 0.35rem; }
.op-card {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 6px; padding: 0.5rem 0.6rem; cursor: pointer;
transition: all 0.2s;
}
.op-card.expanded { border-color: var(--vp-c-brand); box-shadow: 0 1px 8px rgba(0,0,0,0.05); }
.op-header { display: flex; align-items: center; gap: 0.4rem; }
.op-num {
width: 1.2rem; height: 1.2rem; border-radius: 50%;
background: var(--vp-c-brand-soft); color: var(--vp-c-brand);
display: flex; align-items: center; justify-content: center;
font-size: 0.58rem; font-weight: 700; flex-shrink: 0;
}
.op-icon { font-size: 0.9rem; }
.op-name { flex: 1; font-size: 0.72rem; font-weight: 600; color: var(--vp-c-text-1); }
.op-toggle { font-size: 0.65rem; color: var(--vp-c-text-3); }
.op-detail { margin-top: 0.4rem; padding-top: 0.4rem; border-top: 1px dashed var(--vp-c-divider); }
.op-what { font-size: 0.66rem; color: var(--vp-c-text-2); line-height: 1.6; margin-bottom: 0.3rem; }
.op-details { display: flex; flex-direction: column; gap: 0.15rem; }
.op-detail-item {
display: flex; align-items: flex-start; gap: 0.3rem;
font-size: 0.62rem; color: var(--vp-c-text-3); line-height: 1.4;
}
.od-dot { color: var(--vp-c-brand); flex-shrink: 0; }
/* 类比 */
.info-analogy {
display: flex; align-items: flex-start; gap: 0.4rem;
margin-top: 0.6rem; padding: 0.5rem 0.6rem;
background: var(--vp-c-brand-soft); border-radius: 6px;
font-size: 0.64rem; color: var(--vp-c-text-2);
line-height: 1.5; font-style: italic;
}
.analogy-icon { font-size: 0.85rem; flex-shrink: 0; }
/* 操作系统对比表 */
.os-comparison {
margin-top: 0.6rem; padding: 0.5rem 0.6rem;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.os-comp-header {
display: flex; align-items: center; gap: 0.3rem;
margin-bottom: 0.4rem;
}
.os-comp-icon { font-size: 0.9rem; }
.os-comp-title { font-size: 0.7rem; font-weight: 600; color: var(--vp-c-text-1); }
.os-comp-table { display: flex; flex-direction: column; gap: 0.2rem; }
.os-comp-row {
display: grid; grid-template-columns: 1.2fr 1.5fr 1.3fr;
gap: 0.3rem; font-size: 0.6rem; padding: 0.2rem 0;
border-bottom: 1px solid var(--vp-c-divider);
}
.os-comp-row:last-child { border-bottom: none; }
.os-comp-header-row { font-weight: 600; color: var(--vp-c-text-1); }
.os-comp-cell { color: var(--vp-c-text-2); }
.os-name-cell { display: flex; align-items: center; gap: 0.2rem; }
.os-comp-icon-small { font-size: 0.7rem; }
/* 启动流程对比 */
.boot-flow-comparison {
margin-top: 0.6rem; padding: 0.5rem 0.6rem;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.bf-header {
display: flex; align-items: center; gap: 0.3rem;
margin-bottom: 0.4rem;
}
.bf-icon { font-size: 0.9rem; }
.bf-title { font-size: 0.7rem; font-weight: 600; color: var(--vp-c-text-1); }
.bf-content { display: flex; gap: 0.5rem; }
.bf-col { flex: 1; }
.bf-os-name { font-size: 0.65rem; font-weight: 600; color: var(--vp-c-text-1); margin-bottom: 0.3rem; }
.bf-flow { display: flex; flex-direction: column; gap: 0.15rem; }
.bf-step { display: flex; flex-direction: column; align-items: center; font-size: 0.55rem; }
.bf-arrow { color: var(--vp-c-brand); font-size: 0.6rem; }
.bf-step-text {
padding: 0.15rem 0.3rem; background: var(--vp-c-bg-soft);
border-radius: 3px; color: var(--vp-c-text-2);
}
/* 展开动画 */
.expand-enter-active, .expand-leave-active { transition: all 0.25s ease; overflow: hidden; }
.expand-enter-from, .expand-leave-to { opacity: 0; max-height: 0; }
.expand-enter-to, .expand-leave-from { opacity: 1; max-height: 20rem; }
@media (max-width: 720px) {
.main-layout { flex-direction: column; }
.screen-panel { flex: none; width: 100%; }
.bf-content { flex-direction: column; }
}
</style>
@@ -0,0 +1,123 @@
<!--
CDNAccelerationDemo.vue
CDN 加速演示展示 CDN 如何加速文件访问
-->
<template>
<div class="cdn-demo">
<div class="header">
<div class="title">CDN 加速原理</div>
<div class="subtitle">对比有无 CDN 时的文件访问路径</div>
</div>
<div class="mode-tabs">
<button :class="['tab', { active: !cdnEnabled }]" @click="cdnEnabled = false"> CDN</button>
<button :class="['tab', { active: cdnEnabled }]" @click="cdnEnabled = true"> CDN</button>
</div>
<div class="diagram">
<div class="node user-node">
<div class="node-icon">👤</div>
<div class="node-label">北京用户</div>
</div>
<div class="path-line" :class="{ highlight: !cdnEnabled }">
<span class="latency">{{ cdnEnabled ? '5ms' : '200ms' }}</span>
</div>
<div v-if="cdnEnabled" class="node cdn-node">
<div class="node-icon"></div>
<div class="node-label">北京 CDN 节点</div>
<div class="node-detail">缓存命中</div>
</div>
<div v-if="cdnEnabled" class="path-line miss-line">
<span class="latency miss">缓存未命中时回源</span>
</div>
<div class="node origin-node">
<div class="node-icon">🏢</div>
<div class="node-label">源站美西 S3</div>
</div>
</div>
<div class="metrics">
<div class="metric">
<div class="metric-label">首字节时间 (TTFB)</div>
<div class="metric-bar">
<div class="bar-fill" :style="{ width: cdnEnabled ? '15%' : '100%' }"></div>
</div>
<div class="metric-value">{{ cdnEnabled ? '~30ms' : '~200ms' }}</div>
</div>
<div class="metric">
<div class="metric-label">下载 1MB 图片</div>
<div class="metric-bar">
<div class="bar-fill" :style="{ width: cdnEnabled ? '20%' : '100%' }"></div>
</div>
<div class="metric-value">{{ cdnEnabled ? '~50ms' : '~800ms' }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const cdnEnabled = ref(true)
</script>
<style scoped>
.cdn-demo {
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
}
.header { margin-bottom: 1rem; }
.title { font-weight: 700; font-size: 1.1rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.mode-tabs { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; }
.tab {
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
}
.tab.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.diagram {
display: flex; align-items: center; justify-content: center;
gap: 0.5rem; margin-bottom: 1.5rem; flex-wrap: wrap;
}
.node {
padding: 0.75rem 1rem; border-radius: 10px; text-align: center;
border: 2px solid var(--vp-c-divider); background: var(--vp-c-bg);
}
.cdn-node { border-color: #22c55e; background: rgba(34,197,94,0.05); }
.node-icon { font-size: 1.5rem; }
.node-label { font-weight: 600; font-size: 0.85rem; margin-top: 0.25rem; }
.node-detail { font-size: 0.75rem; color: #22c55e; }
.path-line {
display: flex; align-items: center; padding: 0 0.5rem;
font-size: 0.8rem; color: var(--vp-c-text-3);
}
.path-line::before, .path-line::after { content: '→'; margin: 0 0.25rem; }
.latency {
padding: 0.15rem 0.4rem; border-radius: 4px; font-family: var(--vp-font-family-mono);
background: rgba(var(--vp-c-brand-rgb), 0.1); color: var(--vp-c-brand); font-size: 0.75rem;
}
.latency.miss { background: rgba(245,158,11,0.1); color: #f59e0b; font-family: var(--vp-font-family-base); }
.miss-line { opacity: 0.5; }
.metrics { display: flex; flex-direction: column; gap: 0.75rem; }
.metric { display: flex; align-items: center; gap: 0.75rem; }
.metric-label { min-width: 140px; font-size: 0.85rem; font-weight: 600; }
.metric-bar {
flex: 1; height: 20px; background: var(--vp-c-bg); border-radius: 4px;
border: 1px solid var(--vp-c-divider); overflow: hidden;
}
.bar-fill {
height: 100%; background: var(--vp-c-brand); border-radius: 3px;
transition: width 0.5s ease;
}
.metric-value { min-width: 80px; font-size: 0.85rem; font-family: var(--vp-font-family-mono); text-align: right; }
@media (max-width: 640px) {
.diagram { flex-direction: column; }
.path-line::before, .path-line::after { content: '↓'; }
.metric { flex-direction: column; align-items: flex-start; gap: 0.25rem; }
.metric-label { min-width: auto; }
.metric-value { min-width: auto; }
}
</style>
@@ -0,0 +1,114 @@
<!--
StorageTypeDemo.vue (file-storage)
文件存储类型对比演示
-->
<template>
<div class="storage-type-demo">
<div class="header">
<div class="title">存储类型对比</div>
<div class="subtitle">点击查看不同存储方式的特点</div>
</div>
<div class="type-cards">
<div
v-for="t in types"
:key="t.key"
:class="['type-card', { active: selected === t.key }]"
@click="selected = t.key"
>
<div class="type-icon">{{ t.icon }}</div>
<div class="type-name">{{ t.name }}</div>
</div>
</div>
<div v-if="current" class="detail">
<div class="detail-title">{{ current.name }}</div>
<div class="detail-desc">{{ current.desc }}</div>
<div class="detail-grid">
<div class="detail-item">
<div class="item-label">访问方式</div>
<div class="item-value">{{ current.access }}</div>
</div>
<div class="detail-item">
<div class="item-label">典型场景</div>
<div class="item-value">{{ current.scenario }}</div>
</div>
<div class="detail-item">
<div class="item-label">代表产品</div>
<div class="item-value">{{ current.products }}</div>
</div>
<div class="detail-item">
<div class="item-label">扩展性</div>
<div class="item-value">{{ current.scalability }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selected = ref('object')
const types = [
{
key: 'block', icon: '🧱', name: '块存储',
desc: '将数据切分为固定大小的"块",像硬盘一样提供原始存储空间。操作系统可以在上面创建文件系统。性能最高,但不能直接通过网络共享。',
access: 'iSCSI / FC 协议,挂载为磁盘设备',
scenario: '数据库存储、虚拟机磁盘',
products: 'AWS EBS、阿里云云盘、Ceph RBD',
scalability: '单卷有容量上限,需要手动扩容'
},
{
key: 'file', icon: '📁', name: '文件存储',
desc: '提供传统的文件系统接口(目录 + 文件),支持多台服务器同时挂载和读写。就像一个网络共享文件夹。',
access: 'NFS / SMB / CIFS 协议,挂载为目录',
scenario: '共享配置文件、CMS 媒体文件、日志收集',
products: 'AWS EFS、阿里云 NAS、NFS Server',
scalability: '容量可弹性伸缩,但性能受限于协议开销'
},
{
key: 'object', icon: '☁️', name: '对象存储',
desc: '通过 HTTP API 存取文件(对象),每个对象有唯一 Key。扁平结构,无目录层级。容量几乎无限,成本最低,是互联网应用的首选。',
access: 'HTTP/HTTPS RESTful APIPUT/GET/DELETE',
scenario: '图片、视频、备份、静态网站托管、数据湖',
products: 'AWS S3、阿里云 OSS、MinIO、Cloudflare R2',
scalability: '近乎无限扩展,自动分布式存储'
}
]
const current = computed(() => types.find(t => t.key === selected.value))
</script>
<style scoped>
.storage-type-demo {
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
}
.header { margin-bottom: 1rem; }
.title { font-weight: 700; font-size: 1.1rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.type-cards { display: flex; gap: 0.75rem; margin-bottom: 1rem; }
.type-card {
flex: 1; padding: 1rem; border-radius: 10px; background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider); cursor: pointer; text-align: center; transition: all 0.2s;
}
.type-card:hover { border-color: var(--vp-c-brand); }
.type-card.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
.type-icon { font-size: 2rem; margin-bottom: 0.5rem; }
.type-name { font-weight: 700; font-size: 0.95rem; }
.detail {
padding: 1rem; border-radius: 10px; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
}
.detail-title { font-weight: 700; font-size: 1rem; margin-bottom: 0.5rem; }
.detail-desc { font-size: 0.9rem; color: var(--vp-c-text-2); line-height: 1.6; margin-bottom: 1rem; }
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
.detail-item { padding: 0.5rem 0.75rem; background: var(--vp-c-bg-soft); border-radius: 6px; }
.item-label { font-weight: 600; font-size: 0.8rem; color: var(--vp-c-text-3); margin-bottom: 0.25rem; }
.item-value { font-size: 0.85rem; color: var(--vp-c-text-2); line-height: 1.5; }
@media (max-width: 640px) {
.type-cards { flex-direction: column; }
.detail-grid { grid-template-columns: 1fr; }
}
</style>
@@ -0,0 +1,135 @@
<!--
FileUploadFlowDemo.vue
文件上传流程演示直传 vs 服务端中转
-->
<template>
<div class="upload-flow-demo">
<div class="header">
<div class="title">文件上传方式对比</div>
<div class="subtitle">点击切换查看两种上传方式的流程差异</div>
</div>
<div class="mode-tabs">
<button
:class="['tab', { active: mode === 'proxy' }]"
@click="mode = 'proxy'; reset()"
>服务端中转</button>
<button
:class="['tab', { active: mode === 'direct' }]"
@click="mode = 'direct'; reset()"
>客户端直传</button>
</div>
<div class="flow-steps">
<div
v-for="(step, i) in currentSteps"
:key="i"
:class="['step', { active: currentStep === i, done: currentStep > i }]"
>
<div class="step-num">{{ i + 1 }}</div>
<div class="step-content">
<div class="step-title">{{ step.title }}</div>
<div class="step-desc">{{ step.desc }}</div>
<div v-if="step.note" class="step-note">{{ step.note }}</div>
</div>
</div>
</div>
<button class="play-btn" @click="playFlow" :disabled="playing">
{{ playing ? '演示中...' : '播放流程' }}
</button>
<div :class="['verdict', mode]" v-if="currentStep >= currentSteps.length">
<template v-if="mode === 'proxy'">
服务端中转文件经过你的服务器占用带宽和内存大文件容易超时
</template>
<template v-else>
客户端直传文件直接上传到 OSS服务器只负责签发凭证高效且省资源
</template>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const mode = ref('proxy')
const currentStep = ref(-1)
const playing = ref(false)
const proxySteps = [
{ title: '客户端 → 服务器', desc: '用户选择文件,上传到你的后端服务器', note: '大文件会占用服务器带宽和内存' },
{ title: '服务器接收文件', desc: '后端将文件暂存到本地磁盘或内存', note: '可能触发 Nginx 的 body size 限制' },
{ title: '服务器 → OSS', desc: '后端再将文件转发到对象存储', note: '文件传输了两次,效率低' },
{ title: 'OSS 返回 URL', desc: '对象存储返回文件的访问地址', note: '' },
{ title: '服务器 → 客户端', desc: '后端将文件 URL 返回给前端', note: '' }
]
const directSteps = [
{ title: '客户端 → 服务器', desc: '前端请求一个临时上传凭证(Pre-signed URL', note: '只传少量 JSON 数据,毫秒级' },
{ title: '服务器签发凭证', desc: '后端用 OSS SDK 生成带签名的临时上传 URL', note: '凭证有效期通常 5-15 分钟' },
{ title: '客户端 → OSS', desc: '前端直接将文件上传到对象存储', note: '文件不经过你的服务器,节省带宽' },
{ title: 'OSS 回调通知', desc: '上传完成后 OSS 回调你的服务器确认', note: '服务器记录文件元信息到数据库' }
]
const currentSteps = computed(() => mode.value === 'proxy' ? proxySteps : directSteps)
function reset() {
currentStep.value = -1
playing.value = false
}
async function playFlow() {
reset()
playing.value = true
for (let i = 0; i < currentSteps.value.length; i++) {
currentStep.value = i
await new Promise(r => setTimeout(r, 800))
}
currentStep.value = currentSteps.value.length
playing.value = false
}
</script>
<style scoped>
.upload-flow-demo {
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
}
.header { margin-bottom: 1rem; }
.title { font-weight: 700; font-size: 1.1rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.mode-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.tab {
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
}
.tab.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.flow-steps { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
.step {
display: flex; gap: 0.75rem; padding: 0.6rem 0.75rem; border-radius: 8px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); transition: all 0.3s;
}
.step.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
.step.done { border-color: #22c55e; background: rgba(34,197,94,0.03); }
.step-num {
width: 28px; height: 28px; border-radius: 50%; background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider); display: flex; align-items: center;
justify-content: center; font-weight: 700; font-size: 0.8rem; flex-shrink: 0;
}
.step.active .step-num { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.step.done .step-num { border-color: #22c55e; color: #22c55e; }
.step-title { font-weight: 600; font-size: 0.9rem; }
.step-desc { font-size: 0.8rem; color: var(--vp-c-text-2); }
.step-note { font-size: 0.75rem; color: var(--vp-c-text-3); font-style: italic; margin-top: 0.2rem; }
.play-btn {
padding: 0.5rem 1.5rem; border-radius: 6px; border: none;
background: var(--vp-c-brand); color: #fff; cursor: pointer; font-size: 0.9rem;
}
.play-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.verdict {
margin-top: 1rem; padding: 0.75rem; border-radius: 8px; font-size: 0.9rem;
}
.verdict.proxy { background: rgba(245,158,11,0.08); border: 1px solid rgba(245,158,11,0.3); }
.verdict.direct { background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.3); }
</style>
@@ -0,0 +1,551 @@
<template>
<div class="architecture-comparison-demo">
<div class="demo-header">
<span class="icon">🏗</span>
<span class="title">前后端项目架构对比</span>
<span class="subtitle">点击切换查看不同架构层次</span>
</div>
<!-- 切换按钮 -->
<div class="toggle-buttons">
<button
:class="['toggle-btn', { active: activeType === 'frontend' }]"
@click="activeType = 'frontend'"
>
<span class="btn-icon">🎨</span>
前端架构
</button>
<button
:class="['toggle-btn', { active: activeType === 'backend' }]"
@click="activeType = 'backend'"
>
<span class="btn-icon"></span>
后端架构
</button>
</div>
<!-- 架构展示 -->
<div class="architecture-display">
<!-- 前端架构 -->
<div v-if="activeType === 'frontend'" class="architecture-layers">
<div
v-for="(layer, index) in frontendLayers"
:key="layer.id"
class="layer-box"
:class="[layer.class, { active: activeLayer === layer.id }]"
:style="{ animationDelay: `${index * 0.1}s` }"
@click="setActiveLayer(layer.id)"
>
<div class="layer-header">
<span class="layer-icon">{{ layer.icon }}</span>
<span class="layer-name">{{ layer.name }}</span>
<span class="layer-badge">{{ layer.badge }}</span>
</div>
<div class="layer-content">
<div class="duty">{{ layer.duty }}</div>
<div class="example">🌰 {{ layer.example }}</div>
</div>
<div v-if="index < frontendLayers.length - 1" class="layer-arrow">
<span class="arrow-icon"></span>
<span class="arrow-text">{{ layer.arrow }}</span>
</div>
</div>
</div>
<!-- 后端架构 -->
<div v-else class="architecture-layers">
<div
v-for="(layer, index) in backendLayers"
:key="layer.id"
class="layer-box"
:class="[layer.class, { active: activeLayer === layer.id }]"
:style="{ animationDelay: `${index * 0.1}s` }"
@click="setActiveLayer(layer.id)"
>
<div class="layer-header">
<span class="layer-icon">{{ layer.icon }}</span>
<span class="layer-name">{{ layer.name }}</span>
<span class="layer-badge">{{ layer.badge }}</span>
</div>
<div class="layer-content">
<div class="duty">{{ layer.duty }}</div>
<div class="example">🌰 {{ layer.example }}</div>
</div>
<div v-if="index < backendLayers.length - 1" class="layer-arrow">
<span class="arrow-icon"></span>
<span class="arrow-text">{{ layer.arrow }}</span>
</div>
</div>
</div>
</div>
<!-- 详情面板 -->
<Transition name="slide">
<div v-if="currentLayer" class="detail-panel">
<div class="detail-header">
<span class="detail-icon">{{ currentLayer.icon }}</span>
<span class="detail-title">{{ currentLayer.name }}</span>
</div>
<div class="detail-content">
<div class="detail-section">
<div class="section-title">📁 典型文件</div>
<div class="file-list">
<code v-for="file in currentLayer.files" :key="file" class="file-tag">{{ file }}</code>
</div>
</div>
<div class="detail-section">
<div class="section-title"> 设计原则</div>
<ul class="principle-list">
<li v-for="principle in currentLayer.principles" :key="principle">{{ principle }}</li>
</ul>
</div>
</div>
</div>
</Transition>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>好的架构就像整理好的空间前端像衣柜按功能分类展示后端像厨房按流程分工协作点击上方层次查看详情
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeType = ref('frontend')
const activeLayer = ref(null)
const frontendLayers = [
{
id: 'views',
name: 'Views / Pages',
icon: '📄',
badge: '页面层',
class: 'views-layer',
duty: '职责:页面组件,对应路由',
example: 'Home.vue、UserProfile.vue',
arrow: '组合',
files: ['Home/index.vue', 'User/Profile.vue', 'pages/about.tsx'],
principles: ['保持"薄",逻辑下沉到 hooks', '页面级状态管理', '路由懒加载']
},
{
id: 'components',
name: 'Components',
icon: '🧩',
badge: '组件层',
class: 'components-layer',
duty: '职责:可复用的 UI 组件',
example: 'Button.vue、Modal.vue、UserCard.vue',
arrow: '调用',
files: ['common/Button/', 'business/UserCard/', 'layout/Header/'],
principles: ['单一职责,一个组件只做一件事', 'Props 清晰可预测', '样式隔离(scoped/css-modules']
},
{
id: 'hooks',
name: 'Hooks / Composables',
icon: '🎣',
badge: '逻辑层',
class: 'hooks-layer',
duty: '职责:可复用的业务逻辑',
example: 'useAuth()、useLoading()、useForm()',
arrow: '使用',
files: ['useAuth.js', 'usePagination.ts', 'composables/useFetch.js'],
principles: ['纯函数优先', '单一功能,便于测试', '命名以 use 开头']
},
{
id: 'services',
name: 'Services / API',
icon: '🌐',
badge: '服务层',
class: 'services-layer',
duty: '职责:API 调用,数据获取',
example: 'userApi.getProfile()、orderApi.create()',
arrow: '请求',
files: ['services/user.js', 'api/request.ts', 'clients/http.js'],
principles: ['统一错误处理', '请求/响应拦截', '接口统一管理']
},
{
id: 'utils',
name: 'Utils / Helpers',
icon: '🛠️',
badge: '工具层',
class: 'utils-layer',
duty: '职责:通用工具函数',
example: 'formatDate()、storage.set()、validator.email()',
arrow: '',
files: ['utils/format.js', 'helpers/storage.ts', 'lib/validator.js'],
principles: ['纯函数,无副作用', '单一职责', '完善的 JSDoc 注释']
}
]
const backendLayers = [
{
id: 'controller',
name: 'Controller',
icon: '🎮',
badge: '入口层',
class: 'controller-layer',
duty: '职责:接收 HTTP 请求,返回响应',
example: 'UserController.getById()、OrderController.create()',
arrow: '调用',
files: ['userController.js', 'routes/api.js', 'handlers/order.ts'],
principles: ['只处理 HTTP 相关逻辑', '参数校验', '不直接操作数据库']
},
{
id: 'service',
name: 'Service',
icon: '⚙️',
badge: '业务层',
class: 'service-layer',
duty: '职责:核心业务逻辑,事务管理',
example: 'UserService.createUser()、OrderService.process()',
arrow: '调用',
files: ['userService.js', 'services/order.ts', 'business/user.js'],
principles: ['包含核心业务规则', '协调多个 Repository', '管理事务边界']
},
{
id: 'repository',
name: 'Repository',
icon: '🗄️',
badge: '数据层',
class: 'repository-layer',
duty: '职责:数据持久化,数据库操作',
example: 'UserRepository.findById()、OrderRepository.save()',
arrow: '查询',
files: ['userRepository.js', 'dao/order.ts', 'models/user.js'],
principles: ['只负责数据存取', 'ORM 封装', '不包含业务逻辑']
},
{
id: 'model',
name: 'Model / Entity',
icon: '📊',
badge: '模型层',
class: 'model-layer',
duty: '职责:数据结构和业务规则定义',
example: 'User 类、Order 实体、DTO 定义',
arrow: '',
files: ['models/User.js', 'entities/order.ts', 'dto/userDto.js'],
principles: ['定义数据结构', '字段验证规则', '与其他层解耦']
}
]
const currentLayer = computed(() => {
const layers = activeType.value === 'frontend' ? frontendLayers : backendLayers
return layers.find(l => l.id === activeLayer.value)
})
function setActiveLayer(id) {
activeLayer.value = activeLayer.value === id ? null : id
}
</script>
<style scoped>
.architecture-comparison-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 700px;
overflow-y: auto;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.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;
}
/* 切换按钮 */
.toggle-buttons {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.toggle-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.6rem 1rem;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
cursor: pointer;
transition: all 0.2s;
font-size: 0.9rem;
}
.toggle-btn:hover {
border-color: var(--vp-c-brand);
}
.toggle-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.btn-icon {
font-size: 1.1rem;
}
/* 架构层 */
.architecture-layers {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.layer-box {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
cursor: pointer;
transition: all 0.2s;
animation: fadeInUp 0.3s ease forwards;
opacity: 0;
transform: translateY(10px);
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
.layer-box:hover {
border-color: var(--vp-c-brand);
transform: translateX(4px);
}
.layer-box.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
/* 不同层的颜色 */
.views-layer {
border-left: 4px solid #3498db;
}
.components-layer {
border-left: 4px solid #2ecc71;
}
.hooks-layer {
border-left: 4px solid #9b59b6;
}
.services-layer {
border-left: 4px solid #e67e22;
}
.utils-layer {
border-left: 4px solid #95a5a6;
}
.controller-layer {
border-left: 4px solid #3498db;
}
.service-layer {
border-left: 4px solid #2ecc71;
}
.repository-layer {
border-left: 4px solid #e67e22;
}
.model-layer {
border-left: 4px solid #9b59b6;
}
.layer-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.layer-icon {
font-size: 1.2rem;
}
.layer-name {
font-weight: bold;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.layer-badge {
margin-left: auto;
padding: 0.15rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.layer-content {
font-size: 0.85rem;
}
.duty {
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.example {
color: var(--vp-c-text-2);
font-size: 0.8rem;
}
.layer-arrow {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 0.5rem;
padding: 0.25rem;
color: var(--vp-c-text-3);
font-size: 0.75rem;
}
.arrow-icon {
font-size: 1rem;
}
/* 详情面板 */
.detail-panel {
margin-top: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
}
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease;
}
.slide-enter-from,
.slide-leave-to {
opacity: 0;
transform: translateY(-10px);
}
.detail-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-icon {
font-size: 1.25rem;
}
.detail-title {
font-weight: bold;
font-size: 1rem;
}
.detail-section {
margin-bottom: 0.75rem;
}
.detail-section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.5rem;
}
.file-list {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.file-tag {
padding: 0.2rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.75rem;
color: var(--vp-c-text-2);
font-family: monospace;
}
.principle-list {
margin: 0;
padding-left: 1.2rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.principle-list li {
margin-bottom: 0.25rem;
}
/* 信息框 */
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 1rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
/* 响应式 */
@media (max-width: 640px) {
.toggle-btn {
font-size: 0.8rem;
padding: 0.5rem;
}
.layer-name {
font-size: 0.85rem;
}
.duty, .example {
font-size: 0.75rem;
}
}
</style>
@@ -0,0 +1,173 @@
<!--
BackpressureDemo.vue
背压控制演示展示生产者速度 > 消费者速度时的处理策略
-->
<template>
<div class="backpressure-demo">
<div class="header">
<div class="title">背压控制 (Backpressure)</div>
<div class="subtitle">当生产速度超过消费速度时会发生什么</div>
</div>
<div class="controls">
<div class="speed-control">
<span class="ctrl-label">生产速率</span>
<input type="range" min="1" max="10" v-model.number="produceRate" />
<span class="ctrl-value">{{ produceRate }}/s</span>
</div>
<div class="speed-control">
<span class="ctrl-label">消费速率</span>
<input type="range" min="1" max="10" v-model.number="consumeRate" />
<span class="ctrl-value">{{ consumeRate }}/s</span>
</div>
<div class="btn-group">
<button class="ctrl-btn primary" @click="start" :disabled="running">开始</button>
<button class="ctrl-btn" @click="stop">停止</button>
</div>
</div>
<div class="buffer-visual">
<div class="producer-side">
<div class="side-label">生产者</div>
<div class="rate-indicator" :class="{ fast: produceRate > consumeRate }">
{{ produceRate }}/s
</div>
</div>
<div class="buffer-section">
<div class="buffer-label">缓冲区 ({{ bufferSize }}/{{ maxBuffer }})</div>
<div class="buffer-bar">
<div
class="buffer-fill"
:style="{ width: (bufferSize / maxBuffer * 100) + '%' }"
:class="bufferLevel"
></div>
</div>
<div class="buffer-status" :class="bufferLevel">{{ statusText }}</div>
</div>
<div class="consumer-side">
<div class="side-label">消费者</div>
<div class="rate-indicator">{{ consumeRate }}/s</div>
</div>
</div>
<div class="strategies">
<div class="strat-title">背压处理策略</div>
<div class="strat-grid">
<div v-for="s in strategies" :key="s.name" class="strat-card">
<div class="strat-name">{{ s.name }}</div>
<div class="strat-desc">{{ s.desc }}</div>
<div class="strat-example">{{ s.example }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue'
const produceRate = ref(6)
const consumeRate = ref(3)
const bufferSize = ref(0)
const maxBuffer = 20
const running = ref(false)
let timer = null
const bufferLevel = computed(() => {
const ratio = bufferSize.value / maxBuffer
if (ratio >= 0.9) return 'critical'
if (ratio >= 0.6) return 'warning'
return 'normal'
})
const statusText = computed(() => {
const ratio = bufferSize.value / maxBuffer
if (ratio >= 1) return '缓冲区溢出!数据丢失'
if (ratio >= 0.8) return '即将溢出,需要背压控制'
if (ratio >= 0.5) return '缓冲区压力较大'
return '正常运行'
})
function start() {
running.value = true
timer = setInterval(() => {
const produced = produceRate.value
const consumed = consumeRate.value
bufferSize.value = Math.max(0, Math.min(maxBuffer, bufferSize.value + produced - consumed))
}, 1000)
}
function stop() {
running.value = false
if (timer) clearInterval(timer)
timer = null
bufferSize.value = 0
}
const strategies = [
{ name: '丢弃策略', desc: '缓冲区满时直接丢弃新数据', example: '如:日志采集、实时监控指标' },
{ name: '阻塞策略', desc: '缓冲区满时让生产者等待', example: '如:Go channel、Java BlockingQueue' },
{ name: '采样策略', desc: '只处理部分数据,跳过其余', example: '如:高频传感器数据降采样' },
{ name: '弹性扩容', desc: '动态增加消费者数量', example: '如:K8s HPA 自动扩缩容' }
]
onUnmounted(() => { if (timer) clearInterval(timer) })
</script>
<style scoped>
.backpressure-demo {
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
}
.header { margin-bottom: 1rem; }
.title { font-weight: 700; font-size: 1.1rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.controls { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; margin-bottom: 1.5rem; }
.speed-control { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; }
.ctrl-label { font-weight: 600; min-width: 70px; }
.ctrl-value { font-family: var(--vp-font-family-mono); min-width: 30px; }
.btn-group { display: flex; gap: 0.5rem; }
.ctrl-btn {
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
}
.ctrl-btn.primary { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
.ctrl-btn:disabled { opacity: 0.5; }
.buffer-visual { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; }
.producer-side, .consumer-side { text-align: center; min-width: 80px; }
.side-label { font-weight: 600; font-size: 0.85rem; margin-bottom: 0.25rem; }
.rate-indicator {
padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.8rem;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
font-family: var(--vp-font-family-mono);
}
.rate-indicator.fast { border-color: #ef4444; color: #ef4444; }
.buffer-section { flex: 1; }
.buffer-label { font-size: 0.8rem; color: var(--vp-c-text-2); margin-bottom: 0.25rem; }
.buffer-bar {
height: 24px; background: var(--vp-c-bg); border-radius: 6px;
border: 1px solid var(--vp-c-divider); overflow: hidden;
}
.buffer-fill { height: 100%; border-radius: 5px; transition: width 0.5s, background 0.3s; }
.buffer-fill.normal { background: #22c55e; }
.buffer-fill.warning { background: #f59e0b; }
.buffer-fill.critical { background: #ef4444; }
.buffer-status { font-size: 0.8rem; margin-top: 0.25rem; }
.buffer-status.normal { color: #22c55e; }
.buffer-status.warning { color: #f59e0b; }
.buffer-status.critical { color: #ef4444; }
.strat-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 0.5rem; }
.strat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.5rem; }
.strat-card {
padding: 0.75rem; border-radius: 8px; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
}
.strat-name { font-weight: 700; font-size: 0.85rem; margin-bottom: 0.25rem; }
.strat-desc { font-size: 0.8rem; color: var(--vp-c-text-2); margin-bottom: 0.25rem; }
.strat-example { font-size: 0.75rem; color: var(--vp-c-text-3); }
@media (max-width: 640px) {
.buffer-visual { flex-direction: column; }
.strat-grid { grid-template-columns: 1fr; }
}
</style>
@@ -0,0 +1,219 @@
<!--
RateLimitAlgorithmDemo.vue
限流算法演示令牌桶漏桶滑动窗口
-->
<template>
<div class="rate-limit-demo">
<div class="header">
<div class="title">限流算法对比</div>
<div class="subtitle">选择算法点击"发送请求"观察效果</div>
</div>
<div class="algo-tabs">
<button
v-for="a in algorithms"
:key="a.key"
:class="['tab', { active: algo === a.key }]"
@click="algo = a.key; reset()"
>{{ a.label }}</button>
</div>
<div class="sim-area">
<div class="controls">
<button class="send-btn" @click="sendRequest">发送请求</button>
<button class="burst-btn" @click="burstRequests">突发 10 个请求</button>
<button class="reset-btn" @click="reset">重置</button>
</div>
<div class="stats">
<div class="stat">
<span class="stat-label">通过</span>
<span class="stat-value ok">{{ passed }}</span>
</div>
<div class="stat">
<span class="stat-label">拒绝</span>
<span class="stat-value reject">{{ rejected }}</span>
</div>
<div class="stat" v-if="algo === 'token'">
<span class="stat-label">剩余令牌</span>
<span class="stat-value">{{ tokens }}</span>
</div>
<div class="stat" v-if="algo === 'leaky'">
<span class="stat-label">桶中排队</span>
<span class="stat-value">{{ bucketQueue }}</span>
</div>
<div class="stat" v-if="algo === 'sliding'">
<span class="stat-label">窗口内请求</span>
<span class="stat-value">{{ windowCount }}</span>
</div>
</div>
<div class="log-area">
<div
v-for="(log, i) in logs.slice(-8)"
:key="i"
:class="['log-item', log.status]"
>
<span class="log-time">{{ log.time }}</span>
<span>{{ log.msg }}</span>
</div>
</div>
</div>
<div class="algo-info">
<div class="info-name">{{ currentAlgo.label }}</div>
<div class="info-desc">{{ currentAlgo.desc }}</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const algo = ref('token')
const passed = ref(0)
const rejected = ref(0)
const tokens = ref(5)
const bucketQueue = ref(0)
const windowCount = ref(0)
const logs = ref([])
const algorithms = [
{ key: 'token', label: '令牌桶', desc: '以固定速率往桶里放令牌,每个请求消耗一个令牌。桶满时多余令牌丢弃。允许一定程度的突发流量(桶里有存量令牌时)。' },
{ key: 'leaky', label: '漏桶', desc: '请求先进入桶中排队,以固定速率从桶底"漏出"处理。桶满时新请求被拒绝。输出速率恒定,完全平滑流量。' },
{ key: 'sliding', label: '滑动窗口', desc: '统计最近 N 秒内的请求数,超过阈值则拒绝。比固定窗口更精确,避免窗口边界的突发问题。' }
]
const currentAlgo = computed(() => algorithms.find(a => a.key === algo.value))
// Token bucket: refill 1 token per second, max 5
let tokenTimer = null
function startTokenRefill() {
if (tokenTimer) clearInterval(tokenTimer)
tokenTimer = setInterval(() => {
if (tokens.value < 5) tokens.value++
}, 1000)
}
// Leaky bucket: drain 1 per second, max queue 5
let leakyTimer = null
function startLeakyDrain() {
if (leakyTimer) clearInterval(leakyTimer)
leakyTimer = setInterval(() => {
if (bucketQueue.value > 0) {
bucketQueue.value--
passed.value++
addLog('ok', '漏桶处理了一个排队请求')
}
}, 1000)
}
// Sliding window: max 5 per 3 seconds
const windowRequests = ref([])
function reset() {
passed.value = 0
rejected.value = 0
tokens.value = 5
bucketQueue.value = 0
windowCount.value = 0
logs.value = []
windowRequests.value = []
if (tokenTimer) clearInterval(tokenTimer)
if (leakyTimer) clearInterval(leakyTimer)
if (algo.value === 'token') startTokenRefill()
if (algo.value === 'leaky') startLeakyDrain()
}
function addLog(status, msg) {
const now = new Date()
logs.value.push({ status, msg, time: now.toLocaleTimeString() })
}
function sendRequest() {
if (algo.value === 'token') {
if (tokens.value > 0) {
tokens.value--
passed.value++
addLog('ok', `请求通过(剩余令牌: ${tokens.value}`)
} else {
rejected.value++
addLog('reject', '令牌不足,请求被拒绝 (429)')
}
if (!tokenTimer) startTokenRefill()
} else if (algo.value === 'leaky') {
if (bucketQueue.value < 5) {
bucketQueue.value++
addLog('ok', `请求进入排队(队列: ${bucketQueue.value}/5`)
} else {
rejected.value++
addLog('reject', '桶已满,请求被拒绝 (429)')
}
if (!leakyTimer) startLeakyDrain()
} else {
const now = Date.now()
windowRequests.value = windowRequests.value.filter(t => now - t < 3000)
windowCount.value = windowRequests.value.length
if (windowCount.value < 5) {
windowRequests.value.push(now)
windowCount.value++
passed.value++
addLog('ok', `请求通过(窗口内: ${windowCount.value}/5`)
} else {
rejected.value++
addLog('reject', '窗口内请求数超限 (429)')
}
}
}
function burstRequests() {
for (let i = 0; i < 10; i++) {
setTimeout(() => sendRequest(), i * 80)
}
}
reset()
</script>
<style scoped>
.rate-limit-demo {
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
}
.header { margin-bottom: 1rem; }
.title { font-weight: 700; font-size: 1.1rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.algo-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.tab {
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
}
.tab.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.controls { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; }
.send-btn, .burst-btn, .reset-btn {
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
}
.send-btn { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
.burst-btn { background: #f59e0b; color: #fff; border-color: #f59e0b; }
.stats { display: flex; gap: 1rem; margin-bottom: 0.75rem; }
.stat { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; }
.stat-label { color: var(--vp-c-text-3); }
.stat-value { font-weight: 700; font-family: var(--vp-font-family-mono); }
.stat-value.ok { color: #22c55e; }
.stat-value.reject { color: #ef4444; }
.log-area { max-height: 180px; overflow-y: auto; display: flex; flex-direction: column; gap: 0.25rem; }
.log-item {
padding: 0.3rem 0.5rem; border-radius: 4px; font-size: 0.8rem;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
}
.log-item.ok { border-color: rgba(34,197,94,0.3); }
.log-item.reject { border-color: rgba(239,68,68,0.3); background: rgba(239,68,68,0.03); }
.log-time { color: var(--vp-c-text-3); margin-right: 0.5rem; font-family: var(--vp-font-family-mono); }
.algo-info {
margin-top: 1rem; padding: 0.75rem; border-radius: 8px;
background: rgba(var(--vp-c-brand-rgb), 0.05); border: 1px solid var(--vp-c-brand);
}
.info-name { font-weight: 700; font-size: 0.9rem; margin-bottom: 0.25rem; }
.info-desc { font-size: 0.85rem; color: var(--vp-c-text-2); line-height: 1.6; }
</style>
@@ -0,0 +1,204 @@
<!--
RateLimiterDemo.vue
限流算法演示令牌桶 vs 滑动窗口
-->
<template>
<div class="rate-limiter-demo">
<div class="header">
<div class="title">限流算法可视化</div>
<div class="subtitle">选择算法点击发送请求观察限流效果</div>
</div>
<div class="algo-tabs">
<button
v-for="a in algorithms"
:key="a.key"
:class="['tab', { active: algo === a.key }]"
@click="algo = a.key; reset()"
>{{ a.label }}</button>
</div>
<div class="sim-area">
<div class="controls">
<button class="send-btn" @click="sendRequest">发送请求</button>
<button class="burst-btn" @click="sendBurst">模拟突发 (10个)</button>
<button class="reset-btn" @click="reset">重置</button>
</div>
<div class="stats">
<div class="stat">
<span class="stat-label">已发送</span>
<span class="stat-value">{{ totalSent }}</span>
</div>
<div class="stat">
<span class="stat-label">通过</span>
<span class="stat-value pass">{{ passed }}</span>
</div>
<div class="stat">
<span class="stat-label">拒绝</span>
<span class="stat-value reject">{{ rejected }}</span>
</div>
<div class="stat" v-if="algo === 'token'">
<span class="stat-label">剩余令牌</span>
<span class="stat-value">{{ tokens }}</span>
</div>
</div>
<div class="request-log">
<div
v-for="(req, i) in recentRequests"
:key="i"
:class="['req-item', req.status]"
>
<span>{{ req.status === 'pass' ? '✅' : '❌' }}</span>
<span>请求 #{{ req.id }}</span>
<span class="req-time">{{ req.time }}</span>
</div>
</div>
</div>
<div class="algo-info">
<div class="info-name">{{ currentAlgo.label }}</div>
<div class="info-desc">{{ currentAlgo.desc }}</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const algo = ref('token')
const totalSent = ref(0)
const passed = ref(0)
const rejected = ref(0)
const tokens = ref(5)
const recentRequests = ref([])
const algorithms = [
{ key: 'token', label: '令牌桶', desc: '以固定速率往桶里放令牌,每个请求消耗一个令牌。桶满时多余令牌丢弃。允许一定程度的突发流量(桶里有存量令牌时)。' },
{ key: 'sliding', label: '滑动窗口', desc: '在一个滑动的时间窗口内统计请求数,超过阈值则拒绝。比固定窗口更平滑,避免窗口边界的突发问题。' },
{ key: 'leaky', label: '漏桶', desc: '请求先进入桶中排队,以固定速率流出处理。无论请求多快到达,处理速率恒定。适合需要严格匀速的场景。' }
]
const currentAlgo = computed(() => algorithms.find(a => a.key === algo.value))
// Sliding window state
const windowRequests = ref([])
const WINDOW_SIZE = 3000 // 3s window
const WINDOW_LIMIT = 5
// Token bucket refill
let tokenInterval = null
function startTokenRefill() {
if (tokenInterval) clearInterval(tokenInterval)
tokenInterval = setInterval(() => {
if (tokens.value < 5) tokens.value++
}, 1000)
}
function reset() {
totalSent.value = 0
passed.value = 0
rejected.value = 0
tokens.value = 5
recentRequests.value = []
windowRequests.value = []
if (tokenInterval) clearInterval(tokenInterval)
startTokenRefill()
}
function checkLimit() {
if (algo.value === 'token') {
if (tokens.value > 0) { tokens.value--; return true }
return false
}
if (algo.value === 'sliding') {
const now = Date.now()
windowRequests.value = windowRequests.value.filter(t => now - t < WINDOW_SIZE)
if (windowRequests.value.length < WINDOW_LIMIT) {
windowRequests.value.push(now)
return true
}
return false
}
// leaky bucket: simple counter-based
if (algo.value === 'leaky') {
const now = Date.now()
windowRequests.value = windowRequests.value.filter(t => now - t < 2000)
if (windowRequests.value.length < 3) {
windowRequests.value.push(now)
return true
}
return false
}
return true
}
function sendRequest() {
totalSent.value++
const allowed = checkLimit()
if (allowed) passed.value++
else rejected.value++
const now = new Date()
recentRequests.value.unshift({
id: totalSent.value,
status: allowed ? 'pass' : 'reject',
time: `${now.getHours()}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`
})
if (recentRequests.value.length > 10) recentRequests.value.pop()
}
async function sendBurst() {
for (let i = 0; i < 10; i++) {
sendRequest()
await new Promise(r => setTimeout(r, 100))
}
}
startTokenRefill()
</script>
<style scoped>
.rate-limiter-demo {
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
}
.header { margin-bottom: 1rem; }
.title { font-weight: 700; font-size: 1.1rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.algo-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.tab {
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
}
.tab.active { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.controls { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
.send-btn, .burst-btn, .reset-btn {
padding: 0.4rem 0.8rem; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); cursor: pointer; font-size: 0.85rem;
}
.send-btn { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
.burst-btn { background: #f59e0b; color: #fff; border-color: #f59e0b; }
.stats { display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; }
.stat { display: flex; flex-direction: column; align-items: center; }
.stat-label { font-size: 0.75rem; color: var(--vp-c-text-3); }
.stat-value { font-weight: 700; font-size: 1.2rem; font-family: var(--vp-font-family-mono); }
.stat-value.pass { color: #22c55e; }
.stat-value.reject { color: #ef4444; }
.request-log { display: flex; flex-direction: column; gap: 0.25rem; max-height: 200px; overflow-y: auto; margin-bottom: 1rem; }
.req-item {
display: flex; gap: 0.5rem; padding: 0.3rem 0.5rem; border-radius: 4px;
font-size: 0.8rem; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
}
.req-item.reject { border-color: rgba(239,68,68,0.3); background: rgba(239,68,68,0.03); }
.req-item.pass { border-color: rgba(34,197,94,0.3); background: rgba(34,197,94,0.03); }
.req-time { margin-left: auto; color: var(--vp-c-text-3); font-family: var(--vp-font-family-mono); }
.algo-info {
padding: 0.75rem; border-radius: 8px;
background: rgba(var(--vp-c-brand-rgb), 0.05); border: 1px solid var(--vp-c-brand);
}
.info-name { font-weight: 700; font-size: 0.9rem; margin-bottom: 0.25rem; }
.info-desc { font-size: 0.85rem; color: var(--vp-c-text-2); line-height: 1.6; }
</style>
@@ -0,0 +1,145 @@
<!--
InvertedIndexDemo.vue
倒排索引演示展示搜索引擎的核心数据结构
-->
<template>
<div class="inverted-index-demo">
<div class="header">
<div class="title">倒排索引 (Inverted Index)</div>
<div class="subtitle">输入搜索词观察倒排索引如何工作</div>
</div>
<div class="search-box">
<input
v-model="query"
placeholder="试试搜索:苹果、手机、水果..."
class="search-input"
@input="search"
/>
</div>
<div class="index-layout">
<div class="docs-section">
<div class="section-title">原始文档</div>
<div
v-for="doc in docs"
:key="doc.id"
:class="['doc-card', { highlight: matchedDocs.includes(doc.id) }]"
>
<span class="doc-id">Doc {{ doc.id }}</span>
<span class="doc-text">{{ doc.text }}</span>
</div>
</div>
<div class="index-section">
<div class="section-title">倒排索引表</div>
<div class="index-table">
<div
v-for="(entry, word) in invertedIndex"
:key="word"
:class="['index-row', { highlight: matchedWords.includes(word) }]"
>
<span class="index-word">{{ word }}</span>
<span class="index-arrow"></span>
<span class="index-docs">
<span v-for="id in entry" :key="id" class="doc-ref">[{{ id }}]</span>
</span>
</div>
</div>
</div>
</div>
<div v-if="query && matchedDocs.length > 0" class="result">
命中文档{{ matchedDocs.map(id => 'Doc ' + id).join('、') }}
</div>
<div v-else-if="query" class="result no-match">
未找到匹配文档
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const query = ref('')
const matchedDocs = ref([])
const matchedWords = ref([])
const docs = [
{ id: 1, text: '苹果是一种常见的水果' },
{ id: 2, text: '苹果公司发布了新款手机' },
{ id: 3, text: '我喜欢吃水果和蔬菜' },
{ id: 4, text: '这款手机的价格很实惠' },
{ id: 5, text: '水果店里有苹果和香蕉' }
]
const invertedIndex = {
'苹果': [1, 2, 5],
'水果': [1, 3, 5],
'手机': [2, 4],
'公司': [2],
'发布': [2],
'喜欢': [3],
'蔬菜': [3],
'价格': [4],
'实惠': [4],
'香蕉': [5],
'常见': [1]
}
function search() {
const q = query.value.trim()
if (!q) {
matchedDocs.value = []
matchedWords.value = []
return
}
const words = Object.keys(invertedIndex).filter(w => q.includes(w))
matchedWords.value = words
const docSet = new Set()
words.forEach(w => invertedIndex[w].forEach(id => docSet.add(id)))
matchedDocs.value = [...docSet].sort()
}
</script>
<style scoped>
.inverted-index-demo {
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
}
.header { margin-bottom: 1rem; }
.title { font-weight: 700; font-size: 1.1rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.search-box { margin-bottom: 1rem; }
.search-input {
width: 100%; padding: 0.6rem 0.75rem; border-radius: 8px;
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg);
font-size: 0.9rem; outline: none;
}
.search-input:focus { border-color: var(--vp-c-brand); }
.index-layout { display: flex; gap: 1rem; margin-bottom: 1rem; }
.docs-section, .index-section { flex: 1; }
.section-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 0.5rem; }
.doc-card {
display: flex; gap: 0.5rem; padding: 0.4rem 0.6rem; margin-bottom: 0.25rem;
border-radius: 6px; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
font-size: 0.8rem; transition: all 0.2s;
}
.doc-card.highlight { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
.doc-id { font-weight: 700; color: var(--vp-c-brand); white-space: nowrap; }
.index-row {
display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0.5rem;
margin-bottom: 0.2rem; border-radius: 4px; font-size: 0.8rem;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
}
.index-row.highlight { border-color: #22c55e; background: rgba(34,197,94,0.05); }
.index-word { font-weight: 700; min-width: 40px; }
.index-arrow { color: var(--vp-c-text-3); }
.doc-ref {
padding: 0.1rem 0.3rem; background: var(--vp-c-bg-soft); border-radius: 3px;
font-family: var(--vp-font-family-mono); font-size: 0.75rem; margin-right: 0.2rem;
}
.result { padding: 0.5rem 0.75rem; border-radius: 6px; font-size: 0.85rem; background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.3); }
.result.no-match { background: rgba(245,158,11,0.08); border-color: rgba(245,158,11,0.3); }
@media (max-width: 640px) { .index-layout { flex-direction: column; } }
</style>
@@ -0,0 +1,126 @@
<!--
SearchRelevanceDemo.vue
搜索相关性评分演示展示 TF-IDF BM25 评分原理
-->
<template>
<div class="relevance-demo">
<div class="header">
<div class="title">搜索相关性评分</div>
<div class="subtitle">输入查询词观察不同文档的相关性得分</div>
</div>
<div class="search-box">
<input v-model="query" placeholder="输入搜索词,如:数据库" class="search-input" />
<button class="search-btn" @click="calcScores">计算得分</button>
</div>
<div v-if="results.length > 0" class="results">
<div
v-for="(r, i) in results"
:key="i"
class="result-item"
>
<div class="result-rank">#{{ i + 1 }}</div>
<div class="result-content">
<div class="result-title">{{ r.title }}</div>
<div class="result-snippet">{{ r.snippet }}</div>
</div>
<div class="result-score">
<div class="score-bar">
<div class="score-fill" :style="{ width: r.scorePercent + '%' }"></div>
</div>
<div class="score-value">{{ r.score.toFixed(2) }}</div>
</div>
</div>
</div>
<div class="scoring-info">
<div class="info-title">BM25 评分因子</div>
<div class="factor-grid">
<div class="factor">
<div class="factor-name">词频 (TF)</div>
<div class="factor-desc">关键词在文档中出现的次数越多得分越高但有上限</div>
</div>
<div class="factor">
<div class="factor-name">逆文档频率 (IDF)</div>
<div class="factor-desc">越稀有的词权重越高"的"这种常见词权重很低</div>
</div>
<div class="factor">
<div class="factor-name">文档长度</div>
<div class="factor-desc">较短文档中出现关键词比长文档中出现更有意义</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const query = ref('')
const results = ref([])
const documents = [
{ title: 'MySQL 数据库入门', snippet: '数据库是存储和管理数据的系统,MySQL 是最流行的关系型数据库之一', keywords: { '数据库': 3, '数据': 2, 'MySQL': 2, '存储': 1 } },
{ title: 'Redis 缓存设计', snippet: 'Redis 是内存数据库,常用作缓存层,提升数据读取性能', keywords: { 'Redis': 2, '缓存': 2, '数据库': 1, '数据': 1, '性能': 1 } },
{ title: 'Python 数据分析', snippet: '使用 Python 进行数据清洗、分析和可视化', keywords: { 'Python': 2, '数据': 3, '分析': 2, '可视化': 1 } },
{ title: '分布式数据库架构', snippet: '分布式数据库通过分片和复制实现高可用和水平扩展', keywords: { '分布式': 2, '数据库': 2, '分片': 1, '高可用': 1 } },
{ title: 'API 接口设计', snippet: 'RESTful API 设计规范与最佳实践', keywords: { 'API': 3, '设计': 2, 'RESTful': 1 } }
]
function calcScores() {
if (!query.value.trim()) { results.value = []; return }
const q = query.value.trim()
const scored = documents.map(doc => {
let score = 0
for (const [word, tf] of Object.entries(doc.keywords)) {
if (word.includes(q) || q.includes(word)) {
const idf = Math.log(documents.length / (1 + documents.filter(d => d.keywords[word]).length))
score += tf * (idf + 1)
}
}
return { ...doc, score }
}).filter(d => d.score > 0).sort((a, b) => b.score - a.score)
const maxScore = scored.length > 0 ? scored[0].score : 1
results.value = scored.map(r => ({ ...r, scorePercent: (r.score / maxScore) * 100 }))
}
</script>
<style scoped>
.relevance-demo {
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
}
.header { margin-bottom: 1rem; }
.title { font-weight: 700; font-size: 1.1rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.search-box { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.search-input {
flex: 1; padding: 0.5rem 0.75rem; border-radius: 6px;
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg); font-size: 0.9rem;
}
.search-btn {
padding: 0.5rem 1rem; border-radius: 6px; border: none;
background: var(--vp-c-brand); color: #fff; cursor: pointer; font-size: 0.85rem;
}
.results { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
.result-item {
display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem;
border-radius: 8px; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
}
.result-rank { font-weight: 700; font-size: 1rem; color: var(--vp-c-brand); min-width: 30px; }
.result-content { flex: 1; }
.result-title { font-weight: 600; font-size: 0.9rem; }
.result-snippet { font-size: 0.8rem; color: var(--vp-c-text-2); }
.result-score { min-width: 120px; }
.score-bar { height: 8px; background: var(--vp-c-bg-soft); border-radius: 4px; overflow: hidden; }
.score-fill { height: 100%; background: var(--vp-c-brand); border-radius: 4px; transition: width 0.3s; }
.score-value { font-size: 0.75rem; color: var(--vp-c-text-3); text-align: right; font-family: var(--vp-font-family-mono); }
.scoring-info { padding: 0.75rem; border-radius: 8px; background: rgba(var(--vp-c-brand-rgb),0.05); border: 1px solid var(--vp-c-brand); }
.info-title { font-weight: 700; font-size: 0.9rem; margin-bottom: 0.5rem; }
.factor-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.5rem; }
.factor { padding: 0.5rem; background: var(--vp-c-bg); border-radius: 6px; }
.factor-name { font-weight: 600; font-size: 0.85rem; margin-bottom: 0.2rem; }
.factor-desc { font-size: 0.75rem; color: var(--vp-c-text-2); line-height: 1.5; }
</style>
+52
View File
@@ -206,7 +206,11 @@ import LearningStrategyDemo from './components/appendix/computer-fundamentals/Le
import VibeCodingFlowDemo from './components/appendix/computer-fundamentals/VibeCodingFlowDemo.vue'
import PowerOnDemo from './components/appendix/computer-fundamentals/PowerOnDemo.vue'
import BootProcessDemo from './components/appendix/computer-fundamentals/BootProcessDemo.vue'
import BiosUefiDemo from './components/appendix/computer-fundamentals/BiosUefiDemo.vue'
import BiosUefiInteractiveDemo from './components/appendix/computer-fundamentals/BiosUefiInteractiveDemo.vue'
import AppLaunchDemo from './components/appendix/computer-fundamentals/AppLaunchDemo.vue'
import DesktopDemo from './components/appendix/computer-fundamentals/DesktopDemo.vue'
import OSBootInteractiveDemo from './components/appendix/computer-fundamentals/OSBootInteractiveDemo.vue'
import BrowserArchitectureDemo from './components/appendix/computer-fundamentals/BrowserArchitectureDemo.vue'
import URLRequestDemo from './components/appendix/computer-fundamentals/URLRequestDemo.vue'
import RenderingDemo from './components/appendix/computer-fundamentals/RenderingDemo.vue'
@@ -758,6 +762,28 @@ import IncidentCommandDemo from './components/appendix/incident-response/Inciden
import AlertEscalationDemo from './components/appendix/incident-response/AlertEscalationDemo.vue'
import PostmortemDemo from './components/appendix/incident-response/PostmortemDemo.vue'
// Async Task Queues Components
import AsyncTaskFlowDemo from './components/appendix/async-task-queues/AsyncTaskFlowDemo.vue'
import TaskWorkerDemo from './components/appendix/async-task-queues/TaskWorkerDemo.vue'
import TaskRetryDemo from './components/appendix/async-task-queues/TaskRetryDemo.vue'
import AsyncComparisonDemo from './components/appendix/async-task-queues/AsyncComparisonDemo.vue'
// File Storage Components
import FileStorageTypeDemo from './components/appendix/file-storage/FileStorageTypeDemo.vue'
import FileUploadFlowDemo from './components/appendix/file-storage/FileUploadFlowDemo.vue'
import CDNAccelerationDemo from './components/appendix/file-storage/CDNAccelerationDemo.vue'
// Rate Limiting Components
import RateLimitAlgorithmDemo from './components/appendix/rate-limiting/RateLimitAlgorithmDemo.vue'
import BackpressureDemo from './components/appendix/rate-limiting/BackpressureDemo.vue'
// Search Engines Components Registration
import InvertedIndexDemo from './components/appendix/search-engines/InvertedIndexDemo.vue'
import SearchRelevanceDemo from './components/appendix/search-engines/SearchRelevanceDemo.vue'
// Project Architecture Components
import ArchitectureComparisonDemo from './components/appendix/project-architecture/ArchitectureComparisonDemo.vue'
export default {
extends: DefaultTheme,
Layout,
@@ -969,7 +995,11 @@ export default {
app.component('VibeCodingFlowDemo', VibeCodingFlowDemo)
app.component('PowerOnDemo', PowerOnDemo)
app.component('BootProcessDemo', BootProcessDemo)
app.component('BiosUefiDemo', BiosUefiDemo)
app.component('BiosUefiInteractiveDemo', BiosUefiInteractiveDemo)
app.component('AppLaunchDemo', AppLaunchDemo)
app.component('DesktopDemo', DesktopDemo)
app.component('OSBootInteractiveDemo', OSBootInteractiveDemo)
app.component('BrowserArchitectureDemo', BrowserArchitectureDemo)
app.component('URLRequestDemo', URLRequestDemo)
app.component('RenderingDemo', RenderingDemo)
@@ -1533,6 +1563,28 @@ export default {
app.component('IncidentCommandDemo', IncidentCommandDemo)
app.component('AlertEscalationDemo', AlertEscalationDemo)
app.component('PostmortemDemo', PostmortemDemo)
// Async Task Queues Components Registration
app.component('AsyncTaskFlowDemo', AsyncTaskFlowDemo)
app.component('TaskWorkerDemo', TaskWorkerDemo)
app.component('TaskRetryDemo', TaskRetryDemo)
app.component('AsyncComparisonDemo', AsyncComparisonDemo)
// File Storage Components Registration
app.component('FileStorageTypeDemo', FileStorageTypeDemo)
app.component('FileUploadFlowDemo', FileUploadFlowDemo)
app.component('CDNAccelerationDemo', CDNAccelerationDemo)
// Rate Limiting Components Registration
app.component('RateLimitAlgorithmDemo', RateLimitAlgorithmDemo)
app.component('BackpressureDemo', BackpressureDemo)
// Search Engines Components Registration
app.component('InvertedIndexDemo', InvertedIndexDemo)
app.component('SearchRelevanceDemo', SearchRelevanceDemo)
// Project Architecture Components Registration
app.component('ArchitectureComparisonDemo', ArchitectureComparisonDemo)
},
setup() {
const route = useRoute()
@@ -69,6 +69,11 @@
**生活类比**:猜数字游戏。我想一个 1-100 的数,你每次猜中间,我告诉你大了还是小了。最多猜 7 次就能猜中(因为 2⁷ = 128 > 100)。
:::
👇 **动手试试看**
下面这个演示展示了二分查找的工作原理,你可以选择顺序查找或二分查找来对比:
<SearchAlgorithmDemo />
### 1.2 为什么二分查找这么快?
| 数据量 | 线性查找 | 二分查找 |
@@ -78,7 +83,17 @@
| 1,000,000 | 1,000,000 次 | 20 次 |
| 1,000,000,000 | 1,000,000,000 次 | 30 次 |
::: tip 💡 对数增长的威力
::: tip 逐行解读这张表
**第一列(数据量)**:要查找的数据有多少。可以看到数据量从 100 增长到 10 亿(扩大了 1000 万倍!)
**第二列(线性查找)**:最"笨"的方法,从第一个开始一个一个找。查找次数等于数据量,数据量越大,查找次数越多。
**第三列(二分查找)**:聪明的方法,每次排除一半。查找次数只和数据量的对数有关,即使 10 亿数据也只需要 30 次!
**对比结论**:当数据量达到 100 万时,线性查找需要 100 万次,二分查找只需要 20 次——差距达 5 万倍!
:::
::: tip 对数增长的威力
二分查找的时间复杂度是 O(log n),这意味着:
- 10 亿数据,最多查找 30 次
@@ -102,6 +117,20 @@
| **归并排序** | O(n log n) | 稳定排序 | 需要稳定性的场景 |
| **堆排序** | O(n log n) | 原地排序 | 内存受限场景 |
::: tip 📊 逐行解读这张表
**冒泡排序**:最基础的排序算法,就像水底的气泡往上冒一样。简单易懂,但速度最慢。适合学习排序思想,不适合实际使用。
**选择排序**:每次选出最小的放到前面。也很简单,但无论数据是否有序都要做同样多的比较。
**插入排序**:像打扑克牌时整理手牌一样。把每个元素插入到前面已经排好序的部分中。对近乎有序的数据效率很高。
**快速排序**:实际开发中最常用的排序。平均情况下最快,但最坏情况(数据已经有序)会退化到 O(n²)。
**归并排序**:采用"分而治之"的思想,总是 O(n log n),但需要额外空间。适合需要稳定排序的场景。
**堆排序**:利用堆这种数据结构的排序,原地排序(不需要额外空间),但实际运行往往比快速排序慢。
:::
### 2.2 为什么快速排序"快"
::: tip 💡 快速排序的原理
@@ -120,6 +149,11 @@
**生活类比**:整理书架。先抽出一本书,把比它薄的放左边,比它厚的放右边。然后对左右两堆分别重复这个过程。
:::
👇 **动手试试看**
下面这个演示展示了排序算法的可视化,你可以生成数组,观察冒泡排序和快速排序的过程对比:
<SortingAlgorithmDemo />
---
## 3. 递归:自己调用自己
@@ -153,6 +187,16 @@ function factorial(n) {
| **性能** | 稍慢(函数调用开销) | 更快 |
| **适用场景** | 树遍历、分治算法 | 简单重复任务 |
::: tip 📊 逐行解读这张表
**代码简洁度**:递归通常只需要几行代码就能表达复杂的逻辑(如遍历树结构),而用循环可能需要更多的变量和嵌套。
**内存消耗**:递归会使用"调用栈"来保存每一层的信息,就像叠盘子一样,每递归一层就多一个盘子。循环则不需要这种开销。
**性能**:每次函数调用都有开销(参数传递、栈操作等),所以递归通常比循环慢一些。
**适用场景**:递归擅长处理本身就是递归结构的问题(如文件树、DOM 树);循环擅长简单的重复操作(如遍历数组)。
:::
::: warning ⚠️ 递归的陷阱
**栈溢出**:递归层次太深,调用栈空间耗尽。
@@ -162,6 +206,11 @@ function factorial(n) {
- 限制递归深度
:::
👇 **动手试试看**
下面这个演示展示了递归的调用过程,观察函数如何自己调用自己:
<RecursiveThinkingDemo />
---
## 4. 贪心算法:每步选最优
@@ -197,6 +246,11 @@ function factorial(n) {
**教训**:贪心算法简单高效,但不总是能得到最优解。使用前要证明问题满足贪心条件。
:::
👇 **动手试试看**
下面这个演示展示了贪心算法的实际效果,你可以尝试不同的硬币组合,观察贪心策略的表现:
<GreedyThinkingDemo />
---
## 5. 算法设计范式
@@ -208,6 +262,21 @@ function factorial(n) {
| **动态规划** | 记录子问题的解 | 背包问题、最短路径 | 有重叠子问题 |
| **回溯** | 试错,走不通就回退 | 八皇后、全排列 | 搜索问题 |
::: tip 📊 逐行解读这张表
**分治**:把大问题拆成小问题,分别解决后再合并。就像整理房间,先分成客厅、卧室、厨房分别打扫,最后整体整洁。
**贪心**:每步都选当前最好的,不考虑长远后果。像吃饭时先挑最喜欢吃的菜,可能不是最优的吃法,但速度快。
**动态规划**:记住中间结果,避免重复计算。像记笔记,下次遇到同样问题直接查答案,不用重新推导。
**回溯**:走不通就退回来重试。像走迷宫,此路不通就返回上一个路口尝试别的路。
:::
👇 **动手试试看**
下面这个演示展示了不同算法设计范式的特点和应用场景:
<AlgorithmParadigmDemo />
---
## 6. 总结:算法是解决问题的艺术
@@ -1,230 +1,293 @@
# 数据结构
::: tip 前言
**如何高效地组织和存储数据** 你可能遇到过这样的困惑:为什么有些程序处理几万条数据很快,有些处理几百条就卡住了?答案往往在于数据结构的选择。本章带你理解常见数据结构的特点和适用场景
**程序 = 数据结构 + 算法。** 前面我们学了 CPU 如何执行指令、操作系统如何管理资源。但程序要处理的核心对象是**数据**——用户信息、商品列表、社交关系……这些数据怎么在内存里组织,直接决定了程序的快慢。你可能遇到过这样的困惑:为什么有些程序处理几万条数据很快,有些处理几百条就卡住了?答案往往在于**数据结构的选择**
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **选型决策能**知道什么时候用数组快速访问,什么时候用链表灵活插入
- **性能分析视角**:能判断性能问题是数据结构选择不当,还是算法效率低
- **直觉判断**看到一个需求,脑子里自动浮现该用什么数据结构
- **性能分析视角**:能判断性能瓶颈是数据结构选错了,还是算法效率低
- **权衡思维**:理解"空间换时间"与"时间换空间",知道没有完美的数据结构
- **后续学习基础**:为数据库、缓存系统、搜索引擎等技术打下基础
- **代码阅读能力**:看到 HashMap、Stack、Queue 这些词不再陌生
- **后续学习基础**:为数据库索引、缓存系统、搜索引擎等技术打下基础
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 线性结构 | 数组、链表、栈、队列 |
| **第 2 章** | 哈希结构 | 哈希表、冲突处理 |
| **第 3 章** | 树形结构 | 二叉树、B树、堆 |
| **第 4 章** | 结构 | 有向图、无向图、遍历算法 |
| **第 1 章** | 全景图 | 四大类数据结构、分类标准 |
| **第 2 章** | 线性结构 | 数组、链表、栈、队列 |
| **第 3 章** | 哈希表 | 哈希函数、冲突处理、O(1) 查找 |
| **第 4 章** | 树形结构 | 二叉树、文件系统树、DOM 树 |
| **第 5 章** | 图结构 | 有向图、无向图、遍历算法 |
| **第 6 章** | 性能对比 | 时间复杂度、空间复杂度 |
| **第 7 章** | 选型指南 | 场景分析、决策流程 |
---
## 0. 全景图:数据结构是什么?
## 1. 全景图:数据结构是什么?
想象你要整理一堆书:
- **堆在地上**:找书要一本本翻(链表)
- **按编号放书架**:直接去对应位置拿(数组)
- **按类别分柜子**:先柜子再找书哈希表
- **按书名排序**二分查找,每次排除一半(树)
- **堆在地上**:找书要一本本翻——这就是最原始的存储
- **按编号放书架**:直接去对应位置拿——这就是**数组**
- **按类别分柜子**:先确定柜子再找书——这就是**哈希表**
- **按书名排序放多层架**:每次排除一半——这就是**树**
不同的整理方式,找书的效率天差地别。**数据结构就是数据的"整理方式"**。
不同的整理方式,找书的效率天差地别。**数据结构就是数据的"整理方式"**——它决定了数据怎么存、怎么找、怎么改
<DataStructureDemo />
<DataStructureOverviewDemo />
**常见数据结构分类:**
所有数据结构可以归为四大类:
| 类型 | 特点 | 典型代表 | 适用场景 |
|------|------|---------|---------|
| **线性结构** | 数据排成一排 | 数组、链表、栈、队列 | 顺序处理、历史记录 |
| **哈希结构** | 键值映射 | 哈希表 | 快速查找、缓存 |
| **树形结构** | 层次关系 | 二叉树、B树 | 排序、搜索、文件系统 |
| **图结构** | 网状关系 | 有向图、无向图 | 社交网络、路径规划 |
| 类型 | 数据关系 | 典型代表 | 生活类比 |
|------|---------|---------|---------|
| **线性结构** | 一对一,排成一排 | 数组、链表、栈、队列 | 火车车厢、排队队伍 |
| **哈希结构** | 键值映射 | 哈希表、字典、集合 | 图书馆索引卡片 |
| **树形结构** | 一对多,层级关系 | 二叉树、B树、堆 | 家族族谱、文件 |
| **图结构** | 多对多,网状关系 | 有向图、无向图 | 地铁线路图、社交网络 |
::: tip 📊 逐行解读这张表
**线性结构**:最简单的数据组织方式,数据一个接一个排列。数组适合随机访问,链表适合频繁插入删除
**哈希结构**:用"键"直接找到"值",查找速度最快。但需要处理"冲突"问题(两个键映射到同一位置)。
**树形结构**:有层次关系的数据。二叉搜索树适合排序和搜索,B树适合磁盘存储(数据库索引)。
**图结构**:最复杂的结构,表示任意的关系网络。社交网络、地图导航都用图来建模。
::: tip 为什么要学这么多种?
因为**没有万能的数据结构**。每种结构都是在"查找速度"、"插入速度"、"内存占用"之间做权衡。就像你不会用书包装家具,也不会用卡车送一封信——选对工具,事半功倍
:::
---
## 1. 线性结构:最基础的组织方式
## 2. 线性结构:最基础的组织方式
### 1.1 数组:连续存储
线性结构是最直觉的数据组织方式——数据一个接一个排列,就像火车车厢。但"怎么连接"和"从哪端操作"的不同,产生了四种变体,各有各的绝活。
::: tip 💡 数组的特点
**数组**是一块连续的内存空间,每个元素大小相同。
<LinearStructuresDemo />
**优点**
- 随机访问快:`arr[100]` 直接计算地址,O(1)
- 缓存友好:连续存储,CPU 缓存命中率高
### 2.1 数组 vs 链表:两种截然不同的存储方式
**缺点**
- 插入删除慢:需要移动后面所有元素,O(n)
- 大小固定:需要预先分配空间
数组和链表是最基础的两种线性结构,它们的核心区别在于**内存布局**
**生活类比**:一排编号的储物柜,每个柜子大小相同。找第 10 号柜子直接去,但要在中间插入一个柜子,后面的都要往后挪。
| 对比维度 | 数组 | 链表 |
|---------|------|------|
| **内存布局** | 连续的一整块 | 散落在各处,用指针串起来 |
| **访问第 n 个** | 直接算地址,O(1) | 从头一个个找,O(n) |
| **中间插入** | 后面的都要挪,O(n) | 改两个指针就行,O(1) |
| **大小** | 创建时就固定了 | 随时可以增长 |
| **生活类比** | 一排编号储物柜 | 寻宝游戏的线索链 |
::: tip 什么时候用数组?什么时候用链表?
- **数据量已知、频繁按位置访问** → 数组(比如学生成绩表、像素矩阵)
- **数据量未知、频繁插入删除** → 链表(比如播放列表、撤销历史)
- **不确定?** → 先用数组。大多数场景下,数组的缓存友好性带来的性能优势更大
:::
### 1.2 链表:节点相连
### 2.2 栈和队列:加了"规矩"的线性结构
::: tip 💡 链表的特点
**链表**由一系列节点组成,每个节点包含数据和指向下一个节点的指针。
栈和队列本质上就是数组或链表,只是**限制了操作方式**。看起来功能变少了,但正是这种限制让它们有了明确的用途:
**优点**
- 插入删除快:只需修改指针,O(1)
- 大小灵活:可以动态增长
| 结构 | 规则 | 操作 | 类比 | 你写的代码里在哪? |
|------|------|------|------|-----------------|
| **栈** | 后进先出 (LIFO) | push / pop | 一摞盘子 | 函数调用栈、浏览器后退、Ctrl+Z 撤销 |
| **队列** | 先进先出 (FIFO) | enqueue / dequeue | 排队买票 | 任务调度、消息队列、打印队列 |
**缺点**
- 访问慢:要从头开始遍历,O(n)
- 额外空间:每个节点需要存储指针
**生活类比**:寻宝游戏,每个线索指向下一个地点。要找第 10 个线索,必须从第 1 个开始一步步找。
:::
### 1.3 栈和队列:受限的线性结构
| 结构 | 规则 | 操作 | 类比 | 应用 |
|------|------|------|------|------|
| **栈** | 后进先出 (LIFO) | push/pop | 一摞盘子 | 函数调用、撤销操作 |
| **队列** | 先进先出 (FIFO) | enqueue/dequeue | 排队买票 | 任务调度、消息队列 |
::: tip 💡 为什么要有"受限"的结构?
栈和队列看起来比数组、链表功能少,但正是这种"限制"让它们有明确的用途:
- **栈**:函数调用时,最后调用的函数最先返回
- **队列**:任务调度时,先来的任务先处理
限制带来简洁,简洁带来高效。
::: tip 为什么"限制"反而是好事?
想象一个只有"放盘子"和"拿盘子"两个操作的栈——你永远不会拿错顺序。**限制带来确定性,确定性带来可靠性。** 函数调用栈就是靠"后进先出"保证最后调用的函数最先返回,如果允许随意访问中间的函数,程序就乱套了。
:::
---
## 2. 哈希表:最快的查找
## 3. 哈希表:最快的查找
### 2.1 哈希表原理
线性结构的查找都不够快——数组要遍历 O(n),即使排好序用二分查找也要 O(log n)。有没有一种结构能做到 **O(1) 直接找到**?有,就是哈希表
::: tip 💡 哈希表如何工作?
**哈希表**通过"哈希函数"把键映射到数组索引。
<HashTableDemo />
**工作流程**
1. 输入键(如 "apple"
2. 哈希函数计算:`hash("apple") = 3`
3. 直接去数组索引 3 的位置找
### 3.1 哈希表的核心思想
**冲突处理**
- 两个不同的键可能映射到同一索引
- 解决方法:链地址法(同一位置用链表存储多个值)
哈希表的原理其实很简单
**生活类比**:图书馆按书名首字母分柜子。"Apple" 开头的书都放 A 柜,"Banana" 开头的放 B 柜。找书时先确定柜子,再在柜子里找。
:::
1. 你给一个**键**(比如 "apple"
2. **哈希函数**把键算成一个数字(比如 `hash("apple") = 3`
3. 直接去数组的第 3 个位置找——不用遍历,一步到位
### 2.2 哈希表的时间复杂度
这就像图书馆的索引系统:你不用在一排排书架上找,查索引卡片就能直接定位到书的位置。
| 操作 | 平均情况 | 最坏情况 |
|------|---------|---------|
### 3.2 哈希冲突:两个键撞车了怎么办?
两个不同的键可能算出同一个索引——这叫**哈希冲突**。就像两本书的索引号相同,都指向同一个位置。
| 解决方法 | 原理 | 类比 |
|---------|------|------|
| **链地址法** | 同一位置用链表存多个值 | 同一个柜子里放多本书 |
| **开放寻址法** | 冲突了就往后找空位 | 柜子满了就放隔壁柜子 |
### 3.3 哈希表的性能
| 操作 | 平均情况 | 最坏情况(全部冲突) |
|------|---------|-------------------|
| **查找** | O(1) | O(n) |
| **插入** | O(1) | O(n) |
| **删除** | O(1) | O(n) |
::: warning ⚠️ 什么时候会退化?
当所有键都映射到同一个索引时,哈希表退化为链表,所有操作变成 O(n)。
::: warning 什么时候会退化?
当所有键都映射到同一个索引时,哈希表退化为链表,所有操作变成 O(n)。避免方法:选择好的哈希函数 + 动态扩容(负载因子超过阈值时扩容)。
:::
**避免方法**
- 选择好的哈希函数
- 动态扩容(负载因子超过阈值时扩容)
::: tip 哈希表在你的代码里无处不在
- JavaScript 的 `{}` 对象和 `Map` → 哈希表
- Python 的 `dict` → 哈希表
- Java 的 `HashMap` → 哈希表
- 数据库的索引 → 底层也用哈希
你每次写 `user["name"]``map.get("key")`,背后都是哈希表在工作。
:::
---
## 3. 树:层次结构
## 4. 树形结构:层级关系的表达
### 3.1 二叉搜索树
哈希表查找快,但数据是无序的。如果你需要**既能快速查找,又能保持数据有序**,就需要树形结构了。
树的核心特征:每个节点可以有多个"孩子",但只有一个"父亲"(根节点除外)。这种一对多的层级关系,在现实中随处可见。
<TreeStructureDemo />
### 4.1 二叉搜索树:有序的树
二叉搜索树有一个简单但强大的规则:**左小右大**。
::: tip 💡 二叉搜索树的规则
**二叉搜索树**是一种特殊的二叉树:
- 左子树的所有值 < 根节点
- 右子树的所有值 > 根节点
**查找过程**
1. 从根节点开始
2. 如果目标值 < 当前值,往左走
3. 如果目标值 > 当前值,往右走
4. 每次比较排除一半节点
查找时,每次比较都能排除一半节点,时间复杂度 O(log n)。就像猜数字游戏——"比 50 大还是小?""大。""比 75 大还是小?"——每次排除一半。
**时间复杂度**:O(log n),但最坏情况(变成链表)是 O(n)
:::
### 4.2 平衡树:防止退化
### 3.2 平衡树
二叉搜索树有个问题:如果数据按顺序插入(1, 2, 3, 4, 5),树会退化成一条链,查找变回 O(n)。平衡树通过自动调整结构来避免这个问题:
为了防止二叉搜索树退化,引入了**平衡树**:
| 类型 | 平衡策略 | 特点 | 典型应用 |
|------|---------|------|---------|
| **AVL 树** | 严格平衡(高度差 ≤ 1) | 查找最快,插入删除稍慢 | 需要频繁查找的场景 |
| **红黑树** | 近似平衡 | 综合性能好 | Java TreeMap、Linux 内核 |
| **B 树** | 多路平衡,一个节点存多个值 | 减少磁盘 I/O | 数据库索引 |
| 类型 | 平衡方式 | 特点 |
|------|---------|------|
| **AVL 树** | 严格平衡(高度差 ≤ 1) | 查找最快,插入删除稍慢 |
| **红黑树** | 近似平衡 | 综合性能好,应用最广 |
| **B 树** | 多路平衡 | 适合磁盘存储,数据库索引 |
---
## 4. 如何选择数据结构?
| 需求 | 推荐结构 | 原因 |
|------|---------|------|
| **快速随机访问** | 数组 | O(1) 索引访问 |
| **频繁插入删除** | 链表 | O(1) 插入删除 |
| **快速查找** | 哈希表 | O(1) 平均查找 |
| **有序数据** | 平衡树 | O(log n) 查找,保持有序 |
| **最近使用** | 栈 | LIFO 特性 |
| **任务排队** | 队列 | FIFO 特性 |
::: tip 💡 选择数据结构的心法
**没有最好的数据结构,只有最合适的数据结构。**
选择时要考虑:
1. **主要操作是什么?** 查找?插入?删除?
2. **数据量多大?** 小数据量差别不大,大数据量要慎重
3. **数据有序吗?** 有序数据可以用二分查找
4. **内存限制?** 某些结构需要额外空间
::: tip 树在你的代码里在哪?
- **文件系统**:文件夹嵌套就是树结构
- **HTML DOM**`<html>``<body>``<div>``<p>` 就是一棵树
- **数据库索引**:B+ 树让百万级数据的查找只需要 3-4 次磁盘读取
- **JSON/XML**:嵌套的数据格式本质上就是树
:::
---
## 5. 总结:数据结构是程序的基础
## 5. 图结构:复杂关系的网络
让我们用一个比喻总结各种数据结构:
树只能表示"一对多"的层级关系。但现实中很多关系是"多对多"的——你的朋友也有朋友,城市之间有多条路可以走。这种**任意节点之间都可能有连接**的结构,就是图。
| 结构 | 比喻 | 核心特点 |
|------|------|---------|
| **数组** | 编号储物柜 | 访问快,插入慢 |
| **链表** | 寻宝线索 | 插入快,访问慢 |
| **栈** | 一摞盘子 | 后进先出 |
| **队列** | 排队队伍 | 先进先出 |
| **哈希表** | 分类柜子 | 查找最快 |
| **树** | 家族族谱 | 层次结构 |
<GraphStructureDemo />
::: tip 💡 核心启示
**数据结构决定了程序的效率上限。**
### 5.1 图的三种形态
- 选对数据结构,问题迎刃而解
- 选错数据结构,再好的算法也无济于事
| 类型 | 特点 | 类比 | 典型应用 |
|------|------|------|---------|
| **无向图** | 边没有方向,A→B 等于 B→A | 微信好友(互相的) | 社交网络、通信网络 |
| **有向图** | 边有方向,A→B 不等于 B→A | 微博关注(单向的) | 网页链接、依赖关系 |
| **带权图** | 边有权重(距离、费用等) | 城市间的公路(有里程数) | 地图导航、最短路径 |
理解数据结构,就是理解"如何高效地组织数据"。这是每个程序员的基本功。
### 5.2 图的遍历
图的遍历比线性结构复杂,因为可能有环(A→B→C→A),需要记录"已访问"的节点:
| 遍历方式 | 策略 | 类比 | 适用场景 |
|---------|------|------|---------|
| **BFS(广度优先)** | 先访问所有邻居,再访问邻居的邻居 | 水波纹扩散 | 最短路径、层级遍历 |
| **DFS(深度优先)** | 一条路走到底,走不通再回头 | 走迷宫 | 路径搜索、连通性判断 |
::: tip 图在现实中的应用
- **地图导航**:城市是节点,道路是边,导航就是在图上找最短路径
- **社交网络**:用户是节点,关注/好友是边,"你可能认识的人"就是图算法推荐的
- **包管理器**:npm/pip 的依赖关系就是有向图,`npm install` 就是在做图的拓扑排序
:::
---
## 6. 性能对比:一张表看清所有数据结构
学了这么多数据结构,它们的性能到底差多少?下面这个交互式对比能帮你建立直觉:
<DataStructureDemo />
**核心性能对比表:**
| 数据结构 | 访问 | 查找 | 插入 | 删除 | 空间 |
|---------|------|------|------|------|------|
| **数组** | O(1) | O(n) | O(n) | O(n) | O(n) |
| **链表** | O(n) | O(n) | O(1) | O(1) | O(n) |
| **栈/队列** | O(n) | O(n) | O(1) | O(1) | O(n) |
| **哈希表** | — | O(1) | O(1) | O(1) | O(n) |
| **二叉搜索树** | — | O(log n) | O(log n) | O(log n) | O(n) |
| **图** | — | O(V+E) | O(1) | O(E) | O(V+E) |
::: tip 怎么读这张表?
- **O(1)**:不管数据量多大,操作时间恒定——最快
- **O(log n)**:数据量翻倍,时间只多一步——很快
- **O(n)**:数据量翻倍,时间也翻倍——一般
- **O(V+E)**:取决于节点数和边数——图的特殊表示
注意:这些都是**平均情况**。最坏情况下,哈希表会退化到 O(n),二叉搜索树也会退化到 O(n)。
:::
---
## 7. 选型指南:该用哪种数据结构?
学了这么多数据结构,面对实际需求时该怎么选?关键是**从需求出发**,问自己几个问题:
1. **最频繁的操作是什么?** 查找?插入?删除?遍历?
2. **数据之间有什么关系?** 一对一?一对多?多对多?
3. **数据量有多大?** 几十条和几百万条的最优选择可能完全不同
4. **需要有序吗?** 是否需要按某种顺序遍历数据
<DataStructureSelectorDemo />
**快速决策流程:**
| 你的需求 | 推荐结构 | 原因 |
|---------|---------|------|
| 按位置快速访问 | 数组 | O(1) 随机访问 |
| 频繁在中间插入删除 | 链表 | O(1) 插入删除,不用移动元素 |
| 后进先出(撤销、递归) | 栈 | LIFO 语义天然匹配 |
| 先进先出(任务队列) | 队列 | FIFO 语义天然匹配 |
| 按键快速查找 | 哈希表 | O(1) 平均查找 |
| 有序数据 + 快速查找 | 二叉搜索树 | O(log n) 查找且保持有序 |
| 复杂多对多关系 | 图 | 能表达任意节点间的连接 |
::: tip 实际开发中的经验法则
- **80% 的场景**用数组和哈希表就够了
- **需要有序**时考虑树
- **关系复杂**时考虑图
- **不确定?** 先用最简单的,遇到性能问题再换。过早优化是万恶之源
:::
---
## 总结
> 数据结构是程序的骨架。**数组**像一排编号储物柜,按位置取东西最快;**链表**像寻宝线索链,插入删除最灵活;**哈希表**像图书馆索引,按名字找东西最快;**树**像家族族谱,表达层级关系且保持有序;**图**像地铁线路图,表达任意复杂的网状关系。没有最好的数据结构,只有最合适的——关键是理解每种结构的优势和代价,根据实际需求做出权衡。
---
## 延伸阅读
- **数据结构实现**:自己动手实现各种数据结构,加深理解
- **高级数据结构**:学习跳表、布隆过滤器、并查集等
- **数据库索引**:了解 B+ 树在数据库中的应用
- **缓存设计**:学习 LRU 缓存如何结合哈希表和链表
| 主题 | 推荐资源 |
|------|---------|
| 数据结构可视化 | [VisuAlgo](https://visualgo.net/) - 动画演示各种数据结构和算法 |
| 算法与数据结构 | 《算法图解》- Aditya Bhargava,图文并茂适合入门 |
| 深入理解 | 《数据结构与算法分析》- Mark Allen Weiss |
| 刷题练习 | [LeetCode](https://leetcode.cn/) - 按数据结构分类练习 |
---
## 下一步
现在你已经掌握了数据结构的核心知识。接下来可以继续学习:
- **[算法思维](./algorithm-thinking.md)**:学会用排序、搜索、递归、动态规划等算法思维解决问题
- **[编程语言](./programming-languages.md)**:了解不同编程语言如何实现这些数据结构
@@ -52,47 +52,7 @@ CPU 接收到复位信号后,把内部所有寄存器和缓存清零,从一
## 2. BIOS/UEFI:硬件的自检
### 2.1 什么是 BIOS/UEFI
**BIOSBasic Input/Output System** 是电脑启动后第一个运行的程序,存储在主板的一个**只读芯片**中。
**UEFIUnified Extensible Firmware Interface** 是 BIOS 的升级版,更安全、更现代。现在的电脑大多使用 UEFI。
### 2.2 BIOS/UEFI 做了什么?
1. **硬件自检(POST**:检查内存、显卡、键盘等部件是否正常
2. **初始化硬件**:设置硬件工作模式
3. **启动顺序**:按照设定顺序,尝试从硬盘/U 盘/网络启动
```
BIOS/UEFI 工作流程:
┌─────────────────────────────────────┐
│ 1. 硬件自检 (POST) │
│ - 检查内存是否正常 │
│ - 检查显卡是否正常 │
│ - 检查键盘/鼠标是否正常 │
├─────────────────────────────────────┤
│ 2. 初始化硬件 │
│ - 设置硬件工作模式 │
│ - 配置中断向量表 │
├─────────────────────────────────────┤
│ 3. 寻找启动设备 │
│ - 按启动顺序查找可启动设备 │
│ - 读取启动扇区 │
└─────────────────────────────────────┘
```
如果发现问题,主板会发出**蜂鸣声**(不同次数代表不同错误)。
### 2.3 启动顺序
BIOS/UEFI 会按照设定的**启动顺序**查找启动设备:
1. 硬盘(最常见)
2. U 盘/光盘(重装系统时用)
3. 网络( PXE 启动,企业批量部署用)
找到第一个可启动设备后,读取它的**启动扇区(Boot Sector)**,把控制权交给操作系统。
<BiosUefiInteractiveDemo />
---
@@ -104,94 +64,7 @@ BIOS/UEFI 会按照设定的**启动顺序**查找启动设备:
## 3. 操作系统启动:从内核到桌面
### 3.1 什么是操作系统?
**操作系统(Operating System,简称 OS)** 是管理计算机硬件和软件资源的程序集合。它就像一个"大管家",帮我们管理内存、CPU、文件等资源,让我们不需要直接和硬件打交道。
常见的操作系统:
| 操作系统 | 特点 | 典型设备 |
|---------|------|---------|
| **Windows** | 生态丰富,兼容性好 | 桌面电脑、笔记本 |
| **macOS** | 苹果生态,流畅稳定 | Mac 电脑 |
| **Linux** | 开源免费,服务器首选 | 服务器、嵌入式设备 |
| **Android** | 移动端 Linux | 手机、平板 |
| **iOS** | 苹果移动端 | iPhone、iPad |
### 3.2 操作系统的启动过程
当你从硬盘启动时,操作系统的启动过程如下:
<BootProcessDemo />
#### 第一步:引导程序(Bootloader)
硬盘的第一个扇区存放着**引导程序(Bootloader)**,它的任务是把操作系统内核加载到内存中。
- **Windows**Bootloader 叫 `bootmgr`
- **Linux**:常见的引导程序有 `GRUB``rEFInd`
```
引导程序工作流程:
┌─────────────────────────────────────┐
│ 1. 读取硬盘分区表 │
│ 2. 找到操作系统分区 │
│ 3. 加载操作系统内核到内存 │
│ 4. 跳转到内核入口点 │
└─────────────────────────────────────┘
```
#### 第二步:内核加载(Kernel)
操作系统**内核(Kernel)** 是操作系统的核心,负责管理内存、CPU、进程等核心功能。
```
内核的主要功能:
┌─────────────────────────────────────┐
│ • 进程管理 - 创建/调度进程 │
│ • 内存管理 - 分配/回收内存 │
│ • 文件系统 - 管理文件存储 │
│ • 设备驱动 - 控制硬件设备 │
│ • 网络通信 - 处理网络协议 │
└─────────────────────────────────────┘
```
#### 第三步:系统服务启动
内核加载后,会启动各种**系统服务**:
- **Windows 服务**:更新服务、安全中心、打印机服务
- **Linux 服务**:SSH 服务、网络服务、图形界面(GNOME、KDE)
```
Windows 启动过程:
BIOS → MBR → bootmgr → winload.exe → ntoskrnl.exe → 系统服务 → 桌面
Linux 启动过程:
BIOS → GRUB → vmlinuz (内核) → systemd → 系统服务 → 桌面环境
```
#### 第四步:显示桌面
最后,操作系统启动**图形界面(GUI)**,显示桌面:
- **Windows**explorer.exe(资源管理器)显示桌面
- **Linux**GNOME、KDE、XFCE 等桌面环境
- **macOS**Finder 显示桌面
```
桌面出现的过程:
┌─────────────────────────────────────┐
│ 1. 显卡驱动加载 │
│ 2. 显示服务器启动 │
│ (Windows: Desktop Window Manager)│
│ (Linux: X Server / Wayland) │
│ 3. 桌面环境启动 │
│ 4. 显示桌面背景和图标 │
└─────────────────────────────────────┘
```
<DesktopDemo />
<OSBootInteractiveDemo />
---
@@ -99,7 +99,7 @@ SELECT name FROM users WHERE active = true
- **JavaScript(弱类型)**`"11"` — 悄悄帮你转了
- **Python(强类型)**`TypeError` — 让你自己想清楚
想深入了解类型系统?→ [类型系统与编译原理入门](./type-systems-compilers)
想深入了解类型系统?→ [类型系统入门](./type-systems) | [编译原理入门](./compilers)
---
@@ -147,6 +147,7 @@ SELECT name FROM users WHERE active = true
:::
**下一步学习**
- [类型系统与编译原理入门](./type-systems-compilers) - 深入理解类型系统和编译过程
- [编译原理入门](./compilers) - 深入理解编译过程和代码优化
- [类型系统入门](./type-systems) - 深入理解类型系统和类型安全
- [数据结构](./data-structures) - 理解数据的组织方式
- [算法思维入门](./algorithm-thinking) - 学习解决问题的方法
@@ -1,2 +1,432 @@
# 调试的艺术
> 待实现
::: tip 前言
**代码写完了,运行报错——然后呢?** 很多新手在这一步就卡住了,盯着屏幕不知所措。调试(Debug)是编程中最核心的技能之一,甚至比写代码本身更重要。因为写代码只占开发时间的 30%,剩下的 70% 都在理解问题、定位 Bug、验证修复。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **调试思维**:建立系统化的问题定位方法,不再"瞎猜"
- **错误阅读能力**:看懂报错信息,从错误堆栈中快速定位问题
- **常用调试方法**:掌握二分法、橡皮鸭、最小复现等经典调试技巧
- **工具使用能力**:了解断点调试、日志调试、网络调试等工具的使用场景
- **AI 辅助调试**:学会用 AI 加速调试过程,但不依赖 AI
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 读懂错误信息 | 错误类型、堆栈追踪 |
| **第 2 章** | 经典调试方法 | 二分法、橡皮鸭、最小复现 |
| **第 3 章** | 调试工具箱 | 断点、日志、网络抓包 |
| **第 4 章** | AI 时代的调试 | AI 辅助 + 人工判断 |
| **第 5 章** | 调试心态与习惯 | 防御性编程、调试日志 |
---
## 0. 全景图:调试是一种科学方法
调试不是"碰运气",而是一个严谨的科学过程。物理学家做实验的方法论,完全适用于调试:
1. **观察现象**:程序出了什么问题?报了什么错?
2. **提出假设**:可能是什么原因导致的?
3. **设计实验**:怎么验证这个假设?
4. **验证结论**:假设对了就修复,错了就换一个假设
::: tip 调试的黄金法则
- **先复现,再修复**:不能稳定复现的 Bug,修了也不知道是不是真的修好了
- **一次只改一个变量**:同时改多处,就不知道是哪个改动解决了问题
- **相信证据,不相信直觉**:你觉得"不可能是这里的问题",往往就是这里的问题
- **最近改了什么?**:80% 的 Bug 都是最近的改动引入的
:::
---
## 1. 读懂错误信息:报错不是敌人,是线索
新手最常犯的错误:看到报错就慌,直接关掉或者忽略。其实,**错误信息是程序在告诉你哪里出了问题**——它是你最好的朋友。
### 1.1 错误的三大类型
| 类型 | 什么时候出现 | 举例 | 严重程度 |
|-----|------------|------|---------|
| **语法错误** | 代码还没运行就报错 | 少了括号、拼错关键字 | 最容易修 |
| **运行时错误** | 代码运行到某一行崩溃 | 访问不存在的变量、除以零 | 中等难度 |
| **逻辑错误** | 代码能运行,但结果不对 | 计算公式写错、条件判断反了 | 最难发现 |
### 1.2 如何阅读错误堆栈
以 JavaScript 为例,一个典型的错误信息:
```
TypeError: Cannot read properties of undefined (reading 'name')
at getUserName (app.js:15:23)
at handleClick (app.js:42:10)
at HTMLButtonElement.<anonymous> (app.js:58:5)
```
**从上往下读**
1. **第一行**:错误类型 + 错误描述 → `TypeError`,试图读取 `undefined``name` 属性
2. **第二行**:出错的函数和位置 → `getUserName` 函数,`app.js` 第 15 行第 23 列
3. **后续行**:调用链 → 谁调用了这个函数?`handleClick` → 按钮点击事件
::: tip 阅读堆栈的口诀
**从上往下找原因,从下往上找源头。** 第一行告诉你"出了什么错",最后一行告诉你"从哪里开始的"。
:::
### 1.3 常见错误类型速查
| 错误名称 | 含义 | 常见原因 |
|---------|------|---------|
| `SyntaxError` | 语法错误 | 括号不匹配、少了逗号 |
| `TypeError` | 类型错误 | 对 `undefined`/`null` 做操作 |
| `ReferenceError` | 引用错误 | 使用了未声明的变量 |
| `RangeError` | 范围错误 | 数组越界、递归太深 |
| `NetworkError` | 网络错误 | API 请求失败、跨域问题 |
| `404 Not Found` | 资源不存在 | URL 写错、文件被删除 |
| `500 Internal Server Error` | 服务器内部错误 | 后端代码崩溃 |
### 1.4 Python 错误信息对比
Python 的堆栈和 JavaScript 相反——**从下往上读**:
```python
Traceback (most recent call last):
File "main.py", line 10, in <module>
result = calculate(data)
File "main.py", line 5, in calculate
return data["price"] * data["quantity"]
KeyError: 'quantity'
```
**最后一行**才是错误原因:`KeyError: 'quantity'`,字典里没有 `quantity` 这个键。
::: tip 不同语言,同一个思路
不管什么语言,错误信息都包含三个关键信息:**什么错**(错误类型)、**哪里错**(文件和行号)、**为什么错**(错误描述)。学会提取这三个信息,就能读懂任何语言的报错。
:::
---
## 2. 经典调试方法:前人总结的智慧
这些方法不需要任何工具,只需要你的大脑。它们是所有高级调试技巧的基础。
### 2.1 二分法调试
**核心思想**:把问题范围缩小一半,再缩小一半,直到找到根源。
**场景**:代码很长,不知道哪一段出了问题。
**步骤**
1. 在代码中间加一个 `console.log`(或 `print`
2. 如果中间点之前就出错了 → 问题在上半部分
3. 如果中间点之后才出错 → 问题在下半部分
4. 对出错的那一半,重复上述步骤
```
100 行代码出了 Bug
↓ 在第 50 行加 log
问题在 50-100 行
↓ 在第 75 行加 log
问题在 50-75 行
↓ 在第 62 行加 log
问题在第 60-62 行!
```
::: tip 二分法的威力
100 行代码,最多只需要 7 次(log₂100 ≈ 7)就能定位到具体行。1000 行也只需要 10 次。
:::
### 2.2 橡皮鸭调试法
**核心思想**:把问题一行一行地"讲"给别人听(或者一只橡皮鸭),讲着讲着你自己就发现问题了。
**为什么有效?** 因为"写代码"和"解释代码"用的是大脑的不同区域。当你被迫用语言描述每一步逻辑时,那些你"以为对了"的假设会暴露出来。
**实践方法**
1. 打开出问题的代码
2. 逐行解释:"这一行做了什么?为什么要这么做?"
3. 当你说出"嗯,这里应该是……等等"的时候,Bug 往往就在那里
### 2.3 最小复现
**核心思想**:把复杂的问题简化到最小,只保留能触发 Bug 的最少代码。
**为什么重要?**
- 复杂系统中,Bug 可能被其他代码"掩盖"
- 最小复现能排除干扰因素,让问题一目了然
- 也方便你向别人求助——没人愿意看你 500 行代码
**步骤**
1. 创建一个新的空文件
2. 只复制和问题相关的代码
3. 逐步删减,直到删掉任何一行 Bug 就消失
4. 剩下的就是 Bug 的根源
### 2.4 回退法(Git Bisect
**核心思想**:如果代码"之前是好的,现在坏了",那就找到是哪次提交引入的问题。
```bash
# Git 自带的二分查找工具
git bisect start
git bisect bad # 标记当前版本有 Bug
git bisect good abc123 # 标记某个正常的旧版本
# Git 会自动切换到中间的提交,你测试后告诉它 good 或 bad
# 重复几次就能找到引入 Bug 的那次提交
```
::: tip 调试方法选择指南
| 情况 | 推荐方法 |
|-----|---------|
| 不知道哪一段代码出错 | 二分法 |
| 逻辑看起来对但结果不对 | 橡皮鸭 |
| 复杂系统中的 Bug | 最小复现 |
| "之前好好的突然坏了" | 回退法 / Git Bisect |
:::
---
## 3. 调试工具箱:用对工具事半功倍
方法论是基础,但好的工具能让调试效率翻倍。
### 3.1 console.log / print:最朴素也最实用
**适用场景**:快速查看变量值、确认代码执行到了哪里。
```javascript
// JavaScript
console.log('函数被调用了,参数是:', data)
console.log('计算结果:', result)
console.table(arrayData) // 表格形式展示数组/对象
```
```python
# Python
print(f"当前值: {value}")
print(f"类型: {type(data)}") # 检查数据类型
```
**进阶技巧**
| 方法 | 用途 |
|-----|------|
| `console.log()` | 普通输出 |
| `console.warn()` | 黄色警告,容易在大量日志中找到 |
| `console.error()` | 红色错误 |
| `console.table()` | 表格展示数组和对象 |
| `console.time()` / `console.timeEnd()` | 测量代码执行时间 |
| `console.trace()` | 打印调用堆栈 |
### 3.2 断点调试:逐行执行,看清每一步
**适用场景**:逻辑复杂,需要一步步跟踪代码执行过程。
**在浏览器中**Chrome DevTools):
1. 打开开发者工具(F12)→ Sources 面板
2. 找到源代码文件,点击行号设置断点
3. 触发相关操作,代码会在断点处暂停
4. 用控制按钮逐步执行:
- **继续**F8):运行到下一个断点
- **单步跳过**F10):执行当前行,不进入函数内部
- **单步进入**F11):进入函数内部
- **单步跳出**Shift+F11):跳出当前函数
**在 VS Code 中**
1. 点击行号左侧设置断点(红色圆点)
2. 按 F5 启动调试
3. 在"变量"面板查看所有变量的当前值
4. 在"监视"面板添加你关心的表达式
::: tip 断点 vs console.log
**console.log** 适合快速验证,用完就删。**断点调试**适合深入分析复杂逻辑。两者不是替代关系,而是互补关系。
:::
### 3.3 网络调试:前后端之间的问题
**适用场景**:页面显示不对,但不确定是前端的问题还是后端返回的数据有问题。
**Chrome DevTools → Network 面板**
| 查看内容 | 能发现什么问题 |
|---------|--------------|
| **状态码** | 404(地址错)、500(服务器崩了)、403(没权限) |
| **请求参数** | 前端发送的数据对不对 |
| **响应数据** | 后端返回的数据格式对不对 |
| **请求时间** | 哪个接口太慢,拖慢了页面 |
| **请求头** | Token 有没有带、Content-Type 对不对 |
**调试口诀**:先看状态码,再看请求参数,最后看响应数据。
### 3.4 调试工具选择速查
| 问题类型 | 推荐工具 |
|---------|---------|
| 变量值不对 | console.log / 断点 |
| 逻辑执行顺序不对 | 断点调试 |
| API 请求失败 | Network 面板 |
| 页面样式不对 | Elements 面板(检查 CSS |
| 性能问题 | Performance 面板 / console.time |
| 内存泄漏 | Memory 面板 |
---
## 4. AI 时代的调试:让 AI 当你的助手
AI 工具(ChatGPT、Claude、Cursor 等)能大幅加速调试过程,但前提是你得知道怎么用。
### 4.1 AI 擅长什么?
| AI 擅长 | AI 不擅长 |
|--------|----------|
| 解释错误信息的含义 | 理解你的业务逻辑 |
| 提供常见问题的解决方案 | 判断哪个方案最适合你的项目 |
| 生成调试代码片段 | 复现只在特定环境出现的 Bug |
| 分析代码中的潜在问题 | 理解复杂的系统上下文 |
### 4.2 向 AI 提问的正确姿势
**差的提问**
> "我的代码报错了,帮我看看"
**好的提问**
> "我在用 React 写一个表单组件,提交时报错 `TypeError: Cannot read properties of undefined (reading 'email')`。以下是相关代码:[贴代码]。我已经确认 API 返回的数据格式是正确的,问题可能出在前端数据处理。"
**提问模板**
```
1. 我在做什么:[背景]
2. 期望的行为:[应该怎样]
3. 实际的行为:[实际怎样]
4. 错误信息:[完整报错]
5. 相关代码:[贴代码]
6. 我已经尝试了:[排除了什么]
```
### 4.3 AI 调试的陷阱
::: warning AI 调试的三个坑
1. **AI 可能"自信地胡说"**:AI 给的方案看起来很合理,但可能完全不对。永远要自己验证。
2. **AI 不了解你的上下文**:它不知道你的项目结构、依赖版本、运行环境。你需要提供足够的上下文。
3. **过度依赖 AI 会退化调试能力**:如果每次报错都直接丢给 AI,你永远学不会自己调试。建议先自己分析 5 分钟,再求助 AI。
:::
### 4.4 AI + 人工的最佳组合
```
遇到 Bug
第 1 步:自己读错误信息(1 分钟)
第 2 步:自己提出假设(2 分钟)
第 3 步:快速验证假设(2 分钟)
卡住了?→ 把错误信息 + 代码 + 你的分析发给 AI
AI 给出建议 → 你判断是否合理 → 验证
```
---
## 5. 调试心态与习惯:从"救火"到"防火"
最好的调试是不需要调试。养成好习惯,能从源头减少 Bug。
### 5.1 防御性编程
**核心思想**:写代码时就假设"一切都可能出错",提前做好防护。
```javascript
// 差:假设 data 一定存在
const name = data.user.name
// 好:防御性写法
const name = data?.user?.name ?? '未知用户'
```
```python
# 差:假设文件一定能打开
content = open('config.json').read()
# 好:防御性写法
try:
content = open('config.json').read()
except FileNotFoundError:
print("配置文件不存在,使用默认配置")
content = '{}'
```
### 5.2 写好日志
日志是"事后调试"的关键。线上环境不能打断点,只能靠日志。
| 日志级别 | 用途 | 举例 |
|---------|------|------|
| **DEBUG** | 开发时的详细信息 | 变量值、函数参数 |
| **INFO** | 正常的业务流程 | "用户登录成功"、"订单创建" |
| **WARN** | 不影响功能但需要注意 | "缓存未命中"、"重试第 2 次" |
| **ERROR** | 出错了,需要处理 | "数据库连接失败"、"API 超时" |
::: tip 好日志的标准
一条好的日志应该回答:**什么时候**、**在哪里**、**发生了什么**、**关键数据是什么**。
```
[2025-01-15 14:30:22] [ERROR] [OrderService] 创建订单失败
用户ID: 12345, 商品ID: 67890, 原因: 库存不足
```
:::
### 5.3 调试检查清单
遇到 Bug 时,按这个顺序排查:
1. **读错误信息**:错误类型、文件、行号
2. **最近改了什么?**:用 `git diff` 看最近的改动
3. **能复现吗?**:找到稳定的复现步骤
4. **缩小范围**:用二分法或最小复现定位
5. **提出假设并验证**:一次只改一个变量
6. **修复后回归测试**:确保修复没有引入新问题
### 5.4 新手常踩的调试陷阱
| 陷阱 | 正确做法 |
|-----|---------|
| 不看报错就开始改代码 | 先完整阅读错误信息 |
| 同时改好几个地方 | 一次只改一处,验证后再改下一处 |
| 改完不测试就提交 | 每次修改后都运行测试 |
| 只在自己电脑上测试 | 考虑不同环境(浏览器、系统、网络) |
| 调试完不清理 console.log | 提交前删除所有调试代码 |
| 遇到问题就重启/重装 | 先理解问题原因,重启只是临时方案 |
---
## 6. 总结
调试是一门手艺,需要刻意练习。回顾本章的核心要点:
1. **调试是科学方法**:观察 → 假设 → 实验 → 验证,不是碰运气
2. **错误信息是朋友**:学会从报错中提取"什么错、哪里错、为什么错"
3. **经典方法永不过时**:二分法、橡皮鸭、最小复现是所有调试的基础
4. **工具要用对场景**console.log 快速验证,断点深入分析,Network 排查接口
5. **AI 是助手不是拐杖**:先自己分析,再让 AI 辅助,最后自己验证
6. **防火胜于救火**:防御性编程、好的日志习惯能从源头减少 Bug
::: tip 记住这句话
**每个 Bug 都是一次学习机会。** 你修过的每一个 Bug,都在帮你建立"模式识别"能力——下次遇到类似问题,你会更快地定位到原因。
:::
---
## 延伸阅读
- [Chrome DevTools 官方文档](https://developer.chrome.com/docs/devtools/) — 浏览器调试工具的完整指南
- [VS Code Debugging](https://code.visualstudio.com/docs/editor/debugging) — VS Code 断点调试教程
- [How to Debug Anything](https://www.debuggingbook.org/) — 系统化调试方法论
@@ -0,0 +1,648 @@
# 前端项目架构设计
::: tip 🎯 核心问题
**文件越放越乱,代码越写越难找,如何设计一个清晰、可维护的前端项目结构?** 这就像问:你是把所有衣服都扔进一个箱子,还是按季节、类型、颜色分类整理?好的项目架构能让团队协作更高效,让代码维护更轻松。
:::
---
## 1. 为什么要关注项目架构?
### 1.1 从小项目到大项目的演变
很多初学者刚开始写前端时,项目结构非常简单:
```
my-project/
├── index.html
├── style.css
└── app.js
```
三个文件搞定一切,简单直接。但随着项目增长,问题开始出现:
- **页面多了**`page1.html`, `page2.html`... 文件散落在根目录
- **组件多了**:按钮、弹窗、表单各自为政,复用困难
- **工具函数多了**:到处复制粘贴,改一个地方要改十处
- **样式冲突了**:全局 CSS 互相覆盖,调试困难
**问题的本质**:没有"章法",文件随意存放,就像把春夏秋冬的衣服都扔进一个箱子。
### 1.2 好的架构像整理好的衣柜
想象一个整理好的衣柜:
| 区域 | 存放物品 | 特点 |
|------|----------|------|
| **挂衣区** | 外套、衬衫 | 常穿,方便取用 |
| **抽屉区** | 内衣、袜子 | 分类摆放,整齐 |
| **隔板区** | 毛衣、裤子 | 叠放,节省空间 |
| **顶层区** | 换季衣物 | 不常用,收纳起来 |
**好的项目架构**就是把代码也这样组织:每一类文件有自己的"位置",团队成员都知道该去哪找、该往哪放。
::: tip 💡 通俗比喻:餐厅后厨的组织
把前端项目想象成一家餐厅的后厨:
- **`src/pages/`(页面区)** = 出餐口:每个订单对应一个成品菜
- **`src/components/`(组件区)** = 备料台:切好的蔬菜、调好的酱料,随时可用
- **`src/utils/`(工具区)** = 工具柜:刀、勺、温度计等通用工具
- **`src/assets/`(食材区)** = 冷藏库:图片、字体、样式等原材料
- **`src/services/`(服务层)** = 传菜窗口:与外部(服务员/后端)交互
**关键点**:每个区域职责明确,不会混乱。你不会在冷藏库里切菜,也不会把刀具扔进汤锅。
:::
---
## 2. 经典目录结构解析
### 2.1 标准目录结构(以 Vue/React 为例)
一个中大型前端项目的典型结构如下:
```
my-frontend-project/
├── public/ # 静态资源(不经过构建)
│ ├── favicon.ico
│ ├── index.html
│ └── robots.txt
├── src/
│ ├── assets/ # 项目资源(会被构建工具处理)
│ │ ├── images/
│ │ ├── fonts/
│ │ └── styles/
│ │ ├── variables.scss # 变量定义
│ │ ├── mixins.scss # 混入样式
│ │ └── global.css # 全局样式
│ ├── components/ # 通用组件
│ │ ├── common/ # 全局通用组件
│ │ │ ├── Button/
│ │ │ │ ├── index.vue
│ │ │ │ ├── Button.scss
│ │ │ │ └── Button.test.js
│ │ │ ├── Modal/
│ │ │ └── Loading/
│ │ └── business/ # 业务组件
│ │ ├── UserCard/
│ │ └── ProductList/
│ ├── views/ 或 pages/ # 页面组件
│ │ ├── Home/
│ │ ├── About/
│ │ └── User/
│ │ ├── Profile/
│ │ └── Settings/
│ ├── router/ 或 navigation/ # 路由配置
│ │ └── index.js
│ ├── stores/ 或 state/ # 状态管理
│ │ ├── user.js
│ │ └── app.js
│ ├── services/ 或 api/ # API 服务
│ │ ├── user.js
│ │ └── product.js
│ ├── utils/ 或 helpers/ # 工具函数
│ │ ├── request.js # 请求封装
│ │ ├── storage.js # 本地存储
│ │ └── format.js # 格式化工具
│ ├── hooks/ 或 composables/ # 组合式函数
│ │ ├── useAuth.js
│ │ └── useLoading.js
│ ├── directives/ # 自定义指令
│ ├── plugins/ # 插件配置
│ ├── constants/ # 常量定义
│ ├── types/ 或 @types/ # TypeScript 类型
│ └── App.vue 或 App.jsx # 根组件
│ └── main.js 或 main.ts # 入口文件
├── tests/ # 测试文件
│ ├── unit/
│ └── e2e/
├── .env # 环境变量
├── .env.development
├── .env.production
├── vite.config.js # 构建配置
├── package.json
└── README.md
```
::: tip 📊 从图解中你能看到什么?
**分层逻辑**
- **`public/` vs `src/assets/`**:前者直接复制到输出目录,后者会被构建工具处理(压缩、转译、添加哈希值)
- **`components/` vs `views/`**:组件是"零件",页面是"成品"。一个页面由多个组件组装而成
- **`services/` 独立出来**:把 API 调用集中管理,方便统一处理错误、加载状态、请求拦截
**依赖方向**
```
views/pages → components → utils/hooks
services → stores
```
上层可以调用下层,但下层不应该依赖上层。
:::
### 2.2 按功能组织 vs 按类型组织
项目结构有两种主流的组织方式:
#### 方式一:按类型组织(Type-based)
```
src/
├── components/
│ ├── Button.vue
│ ├── Modal.vue
│ └── Card.vue
├── views/
│ ├── Home.vue
│ ├── User.vue
│ └── Product.vue
├── stores/
│ ├── user.js
│ └── product.js
└── services/
├── user.js
└── product.js
```
**优点**
- 结构清晰,同类文件在一起
- 适合小型项目,一目了然
**缺点**
- 修改一个功能要跨多个目录
- 大型项目中文件过多,难以定位
#### 方式二:按功能组织(Feature-based
```
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ │ ├── LoginForm.vue
│ │ │ └── RegisterForm.vue
│ │ ├── stores/
│ │ │ └── authStore.js
│ │ ├── services/
│ │ │ └── authApi.js
│ │ ├── hooks/
│ │ │ └── useAuth.js
│ │ └── index.js # 统一导出
│ ├── user/
│ │ ├── components/
│ │ ├── stores/
│ │ └── services/
│ └── product/
│ ├── components/
│ ├── stores/
│ └── services/
├── shared/ # 共享资源
│ ├── components/
│ ├── utils/
│ └── styles/
└── App.vue
```
**优点**
- 高内聚,修改一个功能在一个目录完成
- 便于团队协作,不同人负责不同 feature
- 易于删除或重构,不会散落各处
**缺点**
- 初期设计需要考虑 feature 划分
- 共享组件需要额外考虑
::: tip 💡 如何选择?
| 项目规模 | 推荐方式 | 原因 |
|----------|----------|------|
| 小型项目(< 10 个页面) | 按类型组织 | 简单直接,快速上手 |
| 中大型项目(> 20 个页面) | 按功能组织 | 便于维护,团队协作 |
| 微前端/大型应用 | 按功能 + 模块拆分 | 独立部署,团队自治 |
**实际建议**:很多项目采用"混合模式"——整体按功能组织,内部按类型细分。
:::
---
## 3. 各目录的职责与最佳实践
### 3.1 `components/` 组件目录
组件是前端项目的核心,良好的组件设计能大幅提升开发效率。
#### 组件分类
```
components/
├── common/ # 通用组件(跨项目可复用)
│ ├── Button/
│ ├── Input/
│ ├── Modal/
│ └── Loading/
├── business/ # 业务组件(项目特定)
│ ├── UserCard/
│ ├── ProductItem/
│ └── OrderTable/
└── layout/ # 布局组件
├── Header/
├── Sidebar/
└── Footer/
```
#### 单文件组件结构
每个组件建议包含以下文件:
```
Button/
├── index.vue # 主组件(或 .tsx/.jsx
├── Button.scss # 样式(可选 CSS Modules
├── Button.test.js # 单元测试
├── Button.stories.js # Storybook 文档(可选)
├── types.ts # 类型定义(TS 项目)
└── index.ts # 统一导出
```
::: details 📝 组件代码示例
```vue
<!-- Button/index.vue -->
<template>
<button
:class="['btn', `btn--${type}`, { 'btn--disabled': disabled }]"
:disabled="disabled"
@click="handleClick"
>
<Loading v-if="loading" size="small" />
<slot />
</button>
</template>
<script setup>
import { Loading } from '../Loading'
defineProps({
type: { type: String, default: 'primary' },
disabled: Boolean,
loading: Boolean
})
const emit = defineEmits(['click'])
const handleClick = () => emit('click')
</script>
<style scoped lang="scss">
.btn {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
&--primary {
background: var(--primary-color);
color: white;
}
&--disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
</style>
```
:::
### 3.2 `views/``pages/` 页面目录
页面是用户看到的"成品",通常对应路由。
```
views/
├── Home/ # 首页
│ ├── index.vue
│ ├── components/ # 页面私有组件
│ │ ├── HeroSection.vue
│ │ └── FeatureList.vue
│ └── hooks/ # 页面私有 hooks
│ └── useHomeData.js
├── User/
│ ├── Profile/
│ ├── Settings/
│ └── OrderHistory/
└── Product/
├── List/
└── Detail/
```
**最佳实践**
- 页面组件保持"薄",逻辑下沉到 hooks 或 services
- 页面私有组件放在页面目录下,避免污染全局
- 复杂页面可以进一步拆分子目录
### 3.3 `services/``api/` 服务层
集中管理所有 API 调用,统一处理请求/响应拦截。
```
services/
├── request.js # 请求实例配置(axios/fetch 封装)
├── user.js # 用户相关 API
├── product.js # 商品相关 API
├── order.js # 订单相关 API
└── index.js # 统一导出
```
::: details 📝 服务层代码示例
```javascript
// services/request.js
import axios from 'axios'
import { useAuthStore } from '@/stores/auth'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
const authStore = useAuthStore()
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
return config
}
)
// 响应拦截器
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
// 统一处理登录过期
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default request
```
```javascript
// services/user.js
import request from './request'
export const userApi = {
login: (data) => request.post('/auth/login', data),
register: (data) => request.post('/auth/register', data),
getProfile: () => request.get('/user/profile'),
updateProfile: (data) => request.put('/user/profile', data)
}
```
:::
### 3.4 `stores/` 状态管理
```
stores/
├── index.js # store 入口
├── auth.js # 认证状态
├── user.js # 用户信息
├── app.js # 应用级状态(主题、语言等)
└── cart.js # 购物车状态
```
**建议**
- 按功能拆分 store,避免单个文件过大
- 区分全局状态和局部状态,不要什么都放全局
- 使用组合式 APIPinia/Vuex 4+)更灵活
### 3.5 `utils/` 工具函数
```
utils/
├── format.js # 格式化(日期、金额等)
├── storage.js # 本地存储封装
├── validate.js # 表单验证
├── dom.js # DOM 操作
├── date.js # 日期处理
└── index.js # 统一导出
```
**原则**
- 纯函数优先,便于测试
- 单一职责,一个函数只做一件事
- 添加 JSDoc 注释,说明参数和返回值
::: details 📝 工具函数示例
```javascript
// utils/storage.js
const STORAGE_PREFIX = 'myapp_'
export const storage = {
get(key) {
const value = localStorage.getItem(STORAGE_PREFIX + key)
try {
return JSON.parse(value)
} catch {
return value
}
},
set(key, value) {
localStorage.setItem(
STORAGE_PREFIX + key,
typeof value === 'string' ? value : JSON.stringify(value)
)
},
remove(key) {
localStorage.removeItem(STORAGE_PREFIX + key)
}
}
```
:::
### 3.6 `hooks/``composables/` 组合式函数
```
hooks/
├── useAuth.js # 认证逻辑
├── useLoading.js # 加载状态
├── usePagination.js # 分页逻辑
├── useForm.js # 表单处理
└── useWebsocket.js # WebSocket
```
::: details 📝 Hook 示例
```javascript
// hooks/useLoading.js
import { ref } from 'vue'
export function useLoading() {
const loading = ref(false)
const withLoading = async (fn) => {
loading.value = true
try {
return await fn()
} finally {
loading.value = false
}
}
return { loading, withLoading }
}
// 使用
const { loading, withLoading } = useLoading()
const fetchData = () => withLoading(async () => {
const data = await api.getData()
list.value = data
})
```
:::
---
## 4. 知名开源项目的架构参考
### 4.1 Vue 3 官方仓库
```
vue/
├── packages/
│ ├── vue/ # 核心包
│ ├── reactivity/ # 响应式系统
│ ├── runtime-core/ # 运行时核心
│ ├── runtime-dom/ # DOM 运行时
│ ├── compiler-sfc/ # 单文件组件编译器
│ └── shared/ # 共享工具
├── scripts/ # 构建脚本
└── tsconfig.json
```
**特点**
- Monorepo 结构,多个包统一管理
- 按功能拆分 package,职责清晰
- 共享工具提取到 shared 包
### 4.2 React 官方仓库
```
react/
├── packages/
│ ├── react/ # React 核心
│ ├── react-dom/ # DOM 渲染器
│ ├── react-reconciler/ # 协调器
│ ├── scheduler/ # 调度器
│ └── shared/ # 共享代码
├── fixtures/ # 测试用例
└── scripts/
```
**特点**
- 核心与渲染器分离(react vs react-dom
- reconciler 独立,支持多平台
- scheduler 单独抽离,可独立使用
### 4.3 Ant Design Vue
```
ant-design-vue/
├── components/ # 组件目录
│ ├── button/
│ ├── modal/
│ └── ...
├── docs/ # 文档
├── site/ # 官网
├── tests/ # 测试
└── typings/ # 类型定义
```
**特点**
- 组件与文档分离
- 每个组件独立目录,包含 demo、test、style
- 统一的类型定义
### 4.4 Next.js(全栈框架)
```
my-nextjs-app/
├── app/ # App Router(新版)
│ ├── page.js # 页面
│ ├── layout.js # 布局
│ ├── loading.js # 加载状态
│ └── api/ # API 路由
├── components/ # 组件
├── lib/ # 工具函数
├── public/ # 静态资源
└── styles/ # 全局样式
```
**特点**
- 约定式路由,文件即路由
- 内置 loading、error、layout 等约定文件
- API 路由与页面共存
---
## 5. 架构设计原则与检查清单
### 5.1 核心原则
| 原则 | 说明 | 实践建议 |
|------|------|----------|
| **单一职责** | 一个模块只做一件事 | 组件、函数保持简洁 |
| **高内聚低耦合** | 相关代码放在一起,减少依赖 | 按功能组织目录 |
| **可预测性** | 代码行为符合直觉 | 命名清晰,结构一致 |
| **可测试性** | 便于编写单元测试 | 纯函数、依赖注入 |
| **可扩展性** | 新功能容易添加 | 预留扩展点,避免硬编码 |
### 5.2 检查清单
**目录结构**
- [ ] 是否有清晰的目录划分?
- [ ] 新成员能否快速找到文件位置?
- [ ] 是否避免了过深的嵌套(建议不超过 4 层)?
**组件设计**
- [ ] 组件是否单一职责?
- [ ] Props 是否清晰、可预测?
- [ ] 是否提取了可复用的逻辑到 hooks?
**代码组织**
- [ ] 是否避免了循环依赖?
- [ ] 工具函数是否纯函数优先?
- [ ] 常量、配置是否集中管理?
**团队协作**
- [ ] 是否有编码规范文档?
- [ ] 是否有文件命名约定?
- [ ] 代码审查是否关注架构问题?
---
## 6. 总结
::: tip 💡 核心思想
好的前端项目架构不是一成不变的,而是随着项目发展不断演进的。关键是建立清晰的**组织原则**和**命名约定**,让团队成员达成共识。
**记住这几点**
1. **先简单后复杂**:小项目不要过度设计
2. **按功能组织**:中大型项目推荐 Feature-based
3. **统一约定**:命名、结构、代码风格保持一致
4. **持续重构**:定期审视架构,及时调整
**最终目标**:让代码像整理好的衣柜一样,想找什么立刻能找到,新成员也能快速上手。
:::
---
## 参考资源
- [Vue 风格指南](https://vuejs.org/style-guide/)
- [React 项目结构建议](https://react.dev/learn/thinking-in-react)
- [Bulletproof React - 架构指南](https://github.com/alan2207/bulletproof-react)
- [Feature Sliced Design](https://feature-sliced.design/)
@@ -434,69 +434,365 @@ Flexbox 是现代 CSS 最常用的布局方式。它让元素自动排列对齐
| `flex-wrap` | 是否换行 | `nowrap``wrap` |
| `gap` | 元素间距 | `10px``1rem` |
### 3.7 SCSSCSS 的"升级版"
### 3.7 CSS 预处理器:SCSS/SASS 与 LESS
::: tip 🎯 真实场景
你写了一个项目,CSS 文件有 2000 行。后来要改主题色,你发现:
- 主色调 `#3b82f6` 出现了 50 次
- 改一个颜色要全局搜索替换
- 还要担心漏改了某个地方
- 改一个颜色要全局搜索替换,还要担心漏改
- 选择器写成 `.nav .nav-list .nav-item .nav-link` 又长又难维护
**SCSS 解决的问题**变量、嵌套、混入、模块化
**CSS 预处理器**就是来解决这些问题的。它让 CSS 也能"编程":有变量、嵌套、能复用代码。
:::
**SCSS 示例**
#### 3.7.1 什么是 CSS 预处理器?
```scss
// 1. 变量:定义主题色
$primary-color: #3b82f6;
**用人话解释**:预处理器是一种"更聪明的 CSS"。你用更强大的语法写样式,然后它帮你**编译**成普通 CSS,浏览器就能正常识别了。
// 2. 嵌套:父子关系一目了然
.card {
background: white;
h2 {
color: $primary-color;
}
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
```
**为什么要用?**
**编译后变成普通 CSS**
| 痛点 | 原生 CSS | 预处理器 |
|------|----------|----------|
| 颜色重复出现 | 到处复制粘贴 | 定义变量,一处修改全局生效 |
| 选择器层级太深 | 写成一长串 | 嵌套语法,层级一目了然 |
| 相同样式重复写 | 复制粘贴 | 混入(Mixin),像函数一样复用 |
#### 3.7.2 三大预处理器对比
| 特性 | 原生 CSS | **SCSS/SASS** | **LESS** |
|------|----------|---------------|----------|
| **变量写法** | `--primary` | `$primary` | `@primary` |
| **嵌套语法** | ❌ 不支持 | ✅ 支持 | ✅ 支持 |
| **混入(复用代码)** | ❌ 不支持 | ✅ `@mixin` | ✅ `.mixin()` |
| **学习难度** | 简单 | 中等 | 中等 |
| **流行程度** | - | ⭐⭐⭐ 最流行 | ⭐⭐ 较流行 |
**简单记忆**
- **SCSS**:用 `$` 符号,Bootstrap 5 在用,生态最好
- **LESS**:用 `@` 符号,和 CSS 的 `@media` 写法一致,容易上手
#### 3.7.3 核心功能对比示例
##### 1. 变量:一处修改,全局生效
**场景**:主题色 `#3b82f6` 在 20 个地方用到,要改成红色。
<Tabs>
<TabItem label="原生 CSS">
```css
.card {
background: white;
}
.card h2 {
color: #3b82f6;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
/* 要改 20 处,容易漏 */
.button { background: #3b82f6; }
.link { color: #3b82f6; }
.border { border-color: #3b82f6; }
```
</TabItem>
<TabItem label="SCSS">
```scss
$primary: #3b82f6;
.button { background: $primary; }
.link { color: $primary; }
.border { border-color: $primary; }
/* 改 $primary 一处即可 */
```
</TabItem>
<TabItem label="LESS">
```less
@primary: #3b82f6;
.button { background: @primary; }
.link { color: @primary; }
.border { border-color: @primary; }
/* 改 @primary 一处即可 */
```
</TabItem>
</Tabs>
##### 2. 嵌套:层级关系一目了然
**场景**:导航栏里有多层结构。
<Tabs>
<TabItem label="原生 CSS">
```css
/* 写成一长串,难看出层级关系 */
.navbar .nav-list .nav-item .nav-link { }
.navbar .nav-list .nav-item .nav-link:hover { }
```
</TabItem>
<TabItem label="SCSS">
```scss
.navbar {
.nav-list {
.nav-item {
.nav-link {
&:hover { } /* & 表示父选择器 */
}
}
}
}
```
**SCSS vs Less vs 原生 CSS**
</TabItem>
<TabItem label="LESS">
| 特性 | 原生 CSS | SCSS | Less |
|------|----------|------|------|
| 变量 | ✅ `--var` | ✅ `$var` | ✅ `@var` |
| 嵌套 | ❌ | ✅ | ✅ |
| 混入 | ❌ | ✅ `@mixin` | ✅ `.mixin()` |
| 学习曲线 | 简单 | 中等 | 中等 |
```less
.navbar {
.nav-list {
.nav-item {
.nav-link {
&:hover { }
}
}
}
}
```
</TabItem>
</Tabs>
##### 3. 混入(Mixin):复用代码片段
**场景**:多个按钮都需要"居中显示"的样式。
<Tabs>
<TabItem label="原生 CSS">
```css
/* 复制粘贴 3 次 */
.btn-primary {
display: flex;
justify-content: center;
align-items: center;
}
.btn-secondary {
display: flex;
justify-content: center;
align-items: center;
}
```
</TabItem>
<TabItem label="SCSS">
```scss
@mixin center {
display: flex;
justify-content: center;
align-items: center;
}
.btn-primary { @include center; }
.btn-secondary { @include center; }
```
</TabItem>
<TabItem label="LESS">
```less
.center() {
display: flex;
justify-content: center;
align-items: center;
}
.btn-primary { .center(); }
.btn-secondary { .center(); }
```
</TabItem>
</Tabs>
#### 3.7.4 如何选择?
| 情况 | 推荐选择 |
|------|----------|
| 刚开始学,项目小 | **原生 CSS**(先打好基础) |
| 项目用 Bootstrap 5 | **SCSS**Bootstrap 源码是 SCSS |
| 团队熟悉 `@` 符号 | **LESS**(和 CSS 的 `@media` 写法一致) |
| 需要复杂逻辑(循环、条件) | **SCSS**(功能更强大) |
#### 3.7.5 在项目中使用
**Vite 项目(最简单)**
```bash
# 安装 sass
npm install -D sass
# 直接使用 .scss 或 .less 文件
```
::: tip 💡 新手建议
1. **先学好原生 CSS**:预处理器只是"语法糖",本质还是 CSS
2. **项目大了再用 SCSS**:小项目直接写 CSS 更简单
3. **现代 CSS 已经支持变量**`--primary-color: #3b82f6;` 原生就能用
1. **先学好原生 CSS**:预处理器只是"语法糖",不懂 CSS 基础会越用越乱
2. **项目不用强上**CSS 不到 200 行,直接写 CSS 更简单
3. **从 SCSS 开始**:语法和 CSS 几乎一样,只是多了 `$` 变量
4. **不要嵌套太深**:超过 3 层会让代码难维护
:::
#### 3.7.6 不同技术栈的文件组织对比
**同样的项目,用不同技术栈,文件结构有什么不同?**
<Tabs>
<TabItem label="原生 HTML + CSS">
```
my-website/
├── index.html # 页面结构
├── about.html
├── css/
│ ├── reset.css # 重置样式
│ ├── layout.css # 布局样式
│ ├── components.css # 组件样式
│ └── style.css # 主样式(可能上千行)
├── js/
│ └── main.js
└── images/
└── logo.png
```
**特点**
- CSS 集中在一个或几个文件
- 改样式要来回切换 HTML 和 CSS 文件
- 样式容易互相冲突
</TabItem>
<TabItem label="Vue + 原生 CSS">
```
src/
├── components/ # 组件文件夹
│ ├── Button/
│ │ ├── Button.vue # 模板 + 样式 + 逻辑
│ │ └── Button.test.js
│ ├── Header/
│ │ └── Header.vue
│ └── Footer/
│ └── Footer.vue
├── views/ # 页面文件夹
│ ├── Home.vue
│ └── About.vue
├── App.vue # 根组件
└── main.js # 入口文件
```
**Button.vue 内部结构**
```vue
<template>
<button class="btn">点击</button>
</template>
<script>
export default { name: 'Button' }
</script>
<style scoped> <!-- scoped 样式只影响当前组件 -->
.btn { background: #3b82f6; }
</style>
```
</TabItem>
<TabItem label="Vue + SCSS">
```
src/
├── assets/
│ └── styles/
│ ├── _variables.scss # 变量:颜色、间距等
│ ├── _mixins.scss # 混入:复用代码块
│ ├── _functions.scss # 函数:颜色计算等
│ └── global.scss # 全局样式入口
├── components/
│ ├── Button/
│ │ └── Button.vue # 组件内用 @import 引入变量
│ └── Card/
│ └── Card.vue
├── views/
│ ├── Home.vue
│ └── About.vue
├── App.vue
└── main.js
```
**_variables.scss**
```scss
$primary: #3b82f6;
$secondary: #64748b;
$spacing-sm: 8px;
$spacing-md: 16px;
```
**Button.vue**
```vue
<style scoped lang="scss">
@import '@/assets/styles/variables';
.btn {
background: $primary; // 使用变量
padding: $spacing-md;
}
</style>
```
</TabItem>
<TabItem label="Vue + Tailwind CSS">
```
src/
├── components/
│ ├── Button.vue # 不需要 style 块
│ ├── Card.vue
│ └── Header.vue
├── views/
│ ├── Home.vue
│ └── About.vue
├── App.vue
└── main.js
# 配置文件(根目录)
tailwind.config.js # 主题配置
tailwind.css # 基础样式入口
```
**Button.vue**(没有 style 块):
```vue
<template>
<button class="bg-blue-500 hover:bg-blue-600 px-4 py-2 rounded">
点击
</button>
</template>
```
**特点**
- 没有单独的样式文件
- 类名就是样式(`bg-blue-500` = 蓝色背景)
- 配置集中在 `tailwind.config.js`
</TabItem>
</Tabs>
**核心区别总结**
| 技术栈 | 样式文件位置 | 主题管理 | 代码复用 |
|--------|-------------|----------|----------|
| 原生 HTML+CSS | 集中式 `css/` 文件夹 | 搜索替换 | 复制粘贴 |
| Vue + CSS | 分散在 `.vue` 组件内 | 搜索替换 | 复制粘贴 |
| Vue + SCSS | 组件内 + `styles/` 公共文件 | 变量统一管理 | 混入复用 |
| Vue + Tailwind | 无(类名里) | `tailwind.config.js` | 类名组合 |
### 3.8 如何记住这么多 CSS 属性?
::: tip 🎯 新手困惑
@@ -1,3 +1,151 @@
# 异步任务队列与生产消费模型
> 待实现
::: tip 前言
**用户点了"导出报表"按钮,然后盯着转圈的加载动画等了 30 秒——这合理吗?** 当一个操作需要几秒甚至几分钟才能完成时,让用户干等着显然不是好体验。异步任务队列就是解决这个问题的核心架构模式——把耗时操作丢到后台去处理,让用户立刻得到响应。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **同步异步对比**:理解为什么某些操作必须异步化,以及异步化带来的用户体验提升
- **生产消费模型**:掌握 Producer-Consumer 模式的核心思想和工作流程
- **Worker 池机制**:了解任务如何被分发到多个 Worker 并行处理
- **可靠性保障**:掌握任务重试、幂等性、死信队列等保障机制
- **技术选型能力**:了解主流异步任务框架的特点和适用场景
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 为什么需要异步 | 同步阻塞 vs 异步非阻塞 |
| **第 2 章** | 生产消费模型 | Producer、Queue、Consumer |
| **第 3 章** | Worker 工作池 | 并发处理、任务分发 |
| **第 4 章** | 可靠性保障 | 重试策略、幂等性、死信队列 |
| **第 5 章** | 框架选型 | Celery、Sidekiq、Bull、RQ |
---
## 0. 全景图:为什么不能让用户"干等着"?
想象你去餐厅点餐。好的餐厅会在你点完餐后立刻给你一个取餐号,然后你可以去找座位、玩手机,等餐好了再来取。而不是让你站在柜台前,盯着厨师做完整道菜。
Web 应用中有很多类似的"做菜"操作:
- **发送邮件/短信**:调用第三方 API,可能需要几秒
- **生成报表/PDF**:大量数据计算,可能需要几十秒
- **图片/视频处理**:压缩、转码、加水印,可能需要几分钟
- **数据同步**:跨系统数据同步,耗时不确定
::: tip 异步任务的核心思想
把耗时操作从"请求-响应"的主流程中剥离出来,放到后台队列中异步处理。用户提交请求后立刻得到"已收到,正在处理"的响应,处理完成后通过通知、轮询或 WebSocket 告知结果。
:::
---
## 1. 同步 vs 异步:一个订单的故事
当用户提交一个订单时,后端需要做很多事情:扣减库存、创建订单记录、发送确认邮件、更新推荐系统、记录审计日志……
在同步模式下,这些操作串行执行,用户必须等所有操作完成才能看到结果。在异步模式下,只需要完成核心操作(扣减库存、创建订单),其余操作丢到队列里后台处理。
<AsyncTaskFlowDemo />
| 对比维度 | 同步处理 | 异步处理 |
|---------|---------|---------|
| 用户等待时间 | 所有操作总耗时 | 仅核心操作耗时 |
| 系统吞吐量 | 低(线程被阻塞) | 高(快速释放线程) |
| 失败影响 | 非核心失败导致整体失败 | 非核心失败不影响主流程 |
| 实现复杂度 | 简单 | 需要额外的队列基础设施 |
| 数据一致性 | 强一致 | 最终一致 |
::: tip 什么时候该用异步?
三个判断标准:**耗时长**(超过 1-2 秒)、**非核心**(失败不应影响主流程)、**可延迟**(不需要立刻得到结果)。满足其中任意两个,就应该考虑异步化。
:::
---
## 2. 生产消费模型:任务的"流水线"
异步任务队列的核心是经典的 **生产者-消费者模式(Producer-Consumer Pattern**。这个模式有三个角色:
- **生产者(Producer**:产生任务的一方,通常是 Web 服务器处理用户请求时
- **队列(Queue**:存储待处理任务的缓冲区,通常用 Redis、RabbitMQ 等实现
- **消费者(Consumer/Worker**:从队列中取出任务并执行的工作进程
<TaskWorkerDemo />
::: tip 队列的三大价值
1. **解耦**:生产者不需要知道谁来处理任务,消费者不需要知道任务从哪来
2. **削峰填谷**:突发流量时任务先堆积在队列中,消费者按自己的节奏处理
3. **可靠性**:任务持久化在队列中,即使消费者崩溃也不会丢失
:::
| 组件 | 职责 | 常见实现 |
|------|------|---------|
| 消息中间件 | 存储和转发任务消息 | Redis、RabbitMQ、Kafka |
| 序列化器 | 将任务参数序列化/反序列化 | JSON、MessagePack、Pickle |
| 调度器 | 管理定时任务和延迟任务 | Cron、APScheduler、node-cron |
| 结果存储 | 保存任务执行结果 | Redis、数据库、S3 |
---
## 3. 可靠性保障:任务不能"丢了"也不能"重复"
在分布式环境中,网络抖动、服务重启、资源不足等问题随时可能发生。异步任务系统必须具备完善的可靠性保障机制。
最核心的两个问题:**任务丢失**(消费者处理到一半崩溃了)和**重复执行**(任务被投递了两次)。
<TaskRetryDemo />
::: tip 可靠性三板斧
1. **ACK 机制**:消费者处理完任务后才发送确认(ACK),未确认的任务会被重新投递
2. **重试策略**:任务失败后按策略重试,指数退避 + 抖动是最佳实践
3. **幂等性设计**:同一个任务执行多次和执行一次的效果相同,通过唯一 ID 去重实现
:::
| 机制 | 解决的问题 | 实现方式 |
|------|-----------|---------|
| ACK 确认 | 任务丢失 | 处理完成后手动确认,超时未确认则重新投递 |
| 死信队列(DLQ) | 反复失败的"毒消息" | 重试超过上限后转入死信队列,人工介入处理 |
| 幂等性 | 重复执行 | 用任务唯一 ID 做去重,数据库唯一约束 |
| 优先级队列 | 任务饥饿 | 高优先级任务优先处理,避免被低优先级任务阻塞 |
| 超时控制 | 任务卡死 | 设置最大执行时间,超时自动终止并重试 |
---
## 4. 框架选型:选择适合你的工具
不同语言生态有不同的异步任务框架,它们在功能丰富度、性能、易用性上各有侧重。选择框架时,首先考虑你的技术栈,然后根据项目规模和需求做决定。
<AsyncComparisonDemo />
::: tip 选型建议
- **Python 项目**:中大型用 Celery,小型用 RQ
- **Node.js 项目**:首选 BullMQBull 的下一代)
- **Ruby 项目**Sidekiq 几乎是唯一选择
- **Java 项目**Spring 生态用 Spring Batch,高吞吐用 Kafka Streams
- **Go 项目**Asynq(基于 Redis)或 Machinery
如果你的项目已经在用 Redis,那么基于 Redis 的方案(Celery+Redis、BullMQ、Sidekiq)是最简单的起步方式。
:::
---
## 总结
异步任务队列是后端架构中不可或缺的基础设施。它让系统能够优雅地处理耗时操作,提升用户体验的同时提高系统吞吐量。
回顾本章的关键要点:
1. **异步化的判断标准**:耗时长、非核心、可延迟,满足两个就该异步化
2. **生产消费模型**Producer → Queue → Consumer,三者解耦协作
3. **Worker 池**:多个 Worker 并行消费,提高处理能力
4. **可靠性保障**:ACK 确认 + 重试策略 + 幂等性,三者缺一不可
5. **框架选型**:根据技术栈和项目规模选择,Redis 是最常见的消息中间件
## 延伸阅读
- [Celery 官方文档](https://docs.celeryq.dev/) - Python 最流行的分布式任务队列
- [BullMQ 文档](https://docs.bullmq.io/) - Node.js 高性能任务队列
- [Sidekiq Wiki](https://github.com/sidekiq/sidekiq/wiki) - Ruby 生态的任务处理标杆
- [RabbitMQ Tutorials](https://www.rabbitmq.com/tutorials) - 消息中间件入门教程
- [异步任务最佳实践](https://brandur.org/job-drain) - 任务队列的设计模式与陷阱
@@ -0,0 +1,751 @@
# 后端项目架构设计
::: tip 🎯 核心问题
**API 越写越多,代码越来越乱,如何设计一个清晰、可维护的后端项目结构?** 这就像问:你是把所有工具都扔进一个抽屉,还是按功能分类整理?好的项目架构能让团队协作更高效,让系统扩展更轻松。
:::
---
## 1. 为什么要关注后端项目架构?
### 1.1 从小脚本到大系统的演变
很多初学者刚开始写后端时,代码结构非常简单:
```python
# app.py - 所有代码在一个文件
from flask import Flask, request, jsonify
import sqlite3
app = Flask(__name__)
@app.route('/users', methods=['GET'])
def get_users():
conn = sqlite3.connect('db.sqlite')
users = conn.execute('SELECT * FROM users').fetchall()
return jsonify(users)
@app.route('/users', methods=['POST'])
def create_user():
data = request.json
conn = sqlite3.connect('db.sqlite')
conn.execute('INSERT INTO users (name, email) VALUES (?, ?)',
(data['name'], data['email']))
conn.commit()
return jsonify({'message': 'User created'})
# 还有订单、商品、支付...所有接口都在这个文件
```
几百行代码搞定一切,简单直接。但随着业务发展,问题开始出现:
- **接口多了**:一个文件几千行,找代码像"考古"
- **逻辑复杂了**:业务规则散落在各处,修改容易遗漏
- **数据库操作重复**:到处写 SQL,改表结构要改几十处
- **测试困难**:代码耦合严重,单元测试难以编写
**问题的本质**:没有"章法",所有的逻辑都堆在一起,就像把所有的工具、零件、说明书都扔进一个抽屉。
### 1.2 好的架构像整理好的车间
想象一个整理好的工厂车间:
| 区域 | 功能 | 特点 |
|------|------|------|
| **原料区** | 存放原材料 | 分类摆放,标签清晰 |
| **加工区** | 生产加工 | 流水线作业,工序明确 |
| **质检区** | 质量检查 | 统一标准,严格把关 |
| **成品区** | 存放成品 | 整齐有序,易于出库 |
| **工具室** | 存放工具 | 按需借用,用完归还 |
**好的后端架构**就是把代码也这样组织:每一层只关心自己的职责,数据像流水一样在各层之间传递。
::: tip 💡 通俗比喻:餐厅后厨的组织
把后端系统想象成一家餐厅的后厨:
- **`controllers/`(出餐口)** = 服务员接单:接收订单、核对信息、上菜
- **`services/`(厨师团队)** = 厨师做菜:按照菜谱加工、协调各工序
- **`repositories/`(仓库管理)** = 仓管取料:从仓库取食材、记录库存
- **`models/`(菜谱标准)** = 菜谱定义:宫保鸡丁需要什么料、什么口味
- **`utils/`(工具柜)** = 厨具存放:刀、勺、秤等通用工具
**关键点**:每个角色职责明确,不会越界。服务员不会自己炒菜,厨师不会擅自改菜谱。
:::
---
## 2. 经典分层架构详解
### 2.1 四层架构(Controller-Service-Repository-Model
最经典的后端分层架构如下:
```
my-backend-project/
├── src/
│ ├── controllers/ # 控制器层(Controller
│ │ ├── userController.js
│ │ ├── orderController.js
│ │ └── index.js
│ ├── services/ # 业务逻辑层(Service
│ │ ├── userService.js
│ │ ├── orderService.js
│ │ └── index.js
│ ├── repositories/ # 数据访问层(Repository/DAO
│ │ ├── userRepository.js
│ │ └── index.js
│ ├── models/ # 数据模型层(Model/Entity
│ │ ├── user.js
│ │ ├── order.js
│ │ └── index.js
│ ├── middlewares/ # 中间件
│ │ ├── auth.js
│ │ ├── errorHandler.js
│ │ └── validator.js
│ ├── utils/ # 工具函数
│ │ ├── logger.js
│ │ ├── response.js
│ │ └── validator.js
│ ├── config/ # 配置文件
│ │ ├── database.js
│ │ ├── redis.js
│ │ └── index.js
│ ├── routes/ # 路由定义
│ │ ├── userRoutes.js
│ │ ├── index.js
│ │ └── api.js
│ ├── jobs/ 或 workers/ # 定时任务/后台任务
│ │ └── emailWorker.js
│ ├── events/ 或 subscribers/ # 事件监听
│ │ └── userEvents.js
│ └── app.js # 应用入口
├── tests/ # 测试文件
│ ├── unit/
│ ├── integration/
│ └── e2e/
├── migrations/ # 数据库迁移
├── seeds/ # 种子数据
├── docs/ # 文档
├── .env # 环境变量
├── package.json
└── README.md
```
::: tip 📊 从图解中你能看到什么?
**分层逻辑**
```
┌─────────────────────────────────────────┐
│ Controller 层(控制器层) │ ← 接待员:接收请求,返回响应
│ - 接收 HTTP 请求 │
│ - 参数校验、权限检查 │
│ - 调用 Service │
│ - 格式化响应 │
├─────────────────────────────────────────┤
│ Service 层(业务逻辑层) │ ← 厨师:处理核心业务
│ - 业务逻辑编排 │
│ - 事务管理 │
│ - 调用 Repository │
│ - 跨模块协调 │
├─────────────────────────────────────────┤
│ Repository 层(数据访问层) │ ← 仓管员:管理数据存取
│ - 数据库操作 │
│ - ORM 封装 │
│ - 查询构建 │
├─────────────────────────────────────────┤
│ Model 层(数据模型层) │ ← 菜谱标准:定义数据结构
│ - 实体定义(Entity) │
│ - 类型定义 │
│ - 业务规则验证 │
└─────────────────────────────────────────┘
```
**依赖方向**
```
Controller → Service → Repository → Model
Middleware / Utils
```
上层依赖下层,下层不依赖上层。Model 是核心,所有层都可能依赖它。
:::
### 2.2 各层职责详解
#### Controller 层:请求的"接待员"
Controller 是系统的"门面",负责接收 HTTP 请求并返回响应。
**职责**
- 接收和解析请求参数
- 调用相应的 Service 处理业务
- 格式化响应数据
- 处理 HTTP 相关逻辑(状态码、Header 等)
**不应该做的事**
- 直接操作数据库
- 编写复杂业务逻辑
- 处理事务
::: details 📝 Controller 代码示例(Node.js/Express
```javascript
// controllers/userController.js
const userService = require('../services/userService')
const { success, error } = require('../utils/response')
class UserController {
// 获取用户列表
async list(req, res) {
try {
const { page = 1, limit = 10 } = req.query
const users = await userService.getUsers({ page, limit })
return success(res, users)
} catch (err) {
return error(res, err.message, 500)
}
}
// 获取单个用户
async getById(req, res) {
try {
const { id } = req.params
const user = await userService.getUserById(id)
if (!user) {
return error(res, 'User not found', 404)
}
return success(res, user)
} catch (err) {
return error(res, err.message, 500)
}
}
// 创建用户
async create(req, res) {
try {
const userData = req.body
const newUser = await userService.createUser(userData)
return success(res, newUser, 201)
} catch (err) {
return error(res, err.message, 400)
}
}
}
module.exports = new UserController()
```
:::
#### Service 层:业务的"厨师"
Service 是系统的"大脑",包含核心业务逻辑。
**职责**
- 实现业务规则和流程
- 协调多个 Repository 完成复杂操作
- 管理事务
- 数据转换和计算
**不应该做的事**
- 直接处理 HTTP 请求/响应
- 直接操作数据库(通过 Repository)
::: details 📝 Service 代码示例
```javascript
// services/userService.js
const userRepository = require('../repositories/userRepository')
const orderRepository = require('../repositories/orderRepository')
const emailService = require('./emailService')
const { hashPassword } = require('../utils/crypto')
class UserService {
// 获取用户列表
async getUsers({ page, limit }) {
const offset = (page - 1) * limit
const [users, total] = await Promise.all([
userRepository.findAll({ limit, offset }),
userRepository.count()
])
return {
data: users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
}
}
// 获取用户详情(包含订单信息)
async getUserById(id) {
const user = await userRepository.findById(id)
if (!user) return null
// 获取用户订单统计
const orderStats = await orderRepository.getStatsByUserId(id)
return {
...user,
orderStats
}
}
// 创建用户(包含事务和邮件通知)
async createUser(userData) {
// 检查邮箱是否已存在
const existingUser = await userRepository.findByEmail(userData.email)
if (existingUser) {
throw new Error('Email already exists')
}
// 密码加密
const hashedPassword = await hashPassword(userData.password)
// 创建用户
const newUser = await userRepository.create({
...userData,
password: hashedPassword
})
// 发送欢迎邮件(异步,不阻塞)
emailService.sendWelcomeEmail(newUser.email).catch(console.error)
return newUser
}
}
module.exports = new UserService()
```
:::
#### Repository 层:数据的"仓管员"
Repository 负责所有与数据存储相关的操作。
**职责**
- 数据库的增删改查
- ORM 映射
- 查询优化
**不应该做的事**
- 包含业务逻辑
- 处理事务(由 Service 控制)
::: details 📝 Repository 代码示例
```javascript
// repositories/userRepository.js
const { User } = require('../models')
class UserRepository {
// 查询所有用户
async findAll({ limit, offset }) {
return await User.findAll({
limit,
offset,
attributes: { exclude: ['password'] } // 不返回密码
})
}
// 根据 ID 查询
async findById(id) {
return await User.findByPk(id, {
attributes: { exclude: ['password'] }
})
}
// 根据邮箱查询
async findByEmail(email) {
return await User.findOne({ where: { email } })
}
// 创建用户
async create(data) {
return await User.create(data)
}
// 更新用户
async update(id, data) {
const user = await User.findByPk(id)
if (!user) return null
return await user.update(data)
}
// 删除用户
async delete(id) {
const user = await User.findByPk(id)
if (!user) return null
await user.destroy()
return true
}
// 统计用户数量
async count() {
return await User.count()
}
}
module.exports = new UserRepository()
```
:::
#### Model 层:数据的"定义"
Model 定义数据结构和业务规则。
::: details 📝 Model 代码示例(Sequelize
```javascript
// models/user.js
const { DataTypes } = require('sequelize')
const { sequelize } = require('../config/database')
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
validate: {
len: [2, 100]
}
},
email: {
type: DataTypes.STRING(255),
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING(255),
allowNull: false
},
status: {
type: DataTypes.ENUM('active', 'inactive', 'banned'),
defaultValue: 'active'
}
}, {
tableName: 'users',
timestamps: true, // 自动添加 createdAt 和 updatedAt
indexes: [
{ fields: ['email'] },
{ fields: ['status'] }
]
})
module.exports = User
```
:::
---
## 3. 其他重要目录
### 3.1 `middlewares/` 中间件
中间件是请求处理流程中的"过滤器"。
```
middlewares/
├── auth.js # 认证中间件
├── errorHandler.js # 错误处理
├── validator.js # 参数校验
├── rateLimiter.js # 限流
├── logger.js # 请求日志
└── cors.js # 跨域处理
```
::: details 📝 中间件示例
```javascript
// middlewares/auth.js
const jwt = require('jsonwebtoken')
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1]
if (!token) {
return res.status(401).json({ message: 'No token provided' })
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)
req.user = decoded
next()
} catch (err) {
return res.status(401).json({ message: 'Invalid token' })
}
}
module.exports = authMiddleware
```
:::
### 3.2 `routes/` 路由
集中管理所有 API 路由。
```javascript
// routes/userRoutes.js
const express = require('express')
const router = express.Router()
const userController = require('../controllers/userController')
const authMiddleware = require('../middlewares/auth')
// 公开路由
router.get('/', userController.list)
router.get('/:id', userController.getById)
// 需要认证的路由
router.post('/', authMiddleware, userController.create)
router.put('/:id', authMiddleware, userController.update)
router.delete('/:id', authMiddleware, userController.delete)
module.exports = router
```
```javascript
// routes/index.js
const express = require('express')
const router = express.Router()
router.use('/users', require('./userRoutes'))
router.use('/orders', require('./orderRoutes'))
router.use('/products', require('./productRoutes'))
module.exports = router
```
### 3.3 `config/` 配置
集中管理所有配置,支持多环境。
```javascript
// config/index.js
const env = process.env.NODE_ENV || 'development'
const configs = {
development: {
port: 3000,
database: {
host: 'localhost',
port: 5432,
name: 'myapp_dev'
},
redis: {
host: 'localhost',
port: 6379
}
},
production: {
port: process.env.PORT || 80,
database: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
name: process.env.DB_NAME
}
}
}
module.exports = configs[env]
```
### 3.4 `utils/` 工具
```
utils/
├── logger.js # 日志工具
├── response.js # 响应封装
├── crypto.js # 加密解密
├── date.js # 日期处理
└── validator.js # 验证工具
```
---
## 4. 按功能组织(Feature-based
对于中大型项目,可以采用按功能组织的方式:
```
src/
├── features/
│ ├── users/
│ │ ├── users.controller.js
│ │ ├── users.service.js
│ │ ├── users.repository.js
│ │ ├── users.model.js
│ │ ├── users.routes.js
│ │ ├── users.validator.js
│ │ └── index.js # 统一导出
│ ├── orders/
│ │ ├── orders.controller.js
│ │ ├── orders.service.js
│ │ └── ...
│ └── products/
│ ├── products.controller.js
│ └── ...
├── shared/ # 共享资源
│ ├── middlewares/
│ ├── utils/
│ └── config/
└── app.js
```
**优点**
- 高内聚,一个功能的所有代码在一起
- 便于团队协作,不同人负责不同 feature
- 易于删除或重构
---
## 5. 知名开源项目的架构参考
### 5.1 Express.js 官方示例
```
express-example/
├── bin/ # 启动脚本
├── public/ # 静态资源
├── routes/ # 路由
├── views/ # 视图模板
├── app.js # 应用配置
└── package.json
```
**特点**:简单直接,适合小型项目。
### 5.2 NestJS(企业级 Node.js 框架)
```
nestjs-project/
├── src/
│ ├── modules/ # 功能模块
│ │ ├── users/
│ │ │ ├── users.controller.ts
│ │ │ ├── users.service.ts
│ │ │ ├── users.module.ts
│ │ │ └── dto/
│ │ └── orders/
│ ├── common/ # 共享模块
│ ├── config/ # 配置
│ └── main.ts # 入口
```
**特点**
- 强制模块化结构
- 内置依赖注入
- 适合大型项目
### 5.3 DjangoPython
```
django-project/
├── project_name/ # 项目配置
├── apps/
│ ├── users/ # 用户应用
│ │ ├── models.py
│ │ ├── views.py
│ │ ├── serializers.py
│ │ └── urls.py
│ └── orders/ # 订单应用
├── templates/
├── static/
└── manage.py
```
**特点**
- 约定优于配置
- MTVModel-Template-View)模式
- 应用可复用
### 5.4 Spring BootJava
```
spring-boot-project/
├── src/main/java/
│ └── com/example/
│ ├── controller/
│ ├── service/
│ ├── repository/
│ ├── entity/
│ ├── dto/
│ ├── config/
│ └── Application.java
├── src/main/resources/
│ ├── application.yml
│ └── mapper/
└── src/test/
```
**特点**
- 严格的分层架构
- 注解驱动开发
- 强大的生态
---
## 6. 架构设计原则与检查清单
### 6.1 核心原则
| 原则 | 说明 | 实践建议 |
|------|------|----------|
| **单一职责** | 一个模块只做一件事 | Controller 只处理 HTTPService 只处理业务 |
| **依赖倒置** | 依赖抽象而非具体实现 | 使用接口/抽象类 |
| **开闭原则** | 对扩展开放,对修改关闭 | 新增功能不修改原有代码 |
| **DRY** | 不要重复自己 | 提取公共逻辑到 utils 或基类 |
| **KISS** | 保持简单 | 不要过度设计 |
### 6.2 检查清单
**分层检查**
- [ ] Controller 是否只处理 HTTP 相关逻辑?
- [ ] Service 是否包含核心业务逻辑?
- [ ] Repository 是否只负责数据访问?
- [ ] 层与层之间是否通过明确的接口交互?
**代码质量**
- [ ] 是否有统一的错误处理机制?
- [ ] 是否使用环境变量管理配置?
- [ ] 是否有日志记录?
- [ ] 是否编写了单元测试?
**安全**
- [ ] 敏感配置是否放入环境变量?
- [ ] 是否有输入验证?
- [ ] 是否有认证和授权?
- [ ] 密码是否加密存储?
---
## 7. 总结
::: tip 💡 核心思想
好的后端架构应该像一家组织良好的餐厅:
- **分工明确**:每个角色知道自己的职责
- **流程清晰**:数据像流水一样在各层之间传递
- **易于扩展**:新增功能不会破坏现有结构
- **便于测试**:各层可以独立测试
**记住这几点**
1. **分层是手段,不是目的**:不要为了分层而分层
2. **按功能组织**:中大型项目推荐 Feature-based
3. **统一约定**:命名、结构、错误处理保持一致
4. **持续重构**:定期审视架构,及时调整
**最终目标**:让代码像整理好的车间一样,想找什么立刻能找到,新功能容易添加,旧代码容易维护。
:::
---
## 参考资源
- [NestJS 文档](https://docs.nestjs.com/)
- [Express 最佳实践](https://expressjs.com/en/advanced/best-practice-security.html)
- [Bulletproof Node.js](https://github.com/santiq/bulletproof-nodejs)
- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
@@ -1,3 +1,162 @@
# 文件存储与对象存储
> 待实现
::: tip 前言
**用户上传了一张头像,你把它存在服务器的 `/uploads` 目录下——然后服务器磁盘满了,或者你加了第二台服务器,用户发现头像时有时无。** 文件存储看似简单,但在分布式环境下却是一个需要认真对待的架构问题。对象存储就是互联网时代解决这个问题的标准答案。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **存储类型认知**:理解块存储、文件存储、对象存储的区别和适用场景
- **对象存储核心概念**:掌握 Bucket、Object、Key、Pre-signed URL 等核心概念
- **上传方案设计**:学会客户端直传 vs 服务端中转的方案选型
- **CDN 加速原理**:理解 CDN 如何加速静态资源的全球分发
- **最佳实践**:掌握文件命名、权限控制、生命周期管理等实战技巧
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 存储类型对比 | 块存储、文件存储、对象存储 |
| **第 2 章** | 对象存储核心概念 | Bucket、Object、Key、元数据 |
| **第 3 章** | 文件上传方案 | 客户端直传、Pre-signed URL |
| **第 4 章** | CDN 加速 | 边缘节点、缓存策略、回源 |
| **第 5 章** | 最佳实践 | 命名规范、权限、生命周期 |
---
## 0. 全景图:为什么不能把文件存在服务器本地?
刚开始做项目时,把用户上传的文件存在服务器本地目录是最直觉的做法。但随着项目发展,你会遇到一系列问题:
- **磁盘空间有限**:服务器磁盘总会满,扩容麻烦
- **多服务器不共享**:负载均衡后,用户请求可能打到不同服务器,文件找不到
- **没有备份**:服务器挂了,文件就丢了
- **没有 CDN**:全球用户访问同一台服务器,速度慢
::: tip 对象存储的核心价值
对象存储(如 AWS S3、阿里云 OSS)解决了所有这些问题:**容量无限、全球可访问、自动备份、天然支持 CDN**。它已经成为互联网应用存储文件的事实标准。
:::
---
## 1. 存储类型对比:块、文件、对象
计算机世界有三种主要的存储方式,它们解决不同层次的问题。
<FileStorageTypeDemo />
| 维度 | 块存储 | 文件存储 | 对象存储 |
|------|--------|---------|---------|
| 数据单位 | 固定大小的块 | 文件 + 目录 | 对象(Key-Value |
| 访问协议 | iSCSI/FC | NFS/SMB | HTTP REST API |
| 性能 | 最高(毫秒级) | 中等 | 较低(但够用) |
| 扩展性 | 有限 | 中等 | 近乎无限 |
| 成本 | 最高 | 中等 | 最低 |
| 典型场景 | 数据库 | 共享文件 | 图片/视频/备份 |
::: tip 简单记忆
- **块存储**像硬盘——给数据库用
- **文件存储**像网络共享文件夹——给多台服务器共享配置用
- **对象存储**像网盘——给用户上传的图片、视频用
:::
---
## 2. 对象存储核心概念
对象存储的数据模型非常简单:**Bucket(桶)** 是容器,**Object(对象)** 是文件,每个对象通过唯一的 **Key(键)** 来标识。
```
my-app-bucket/ ← Bucket(桶)
├── avatars/user-123.jpg ← Object Key
├── avatars/user-456.png ← Object Key
├── reports/2024/q1-report.pdf ← Object Key"目录"只是 Key 的前缀)
└── uploads/temp/file.zip ← Object Key
```
| 概念 | 说明 | 示例 |
|------|------|------|
| Bucket | 存储容器,全局唯一命名 | `my-app-prod``company-assets` |
| Object | 存储的文件本体 + 元数据 | 一张图片、一个 PDF |
| Key | 对象的唯一标识符 | `avatars/user-123.jpg` |
| 元数据 | 对象的附加信息 | Content-Type、自定义标签 |
| ACL | 访问控制列表 | public-read、private |
| Pre-signed URL | 临时授权访问链接 | 有效期 15 分钟的上传/下载链接 |
::: tip 对象存储没有真正的"目录"
`avatars/user-123.jpg` 中的 `avatars/` 不是目录,只是 Key 的前缀。对象存储是扁平结构,所有对象在同一层级。控制台显示的"文件夹"只是按前缀分组的视觉效果。
:::
---
## 3. 文件上传方案:谁来传文件?
文件上传有两种主流方案:服务端中转和客户端直传。对于大多数场景,**客户端直传**是更优的选择。
<FileUploadFlowDemo />
::: tip 客户端直传的优势
1. **节省服务器带宽**:文件不经过你的服务器,直接到 OSS
2. **避免超时**:大文件上传不会触发 Nginx/网关的超时限制
3. **降低服务器负载**:服务器只需要签发凭证,不需要处理文件流
4. **支持断点续传**:OSS 原生支持分片上传,前端可以实现断点续传
实现步骤:前端请求后端获取 Pre-signed URL → 前端用这个 URL 直接上传到 OSS → OSS 回调通知后端
:::
---
## 4. CDN 加速:让全球用户都快
当你的用户遍布全球时,从单一源站下载文件会很慢。CDNContent Delivery Network)通过在全球部署边缘节点,将文件缓存到离用户最近的节点,大幅降低访问延迟。
<CDNAccelerationDemo />
| CDN 概念 | 说明 |
|---------|------|
| 边缘节点 | 分布在全球各地的缓存服务器 |
| 回源 | 边缘节点没有缓存时,向源站请求文件 |
| 缓存命中率 | 请求被边缘节点直接响应的比例,越高越好 |
| TTL | 缓存有效期,过期后需要重新回源 |
| 缓存刷新 | 主动清除边缘节点的缓存,让新文件生效 |
::: tip CDN 最佳实践
- **文件名加 hash**`logo.a3f2b1.png` 而不是 `logo.png`,这样更新文件时不需要刷新缓存
- **设置合理的 TTL**:静态资源(JS/CSS/图片)设长 TTL1年),HTML 设短 TTL5分钟)
- **开启 Gzip/Brotli 压缩**:文本类资源压缩后体积减少 60-80%
:::
---
## 5. 最佳实践
| 实践 | 说明 | 示例 |
|------|------|------|
| Key 命名规范 | 用有意义的前缀组织文件 | `{type}/{date}/{uuid}.{ext}` |
| 避免热点 Key | 不要用递增数字开头 | 用 UUID 或 hash 前缀 |
| 权限最小化 | Bucket 默认 private | 只对需要公开的文件设置 public-read |
| 生命周期规则 | 自动清理过期文件 | 临时文件 7 天后自动删除 |
| 跨域配置 | 前端直传需要配置 CORS | 允许你的域名 PUT/POST |
| 服务端加密 | 敏感文件开启 SSE | SSE-S3 或 SSE-KMS |
---
## 总结
文件存储是每个 Web 应用都会遇到的基础问题。对象存储以其无限容量、低成本、高可用的特性,成为了互联网应用的标准选择。
回顾本章的关键要点:
1. **三种存储类型**:块存储给数据库、文件存储给共享、对象存储给用户文件
2. **对象存储模型**Bucket + Key + Object,扁平结构,HTTP API 访问
3. **客户端直传**Pre-signed URL 方案,文件不经过服务器,高效省资源
4. **CDN 加速**:边缘节点缓存 + 文件名 hash,让全球用户都快
5. **安全与管理**:权限最小化、生命周期规则、服务端加密
## 延伸阅读
- [AWS S3 开发者指南](https://docs.aws.amazon.com/s3/) - 对象存储的标杆文档
- [阿里云 OSS 最佳实践](https://help.aliyun.com/document_detail/31853.html) - 国内最常用的对象存储
- [MinIO 文档](https://min.io/docs/minio/linux/index.html) - 开源的 S3 兼容对象存储
- [Cloudflare R2](https://developers.cloudflare.com/r2/) - 零出口费用的对象存储
- [Pre-signed URL 详解](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html) - 客户端直传的核心机制
@@ -1,3 +1,131 @@
# 限流与背压控制
> 待实现
::: tip 前言
**双十一零点,几亿用户同时涌入——服务器扛得住吗?** 任何系统都有处理能力的上限。当请求量超过系统承载能力时,如果不加控制,结果就是所有人都用不了。限流和背压就是保护系统不被"压垮"的两道防线。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **限流必要性**:理解为什么需要主动拒绝部分请求来保护系统
- **限流算法**:掌握令牌桶、漏桶、滑动窗口三种核心算法的原理和差异
- **背压机制**:理解当上游速度超过下游时的处理策略
- **多层限流**:了解从客户端到网关到服务的多层限流架构
- **实战能力**:知道在什么场景下选择什么限流策略
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 为什么需要限流 | 雪崩效应、服务保护 |
| **第 2 章** | 限流算法 | 令牌桶、漏桶、滑动窗口 |
| **第 3 章** | 背压控制 | 缓冲区、丢弃策略、弹性扩容 |
| **第 4 章** | 多层限流架构 | 客户端、网关、服务端 |
| **第 5 章** | 实战与选型 | Nginx、Redis、Sentinel |
---
## 0. 全景图:为什么要"拒绝"用户?
这听起来很反直觉——我们不是应该服务好每一个用户吗?但现实是:**不拒绝一部分请求,所有请求都会失败**。
想象一个只能坐 100 人的餐厅,突然涌进来 1000 人。如果不限流,结果不是 1000 人都能吃上饭,而是厨房崩溃、服务员瘫痪,1000 人谁都吃不上。正确的做法是在门口排队限流,让 100 人先进去,其余人等候。
::: tip 限流的核心目标
- **保护系统**:防止过载导致服务完全不可用
- **公平分配**:确保已接受的请求能正常处理
- **优雅降级**:被限流的请求收到明确的 429 状态码,而不是超时或 500 错误
:::
---
## 1. 限流算法:三种经典方案
限流的核心问题是:**在单位时间内,最多允许多少个请求通过?** 不同的算法在精确度、突发流量处理、实现复杂度上各有取舍。
<RateLimitAlgorithmDemo />
| 算法 | 原理 | 突发流量 | 精确度 | 实现复杂度 |
|------|------|---------|--------|-----------|
| 令牌桶 | 固定速率放令牌,请求消耗令牌 | 允许(桶中有存量) | 高 | 中 |
| 漏桶 | 请求排队,固定速率处理 | 不允许(完全平滑) | 高 | 中 |
| 滑动窗口 | 统计窗口内请求数 | 部分允许 | 较高 | 低 |
| 固定窗口 | 按时间窗口计数 | 边界处可能突发 | 低 | 最低 |
::: tip 选哪个算法?
- **API 限流**:令牌桶最常用,允许合理的突发流量
- **流量整形**:漏桶适合需要恒定输出速率的场景
- **简单计数**:滑动窗口实现简单,适合大多数 Web 应用
:::
---
## 2. 背压控制:当上游比下游快
限流解决的是"外部请求太多"的问题,而**背压(Backpressure**解决的是"内部组件速度不匹配"的问题。
当生产者产生数据的速度持续超过消费者处理数据的速度时,中间的缓冲区会不断膨胀,最终导致内存溢出或数据丢失。背压机制就是让消费者能够"反向通知"生产者减速。
<BackpressureDemo />
::: tip 背压的四种策略
1. **丢弃(Drop**:缓冲区满时丢弃新数据或旧数据,适合实时性要求高但允许丢失的场景
2. **阻塞(Block**:让生产者暂停,等消费者处理完再继续,适合数据不能丢失的场景
3. **采样(Sample**:只处理部分数据,适合高频数据流
4. **弹性扩容(Scale**:动态增加消费者数量,适合云原生环境
:::
---
## 3. 多层限流架构
生产环境中,限流不是在某一个点做就够了,而是需要**多层防护**,每一层解决不同粒度的问题。
| 层级 | 位置 | 限流粒度 | 工具 |
|------|------|---------|------|
| 客户端 | 前端/App | 按钮防抖、请求节流 | lodash.throttle、debounce |
| CDN/WAF | 边缘节点 | IP 级别、地域级别 | Cloudflare Rate Limiting |
| API 网关 | 入口网关 | 路由级别、用户级别 | Nginx limit_req、Kong |
| 服务端 | 应用内部 | 接口级别、资源级别 | Sentinel、Resilience4j |
| 数据库 | 存储层 | 连接数、QPS | 连接池配置、慢查询熔断 |
::: tip 限流的 HTTP 规范
被限流的请求应该返回 `429 Too Many Requests` 状态码,并在响应头中包含:
- `Retry-After`: 建议客户端多久后重试(秒数或日期)
- `X-RateLimit-Limit`: 限流上限
- `X-RateLimit-Remaining`: 剩余配额
- `X-RateLimit-Reset`: 配额重置时间
:::
---
## 4. 实战选型
| 场景 | 推荐方案 | 说明 |
|------|---------|------|
| Nginx 入口限流 | `limit_req_zone` | 基于漏桶算法,配置简单 |
| 分布式限流 | Redis + Lua 脚本 | 令牌桶或滑动窗口,多实例共享计数 |
| Java 微服务 | Sentinel / Resilience4j | 支持熔断、降级、热点限流 |
| Node.js API | express-rate-limit | 简单易用,支持 Redis 存储 |
| Go 服务 | golang.org/x/time/rate | 标准库令牌桶实现 |
---
## 总结
限流和背压是保护系统稳定性的两道关键防线。限流控制外部流量的涌入速度,背压协调内部组件的处理速度。
回顾本章的关键要点:
1. **限流的必要性**:不拒绝部分请求,所有请求都会失败
2. **三种核心算法**:令牌桶(允许突发)、漏桶(完全平滑)、滑动窗口(简单精确)
3. **背压机制**:丢弃、阻塞、采样、扩容四种策略
4. **多层防护**:从客户端到数据库,每层解决不同粒度的问题
5. **429 规范**:被限流时返回标准状态码和限流头信息
## 延伸阅读
- [Stripe 的限流实践](https://stripe.com/blog/rate-limiters) - 支付系统的限流设计
- [Nginx limit_req 文档](https://nginx.org/en/docs/http/ngx_http_limit_req_module.html) - Nginx 限流模块
- [Alibaba Sentinel](https://sentinelguard.io/) - 面向分布式服务的流量控制组件
- [Resilience4j](https://resilience4j.readme.io/) - Java 轻量级容错库
- [Token Bucket 算法详解](https://en.wikipedia.org/wiki/Token_bucket) - 令牌桶算法的数学原理
@@ -1,3 +1,295 @@
# 一个请求的完整旅程
> 待实现
::: tip 前言
**当你在浏览器里输入一个网址按下回车,到页面显示出来,中间到底发生了什么?** 这个问题是面试经典题,更是理解整个 Web 架构的钥匙。搞懂这条链路,你就能理解前端、后端、网络、数据库是怎么协作的。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **全链路视角**:理解一个 HTTP 请求从发出到返回的完整过程
- **各层职责认知**:DNS、TCP、负载均衡、Web 服务器、应用服务器、数据库各自做什么
- **问题定位能力**:请求慢或失败时,知道从哪一层开始排查
- **性能优化思路**:每一层都有优化空间,知道优化点在哪里
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 浏览器发起请求 | DNS 解析、TCP 连接、HTTP 请求 |
| **第 2 章** | 网络传输 | 路由、CDN、负载均衡 |
| **第 3 章** | 服务器处理 | Web 服务器、应用逻辑、数据库查询 |
| **第 4 章** | 响应返回 | 序列化、压缩、渲染 |
| **第 5 章** | 全链路优化 | 缓存、连接复用、异步处理 |
---
## 0. 全景图:一个请求经历了什么?
用一个比喻来理解:你在网上下单买书,这个过程和 HTTP 请求惊人地相似。
| 请求阶段 | 买书类比 | 技术对应 |
|---------|---------|---------|
| 输入网址 | 你说"我要去某某书店" | 浏览器解析 URL |
| DNS 解析 | 查地图找到书店地址 | 域名 → IP 地址 |
| TCP 连接 | 走到书店门口,推门进去 | 三次握手建立连接 |
| 发送请求 | 告诉店员"我要《xxx》这本书" | HTTP 请求报文 |
| 服务器处理 | 店员去仓库找书、查库存、算价格 | 应用逻辑 + 数据库查询 |
| 返回响应 | 店员把书递给你 | HTTP 响应报文 |
| 浏览器渲染 | 你打开书开始阅读 | HTML/CSS/JS 解析渲染 |
<RequestJourneyFlow />
---
## 1. 浏览器发起请求
### 1.1 URL 解析
当你输入 `https://api.example.com/books?id=123` 时,浏览器会把它拆解成几个部分:
| 部分 | 值 | 含义 |
|-----|-----|------|
| 协议 | `https` | 用加密方式通信 |
| 域名 | `api.example.com` | 服务器的"名字" |
| 路径 | `/books` | 要访问的资源 |
| 查询参数 | `id=123` | 附加条件 |
### 1.2 DNS 解析:域名 → IP 地址
计算机不认识域名,只认识 IP 地址(如 `93.184.216.34`)。DNS 就是互联网的"电话簿"。
```
浏览器缓存 → 系统缓存 → 路由器缓存 → ISP DNS → 根域名服务器
↓ 命中就直接用,不命中就往下查
```
::: tip DNS 缓存的意义
如果每次请求都从根域名服务器查起,全球互联网会被 DNS 查询压垮。所以每一层都有缓存,大部分请求在浏览器或系统层就能解析完成。
:::
### 1.3 TCP 三次握手
找到 IP 地址后,浏览器需要和服务器"建立连接"。TCP 用三次握手确保双方都准备好了:
```
客户端 → 服务器:你好,我想连接(SYN)
服务器 → 客户端:好的,我准备好了(SYN + ACK)
客户端 → 服务器:收到,开始通信(ACK)
```
如果是 HTTPS,还需要额外的 TLS 握手来协商加密方式。
### 1.4 发送 HTTP 请求
连接建立后,浏览器发送 HTTP 请求报文:
```http
GET /books?id=123 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGci...
User-Agent: Chrome/120.0
```
| 组成部分 | 内容 |
|---------|------|
| 请求行 | 方法(GET)+ 路径 + 协议版本 |
| 请求头 | 元信息:身份认证、期望的数据格式等 |
| 请求体 | POST/PUT 请求才有,携带要提交的数据 |
---
## 2. 网络传输:请求在路上
### 2.1 路由转发
请求离开你的电脑后,会经过多个路由器的转发,就像快递经过多个中转站:
```
你的电脑 → 家庭路由器 → 运营商网络 → 骨干网 → 目标机房
```
每个路由器根据 IP 地址决定"下一跳"往哪里转发。可以用 `traceroute` 命令查看请求经过了哪些节点。
### 2.2 CDN 加速
如果目标网站使用了 CDN(内容分发网络),请求可能不需要到达源服务器:
| 场景 | 走向 |
|-----|------|
| 请求静态资源(图片、CSS、JS) | CDN 边缘节点直接返回 |
| 请求动态数据(API) | 穿透 CDN,到达源服务器 |
CDN 的本质是"把内容提前放到离用户最近的地方"。
### 2.3 负载均衡
大型网站不会只有一台服务器。负载均衡器负责把请求分配到多台服务器上:
```
用户请求 → 负载均衡器 → 服务器 A(30% 流量)
→ 服务器 B(30% 流量)
→ 服务器 C(40% 流量)
```
常见的分配策略:
| 策略 | 原理 | 适用场景 |
|-----|------|---------|
| 轮询 | 依次分配 | 服务器配置相同 |
| 加权轮询 | 按权重分配 | 服务器配置不同 |
| IP 哈希 | 同一用户固定到同一台 | 需要会话保持 |
| 最少连接 | 分给当前连接最少的 | 请求处理时间差异大 |
---
## 3. 服务器处理:厨房里发生了什么
请求到达服务器后,会经过多层处理。
### 3.1 Web 服务器(Nginx / Apache
第一个接收请求的通常是 Web 服务器,它负责:
| 职责 | 说明 |
|-----|------|
| 静态文件服务 | 直接返回 HTML、CSS、JS、图片 |
| 反向代理 | 把 API 请求转发给后端应用 |
| SSL 终止 | 处理 HTTPS 加密解密 |
| 请求过滤 | 拦截恶意请求、限流 |
### 3.2 应用服务器处理
Web 服务器把请求转发给应用服务器(Node.js、Spring、Django 等),处理流程:
```
请求进入 → 中间件链 → 路由匹配 → 控制器 → 服务层 → 数据访问层
```
**中间件**做的事情:
1. 解析请求体(JSON、表单数据)
2. 验证身份(检查 Token
3. 检查权限(这个用户能访问这个接口吗?)
4. 记录日志(谁在什么时候访问了什么)
### 3.3 数据库查询
大部分请求最终都要和数据库打交道:
```
应用代码:SELECT * FROM books WHERE id = 123
数据库引擎:解析 SQL → 查询优化 → 执行计划 → 读取数据
返回结果:{ id: 123, title: "xxx", price: 59.9 }
```
::: tip 数据库是最常见的性能瓶颈
网络传输通常是毫秒级,应用逻辑也很快,但一个没有索引的数据库查询可能要几秒甚至几十秒。所以"慢请求"大概率是数据库查询慢。
:::
---
## 4. 响应返回:数据的归途
### 4.1 构造 HTTP 响应
服务器处理完后,构造响应报文:
```http
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: gzip
Cache-Control: max-age=3600
{"id": 123, "title": "xxx", "price": 59.9}
```
| 组成部分 | 内容 |
|---------|------|
| 状态行 | 协议版本 + 状态码(200 成功、404 未找到、500 服务器错误) |
| 响应头 | 数据格式、缓存策略、压缩方式等 |
| 响应体 | 实际的数据内容(JSON、HTML 等) |
### 4.2 数据压缩
服务器通常会用 gzip 或 brotli 压缩响应体,减少传输量:
| 压缩算法 | 压缩率 | 速度 |
|---------|--------|------|
| gzip | 约 70% | 快 |
| brotli | 约 80% | 较慢但压缩更好 |
一个 100KB 的 JSON,压缩后可能只有 20-30KB。
### 4.3 浏览器渲染
浏览器收到响应后:
1. **解析 HTML** → 构建 DOM 树
2. **解析 CSS** → 构建样式树
3. **合并** → 生成渲染树
4. **布局** → 计算每个元素的位置和大小
5. **绘制** → 把像素画到屏幕上
<RequestTimeline />
---
## 5. 全链路优化:每一层都能更快
### 5.1 各层优化手段
| 层级 | 优化手段 | 效果 |
|-----|---------|------|
| DNS | DNS 预解析、使用快速 DNS 服务 | 减少 DNS 查询时间 |
| 网络 | CDN、HTTP/2、连接复用 | 减少传输延迟 |
| 服务器 | 缓存(Redis)、异步处理 | 减少处理时间 |
| 数据库 | 索引、查询优化、读写分离 | 减少查询时间 |
| 前端 | 懒加载、代码分割、资源压缩 | 减少渲染时间 |
### 5.2 缓存:最有效的优化
缓存存在于请求链路的每一层:
```
浏览器缓存 → CDN 缓存 → 反向代理缓存 → 应用缓存(Redis)→ 数据库缓存
```
::: tip 缓存的本质
用空间换时间。把计算过的结果存起来,下次直接用,不用重新算。缓存命中率每提高 10%,系统性能可能提升数倍。
:::
### 5.3 请求失败时的排查思路
| 现象 | 可能的问题层 | 排查方法 |
|-----|------------|---------|
| 完全无响应 | DNS / 网络 | ping、nslookup |
| 连接超时 | 网络 / 服务器宕机 | telnet、curl |
| 返回 4xx | 客户端请求有误 | 检查 URL、参数、Token |
| 返回 5xx | 服务器内部错误 | 查看服务器日志 |
| 响应很慢 | 数据库 / 应用逻辑 | 查看慢查询日志、APM 工具 |
---
## 6. 总结
一个 HTTP 请求的完整旅程:
1. **浏览器**:解析 URL → DNS 查询 → TCP 连接 → 发送请求
2. **网络**:路由转发 → CDN 判断 → 负载均衡分发
3. **服务器**:Web 服务器接收 → 中间件处理 → 业务逻辑 → 数据库查询
4. **返回**:构造响应 → 压缩 → 网络传输 → 浏览器渲染
::: tip 理解全链路的价值
当你能在脑中画出请求的完整链路时,遇到任何问题都能快速定位到是哪一层出了问题。这是从"初级开发"到"能独立排查问题"的关键跨越。
:::
---
## 延伸阅读
- [HTTP 权威指南](https://developer.mozilla.org/zh-CN/docs/Web/HTTP) — MDN 的 HTTP 文档
- [High Performance Browser Networking](https://hpbn.co/) — 浏览器网络性能优化
- [What happens when...](https://github.com/alex/what-happens-when) — 经典的"输入 URL 后发生了什么"详解
@@ -1,3 +1,153 @@
# 搜索引擎原理
> 待实现
::: tip 前言
**你在淘宝搜"红色连衣裙",0.1 秒内从几十亿商品中找到了最相关的结果——这背后是怎么做到的?** 搜索引擎是互联网最核心的基础设施之一,从 Google 到电商站内搜索,它的核心原理都是一样的:倒排索引 + 相关性排序。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **倒排索引**:理解搜索引擎最核心的数据结构
- **分词技术**:了解中文分词的挑战和常见方案
- **相关性排序**:掌握 TF-IDF 和 BM25 的基本原理
- **Elasticsearch**:了解最流行的搜索引擎的架构和使用场景
- **搜索优化**:掌握同义词、纠错、高亮等实用搜索功能
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 倒排索引 | 正排索引 vs 倒排索引 |
| **第 2 章** | 分词与分析 | 中文分词、停用词、词干提取 |
| **第 3 章** | 相关性排序 | TF-IDF、BM25 |
| **第 4 章** | Elasticsearch | 分布式架构、分片、副本 |
| **第 5 章** | 搜索优化 | 同义词、纠错、自动补全 |
---
## 0. 全景图:搜索的本质是什么?
搜索的本质是一个**信息检索(Information Retrieval**问题:给定一个查询,从海量文档中找到最相关的结果,并按相关性排序返回。
这个过程分为两个阶段:
- **索引阶段(离线)**:提前把所有文档处理好,建立高效的查找结构
- **查询阶段(在线)**:用户输入关键词时,快速找到匹配的文档并排序
::: tip 为什么不能用数据库 LIKE 查询?
`SELECT * FROM products WHERE name LIKE '%红色连衣裙%'` 看起来能搜索,但它需要**全表扫描**——逐行检查每条记录。当数据量达到百万级时,这种查询会慢到不可用。倒排索引把这个 O(n) 的操作变成了 O(1) 的查找。
:::
---
## 1. 倒排索引:搜索引擎的"心脏"
传统数据库用的是**正排索引**:从文档 ID 找到文档内容。而搜索引擎用的是**倒排索引**:从关键词找到包含它的文档列表。
<InvertedIndexDemo />
| 索引类型 | 方向 | 查找方式 | 适用场景 |
|---------|------|---------|---------|
| 正排索引 | 文档 → 内容 | 知道 ID,查内容 | 数据库主键查询 |
| 倒排索引 | 关键词 → 文档列表 | 知道关键词,查文档 | 全文搜索 |
::: tip 倒排索引的构建过程
1. **文档收集**:获取所有需要被搜索的文档
2. **分词(Tokenization**:将文档拆分为一个个词语
3. **建立映射**:记录每个词语出现在哪些文档中(以及出现位置、频率等)
4. **持久化存储**:将索引写入磁盘,支持快速查找
:::
---
## 2. 分词与文本分析
分词是搜索引擎的第一步,也是中文搜索的最大挑战。英文天然以空格分词,但中文没有分隔符——"乒乓球拍卖了"可以分成"乒乓球/拍卖/了"或"乒乓/球拍/卖/了"。
| 分词方式 | 说明 | 示例 |
|---------|------|------|
| 标准分词 | 按空格和标点切分(英文) | "hello world" → ["hello", "world"] |
| 中文分词 | 基于词典或模型切分 | "搜索引擎" → ["搜索", "引擎"] |
| N-gram | 按固定长度滑动窗口切分 | "搜索" → ["搜索", "索引"] |
| 自定义词典 | 添加业务专有词汇 | "iPhone16ProMax" 作为一个词 |
::: tip 文本分析管道
分词只是文本分析的一步,完整的管道包括:
1. **字符过滤**:去除 HTML 标签、特殊字符
2. **分词**:将文本拆分为词语(Token
3. **停用词过滤**:去除"的"、"了"、"是"等无意义的高频词
4. **同义词扩展**:将"手机"扩展为"手机、电话、移动电话"
5. **词干提取**:将 "running" 还原为 "run"(英文)
:::
---
## 3. 相关性排序:哪个结果最"相关"?
找到匹配的文档只是第一步,更重要的是**排序**——把最相关的结果排在最前面。
| 算法 | 原理 | 特点 |
|------|------|------|
| TF-IDF | 词频(TF) × 逆文档频率(IDF) | 经典算法,简单有效 |
| BM25 | TF-IDF 的改进版,加入文档长度归一化 | Elasticsearch 默认算法 |
| 向量检索 | 将文档和查询转为向量,计算余弦相似度 | 支持语义搜索 |
::: tip TF-IDF 直觉理解
- **TF(词频)**:一个词在文档中出现越多次,这个文档越可能与该词相关
- **IDF(逆文档频率)**:一个词在越少的文档中出现,它的区分度越高
- "的"在所有文档中都出现(IDF 低),所以搜索"的"没有意义
- "Elasticsearch"只在少数文档中出现(IDF 高),搜索它能精确定位
:::
---
## 4. Elasticsearch:最流行的搜索引擎
Elasticsearch 是目前最流行的开源搜索引擎,基于 Apache Lucene 构建,提供分布式、RESTful API 的全文搜索能力。
| 概念 | 说明 |
|------|------|
| Index | 类似数据库的"表",存储同类文档 |
| Document | 一条记录,JSON 格式 |
| Shard | 分片,将索引拆分到多个节点 |
| Replica | 副本,提供高可用和读扩展 |
| Mapping | 字段类型定义,类似数据库 Schema |
| Analyzer | 文本分析器,定义分词规则 |
::: tip ES vs 数据库
Elasticsearch 不是用来替代数据库的,而是作为搜索层与数据库配合使用。典型架构:数据写入数据库 → 同步到 ES → 搜索请求走 ES → 详情请求走数据库。
:::
---
## 5. 搜索优化:让搜索更"聪明"
| 优化手段 | 说明 | 效果 |
|---------|------|------|
| 同义词 | "手机"也能搜到"电话" | 提高召回率 |
| 拼写纠错 | "iphoen" 自动纠正为 "iphone" | 容错性 |
| 自动补全 | 输入"苹"提示"苹果手机" | 提升体验 |
| 高亮 | 搜索结果中标红匹配词 | 直观展示 |
| 权重调整 | 标题匹配权重 > 内容匹配 | 提高精确度 |
| 过滤与聚合 | 按价格区间、品牌筛选 | 缩小范围 |
---
## 总结
搜索引擎是互联网应用的核心基础设施。理解倒排索引、分词、相关性排序这三个核心概念,就掌握了搜索引擎的本质。
回顾本章的关键要点:
1. **倒排索引**:从关键词到文档的反向映射,是搜索引擎的核心数据结构
2. **分词是基础**:中文分词是搜索质量的关键,需要选择合适的分词器
3. **BM25 排序**:基于词频和文档频率的相关性评分,是 ES 的默认算法
4. **ES 架构**:分片 + 副本实现分布式和高可用
5. **搜索优化**:同义词、纠错、补全让搜索更智能
## 延伸阅读
- [Elasticsearch 官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) - 最权威的 ES 参考
- [Elasticsearch 权威指南](https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html) - 中文入门指南
- [Apache Lucene](https://lucene.apache.org/) - ES 底层的搜索引擎库
- [MeiliSearch](https://www.meilisearch.com/) - 轻量级搜索引擎,适合中小项目
- [Typesense](https://typesense.org/) - 开源的即时搜索引擎
+8 -3
View File
@@ -49,9 +49,14 @@
description="从汇编到高级语言,理解编程语言的演进与分类"
/>
<NavCard
href="/zh-cn/appendix/1-computer-fundamentals/type-systems-compilers"
title="类型系统与编译原理入门"
description="静态类型 vs 动态类型,编译器如何理解你的代码"
href="/zh-cn/appendix/1-computer-fundamentals/compilers"
title="编译原理入门"
description="词法分析、语法分析、AST——编译器如何理解你的代码"
/>
<NavCard
href="/zh-cn/appendix/1-computer-fundamentals/type-systems"
title="类型系统入门"
description="静态类型 vs 动态类型,类型安全与类型推断"
/>
</NavGrid>