feat: enhance demo components with consistent styling and info boxes

- Add standardized header and info box components to all demo files
- Improve visual consistency with theme colors and spacing
- Add max-height and overflow-y for better content containment
- Update package.json build script with --force flag
- Add .gitignore entries for REFACTORING files
- Fix table formatting in audio-intro.md
This commit is contained in:
sanbuphy
2026-02-14 12:14:07 +08:00
parent cd2ce9e661
commit ebe2bf6109
70 changed files with 12307 additions and 10445 deletions
@@ -1,50 +1,496 @@
<!--
DocumentationDemo.vue - API 文档演示组件
展示 API 文档的编写规范和最佳实践
-->
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<div class="demo">
<div class="header">
<span class="icon">📚</span>
<span class="title">API 文档最好的 API 文档就是代码本身</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
文档演示组件占位符 - 待实现具体交互
</el-alert>
<div class="content">
<div class="tools-tabs">
<button
v-for="tool in tools"
:key="tool.id"
class="tool-btn"
:class="{ active: selectedTool === tool.id }"
@click="selectedTool = tool.id"
>
<span class="tool-icon">{{ tool.icon }}</span>
<span class="tool-name">{{ tool.name }}</span>
</button>
</div>
<div class="tool-detail" v-if="currentTool">
<div class="tool-header">
<div class="tool-title">{{ currentTool.name }}</div>
<div class="tool-tags">
<span class="tag" :class="tag.class" v-for="tag in currentTool.tags" :key="tag.text">
{{ tag.text }}
</span>
</div>
</div>
<div class="tool-description">
<p>{{ currentTool.description }}</p>
</div>
<div class="feature-section">
<h4>核心特性</h4>
<div class="feature-list">
<div v-for="(feature, idx) in currentTool.features" :key="idx" class="feature-item">
<span class="feature-icon"></span>
<span class="feature-text">{{ feature }}</span>
</div>
</div>
</div>
<div class="example-section">
<h4>文档示例OpenAPI 3.0</h4>
<div class="code-block">
<pre><code>{{ currentTool.example }}</code></pre>
</div>
</div>
<div class="tools-section">
<h4>🔧 推荐工具</h4>
<div class="tools-grid">
<div v-for="(rec, idx) in currentTool.recommendations" :key="idx" class="tool-card">
<div class="rec-name">{{ rec.name }}</div>
<div class="rec-desc">{{ rec.description }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
const title = ref('API文档演示')
const description = ref('展示RESTful API文档的编写规范和最佳实践,包括Swagger、OpenAPI等工具的使用')
const tools = [
{
id: 'openapi',
name: 'OpenAPI 规范',
icon: '📋',
tags: [
{ text: '行业标准', class: 'primary' },
{ text: '语言无关', class: 'secondary' }
],
description: 'OpenAPI Specification(原 Swagger)是描述 REST API 的标准格式,可以被工具解析生成交互式文档、客户端 SDK、服务器存根等。',
features: [
'标准化的 YAML/JSON 格式描述 API',
'支持路径、参数、响应模型、认证等完整定义',
'生态系统丰富,支持 100+ 工具',
'可以生成交互式文档(Swagger UI',
'可以从代码注释自动生成',
'支持 API 版本控制和演进'
],
example: `openapi: 3.0.0
info:
title: 用户服务 API
version: 1.0.0
description: 提供用户管理相关接口
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: 获取用户列表
parameters:
- name: page
in: query
schema:
type: integer
default: 1
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
code:
type: integer
data:
type: array
items:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email`,
recommendations: [
{ name: 'Swagger UI', description: '最流行的交互式文档界面' },
{ name: 'Redoc', description: '美观的现代文档生成器' },
{ name: 'Stoplight', description: '可视化的 API 设计平台' }
]
},
{
id: 'swagger',
name: 'Swagger 工具链',
icon: '🛠️',
tags: [
{ text: '工具集', class: 'success' },
{ text: '自动化', class: 'info' }
],
description: 'Swagger 是一套围绕 OpenAPI 规范构建的工具,包括编辑器、UI、代码生成器等,帮助开发者快速构建和使用 API。',
features: [
'Swagger Editor:在线编写和验证 OpenAPI 文档',
'Swagger UI:自动生成交互式文档',
'Swagger Codegen:根据文档生成客户端 SDK',
'支持主流编程语言和框架',
'集成到 CI/CD 流程',
'自动保持文档与代码同步'
],
example: `# Swagger Editor 示例配置
swagger: '2.0'
info:
title: 示例 API
version: '1.0.0'
host: api.example.com
basePath: /v1
schemes:
- https
paths:
/users:
get:
tags:
- Users
summary: 获取所有用户
produces:
- application/json
responses:
200:
description: 成功
schema:
type: object
properties:
code:
type: integer
data:
type: array`,
recommendations: [
{ name: 'Swagger Editor', description: '在线编辑器,实时预览' },
{ name: 'Swagger Codegen', description: '生成 40+ 种语言的客户端' },
{ name: 'Postman', description: '导入 OpenAPI 进行测试' }
]
},
{
id: 'best-practices',
name: '文档最佳实践',
icon: '⭐',
tags: [
{ text: '经验', class: 'warning' },
{ text: '规范', class: 'secondary' }
],
description: '好的 API 文档应该像用户手册一样清晰,让开发者不问问题就能完成集成。',
features: [
'每个接口都有完整的请求示例',
'提供多种语言的代码示例(curl、JavaScript、Python',
'错误码文档化,附带解决方案',
'提供沙箱环境或测试工具',
'包含认证流程和获取 Token 的方法',
'实时更新,与代码保持一致',
'版本变更日志和迁移指南'
],
example: `# 完整的接口文档示例
## 获取用户信息
**请求示例:**
\`\`\`bash
curl -X GET \\
https://api.example.com/v1/users/123 \\
-H "Authorization: Bearer YOUR_TOKEN"
\`\`\`
**成功响应:**
\`\`\`json
{
"code": 0,
"message": "success",
"data": {
"id": 123,
"name": "张三",
"email": "zhangsan@example.com"
}
}
\`\`\`
**错误响应:**
| 错误码 | 说明 | 解决方案 |
|--------|------|----------|
| 10010 | 用户不存在 | 检查 user_id 是否正确 |
| 10018 | Token 已过期 | 重新调用登录接口 |
**在线测试:**
[🚀 在 API Explorer 中测试](https://api.example.com/docs)`,
recommendations: [
{ name: 'API Blueprint', description: ' Markdown 风格的 API 文档' },
{ name: 'Docusaurus', description: ' Facebook 开源的文档平台' },
{ name: 'GitBook', description: '美观的文档托管平台' }
]
}
]
const selectedTool = ref('openapi')
const currentTool = computed(() =>
tools.find(t => t.id === selectedTool.value)
)
</script>
<style scoped>
.demo-container {
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.tools-tabs {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tool-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s ease;
}
.tool-btn:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
transform: translateY(-2px);
}
.tool-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.tool-icon {
font-size: 18px;
}
.tool-name {
font-weight: 600;
font-size: 14px;
}
.tool-detail {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
overflow: hidden;
}
.tool-header {
padding: 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
align-items: center;
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
.tool-title {
font-weight: 700;
font-size: 16px;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
.tool-tags {
display: flex;
gap: 8px;
}
.demo-content {
.tag {
padding: 4px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
}
.tag.primary {
background: #dbeafe;
color: #1d4ed8;
}
.tag.secondary {
background: #e0e7ff;
color: #4338ca;
}
.tag.success {
background: #dcfce7;
color: #16a34a;
}
.tag.info {
background: #ccfbf1;
color: #0f766e;
}
.tag.warning {
background: #fef3c7;
color: #d97706;
}
.tool-description {
padding: 16px;
font-size: 14px;
color: var(--vp-c-text-2);
line-height: 1.7;
border-bottom: 1px solid var(--vp-c-divider);
}
.feature-section, .example-section, .tools-section {
padding: 16px;
}
.feature-section h4, .example-section h4, .tools-section h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.feature-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.feature-item {
display: flex;
flex-direction: column;
gap: 16px;
align-items: flex-start;
gap: 8px;
padding: 8px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.feature-icon {
color: #22c55e;
font-weight: 700;
font-size: 16px;
flex-shrink: 0;
}
.feature-text {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.code-block {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.code-block pre {
margin: 0;
padding: 16px;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
}
.code-block code {
font-family: monospace;
color: var(--vp-c-text-1);
}
.tools-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.tool-card {
padding: 12px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
text-align: center;
}
.rec-name {
font-weight: 600;
font-size: 13px;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.rec-desc {
font-size: 12px;
color: var(--vp-c-text-2);
line-height: 1.4;
}
@media (max-width: 768px) {
.tools-tabs {
flex-direction: column;
}
.feature-list {
grid-template-columns: 1fr;
}
.tools-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,97 +1,379 @@
<!--
ErrorHandlingDemo.vue - 错误处理演示组件
展示错误处理的正确和错误示例对比
-->
<template>
<div class="demo-container">
<div class="demo-header">
<h4>错误处理演示</h4>
<p class="hint">展示RESTful API中的错误处理机制</p>
<div class="demo">
<div class="header">
<span class="icon">🚨</span>
<span class="title">错误处理优雅地"拒绝"</span>
</div>
<div class="demo-content">
<div class="error-types">
<div class="error-item">
<span class="code">400</span>
<span class="name">Bad Request</span>
<span class="desc">请求参数错误</span>
<div class="content">
<div class="comparison-tabs">
<button
class="tab-btn bad"
:class="{ active: selectedTab === 'bad' }"
@click="selectedTab = 'bad'"
>
错误示范
</button>
<button
class="tab-btn good"
:class="{ active: selectedTab === 'good' }"
@click="selectedTab = 'good'"
>
正确示范
</button>
</div>
<!-- 错误示范 -->
<div v-if="selectedTab === 'bad'" class="comparison bad">
<div class="response-preview">
<div class="status-line bad">
<span>HTTP/1.1</span>
<span class="code">200 OK</span>
</div>
<div class="response-body">
<pre><code>{
"error": "出错了"
}</code></pre>
</div>
</div>
<div class="error-item">
<span class="code">401</span>
<span class="name">Unauthorized</span>
<span class="desc">未授权访问</span>
<div class="problems">
<h4>问题分析</h4>
<ul>
<li>
<span class="icon"></span>
HTTP 状态码说"成功"但业务说"出错" - 前后端状态不一致
</li>
<li>
<span class="icon"></span>
错误信息太笼统无法定位问题
</li>
<li>
<span class="icon"></span>
没有错误代码难以程序化判断
</li>
<li>
<span class="icon"></span>
浏览器和 CDN 会缓存这个"成功的"响应
</li>
</ul>
</div>
<div class="error-item">
<span class="code">404</span>
<span class="name">Not Found</span>
<span class="desc">资源不存在</span>
</div>
<!-- 正确示范 -->
<div v-if="selectedTab === 'good'" class="comparison good">
<div class="response-preview">
<div class="status-line">
<span>HTTP/1.1</span>
<span class="code">422 Unprocessable Entity</span>
</div>
<div class="response-body">
<pre><code>{{ JSON.stringify(goodResponse, null, 2) }}</code></pre>
</div>
</div>
<div class="error-item">
<span class="code">500</span>
<span class="name">Server Error</span>
<span class="desc">服务器内部错误</span>
<div class="highlights">
<h4>正确做法</h4>
<ul>
<li>
<span class="icon"></span>
<strong>正确的 HTTP 状态码</strong>: 422 表示语义错误
</li>
<li>
<span class="icon"></span>
<strong>业务错误码</strong>: `code: 20003` 可用于程序判断
</li>
<li>
<span class="icon"></span>
<strong>详细错误信息</strong>: `errors` 数组包含具体字段和原因
</li>
<li>
<span class="icon"></span>
<strong>可追踪性</strong>: `request_id` 用于日志查询
</li>
<li>
<span class="icon"></span>
<strong>帮助链接</strong>: `help_url` 引导用户查看文档
</li>
</ul>
</div>
<div class="error-codes">
<h4>错误码体系</h4>
<div class="code-list">
<div v-for="item in errorCodeItems" :key="item.code" class="code-item">
<span class="code-badge">{{ item.code }}</span>
<span class="code-desc">{{ item.desc }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedTab = ref('bad')
const goodResponse = {
code: 20003,
message: '密码强度不足',
errors: [
{
field: 'password',
code: 'VALIDATION_ERROR',
message: '密码必须包含至少 1 个大写字母、1 个小写字母、1 个数字,且长度至少 8 位'
}
],
request_id: 'req-550e8400-e29b-41d4-a716-44665544000',
timestamp: '2024-01-15T09:30:00.000Z',
help_url: 'https://docs.example.com/errors/20003'
}
const errorCodeItems = [
{ code: '1XXYY', desc: '通用错误(第1位固定为1' },
{ code: '10001', desc: '参数错误' },
{ code: '10010', desc: '用户不存在' },
{ code: '10018', desc: 'Token 已过期' },
{ code: '10021', desc: '权限不足' },
{ code: '20003', desc: '密码强度不足' },
{ code: '20014', desc: '余额不足' }
]
</script>
<style scoped>
.demo-container {
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
display: flex;
flex-direction: column;
gap: 16px;
}
.error-types {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.error-item {
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.comparison-tabs {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
gap: 12px;
margin-bottom: 20px;
}
.tab-btn {
flex: 1;
padding: 12px;
border: 2px solid;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.tab-btn.bad {
border-color: #ef4444;
background: var(--vp-c-bg);
color: #ef4444;
}
.tab-btn.bad:hover {
background: #fef2f2;
}
.tab-btn.bad.active {
background: #ef4444;
color: white;
}
.tab-btn.good {
border-color: #22c55e;
background: var(--vp-c-bg);
color: #22c55e;
}
.tab-btn.good:hover {
background: #f0fdf4;
}
.tab-btn.good.active {
background: #22c55e;
color: white;
}
.comparison {
background: var(--vp-c-bg);
border-radius: 8px;
border-left: 4px solid #f56c6c;
overflow: hidden;
}
.error-item .code {
.response-preview {
margin-bottom: 20px;
}
.status-line {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
font-family: monospace;
font-weight: 600;
color: #f56c6c;
font-size: 16px;
min-width: 50px;
font-size: 13px;
}
.error-item .name {
font-weight: 500;
.status-line.bad .code {
color: #ef4444;
font-weight: 700;
}
.status-line:not(.bad) .code {
color: #d97706;
font-weight: 700;
}
.response-body {
padding: 16px;
}
.response-body pre {
margin: 0;
background: var(--vp-c-bg-alt);
padding: 16px;
border-radius: 6px;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
}
.response-body code {
font-family: monospace;
color: var(--vp-c-text-1);
min-width: 120px;
}
.error-item .desc {
color: var(--vp-c-text-2);
.problems, .highlights {
padding: 16px;
}
.problems h4, .highlights h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.problems ul, .highlights ul {
margin: 0;
padding-left: 0;
list-style: none;
}
.problems li, .highlights li {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px;
margin-bottom: 8px;
border-radius: 6px;
line-height: 1.6;
font-size: 13px;
}
.problems li {
background: #fef2f2;
color: #991b1b;
}
.highlights li {
background: #f0fdf4;
color: #166534;
}
.problems li .icon, .highlights li .icon {
font-size: 16px;
flex-shrink: 0;
}
.problems li strong, .highlights li strong {
font-weight: 600;
}
.error-codes {
padding: 16px;
border-top: 1px solid var(--vp-c-divider);
}
.error-codes h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.code-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.code-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
}
.code-badge {
font-family: monospace;
font-size: 12px;
font-weight: 700;
padding: 4px 8px;
background: var(--vp-c-brand);
color: white;
border-radius: 4px;
min-width: 70px;
text-align: center;
}
.code-desc {
font-size: 13px;
color: var(--vp-c-text-2);
}
@media (max-width: 640px) {
.comparison-tabs {
flex-direction: column;
}
.status-line {
flex-wrap: wrap;
}
}
</style>
@@ -1,50 +1,397 @@
<!--
ResponseStructureDemo.vue - HTTP 响应结构演示组件
展示标准化 API 响应结构和分页响应
-->
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<div class="demo">
<div class="header">
<span class="icon">📦</span>
<span class="title">HTTP 响应结构标准化的数据契约</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
响应结构演示组件占位符 - 待实现具体交互
</el-alert>
<div class="content">
<div class="response-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
class="tab-btn"
:class="{ active: selectedTab === tab.id }"
@click="selectedTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<div class="response-detail">
<div class="response-header">
<div class="status-line">
<span class="http-version">HTTP/1.1</span>
<span class="status-code" :class="getStatusClass(currentResponse.status)">{{ currentResponse.status }}</span>
<span class="status-text">{{ currentResponse.statusText }}</span>
</div>
</div>
<div class="response-headers">
<div class="header-item">
<span class="header-key">Content-Type:</span>
<span class="header-value">application/json</span>
</div>
<div class="header-item">
<span class="header-key">X-Request-ID:</span>
<span class="header-value">req-550e8400-e29b-41d4</span>
</div>
<div class="header-item">
<span class="header-key">X-Response-Time:</span>
<span class="header-value">45ms</span>
</div>
</div>
<div class="response-body">
<pre><code>{{ JSON.stringify(currentResponse.body, null, 2) }}</code></pre>
</div>
<div class="field-descriptions">
<h4>字段说明</h4>
<div class="field-list">
<div v-for="field in currentResponse.fields" :key="field.name" class="field-item">
<div class="field-name">
<code>{{ field.name }}</code>
<span class="field-type">{{ field.type }}</span>
</div>
<div class="field-desc">{{ field.description }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
const title = ref('HTTP响应结构演示')
const description = ref('展示HTTP响应的结构,包括状态行、响应头和响应')
const tabs = [
{ id: 'success', name: '成功响应' },
{ id: 'pagination', name: '分页响应' },
{ id: 'error', name: '错误响应' }
]
const responses = {
success: {
status: 200,
statusText: 'OK',
body: {
code: 0,
message: 'success',
data: {
id: 123,
name: '张三',
email: 'zhangsan@example.com',
phone: '13800138000',
created_at: '2024-01-15T10:30:00.000Z'
},
request_id: 'req-550e8400-e29b-41d4-a716-446655440000',
timestamp: '2024-01-15T10:30:00.000Z'
},
fields: [
{ name: 'code', type: 'integer', description: '业务状态码,0 表示成功' },
{ name: 'message', type: 'string', description: '状态描述,成功时为 "success"' },
{ name: 'data', type: 'object', description: '业务数据,成功时返回具体数据' },
{ name: 'request_id', type: 'string', description: '请求唯一标识,用于问题追踪' },
{ name: 'timestamp', type: 'string', description: '响应时间戳,ISO 8601 格式' }
]
},
pagination: {
status: 200,
statusText: 'OK',
body: {
code: 0,
message: 'success',
data: {
list: [
{ id: 1, name: '商品A', price: 100 },
{ id: 2, name: '商品B', price: 200 }
],
pagination: {
page: 1,
page_size: 20,
total: 156,
total_pages: 8,
has_next: true,
has_prev: false
}
},
request_id: 'req-550e8400-e29b-41d4-a716-446655440000',
timestamp: '2024-01-15T10:30:00.000Z'
},
fields: [
{ name: 'list', type: 'array', description: '数据列表' },
{ name: 'pagination', type: 'object', description: '分页信息对象' },
{ name: 'page', type: 'integer', description: '当前页码' },
{ name: 'page_size', type: 'integer', description: '每页数量' },
{ name: 'total', type: 'integer', description: '总记录数' },
{ name: 'total_pages', type: 'integer', description: '总页数' },
{ name: 'has_next', type: 'boolean', description: '是否有下一页' },
{ name: 'has_prev', type: 'boolean', description: '是否有上一页' }
]
},
error: {
status: 422,
statusText: 'Unprocessable Entity',
body: {
code: 20003,
message: '密码强度不足',
errors: [
{
field: 'password',
code: 'VALIDATION_ERROR',
message: '密码必须包含至少 1 个大写字母、1 个小写字母、1 个数字,且长度至少 8 位'
}
],
request_id: 'req-550e8400-e29b-41d4-a716-446655440000',
timestamp: '2024-01-15T10:30:00.000Z',
help_url: 'https://docs.example.com/errors/20003'
},
fields: [
{ name: 'code', type: 'integer', description: '错误码,非 0 表示失败' },
{ name: 'message', type: 'string', description: '错误描述,供用户阅读' },
{ name: 'errors', type: 'array', description: '详细错误信息数组(可选)' },
{ name: 'help_url', type: 'string', description: '错误文档链接(可选)' }
]
}
}
const selectedTab = ref('success')
const currentResponse = computed(() => responses[selectedTab.value])
function getStatusClass(status) {
const prefix = Math.floor(status / 100)
switch (prefix) {
case 2: return 'success'
case 3: return 'redirect'
case 4: return 'client-error'
case 5: return 'server-error'
default: return ''
}
}
</script>
<style scoped>
.demo-container {
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.demo-header {
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.response-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
.tab-btn {
padding: 8px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
}
.hint {
margin: 0;
font-size: 14px;
.tab-btn:hover {
transform: translateY(-1px);
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
transform: scale(1.05);
}
.response-detail {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.response-header {
padding: 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.status-line {
display: flex;
align-items: center;
gap: 12px;
}
.http-version {
font-family: monospace;
font-size: 13px;
color: var(--vp-c-text-2);
}
.demo-content {
.status-code {
padding: 4px 8px;
border-radius: 4px;
font-weight: 700;
font-size: 13px;
font-family: monospace;
}
.status-code.success {
background: #dcfce7;
color: #16a34a;
}
.status-code.client-error {
background: #fef3c7;
color: #d97706;
}
.status-text {
font-size: 13px;
color: var(--vp-c-text-2);
}
.response-headers {
padding: 12px 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.header-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
font-size: 12px;
}
.header-key {
font-family: monospace;
font-weight: 600;
color: var(--vp-c-text-2);
min-width: 120px;
}
.header-value {
font-family: monospace;
color: var(--vp-c-text-1);
}
.response-body {
padding: 16px;
border-bottom: 1px solid var(--vp-c-divider);
}
.response-body pre {
margin: 0;
background: var(--vp-c-bg-alt);
padding: 16px;
border-radius: 6px;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
}
.response-body code {
font-family: monospace;
color: var(--vp-c-text-1);
}
.field-descriptions {
padding: 16px;
}
.field-descriptions h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.field-list {
display: flex;
flex-direction: column;
gap: 16px;
gap: 12px;
}
.field-item {
padding: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
}
.field-name {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.field-name code {
font-family: monospace;
font-size: 13px;
color: var(--vp-c-brand);
font-weight: 600;
}
.field-type {
font-size: 11px;
padding: 2px 6px;
background: rgba(var(--vp-c-brand-rgb), 0.1);
color: var(--vp-c-brand);
border-radius: 4px;
font-family: monospace;
}
.field-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
@media (max-width: 640px) {
.response-tabs {
flex-direction: column;
}
.status-line {
flex-wrap: wrap;
}
.header-item {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -1,50 +1,413 @@
<!--
VersioningStrategyDemo.vue - API 版本控制策略演示
展示 4 种版本控制策略的对比
-->
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<div class="demo">
<div class="header">
<span class="icon">🔢</span>
<span class="title">API 版本控制向后兼容的艺术</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
版本策略演示组件占位符 - 待实现具体交互
</el-alert>
<div class="content">
<div class="strategies">
<div
v-for="strategy in strategies"
:key="strategy.id"
class="strategy-card"
:class="{ active: selectedStrategy === strategy.id }"
@click="selectedStrategy = strategy.id"
>
<div class="strategy-header">
<div class="strategy-name">{{ strategy.name }}</div>
<div class="strategy-stars">
<span v-for="n in strategy.stars" :key="n" class="star"></span>
</div>
</div>
<div class="strategy-example">{{ strategy.example }}</div>
</div>
</div>
<div class="strategy-detail" v-if="currentStrategy">
<div class="detail-header">
<div class="detail-title">{{ currentStrategy.name }}</div>
<div class="detail-recommendation" :class="currentStrategy.level">
{{ currentStrategy.level === 'high' ? '强烈推荐' : currentStrategy.level === 'medium' ? '可以使用' : '不推荐' }}
</div>
</div>
<div class="detail-sections">
<div class="detail-section">
<h4> 优点</h4>
<ul>
<li v-for="(pro, idx) in currentStrategy.pros" :key="idx">{{ pro }}</li>
</ul>
</div>
<div class="detail-section">
<h4> 缺点</h4>
<ul>
<li v-for="(con, idx) in currentStrategy.cons" :key="idx">{{ con }}</li>
</ul>
</div>
</div>
<div class="detail-section example">
<h4>💻 实现示例</h4>
<div class="code-box">
<div class="code-header">Request</div>
<pre><code>{{ currentStrategy.codeExample.request }}</code></pre>
<div class="code-header">Response Headers</div>
<pre><code>{{ currentStrategy.codeExample.response }}</code></pre>
</div>
</div>
<div class="detail-section tips">
<h4>💡 最佳实践</h4>
<ul>
<li v-for="(tip, idx) in currentStrategy.tips" :key="idx">{{ tip }}</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
const title = ref('版本策略演示')
const description = ref('展示API版本控制的策略,包括URL版本、Header版本、内容协商等方式')
const strategies = [
{
id: 'url-path',
name: 'URL Path 版本',
example: '/v1/users',
stars: 4,
level: 'high',
pros: [
'最直观,一目了然看到版本号',
'易于缓存和控制权限',
'文档清晰,社区主流做法',
'支持不同版本的并行部署'
],
cons: [
'URL 会变化,不符合 REST 资源唯一性',
'需要配置路由规则'
],
codeExample: {
request: `GET /v1/users HTTP/1.1
Host: api.example.com`,
response: `HTTP/1.1 200 OK
Content-Type: application/json
X-API-Version: v1`
},
tips: [
'版本号放在路径开头:`/v1/users`',
'使用语义化版本号(Semantic Versioning',
'废弃版本返回 Sunset 头部',
'客户端升级提示可通过响应头提示'
]
},
{
id: 'header',
name: 'Header 版本',
example: 'API-Version: v1',
stars: 2,
level: 'medium',
pros: [
'URL 保持简洁不变',
'版本控制不影响路由'
],
cons: [
'不直观,需要在工具里配置 Header',
'缓存策略复杂',
'文档不够清晰',
'调试不便'
],
codeExample: {
request: `GET /users HTTP/1.1
Host: api.example.com
API-Version: v1`,
response: `HTTP/1.1 200 OK
Content-Type: application/json
X-API-Version: v1`
},
tips: [
'使用自定义 Header`API-Version` 或 `Accept`',
'需在 API Gateway 中统一处理',
'适合内部系统或对 API 规范要求高的场景'
]
},
{
id: 'content-negotiation',
name: '内容协商',
example: 'Accept: application/vnd.api.v1+json',
stars: 2,
level: 'medium',
pros: [
'符合 HTTP 标准',
'URL 完全不变'
],
cons: [
'复杂,理解成本高',
'开发者容易用错',
'缓存和代理支持不佳'
],
codeExample: {
request: `GET /users HTTP/1.1
Host: api.example.com
Accept: application/vnd.api.v1+json`,
response: `HTTP/1.1 200 OK
Content-Type: application/vnd.api.v1+json`
},
tips: [
'使用 Vendor MIME 类型:`application/vnd.{company}.{resource}.v{version}+json`',
'需要 API Gateway 或框架支持内容协商',
'GitHub API 使用此策略'
]
},
{
id: 'query-param',
name: 'Query 参数',
example: '/users?version=v1',
stars: 1,
level: 'low',
pros: [
'实现简单'
],
cons: [
'不专业,容易忽视',
'缓存麻烦(不同参数视为不同资源)',
'URL 混乱'
],
codeExample: {
request: `GET /users?version=v1 HTTP/1.1
Host: api.example.com`,
response: `HTTP/1.1 200 OK
Content-Type: application/json`
},
tips: [
'仅用于快速原型或内部工具',
'生产环境不推荐使用'
]
}
]
const selectedStrategy = ref('url-path')
const currentStrategy = computed(() =>
strategies.find(s => s.id === selectedStrategy.value)
)
</script>
<style scoped>
.demo-container {
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.demo-header {
margin-bottom: 20px;
.header {
padding: 16px 20px;
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.demo-header h4 {
margin: 0 0 8px 0;
.icon {
font-size: 24px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.content {
padding: 24px;
}
.strategies {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.strategy-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.strategy-card:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
transform: translateY(-2px);
}
.strategy-card.active {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.15);
}
.strategy-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.strategy-name {
font-weight: 600;
font-size: 14px;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
.strategy-stars {
display: flex;
gap: 2px;
}
.demo-content {
.star {
font-size: 12px;
}
.strategy-example {
font-family: monospace;
font-size: 12px;
color: var(--vp-c-text-2);
background: var(--vp-c-bg-soft);
padding: 6px 10px;
border-radius: 4px;
margin-top: 8px;
}
.strategy-detail {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.detail-header {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 16px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-title {
font-weight: 700;
font-size: 16px;
color: var(--vp-c-text-1);
}
.detail-recommendation {
padding: 4px 12px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.detail-recommendation.high {
background: #dcfce7;
color: #16a34a;
}
.detail-recommendation.medium {
background: #fef3c7;
color: #d97706;
}
.detail-recommendation.low {
background: #fee2e2;
color: #dc2626;
}
.detail-sections {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
padding: 16px;
}
.detail-section {
padding: 16px;
}
.detail-section h4 {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin: 0 0 12px 0;
}
.detail-section ul {
margin: 0;
padding-left: 20px;
}
.detail-section li {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 6px 0;
}
.detail-section.example {
grid-column: 1 / -1;
padding: 16px;
background: var(--vp-c-bg-soft);
}
.code-box {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
}
.code-header {
padding: 8px 12px;
background: var(--vp-c-bg-soft);
font-size: 12px;
font-weight: 600;
color: var(--vp-c-text-2);
border-bottom: 1px solid var(--vp-c-divider);
}
.code-box pre {
margin: 0;
padding: 12px;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
}
.code-box code {
font-family: monospace;
color: var(--vp-c-text-1);
}
.detail-section.tips {
background: #eff6ff;
border-left: 3px solid #3b82f6;
}
@media (max-width: 768px) {
.strategies {
grid-template-columns: 1fr;
}
.detail-sections {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="architecture-comparison-demo">
<div class="demo-header">
<h4>🏗 架构演进对比</h4>
<p>四个时代的核心架构特征对比</p>
<span class="icon">🏗</span>
<span class="title">架构演进对比</span>
<span class="subtitle">四个时代的核心架构特征</span>
</div>
<div class="comparison-grid">
@@ -50,6 +51,11 @@
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想:</strong>架构演进是为了解决上一个时代的痛点,但也带来了新的复杂度
</div>
</div>
</template>
@@ -108,24 +114,35 @@ const currentEra = computed(() => {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.comparison-grid {
@@ -152,8 +169,7 @@ const currentEra = computed(() => {
.era-card.active {
border-color: var(--vp-c-brand);
background: rgba(102, 126, 234, 0.1);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
background: var(--vp-c-brand-soft);
}
.era-icon {
@@ -271,6 +287,25 @@ const currentEra = computed(() => {
color: var(--vp-c-text-2);
}
.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: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
@media (max-width: 768px) {
.comparison-grid {
grid-template-columns: repeat(2, 1fr);
@@ -1,8 +1,9 @@
<template>
<div class="container-docker-demo">
<div class="demo-header">
<h4>🐳 Docker 容器化演示</h4>
<p>理解容器如何让应用"一次打包,到处运行"</p>
<span class="icon">🐳</span>
<span class="title">Docker 容器化演示</span>
<span class="subtitle">理解容器如何让应用"一次打包,到处运行"</span>
</div>
<div class="docker-visualization">
@@ -46,6 +47,11 @@
<div class="benefit-desc">{{ benefit.desc }}</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>容器化让应用"一次构建,到处运行"解决了环境一致性和快速部署的问题
</div>
</div>
</template>
@@ -69,24 +75,35 @@ const benefits = [
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.docker-visualization {
@@ -268,4 +285,23 @@ const benefits = [
grid-template-columns: 1fr;
}
}
.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;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="evolution-intro-demo">
<div class="intro-header">
<h3>后端架构进化之旅</h3>
<p>用一个餐厅的成长历程理解后端架构的 30 年变迁</p>
<div class="demo-header">
<span class="icon">🏗</span>
<span class="title">后端架构进化之旅</span>
<span class="subtitle">用餐厅比喻理解 30 年架构演进</span>
</div>
<div class="timeline-cards">
@@ -20,7 +21,7 @@
</div>
</div>
<div class="stage-detail">
<div class="stage-detail" v-if="currentStage !== null">
<Transition name="fade" mode="out-in">
<div :key="currentStage" class="detail-panel">
<div class="detail-header">
@@ -46,6 +47,11 @@
</div>
</Transition>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>架构演进是为了解决上一个时代的痛点但也带来了新的复杂度没有最好的架构只有最适合的架构
</div>
</div>
</template>
@@ -116,84 +122,95 @@ const stages = [
<style scoped>
.evolution-intro-demo {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 32px;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.intro-header {
text-align: center;
margin-bottom: 32px;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.intro-header h3 {
font-size: 24px;
font-weight: 700;
margin: 0 0 8px 0;
.demo-header .icon {
font-size: 1.25rem;
}
.intro-header p {
font-size: 14px;
opacity: 0.9;
margin: 0;
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.timeline-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 24px;
gap: 0.75rem;
margin-bottom: 1rem;
}
.stage-card {
background: rgba(255, 255, 255, 0.1);
background: var(--vp-c-bg);
border: 2px solid transparent;
border-radius: 12px;
padding: 16px 12px;
border-radius: 8px;
padding: 0.75rem 0.5rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
transition: all 0.3s;
}
.stage-card:hover {
background: rgba(255, 255, 255, 0.15);
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.stage-card.active {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.5);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stage-era {
font-size: 11px;
opacity: 0.7;
margin-bottom: 4px;
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-bottom: 0.25rem;
}
.stage-icon {
font-size: 32px;
margin-bottom: 8px;
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.stage-name {
font-size: 14px;
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 2px;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.stage-arch {
font-size: 11px;
opacity: 0.7;
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.stage-detail {
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
padding: 24px;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
}
.detail-panel {
@@ -208,56 +225,76 @@ const stages = [
.detail-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-icon {
font-size: 32px;
font-size: 1.5rem;
}
.detail-header h4 {
font-size: 20px;
font-size: 1rem;
font-weight: 600;
margin: 0;
color: var(--vp-c-text-1);
}
.detail-content {
display: grid;
gap: 16px;
gap: 0.75rem;
}
.detail-section h5 {
font-size: 14px;
font-size: 0.85rem;
font-weight: 600;
margin: 0 0 8px 0;
color: #ffd700;
margin: 0 0 0.5rem 0;
color: var(--vp-c-brand);
}
.detail-section p {
font-size: 13px;
font-size: 0.8rem;
line-height: 1.6;
margin: 0;
opacity: 0.9;
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-2);
}
.detail-section ul {
margin: 0;
padding-left: 18px;
padding-left: 1rem;
}
.detail-section li {
font-size: 13px;
font-size: 0.8rem;
line-height: 1.6;
margin-bottom: 4px;
opacity: 0.9;
margin-bottom: 0.25rem;
color: var(--vp-c-text-2);
}
.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;
}
.info-box strong {
color: var(--vp-c-text-1);
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.4s ease;
transition: all 0.3s ease;
}
.fade-enter-from {
@@ -275,8 +312,8 @@ const stages = [
grid-template-columns: repeat(2, 1fr);
}
.stage-detail {
padding: 16px;
.detail-content {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="monolith-demo">
<div class="demo-header">
<h4>🏢 单体架构演示</h4>
<p>观察单体应用如何处理请求以及模块间的依赖关系</p>
<span class="icon">🏢</span>
<span class="title">单体架构演示</span>
<span class="subtitle">观察单体应用如何处理请求</span>
</div>
<div class="monolith-diagram">
@@ -47,13 +48,9 @@
<button class="control-btn" @click="reset">重置</button>
</div>
<div class="demo-explanation">
<h5>💡 单体架构的特点</h5>
<ul>
<li><strong>共享进程空间</strong>所有模块在同一个进程中运行内存共享</li>
<li><strong>数据库耦合</strong>所有模块共享同一个数据库Schema变更影响全局</li>
<li><strong>级联故障</strong>一个模块崩溃可能导致整个进程挂掉雪崩效应</li>
</ul>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>所有模块在同一个进程中运行内存共享但一个模块崩溃可能导致整个进程挂掉雪崩效应
</div>
</div>
</template>
@@ -136,24 +133,35 @@ const reset = () => {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.monolith-diagram {
@@ -378,4 +386,23 @@ const reset = () => {
flex-wrap: wrap;
}
}
.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;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="physical-server-demo">
<div class="demo-header">
<h4>🖥 物理服务器时代演示</h4>
<p>点击"发送请求"观察早期 CGI 服务器的处理瓶颈</p>
<span class="icon">🖥</span>
<span class="title">物理服务器时代演示</span>
<span class="subtitle">观察早期 CGI 服务器的处理瓶颈</span>
</div>
<div class="demo-stage">
@@ -85,27 +86,9 @@
</div>
</div>
<div class="demo-explanation">
<h5>💡 早期的痛点在哪里</h5>
<ul>
<li>
<strong>进程启动开销</strong>每个请求都要启动新的 CGI
进程就像每来一个客人都要重新搭一个厨房
</li>
<li>
<strong>资源无法复用</strong>数据库连接每次都要重新建立CPU
频繁在进程间切换
</li>
<li>
<strong>扩展困难</strong>只能买更强的单机垂直扩展无法通过增加机器分担压力
</li>
</ul>
<p class="demo-conclusion">
这就是<strong>物理服务器 + CGI</strong>时代的核心问题<span
class="highlight"
>进程级隔离带来了稳定性但也带来了巨大的性能开销</span
>
</p>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>进程级隔离带来了稳定性但也带来了巨大的性能开销
</div>
</div>
</template>
@@ -192,24 +175,35 @@ const sendRequest = async () => {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header p {
margin: 0;
font-size: 0.9rem;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-stage {
@@ -472,4 +466,23 @@ const sendRequest = async () => {
height: 3px;
}
}
.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;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="tech-stack-timeline-demo">
<div class="demo-header">
<h4>📚 技术栈演进时间线</h4>
<p>每个时代的主流技术栈</p>
<span class="icon">📚</span>
<span class="title">技术栈演进时间线</span>
<span class="subtitle">每个时代的主流技术栈</span>
</div>
<div class="timeline">
@@ -106,6 +107,8 @@ const eras = [
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
@@ -247,4 +250,23 @@ const eras = [
grid-template-columns: 1fr;
}
}
.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;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -408,14 +408,15 @@ onUnmounted(() => {
<style scoped>
.animation-demo {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: #fafafa;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.control-panel {
margin-bottom: 20px;
margin-bottom: 1.5rem;
}
.playback-controls {
@@ -426,13 +427,16 @@ onUnmounted(() => {
.play-btn,
.reset-btn {
padding: 10px 20px;
padding: 0.625rem 1.25rem;
border: none;
border-radius: 6px;
font-size: 14px;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
transition: all 0.25s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.play-btn {
@@ -442,7 +446,8 @@ onUnmounted(() => {
.play-btn:hover {
background: #27ae60;
transform: translateY(-1px);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.4);
}
.reset-btn {
@@ -452,28 +457,35 @@ onUnmounted(() => {
.reset-btn:hover {
background: #7f8c8d;
transform: translateY(-1px);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(149, 165, 166, 0.4);
}
.animation-selector {
margin-bottom: 15px;
margin-bottom: 1.25rem;
}
.animation-selector label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #2c3e50;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
font-size: 0.875rem;
}
.animation-selector select {
width: 100%;
padding: 8px 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 14px;
background: white;
padding: 0.5rem 0.75rem;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
font-size: 0.875rem;
background: var(--vp-c-bg);
cursor: pointer;
transition: all 0.2s;
}
.animation-selector select:hover {
border-color: var(--vp-c-brand);
}
.parameters {
@@ -531,30 +543,35 @@ onUnmounted(() => {
.canvas-container {
display: flex;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 1.5rem 0;
padding: 1.5rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
canvas {
border: 2px solid #ddd;
border-radius: 4px;
border: 3px solid var(--vp-c-divider);
border-radius: 8px;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.code-display {
margin-top: 20px;
padding: 15px;
background: #2c3e50;
border-radius: 6px;
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #ecf0f1;
margin: 0 0 10px 0;
font-size: 14px;
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
@@ -562,54 +579,60 @@ canvas {
}
.code-display code {
color: #ecf0f1;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.explanation {
margin: 20px 0;
padding: 15px;
background: white;
border-radius: 6px;
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 10px 0;
color: #2c3e50;
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 20px;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 8px;
color: #555;
font-size: 14px;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
.info-box {
margin-top: 15px;
padding: 12px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 4px;
margin-top: 1.5rem;
padding: 1rem 1.25rem;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border-radius: 12px;
border-left: 4px solid #f59e0b;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
}
.info-box p {
margin: 0;
font-size: 14px;
color: #856404;
font-size: 0.875rem;
color: #92400e;
display: flex;
align-items: flex-start;
gap: 8px;
gap: 0.5rem;
line-height: 1.6;
}
.info-box .icon {
font-size: 16px;
font-size: 1.125rem;
flex-shrink: 0;
}
</style>
@@ -13,85 +13,89 @@
-->
<template>
<div class="canvas-basics-demo">
<div class="control-panel">
<div class="shape-selector">
<label>Shape / 形状</label>
<div class="button-group">
<button
v-for="shape in shapes"
:key="shape.value"
:class="{ active: currentShape === shape.value }"
@click="currentShape = shape.value"
>
{{ shape.label }}
</button>
</div>
</div>
<div class="parameters">
<div class="param-row">
<label>Fill Color / 填充颜色</label>
<input type="color" v-model="fillColor" />
</div>
<div class="param-row">
<label>Stroke Color / 描边颜色</label>
<input type="color" v-model="strokeColor" />
</div>
<div class="param-row">
<label>Stroke Width / 描边宽度: {{ strokeWidth }}px</label>
<input type="range" v-model.number="strokeWidth" min="1" max="20" />
</div>
<div class="param-row" v-if="currentShape === 'rect'">
<label>Size / 大小: {{ rectSize }}px</label>
<input type="range" v-model.number="rectSize" min="20" max="200" />
</div>
<div class="param-row" v-if="currentShape === 'circle'">
<label>Radius / 半径: {{ circleRadius }}px</label>
<input
type="range"
v-model.number="circleRadius"
min="10"
max="150"
/>
</div>
<div class="param-row" v-if="currentShape === 'line'">
<label>Line Length / 线条长度: {{ lineLength }}px</label>
<input type="range" v-model.number="lineLength" min="50" max="300" />
</div>
</div>
<button class="draw-btn" @click="draw">
<span class="icon">🎨</span>
Draw / 绘制
</button>
<button class="clear-btn" @click="clearCanvas">
<span class="icon">🗑</span>
Clear / 清除
</button>
<div class="demo-header">
<span class="icon">🎨</span>
<span class="title">Canvas 基础</span>
<span class="subtitle">用代码画图通俗说编程画板</span>
</div>
<div class="canvas-container">
<canvas ref="canvasRef" width="600" height="400"></canvas>
</div>
<div class="demo-content">
<div class="controls">
<div class="shape-selector">
<label>Shape / 形状</label>
<div class="button-group">
<button
v-for="shape in shapes"
:key="shape.value"
:class="{ active: currentShape === shape.value }"
@click="currentShape = shape.value"
>
{{ shape.label }}
</button>
</div>
</div>
<div class="code-display">
<h4>Code / 代码</h4>
<pre><code>{{ currentCode }}</code></pre>
<div class="parameters">
<div class="param-row">
<label>Fill Color / 填充颜色</label>
<input type="color" v-model="fillColor" />
</div>
<div class="param-row">
<label>Stroke Color / 描边颜色</label>
<input type="color" v-model="strokeColor" />
</div>
<div class="param-row">
<label>Stroke Width / 描边宽度: {{ strokeWidth }}px</label>
<input type="range" v-model.number="strokeWidth" min="1" max="20" />
</div>
<div class="param-row" v-if="currentShape === 'rect'">
<label>Size / 大小: {{ rectSize }}px</label>
<input type="range" v-model.number="rectSize" min="20" max="200" />
</div>
<div class="param-row" v-if="currentShape === 'circle'">
<label>Radius / 半径: {{ circleRadius }}px</label>
<input
type="range"
v-model.number="circleRadius"
min="10"
max="150"
/>
</div>
<div class="param-row" v-if="currentShape === 'line'">
<label>Line Length / 线条长度: {{ lineLength }}px</label>
<input type="range" v-model.number="lineLength" min="50" max="300" />
</div>
</div>
<button class="draw-btn" @click="draw">
<span class="icon">🎨</span>
Draw / 绘制
</button>
<button class="clear-btn" @click="clearCanvas">
<span class="icon">🗑</span>
Clear / 清除
</button>
</div>
<div class="canvas-container">
<canvas ref="canvasRef" width="600" height="400"></canvas>
</div>
<div class="code-display">
<h4>Code / 代码</h4>
<pre><code>{{ currentCode }}</code></pre>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>提示</strong>
Canvas
是一个位图画布所有绘制都是像素操作绘制后无法修改已有内容只能覆盖或清除重绘
</p>
<span class="icon">💡</span>
<strong>核心思想</strong>Canvas 是一个位图画布所有绘制都是像素操作绘制后无法修改已有内容只能覆盖或清除重绘
</div>
</div>
</template>
@@ -101,8 +105,8 @@ import { ref, computed, watch, onMounted } from 'vue'
const canvasRef = ref(null)
const currentShape = ref('rect')
const fillColor = ref('#3498db')
const strokeColor = ref('#2c3e50')
const fillColor = ref('#3b82f6')
const strokeColor = ref('#1e293b')
const strokeWidth = ref(2)
const rectSize = ref(100)
const circleRadius = ref(50)
@@ -231,145 +235,193 @@ onMounted(() => {
<style scoped>
.canvas-basics-demo {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: #fafafa;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1.5rem 0;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.control-panel {
margin-bottom: 20px;
.demo-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.25rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--vp-c-divider);
}
.demo-header .icon {
font-size: 1.5rem;
}
.demo-header .title {
font-weight: 700;
font-size: 1.125rem;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.875rem;
margin-left: 0.75rem;
padding: 0.25rem 0.75rem;
background: var(--vp-c-brand);
color: white;
border-radius: 20px;
font-weight: 500;
}
.demo-content {
margin-bottom: 0.5rem;
}
.controls {
margin-bottom: 1rem;
}
.shape-selector {
margin-bottom: 15px;
margin-bottom: 1.25rem;
}
.shape-selector label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #2c3e50;
margin-bottom: 0.625rem;
color: var(--vp-c-text-1);
font-size: 0.875rem;
}
.button-group {
display: flex;
gap: 8px;
gap: 0.625rem;
flex-wrap: wrap;
}
.button-group button {
padding: 8px 16px;
border: 2px solid #ddd;
background: white;
border-radius: 6px;
padding: 0.625rem 1.25rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.25s ease;
}
.button-group button:hover {
border-color: #3498db;
background: #f0f8ff;
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.button-group button.active {
border-color: #3498db;
background: #3498db;
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.parameters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 15px;
gap: 0.75rem;
margin-bottom: 1rem;
}
.param-row {
display: flex;
flex-direction: column;
gap: 6px;
gap: 0.25rem;
}
.param-row label {
font-size: 13px;
font-size: 0.75rem;
font-weight: 500;
color: #555;
color: var(--vp-c-text-1);
}
.param-row input[type='range'] {
width: 100%;
accent-color: var(--vp-c-brand);
}
.param-row input[type='color'] {
width: 100%;
height: 36px;
border: 1px solid #ddd;
height: 32px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
cursor: pointer;
}
.draw-btn,
.clear-btn {
padding: 10px 20px;
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
font-size: 14px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
margin-right: 10px;
margin-right: 0.5rem;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.draw-btn {
background: #3498db;
background: var(--vp-c-brand);
color: white;
}
.draw-btn:hover {
background: #2980b9;
transform: translateY(-1px);
opacity: 0.9;
}
.clear-btn {
background: #e74c3c;
background: var(--vp-c-danger);
color: white;
}
.clear-btn:hover {
background: #c0392b;
transform: translateY(-1px);
opacity: 0.9;
}
.canvas-container {
display: flex;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 1.5rem 0;
padding: 1.5rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
canvas {
border: 2px solid #ddd;
border-radius: 4px;
background: white;
border: 3px solid var(--vp-c-divider);
border-radius: 8px;
background: #ffffff;
max-width: 100%;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.code-display {
margin-top: 20px;
padding: 15px;
background: #2c3e50;
border-radius: 6px;
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #ecf0f1;
margin: 0 0 10px 0;
font-size: 14px;
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
@@ -377,31 +429,39 @@ canvas {
}
.code-display code {
color: #ecf0f1;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.info-box {
margin-top: 15px;
padding: 12px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 4px;
margin-top: 1.5rem;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
padding: 1rem 1.25rem;
border-radius: 12px;
font-size: 0.875rem;
color: #92400e;
border-left: 4px solid #f59e0b;
display: flex;
gap: 0.5rem;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
}
.info-box p {
margin: 0;
font-size: 14px;
color: #856404;
display: flex;
align-items: flex-start;
gap: 8px;
gap: 0.625rem;
line-height: 1.6;
}
.info-box .icon {
font-size: 16px;
font-size: 1.125rem;
flex-shrink: 0;
}
.info-box strong {
color: #78350f;
}
</style>
@@ -272,117 +272,140 @@ onMounted(() => {
<style scoped>
.coordinate-demo {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: #fafafa;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.control-panel {
margin-bottom: 20px;
margin-bottom: 1.5rem;
}
.toggle-group {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 15px;
gap: 1rem;
margin-bottom: 1.25rem;
}
.toggle-option {
display: flex;
align-items: center;
gap: 8px;
gap: 0.5rem;
cursor: pointer;
font-size: 14px;
font-size: 0.875rem;
padding: 0.5rem 1rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
transition: all 0.2s;
}
.toggle-option:hover {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.toggle-option input[type='checkbox'] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: var(--vp-c-brand);
}
.info-display {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
padding: 12px;
background: white;
border-radius: 6px;
gap: 0.75rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
font-size: 0.875rem;
}
.info-item .label {
font-weight: 600;
color: #555;
color: var(--vp-c-text-2);
}
.info-item .value {
font-family: 'Courier New', monospace;
color: #2c3e50;
background: #f0f0f0;
padding: 2px 8px;
border-radius: 4px;
font-family: var(--vp-font-family-mono);
color: var(--vp-c-text-1);
background: var(--vp-c-bg-soft);
padding: 0.25rem 0.75rem;
border-radius: 6px;
font-weight: 600;
}
.canvas-container {
display: flex;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 1.5rem 0;
padding: 1.5rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
canvas {
border: 2px solid #ddd;
border-radius: 4px;
border: 3px solid var(--vp-c-divider);
border-radius: 8px;
cursor: crosshair;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.explanation {
margin: 20px 0;
padding: 15px;
background: white;
border-radius: 6px;
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 10px 0;
color: #2c3e50;
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 20px;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 8px;
color: #555;
font-size: 14px;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
.code-display {
margin-top: 20px;
padding: 15px;
background: #2c3e50;
border-radius: 6px;
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #ecf0f1;
margin: 0 0 10px 0;
font-size: 14px;
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
@@ -390,31 +413,33 @@ canvas {
}
.code-display code {
color: #ecf0f1;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.info-box {
margin-top: 15px;
padding: 12px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 4px;
margin-top: 1.5rem;
padding: 1rem 1.25rem;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border-radius: 12px;
border-left: 4px solid #f59e0b;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
}
.info-box p {
margin: 0;
font-size: 14px;
color: #856404;
font-size: 0.875rem;
color: #92400e;
display: flex;
align-items: flex-start;
gap: 8px;
gap: 0.5rem;
line-height: 1.6;
}
.info-box .icon {
font-size: 16px;
font-size: 1.125rem;
flex-shrink: 0;
}
</style>
@@ -491,14 +491,15 @@ onMounted(() => {
<style scoped>
.event-demo {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: #fafafa;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.control-panel {
margin-bottom: 20px;
margin-bottom: 1.5rem;
}
.mode-selector {
@@ -514,53 +515,60 @@ onMounted(() => {
.button-group {
display: flex;
gap: 8px;
gap: 0.5rem;
flex-wrap: wrap;
}
.button-group button {
padding: 8px 16px;
border: 2px solid #ddd;
background: white;
border-radius: 6px;
padding: 0.5rem 1rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.25s ease;
}
.button-group button:hover {
border-color: #3498db;
background: #f0f8ff;
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.button-group button.active {
border-color: #3498db;
background: #3498db;
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.instructions {
margin-bottom: 15px;
padding: 12px;
background: white;
border-radius: 6px;
margin-bottom: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.instructions h4 {
margin: 0 0 8px 0;
color: #2c3e50;
font-size: 14px;
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.instructions ul {
margin: 0;
padding-left: 20px;
padding-left: 1.25rem;
}
.instructions li {
margin-bottom: 6px;
color: #555;
font-size: 13px;
margin-bottom: 0.375rem;
color: var(--vp-c-text-2);
font-size: 0.813rem;
line-height: 1.5;
}
.event-log {
@@ -635,36 +643,41 @@ onMounted(() => {
.canvas-container {
display: flex;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 1.5rem 0;
padding: 1.5rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
canvas {
border: 2px solid #ddd;
border-radius: 4px;
border: 3px solid var(--vp-c-divider);
border-radius: 8px;
cursor: crosshair;
outline: none;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
canvas:focus {
border-color: #3498db;
border-color: var(--vp-c-brand);
}
.code-display {
margin-top: 20px;
padding: 15px;
background: #2c3e50;
border-radius: 6px;
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #ecf0f1;
margin: 0 0 10px 0;
font-size: 14px;
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
@@ -672,32 +685,36 @@ canvas:focus {
}
.code-display code {
color: #ecf0f1;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.explanation {
margin: 20px 0;
padding: 15px;
background: white;
border-radius: 6px;
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 10px 0;
color: #2c3e50;
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 20px;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 8px;
color: #555;
font-size: 14px;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
</style>
@@ -378,14 +378,15 @@ onUnmounted(() => {
<style scoped>
.particle-demo {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: #fafafa;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.control-panel {
margin-bottom: 20px;
margin-bottom: 1.5rem;
}
.effect-selector {
@@ -401,29 +402,33 @@ onUnmounted(() => {
.button-group {
display: flex;
gap: 8px;
gap: 0.5rem;
flex-wrap: wrap;
}
.button-group button {
padding: 8px 16px;
border: 2px solid #ddd;
background: white;
border-radius: 6px;
padding: 0.5rem 1rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.25s ease;
}
.button-group button:hover {
border-color: #3498db;
background: #f0f8ff;
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.button-group button.active {
border-color: #3498db;
background: #3498db;
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.parameters {
@@ -499,31 +504,36 @@ onUnmounted(() => {
.canvas-container {
display: flex;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 1.5rem 0;
padding: 1.5rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
canvas {
border: 2px solid #ddd;
border-radius: 4px;
border: 3px solid var(--vp-c-divider);
border-radius: 8px;
cursor: crosshair;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.code-display {
margin-top: 20px;
padding: 15px;
background: #2c3e50;
border-radius: 6px;
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #ecf0f1;
margin: 0 0 10px 0;
font-size: 14px;
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
@@ -531,54 +541,60 @@ canvas {
}
.code-display code {
color: #ecf0f1;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.explanation {
margin: 20px 0;
padding: 15px;
background: white;
border-radius: 6px;
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 10px 0;
color: #2c3e50;
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 20px;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 8px;
color: #555;
font-size: 14px;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
.info-box {
margin-top: 15px;
padding: 12px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 4px;
margin-top: 1.5rem;
padding: 1rem 1.25rem;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border-radius: 12px;
border-left: 4px solid #f59e0b;
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
}
.info-box p {
margin: 0;
font-size: 14px;
color: #856404;
font-size: 0.875rem;
color: #92400e;
display: flex;
align-items: flex-start;
gap: 8px;
gap: 0.5rem;
line-height: 1.6;
}
.info-box .icon {
font-size: 16px;
font-size: 1.125rem;
flex-shrink: 0;
}
</style>
@@ -553,14 +553,15 @@ onUnmounted(() => {
<style scoped>
.performance-demo {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
background: #fafafa;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
background: var(--vp-c-bg-soft);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.control-panel {
margin-bottom: 20px;
margin-bottom: 1.5rem;
}
.test-selector {
@@ -576,29 +577,33 @@ onUnmounted(() => {
.button-group {
display: flex;
gap: 8px;
gap: 0.5rem;
flex-wrap: wrap;
}
.button-group button {
padding: 8px 16px;
border: 2px solid #ddd;
background: white;
border-radius: 6px;
padding: 0.5rem 1rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.25s ease;
}
.button-group button:hover {
border-color: #3498db;
background: #f0f8ff;
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.button-group button.active {
border-color: #3498db;
background: #3498db;
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.parameters {
@@ -721,28 +726,34 @@ onUnmounted(() => {
.canvas-container {
display: flex;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin: 1.5rem 0;
padding: 1.5rem;
background: var(--vp-c-bg);
border-radius: 12px;
border: 2px solid var(--vp-c-divider);
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
canvas {
border: 2px solid #ddd;
border-radius: 4px;
border: 3px solid var(--vp-c-divider);
border-radius: 8px;
background: #ffffff;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.comparison {
margin: 20px 0;
padding: 15px;
background: white;
border-radius: 6px;
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.comparison h4 {
margin: 0 0 15px 0;
color: #2c3e50;
margin: 0 0 1rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.comparison-table {
@@ -756,29 +767,37 @@ canvas {
.comparison-table th,
.comparison-table td {
padding: 10px;
padding: 0.625rem;
text-align: left;
border-bottom: 1px solid #e0e0e0;
border-bottom: 1px solid var(--vp-c-divider);
}
.comparison-table th {
background: #f8f9fa;
background: var(--vp-c-bg-soft);
font-weight: 600;
color: #2c3e50;
color: var(--vp-c-text-1);
font-size: 0.813rem;
}
.comparison-table td {
font-size: 0.813rem;
color: var(--vp-c-text-2);
}
.code-display {
margin-top: 20px;
padding: 15px;
background: #2c3e50;
border-radius: 6px;
margin-top: 1.5rem;
padding: 1.25rem;
background: #1e293b;
border-radius: 12px;
overflow-x: auto;
border: 2px solid #334155;
}
.code-display h4 {
color: #ecf0f1;
margin: 0 0 10px 0;
font-size: 14px;
color: #f8fafc;
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.code-display pre {
@@ -786,32 +805,36 @@ canvas {
}
.code-display code {
color: #ecf0f1;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #e2e8f0;
font-family: var(--vp-font-family-mono);
font-size: 0.75rem;
line-height: 1.7;
}
.explanation {
margin: 20px 0;
padding: 15px;
background: white;
border-radius: 6px;
margin: 1.5rem 0;
padding: 1.25rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.explanation h4 {
margin: 0 0 10px 0;
color: #2c3e50;
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-1);
font-size: 0.875rem;
font-weight: 600;
}
.explanation ul {
margin: 0;
padding-left: 20px;
padding-left: 1.25rem;
}
.explanation li {
margin-bottom: 8px;
color: #555;
font-size: 14px;
margin-bottom: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
line-height: 1.6;
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="access-key-management-demo">
<div class="demo-header">
<h4>访问密钥AK/SK生命周期管理</h4>
<p class="intro-text">模拟 AK/SK 的创建使用和轮换流程</p>
<span class="icon">🔑</span>
<span class="title">访问密钥管理</span>
<span class="subtitle">理解 AK/SK 生命周期和轮换流程</span>
</div>
<div class="demo-content">
@@ -88,7 +89,8 @@
</div>
<div class="info-box">
<strong>💡 安全提示</strong>访问密钥泄露是云安全事件的主要原因之一建议优先使用 IAM 角色替代访问密钥如果必须使用请务必定期轮换
<span class="icon">💡</span>
<strong>核心思想</strong>访问密钥泄露是云安全事件的主要原因之一建议优先使用 IAM 角色替代访问密钥如果必须使用请务必定期轮换
</div>
</div>
</template>
@@ -1,8 +1,9 @@
<template>
<div class="best-practices-demo">
<div class="demo-header">
<h4>云账号权限管理最佳实践清单</h4>
<p class="intro-text">点击查看详细的实施指南和代码示例</p>
<span class="icon"></span>
<span class="title">权限管理最佳实践</span>
<span class="subtitle">理解云账号安全管理的核心原则</span>
</div>
<div class="demo-content">
@@ -52,7 +53,8 @@
</div>
<div class="info-box">
<strong>💡 实施建议</strong>按照优先级从 P0 开始逐步实施最佳实践每个改进都能显著提升账号安全性不要试图一次性完成所有改进
<span class="icon">💡</span>
<strong>核心思想</strong>按照优先级从 P0 开始逐步实施最佳实践每个改进都能显著提升账号安全性不要试图一次性完成所有改进
</div>
</div>
</template>
@@ -1,8 +1,9 @@
<template>
<div class="cross-account-access-demo">
<div class="demo-header">
<h4>跨账号访问流程演示</h4>
<p class="intro-text">角色扮演AssumeRole获取临时凭证</p>
<span class="icon">🔗</span>
<span class="title">跨账号访问</span>
<span class="subtitle">理解跨账号访问的 AssumeRole 机制</span>
</div>
<div class="demo-content">
@@ -65,7 +66,8 @@ s3_client = boto3.client(
</div>
<div class="info-box">
<strong>💡 跨账号访问优势</strong>通过角色扮演实现跨账号访问无需在每个账号创建 IAM 用户临时凭证自动过期更安全更易管理
<span class="icon">💡</span>
<strong>核心思想</strong>通过角色扮演实现跨账号访问无需在每个账号创建 IAM 用户临时凭证自动过期更安全更易管理
</div>
</div>
</template>
@@ -1,6 +1,13 @@
<template>
<div class="iam-structure">
<div class="structure-layers">
<div class="demo-header">
<span class="icon">🏗</span>
<span class="title">IAM 五大核心概念</span>
<span class="subtitle">云上权限管理的基础构件</span>
</div>
<div class="demo-content">
<div class="structure-layers">
<div
v-for="(layer, index) in layers"
:key="index"
@@ -14,21 +21,27 @@
<div class="layer-desc">{{ layer.shortDesc }}</div>
</div>
</div>
</div>
<div v-if="selectedLayerData" class="layer-detail">
<div class="detail-header">
<span class="detail-icon">{{ selectedLayerData.icon }}</span>
<span class="detail-name">{{ selectedLayerData.name }}</span>
</div>
<div class="detail-desc">{{ selectedLayerData.description }}</div>
<div class="detail-examples">
<div class="example-title">示例</div>
<ul>
<li v-for="(example, i) in selectedLayerData.examples" :key="i">
{{ example }}
</li>
</ul>
<div v-if="selectedLayerData" class="layer-detail">
<div class="detail-header">
<span class="detail-icon">{{ selectedLayerData.icon }}</span>
<span class="detail-name">{{ selectedLayerData.name }}</span>
</div>
<div class="detail-desc">{{ selectedLayerData.description }}</div>
<div class="detail-examples">
<div class="example-title">示例</div>
<ul>
<li v-for="(example, i) in selectedLayerData.examples" :key="i">
{{ example }}
</li>
</ul>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>IAM 就像公司的门禁系统根账号是老板拥有所有钥匙用户是员工有特定权限角色是临时访客证有时效策略是"谁能进哪些门"的规则
</div>
</div>
</div>
@@ -113,9 +126,39 @@ function selectLayer(index) {
.iam-structure {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.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;
}
.demo-content {
margin-bottom: 0.5rem;
}
.structure-layers {
@@ -221,4 +264,23 @@ function selectLayer(index) {
.detail-examples li:last-child {
margin-bottom: 0;
}
.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: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="iam-ram-comparison-demo">
<div class="demo-header">
<h4>AWS IAM vs 阿里云 RAM 对比</h4>
<p class="intro-text">点击各个模块查看详细对比</p>
<span class="icon">🔐</span>
<span class="title">IAM vs RAM 对比</span>
<span class="subtitle">理解不同云厂商的权限管理服务</span>
</div>
<div class="demo-content">
@@ -80,7 +81,8 @@
</div>
<div class="info-box">
<strong>💡 提示</strong>IAM RAM 的核心概念基本一致只是术语和实现细节略有不同掌握一个平台后可以快速迁移到另一个平台
<span class="icon">💡</span>
<strong>核心思想</strong>IAM RAM 的核心概念基本一致只是术语和实现细节略有不同掌握一个平台后可以快速迁移到另一个平台
</div>
</div>
</template>
@@ -163,31 +165,24 @@ function selectFeature(platform, index) {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1.5rem;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header h4 {
margin: 0 0 0.5rem 0;
font-weight: 800;
color: var(--vp-c-text-1);
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.intro-text {
margin: 0;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.demo-content {
margin-bottom: 1rem;
}
.demo-content { margin-bottom: 0.75rem; }
.comparison-container {
display: grid;
@@ -214,7 +209,7 @@ function selectFeature(platform, index) {
}
.platform-header.ram {
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
background: var(--vp-c-bg-soft);
}
.platform-header .logo {
@@ -234,9 +229,7 @@ function selectFeature(platform, index) {
color: var(--vp-c-text-2);
}
.features-list {
padding: 0.75rem;
}
.features-list { padding: 0.75rem; }
.feature-item {
display: flex;
@@ -258,9 +251,7 @@ function selectFeature(platform, index) {
transform: translateX(4px);
}
.feature-icon {
font-size: 1.2rem;
}
.feature-icon { font-size: 1.2rem; }
.feature-content {
display: flex;
@@ -289,13 +280,11 @@ function selectFeature(platform, index) {
padding: 1rem;
}
.detail-card {
text-align: center;
}
.detail-card { text-align: center; }
.detail-card h6 {
margin: 0 0 1rem 0;
font-size: 1.1rem;
font-size: 1rem;
color: var(--vp-c-brand-1);
}
@@ -324,18 +313,13 @@ function selectFeature(platform, index) {
color: var(--vp-c-text-1);
}
.aws-detail .label {
color: var(--vp-c-brand-1);
}
.ram-detail .label {
color: var(--vp-c-brand-delta);
}
.aws-detail .label { color: var(--vp-c-brand-1); }
.ram-detail .label { color: var(--vp-c-brand-delta); }
.aws-detail p,
.ram-detail p {
margin: 0 0 0.5rem 0;
font-size: 0.8rem;
font-size: 0.75rem;
line-height: 1.4;
color: var(--vp-c-text-2);
}
@@ -347,7 +331,7 @@ function selectFeature(platform, index) {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.65rem;
font-size: 0.6rem;
word-break: break-all;
color: var(--vp-c-text-2);
}
@@ -363,19 +347,17 @@ function selectFeature(platform, index) {
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-left: 4px solid var(--vp-c-brand);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.6;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box strong {
color: var(--vp-c-text-1);
}
.info-box .icon { flex-shrink: 0; }
@media (max-width: 1024px) {
.comparison-container {
@@ -383,16 +365,8 @@ function selectFeature(platform, index) {
gap: 1rem;
}
.comparison-details {
order: -1;
}
.comparison-row {
flex-direction: column;
}
.vs-divider {
padding: 0.5rem 0;
}
.comparison-details { order: -1; }
.comparison-row { flex-direction: column; }
.vs-divider { padding: 0.5rem 0; }
}
</style>
@@ -1,8 +1,9 @@
<template>
<div class="identity-provider-demo">
<div class="demo-header">
<h4>身份提供商(IdP)集成流程</h4>
<p class="intro-text">点击步骤查看 SSO 单点登录流程</p>
<span class="icon">🔐</span>
<span class="title">身份提供商集成</span>
<span class="subtitle">理解企业 SSO 单点登录流程</span>
</div>
<div class="demo-content">
@@ -38,7 +39,8 @@
</div>
<div class="info-box">
<strong>💡 SSO 优势</strong>通过企业 IdP 统一管理用户身份避免在每个云平台单独创建账号提高安全性和管理效率
<span class="icon">💡</span>
<strong>核心思想</strong>通过企业 IdP 统一管理用户身份避免在每个云平台单独创建账号提高安全性和管理效率
</div>
</div>
</template>
@@ -1,8 +1,9 @@
<template>
<div class="mfa-security-demo">
<div class="demo-header">
<h4>MFA 多因素认证模拟</h4>
<p class="intro-text">体验 MFA 因素认证流程</p>
<span class="icon">🔐</span>
<span class="title">因素认证</span>
<span class="subtitle">理解 MFA 双因素认证流程</span>
</div>
<div class="demo-content">
@@ -50,7 +51,8 @@
</div>
<div class="info-box">
<strong>💡 MFA 安全价值</strong>启用 MFA 可降低 99.9% 的账号被盗风险即使密码泄露攻击者没有你的 MFA 设备也无法登录
<span class="icon">💡</span>
<strong>核心思想</strong>启用 MFA 可降低 99.9% 的账号被盗风险即使密码泄露攻击者没有你的 MFA 设备也无法登录
</div>
</div>
</template>
@@ -1,8 +1,9 @@
<template>
<div class="permission-hierarchy-demo">
<div class="demo-header">
<h4>权限层级结构</h4>
<p class="intro-text">点击层级查看详细权限范围</p>
<span class="icon">🏛</span>
<span class="title">权限层级结构</span>
<span class="subtitle">理解不同权限级别的范围差异</span>
</div>
<div class="demo-content">
@@ -61,7 +62,8 @@
</div>
<div class="info-box">
<strong>💡 最小权限原则</strong>始终授予用户完成工作所需的最小权限从低权限开始根据实际需求逐步提升而不是一开始就授予高权限
<span class="icon">💡</span>
<strong>核心思想</strong>最小权限原则始终授予用户完成工作所需的最小权限从低权限开始根据实际需求逐步提升而不是一开始就授予高权限
</div>
</div>
</template>
@@ -1,5 +1,11 @@
<template>
<div class="policy-editor-demo">
<div class="demo-header">
<span class="icon">📋</span>
<span class="title">策略编辑器</span>
<span class="subtitle">理解 IAM 策略的 JSON 结构</span>
</div>
<div class="editor-layout">
<div class="editor-panel">
<div class="panel-title">策略编辑器</div>
@@ -42,6 +48,11 @@
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>策略由 EffectActionResourceCondition 四个核心元素组成理解这四个元素的作用是编写 IAM 策略的基础
</div>
</div>
</template>
@@ -1,8 +1,9 @@
<template>
<div class="role-policy-demo">
<div class="demo-header">
<h4>角色与策略关系可视化</h4>
<p class="intro-text">拖动查看角色如何关联多个策略</p>
<span class="icon">🎭</span>
<span class="title">角色与策略</span>
<span class="subtitle">理解角色如何关联多个策略</span>
</div>
<div class="demo-content">
@@ -80,7 +81,8 @@
</div>
<div class="info-box">
<strong>💡 策略叠加</strong>一个角色可以附加多个策略最终的权限是所有策略的叠加结果Deny 策略优先级高于 Allow
<span class="icon">💡</span>
<strong>核心思想</strong>策略叠加一个角色可以附加多个策略最终的权限是所有策略的叠加结果Deny 策略优先级高于 Allow
</div>
</div>
</template>
@@ -1,14 +1,20 @@
<template>
<div class="demo-container">
<div class="access-analytics-demo">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<span class="icon">📊</span>
<span class="title">访问分析</span>
<span class="subtitle">理解 CDN 访问统计和日志分析</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
访问分析演示组件占位符 - 待实现具体交互
</el-alert>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>通过日志分析可以了解谁在何时访问了什么资源帮助发现异常访问模式和安全事件
</div>
</div>
</template>
@@ -20,31 +26,58 @@ const description = ref('展示CDN和对象存储的访问统计分析,包括
</script>
<style scoped>
.demo-container {
.access-analytics-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 20px;
margin-bottom: 1rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
.demo-header .icon {
font-size: 1.25rem;
}
.hint {
margin: 0;
font-size: 14px;
.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;
}
.demo-content {
margin-bottom: 1rem;
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-left: 4px solid var(--vp-c-brand);
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.6;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
flex-direction: column;
gap: 16px;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,14 +1,20 @@
<template>
<div class="demo-container">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<span class="icon"></span>
<span class="title">{{ title }}</span>
<span class="subtitle">{{ description }}</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
缓存策略演示组件占位符 - 待实现具体交互
</el-alert>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>缓存策略平衡命中率和新鲜度TTL 设置太短会导致频繁回源太长会导致内容过期
</div>
</div>
</template>
@@ -4,9 +4,10 @@
-->
<template>
<div class="cdn-acceleration-demo">
<div class="header">
<div class="title">CDN 加速原理</div>
<div class="subtitle">边缘节点源站与回源的协同工作</div>
<div class="demo-header">
<span class="icon">🌐</span>
<span class="title">CDN 加速原理</span>
<span class="subtitle">边缘节点源站与回源的协同工作</span>
</div>
<div class="cdn-architecture">
@@ -64,7 +65,7 @@
</div>
<div class="stat">
<span class="stat-label">命中</span>
<span class="stat-value" :style="{ color: node.hitRate > 80 ? '#22c55e' : '#f59e0b' }">
<span class="stat-value" :style="{ color: node.hitRate > 80 ? 'var(--vp-c-brand-1)' : 'var(--vp-c-brand)' }">
{{ node.hitRate }}%
</span>
</div>
@@ -135,25 +136,30 @@
<div class="stats-title">📊 访问统计</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" :style="{ color: '#22c55e' }">{{ stats.cacheHit }}</div>
<div class="stat-value" :style="{ color: 'var(--vp-c-brand-1)' }">{{ stats.cacheHit }}</div>
<div class="stat-label">缓存命中</div>
</div>
<div class="stat-card">
<div class="stat-value" :style="{ color: '#ef4444' }">{{ stats.cacheMiss }}</div>
<div class="stat-value" :style="{ color: 'var(--vp-c-brand-delta)' }">{{ stats.cacheMiss }}</div>
<div class="stat-label">缓存未命中</div>
</div>
<div class="stat-card">
<div class="stat-value" :style="{ color: stats.hitRate > 80 ? '#22c55e' : '#f59e0b' }">
<div class="stat-value" :style="{ color: stats.hitRate > 80 ? 'var(--vp-c-brand-1)' : 'var(--vp-c-brand)' }">
{{ stats.hitRate }}%
</div>
<div class="stat-label">命中率</div>
</div>
<div class="stat-card">
<div class="stat-value" :style="{ color: '#3b82f6' }">{{ stats.avgResponseTime }}ms</div>
<div class="stat-value" :style="{ color: 'var(--vp-c-brand)' }">{{ stats.avgResponseTime }}ms</div>
<div class="stat-label">平均响应</div>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>CDN就像在全球开了分店用户访问最近的分店拿资源不用都跑总店来速度自然快
</div>
</div>
</template>
@@ -285,21 +291,32 @@ const resetDemo = () => {
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
max-height: 600px;
overflow-y: auto;
}
.header {
margin-bottom: 1.5rem;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.25rem;
.demo-header .icon {
font-size: 1.25rem;
}
.subtitle {
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
font-size: 0.85rem;
margin-left: 0.5rem;
}
.cdn-architecture {
@@ -338,27 +355,27 @@ const resetDemo = () => {
}
.layer-status.hit {
background: #dcfce7;
color: #166534;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.layer-status.miss {
background: #fee2e2;
color: #991b1b;
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
color: var(--vp-c-brand-delta);
}
.layer-status.active {
background: #dbeafe;
color: #1e40af;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
/* 用户层 */
.users-map {
position: relative;
height: 120px;
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
background: var(--vp-c-bg-soft);
border-radius: 8px;
border: 1px solid #bae6fd;
border: 1px solid var(--vp-c-divider);
overflow: hidden;
}
@@ -396,18 +413,18 @@ const resetDemo = () => {
display: flex;
align-items: center;
justify-content: center;
background: white;
background: var(--vp-c-bg);
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.user-label {
font-size: 0.65rem;
font-weight: 600;
color: #0369a1;
color: var(--vp-c-brand-1);
margin-top: 0.25rem;
white-space: nowrap;
background: rgba(255, 255, 255, 0.9);
background: var(--vp-c-bg);
padding: 0.1rem 0.4rem;
border-radius: 4px;
}
@@ -511,8 +528,8 @@ const resetDemo = () => {
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: linear-gradient(135deg, #fef3c7, #fde68a);
border: 2px solid #f59e0b;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
}
@@ -528,12 +545,12 @@ const resetDemo = () => {
.server-name {
font-weight: 600;
font-size: 0.9rem;
color: #92400e;
color: var(--vp-c-text-1);
}
.server-address {
font-size: 0.75rem;
color: #b45309;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
@@ -543,14 +560,14 @@ const resetDemo = () => {
gap: 0.4rem;
font-size: 0.75rem;
font-weight: 600;
color: #15803d;
color: var(--vp-c-brand-1);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
background: var(--vp-c-brand-1);
animation: statusPulse 2s infinite;
}
@@ -560,8 +577,8 @@ const resetDemo = () => {
}
.back-to-source-flow {
background: #fef2f2;
border: 1px solid #fecaca;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
margin-top: 0.5rem;
@@ -571,7 +588,7 @@ const resetDemo = () => {
text-align: center;
font-size: 0.9rem;
font-weight: 600;
color: #dc2626;
color: var(--vp-c-brand-delta);
margin-bottom: 0.5rem;
}
@@ -589,11 +606,11 @@ const resetDemo = () => {
.flow-step {
font-size: 0.75rem;
color: #991b1b;
background: white;
color: var(--vp-c-text-1);
background: var(--vp-c-bg);
padding: 0.4rem 0.6rem;
border-radius: 4px;
border-left: 3px solid #dc2626;
border-left: 3px solid var(--vp-c-brand);
}
/* 控制区 */
@@ -639,13 +656,13 @@ const resetDemo = () => {
}
.control-btn.reset {
background: #fef2f2;
border-color: #fecaca;
color: #dc2626;
background: rgba(var(--vp-c-brand-delta-rgb), 0.1);
border-color: var(--vp-c-brand-delta);
color: var(--vp-c-brand-delta);
}
.control-btn.reset:hover {
background: #fee2e2;
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
}
/* 统计面板 */
@@ -693,4 +710,23 @@ const resetDemo = () => {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.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: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,50 +1,83 @@
<template>
<div class="demo-container">
<div class="https-optimization-demo">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<span class="icon">🔒</span>
<span class="title">HTTPS 优化</span>
<span class="subtitle">理解 CDN HTTPS 协议和证书管理</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
HTTPS优化演示组件占位符 - 待实现具体交互
HTTPS 优化演示组件占位符 - 待实现具体交互
</el-alert>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>HTTPS 通过 TLS/SSL 加密数据传输防止中间人攻击和数据泄露是现代 Web 应用的安全基础
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('HTTPS优化演示')
const title = ref('HTTPS 优化演示')
const description = ref('展示CDN的HTTPS优化技术,包括TLS握手优化、证书管理、HSTS等')
</script>
<style scoped>
.demo-container {
.https-optimization-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 20px;
margin-bottom: 1rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
.demo-header .icon {
font-size: 1.25rem;
}
.hint {
margin: 0;
font-size: 14px;
.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;
}
.demo-content {
margin-bottom: 1rem;
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-left: 4px solid var(--vp-c-brand);
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.6;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
flex-direction: column;
gap: 16px;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,12 +1,9 @@
<!--
ObjectStorageDemo.vue
对象存储架构演示 - 展示桶对象元数据的核心概念
-->
<template>
<div class="object-storage-demo">
<div class="header">
<div class="title">对象存储架构</div>
<div class="subtitle">理解 BucketObject Metadata 的关系</div>
<div class="demo-header">
<span class="icon">🗄</span>
<span class="title">对象存储架构</span>
<span class="subtitle">理解 BucketObject Metadata 的关系</span>
</div>
<div class="storage-architecture">
@@ -113,38 +110,9 @@
</div>
</div>
<div class="architecture-summary">
<div class="summary-title">架构要点总结</div>
<div class="summary-grid">
<div class="summary-item">
<div class="summary-icon">📦</div>
<div class="summary-text">
<strong>Bucket</strong>
<span>全局命名空间用于组织和隔离数据</span>
</div>
</div>
<div class="summary-item">
<div class="summary-icon">📄</div>
<div class="summary-text">
<strong>Object对象</strong>
<span>键值对存储包含数据元数据和唯一 Key</span>
</div>
</div>
<div class="summary-item">
<div class="summary-icon">🏷</div>
<div class="summary-text">
<strong>Metadata元数据</strong>
<span>系统元数据 + 自定义标签支持检索和管理</span>
</div>
</div>
<div class="summary-item">
<div class="summary-icon">🔐</div>
<div class="summary-text">
<strong>Access Control访问控制</strong>
<span>Bucket PolicyACLSTS 临时凭证多层权限</span>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>对象存储采用三层架构Account账户 Bucket Object对象每个对象都附带丰富的元数据用于检索和管理理解这个层次结构是掌握对象存储的第一步
</div>
</div>
</template>
@@ -262,69 +230,67 @@ const getFileIcon = (type) => {
.object-storage-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.header {
margin-bottom: 1.5rem;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.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; }
.storage-architecture {
display: flex;
flex-direction: column;
gap: 0.75rem;
gap: 0.5rem;
}
.account-layer {
background: linear-gradient(135deg, #e0e7ff, #c7d2fe);
padding: 1rem;
border-radius: 10px;
background: var(--vp-c-brand-soft);
padding: 0.75rem;
border-radius: 8px;
text-align: center;
border: 2px solid #6366f1;
border: 2px solid var(--vp-c-brand);
}
.account-icon {
font-size: 2rem;
font-size: 1.5rem;
margin-bottom: 0.25rem;
}
.account-name {
font-weight: 600;
font-size: 0.95rem;
color: #4338ca;
font-size: 0.9rem;
color: var(--vp-c-brand-1);
margin-bottom: 0.25rem;
}
.account-desc {
font-size: 0.75rem;
color: #6366f1;
font-size: 0.7rem;
color: var(--vp-c-text-2);
margin-top: 0.25rem;
}
.connector {
text-align: center;
color: var(--vp-c-text-3);
font-size: 1.25rem;
font-size: 1rem;
}
.buckets-container {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
border-radius: 8px;
padding: 0.75rem;
}
.section-title {
@@ -332,13 +298,13 @@ const getFileIcon = (type) => {
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
font-size: 0.85rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.section-desc {
font-size: 0.75rem;
font-size: 0.65rem;
font-weight: normal;
color: var(--vp-c-text-2);
margin-left: auto;
@@ -346,17 +312,17 @@ const getFileIcon = (type) => {
.buckets-row {
display: flex;
gap: 0.75rem;
gap: 0.5rem;
flex-wrap: wrap;
}
.bucket-card {
flex: 1;
min-width: 140px;
min-width: 120px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
border-radius: 6px;
padding: 0.5rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
@@ -370,29 +336,26 @@ const getFileIcon = (type) => {
.bucket-card.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
box-shadow: 0 0 0 3px var(--vp-c-brand-dimm);
box-shadow: 0 0 3px var(--vp-c-brand-dimm);
}
.bucket-icon {
font-size: 1.75rem;
margin-bottom: 0.25rem;
}
.bucket-icon { font-size: 1.5rem; margin-bottom: 0.25rem; }
.bucket-name {
font-weight: 600;
font-size: 0.8rem;
font-size: 0.75rem;
color: var(--vp-c-text-1);
word-break: break-all;
}
.bucket-meta {
font-size: 0.7rem;
font-size: 0.65rem;
color: var(--vp-c-text-2);
margin-top: 0.25rem;
}
.bucket-size {
font-size: 0.75rem;
font-size: 0.7rem;
color: var(--vp-c-brand);
font-weight: 600;
margin-top: 0.25rem;
@@ -401,30 +364,30 @@ const getFileIcon = (type) => {
.objects-container {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
min-height: 150px;
border-radius: 8px;
padding: 0.75rem;
min-height: 120px;
}
.objects-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: 0.4rem;
}
.object-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
gap: 0.5rem;
padding: 0.4rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.object-item:hover {
background: var(--vp-c-bg-mute);
background: var(--vp-c-bg-alt);
}
.object-item.selected {
@@ -432,9 +395,7 @@ const getFileIcon = (type) => {
border: 1px solid var(--vp-c-brand);
}
.object-icon {
font-size: 1.25rem;
}
.object-icon { font-size: 1rem; }
.object-info {
flex: 1;
@@ -442,7 +403,7 @@ const getFileIcon = (type) => {
}
.object-key {
font-size: 0.8rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-1);
white-space: nowrap;
@@ -451,33 +412,35 @@ const getFileIcon = (type) => {
}
.object-meta {
font-size: 0.7rem;
font-size: 0.65rem;
color: var(--vp-c-text-2);
}
.object-arrow {
color: var(--vp-c-text-3);
font-size: 0.8rem;
}
.objects-placeholder {
.objects-placeholder,
.metadata-placeholder {
text-align: center;
padding: 2rem;
padding: 1.5rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
font-size: 0.8rem;
}
.metadata-container {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
min-height: 150px;
border-radius: 8px;
padding: 0.75rem;
min-height: 120px;
}
.metadata-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
gap: 0.75rem;
}
@media (max-width: 768px) {
@@ -488,30 +451,30 @@ const getFileIcon = (type) => {
.metadata-section {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 0.75rem;
border-radius: 6px;
padding: 0.5rem;
}
.metadata-section-title {
font-weight: 600;
font-size: 0.85rem;
font-size: 0.75rem;
color: var(--vp-c-brand);
margin-bottom: 0.5rem;
padding-bottom: 0.5rem;
margin-bottom: 0.4rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.metadata-list {
display: flex;
flex-direction: column;
gap: 0.4rem;
gap: 0.3rem;
}
.metadata-item {
display: flex;
flex-direction: column;
gap: 0.1rem;
font-size: 0.75rem;
font-size: 0.7rem;
}
.metadata-key {
@@ -525,66 +488,16 @@ const getFileIcon = (type) => {
word-break: break-all;
}
.metadata-placeholder {
text-align: center;
padding: 2rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.architecture-summary {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.25rem;
margin-top: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.summary-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
color: var(--vp-c-text-1);
}
.summary-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
@media (max-width: 640px) {
.summary-grid {
grid-template-columns: 1fr;
}
}
.summary-item {
display: flex;
gap: 0.75rem;
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.summary-icon {
font-size: 1.5rem;
}
.summary-text {
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.summary-text strong {
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.summary-text span {
font-size: 0.75rem;
color: var(--vp-c-text-2);
line-height: 1.4;
}
.info-box .icon { flex-shrink: 0; }
</style>
@@ -1,14 +1,20 @@
<template>
<div class="demo-container">
<div class="traffic-scheduling-demo">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<span class="icon">🚦</span>
<span class="title">流量调度</span>
<span class="subtitle">理解 CDN 智能调度和负载均衡</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
流量调度演示组件占位符 - 待实现具体交互
</el-alert>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>智能调度通过就近访问负载均衡和故障切换实现全球加速和高可用性
</div>
</div>
</template>
@@ -20,31 +26,58 @@ const description = ref('展示CDN的智能流量调度机制,包括负载均
</script>
<style scoped>
.demo-container {
.traffic-scheduling-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 20px;
margin-bottom: 1rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
.demo-header .icon {
font-size: 1.25rem;
}
.hint {
margin: 0;
font-size: 14px;
.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;
}
.demo-content {
margin-bottom: 1rem;
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-left: 4px solid var(--vp-c-brand);
border-radius: 6px;
font-size: 0.9rem;
line-height: 1.6;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
flex-direction: column;
gap: 16px;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -4,9 +4,10 @@
-->
<template>
<div class="upload-process-demo">
<div class="header">
<div class="title">文件上传流程</div>
<div class="subtitle">直传 vs 分片上传 vs 断点续传</div>
<div class="demo-header">
<span class="icon">📤</span>
<span class="title">文件上传流程</span>
<span class="subtitle">理解直传分片断点续传三种方式</span>
</div>
<!-- 上传方式选择 -->
@@ -179,6 +180,11 @@
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>大文件分片上传提高可靠性网络中断可以从断点续传避免重复上传整个文件
</div>
</div>
</template>
@@ -4,100 +4,62 @@
-->
<template>
<div class="evolution-timeline">
<div class="timeline-header">
<span class="header-icon">🚀</span>
<span class="header-title">前端开发演进时间线</span>
<span class="header-subtitle">"贴海报""搭乐高" 20 年变迁</span>
<div class="demo-header">
<span class="icon">🚀</span>
<span class="title">前端演进时间线</span>
<span class="subtitle">"贴海报""搭乐高"20年变迁</span>
</div>
<!-- 时间线 -->
<div class="timeline-container">
<div
v-for="(era, index) in eras"
:key="era.id"
class="era-item"
:class="{ active: activeEra === era.id }"
@click="activeEra = activeEra === era.id ? null : era.id"
>
<div class="era-marker">
<div class="era-dot">{{ era.emoji }}</div>
<div v-if="index < eras.length - 1" class="era-line"></div>
</div>
<div class="era-content">
<div class="era-header">
<span class="era-year">{{ era.year }}</span>
<span class="era-name">{{ era.name }}</span>
<div class="demo-content">
<!-- 时间线 -->
<div class="timeline-container">
<div
v-for="(era, index) in eras"
:key="era.id"
class="era-item"
:class="{ active: activeEra === era.id }"
@click="activeEra = activeEra === era.id ? null : era.id"
>
<div class="era-marker">
<div class="era-dot">{{ era.emoji }}</div>
<div v-if="index < eras.length - 1" class="era-line"></div>
</div>
<div class="era-brief">{{ era.brief }}</div>
<Transition name="expand">
<div v-if="activeEra === era.id" class="era-detail">
<div class="detail-section">
<div class="section-title">🔑 关键技术</div>
<div class="tech-tags">
<span
v-for="tech in era.technologies"
:key="tech"
class="tech-tag"
>{{ tech }}</span>
</div>
</div>
<div class="detail-section">
<div class="section-title">💪 优点</div>
<div class="benefit-list">
<div
v-for="benefit in era.pros"
:key="benefit"
class="benefit-item"
>
<span class="check-icon"></span>
<span>{{ benefit }}</span>
</div>
</div>
</div>
<div class="detail-section">
<div class="section-title"> 缺点</div>
<div class="problem-list">
<div
v-for="problem in era.cons"
:key="problem"
class="problem-item"
>
<span class="warn-icon">!</span>
<span>{{ problem }}</span>
</div>
</div>
</div>
<div class="detail-section" v-if="era.metaphor">
<div class="section-title">💡 生活比喻</div>
<div class="metaphor-box">{{ era.metaphor }}</div>
</div>
<div class="era-content">
<div class="era-header">
<span class="era-year">{{ era.year }}</span>
<span class="era-name">{{ era.name }}</span>
</div>
</Transition>
<div class="era-brief">{{ era.brief }}</div>
<Transition name="expand">
<div v-if="activeEra === era.id" class="era-detail">
<div class="detail-section">
<div class="section-title">🔑 关键技术</div>
<div class="tech-tags">
<span
v-for="tech in era.technologies.slice(0, 5)"
:key="tech"
class="tech-tag"
>{{ tech }}</span>
</div>
</div>
<div class="detail-section" v-if="era.metaphor">
<div class="section-title">💡 生活比喻</div>
<div class="metaphor-box">{{ era.metaphor }}</div>
</div>
</div>
</Transition>
</div>
</div>
</div>
</div>
<!-- 提示 -->
<div class="timeline-hint">
<span>👆</span>
<span>点击任意时代查看详细信息</span>
</div>
<!-- 核心要点 -->
<div class="key-takeaway">
<span class="takeaway-icon">🎯</span>
<div class="takeaway-content">
<strong>核心思想</strong>
前端技术的演进本质是为了解决两个问题
<strong>提升开发效率</strong>从手动到自动化
<strong>支撑更复杂的应用</strong>从简单页面到桌面级应用
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>前端技术的演进本质是为了解决两个问题提升开发效率从手动到自动化和支撑更复杂的应用从简单页面到桌面级应用
</div>
</div>
</template>
@@ -168,37 +130,39 @@ const eras = [
<style scoped>
.evolution-timeline {
border: 2px solid #e0e0e0;
border-radius: 16px;
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.timeline-header {
text-align: center;
margin-bottom: 32px;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.header-icon {
font-size: 48px;
display: block;
margin-bottom: 12px;
.demo-header .icon {
font-size: 1.25rem;
}
.header-title {
display: block;
font-size: 24px;
.demo-header .title {
font-weight: bold;
color: #333;
margin-bottom: 8px;
font-size: 1rem;
}
.header-subtitle {
display: block;
font-size: 14px;
color: #666;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
margin-bottom: 0.5rem;
}
/* 时间线容器 */
@@ -208,8 +172,8 @@ const eras = [
.era-item {
display: flex;
gap: 20px;
margin-bottom: 24px;
gap: 1rem;
margin-bottom: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
@@ -231,89 +195,88 @@ const eras = [
}
.era-dot {
width: 56px;
height: 56px;
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
background: var(--vp-c-brand);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
font-size: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 1;
transition: all 0.3s ease;
}
.era-item:hover .era-dot {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
.era-line {
width: 4px;
flex: 1;
background: linear-gradient(180deg, #667eea, #e0e0e0);
background: var(--vp-c-divider);
margin-top: 8px;
min-height: 40px;
min-height: 30px;
}
/* 内容区域 */
.era-content {
flex: 1;
background: white;
border-radius: 12px;
padding: 16px;
border: 2px solid #e0e0e0;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 0.75rem;
border: 2px solid var(--vp-c-divider);
transition: all 0.3s ease;
}
.era-item:hover .era-content {
border-color: #667eea;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.1);
border-color: var(--vp-c-brand);
}
.era-item.active .era-content {
border-color: #667eea;
background: linear-gradient(135deg, #f8f9ff, #ffffff);
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.era-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.era-year {
padding: 4px 12px;
background: linear-gradient(135deg, #667eea, #764ba2);
padding: 2px 10px;
background: var(--vp-c-brand);
color: white;
border-radius: 12px;
font-size: 12px;
border-radius: 8px;
font-size: 0.75rem;
font-weight: bold;
}
.era-name {
font-size: 18px;
font-size: 1rem;
font-weight: bold;
color: #333;
color: var(--vp-c-text-1);
}
.era-brief {
font-size: 14px;
color: #666;
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
/* 详情展开 */
.era-detail {
margin-top: 16px;
padding-top: 16px;
border-top: 2px dashed #e0e0e0;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 2px dashed var(--vp-c-divider);
}
.detail-section {
margin-bottom: 16px;
margin-bottom: 0.75rem;
}
.detail-section:last-child {
@@ -321,124 +284,36 @@ const eras = [
}
.section-title {
font-size: 13px;
font-size: 0.8rem;
font-weight: bold;
color: #667eea;
margin-bottom: 8px;
color: var(--vp-c-brand);
margin-bottom: 0.5rem;
}
/* 技术标签 */
.tech-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 0.5rem;
}
.tech-tag {
padding: 4px 12px;
background: #f0f4ff;
color: #667eea;
border-radius: 12px;
font-size: 12px;
padding: 2px 10px;
background: var(--vp-c-bg-soft);
color: var(--vp-c-brand);
border-radius: 8px;
font-size: 0.75rem;
font-weight: 500;
}
/* 优点列表 */
.benefit-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.benefit-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #16a34a;
}
.check-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
background: #dcfce7;
border-radius: 50%;
font-size: 10px;
font-weight: bold;
}
/* 缺点列表 */
.problem-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.problem-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #dc2626;
}
.warn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
background: #fecaca;
border-radius: 50%;
font-size: 10px;
font-weight: bold;
}
/* 比喻框 */
.metaphor-box {
background: linear-gradient(135deg, #fff7ed, #ffedd5);
border-left: 4px solid #f97316;
padding: 12px;
border-radius: 8px;
font-size: 13px;
color: #9a3412;
line-height: 1.6;
}
/* 提示 */
.timeline-hint {
text-align: center;
font-size: 13px;
color: #666;
margin: 16px 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
/* 核心要点 */
.key-takeaway {
display: flex;
gap: 12px;
padding: 16px;
background: linear-gradient(135deg, #dcfce7, #d1fae5);
border-radius: 12px;
border-left: 4px solid #16a34a;
}
.takeaway-icon {
font-size: 24px;
flex-shrink: 0;
}
.takeaway-content {
flex: 1;
font-size: 14px;
color: #14532d;
background: var(--vp-c-bg-alt);
border-left: 4px solid var(--vp-c-brand);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
@@ -457,28 +332,25 @@ const eras = [
.expand-enter-to,
.expand-leave-from {
max-height: 1000px;
max-height: 600px;
opacity: 1;
}
/* 响应式 */
@media (max-width: 768px) {
.era-item {
flex-direction: column;
gap: 12px;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.era-marker {
flex-direction: row;
gap: 12px;
}
.info-box .icon {
flex-shrink: 0;
}
.era-line {
width: 100%;
height: 4px;
min-height: 0;
margin-top: 0;
margin-left: 8px;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,19 +1,19 @@
<!--
ImperativeVsDeclarativeDemo.vue
命令式 vs 声明式编程对比演示
用途
通过并排的交互式计数器直观展示 ImperativejQuery DeclarativeVue
在代码量和心智负担上的差异
交互功能
- 两个可交互的计数器
- 切换展示背后的代码实现
- 高亮显示 jQuery 需要手动更新的多个 DOM 节点 vs Vue 的自动绑定
ImperativeVsDeclarativeDemo.vue - 命令式 vs 声明式编程对比
"画画的两种方式"来解释 jQuery vs Vue/React 的区别
-->
<template>
<div class="imperative-declarative-demo">
<!-- 标题区 -->
<div class="demo-header">
<span class="icon">🎨</span>
<span class="title">编程范式对比</span>
<span class="subtitle">告诉"怎么做" vs 告诉"要什么"</span>
</div>
<!-- 主内容区 -->
<div class="demo-content">
<!-- 视图切换 -->
<div class="toggle-group">
<button
v-for="view in views"
@@ -24,14 +24,13 @@
{{ view.label }}
</button>
</div>
</div>
<div class="comparison-container">
<!-- Imperative Side (jQuery) -->
<div class="side imperative-side">
<div class="side-header">
<span class="badge imperative">jQuery / Imperative</span>
<h4>"Tell me HOW"</h4>
<span class="badge imperative">jQuery / 命令式</span>
<span class="sub-label">通俗说法: 告诉怎么做</span>
</div>
<div class="demo-area">
@@ -112,8 +111,8 @@
<!-- Declarative Side (Vue) -->
<div class="side declarative-side">
<div class="side-header">
<span class="badge declarative">Vue / Declarative</span>
<h4>"Tell me WHAT"</h4>
<span class="badge declarative">Vue / 声明式</span>
<span class="sub-label">通俗说法: 告诉要什么</span>
</div>
<div class="demo-area">
@@ -188,11 +187,20 @@
</div>
</div>
<!-- 底部控制 -->
<div class="demo-controls">
<button class="toggle-btn" @click="showAnalysis = !showAnalysis">
{{ showAnalysis ? '隐藏' : '显示' }}对比分析
</button>
</div>
</div>
<!-- 信息框 -->
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>
命令式编程需要一步步告诉浏览器"怎么做"声明式编程只需告诉浏览器"要什么"框架会自动处理细节
</div>
</div>
</template>
@@ -224,13 +232,40 @@ function updateJq(change) {
.imperative-declarative-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
/* 标题区 */
.demo-header {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.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;
}
.demo-content {
margin-bottom: 0.5rem;
}
.toggle-group {
@@ -275,9 +310,10 @@ function updateJq(change) {
.side-header {
text-align: center;
margin-bottom: 1rem;
}
.badge {
.side-header .badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
@@ -285,14 +321,11 @@ function updateJq(change) {
font-weight: 600;
}
.badge.imperative {
background-color: rgba(7, 105, 173, 0.2);
color: #0769ad;
}
.badge.declarative {
background-color: rgba(66, 184, 131, 0.2);
color: #2c8a5e;
.side-header .sub-label {
display: block;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-top: 0.5rem;
}
.side-header h4 {
@@ -356,7 +389,7 @@ function updateJq(change) {
}
.status-text.warning {
color: #f87171;
color: var(--vp-c-warning);
font-weight: 600;
}
@@ -406,8 +439,8 @@ function updateJq(change) {
}
.imperative-code {
background-color: #1e1e2e;
color: #a6accd;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
}
.imperative-code code {
@@ -415,8 +448,8 @@ function updateJq(change) {
}
.declarative-code {
background-color: #1e1e2e;
color: #a6accd;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
}
.declarative-code code {
@@ -433,7 +466,7 @@ function updateJq(change) {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #0769ad, #42b883);
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
@@ -460,23 +493,43 @@ function updateJq(change) {
}
.pain-point {
background-color: rgba(248, 113, 113, 0.1);
color: #dc2626;
background: var(--vp-c-bg-alt);
color: var(--vp-c-danger);
}
.benefit {
background-color: rgba(74, 222, 128, 0.1);
color: #16a34a;
background: var(--vp-c-bg-alt);
color: var(--vp-c-success);
}
.demo-controls {
display: flex;
justify-content: center;
margin-top: 1.5rem;
padding-top: 1.5rem;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--vp-c-divider);
}
/* 信息框 */
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
margin-top: 0.75rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
@@ -1,152 +1,130 @@
<!--
JQueryVsStateDemo.vue - 餐厅账本对比
JQueryVsStateDemo.vue - 前端开发模式对比
"手工记账 vs 智能管家"的比喻来解释 jQuery vs Vue/React
-->
<template>
<div class="restaurant-demo">
<!-- 故事引入 -->
<div class="story-intro">
<div class="story-icon">👨🍳📒🤖</div>
<h3 class="story-title">老张的餐厅账本</h3>
<p class="story-desc">
老张开了家餐厅每天要点菜做菜算账有两种记账方式<br>
<strong>传统方式老张手工记</strong>jQuery 模式 vs <strong>智能方式请个管家</strong>Vue/React 模式<br>
看看哪种更轻松
</p>
<div class="jquery-vs-state-demo">
<!-- 标题区 -->
<div class="demo-header">
<span class="icon">🔄</span>
<span class="title">前端开发模式</span>
<span class="subtitle">手动操作DOM vs 状态管理</span>
</div>
<!-- 模式选择 -->
<div class="mode-tabs">
<button
class="tab-btn"
:class="{ active: mode === 'manual' }"
@click="mode = 'manual'"
>
<span class="tab-icon"></span>
<span class="tab-text">手工记账</span>
<span class="tab-sub">jQuery 方式</span>
</button>
<button
class="tab-btn"
:class="{ active: mode === 'smart' }"
@click="mode = 'smart'"
>
<span class="tab-icon">🤖</span>
<span class="tab-text">智能管家</span>
<span class="tab-sub">Vue/React 方式</span>
</button>
</div>
<!-- 对比展示区 -->
<div class="comparison-showcase">
<!-- 左侧场景描述 -->
<div class="scenario-panel">
<div class="scenario-header">
<span class="scenario-icon">{{ mode === 'manual' ? '👨‍🍳' : '🤖' }}</span>
<span class="scenario-title">{{ mode === 'manual' ? '老张手工记账' : '智能管家记账' }}</span>
</div>
<div class="scenario-content">
<div class="step-list">
<div
v-for="(step, index) in currentSteps"
:key="index"
class="step-item"
:class="{ active: index === currentStep }"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-text">{{ step }}</div>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="demo-content">
<!-- 模式选择 -->
<div class="mode-tabs">
<button
class="tab-btn"
:class="{ active: mode === 'manual' }"
@click="mode = 'manual'"
>
<span class="tab-icon"></span>
<span class="tab-text">手工记账</span>
<span class="tab-sub">通俗说法: jQuery</span>
</button>
<button
class="tab-btn"
:class="{ active: mode === 'smart' }"
@click="mode = 'smart'"
>
<span class="tab-icon">🤖</span>
<span class="tab-text">智能管家</span>
<span class="tab-sub">通俗说法: Vue/React</span>
</button>
</div>
<!-- 右侧账本展示 -->
<div class="ledger-panel">
<div class="ledger-header">
<span class="ledger-icon">📒</span>
<span class="ledger-title">今日账本</span>
<span class="ledger-status" :class="mode">{{ ledgerStatus }}</span>
</div>
<!-- 对比展示 -->
<div class="comparison-showcase">
<!-- 左侧场景描述 -->
<div class="scenario-panel">
<div class="scenario-header">
<span class="scenario-icon">{{ mode === 'manual' ? '👨‍🍳' : '🤖' }}</span>
<span class="scenario-title">{{ mode === 'manual' ? '手工记账' : '智能管家' }}</span>
</div>
<div class="ledger-content">
<!-- 订单列表 -->
<div class="order-list">
<div
v-for="order in orders"
:key="order.id"
class="order-item"
:class="{ completed: order.completed }"
>
<div class="order-info">
<span class="order-name">{{ order.name }}</span>
<span class="order-price">¥{{ order.price }}</span>
</div>
<div class="order-status">
{{ order.completed ? '✓' : '○' }}
<div class="scenario-content">
<div class="step-list">
<div
v-for="(step, index) in currentSteps"
:key="index"
class="step-item"
:class="{ active: index === currentStep }"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-text">{{ step }}</div>
</div>
</div>
</div>
</div>
<!-- 总计 -->
<div class="total-section">
<div class="total-row">
<span>菜品数量</span>
<span class="total-value">{{ completedCount }}/{{ orders.length }} </span>
<!-- 右侧账本展示 -->
<div class="ledger-panel">
<div class="ledger-header">
<span class="ledger-icon">📒</span>
<span class="ledger-title">今日账本</span>
<span class="ledger-status" :class="mode">{{ ledgerStatus }}</span>
</div>
<div class="ledger-content">
<!-- 订单列表 -->
<div class="order-list">
<div
v-for="order in orders"
:key="order.id"
class="order-item"
:class="{ completed: order.completed }"
>
<div class="order-info">
<span class="order-name">{{ order.name }}</span>
<span class="order-price">¥{{ order.price }}</span>
</div>
<div class="order-status">
{{ order.completed ? '✓' : '○' }}
</div>
</div>
</div>
<div class="total-row total-final">
<span>今日营收</span>
<span class="total-amount">¥{{ totalRevenue }}</span>
<!-- 总计 -->
<div class="total-section">
<div class="total-row">
<span>菜品数量</span>
<span class="total-value">{{ completedCount }}/{{ orders.length }} </span>
</div>
<div class="total-row total-final">
<span>今日营收</span>
<span class="total-amount">¥{{ totalRevenue }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button
class="btn btn-primary"
@click="processOrder"
:disabled="isProcessing || allCompleted"
>
{{ isProcessing ? '处理中...' : allCompleted ? '今日完成!' : '下一道菜' }}
</button>
<button
class="btn btn-secondary"
@click="resetDemo"
>
重新开始
</button>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button
class="btn btn-primary"
@click="processOrder"
:disabled="isProcessing || allCompleted"
>
{{ isProcessing ? '处理中...' : allCompleted ? '今日完成!' : '下一道菜' }}
</button>
<button
class="btn btn-secondary"
@click="resetDemo"
>
重新开始
</button>
</div>
<!-- 优缺点对比 -->
<div class="comparison-table">
<div class="table-header">
<div class="table-title">💡 两种方式对比</div>
</div>
<div class="table-content">
<div class="comparison-row header">
<div class="col-feature">特点</div>
<div class="col-manual">手工记账 (jQuery)</div>
<div class="col-smart">智能管家 (Vue/React)</div>
</div>
<div class="comparison-row">
<div class="col-feature">工作方式</div>
<div class="col-manual">手动改每一处</div>
<div class="col-smart">改数据界面自动变</div>
</div>
<div class="comparison-row">
<div class="col-feature">容易出错</div>
<div class="col-manual">容易漏改某处</div>
<div class="col-smart">自动同步不易错</div>
</div>
<div class="comparison-row">
<div class="col-feature">适合场景</div>
<div class="col-manual">简单页面</div>
<div class="col-smart">复杂交互应用</div>
</div>
</div>
<!-- 信息框 -->
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想:</strong>
<span v-if="mode === 'manual'">jQuery需要手动查找和修改DOM,就像手工记账,容易出错</span>
<span v-else>Vue/React通过状态自动更新界面,就像智能管家,改数据界面自动变</span>
</div>
</div>
</template>
@@ -237,53 +215,53 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
</script>
<style scoped>
.restaurant-demo {
border: 2px solid #e8e8e8;
border-radius: 16px;
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
.jquery-vs-state-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
/* 故事引入 */
.story-intro {
text-align: center;
margin-bottom: 24px;
padding: 20px;
background: linear-gradient(135deg, #fff8e1, #ffecb3);
border-radius: 16px;
border: 2px dashed #ffc107;
/* 标题区 */
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.story-icon {
font-size: 48px;
margin-bottom: 12px;
.demo-header .icon {
font-size: 1.25rem;
}
.story-title {
font-size: 24px;
.demo-header .title {
font-weight: bold;
color: #e65100;
margin: 0 0 8px 0;
font-size: 1rem;
}
.story-desc {
font-size: 14px;
color: #666;
line-height: 1.6;
margin: 0;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
/* 主内容区 */
.demo-content {
margin-bottom: 0.75rem;
}
/* 模式选项卡 */
.mode-tabs {
display: flex;
gap: 12px;
margin-bottom: 24px;
background: white;
padding: 8px;
border-radius: 12px;
border: 2px solid #e0e0e0;
gap: 0.75rem;
margin-bottom: 1rem;
background: var(--vp-c-bg);
padding: 0.5rem;
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
}
.tab-btn {
@@ -291,35 +269,36 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 16px;
gap: 0.25rem;
padding: 0.75rem;
border: none;
border-radius: 8px;
border-radius: 6px;
background: transparent;
cursor: pointer;
transition: all 0.3s ease;
transition: all 0.2s;
color: var(--vp-c-text-1);
}
.tab-btn:hover {
background: #f5f5f5;
background: var(--vp-c-bg-alt);
}
.tab-btn.active {
background: linear-gradient(135deg, #667eea, #764ba2);
background: var(--vp-c-brand);
color: white;
}
.tab-icon {
font-size: 32px;
font-size: 1.5rem;
}
.tab-text {
font-size: 14px;
font-size: 0.85rem;
font-weight: bold;
}
.tab-sub {
font-size: 12px;
font-size: 0.75rem;
opacity: 0.8;
}
@@ -327,8 +306,8 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.comparison-showcase {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 24px;
gap: 1rem;
margin-bottom: 1rem;
}
@media (max-width: 768px) {
@@ -339,67 +318,67 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
/* 场景面板 */
.scenario-panel {
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
background: var(--vp-c-bg);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
overflow: hidden;
}
.scenario-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: linear-gradient(135deg, #ffecb3, #ffe082);
border-bottom: 2px solid #e0e0e0;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-bottom: 2px solid var(--vp-c-divider);
}
.scenario-icon {
font-size: 28px;
font-size: 1.5rem;
}
.scenario-title {
font-size: 16px;
font-size: 0.9rem;
font-weight: bold;
color: #333;
color: var(--vp-c-text-1);
}
.scenario-content {
padding: 16px;
padding: 1rem;
}
.step-list {
display: flex;
flex-direction: column;
gap: 8px;
gap: 0.5rem;
}
.step-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
transition: all 0.3s ease;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
transition: all 0.2s;
}
.step-item.active {
background: linear-gradient(135deg, #667eea, #764ba2);
background: var(--vp-c-brand);
color: white;
transform: translateX(8px);
transform: translateX(4px);
}
.step-number {
width: 28px;
height: 28px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: white;
color: #333;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border-radius: 50%;
font-size: 12px;
font-size: 0.75rem;
font-weight: bold;
}
@@ -409,142 +388,147 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
}
.step-text {
font-size: 14px;
font-size: 0.85rem;
flex: 1;
}
/* 账本面板 */
.ledger-panel {
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
background: var(--vp-c-bg);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
overflow: hidden;
}
.ledger-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: linear-gradient(135deg, #c8e6c9, #a5d6a7);
border-bottom: 2px solid #e0e0e0;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-bottom: 2px solid var(--vp-c-divider);
}
.ledger-icon {
font-size: 28px;
font-size: 1.5rem;
}
.ledger-title {
flex: 1;
font-size: 16px;
font-size: 0.9rem;
font-weight: bold;
color: #333;
color: var(--vp-c-text-1);
}
.ledger-status {
font-size: 12px;
padding: 4px 12px;
font-size: 0.75rem;
padding: 0.25rem 0.75rem;
border-radius: 12px;
background: white;
color: #666;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
}
.ledger-status.manual {
background: #fff3e0;
color: #e65100;
background: var(--vp-c-warning);
color: white;
}
.ledger-status.smart {
background: var(--vp-c-success);
color: white;
}
.ledger-content {
padding: 16px;
padding: 1rem;
}
.order-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
gap: 0.5rem;
margin-bottom: 1rem;
}
.order-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
transition: all 0.3s ease;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
transition: all 0.2s;
}
.order-item.completed {
background: #e8f5e9;
border-left: 4px solid #4caf50;
background: var(--vp-c-success);
border-left: 4px solid var(--vp-c-brand);
opacity: 0.3;
}
.order-info {
display: flex;
flex-direction: column;
gap: 4px;
gap: 0.25rem;
}
.order-name {
font-size: 14px;
font-size: 0.85rem;
font-weight: bold;
color: #333;
color: var(--vp-c-text-1);
}
.order-price {
font-size: 13px;
color: #e65100;
font-size: 0.75rem;
color: var(--vp-c-brand);
font-weight: bold;
}
.order-status {
font-size: 18px;
font-size: 1rem;
}
.total-section {
border-top: 2px dashed #e0e0e0;
padding-top: 12px;
border-top: 2px dashed var(--vp-c-divider);
padding-top: 0.75rem;
}
.total-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 14px;
color: #666;
padding: 0.5rem 0;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.total-row.total-final {
font-size: 16px;
font-size: 0.9rem;
font-weight: bold;
color: #333;
border-top: 2px solid #e0e0e0;
margin-top: 8px;
padding-top: 12px;
color: var(--vp-c-text-1);
border-top: 2px solid var(--vp-c-divider);
margin-top: 0.5rem;
padding-top: 0.75rem;
}
.total-amount {
color: #4caf50;
font-size: 20px;
color: var(--vp-c-success);
font-size: 1.1rem;
}
/* 操作按钮 */
.action-buttons {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 24px;
gap: 0.75rem;
}
.btn {
padding: 12px 24px;
padding: 0.5rem 1rem;
border: none;
border-radius: 8px;
font-size: 16px;
border-radius: 6px;
font-size: 0.85rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
transition: all 0.2s;
}
.btn:hover:not(:disabled) {
@@ -558,91 +542,31 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
background: var(--vp-c-brand);
color: white;
}
.btn-secondary {
background: #f5f5f5;
color: #666;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
}
/* 对比表格 */
.comparison-table {
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
overflow: hidden;
/* 信息框 */
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.table-header {
padding: 16px;
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
border-bottom: 2px solid #e0e0e0;
.info-box .icon {
flex-shrink: 0;
}
.table-title {
font-size: 16px;
font-weight: bold;
color: #1565c0;
}
.table-content {
padding: 0;
}
.comparison-row {
display: grid;
grid-template-columns: 1.2fr 1.4fr 1.4fr;
gap: 16px;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
}
.comparison-row:last-child {
border-bottom: none;
}
.comparison-row.header {
background: #f5f5f5;
font-weight: bold;
color: #333;
}
.col-feature {
color: #666;
}
.col-manual {
color: #e65100;
}
.col-smart {
color: #4caf50;
}
.comparison-row.header .col-manual,
.comparison-row.header .col-smart {
color: #333;
}
/* 响应式 */
@media (max-width: 768px) {
.comparison-showcase {
grid-template-columns: 1fr;
}
.comparison-row {
grid-template-columns: 1fr;
gap: 8px;
}
.comparison-row.header {
display: none;
}
.mode-tabs {
flex-direction: column;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -4,16 +4,24 @@
-->
<template>
<div class="routing-demo">
<!-- 故事引入 -->
<div class="story-box">
<div class="story-emoji">📖📄</div>
<h4 class="story-title">小明看书记</h4>
<p class="story-text">
小明喜欢看书有两种看书方式<br>
<strong>MPA 方式像翻书</strong>每翻一页都要换一本书 <strong>SPA 方式像换纸</strong>在同一本书里换内容
</p>
<!-- 标题区 -->
<div class="demo-header">
<span class="icon">📖</span>
<span class="title">路由模式对比</span>
<span class="subtitle">MPA 多页应用 vs SPA 单页应用</span>
</div>
<!-- 主内容区 -->
<div class="demo-content">
<!-- 故事引入 -->
<div class="story-box">
<p class="story-text">
<strong>通俗说法</strong>小明喜欢看书有两种看书方式<br>
<strong>MPA 方式像翻书</strong>每翻一页都要换一本书<br>
<strong>SPA 方式像换纸</strong>在同一本书里换内容
</p>
</div>
<!-- 模式选择 -->
<div class="mode-selector">
<div
@@ -23,7 +31,7 @@
>
<div class="mode-icon">📚</div>
<div class="mode-name">MPA 多页应用</div>
<div class="mode-sub">像翻书每次都换一本</div>
<div class="mode-sub">通俗说法: 像翻书</div>
<div class="mode-desc">每点一次链接浏览器向服务器要新页面</div>
</div>
@@ -36,7 +44,7 @@
>
<div class="mode-icon">📄</div>
<div class="mode-name">SPA 单页应用</div>
<div class="mode-sub">像换纸同一本书换内容</div>
<div class="mode-sub">通俗说法: 像换纸</div>
<div class="mode-desc">只加载一次后续只切换内容</div>
</div>
</div>
@@ -185,14 +193,13 @@
</div>
<!-- 核心要点 -->
<div class="key-takeaway">
<div class="takeaway-icon">🎯</div>
<div class="takeaway-content">
<strong>核心差异</strong>
<strong>MPA</strong> 每次切换都要"整页刷新"翻书适合内容为主的网站
<strong>SPA</strong> 只加载一次后续"局部更新"像换纸适合交互复杂的应用
关键是<strong>状态会不会丢</strong>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>
<strong>MPA</strong> 每次切换都要"整页刷新"像翻书适合内容为主的网站
<strong>SPA</strong> 只加载一次后续"局部更新"换纸适合交互复杂的应用
关键是<strong>状态会不会丢</strong>
</div>
</div>
</div>
</template>
@@ -251,7 +258,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
<style scoped>
.routing-demo {
border: 2px solid #e0e0e0;
border: 2px solid var(--vp-c-divider);
border-radius: 16px;
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
padding: 24px;
@@ -303,7 +310,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
min-width: 200px;
max-width: 280px;
background: white;
border: 3px solid #e0e0e0;
border: 3px solid var(--vp-c-divider);
border-radius: 16px;
padding: 20px;
text-align: center;
@@ -356,7 +363,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.demo-area {
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
border: 2px solid var(--vp-c-divider);
padding: 20px;
margin-bottom: 24px;
}
@@ -493,7 +500,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
/* 阅读区 */
.reading-paper {
background: white;
border: 2px solid #e0e0e0;
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
padding: 16px;
min-height: 200px;
@@ -524,7 +531,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.state-test {
margin-top: 16px;
padding-top: 16px;
border-top: 2px dashed #e0e0e0;
border-top: 2px dashed var(--vp-c-divider);
}
.test-label {
@@ -537,7 +544,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.test-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #e0e0e0;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
font-size: 13px;
box-sizing: border-box;
@@ -572,7 +579,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.nav-btn {
padding: 8px 16px;
border: 2px solid #e0e0e0;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
background: white;
cursor: pointer;
@@ -632,7 +639,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
.comparison-table {
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
border: 2px solid var(--vp-c-divider);
overflow: hidden;
margin-bottom: 20px;
}
@@ -655,7 +662,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
grid-template-columns: 1fr 1.5fr 1.5fr;
gap: 16px;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
border-bottom: 1px solid var(--vp-c-divider);
}
.comparison-row:last-child {
@@ -1,141 +1,140 @@
<!--
SliceRequestDemo.vue - 搬家快递大作战
"搬家打包"的比喻来解释 HTTP 请求优化切图 vs 雪碧图
SliceRequestDemo.vue - HTTP请求优化对比
"搬家"的比喻来解释雪碧图 vs 切片请求
-->
<template>
<div class="moving-game">
<!-- 故事引入 -->
<div class="story-box">
<div class="story-emoji">📦🚚🏠</div>
<h4 class="story-title">小明搬家记</h4>
<p class="story-text">
小明要搬 6 箱书到新房子有两种搬家方式<br>
<strong>A 方案一箱一箱搬</strong>切图模式 vs <strong>B 方案一次性打包运走</strong>雪碧图模式<br>
看看哪种更省时间
</p>
<div class="slice-request-demo">
<!-- 标题区 -->
<div class="demo-header">
<span class="icon">📦</span>
<span class="title">HTTP请求优化</span>
<span class="subtitle">雪碧图 vs 独立请求</span>
</div>
<!-- 模式选择 -->
<div class="mode-selector">
<div
class="mode-card"
:class="{ active: mode === 'separate' }"
@click="mode = 'separate'"
>
<div class="mode-icon">🛵</div>
<div class="mode-name">A 方案一箱一趟</div>
<div class="mode-desc">小面包车一次拉一箱</div>
<div class="mode-detail">需要 6 趟运输</div>
<!-- 主内容区 -->
<div class="demo-content">
<!-- 故事引入 -->
<div class="story-box">
<p class="story-text">
<strong>通俗说法</strong>就像搬家<br>
<strong>切图模式</strong>一箱一箱搬需要6趟6次HTTP请求<br>
<strong>雪碧图模式</strong>打包一次性运走只需1趟1次HTTP请求
</p>
</div>
<div class="vs-divider">VS</div>
<div
class="mode-card"
:class="{ active: mode === 'packed' }"
@click="mode = 'packed'"
>
<div class="mode-icon">🚚</div>
<div class="mode-name">B 方案打包一车拉</div>
<div class="mode-desc">大卡车6箱一次运走</div>
<div class="mode-detail">只需 1 趟运输</div>
</div>
</div>
<!-- 动画演示区 -->
<div class="animation-area">
<!-- 起点 -->
<div class="location start">
<div class="location-icon">🏠</div>
<div class="location-label">旧家</div>
<div class="boxes-remaining">
剩余箱子: <span class="count">{{ remainingBoxes }}</span>
</div>
</div>
<!-- 道路 -->
<div class="road">
<div class="road-line"></div>
<!-- 运输车辆 -->
<!-- 模式选择 -->
<div class="mode-selector">
<div
v-for="vehicle in vehicles"
:key="vehicle.id"
class="vehicle"
:class="{ 'moving': vehicle.isMoving }"
:style="{ left: vehicle.position + '%' }"
class="mode-card"
:class="{ active: mode === 'separate' }"
@click="mode = 'separate'"
>
<div class="vehicle-body">
{{ mode === 'separate' ? '🛵' : '🚚' }}
<div class="mode-icon">🛵</div>
<div class="mode-name">切图模式</div>
<div class="mode-desc">通俗说法: 一箱一趟</div>
<div class="mode-detail">需要 6 趟运输</div>
</div>
<div class="vs-divider">VS</div>
<div
class="mode-card"
:class="{ active: mode === 'packed' }"
@click="mode = 'packed'"
>
<div class="mode-icon">🚚</div>
<div class="mode-name">雪碧图模式</div>
<div class="mode-desc">通俗说法: 打包一车拉</div>
<div class="mode-detail">只需 1 趟运输</div>
</div>
</div>
<!-- 动画演示区 -->
<div class="animation-area">
<!-- 起点 -->
<div class="location start">
<div class="location-icon">🏠</div>
<div class="location-label">旧家</div>
<div class="boxes-remaining">
剩余箱子: <span class="count">{{ remainingBoxes }}</span>
</div>
<div class="vehicle-cargo" v-if="vehicle.cargo > 0">
{{ mode === 'separate' ? '📦' : '📦×' + vehicle.cargo }}
</div>
<!-- 道路 -->
<div class="road">
<div class="road-line"></div>
<!-- 运输车辆 -->
<div
v-for="vehicle in vehicles"
:key="vehicle.id"
class="vehicle"
:class="{ 'moving': vehicle.isMoving }"
:style="{ left: vehicle.position + '%' }"
>
<div class="vehicle-body">
{{ mode === 'separate' ? '🛵' : '🚚' }}
</div>
<div class="vehicle-cargo" v-if="vehicle.cargo > 0">
{{ mode === 'separate' ? '📦' : '📦×' + vehicle.cargo }}
</div>
</div>
</div>
<!-- 终点 -->
<div class="location end">
<div class="location-icon">🏡</div>
<div class="location-label">新家</div>
<div class="boxes-delivered">
已送达: <span class="count">{{ deliveredBoxes }}</span>/6
</div>
</div>
</div>
<!-- 终点 -->
<div class="location end">
<div class="location-icon">🏡</div>
<div class="location-label">新家</div>
<div class="boxes-delivered">
已送达: <span class="count">{{ deliveredBoxes }}</span>/6
<!-- 统计面板 -->
<div class="stats-panel">
<div class="stat-item">
<div class="stat-label">运输趟数</div>
<div class="stat-value" :class="{ 'good': trips <= 2, 'bad': trips > 2 }">
{{ trips }}
</div>
</div>
<div class="stat-item">
<div class="stat-label">总耗时</div>
<div class="stat-value">{{ totalTime.toFixed(1) }} </div>
</div>
<div class="stat-item">
<div class="stat-label">效率评分</div>
<div class="stat-value" :class="efficiencyClass">
{{ efficiency }}
</div>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button
class="btn btn-primary"
@click="startSimulation"
:disabled="isRunning"
>
{{ isRunning ? '运输中...' : '开始搬家' }}
</button>
<button
class="btn btn-secondary"
@click="resetSimulation"
>
重置
</button>
</div>
</div>
<!-- 统计面板 -->
<div class="stats-panel">
<div class="stat-item">
<div class="stat-label">运输趟数</div>
<div class="stat-value" :class="{ 'good': trips <= 2, 'bad': trips > 2 }">
{{ trips }}
</div>
</div>
<div class="stat-item">
<div class="stat-label">总耗时</div>
<div class="stat-value">{{ totalTime.toFixed(1) }} </div>
</div>
<div class="stat-item">
<div class="stat-label">效率评分</div>
<div class="stat-value" :class="efficiencyClass">
{{ efficiency }}
</div>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button
class="btn btn-primary"
@click="startSimulation"
:disabled="isRunning"
>
{{ isRunning ? '运输中...' : '开始搬家' }}
</button>
<button
class="btn btn-secondary"
@click="resetSimulation"
>
重置
</button>
</div>
<!-- 知识点总结 -->
<div class="knowledge-box">
<div class="knowledge-title">💡 核心原理</div>
<div class="knowledge-content">
<p v-if="mode === 'separate'">
<strong>切图模式分开请求</strong>就像一箱一箱搬每次只拉一件货
浏览器要发起 6 HTTP 请求每次都要建立连接传输数据
<span class="highlight-bad">效率低耗时长</span>
</p>
<p v-else>
<strong>雪碧图模式合并请求</strong>就像用大卡车一次性拉走所有箱子
浏览器只需 1 HTTP 请求就能获取所有图片
<span class="highlight-good">大幅减少连接开销速度更快</span>
</p>
</div>
<!-- 信息框 -->
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想:</strong>
<span v-if="mode === 'separate'">切图模式每次只拉一件货,需要6次HTTP请求,效率低</span>
<span v-else>雪碧图模式打包一次性运走,只需1次HTTP请求,大幅减少连接开销</span>
</div>
</div>
</template>
@@ -254,40 +253,56 @@ const resetStats = () => {
</script>
<style scoped>
.moving-game {
border: 2px solid #e8e8e8;
border-radius: 16px;
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
.slice-request-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
/* 标题区 */
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.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;
}
/* 主内容区 */
.demo-content {
margin-bottom: 0.75rem;
}
/* 故事框 */
.story-box {
text-align: center;
margin-bottom: 24px;
padding: 20px;
background: linear-gradient(135deg, #fff8e1, #ffecb3);
border-radius: 16px;
border: 2px dashed #ffc107;
}
.story-emoji {
font-size: 48px;
margin-bottom: 8px;
}
.story-title {
font-size: 20px;
font-weight: bold;
color: #8b4513;
margin: 0 0 8px 0;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
}
.story-text {
font-size: 14px;
color: #666;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin: 0;
line-height: 1.6;
}
@@ -297,67 +312,67 @@ const resetStats = () => {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 24px;
gap: 0.75rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.mode-card {
background: white;
border: 3px solid #e0e0e0;
border-radius: 16px;
padding: 20px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
min-width: 200px;
transition: all 0.2s;
min-width: 160px;
flex: 1;
max-width: 280px;
max-width: 220px;
}
.mode-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.mode-card.active {
border-color: #4caf50;
background: #e8f5e9;
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
}
.mode-icon {
font-size: 48px;
margin-bottom: 12px;
font-size: 2rem;
margin-bottom: 0.5rem;
}
.mode-name {
font-size: 16px;
font-size: 0.9rem;
font-weight: bold;
color: #333;
margin-bottom: 8px;
color: var(--vp-c-text-1);
margin-bottom: 0.5rem;
}
.mode-desc {
font-size: 13px;
color: #666;
margin-bottom: 8px;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.mode-detail {
font-size: 14px;
font-size: 0.85rem;
font-weight: bold;
color: #e65100;
padding: 4px 12px;
background: #fff3e0;
color: var(--vp-c-brand);
padding: 0.25rem 0.75rem;
background: var(--vp-c-bg-alt);
border-radius: 12px;
display: inline-block;
}
.vs-divider {
font-size: 24px;
font-size: 1.25rem;
font-weight: bold;
color: #999;
padding: 0 8px;
color: var(--vp-c-text-3);
padding: 0 0.5rem;
}
/* 动画演示区 */
@@ -365,52 +380,52 @@ const resetStats = () => {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
padding: 20px;
background: white;
border-radius: 16px;
border: 2px solid #e0e0e0;
gap: 1rem;
margin-bottom: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
}
.location {
text-align: center;
min-width: 100px;
min-width: 80px;
}
.location-icon {
font-size: 40px;
margin-bottom: 8px;
font-size: 2rem;
margin-bottom: 0.5rem;
}
.location-label {
font-size: 14px;
font-size: 0.85rem;
font-weight: bold;
color: #333;
margin-bottom: 8px;
color: var(--vp-c-text-1);
margin-bottom: 0.5rem;
}
.boxes-remaining,
.boxes-delivered {
font-size: 12px;
color: #666;
padding: 4px 8px;
background: #f5f5f5;
border-radius: 8px;
font-size: 0.75rem;
color: var(--vp-c-text-2);
padding: 0.25rem 0.5rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
}
.count {
font-weight: bold;
color: #e65100;
font-size: 16px;
color: var(--vp-c-brand);
font-size: 0.9rem;
}
.road {
flex: 1;
position: relative;
height: 80px;
background: linear-gradient(to bottom, #e8eaf6 0%, #c5cae9 100%);
border-radius: 8px;
height: 60px;
background: var(--vp-c-bg-alt);
border-radius: 6px;
overflow: hidden;
}
@@ -422,8 +437,8 @@ const resetStats = () => {
height: 4px;
background: repeating-linear-gradient(
90deg,
#7986cb 0px,
#7986cb 20px,
var(--vp-c-brand) 0px,
var(--vp-c-brand) 20px,
transparent 20px,
transparent 40px
);
@@ -441,89 +456,86 @@ const resetStats = () => {
}
.vehicle-body {
font-size: 32px;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
font-size: 1.5rem;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}
.vehicle-cargo {
font-size: 12px;
background: white;
padding: 2px 6px;
border-radius: 8px;
margin-top: 2px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
font-size: 0.75rem;
background: var(--vp-c-bg);
padding: 0.125rem 0.375rem;
border-radius: 6px;
margin-top: 0.125rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
font-weight: bold;
color: #e65100;
color: var(--vp-c-brand);
}
/* 统计面板 */
.stats-panel {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 20px;
gap: 1rem;
margin-bottom: 1rem;
}
.stat-item {
background: white;
border-radius: 12px;
padding: 16px;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
text-align: center;
border: 2px solid #e0e0e0;
border: 2px solid var(--vp-c-divider);
}
.stat-label {
font-size: 13px;
color: #666;
margin-bottom: 8px;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 24px;
font-size: 1.25rem;
font-weight: bold;
color: var(--vp-c-text-1);
}
.stat-value.good {
color: #4caf50;
color: var(--vp-c-success);
}
.stat-value.bad {
color: #f44336;
color: var(--vp-c-danger);
}
.stat-value.excellent {
color: #2196f3;
}
.stat-value.good {
color: #4caf50;
color: var(--vp-c-brand);
}
.stat-value.poor {
color: #ff9800;
color: var(--vp-c-warning);
}
/* 控制按钮 */
.controls {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 20px;
gap: 0.75rem;
margin-bottom: 1rem;
}
.btn {
padding: 12px 24px;
padding: 0.5rem 1rem;
border: none;
border-radius: 8px;
font-size: 16px;
border-radius: 6px;
font-size: 0.9rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
transition: all 0.2s;
}
.btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn:disabled {
@@ -532,48 +544,32 @@ const resetStats = () => {
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
background: var(--vp-c-brand);
color: white;
}
.btn-secondary {
background: #f5f5f5;
color: #666;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
}
/* 知识点总结 */
.knowledge-box {
background: linear-gradient(135deg, #e3f2fd, #f3e5f5);
border-radius: 12px;
padding: 20px;
border-left: 4px solid #2196f3;
/* 信息框 */
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.knowledge-title {
font-size: 16px;
font-weight: bold;
color: #1565c0;
margin-bottom: 12px;
.info-box .icon {
flex-shrink: 0;
}
.knowledge-content {
font-size: 14px;
line-height: 1.6;
color: #333;
}
.knowledge-content p {
margin: 0;
}
.highlight-good {
color: #4caf50;
font-weight: bold;
}
.highlight-bad {
color: #f44336;
font-weight: bold;
.info-box strong {
color: var(--vp-c-text-1);
}
/* 响应式 */
@@ -588,7 +584,7 @@ const resetStats = () => {
.animation-area {
flex-direction: column;
gap: 12px;
gap: 0.75rem;
}
.road {
@@ -1,6 +1,13 @@
<template>
<div class="three-areas-demo">
<div class="scene">
<div class="demo-header">
<span class="icon">📂</span>
<span class="title">Git 三区概念</span>
<span class="subtitle">工作区 暂存区 仓库</span>
</div>
<div class="demo-content">
<div class="scene">
<!-- 1. Working Directory (Desk) -->
<div class="zone working">
<div class="zone-header">
@@ -121,6 +128,7 @@
</div>
</div>
</div>
</div>
<div class="bottom">
<div class="block">
@@ -132,6 +140,11 @@
<pre class="mono"><code>{{ statusText }}</code></pre>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>Git 的三区就像餐厅工作区是餐桌随便放暂存区是备菜盘准备上菜仓库是菜单永久记录
</div>
</div>
</template>
@@ -239,11 +252,40 @@ const commitFiles = () => {
.three-areas-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
overflow-x: auto;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.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;
}
.demo-content {
margin-bottom: 0.5rem;
}
.scene {
@@ -641,4 +683,23 @@ const commitFiles = () => {
grid-template-columns: 1fr;
}
}
.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: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,134 +1,136 @@
<!--
GitWorkflowDemo.vue
Git 工作流演示 - 简洁
Git 基础工作流演示 - 寄快递
用途展示 Git 的基本工作流程
交互初始化提交创建分支合并
展示 Git 的基本工作流程修改 暂存 提交
高度控制紧凑布局确保在 600px
-->
<template>
<div class="git-workflow-demo">
<!-- 控制面板 -->
<div class="control-panel">
<button
@click="initRepo"
:disabled="inited || mergePending"
class="action-btn"
>
🎯 初始化仓库
</button>
<button
@click="makeCommit"
:disabled="!inited || mergePending"
class="action-btn"
>
提交
</button>
<button
@click="createBranch"
:disabled="!inited || hasBranch || mergePending"
class="action-btn"
>
🌿 创建分支
</button>
<button
@click="prepareMerge"
:disabled="!hasBranch || mergePending"
class="action-btn"
>
🔀 准备合并
</button>
<button @click="finishMerge" :disabled="!mergePending" class="action-btn">
完成合并
</button>
<button @click="reset" class="action-btn secondary">🔄 重置</button>
<div class="demo-header">
<span class="icon">📦</span>
<span class="title">Git 工作流</span>
<span class="subtitle">修改 暂存 提交三步走</span>
</div>
<!-- 提交历史可视化 -->
<div class="visualization">
<div class="graph-container">
<svg viewBox="0 0 400 150" class="git-graph">
<!-- 主分支线 -->
<line
x1="50"
y1="60"
x2="350"
y2="60"
stroke="#3b82f6"
stroke-width="3"
/>
<div class="demo-content">
<!-- 文件状态区域 -->
<div class="file-area">
<div class="area-header">
<span class="area-icon">📝</span>
<span class="area-name">工作区</span>
<span class="area-desc">你正在改的文件</span>
</div>
<div class="file-list">
<div
v-for="file in files"
:key="file.name"
class="file-item"
:class="{
'modified': file.status === 'modified',
'staged': file.status === 'staged',
'committed': file.status === 'committed'
}"
>
<span class="file-icon">{{ getIcon(file.status) }}</span>
<span class="file-name">{{ file.name }}</span>
<span class="file-status">{{ getStatusText(file.status) }}</span>
</div>
</div>
</div>
<!-- 分支线 -->
<line
v-if="hasBranch"
x1="150"
y1="60"
x2="150"
y2="100"
stroke="#10b981"
stroke-width="3"
/>
<line
v-if="hasBranch"
x1="150"
y1="100"
x2="300"
y2="100"
stroke="#10b981"
stroke-width="3"
/>
<!-- 箭头 -->
<div class="arrow-group" v-if="!allCommitted">
<div class="arrow" :class="{ active: hasStaged }"></div>
<div class="arrow-label" v-if="hasStaged">git add</div>
</div>
<!-- 合并线 -->
<path
v-if="mergePending"
d="M 300 100 Q 320 80, 320 60"
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="2"
stroke-dasharray="5,5"
/>
<!-- 暂存区 -->
<div class="stage-area">
<div class="area-header">
<span class="area-icon">📋</span>
<span class="area-name">暂存区</span>
<span class="area-desc">准备打包的文件</span>
</div>
<div class="file-list">
<div
v-for="file in stagedFiles"
:key="file.name"
class="file-item staged"
>
<span class="file-icon">📌</span>
<span class="file-name">{{ file.name }}</span>
<span class="file-status">待提交</span>
</div>
<div v-if="stagedFiles.length === 0" class="empty-tip">
暂无文件
</div>
</div>
</div>
<!-- 提交节点 -->
<circle
v-for="(commit, i) in mainCommits"
:key="'main-' + i"
:cx="80 + i * 60"
cy="60"
r="10"
fill="#3b82f6"
/>
<circle
v-for="(commit, i) in branchCommits"
:key="'branch-' + i"
:cx="200 + i * 60"
cy="100"
r="10"
fill="#10b981"
/>
</svg>
<!-- 箭头 -->
<div class="arrow-group" v-if="hasStaged">
<div class="arrow active"></div>
<div class="arrow-label">git commit</div>
</div>
<!-- 仓库区 -->
<div class="repo-area">
<div class="area-header">
<span class="area-icon">🏪</span>
<span class="area-name">仓库</span>
<span class="area-desc">已保存的版本</span>
</div>
<div class="commit-list">
<div
v-for="(commit, i) in commits"
:key="i"
class="commit-item"
>
<span class="commit-icon"></span>
<span class="commit-msg">{{ commit.msg }}</span>
</div>
<div v-if="commits.length === 0" class="empty-tip">
暂无提交
</div>
</div>
</div>
</div>
<!-- 状态信息 -->
<div class="status-panel">
<div class="status-item">
<span class="label">提交数:</span>
<span class="value">{{ mainCommits.length }}</span>
</div>
<div class="status-item">
<span class="label">分支:</span>
<span class="value">{{ hasBranch ? '2' : '1' }}</span>
</div>
<div class="status-item">
<span class="label">状态:</span>
<span class="value">{{ status }}</span>
</div>
<!-- 操作按钮 -->
<div class="action-panel">
<button
@click="modifyFile"
class="action-btn"
:disabled="allModified"
>
修改文件
</button>
<button
@click="stageFiles"
class="action-btn"
:disabled="!hasModified || allStaged"
>
📌 暂存修改
</button>
<button
@click="commitFiles"
class="action-btn"
:disabled="!hasStaged"
>
提交版本
</button>
<button
@click="reset"
class="action-btn secondary"
>
🔄 重置
</button>
</div>
<!-- 说明 -->
<div class="info-box">
<p>
<strong>💡 工作流程:</strong> 初始化 提交 创建分支 开发 合并
</p>
<span class="icon">💡</span>
<strong>核心思想</strong>工作区修改 暂存区挑选 仓库永久保存
</div>
</div>
</template>
@@ -136,56 +138,92 @@
<script setup>
import { ref, computed } from 'vue'
const inited = ref(false)
const hasBranch = ref(false)
const mergePending = ref(false)
const mainCommits = ref([])
const branchCommits = ref([])
const files = ref([
{ name: 'index.html', status: 'unmodified' },
{ name: 'app.js', status: 'unmodified' },
{ name: 'style.css', status: 'unmodified' }
])
const status = computed(() => {
if (mergePending.value) return '准备合并:检查改动/解决冲突后再完成合并'
if (hasBranch) return '分支已创建'
if (inited) return '已初始化'
return '未初始化'
})
const commits = ref([])
const initRepo = () => {
inited.value = true
mainCommits.value = [{ hash: 'abc123' }]
}
const hasModified = computed(() =>
files.value.some(f => f.status === 'modified')
)
const makeCommit = () => {
if (inited.value) {
mainCommits.value.push({ hash: Math.random().toString(16).substr(2, 6) })
const hasStaged = computed(() =>
files.value.some(f => f.status === 'staged')
)
const allCommitted = computed(() =>
files.value.every(f => f.status === 'committed')
)
const allModified = computed(() =>
files.value.every(f => f.status === 'modified')
)
const allStaged = computed(() =>
files.value.every(f => f.status === 'staged' || f.status === 'committed')
)
const stagedFiles = computed(() =>
files.value.filter(f => f.status === 'staged')
)
const getIcon = (status) => {
switch (status) {
case 'modified': return '📝'
case 'staged': return '📌'
case 'committed': return '✅'
default: return '📄'
}
}
const createBranch = () => {
if (inited.value && !hasBranch.value) {
hasBranch.value = true
branchCommits.value = [{ hash: 'def456' }]
const getStatusText = (status) => {
switch (status) {
case 'modified': return '已修改'
case 'staged': return '已暂存'
case 'committed': return '已提交'
default: return '未修改'
}
}
const prepareMerge = () => {
if (!hasBranch.value) return
mergePending.value = true
const modifyFile = () => {
const unmodified = files.value.filter(f => f.status === 'unmodified' || f.status === 'committed')
if (unmodified.length > 0) {
const file = unmodified[0]
file.status = 'modified'
}
}
const finishMerge = () => {
if (!mergePending.value) return
mainCommits.value.push({ hash: Math.random().toString(16).substr(2, 6) })
hasBranch.value = false
branchCommits.value = []
mergePending.value = false
const stageFiles = () => {
files.value.forEach(f => {
if (f.status === 'modified') {
f.status = 'staged'
}
})
}
const commitFiles = () => {
const staged = files.value.filter(f => f.status === 'staged')
if (staged.length > 0) {
files.value.forEach(f => {
if (f.status === 'staged') {
f.status = 'committed'
}
})
commits.value.push({
msg: `提交了 ${staged.length} 个文件`,
files: staged.map(f => f.name)
})
}
}
const reset = () => {
inited.value = false
hasBranch.value = false
mergePending.value = false
mainCommits.value = []
branchCommits.value = []
files.value.forEach(f => {
f.status = 'unmodified'
})
commits.value = []
}
</script>
@@ -193,94 +231,230 @@ const reset = () => {
.git-workflow-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 550px;
overflow-y: auto;
}
.control-panel {
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.demo-header .icon {
font-size: 1.25rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
margin-bottom: 1rem;
}
.area-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.area-icon {
font-size: 1.1rem;
}
.area-name {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.area-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-left: 0.5rem;
}
.file-area,
.stage-area,
.repo-area {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
}
.file-list,
.commit-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-height: 60px;
}
.file-item,
.commit-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.85rem;
}
.file-item.modified {
background: var(--vp-c-bg-alt);
border-left: 3px solid var(--vp-c-warning);
}
.file-item.staged {
background: var(--vp-c-brand-soft);
border-left: 3px solid var(--vp-c-brand);
}
.file-item.committed {
background: var(--vp-c-bg-alt);
border-left: 3px solid var(--vp-c-success);
opacity: 0.7;
}
.file-icon {
font-size: 1rem;
}
.file-name {
flex: 1;
color: var(--vp-c-text-1);
}
.file-status {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.commit-item {
font-size: 0.8rem;
}
.commit-icon {
font-size: 0.9rem;
}
.commit-msg {
flex: 1;
color: var(--vp-c-text-2);
}
.empty-tip {
text-align: center;
color: var(--vp-c-text-3);
font-size: 0.8rem;
padding: 0.5rem;
}
.arrow-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0;
}
.arrow {
font-size: 1.5rem;
color: var(--vp-c-text-3);
transition: all 0.3s;
}
.arrow.active {
color: var(--vp-c-brand);
transform: scale(1.2);
}
.arrow-label {
font-size: 0.7rem;
color: var(--vp-c-brand);
font-family: monospace;
}
.action-panel {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 0.75rem;
}
.action-btn {
padding: 0.625rem 1.25rem;
border: 2px solid var(--vp-c-brand);
background: var(--vp-c-bg);
color: var(--vp-c-brand);
padding: 0.5rem 1rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-size: 0.875rem;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
flex: 1;
min-width: 100px;
}
.action-btn:hover:not(:disabled) {
background: var(--vp-c-brand);
color: var(--vp-c-bg);
opacity: 0.9;
transform: translateY(-1px);
}
.action-btn:disabled {
opacity: 0.5;
opacity: 0.4;
cursor: not-allowed;
border-color: var(--vp-c-divider);
color: var(--vp-c-text-2);
background: var(--vp-c-divider);
}
.action-btn.secondary {
border-color: var(--vp-c-divider);
}
.visualization {
margin: 1.5rem 0;
}
.graph-container {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
background: transparent;
border: 1px solid var(--vp-c-divider);
}
.git-graph {
width: 100%;
height: auto;
}
.status-panel {
display: flex;
gap: 2rem;
margin: 1.5rem 0;
flex-wrap: wrap;
}
.status-item {
display: flex;
gap: 0.5rem;
}
.status-item .label {
color: var(--vp-c-text-2);
}
.status-item .value {
font-weight: 600;
.action-btn.secondary:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.info-box {
padding: 1rem;
background: var(--vp-c-bg);
border-left: 4px solid var(--vp-c-brand);
border-radius: 4px;
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 p {
margin: 0;
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
line-height: 1.6;
}
</style>
@@ -4,9 +4,10 @@
-->
<template>
<div class="coupling-demo">
<div class="header">
<div class="title">系统解耦从紧耦合到松耦合</div>
<div class="subtitle">观察同步调用与异步消息的区别</div>
<div class="demo-header">
<span class="icon">🔗</span>
<span class="title">系统解耦</span>
<span class="subtitle">从紧耦合到松耦合</span>
</div>
<div class="mode-switch">
@@ -26,7 +27,7 @@
</button>
</div>
<div class="demo-container">
<div class="demo-content">
<!-- 紧耦合模式 -->
<div v-if="!useAsync" class="synchronous-mode">
<div class="scenario">
@@ -65,7 +66,7 @@
<div class="problem-list">
<div class="problem-item">
<span class="icon"></span>
<span><strong>依赖性强</strong>通知服务宕机订单创建失败</span>
<span><strong>依赖性强</strong>通知服务宕机,订单创建失败</span>
</div>
<div class="problem-item">
<span class="icon"></span>
@@ -111,7 +112,7 @@
<div class="consumer-box" :class="{ failed: consumerFailed }">
<div class="consumer-name">短信服务</div>
<div class="consumer-status">
{{ consumerFailed ? '离线不影响订单' : '运行中' }}
{{ consumerFailed ? '离线(不影响订单)' : '运行中' }}
</div>
</div>
<div class="consumer-box">
@@ -122,10 +123,6 @@
<div class="consumer-name">积分服务</div>
<div class="consumer-status">运行中</div>
</div>
<div class="consumer-box new">
<div class="consumer-name">数据分析</div>
<div class="consumer-status">新增 </div>
</div>
</div>
</div>
@@ -137,7 +134,7 @@
<div class="benefit-item">
<span class="icon"></span>
<span
><strong>响应快</strong>订单服务只耗时 50ms发送消息</span
><strong>响应快</strong>订单服务只耗时 50ms(发送消息)</span
>
</div>
<div class="benefit-item">
@@ -153,41 +150,9 @@
</div>
</div>
<div class="comparison-summary">
<div class="summary-title">📊 对比总结</div>
<div class="summary-table">
<table>
<thead>
<tr>
<th>维度</th>
<th>紧耦合 (同步)</th>
<th>松耦合 (异步)</th>
</tr>
</thead>
<tbody>
<tr>
<td>服务依赖</td>
<td class="bad">强依赖一个挂全挂</td>
<td class="good">弱依赖独立运行</td>
</tr>
<tr>
<td>响应时间</td>
<td class="bad">1200ms串行执行</td>
<td class="good">50ms只发消息</td>
</tr>
<tr>
<td>扩展性</td>
<td class="bad">修改订单服务代码</td>
<td class="good">增加新消费者即可</td>
</tr>
<tr>
<td>可用性</td>
<td class="bad">90%任一服务故障</td>
<td class="good">99.9%独立故障域</td>
</tr>
</tbody>
</table>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想:</strong>同步调用强依赖响应慢;异步消息解耦响应快易扩展
</div>
</div>
</template>
@@ -203,7 +168,7 @@ const messageInQueue = ref(false)
const syncCalls = ref([
{ id: 1, service: '调用库存服务', active: false, status: '处理中...' },
{ id: 2, service: '调用积分服务', active: false, status: '处理中...' },
{ id: 3, service: '调用通知服务', active: false, status: '失败订单回滚' }
{ id: 3, service: '调用通知服务', active: false, status: '失败!订单回滚' }
])
const testSyncCall = () => {
@@ -232,42 +197,52 @@ const testAsyncCall = () => {
<style scoped>
.coupling-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.header {
margin-bottom: 1rem;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
.demo-header .icon {
font-size: 1.25rem;
}
.subtitle {
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
margin-top: 0.25rem;
font-size: 0.85rem;
margin-left: 0.5rem;
}
.mode-switch {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
margin-bottom: 1rem;
}
.mode-btn {
flex: 1;
padding: 0.75rem 1rem;
padding: 0.5rem 0.75rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
transition: all 0.2s;
}
@@ -277,18 +252,18 @@ const testAsyncCall = () => {
.mode-btn.active {
background: var(--vp-c-brand);
color: #fff;
color: white;
border-color: var(--vp-c-brand);
}
.demo-container {
margin-bottom: 1.5rem;
.demo-content {
margin-bottom: 0.75rem;
}
.scenario-title {
font-weight: 600;
font-size: 1rem;
margin-bottom: 1rem;
font-size: 0.9rem;
margin-bottom: 0.75rem;
text-align: center;
}
@@ -296,55 +271,55 @@ const testAsyncCall = () => {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 1.5rem;
gap: 0.75rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 10px;
margin-bottom: 1rem;
border-radius: 8px;
margin-bottom: 0.75rem;
}
.service-box {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 10px;
padding: 1rem;
border-radius: 8px;
padding: 0.75rem;
text-align: center;
min-width: 180px;
min-width: 140px;
transition: all 0.3s;
}
.service-box.failed {
border-color: #ef4444;
background: rgba(239, 68, 68, 0.1);
border-color: var(--vp-c-danger);
background: var(--vp-c-danger-soft);
}
.service-name {
font-weight: 600;
font-size: 0.95rem;
font-size: 0.85rem;
margin-bottom: 0.25rem;
}
.service-desc {
font-size: 0.8rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.error-msg {
margin-top: 0.5rem;
padding: 0.35rem 0.5rem;
background: #ef4444;
background: var(--vp-c-danger);
color: white;
border-radius: 4px;
font-size: 0.8rem;
font-size: 0.75rem;
font-weight: 600;
}
.arrows {
display: flex;
flex-direction: column;
gap: 0.75rem;
gap: 0.5rem;
width: 100%;
max-width: 300px;
max-width: 250px;
}
.sync-call {
@@ -357,7 +332,7 @@ const testAsyncCall = () => {
}
.sync-call.active {
background: rgba(239, 68, 68, 0.1);
background: var(--vp-c-danger-soft);
}
.call-line {
@@ -367,96 +342,91 @@ const testAsyncCall = () => {
}
.sync-call.active .call-line {
background: #ef4444;
background: var(--vp-c-danger);
}
.call-label {
font-size: 0.8rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
flex: 1;
}
.call-status {
font-size: 0.75rem;
color: #ef4444;
font-size: 0.7rem;
color: var(--vp-c-danger);
font-weight: 600;
}
.mq-bridge {
display: flex;
align-items: center;
gap: 1rem;
gap: 0.75rem;
}
.mq-box {
background: rgba(59, 130, 246, 0.1);
background: var(--vp-c-brand-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 10px;
padding: 1rem;
border-radius: 8px;
padding: 0.75rem;
text-align: center;
min-width: 140px;
min-width: 120px;
}
.mq-icon {
font-size: 2rem;
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.mq-label {
font-weight: 600;
font-size: 0.9rem;
font-size: 0.85rem;
}
.msg-indicator {
margin-top: 0.5rem;
padding: 0.35rem 0.5rem;
background: #dcfce7;
color: #166534;
background: var(--vp-c-success);
color: white;
border-radius: 4px;
font-size: 0.75rem;
font-size: 0.7rem;
font-weight: 600;
}
.flow-arrow {
font-size: 1.5rem;
font-size: 1.25rem;
color: var(--vp-c-brand);
}
.consumers-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 0.75rem;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 0.5rem;
width: 100%;
max-width: 500px;
max-width: 400px;
}
.consumer-box {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 8px;
padding: 0.75rem;
border-radius: 6px;
padding: 0.5rem;
text-align: center;
transition: all 0.3s;
}
.consumer-box.failed {
border-color: #f59e0b;
background: rgba(245, 158, 11, 0.1);
}
.consumer-box.new {
border-color: #22c55e;
background: rgba(34, 197, 94, 0.1);
border-color: var(--vp-c-warning);
background: var(--vp-c-warning-soft);
}
.consumer-name {
font-size: 0.8rem;
font-size: 0.75rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.consumer-status {
font-size: 0.7rem;
font-size: 0.65rem;
color: var(--vp-c-text-2);
}
@@ -464,8 +434,8 @@ const testAsyncCall = () => {
.benefit-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1rem;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.problem-item,
@@ -473,92 +443,66 @@ const testAsyncCall = () => {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.75rem;
border-radius: 8px;
font-size: 0.9rem;
line-height: 1.5;
padding: 0.5rem;
border-radius: 6px;
font-size: 0.8rem;
line-height: 1.4;
}
.problem-item {
background: rgba(239, 68, 68, 0.1);
background: var(--vp-c-danger-soft);
}
.benefit-item {
background: rgba(34, 197, 94, 0.1);
background: var(--vp-c-success-soft);
}
.icon {
font-size: 1.2rem;
font-size: 1rem;
flex-shrink: 0;
}
.test-btn {
width: 100%;
padding: 0.75rem;
padding: 0.5rem;
border: none;
border-radius: 8px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
transition: all 0.2s;
}
.test-btn.fail {
background: #ef4444;
background: var(--vp-c-danger);
color: white;
}
.test-btn.fail:hover {
background: #dc2626;
opacity: 0.9;
}
.test-btn.success {
background: #22c55e;
background: var(--vp-c-success);
color: white;
}
.test-btn.success:hover {
background: #16a34a;
opacity: 0.9;
}
.comparison-summary {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.summary-title {
font-weight: 600;
margin-bottom: 0.75rem;
}
.summary-table {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th,
td {
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
gap: 0.25rem;
}
th {
background: var(--vp-c-bg-soft);
font-weight: 600;
}
.bad {
color: #ef4444;
}
.good {
color: #16a34a;
.info-box .icon {
flex-shrink: 0;
}
</style>
@@ -4,133 +4,132 @@
-->
<template>
<div class="dlq-demo">
<div class="header">
<div class="title">死信队列消息的"急救站"</div>
<div class="subtitle">处理无法消费的消息避免阻塞队列</div>
<div class="demo-header">
<span class="icon">🚑</span>
<span class="title">死信队列</span>
<span class="subtitle">消息的"急救站" - 处理失败消息</span>
</div>
<div class="controls">
<div class="control">
<label>失败率</label>
<input v-model="failureRate" type="range" min="0" max="100" step="10" />
<input v-model.number="failureRate" type="range" min="0" max="100" step="10" />
<span class="value">{{ failureRate }}%</span>
</div>
<div class="control">
<label>最大重试次数</label>
<input v-model="maxRetries" type="range" min="1" max="5" step="1" />
<label>最大重试</label>
<input v-model.number="maxRetries" type="range" min="1" max="5" step="1" />
<span class="value">{{ maxRetries }}</span>
</div>
</div>
<div class="flow-container">
<div class="main-queue-section">
<div class="section-title">📦 主队列</div>
<div class="queue-box main-queue">
<div class="queue-header">
<span>正常消息队列</span>
<span class="count">{{ mainQueue.length }} </span>
</div>
<div class="message-list">
<div
v-for="msg in mainQueue"
:key="msg.id"
class="message-item"
:class="{ processing: msg.processing }"
>
<div class="msg-id">#{{ msg.id }}</div>
<div class="msg-retries" v-if="msg.retries > 0">
重试: {{ msg.retries }}/{{ maxRetries }}
<div class="demo-content">
<div class="flow-container">
<div class="main-queue-section">
<div class="section-title">📦 主队列</div>
<div class="queue-box main-queue">
<div class="queue-header">
<span>正常消息队列</span>
<span class="count">{{ mainQueue.length }} </span>
</div>
<div class="message-list">
<div
v-for="msg in mainQueue.slice(0, 3)"
:key="msg.id"
class="message-item"
:class="{ processing: msg.processing }"
>
<div class="msg-id">#{{ msg.id }}</div>
<div class="msg-retries" v-if="msg.retries > 0">
重试: {{ msg.retries }}/{{ maxRetries }}
</div>
</div>
<div v-if="mainQueue.length === 0" class="empty">队列为空</div>
<div v-else-if="mainQueue.length > 3" class="more">
还有 {{ mainQueue.length - 3 }} 条...
</div>
</div>
<div v-if="mainQueue.length === 0" class="empty">队列为空</div>
</div>
<button class="add-btn" @click="addMessage" :disabled="processing">
+ 添加消息
</button>
</div>
<button class="add-btn" @click="addMessage" :disabled="processing">
+ 添加消息
</button>
</div>
<div class="processing-section">
<div class="section-title"> 消费处理</div>
<div class="processor-box">
<div class="processor-icon" :class="{ active: processing }">
{{ processing ? '⚙️' : '💤' }}
</div>
<div class="processor-status">
{{ processing ? '处理中...' : '空闲' }}
</div>
<div v-if="currentMessage" class="current-msg">
处理: #{{ currentMessage.id }}
</div>
<div v-if="lastResult" class="last-result" :class="lastResult.type">
{{ lastResult.message }}
</div>
</div>
</div>
<div class="dlq-section">
<div class="section-title"> 死信队列</div>
<div class="queue-box dead-letter">
<div class="queue-header">
<span>失败消息</span>
<span class="count">{{ deadLetterQueue.length }} </span>
</div>
<div class="message-list">
<div
v-for="msg in deadLetterQueue"
:key="msg.id"
class="message-item failed"
>
<div class="msg-id">#{{ msg.id }}</div>
<div class="msg-error">{{ msg.error }}</div>
<div class="processing-section">
<div class="section-title"> 消费处理</div>
<div class="processor-box">
<div class="processor-icon" :class="{ active: processing }">
{{ processing ? '⚙️' : '💤' }}
</div>
<div v-if="deadLetterQueue.length === 0" class="empty">
无失败消息
<div class="processor-status">
{{ processing ? '处理中...' : '空闲' }}
</div>
<div v-if="currentMessage" class="current-msg">
处理: #{{ currentMessage.id }}
</div>
<div v-if="lastResult" class="last-result" :class="lastResult.type">
{{ lastResult.message }}
</div>
</div>
</div>
<button
class="retry-btn"
@click="retryDeadLetters"
:disabled="deadLetterQueue.length === 0"
>
🔄 重试死信
</button>
<div class="dlq-section">
<div class="section-title"> 死信队列</div>
<div class="queue-box dead-letter">
<div class="queue-header">
<span>失败消息</span>
<span class="count">{{ deadLetterQueue.length }} </span>
</div>
<div class="message-list">
<div
v-for="msg in deadLetterQueue.slice(0, 2)"
:key="msg.id"
class="message-item failed"
>
<div class="msg-id">#{{ msg.id }}</div>
<div class="msg-error">{{ msg.error }}</div>
</div>
<div v-if="deadLetterQueue.length === 0" class="empty">
无失败消息
</div>
<div v-else-if="deadLetterQueue.length > 2" class="more">
还有 {{ deadLetterQueue.length - 2 }} 条...
</div>
</div>
</div>
<button
class="retry-btn"
@click="retryDeadLetters"
:disabled="deadLetterQueue.length === 0"
>
🔄 重试死信
</button>
</div>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-label">总消息数</div>
<div class="stat-value">{{ totalMessages }}</div>
</div>
<div class="stat-card success">
<div class="stat-label">成功处理</div>
<div class="stat-value">{{ successCount }}</div>
</div>
<div class="stat-card warning">
<div class="stat-label">进入死信</div>
<div class="stat-value">{{ deadLetterCount }}</div>
</div>
<div class="stat-card">
<div class="stat-label">成功率</div>
<div class="stat-value">{{ successRate }}%</div>
</div>
</div>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-label">总消息数</div>
<div class="stat-value">{{ totalMessages }}</div>
</div>
<div class="stat-card success">
<div class="stat-label">成功处理</div>
<div class="stat-value">{{ successCount }}</div>
</div>
<div class="stat-card warning">
<div class="stat-label">进入死信</div>
<div class="stat-value">{{ deadLetterCount }}</div>
</div>
<div class="stat-card">
<div class="stat-label">成功率</div>
<div class="stat-value">{{ successRate }}%</div>
</div>
</div>
<div class="explanation">
<div class="exp-title">💡 死信队列的作用</div>
<div class="exp-content">
<div class="exp-item">
<strong>1. 隔离异常消息</strong>失败消息不会阻塞正常消息的处理
</div>
<div class="exp-item">
<strong>2. 保留失败记录</strong>可以后续人工介入或自动重试
</div>
<div class="exp-item">
<strong>3. 系统保护</strong>避免因持续失败导致消费者崩溃
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想:</strong>失败消息进入死信队列,避免阻塞正常消息,可后续人工介入或自动重试
</div>
</div>
</template>
@@ -174,7 +173,7 @@ const processNext = () => {
return
}
let msg = mainQueue[0]
let msg = mainQueue.value[0]
msg.processing = true
processing.value = true
currentMessage.value = msg
@@ -188,7 +187,7 @@ const processNext = () => {
msg.processing = false
if (msg.retries >= maxRetries.value) {
// 超过最大重试次数进入死信队列
// 超过最大重试次数,进入死信队列
mainQueue.value.shift()
deadLetterQueue.value.push({
id: msg.id,
@@ -202,7 +201,7 @@ const processNext = () => {
// 重新入队
lastResult.value = {
type: 'warning',
message: `⚠️ 消息 #${msg.id} 处理失败重试 ${msg.retries}/${maxRetries.value}`
message: `⚠️ 消息 #${msg.id} 处理失败,重试 ${msg.retries}/${maxRetries.value}`
}
}
@@ -262,39 +261,48 @@ addMessage = addMessageWithAutoProcess
.dlq-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.header {
margin-bottom: 1rem;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
.demo-header .icon {
font-size: 1.25rem;
}
.subtitle {
.demo-header .title {
font-weight: bold;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
margin-top: 0.25rem;
font-size: 0.85rem;
margin-left: 0.5rem;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
margin-bottom: 1rem;
}
.control {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
font-size: 0.85rem;
}
.control input[type='range'] {
@@ -307,25 +315,29 @@ addMessage = addMessageWithAutoProcess
text-align: right;
}
.demo-content {
margin-bottom: 0.75rem;
}
.flow-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
margin-bottom: 1rem;
}
.section-title {
font-size: 0.85rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
text-align: center;
margin-bottom: 0.75rem;
margin-bottom: 0.5rem;
}
.queue-box {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
border-radius: 8px;
overflow: hidden;
}
@@ -334,7 +346,7 @@ addMessage = addMessageWithAutoProcess
}
.queue-box.dead-letter {
border-color: #ef4444;
border-color: var(--vp-c-danger);
}
.queue-header {
@@ -343,12 +355,12 @@ addMessage = addMessageWithAutoProcess
align-items: center;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg-soft);
font-size: 0.8rem;
font-size: 0.75rem;
font-weight: 600;
}
.message-list {
max-height: 200px;
max-height: 150px;
overflow-y: auto;
padding: 0.5rem;
}
@@ -361,17 +373,17 @@ addMessage = addMessageWithAutoProcess
background: var(--vp-c-bg-soft);
border-radius: 6px;
margin-bottom: 0.4rem;
font-size: 0.8rem;
font-size: 0.75rem;
}
.message-item.processing {
border: 1px solid #f59e0b;
background: rgba(245, 158, 11, 0.1);
border: 1px solid var(--vp-c-warning);
background: var(--vp-c-warning-soft);
}
.message-item.failed {
border: 1px solid #ef4444;
background: rgba(239, 68, 68, 0.1);
border: 1px solid var(--vp-c-danger);
background: var(--vp-c-danger-soft);
}
.msg-id {
@@ -379,32 +391,32 @@ addMessage = addMessageWithAutoProcess
}
.msg-retries {
font-size: 0.7rem;
color: #f59e0b;
font-size: 0.65rem;
color: var(--vp-c-warning);
}
.msg-error {
font-size: 0.7rem;
color: #ef4444;
font-size: 0.65rem;
color: var(--vp-c-danger);
}
.empty {
.empty, .more {
text-align: center;
padding: 1.5rem;
padding: 1rem 0.5rem;
color: var(--vp-c-text-3);
font-size: 0.85rem;
font-size: 0.75rem;
}
.add-btn,
.retry-btn {
width: 100%;
padding: 0.6rem;
padding: 0.5rem;
border: none;
border-radius: 8px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
margin-top: 0.75rem;
font-size: 0.8rem;
margin-top: 0.5rem;
transition: all 0.2s;
}
@@ -414,8 +426,7 @@ addMessage = addMessageWithAutoProcess
}
.add-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
opacity: 0.9;
}
.add-btn:disabled {
@@ -424,21 +435,21 @@ addMessage = addMessageWithAutoProcess
}
.retry-btn {
background: #f59e0b;
background: var(--vp-c-warning);
color: white;
}
.retry-btn:hover:not(:disabled) {
background: #d97706;
opacity: 0.8;
}
.processor-box {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1.5rem;
border-radius: 8px;
padding: 1rem;
text-align: center;
min-height: 200px;
min-height: 150px;
display: flex;
flex-direction: column;
justify-content: center;
@@ -446,8 +457,8 @@ addMessage = addMessageWithAutoProcess
}
.processor-icon {
font-size: 2.5rem;
margin-bottom: 0.75rem;
font-size: 2rem;
margin-bottom: 0.5rem;
}
.processor-icon.active {
@@ -455,97 +466,87 @@ addMessage = addMessageWithAutoProcess
}
.processor-status {
font-size: 0.9rem;
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.current-msg {
font-size: 0.85rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.last-result {
font-size: 0.8rem;
font-size: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 6px;
margin-top: 0.5rem;
}
.last-result.success {
background: #dcfce7;
color: #166534;
background: var(--vp-c-success);
color: white;
}
.last-result.warning {
background: rgba(245, 158, 11, 0.1);
color: #d97706;
background: var(--vp-c-warning-soft);
color: var(--vp-c-warning-dark);
}
.last-result.error {
background: rgba(239, 68, 68, 0.1);
color: #dc2626;
background: var(--vp-c-danger-soft);
color: var(--vp-c-danger-dark);
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.stat-card {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border-radius: 8px;
padding: 0.75rem;
text-align: center;
border: 1px solid var(--vp-c-divider);
}
.stat-card.success {
border-color: #22c55e;
background: rgba(34, 197, 94, 0.05);
border-color: var(--vp-c-success);
background: var(--vp-c-success-soft);
}
.stat-card.warning {
border-color: #ef4444;
background: rgba(239, 68, 68, 0.05);
border-color: var(--vp-c-danger);
background: var(--vp-c-danger-soft);
}
.stat-label {
font-size: 0.8rem;
font-size: 0.7rem;
color: var(--vp-c-text-2);
margin-bottom: 0.35rem;
}
.stat-value {
font-size: 1.3rem;
font-size: 1.1rem;
font-weight: 700;
}
.explanation {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.exp-title {
font-weight: 600;
margin-bottom: 0.75rem;
}
.exp-content {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.exp-item {
font-size: 0.9rem;
line-height: 1.5;
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
@keyframes spin {
@@ -0,0 +1,522 @@
<!--
DecouplingDemo.vue
系统解耦演示 - 同步 vs 异步对比
-->
<template>
<div class="decoupling-demo">
<div class="demo-header">
<span class="icon">🔗</span>
<span class="title">系统解耦演示</span>
<span class="subtitle">从紧耦合到松耦合的演进</span>
</div>
<div class="mode-switch">
<button
class="mode-btn"
:class="{ active: !useAsync }"
@click="useAsync = false"
>
🔗 紧耦合 (同步)
</button>
<button
class="mode-btn"
:class="{ active: useAsync }"
@click="useAsync = true"
>
🔓 松耦合 (异步)
</button>
</div>
<div class="demo-content">
<!-- 紧耦合模式 -->
<div v-if="!useAsync" class="synchronous-mode">
<div class="scenario">
<div class="scenario-title"> 紧耦合的致命问题</div>
<div class="flow-diagram">
<div class="service-box order">
<div class="service-name">订单服务</div>
<div class="service-desc">创建订单</div>
</div>
<div class="arrows">
<div
v-for="call in syncCalls"
:key="call.id"
class="sync-call"
:class="{ active: call.active }"
>
<div class="call-line"></div>
<div class="call-label">{{ call.service }}</div>
<div v-if="call.active" class="call-status">
{{ call.status }}
</div>
</div>
</div>
<div
class="service-box notification"
:class="{ failed: notificationFailed }"
>
<div class="service-name">通知服务</div>
<div class="service-desc">发送短信/邮件</div>
<div v-if="notificationFailed" class="error-msg">
服务宕机
</div>
</div>
</div>
<div class="problem-list">
<div class="problem-item">
<span class="icon"></span>
<span
><strong>依赖性强</strong>通知服务宕机,订单创建失败</span
>
</div>
<div class="problem-item">
<span class="icon"></span>
<span
><strong>响应慢</strong>总耗时 = 300ms + 500ms + 400ms =
1200ms</span
>
</div>
<div class="problem-item">
<span class="icon"></span>
<span
><strong>扩展难</strong>增加新服务需要修改订单代码</span
>
</div>
</div>
<button class="test-btn fail" @click="testSyncCall">
模拟通知服务故障
</button>
</div>
</div>
<!-- 松耦合模式 -->
<div v-else class="asynchronous-mode">
<div class="scenario">
<div class="scenario-title"> 松耦合的核心优势</div>
<div class="flow-diagram">
<div class="service-box order">
<div class="service-name">订单服务</div>
<div class="service-desc">创建订单 + 发送消息</div>
</div>
<div class="mq-bridge">
<div class="mq-box">
<div class="mq-icon">📨</div>
<div class="mq-label">消息队列</div>
<div v-if="messageInQueue" class="msg-indicator">
消息已发送
</div>
</div>
<div class="flow-arrow"></div>
</div>
<div class="consumers-group">
<div class="consumer-box" :class="{ failed: consumerFailed }">
<div class="consumer-name">短信服务</div>
<div class="consumer-status">
{{ consumerFailed ? '离线(不影响订单)' : '运行中' }}
</div>
</div>
<div class="consumer-box">
<div class="consumer-name">邮件服务</div>
<div class="consumer-status">运行中</div>
</div>
<div class="consumer-box">
<div class="consumer-name">积分服务</div>
<div class="consumer-status">运行中</div>
</div>
</div>
</div>
<div class="benefit-list">
<div class="benefit-item">
<span class="icon"></span>
<span
><strong>独立运行</strong>通知服务宕机不影响订单创建</span
>
</div>
<div class="benefit-item">
<span class="icon"></span>
<span
><strong>响应快</strong>订单服务只耗时 50ms(发送消息)</span
>
</div>
<div class="benefit-item">
<span class="icon"></span>
<span
><strong>易扩展</strong>增加新消费者无需修改订单代码</span
>
</div>
</div>
<button class="test-btn success" @click="testAsyncCall">
发送订单消息
</button>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想:</strong>同步调用强依赖响应慢;异步消息解耦响应快易扩展
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const useAsync = ref(false)
const notificationFailed = ref(false)
const consumerFailed = ref(false)
const messageInQueue = ref(false)
const syncCalls = ref([
{ id: 1, service: '调用库存服务', active: false, status: '处理中...' },
{ id: 2, service: '调用积分服务', active: false, status: '处理中...' },
{
id: 3,
service: '调用通知服务',
active: false,
status: '失败!订单回滚'
}
])
const testSyncCall = () => {
notificationFailed.value = true
syncCalls.value.forEach((call, index) => {
setTimeout(() => {
call.active = true
if (index === syncCalls.value.length - 1) {
setTimeout(() => {
call.active = false
}, 2000)
}
}, index * 800)
})
}
const testAsyncCall = () => {
messageInQueue.value = true
setTimeout(() => {
messageInQueue.value = false
}, 2000)
}
</script>
<style scoped>
.decoupling-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
font-family: var(--vp-font-family-base);
}
.demo-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.demo-header .icon {
font-size: 24px;
}
.demo-header .title {
font-weight: 700;
font-size: 18px;
color: var(--vp-c-text-1);
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
margin-left: 8px;
}
.mode-switch {
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.mode-btn {
flex: 1;
padding: 12px 16px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
}
.mode-btn:hover {
border-color: var(--vp-c-brand);
}
.mode-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.demo-content {
margin-bottom: 16px;
}
.scenario-title {
font-weight: 600;
font-size: 16px;
margin-bottom: 16px;
text-align: center;
}
.flow-diagram {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 20px;
background: var(--vp-c-bg);
border-radius: 12px;
margin-bottom: 16px;
}
.service-box {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 12px;
padding: 16px;
text-align: center;
min-width: 160px;
transition: all 0.3s;
}
.service-box.failed {
border-color: var(--vp-c-danger);
background: rgba(239, 68, 68, 0.1);
}
.service-name {
font-weight: 600;
font-size: 15px;
margin-bottom: 6px;
}
.service-desc {
font-size: 13px;
color: var(--vp-c-text-2);
}
.error-msg {
margin-top: 10px;
padding: 8px 12px;
background: var(--vp-c-danger);
color: white;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
}
.arrows {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
max-width: 280px;
}
.sync-call {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-radius: 8px;
transition: all 0.3s;
}
.sync-call.active {
background: rgba(239, 68, 68, 0.1);
}
.call-line {
width: 2px;
height: 24px;
background: var(--vp-c-divider);
}
.sync-call.active .call-line {
background: var(--vp-c-danger);
}
.call-label {
font-size: 13px;
color: var(--vp-c-text-2);
flex: 1;
}
.call-status {
font-size: 12px;
color: var(--vp-c-danger);
font-weight: 600;
}
.mq-bridge {
display: flex;
align-items: center;
gap: 16px;
}
.mq-box {
background: rgba(59, 130, 246, 0.1);
border: 2px solid var(--vp-c-brand);
border-radius: 12px;
padding: 16px;
text-align: center;
min-width: 140px;
}
.mq-icon {
font-size: 32px;
margin-bottom: 8px;
}
.mq-label {
font-weight: 600;
font-size: 15px;
}
.msg-indicator {
margin-top: 10px;
padding: 8px 12px;
background: var(--vp-c-success);
color: white;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
}
.flow-arrow {
font-size: 24px;
color: var(--vp-c-brand);
}
.consumers-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
width: 100%;
max-width: 450px;
}
.consumer-box {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 8px;
padding: 12px;
text-align: center;
transition: all 0.3s;
}
.consumer-box.failed {
border-color: var(--vp-c-warning);
background: rgba(245, 158, 11, 0.1);
}
.consumer-name {
font-size: 13px;
font-weight: 600;
margin-bottom: 6px;
}
.consumer-status {
font-size: 12px;
color: var(--vp-c-text-2);
}
.problem-list,
.benefit-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 16px;
}
.problem-item,
.benefit-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px;
border-radius: 8px;
font-size: 14px;
line-height: 1.6;
}
.problem-item {
background: rgba(239, 68, 68, 0.1);
}
.benefit-item {
background: rgba(34, 197, 94, 0.1);
}
.icon {
font-size: 18px;
flex-shrink: 0;
}
.test-btn {
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
}
.test-btn.fail {
background: var(--vp-c-danger);
color: white;
}
.test-btn.fail:hover {
opacity: 0.9;
}
.test-btn.success {
background: var(--vp-c-success);
color: white;
}
.test-btn.success:hover {
opacity: 0.9;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 16px;
border-radius: 8px;
font-size: 14px;
color: var(--vp-c-text-2);
margin-top: 16px;
display: flex;
gap: 8px;
}
.info-box .icon {
flex-shrink: 0;
}
</style>
@@ -1,50 +1,881 @@
<!--
IdempotenceDemo.vue
幂等性演示 - 重复消费处理
-->
<template>
<div class="demo-container">
<div class="idempotence-demo">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<span class="icon">🔄</span>
<span class="title">幂等性演示</span>
<span class="subtitle">保证重复消费不会产生副作用</span>
</div>
<div class="scenario-switch">
<button
class="scenario-btn"
:class="{ active: scenario === 'transfer' }"
@click="scenario = 'transfer'"
>
💰 银行转账
</button>
<button
class="scenario-btn"
:class="{ active: scenario === 'elevator' }"
@click="scenario = 'elevator'"
>
🛗 电梯按钮
</button>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
幂等性演示组件占位符 - 待实现具体交互
</el-alert>
<!-- 银行转账场景 -->
<div v-if="scenario === 'transfer'" class="transfer-scenario">
<div class="scenario-header">
<div class="title"> 非幂等操作: 银行转账</div>
<div class="subtitle">重复消费会导致多次扣款</div>
</div>
<div class="account-system">
<div class="account-card sender">
<div class="account-name">发送方</div>
<div class="account-balance">
余额: ¥<span class="balance-amount">{{ senderBalance }}</span>
</div>
</div>
<div class="transfer-flow">
<div class="flow-animation" :class="{ active: isTransferring }">
<div class="money-icon">💰</div>
<div class="flow-label">转账 ¥100</div>
</div>
<div class="retry-info" v-if="retryCount > 0">
<div class="retry-badge">重试 {{ retryCount }} </div>
</div>
</div>
<div class="account-card receiver">
<div class="account-name">接收方</div>
<div class="account-balance">
余额: ¥<span class="balance-amount">{{ receiverBalance }}</span>
</div>
</div>
</div>
<div class="control-panel">
<div class="control-row">
<div class="control-item">
<label>幂等性保护</label>
<div class="toggle-switch">
<button
class="toggle-btn"
:class="{ active: useIdempotence }"
@click="useIdempotence = !useIdempotence"
>
<span class="toggle-slider"></span>
</button>
<span class="toggle-label">{{ useIdempotence ? '已启用' : '未启用' }}</span>
</div>
</div>
<button
class="action-btn"
@click="simulateTransfer"
:disabled="isTransferring"
>
{{ isTransferring ? '处理中...' : '模拟重复消费' }}
</button>
</div>
<div class="idempotence-info" v-if="useIdempotence">
<div class="info-item">
<span class="info-icon">🔑</span>
<span class="info-text">每笔交易有唯一ID,重复请求被自动过滤</span>
</div>
</div>
</div>
<div class="result-log">
<div class="log-header">处理日志</div>
<div class="log-list">
<div
v-for="(log, index) in logs"
:key="index"
class="log-item"
:class="log.type"
>
<span class="log-time">{{ log.time }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
<div v-if="logs.length === 0" class="log-empty">
暂无日志,点击按钮开始模拟
</div>
</div>
</div>
<div class="comparison-box">
<div class="comparison-item bad">
<div class="comp-header"> 无幂等保护</div>
<div class="comp-body">
<div class="comp-result">扣款 ¥{{ (retryCount + 1) * 100 }}</div>
<div class="comp-desc">重复消费造成多次扣款</div>
</div>
</div>
<div class="comparison-item good">
<div class="comp-header"> 有幂等保护</div>
<div class="comp-body">
<div class="comp-result">扣款 ¥100</div>
<div class="comp-desc">重复请求被过滤,只扣一次</div>
</div>
</div>
</div>
</div>
<!-- 电梯按钮场景 -->
<div v-else class="elevator-scenario">
<div class="scenario-header">
<div class="title"> 天然幂等操作: 电梯按钮</div>
<div class="subtitle">无论按多少次,电梯只响应一次</div>
</div>
<div class="elevator-system">
<div class="elevator-panel">
<div class="panel-title">电梯按钮面板</div>
<div class="button-grid">
<button
v-for="floor in floors"
:key="floor"
class="floor-btn"
:class="{ active: selectedFloor === floor }"
@click="pressFloor(floor)"
>
{{ floor }}F
</button>
</div>
<div class="press-count">
<span class="count-label">按钮按了</span>
<span class="count-value">{{ pressCount }}</span>
<span class="count-label"></span>
</div>
</div>
<div class="elevator-shaft">
<div class="floor-marks">
<div
v-for="floor in floors"
:key="floor"
class="floor-mark"
:class="{ current: elevatorFloor === floor }"
>
<span class="floor-num">{{ floor }}F</span>
</div>
</div>
<div class="elevator-car" :style="{ bottom: elevatorPosition }">
<div class="car-icon">🛗</div>
</div>
</div>
</div>
<div class="control-panel">
<div class="control-item">
<label>快速连按3次</label>
<button class="action-btn" @click="pressMultipleTimes">
🚀 连续点击
</button>
</div>
<div class="info-text">
<span class="info-icon">💡</span>
虽然按了{{ pressCount }},但电梯只响应一次请求
</div>
</div>
<div class="explanation-box">
<div class="explanation-title">为什么电梯按钮是幂等的?</div>
<div class="explanation-list">
<div class="explanation-item">
<span class="icon"></span>
<span>状态只切换一次: 停靠 已选中</span>
</div>
<div class="explanation-item">
<span class="icon"></span>
<span>重复请求不改变目标楼层</span>
</div>
<div class="explanation-item">
<span class="icon"></span>
<span>无需额外的幂等性保护机制</span>
</div>
</div>
</div>
</div>
</div>
<div class="principle-box">
<div class="principle-icon">🎯</div>
<div class="principle-content">
<strong>幂等性核心原则:</strong>
{{ scenario === 'transfer'
? '为每条消息生成唯一ID,处理前检查是否已处理,避免重复操作'
: '设计操作时确保重复执行和执行一次的效果相同' }}
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
const title = ref('幂等性演示')
const description = ref('展示消息消费中的幂等性问题,以及如何通过幂等性设计保证消息处理的正确性')
//
const scenario = ref('transfer')
//
const senderBalance = ref(1000)
const receiverBalance = ref(500)
const isTransferring = ref(false)
const useIdempotence = ref(false)
const retryCount = ref(0)
const logs = ref([])
const addLog = (message, type = 'info') => {
const now = new Date()
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
logs.value.unshift({ time, message, type })
}
const simulateTransfer = () => {
if (isTransferring.value) return
isTransferring.value = true
retryCount.value = 0
logs.value = []
const originalSenderBalance = senderBalance.value
const originalReceiverBalance = receiverBalance.value
addLog('收到转账请求: ¥100', 'info')
//
const processTransfer = (attempt) => {
return new Promise((resolve) => {
setTimeout(() => {
retryCount.value = attempt
if (useIdempotence.value) {
if (attempt === 0) {
senderBalance.value = originalSenderBalance - 100
receiverBalance.value = originalReceiverBalance + 100
addLog(`${attempt + 1}次处理: 成功转账 ¥100`, 'success')
addLog('幂等性检查: 唯一ID已记录,后续请求被过滤', 'info')
} else {
addLog(`${attempt + 1}次处理: 重复请求,已忽略`, 'warning')
}
} else {
senderBalance.value -= 100
receiverBalance.value += 100
addLog(`${attempt + 1}次处理: 转账 ¥100`, attempt === 0 ? 'success' : 'error')
}
if (attempt < 2) {
setTimeout(() => processTransfer(attempt + 1), 1000)
} else {
setTimeout(() => {
isTransferring.value = false
}, 500)
}
resolve()
}, 1000)
})
}
processTransfer(0)
}
//
const floors = [1, 2, 3, 4, 5]
const selectedFloor = ref(null)
const elevatorFloor = ref(1)
const pressCount = ref(0)
const elevatorPosition = computed(() => {
return ((elevatorFloor.value - 1) / 4) * 100 + '%'
})
const pressFloor = (floor) => {
pressCount.value++
selectedFloor.value = floor
setTimeout(() => {
elevatorFloor.value = floor
}, 500)
}
const pressMultipleTimes = () => {
const targetFloor = Math.floor(Math.random() * 5) + 1
let count = 0
const interval = setInterval(() => {
pressFloor(targetFloor)
count++
if (count >= 3) {
clearInterval(interval)
}
}, 200)
}
</script>
<style scoped>
.demo-container {
.idempotence-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
font-family: var(--vp-font-family-base);
}
.demo-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
.demo-header .icon {
font-size: 24px;
}
.demo-header .title {
font-weight: 700;
font-size: 18px;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
margin-left: 8px;
}
.scenario-switch {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.scenario-btn {
flex: 1;
padding: 12px 16px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
}
.scenario-btn:hover {
border-color: var(--vp-c-brand);
}
.scenario-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.scenario-header {
text-align: center;
margin-bottom: 20px;
}
.scenario-header .title {
font-weight: 700;
font-size: 18px;
color: var(--vp-c-text-1);
margin-bottom: 6px;
}
.scenario-header .subtitle {
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-content {
.account-system {
display: flex;
align-items: center;
justify-content: center;
gap: 40px;
margin-bottom: 20px;
padding: 24px;
background: var(--vp-c-bg);
border-radius: 12px;
}
.account-card {
flex: 1;
max-width: 200px;
padding: 20px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-brand);
border-radius: 12px;
text-align: center;
}
.account-name {
font-weight: 600;
font-size: 15px;
margin-bottom: 10px;
}
.account-balance {
font-size: 14px;
color: var(--vp-c-text-2);
}
.balance-amount {
font-weight: 700;
font-size: 18px;
color: var(--vp-c-brand);
}
.transfer-flow {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.flow-animation {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px;
border-radius: 8px;
transition: all 0.3s;
}
.flow-animation.active {
background: rgba(59, 130, 246, 0.1);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
.money-icon {
font-size: 32px;
}
.flow-label {
font-weight: 600;
font-size: 13px;
}
.retry-info {
display: flex;
justify-content: center;
}
.retry-badge {
padding: 4px 10px;
background: var(--vp-c-warning);
color: white;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
}
.control-panel {
background: var(--vp-c-bg);
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.control-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
.control-item {
display: flex;
align-items: center;
gap: 12px;
}
.control-item label {
font-weight: 600;
font-size: 14px;
}
.toggle-switch {
display: flex;
align-items: center;
gap: 8px;
}
.toggle-btn {
position: relative;
width: 48px;
height: 26px;
background: var(--vp-c-divider);
border: none;
border-radius: 13px;
cursor: pointer;
padding: 0;
transition: all 0.3s;
}
.toggle-btn.active {
background: var(--vp-c-brand);
}
.toggle-slider {
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transition: all 0.3s;
}
.toggle-btn.active .toggle-slider {
left: 25px;
}
.toggle-label {
font-size: 13px;
font-weight: 600;
}
.action-btn {
padding: 10px 20px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
transition: all 0.2s;
}
.action-btn:hover:not(:disabled) {
opacity: 0.9;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.idempotence-info {
margin-top: 12px;
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.2);
border-radius: 6px;
font-size: 13px;
}
.info-icon {
font-size: 16px;
}
.info-text {
flex: 1;
}
.result-log {
background: var(--vp-c-bg);
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.log-header {
font-weight: 600;
font-size: 14px;
margin-bottom: 10px;
}
.log-list {
max-height: 200px;
overflow-y: auto;
}
.log-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 8px;
border-radius: 4px;
font-size: 12px;
margin-bottom: 6px;
}
.log-item.success {
background: rgba(34, 197, 94, 0.1);
}
.log-item.error {
background: rgba(239, 68, 68, 0.1);
}
.log-item.warning {
background: rgba(245, 158, 11, 0.1);
}
.log-time {
color: var(--vp-c-text-3);
font-family: monospace;
}
.log-message {
flex: 1;
}
.log-empty {
text-align: center;
padding: 20px;
color: var(--vp-c-text-3);
font-size: 13px;
}
.comparison-box {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.comparison-item {
padding: 16px;
border-radius: 8px;
text-align: center;
}
.comparison-item.bad {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
}
.comparison-item.good {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.2);
}
.comp-header {
font-weight: 600;
font-size: 14px;
margin-bottom: 10px;
}
.comp-result {
font-weight: 700;
font-size: 18px;
margin-bottom: 6px;
}
.comp-desc {
font-size: 12px;
color: var(--vp-c-text-2);
}
.elevator-system {
display: flex;
gap: 24px;
margin-bottom: 20px;
padding: 24px;
background: var(--vp-c-bg);
border-radius: 12px;
}
.elevator-panel {
flex: 1;
max-width: 250px;
}
.panel-title {
font-weight: 600;
font-size: 14px;
text-align: center;
margin-bottom: 16px;
}
.button-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 16px;
}
.floor-btn {
padding: 16px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
}
.floor-btn:hover {
border-color: var(--vp-c-brand);
}
.floor-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.press-count {
text-align: center;
font-size: 13px;
color: var(--vp-c-text-2);
}
.count-label {
font-size: 12px;
}
.count-value {
font-weight: 700;
font-size: 18px;
color: var(--vp-c-brand);
}
.elevator-shaft {
flex: 1;
position: relative;
height: 300px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 20px;
}
.floor-marks {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
.floor-mark {
display: flex;
align-items: center;
gap: 8px;
}
.floor-num {
font-weight: 600;
font-size: 12px;
color: var(--vp-c-text-2);
}
.floor-mark.current .floor-num {
color: var(--vp-c-brand);
font-weight: 700;
}
.elevator-car {
position: absolute;
left: 50%;
transform: translateX(-50%);
transition: bottom 0.5s ease;
}
.car-icon {
font-size: 32px;
animation: bounce 0.5s ease;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
.info-text {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
padding: 10px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.explanation-box {
background: var(--vp-c-bg);
padding: 16px;
border-radius: 8px;
}
.explanation-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 12px;
}
.explanation-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.explanation-item {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
line-height: 1.6;
}
.explanation-item .icon {
flex-shrink: 0;
}
.principle-box {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 8px;
font-size: 14px;
color: var(--vp-c-text-1);
margin-top: 16px;
}
.principle-icon {
font-size: 24px;
}
.principle-content {
flex: 1;
}
</style>
@@ -1,13 +1,207 @@
<!--
ReliabilityDemo.vue
消息可靠性演示 - 三道防线
-->
<template>
<div class="demo-container">
<div class="reliability-demo">
<div class="demo-header">
<h4>{{ title }}</h4>
<p class="hint">{{ description }}</p>
<span class="icon">🛡</span>
<span class="title">消息可靠性演示</span>
<span class="subtitle">三道防线保证消息不丢失</span>
</div>
<div class="demo-content">
<el-alert type="info" :closable="false">
消息可靠性演示组件占位符 - 待实现具体交互
</el-alert>
<div class="defense-system">
<!-- 防线1: 生产者确认 -->
<div class="defense-line">
<div class="defense-header">
<div class="defense-badge line1">防线 1</div>
<div class="defense-title">生产者确认 (Producer ACK)</div>
</div>
<div class="defense-content">
<div class="flow-diagram">
<div class="component producer">
<div class="comp-icon">📤</div>
<div class="comp-label">生产者</div>
<div class="comp-desc">发送消息</div>
</div>
<div class="message-flow">
<div class="msg-item" :class="{ active: step === 1 }">
<div class="msg-icon">📨</div>
<div class="msg-label">消息</div>
<div v-if="step === 1" class="msg-status">
{{ ackStatus }}
</div>
</div>
<div class="ack-item" :class="{ active: step === 2 }">
<div class="ack-icon"></div>
<div class="ack-label">ACK确认</div>
<div v-if="step === 2" class="ack-status">
{{ ackMessage }}
</div>
</div>
</div>
<div class="component broker">
<div class="comp-icon">📦</div>
<div class="comp-label">Broker</div>
<div class="comp-desc">接收并存储</div>
</div>
</div>
<div class="control-panel">
<div class="control-item">
<label>发送消息</label>
<button class="action-btn" @click="sendWithAck" :disabled="step > 0">
发送并等待确认
</button>
</div>
<div class="info-text">
<span class="info-icon">💡</span>
如果没收到ACK,生产者会重试或记录本地日志
</div>
</div>
</div>
</div>
<!-- 防线2: Broker持久化 -->
<div class="defense-line">
<div class="defense-header">
<div class="defense-badge line2">防线 2</div>
<div class="defense-title">Broker持久化</div>
</div>
<div class="defense-content">
<div class="storage-diagram">
<div class="storage-container">
<div class="storage-option" :class="{ active: storageType === 'memory' }">
<div class="option-icon"></div>
<div class="option-label">内存存储</div>
<div class="option-desc">速度快,但重启丢失</div>
<div class="option-risk"> 高风险</div>
</div>
<div class="vs-divider">vs</div>
<div class="storage-option recommended" :class="{ active: storageType === 'disk' }">
<div class="option-icon">💾</div>
<div class="option-label">磁盘存储</div>
<div class="option-desc">落盘保证不丢失</div>
<div class="option-risk"> 推荐</div>
</div>
</div>
<div class="replication-info">
<div class="replication-title">
<span class="icon">🔄</span>
多副本同步
</div>
<div class="replication-detail">
消息同步到3个节点,即使1个节点宕机也不丢数据
</div>
</div>
</div>
<div class="control-panel">
<div class="control-item">
<label>存储方式</label>
<div class="btn-group">
<button
class="toggle-btn"
:class="{ active: storageType === 'memory' }"
@click="storageType = 'memory'"
>
内存
</button>
<button
class="toggle-btn"
:class="{ active: storageType === 'disk' }"
@click="storageType = 'disk'"
>
磁盘
</button>
</div>
</div>
<div class="info-text" :class="{ warning: storageType === 'memory' }">
<span class="info-icon">{{ storageType === 'disk' ? '✅' : '⚠️' }}</span>
{{ storageType === 'disk' ? '消息已落盘,安全可靠' : '消息仅在内存,重启丢失' }}
</div>
</div>
</div>
</div>
<!-- 防线3: 消费者确认 -->
<div class="defense-line">
<div class="defense-header">
<div class="defense-badge line3">防线 3</div>
<div class="defense-title">消费者确认 (Consumer ACK)</div>
</div>
<div class="defense-content">
<div class="consumer-flow">
<div class="flow-step" :class="{ active: consumerStep >= 1 }">
<div class="step-num">1</div>
<div class="step-content">
<div class="step-title">拉取消息</div>
<div class="step-desc">从Broker获取消息</div>
</div>
</div>
<div class="flow-arrow" :class="{ active: consumerStep >= 1 }"></div>
<div class="flow-step" :class="{ active: consumerStep >= 2 }">
<div class="step-num">2</div>
<div class="step-content">
<div class="step-title">处理消息</div>
<div class="step-desc">执行业务逻辑</div>
</div>
</div>
<div class="flow-arrow" :class="{ active: consumerStep >= 2 }"></div>
<div class="flow-step" :class="{ active: consumerStep >= 3 }">
<div class="step-num">3</div>
<div class="step-content">
<div class="step-title">手动ACK</div>
<div class="step-desc">确认处理完成</div>
</div>
</div>
</div>
<div class="ack-comparison">
<div class="ack-option">
<div class="ack-type">自动 ACK</div>
<div class="ack-desc">高效但可能丢消息</div>
<div class="ack-risk"> 不推荐</div>
</div>
<div class="ack-option recommended">
<div class="ack-type">手动 ACK</div>
<div class="ack-desc">可靠,处理完才确认</div>
<div class="ack-risk"> 推荐</div>
</div>
</div>
<div class="control-panel">
<div class="control-item">
<label>模拟消费</label>
<button class="action-btn" @click="simulateConsume" :disabled="consumerStep > 0">
开始消费流程
</button>
</div>
<div class="info-text">
<span class="info-icon">💡</span>
如果处理失败,不发送ACK,Broker会重新投递
</div>
</div>
</div>
</div>
</div>
<div class="summary-box">
<div class="summary-icon">🎯</div>
<div class="summary-content">
<strong>三道防线,缺一不可</strong>生产者确认 Broker持久化 消费者确认
</div>
</div>
</div>
</template>
@@ -15,36 +209,500 @@
<script setup>
import { ref } from 'vue'
const title = ref('消息可靠性演示')
const description = ref('展示消息队列如何保证消息的可靠传输,包括消息确认、持久化、重试机制等')
// 线1:
const step = ref(0)
const ackStatus = ref('')
const ackMessage = ref('')
// 线2:
const storageType = ref('disk')
// 线3:
const consumerStep = ref(0)
const sendWithAck = () => {
step.value = 1
ackStatus.value = '发送中...'
setTimeout(() => {
step.value = 2
ackStatus.value = '已发送'
ackMessage.value = '收到ACK,消息安全'
setTimeout(() => {
step.value = 0
ackStatus.value = ''
ackMessage.value = ''
}, 3000)
}, 1500)
}
const simulateConsume = () => {
consumerStep.value = 1
setTimeout(() => {
consumerStep.value = 2
setTimeout(() => {
consumerStep.value = 3
setTimeout(() => {
consumerStep.value = 0
}, 3000)
}, 1500)
}, 1500)
}
</script>
<style scoped>
.demo-container {
.reliability-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
border-radius: 12px;
background: var(--vp-c-bg-soft);
padding: 20px;
margin: 20px 0;
font-family: var(--vp-font-family-base);
}
.demo-header {
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.demo-header h4 {
margin: 0 0 8px 0;
.demo-header .icon {
font-size: 24px;
}
.demo-header .title {
font-weight: 700;
font-size: 18px;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 14px;
margin-left: 8px;
}
.defense-system {
display: flex;
flex-direction: column;
gap: 24px;
}
.defense-line {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
overflow: hidden;
}
.defense-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.defense-badge {
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 700;
color: white;
}
.defense-badge.line1 {
background: #3b82f6
}
.defense-badge.line2 {
background: #f59e0b
}
.defense-badge.line3 {
background: #22c55e
}
.defense-title {
font-weight: 600;
font-size: 15px;
color: var(--vp-c-text-1);
}
.defense-content {
padding: 20px;
}
.flow-diagram {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
margin-bottom: 20px;
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.component {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-brand);
border-radius: 12px;
min-width: 120px;
}
.comp-icon {
font-size: 32px;
}
.comp-label {
font-weight: 600;
font-size: 14px;
}
.comp-desc {
font-size: 12px;
color: var(--vp-c-text-2);
}
.demo-content {
.message-flow {
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}
.msg-item,
.ack-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 10px;
border-radius: 8px;
transition: all 0.3s;
}
.msg-item.active {
background: rgba(59, 130, 246, 0.1);
}
.ack-item.active {
background: rgba(34, 197, 94, 0.1);
}
.msg-icon,
.ack-icon {
font-size: 24px;
}
.msg-label,
.ack-label {
font-size: 12px;
font-weight: 600;
}
.msg-status,
.ack-status {
font-size: 11px;
color: var(--vp-c-text-2);
}
.storage-diagram {
margin-bottom: 20px;
}
.storage-container {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
margin-bottom: 16px;
}
.storage-option {
flex: 1;
padding: 20px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
text-align: center;
transition: all 0.3s;
}
.storage-option.active {
border-color: var(--vp-c-brand);
background: rgba(59, 130, 246, 0.05);
}
.storage-option.recommended {
border-color: var(--vp-c-success);
}
.storage-option.recommended.active {
background: rgba(34, 197, 94, 0.05);
}
.option-icon {
font-size: 36px;
margin-bottom: 10px;
}
.option-label {
font-weight: 600;
font-size: 15px;
margin-bottom: 6px;
}
.option-desc {
font-size: 13px;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.option-risk {
font-size: 12px;
font-weight: 600;
}
.vs-divider {
font-size: 18px;
font-weight: 700;
color: var(--vp-c-text-2);
}
.replication-info {
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
display: flex;
align-items: center;
gap: 12px;
}
.replication-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 14px;
}
.replication-icon {
font-size: 20px;
}
.replication-detail {
font-size: 13px;
color: var(--vp-c-text-2);
}
.consumer-flow {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 20px;
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.flow-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 12px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
min-width: 100px;
transition: all 0.3s;
}
.flow-step.active {
border-color: var(--vp-c-brand);
background: rgba(59, 130, 246, 0.05);
}
.step-num {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--vp-c-brand);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
}
.step-title {
font-weight: 600;
font-size: 13px;
}
.step-desc {
font-size: 11px;
color: var(--vp-c-text-2);
}
.flow-arrow {
font-size: 24px;
color: var(--vp-c-divider);
transition: all 0.3s;
}
.flow-arrow.active {
color: var(--vp-c-brand);
}
.ack-comparison {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.ack-option {
padding: 16px;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
text-align: center;
}
.ack-option.recommended {
border-color: var(--vp-c-success);
background: rgba(34, 197, 94, 0.05);
}
.ack-type {
font-weight: 600;
font-size: 15px;
margin-bottom: 8px;
}
.ack-desc {
font-size: 13px;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.ack-risk {
font-size: 12px;
font-weight: 600;
}
.control-panel {
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.control-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.control-item label {
font-weight: 600;
font-size: 14px;
}
.action-btn {
padding: 10px 20px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
transition: all 0.2s;
}
.action-btn:hover:not(:disabled) {
opacity: 0.9;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-group {
display: flex;
gap: 8px;
}
.toggle-btn {
padding: 8px 16px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
transition: all 0.2s;
}
.toggle-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand);
color: white;
}
.info-text {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
padding: 10px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.info-text.warning {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
.info-icon {
font-size: 16px;
}
.summary-box {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 8px;
font-size: 14px;
color: var(--vp-c-text-1);
}
.summary-icon {
font-size: 24px;
}
.summary-content {
flex: 1;
}
</style>
@@ -1,50 +1,51 @@
<!--
BigFrontendScopeDemo.vue
前端 vs 大前端跨端范围演示
-->
<template>
<div class="bigfe-demo">
<div class="header">
<div class="title">前端 vs 大前端到底前端都包含什么</div>
<div class="subtitle">点一下不同立刻看到它跑在哪里怎么发布</div>
<div class="demo-header">
<span class="icon">🌐</span>
<span class="title">前端 vs 大前端</span>
<span class="subtitle">了解不同平台的运行环境和技术栈</span>
</div>
<div class="platforms">
<button
v-for="p in platforms"
:key="p.key"
class="platform"
:class="{ active: current === p.key }"
@click="current = p.key"
>
<span class="icon">{{ p.icon }}</span>
<span>{{ p.label }}</span>
</button>
</div>
<div class="demo-content">
<div class="platforms">
<button
v-for="p in platforms"
:key="p.key"
class="platform"
:class="{ active: current === p.key }"
@click="current = p.key"
>
<span class="icon">{{ p.icon }}</span>
<span>{{ p.label }}</span>
</button>
</div>
<div class="cards">
<div class="card">
<div class="label">运行环境</div>
<div class="value">{{ currentData.runtime }}</div>
<div class="cards">
<div class="card">
<div class="label">运行环境</div>
<div class="value">{{ currentData.runtime }}</div>
</div>
<div class="card">
<div class="label">主要技术</div>
<div class="value">{{ currentData.stack }}</div>
</div>
<div class="card">
<div class="label">发布方式</div>
<div class="value">{{ currentData.release }}</div>
</div>
</div>
<div class="card">
<div class="label">主要技术</div>
<div class="value">{{ currentData.stack }}</div>
</div>
<div class="card">
<div class="label">发布方式</div>
<div class="value">{{ currentData.release }}</div>
<div class="skills">
<div class="skills-title">哪些能力是"共通的"</div>
<div class="tags">
<span v-for="t in commonSkills.slice(0, 6)" :key="t" class="tag">{{ t }}</span>
</div>
</div>
</div>
<div class="skills">
<div class="skills-title">哪些能力是共通的</div>
<div class="tags">
<span v-for="t in commonSkills" :key="t" class="tag">{{ t }}</span>
</div>
<div class="skills-note">
大前端的核心不是会更多框架而是<strong>用同一套工程能力把体验交付到不同平台</strong>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>大前端不是"会更多框架"而是用同一套工程能力把体验交付到不同平台
</div>
</div>
</template>
@@ -53,11 +54,11 @@
import { ref, computed } from 'vue'
const platforms = [
{ key: 'web', label: 'Web 网站', icon: '🌐' },
{ key: 'h5', label: 'H5 活动页', icon: '📱' },
{ key: 'web', label: 'Web网站', icon: '🌐' },
{ key: 'h5', label: 'H5活动页', icon: '📱' },
{ key: 'miniapp', label: '小程序', icon: '🧩' },
{ key: 'native', label: 'App(原生)', icon: '📲' },
{ key: 'cross', label: '跨端 App', icon: '🧱' },
{ key: 'native', label: '原生App', icon: '📲' },
{ key: 'cross', label: '跨端App', icon: '🧱' },
{ key: 'desktop', label: '桌面应用', icon: '🖥️' }
]
@@ -112,32 +113,45 @@ const commonSkills = [
<style scoped>
.bigfe-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.header {
margin-bottom: 1rem;
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
.demo-header .icon {
font-size: 1.25rem;
}
.subtitle {
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
font-size: 0.85rem;
margin-left: 0.5rem;
}
.demo-content {
margin-bottom: 0.5rem;
}
.platforms {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.75rem 0 1rem;
margin-bottom: 1rem;
}
.platform {
@@ -150,22 +164,25 @@ const commonSkills = [
display: inline-flex;
align-items: center;
gap: 0.35rem;
transition: all 0.2s;
}
.platform:hover {
background: var(--vp-c-bg-soft);
}
.platform.active {
border-color: #3b82f6;
color: #1d4ed8;
background: rgba(59, 130, 246, 0.12);
}
.icon {
font-size: 1rem;
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
font-weight: 600;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.75rem;
margin-bottom: 1rem;
}
.card {
@@ -185,17 +202,19 @@ const commonSkills = [
font-size: 0.95rem;
font-weight: 600;
line-height: 1.35;
color: var(--vp-c-text-1);
}
.skills {
margin-top: 1rem;
border-top: 1px dashed var(--vp-c-divider);
padding-top: 1rem;
padding-top: 0.75rem;
}
.skills-title {
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.tags {
@@ -208,14 +227,26 @@ const commonSkills = [
font-size: 0.8rem;
padding: 0.2rem 0.55rem;
border-radius: 999px;
background: rgba(34, 197, 94, 0.12);
color: #15803d;
border: 1px solid rgba(34, 197, 94, 0.2);
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.skills-note {
margin-top: 0.75rem;
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -4,6 +4,12 @@
-->
<template>
<div class="box-demo">
<div class="demo-header">
<span class="icon">📦</span>
<span class="title">CSS 盒模型</span>
<span class="subtitle">理解元素尺寸的构成通俗说盒子的四层包装</span>
</div>
<div class="controls">
<div class="control-item">
<div class="control-header">
@@ -67,6 +73,11 @@
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>每个元素都是一个"盒子"从内到外依次是内容区 内边距 边框 外边距
</div>
<div class="code-block">
<div class="code-title">CSS 代码片段</div>
<div class="code-content">
@@ -99,13 +110,37 @@ const total = computed(
<style scoped>
.box-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 24px;
margin: 24px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 24px;
gap: 1rem;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.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;
}
.controls {
@@ -310,4 +345,22 @@ input[type='range'] {
.line {
white-space: pre;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,73 +1,86 @@
<!--
CssFlexbox.vue
Flex 速学三个按钮控制方向/对齐/换行实时看盒子怎么排
Flex 布局速学三个按钮控制方向/对齐/换行实时看盒子怎么排
-->
<template>
<div class="flex-demo">
<div class="controls">
<div class="control-item">
<div class="control-header">
<label>主轴方向 (flex-direction)</label>
<div class="demo-header">
<span class="icon">📐</span>
<span class="title">Flex 布局</span>
<span class="subtitle">一行代码搞定排列对齐通俗说自动排版</span>
</div>
<div class="demo-content">
<div class="controls">
<div class="control-item">
<div class="control-header">
<label>主轴方向 (flex-direction)</label>
</div>
<div class="chips">
<button
v-for="d in directions"
:key="d.id"
:class="['chip', { active: dir === d.id }]"
@click="dir = d.id"
>
{{ d.label }}
</button>
</div>
</div>
<div class="chips">
<button
v-for="d in directions"
:key="d.id"
:class="['chip', { active: dir === d.id }]"
@click="dir = d.id"
>
{{ d.label }}
</button>
<div class="control-item">
<div class="control-header">
<label>主轴对齐 (justify-content)</label>
</div>
<div class="chips">
<button
v-for="j in justifies"
:key="j.id"
:class="['chip', { active: justify === j.id }]"
@click="justify = j.id"
>
{{ j.label }}
</button>
</div>
</div>
<div class="control-item">
<div class="control-header">
<label>是否换行 (flex-wrap)</label>
</div>
<div class="chips">
<button
v-for="w in wraps"
:key="w.id"
:class="['chip', { active: wrap === w.id }]"
@click="wrap = w.id"
>
{{ w.label }}
</button>
</div>
</div>
</div>
<div class="control-item">
<div class="control-header">
<label>主轴对齐 (justify-content)</label>
</div>
<div class="chips">
<button
v-for="j in justifies"
:key="j.id"
:class="['chip', { active: justify === j.id }]"
@click="justify = j.id"
>
{{ j.label }}
</button>
<div class="canvas-container">
<div class="canvas" :style="boxStyle">
<div v-for="n in 6" :key="n" class="item">{{ n }}</div>
</div>
</div>
<div class="control-item">
<div class="control-header">
<label>是否换行 (flex-wrap)</label>
</div>
<div class="chips">
<button
v-for="w in wraps"
:key="w.id"
:class="['chip', { active: wrap === w.id }]"
@click="wrap = w.id"
>
{{ w.label }}
</button>
<div class="code-block">
<div class="code-title">CSS 代码片段</div>
<div class="code-content">
<div class="line">.container {</div>
<div class="line">display: flex;</div>
<div class="line">flex-direction: {{ dir }};</div>
<div class="line">justify-content: {{ justify }};</div>
<div class="line">flex-wrap: {{ wrap }};</div>
<div class="line">}</div>
</div>
</div>
</div>
<div class="canvas-container">
<div class="canvas" :style="boxStyle">
<div v-for="n in 8" :key="n" class="item">{{ n }}</div>
</div>
</div>
<div class="code-block">
<div class="code-title">CSS 代码片段</div>
<div class="code-content">
<div class="line">.container {</div>
<div class="line">display: flex;</div>
<div class="line">flex-direction: {{ dir }};</div>
<div class="line">justify-content: {{ justify }};</div>
<div class="line">flex-wrap: {{ wrap }};</div>
<div class="line">}</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>Flex 让元素自动排列不用手动计算位置就像书架上的书会自动对齐
</div>
</div>
</template>
@@ -99,7 +112,7 @@ const boxStyle = computed(() => ({
justifyContent: justify.value,
flexWrap: wrap.value,
gap: '12px',
minHeight: '200px',
minHeight: '180px',
padding: '16px'
}))
</script>
@@ -107,50 +120,76 @@ const boxStyle = computed(() => ({
<style scoped>
.flex-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 24px;
margin: 24px 0;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
display: flex;
flex-direction: column;
gap: 24px;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.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;
}
.demo-content {
margin-bottom: 0.5rem;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem;
margin-bottom: 1rem;
}
.control-item {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 12px;
gap: 0.5rem;
}
.control-header label {
font-weight: 700;
font-weight: 600;
color: var(--vp-c-text-1);
font-size: 13px;
font-size: 0.8rem;
}
.chips {
display: flex;
gap: 8px;
gap: 0.5rem;
flex-wrap: wrap;
}
.chip {
padding: 6px 12px;
padding: 0.25rem 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
cursor: pointer;
font-size: 13px;
font-size: 0.8rem;
transition: all 0.2s;
}
@@ -161,77 +200,60 @@ const boxStyle = computed(() => ({
.chip.active {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
background: var(--vp-c-brand-dimm);
background: var(--vp-c-brand-soft);
font-weight: 600;
}
.canvas-container {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 4px; /* Tiny padding for the inner canvas */
border-radius: 8px;
padding: 0.25rem;
margin-bottom: 1rem;
}
.canvas {
background: var(--vp-c-bg);
border-radius: 8px;
/* border: 1px dashed var(--vp-c-divider); */
border-radius: 6px;
background-image: radial-gradient(var(--vp-c-divider) 1px, transparent 1px);
background-size: 20px 20px;
}
.item {
width: 60px;
height: 60px;
border-radius: 8px;
background: linear-gradient(135deg, #0ea5e9, #10b981);
width: 50px;
height: 50px;
border-radius: 6px;
background: var(--vp-c-brand);
color: #fff;
font-weight: 800;
font-weight: 700;
display: grid;
place-items: center;
font-size: 18px;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 16px;
transition: all 0.3s;
flex-shrink: 0;
}
.val {
font-family: var(--vp-font-family-mono);
color: var(--vp-c-brand);
font-weight: 600;
font-size: 13px;
}
input[type='range'] {
width: 100%;
accent-color: var(--vp-c-brand);
cursor: pointer;
margin-top: 8px;
}
.code-block {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
border-radius: 8px;
padding: 0.75rem;
}
.code-title {
font-weight: 700;
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.code-content {
background: #0b1221;
color: #e5e7eb;
border-radius: 8px;
padding: 16px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border-radius: 6px;
padding: 0.75rem;
font-family: var(--vp-font-family-mono);
font-size: 13px;
font-size: 0.75rem;
overflow-x: auto;
line-height: 1.6;
}
@@ -239,4 +261,22 @@ input[type='range'] {
.line {
white-space: pre;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,35 +1,45 @@
<!--
DomManipulator.vue
DOM 速体验输入标题+切换高亮类直观看到文本和样式变化
DOM 操作速体验输入标题+切换高亮类直观看到文本和样式变化
-->
<template>
<div class="dom-demo">
<div class="controls">
<div class="field">
<label>改个标题</label>
<input v-model="title" placeholder="输入新标题" />
</div>
<div class="field checkbox">
<label
><input type="checkbox" v-model="highlight" /> 高亮模式
(class="highlight")</label
>
</div>
<div class="demo-header">
<span class="icon">🎯</span>
<span class="title">DOM 操作</span>
<span class="subtitle">网页内容的动态修改通俗说用代码改页面</span>
</div>
<div class="card" :class="{ highlight }">
<h2 id="hero">{{ title }}</h2>
<p id="desc">这里是段落说明勾选高亮看看变化</p>
<button @click="toggleText">{{ buttonText }}</button>
</div>
<div class="demo-content">
<div class="controls">
<div class="field">
<label>改个标题</label>
<input v-model="title" placeholder="输入新标题" />
</div>
<div class="field checkbox">
<label><input type="checkbox" v-model="highlight" /> 高亮模式 (class="highlight")</label>
</div>
</div>
<pre class="code"><code>// JS
<div class="card" :class="{ highlight }">
<h2 id="hero">{{ title }}</h2>
<p id="desc">这里是段落说明勾选高亮看看变化</p>
<button @click="toggleText">{{ buttonText }}</button>
</div>
<pre class="code"><code>// JS
const titleEl = document.getElementById('hero')
titleEl.textContent = '{{ title }}'
// JS class
const card = document.querySelector('.card')
card.classList.toggle('highlight', {{ highlight }})</code></pre>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>DOM 是网页的"乐高积木"JavaScript 可以随时添加删除修改这些积木
</div>
</div>
</template>
@@ -48,73 +58,154 @@ const toggleText = () => {
<style scoped>
.dom-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 16px;
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 12px;
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.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;
}
.demo-content {
margin-bottom: 0.5rem;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 10px;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
margin-bottom: 1rem;
}
.field {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 10px;
border-radius: 8px;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 6px;
gap: 0.5rem;
}
.field label {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.checkbox {
flex-direction: row;
align-items: center;
gap: 8px;
gap: 0.5rem;
}
input[type='text'],
input[type='text'] {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
input[type='text']:focus {
outline: none;
border-color: var(--vp-c-brand);
}
input[type='checkbox'] {
accent-color: var(--vp-c-brand);
}
.card {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 16px;
border-radius: 8px;
padding: 1rem;
background: var(--vp-c-bg);
transition: all 0.2s;
margin-bottom: 1rem;
}
.card.highlight {
border-color: #f59e0b;
box-shadow: 0 8px 18px rgba(245, 158, 11, 0.2);
background: #fff7ed;
border-color: var(--vp-c-warning);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
background: var(--vp-c-bg-soft);
}
.card h2 {
margin: 0 0 8px 0;
margin: 0 0 0.5rem 0;
color: var(--vp-c-text-1);
}
.card p {
margin: 0 0 12px 0;
margin: 0 0 0.75rem 0;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.card button {
background: var(--vp-c-brand);
color: #fff;
color: white;
border: none;
border-radius: 8px;
padding: 8px 12px;
border-radius: 6px;
padding: 0.5rem 1rem;
cursor: pointer;
font-size: 0.85rem;
transition: opacity 0.2s;
}
.card button:hover {
opacity: 0.9;
}
.code {
background: #0b1221;
color: #e5e7eb;
border-radius: 10px;
padding: 12px;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
border-radius: 8px;
padding: 0.75rem;
font-family: var(--vp-font-family-mono);
font-size: 13px;
font-size: 0.75rem;
overflow-x: auto;
line-height: 1.6;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -1,70 +1,83 @@
<template>
<div class="imperative-declarative-demo">
<div class="demo-grid">
<!-- Imperative (jQuery Style) -->
<div class="panel imperative">
<div class="panel-header">
<span class="badge yellow">Imperative (命令式)</span>
<span class="sub-text">jQuery Style</span>
</div>
<div class="code-preview">
<code>
// DOM<br />
$('#count').text(val);<br />
if (val > 5) $('#msg').show();
</code>
</div>
<div class="interactive-area">
<div class="output-box">
Count: <span id="imp-count-display">{{ impCount }}</span>
<div v-show="impShowMsg" class="warning-msg"> Count is high!</div>
</div>
<div class="actions">
<button @click="impIncrement" class="btn">Step 1: Value++</button>
<button @click="impUpdateText" class="btn" :disabled="!impChanged">
Step 2: Update Text
</button>
<button
@click="impCheckState"
class="btn"
:disabled="!impTextUpdated"
>
Step 3: Check Logic
</button>
</div>
<div class="status-log">{{ impStatus }}</div>
</div>
</div>
<div class="demo-header">
<span class="icon">🔄</span>
<span class="title">命令式 vs 声明式</span>
<span class="subtitle">两种编程思维的对比通俗说手动操作 vs 自动响应</span>
</div>
<!-- Declarative (Vue Style) -->
<div class="panel declarative">
<div class="panel-header">
<span class="badge green">Declarative (声明式)</span>
<span class="sub-text">Vue/React Style</span>
</div>
<div class="code-preview">
<code>
// <br />
{{ '{' + '{ count }' + '}' }}<br />
&lt;div v-if="count > 5"&gt;...&lt;/div&gt;
</code>
</div>
<div class="interactive-area">
<div class="output-box">
Count: <span>{{ decCount }}</span>
<div v-if="decCount > 5" class="warning-msg"> Count is high!</div>
<div class="demo-content">
<div class="demo-grid">
<!-- Imperative (jQuery Style) -->
<div class="panel imperative">
<div class="panel-header">
<span class="badge yellow">命令式 (Imperative)</span>
<span class="sub-text">jQuery Style - 手动操作</span>
</div>
<div class="actions">
<button @click="decIncrement" class="btn primary">
Value++ (Auto Render)
</button>
<div class="code-preview">
<code>
// DOM<br />
$('#count').text(val);<br />
if (val > 5) $('#msg').show();
</code>
</div>
<div class="status-log">
Framework handles DOM updates automatically.
<div class="interactive-area">
<div class="output-box">
Count: <span id="imp-count-display">{{ impCount }}</span>
<div v-show="impShowMsg" class="warning-msg"> Count is high!</div>
</div>
<div class="actions">
<button @click="impIncrement" class="btn">Step 1: Value++</button>
<button @click="impUpdateText" class="btn" :disabled="!impChanged">
Step 2: Update Text
</button>
<button
@click="impCheckState"
class="btn"
:disabled="!impTextUpdated"
>
Step 3: Check Logic
</button>
</div>
<div class="status-log">{{ impStatus }}</div>
</div>
</div>
<!-- Declarative (Vue Style) -->
<div class="panel declarative">
<div class="panel-header">
<span class="badge green">声明式 (Declarative)</span>
<span class="sub-text">Vue/React Style - 自动响应</span>
</div>
<div class="code-preview">
<code v-pre>
//
{{ count }}
&lt;div v-if="count > 5"&gt;...&lt;/div&gt;
</code>
</div>
<div class="interactive-area">
<div class="output-box">
Count: <span>{{ decCount }}</span>
<div v-if="decCount > 5" class="warning-msg"> Count is high!</div>
</div>
<div class="actions">
<button @click="decIncrement" class="btn primary">
Value++ (Auto Render)
</button>
</div>
<div class="status-log">
Framework handles DOM updates automatically.
</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>命令式像"手把手教电脑怎么做"声明式像"告诉电脑要什么,它自己搞定"
</div>
</div>
</template>
@@ -119,12 +132,40 @@ const decIncrement = () => {
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.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;
}
.demo-content {
margin-bottom: 0.5rem;
}
.demo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
gap: 1rem;
}
@media (max-width: 640px) {
@@ -143,7 +184,7 @@ const decIncrement = () => {
}
.panel-header {
padding: 0.8rem;
padding: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
justify-content: space-between;
@@ -152,48 +193,51 @@ const decIncrement = () => {
}
.badge {
font-size: 0.8rem;
font-size: 0.75rem;
font-weight: bold;
padding: 2px 6px;
padding: 2px 8px;
border-radius: 4px;
color: white;
}
.badge.yellow {
background: #f59e0b;
background: var(--vp-c-warning);
}
.badge.green {
background: #10b981;
background: var(--vp-c-success);
}
.sub-text {
font-size: 0.8rem;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.code-preview {
background: #1e1e2e;
padding: 0.8rem;
background: var(--vp-c-bg-alt);
padding: 0.75rem;
font-family: monospace;
font-size: 0.8rem;
color: #a6accd;
height: 80px;
font-size: 0.75rem;
color: var(--vp-c-text-1);
height: 70px;
overflow: hidden;
}
.interactive-area {
padding: 1rem;
padding: 0.75rem;
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
gap: 0.75rem;
}
.output-box {
background: var(--vp-c-bg-soft);
padding: 1rem;
padding: 0.75rem;
border-radius: 6px;
text-align: center;
font-weight: bold;
min-height: 80px;
min-height: 70px;
display: flex;
flex-direction: column;
align-items: center;
@@ -201,10 +245,9 @@ const decIncrement = () => {
}
.warning-msg {
color: #ef4444;
color: var(--vp-c-danger);
margin-top: 0.5rem;
font-size: 0.9rem;
animation: popIn 0.3s;
font-size: 0.85rem;
}
.actions {
@@ -217,42 +260,53 @@ const decIncrement = () => {
padding: 0.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: white;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.8rem;
font-size: 0.75rem;
transition: all 0.2s;
}
.btn:hover:not(:disabled) {
background: #f3f4f6;
background: var(--vp-c-bg-soft);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn.primary {
background: #3b82f6;
background: var(--vp-c-brand);
color: white;
border: none;
}
.btn.primary:hover {
background: #2563eb;
opacity: 0.9;
}
.status-log {
font-size: 0.75rem;
color: var(--vp-c-text-3);
font-size: 0.7rem;
color: var(--vp-c-text-2);
font-style: italic;
min-height: 1.2em;
}
@keyframes popIn {
0% {
transform: scale(0.8);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
display: flex;
gap: 0.25rem;
}
.info-box .icon {
flex-shrink: 0;
}
.info-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -303,6 +303,8 @@ const handleBtnClick = () => {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 600px;
overflow-y: auto;
}
/* New Config Panel Styles */
+2
View File
@@ -222,6 +222,7 @@ import MessageQueueComponentsDemo from './components/appendix/queue-design/Messa
import PointToPointVsPubSubDemo from './components/appendix/queue-design/PointToPointVsPubSubDemo.vue'
import MessageQueueComparisonDemo from './components/appendix/queue-design/MessageQueueComparisonDemo.vue'
import CouplingDemo from './components/appendix/queue-design/CouplingDemo.vue'
import DecouplingDemo from './components/appendix/queue-design/DecouplingDemo.vue'
import PubSubDemo from './components/appendix/queue-design/PubSubDemo.vue'
import DeadLetterQueueDemo from './components/appendix/queue-design/DeadLetterQueueDemo.vue'
import DelayedMessageDemo from './components/appendix/queue-design/DelayedMessageDemo.vue'
@@ -693,6 +694,7 @@ export default {
app.component('PointToPointVsPubSubDemo', PointToPointVsPubSubDemo)
app.component('MessageQueueComparisonDemo', MessageQueueComparisonDemo)
app.component('CouplingDemo', CouplingDemo)
app.component('DecouplingDemo', DecouplingDemo)
app.component('PubSubDemo', PubSubDemo)
app.component('DeadLetterQueueDemo', DeadLetterQueueDemo)
app.component('DelayedMessageDemo', DelayedMessageDemo)
File diff suppressed because it is too large Load Diff
+18 -18
View File
@@ -151,12 +151,12 @@ _很多人以为 AI 直接处理"声音",但实际上 AI 处理的是**数字
### 4.1 自回归 vs 非自回归
| 特性 | 自回归 (AR) | 非自回归 (NAR) | 流匹配 (Flow) |
|------|------------|---------------|--------------|
| 生成方式 | 逐个时间步 | 一次性生成 | 流匹配路径 |
| 速度 | 慢 | 快 | 很快 |
| 音质 | 高 | 中高 | 高 |
| 代表模型 | Tacotron 2 | FastSpeech 2 | F5-TTS |
| 特性 | 自回归 (AR) | 非自回归 (NAR) | 流匹配 (Flow) |
| -------- | ----------- | -------------- | ------------- |
| 生成方式 | 逐个时间步 | 一次性生成 | 流匹配路径 |
| 速度 | 慢 | 快 | 很快 |
| 音质 | 高 | 中高 | 高 |
| 代表模型 | Tacotron 2 | FastSpeech 2 | F5-TTS |
### 4.2 关键组件
@@ -285,15 +285,15 @@ GST (Global Style Token) 是一种从参考音频中提取风格特征的方法
## 附录:常用术语表 (Vocabulary)
| 术语 | 英文 | 解释 |
| :------------- | :--------------------------- | :------------------------------------------- |
| **采样率** | Sample Rate | 每秒采集的音频样本数(如 44.1kHz)。 |
| **梅尔频谱** | Mel-Spectrogram | 模拟人耳感知的频谱表示,音频 AI 的核心输入。 |
| **声码器** | Vocoder | 将频谱图还原为音频波形的模型。 |
| **TTS** | Text-to-Speech | 文本转语音,让 AI 说话的技术。 |
| **ASR** | Automatic Speech Recognition | 自动语音识别,让 AI 听懂的技术。 |
| **零样本克隆** | Zero-Shot Cloning | 只需几秒参考音频就能模仿任何声音。 |
| **流匹配** | Flow Matching | 一种高效的生成方法,用于最新的 TTS 模型。 |
| **声音编码器** | Speaker Encoder | 提取声音身份特征的神经网络。 |
| **GST** | Global Style Token | 全局风格 Token,用于情感控制。 |
| **神经编解码器**| Neural Codec | 将音频压缩为离散 Token 的模型。 |
| 术语 | 英文 | 解释 |
| :--------------- | :--------------------------- | :------------------------------------------- |
| **采样率** | Sample Rate | 每秒采集的音频样本数(如 44.1kHz)。 |
| **梅尔频谱** | Mel-Spectrogram | 模拟人耳感知的频谱表示,音频 AI 的核心输入。 |
| **声码器** | Vocoder | 将频谱图还原为音频波形的模型。 |
| **TTS** | Text-to-Speech | 文本转语音,让 AI 说话的技术。 |
| **ASR** | Automatic Speech Recognition | 自动语音识别,让 AI 听懂的技术。 |
| **零样本克隆** | Zero-Shot Cloning | 只需几秒参考音频就能模仿任何声音。 |
| **流匹配** | Flow Matching | 一种高效的生成方法,用于最新的 TTS 模型。 |
| **声音编码器** | Speaker Encoder | 提取声音身份特征的神经网络。 |
| **GST** | Global Style Token | 全局风格 Token,用于情感控制。 |
| **神经编解码器** | Neural Codec | 将音频压缩为离散 Token 的模型。 |
+447 -248
View File
@@ -1,404 +1,603 @@
# 后端架构演进:从单机到云原生
> 💡 **学习指南**:本章节无需编程基础,通过交互式演示带你回顾后端架构的 30 年变迁。我们将从最原始的物理服务器讲起,一直到现代的 Serverless 云计算。理解架构演进的历史,能帮助你在面对技术选型时做出更明智的决策。
::: tip 🎯 核心问题
**代码写好了,怎么让全世界的人都能访问?** 这就像问:你是想开一家路边小摊,还是经营一家跨国连锁餐厅?后端架构的选择,决定了你的"餐厅"能服务多少顾客。
:::
---
## 1. 为什么要了解架构演进?
想象一下,你正在规划一次长途旅行。你可以选择骑自行车、开私家车、坐高铁,或者乘飞机。每种方式都有其适用的场景:自行车适合短距离且想锻炼身体的情况,飞机则适合跨越大陆的长途旅行。
**后端架构的选择也是如此。**
从互联网诞生到现在,后端架构经历了多次重大变革。每一次变革都不是为了"追新潮",而是为了解决当时面临的特定问题:
| 年代 | 核心问题 | 架构演进 |
| ----- | ------------------------ | ------------------- |
| 1990s | 如何把网站跑起来 | 物理服务器 |
| 2000s | 代码越来越乱怎么维护 | 单体架构 + MVC |
| 2010s | 系统太大怎么扩展和协作 | 微服务 + 容器化 |
| 2020s | 如何降低运维成本和复杂性 | Serverless + 云原生 |
::: tip 📊 从表格中你能看到什么?
让我们逐行解读这张表:
**1990s → 2000s**:从"能跑就行"到"需要维护"。网站从静态页面变成动态应用,代码量激增,需要更好的组织方式。
**2000s → 2010s**:从"单机"到"分布式"。用户量爆炸式增长,单台服务器扛不住了,需要拆分系统,水平扩展。
**2010s → 2020s**:从"自己运维"到"云服务"。容器和微服务虽然强大,但运维成本太高,Serverless 让开发者只关注业务逻辑。
**核心启示**:架构演进不是技术选型的游戏,而是**解决实际问题**的过程。每个阶段都有其适用的场景,没有"最好的架构",只有"最适合的架构"。
:::
**了解架构演进的意义在于:**
1. **避免重复造轮子**:很多"新"概念其实早在几十年前就有雏形,了解历史能让你站在巨人的肩膀上
2. **做出合理的技术选型**:没有最好的架构,只有最适合当前阶段的架构
3. **理解技术背后的权衡**:每一次架构演进都是在**开发效率**、**系统性能**、**运维复杂度**之间做取舍
4. **预判技术趋势**:历史总是押韵的,理解过去的演进规律有助于把握未来方向
<EvolutionIntroDemo />
---
## 0. 引言:为什么要了解架构演进?
## 2. 物理服务器时代 (1990s)
想象一下,你正在规划一次长途旅行。你可以选择骑自行车、开私家车、坐高铁,或者乘飞机。每种方式都有其适用的场景:自行车适合短距离且想锻炼身体的情况,飞机则适合跨越大陆的长途旅行。
### 2.1 什么是物理服务器?
**后端架构的选择也是如此。**
在互联网刚起步时,后端就是一台放在机房里的**物理服务器**(一台真实的电脑)。
从互联网诞生到现在,后端架构经历了多次重大变革。每一次变革都不是为了"追新潮",而是为了解决当时面临的特定问题:
::: tip 💡 通俗解释
**物理服务器**就像你家里的台式机,但它:
| 年代 | 核心问题 | 架构演进 |
|------|---------|---------|
| 1990s | 如何把网站跑起来 | 物理服务器 |
| 2000s | 代码越来越乱怎么维护 | 单体架构 + MVC |
| 2010s | 系统太大怎么扩展和协作 | 微服务 + 容器化 |
| 2020s | 如何降低运维成本和复杂性 | Serverless + 云原生 |
- 7×24小时不关机
- 放在专门的数据中心(有空调、UPS电源、消防系统)
- 有更快的网络带宽(企业级光纤)
- 有固定的公网IP地址(全世界都能访问)
**了解架构演进的意义在于:**
这就好比你家 vs 餐厅:你家只是偶尔做饭,餐厅则是专业厨房,全天候营业,设备更专业。
:::
1. **避免重复造轮子**:很多"新"概念其实早在几十年前就有雏形,了解历史能让你站在巨人的肩膀上
2. **做出合理的技术选型**:没有最好的架构,只有最适合当前阶段的架构
3. **理解技术背后的权衡**:每一次架构演进都是在**开发效率**、**系统性能**、**运维复杂度**之间做取舍
4. **预判技术趋势**:历史总是押韵的,理解过去的演进规律有助于把握未来方向
### 2.2 核心特点
<ArchitectureComparisonDemo />
- **单机部署**:所有应用运行在一台物理机上
- **手动运维**:需要人工上架、布线、安装系统
- **垂直扩展**:性能不够时只能买更强的机器
---
::: details 🔧 垂直扩展 vs 水平扩展
**垂直扩展**(Scale Up):升级单台服务器的配置(更多CPU、更大内存、更快硬盘)。
## 1. 物理服务器时代 (1990s)
**水平扩展**(Scale Out):增加更多服务器,让它们一起工作。
在互联网刚起步时,后端就是一台放在机房里的**物理服务器**(一台真实的电脑)。
**比喻**:
- 垂直扩展:把小餐厅改成大餐厅,装修更豪华,但只有一个厨师
- 水平扩展:开连锁店,每个店规模不大,但有100家分店
**优缺点**:
- 垂直扩展简单,但有上限(顶级服务器很贵,且有限制)
- 水平扩展理论上无限,但需要解决数据一致性问题
:::
### 2.3 痛点
- **慢**:每次改代码都要手动上传,然后重启服务器
- **贵**:扩容只能买更大的机器(垂直扩展)
- **难扩展**:一台机器顶住所有请求,CPU满载时就只能排队
<PhysicalServerDemo />
### 1.1 核心特
### 2.4 物理服务器时代的优缺
- **单机部署**:所有应用运行在一台物理机上
- **手动运维**:需要人工上架、布线、安装系统
- **垂直扩展**:性能不够时只能买更强的机器
| 维度 | 评价 |
| ------------ | ------------------------------------------------------------ |
| **优点** | 完全掌控硬件,性能可预测;没有虚拟化开销;数据物理隔离,安全性高 |
| **缺点** | 采购周期长(数周);前期投入大(CapEx);资源利用率低;扩容困难 |
| **适用场景** | 金融核心系统、政府涉密系统、对数据主权有严格要求的场景 |
### 1.2 痛点
::: tip 💡 CapEx vs OpEx
**CapEx**(Capital Expenditure):资本性支出,一次性投入大量资金购买硬件。
- **慢**:每次改代码都要手动上传,然后重启服务器
- **贵**:扩容只能买更大的机器(垂直扩展)
- **难扩展**:一台机器顶住所有请求,CPU 满载时就只能排队
**OpEx**(Operating Expenditure):运营性支出,按使用量付费(如云服务器)。
### 1.3 扩展策略
**比喻**:
<ScalingStrategyDemo />
- CapEx:买房,一次性付几百万,之后每月只需交物业费
- OpEx:租房,每月交房租,不用一次性掏大钱
### 1.4 物理服务器时代的优缺点
| 维度 | 评价 |
|------|------|
| **优点** | 完全掌控硬件,性能可预测;没有虚拟化开销;数据物理隔离,安全性高 |
| **缺点** | 采购周期长(数周);前期投入大(CapEx);资源利用率低;扩容困难 |
| **适用场景** | 金融核心系统、政府涉密系统、对数据主权有严格要求的场景 |
**云时代**的启示:Serverless 和云服务让更多公司从 CapEx 转向 OpEx,降低创业门槛。
:::
---
## 2. 单体架构时代 (2000s)
## 3. 单体架构时代 (2000s)
随着框架的出现(Rails / Django / Spring),大家把所有功能都塞进一个应用里。
### 3.1 什么是单体架构?
随着框架的出现(Rails / Django / Spring),大家把所有功能都塞进一个应用里。
::: tip 💡 通俗解释
**单体架构**(Monolith)就像一个超级商场:
- 服装区、食品区、电器区都在同一栋楼里
- 所有员工在一个管理系统里工作
- 如果整栋楼停电,所有区域都停止营业
对比微服务就像商业街:每家店独立运营,一家店关门不影响其他店。
:::
<MonolithDemo />
### 2.1 核心特点
### 3.2 核心特点
- **单一代码库**所有功能模块在同一个项目中
- **共享数据库**所有模块共用同一个数据库
- **统一部署**整个应用作为一个整体打包部署
- **单一代码库**:所有功能模块在同一个项目中
- **共享数据库**:所有模块共用同一个数据库
- **统一部署**:整个应用作为一个整体打包部署
### 2.2 优点
### 3.3 优点
- **开发简单**一个项目搞定所有功能
- **部署方便**把一个大包扔到服务器上就行
- **调试容易**本地启动就能调试所有功能
- **开发简单**:一个项目搞定所有功能
- **部署方便**:把一个大包扔到服务器上就行
- **调试容易**:本地启动就能调试所有功能
### 2.3 痛点雪崩效应
### 3.4 痛点:雪崩效应
想象一下如果"切菜"的师傅不小心切到了手代码出了 Bug),整个后厨都要停下来处理伤口导致所有客人都吃不上饭。
想象一下,如果"切菜"的师傅不小心切到了手(代码出了Bug),整个后厨都要停下来处理伤口,导致所有客人都吃不上饭。
这就是单体架构最大的风险**隔离性差**。
这就是单体架构最大的风险:**隔离性差**。
### 2.4 单体架构的优缺点与适用场景
::: details 🚨 真实的雪崩案例
某电商公司双十一大促:
| 维度 | 评价 |
|------|------|
| **优点** | 开发简单,无需考虑分布式复杂性;调试方便,本地启动即可调试全功能;部署简单,一个包即可运行;事务管理容易,单机数据库即可保证 ACID |
| **缺点** | 代码耦合度高,随着业务增长代码膨胀;技术栈单一,难以局部升级;扩展困难,只能整体扩容;故障隔离差,一个模块故障影响全局;团队协作效率低,多人改同一套代码 |
| **适用场景** | 初创公司 MVP 验证、小型团队(<10人)、业务相对简单、对交付速度要求高于扩展性的场景 |
| **不适用场景** | 大型团队并行开发、需要频繁发布不同模块、某些模块需要独立扩容的场景 |
- 订单服务因为某个商品的价格计算错误,抛出异常
- 异常没有被正确捕获,导致线程池耗尽
- 所有后续请求(包括商品浏览、搜索、用户登录)都被阻塞
- 整个网站彻底瘫痪,持续1小时
### 2.5 部署流程演进
**如果用微服务**:
<DeploymentFlowDemo />
- 订单服务挂了,但商品浏览、搜索、用户登录仍然可用
- 用户至少可以继续浏览商品,损失降到最低
:::
### 2.6 单体架构的技术栈
### 3.5 单体架构的优缺点与适用场景
在单体架构时代,开发者通常使用以下技术栈:
| 维度 | 评价 |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **优点** | 开发简单,无需考虑分布式复杂性;调试方便,本地启动即可调试全功能;部署简单,一个包即可运行;事务管理容易,单机数据库即可保证ACID |
| **缺点** | 代码耦合度高,随着业务增长代码膨胀;技术栈单一,难以局部升级;扩展困难,只能整体扩容;故障隔离差,一个模块故障影响全局;团队协作效率低,多人改同一套代码 |
| **适用场景** | 初创公司MVP验证、小型团队(<10人)、业务相对简单、对交付速度要求高于扩展性的场景 |
| **不适用场景** | 大型团队并行开发、需要频繁发布不同模块、某些模块需要独立扩容的场景 |
| 语言/框架 | 特点 | 代表企业 |
|---------|------|---------|
| **Java + Spring** | 企业级开发首选,生态完善 | 阿里巴巴、京东 |
| **PHP + Laravel/ThinkPHP** | 快速开发,适合中小型项目 | 早期 Facebook、微博 |
| **Python + Django/Flask** | 开发效率高,适合快速原型 | Instagram、Pinterest |
| **Ruby on Rails** | 约定优于配置,初创公司最爱 | GitHub、Twitter(早期) |
| **Node.js + Express** | 前后端统一语言,I/O 密集型场景 | Netflix、Uber |
::: tip 🎯 初学者建议
如果你正在学习后端开发,**强烈建议从单体架构开始**:
1. **先学会走路**:理解HTTP、数据库、基本的MVC架构
2. **再考虑跑步**:当项目真的遇到扩展性问题,再考虑微服务
3. **避免过度设计**:很多公司的"微服务"其实是"分布式单体",更难维护
**学习路径**:
- 阶段1:用 Spring Boot / Django / Rails 写一个完整的单体应用
- 阶段2:遇到性能瓶颈时,尝试拆分出1-2个服务
- 阶段3:当团队规模>50人,系统真的复杂了,再全面微服务化
:::
### 3.6 单体架构的技术栈
| 语言/框架 | 特点 | 代表企业 |
| -------------------------- | ---------------------------- | --------------------- |
| **Java + Spring** | 企业级开发首选,生态完善 | 阿里巴巴、京东 |
| **PHP + Laravel/ThinkPHP** | 快速开发,适合中小型项目 | 早期 Facebook、微博 |
| **Python + Django/Flask** | 开发效率高,适合快速原型 | Instagram、Pinterest |
| **Ruby on Rails** | 约定优于配置,初创公司最爱 | GitHub、Twitter(早期) |
| **Node.js + Express** | 前后端统一语言,I/O密集型场景 | Netflix、Uber |
---
## 3. 容器化与微服务 (2010s)
## 4. 容器化与微服务 (2010s)
### 3.1 Docker 容器化
### 4.1 为什么需要微服务?
单体架构的痛点在2010年代集中爆发:
- **代码太庞大**:一个项目几百万行代码,新人入职要花一个月才能看懂
- **部署太慢**:构建一次要30分钟,发布一次要小心翼翼
- **协作太难**:100个开发者改同一个项目,代码冲突每天发生
- **扩展太贵**:只需要扩展"聊天服务",却要复制整个应用
**微服务的核心思想**:把大应用拆成多个小服务,每个服务:
- 独立开发、独立部署
- 有自己的数据库
- 通过API通信
<ContainerDockerDemo />
Docker 就像是**集装箱**,它把每个小服务连同它的锅碗瓢盆(依赖库)一起打包。
::: tip 💡 Docker是什么?
**Docker**就像是"集装箱":
无论运到哪里(哪台服务器),打开集装箱就能直接开工,不用再重新安装环境
- 每个集装箱里有独立的货物(代码 + 依赖库 + 运行环境)
- 无论运到哪里(哪台服务器),打开集装箱就能直接开工
- 不用担心"我这台机器没有Python 3.9"、"那个机器缺少某个库"
### 3.2 技术栈时间线
**比喻**:
- 没有 Docker:每次搬家,要把家具、电器、衣服一件件搬上卡车,到了新家再一件件摆好
- 有 Docker:所有东西打包进集装箱,卡车直接运走,到了新家放下就能用
**核心价值**:"一次构建,到处运行"。
:::
### 4.2 技术栈时间线
<TechStackTimelineDemo />
### 3.3 微服务架构
### 4.3 微服务架构
<MicroservicesDemo />
为了解决单体的问题,我们把大厨房拆成了很多个小厨房(服务):
为了解决单体的问题,我们把大厨房拆成了很多个小厨房(服务):
- 专门负责用户的服务
- 专门负责订单的服务
- 专门负责支付的服务
### 3.4 Kubernetes 编排
<MicroservicesDemo />
### 4.4 Kubernetes 编排
当集装箱数量到达成百上千,就需要一个"港口调度系统":
- **Kubernetes (K8s)**:负责把容器安排到合适的机器上(调度、扩缩容、滚动更新)
- **Service Mesh**:负责服务之间的交通规则(熔断、限流、重试、可观测)
<KubernetesDemo />
当集装箱数量到达成百上千,就需要一个"港口调度系统":
::: tip 💡 什么是"编排"?
**编排**(Orchestration)是指自动管理大量容器的系统。
- **Kubernetes (K8s)**:负责把容器安排到合适的机器上(调度、扩缩容、滚动更新)
- **Service Mesh**:负责服务之间的交通规则(熔断、限流、重试、可观测)
**比喻**:
**关键点**:微服务不是"拆开就好",真正的难点在于**治理和运维**。
- 没有 K8s:你手动管理100个容器,哪个挂了要手动重启,哪个流量大了要手动加机器
- 有 K8s:你告诉它"我要这个服务一直有10个实例运行",它会自动完成:
- 哪台服务器资源充足,就把容器调度到那里
- 容器挂了,自动重启
- 流量大了,自动扩容到20个实例
- 更新代码时,滚动更新(先停1个旧实例,启动1个新实例,逐个替换)
### 3.5 微服务与容器化的优缺点
**关键点**:微服务不是"拆开就好",真正的难点在于**治理和运维**。
:::
| 维度 | 评价 |
|------|------|
| **优点** | 服务独立部署,技术栈可异构;故障隔离,单个服务崩溃不影响全局;按需扩展,热点服务单独扩容;团队协作友好,不同团队负责不同服务;代码库更小,易于理解和维护 |
| **缺点** | 分布式复杂性高(网络延迟、分布式事务、服务发现);运维成本高,需要专业的 DevOps 团队;调试困难,问题可能需要跨多个服务追踪;数据一致性难以保证;部署和监控基础设施要求复杂 |
| **适用场景** | 大型团队(>50人)、业务复杂需要分模块独立演进、某些模块需要独立扩容、需要多语言技术栈、对可用性要求高的系统 |
| **不适用场景** | 小型团队、业务简单、流量小且稳定、没有专业运维团队的情况 |
### 4.5 微服务与容器化的优缺点
### 3.6 微服务技术栈
| 维度 | 评价 |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **优点** | 服务独立部署,技术栈可异构;故障隔离,单个服务崩溃不影响全局;按需扩展,热点服务单独扩容;团队协作友好,不同团队负责不同服务;代码库更小,易于理解和维护 |
| **缺点** | 分布式复杂性高(网络延迟、分布式事务、服务发现);运维成本高,需要专业的DevOps团队;调试困难,问题可能需要跨多个服务追踪;数据一致性难以保证;部署和监控基础设施要求复杂 |
| **适用场景** | 大型团队(>50人)、业务复杂需要分模块独立演进、某些模块需要独立扩容、需要多语言技术栈、对可用性要求高的系统 |
| **不适用场景** | 小型团队、业务简单、流量小且稳定、没有专业运维团队的情况 |
| 类别 | 技术/工具 | 作用 |
|------|---------|------|
| **容器化** | Docker, containerd | 应用打包与隔离 |
| **编排调度** | Kubernetes, Docker Swarm | 容器管理与自动扩缩容 |
| **服务发现** | Consul, etcd, ZooKeeper | 服务注册与发现 |
| **API 网关** | Kong, Zuul, Envoy | 统一入口、路由、限流 |
| **配置中心** | Apollo, Nacos, Spring Cloud Config | 集中配置管理 |
| **监控告警** | Prometheus, Grafana, ELK | 指标监控与日志分析 |
| **链路追踪** | Jaeger, Zipkin, SkyWalking | 分布式请求追踪 |
| **服务网格** | Istio, Linkerd | 流量治理与安全 |
::: details ⚠️ 微服务的陷阱
**陷阱1:分布式单体**
拆了10个微服务,但它们之间紧密耦合:
- 服务A调用服务B,服务B调用服务C,服务C又调用服务A
- 改一个功能,要同时改5个服务
- 部署时,必须按顺序依次部署,否则系统报错
**这比单体更糟糕**:你拥有了单体的复杂性,又没有享受到微服务的独立部署好处。
**陷阱2:过度拆分**
把只有100行代码的功能也拆成一个独立服务:
- 10个服务,每个只有100行代码
- 服务间通信的开销(网络序列化/反序列化)比实际业务逻辑还重
- 运维成本爆炸:要部署、监控、日志收集10个服务
**正确做法**:从功能内聚的角度拆分,一个微服务应该是一个完整的业务能力(如"订单服务",而不是"订单创建服务"、"订单查询服务")。
:::
### 4.6 微服务技术栈
| 类别 | 技术/工具 | 作用 |
| ------------ | ---------------------------------- | -------------------- |
| **容器化** | Docker, containerd | 应用打包与隔离 |
| **编排调度** | Kubernetes, Docker Swarm | 容器管理与自动扩缩容 |
| **服务发现** | Consul, etcd, ZooKeeper | 服务注册与发现 |
| **API网关** | Kong, Zuul, Envoy | 统一入口、路由、限流 |
| **配置中心** | Apollo, Nacos, Spring Cloud Config | 集中配置管理 |
| **监控告警** | Prometheus, Grafana, ELK | 指标监控与日志分析 |
| **链路追踪** | Jaeger, Zipkin, SkyWalking | 分布式请求追踪 |
| **服务网格** | Istio, Linkerd | 流量治理与安全 |
---
## 4. Serverless 与云原生时代 (2020s+)
## 5. Serverless 与云原生时代 (2020s+)
微服务虽然好,但维护几十个小厨房还是很累。你需要担心:
### 5.1 为什么需要 Serverless?
- 厨房够不够大?(服务器扩容)
- 停电了怎么办?(高可用)
- 容器太多怎么管?(运维成本)
微服务虽然好,但维护几十个小厨房还是很累。你需要担心:
- 厨房够不够大?(服务器扩容)
- 停电了怎么办?(高可用)
- 容器太多怎么管?(运维成本)
<ServerlessDemo />
### 4.1 什么是 Serverless
::: tip 💡 Serverless 不是真的"没有服务器"
**Serverless**的意思是"你不需要管理服务器",而不是真的没有服务器。
Serverless 并不是"没有服务器",而是**"你不需要管理服务器"**
**比喻**:
就像你现在不想自己做饭,也不想开饭馆,而是直接叫**外卖**。
- **物理服务器时代**:你买地、盖房、装修、雇厨师、买食材...全部自己来
- **云服务器时代**:你租一个已经装修好的餐厅,但自己雇厨师、管理运营
- **Serverless时代**:你只需要设计菜单,云端有共享厨房,有专业厨师,你下单他们做,按次付费
- 你只需要写代码(下单)
- 云厂商(美团)负责准备机器、运行代码、自动扩容
- **按次付费**:代码跑了 100 毫秒,就收 100 毫秒的钱。没人访问就不收钱
**核心变化**:
### 4.2 适用场景
- 以前:买服务器 → 配环境 → 部署代码 → 监控 → 扩容 → 维护
- 现在:写代码 → 上传 → 按使用量付费
Serverless 特别适合:
**就像外卖**:你不需要厨房,只需要设计菜单,有人帮你做。
:::
- **潮汐流量**:比如外卖软件,中午流量大,半夜没人。Serverless 会自动在中午为你分配 1000 台机器,半夜缩减到 0 台
- **事件驱动**:比如"用户上传图片后,自动压缩图片"
- **快速验证**:小团队、MVP、黑客松项目
### 5.2 什么是 Serverless?
### 4.3 BaaS 组合拳
**Serverless = FaaS + BaaS**
Serverless 的真正力量来自于 **BaaS (Backend as a Service)**
**FaaS**(Function as a Service,函数即服务):
- 登录 -> Auth0 / Supabase Auth
- 支付 -> Stripe
- 数据库 -> Supabase / Firebase / DynamoDB
- 消息 -> Kafka / SQS
- 你只写函数(如"用户注册时发送欢迎邮件")
- 云厂商负责运行这个函数,自动扩缩容
- 典型代表:AWS Lambda、阿里云函数计算
**关键点**Serverless 让后端越来越像"搭积木"。
**BaaS**(Backend as a Service,后端即服务):
### 4.4 Serverless 与云原生的优缺点
- 登录 → Auth0 / Supabase Auth
- 支付 → Stripe
- 数据库 → Supabase / Firebase / DynamoDB
- 消息 → Kafka / SQS
| 维度 | 评价 |
|------|------|
| **优点** | 零运维成本,开发者只需关注业务代码;自动扩缩容,完美应对流量峰值;按需付费,无流量时成本接近零;快速上线,几分钟即可部署全球;高可用内置,云服务自动处理故障转移 |
| **缺点** | 冷启动延迟(几百毫秒到数秒);运行时长限制(通常5-15分钟);调试困难,本地难以完全模拟云环境;供应商锁定风险;不适合长时间运行或计算密集型任务;成本在高频持续流量下可能反超传统方案 |
| **适用场景** | 事件驱动处理(图片处理、消息通知);潮汐流量应用(活动页、促销);快速原型验证和MVP;低频API或后台任务;无专职运维团队的小团队 |
| **不适用场景** | 需要持续低延迟的应用;长时间计算任务;对冷启动敏感的场景(高频交易);需要精细控制底层基础设施的场景 |
::: tip 🎯 Serverless 适用场景
**最佳场景**:
### 4.5 Serverless 技术栈与平
1. **潮汐流量**:外卖软件,中午流量大,半夜没人。Serverless会自动在中午分配1000台机器,半夜缩减到0
2. **事件驱动**:"用户上传图片后,自动压缩图片"
3. **快速验证**:小团队、MVP、黑客松项目
| 类别 | 技术/平台 | 特点 |
|------|---------|------|
| **FaaS 平台** | AWS Lambda | 最早的 FaaS 服务,生态最成熟 |
| | Azure Functions | 微软云集成度高,.NET 友好 |
| | Google Cloud Functions | 与 GCP 服务深度集成 |
| | 阿里云函数计算 | 国内生态完善,冷启动优化好 |
| | 腾讯云云函数 | 与微信生态整合 |
| | Vercel/Netlify Functions | 前端开发者友好,边缘部署 |
| **BaaS 服务** | Firebase | Google 的移动端后端方案 |
| | Supabase | PostgreSQL 的 Firebase 开源替代 |
| | AWS Amplify | AWS 的移动和 Web 应用开发平台 |
| **部署工具** | Serverless Framework | 多云部署,社区活跃 |
| | Terraform | 基础设施即代码 |
| | Pulumi | 用编程语言定义基础设施 |
**不适合场景**:
1. **长时间运行的任务**:视频转码(可能跑1小时,函数最大执行时间通常只有15分钟)
2. **需要低延迟的应用**:高频交易(冷启动延迟可能几十毫秒到几秒)
3. **需要精细控制底层**:操作系统内核调优、GPU直接访问
:::
### 5.3 Serverless 与云原生的优缺点
| 维度 | 评价 |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **优点** | 零运维成本,开发者只需关注业务代码;自动扩缩容,完美应对流量峰值;按需付费,无流量时成本接近零;快速上线,几分钟即可部署全球;高可用内置,云服务自动处理故障转移 |
| **缺点** | 冷启动延迟(几百毫秒到数秒);运行时长限制(通常5-15分钟);调试困难,本地难以完全模拟云环境;供应商锁定风险;不适合长时间运行或计算密集型任务;成本在高频持续流量下可能反超传统方案 |
| **适用场景** | 事件驱动处理(图片处理、消息通知);潮汐流量应用(活动页、促销);快速原型验证和MVP;低频API或后台任务;无专职运维团队的小团队 |
| **不适用场景** | 需要持续低延迟的应用;长时间计算任务;对冷启动敏感的场景(高频交易);需要精细控制底层基础设施的场景 |
::: details 💰 成本对比:何时Serverless更贵?
**场景1:低频访问**
- 传统服务器:每月$20(不管有没有人访问)
- Serverless:100万次请求 × $0.0002/次 = $20(仅在有流量时付费)
- **结论**:低频场景,Serverless更省钱
**场景2:高频持续访问**
- 传统服务器:每月$20
- Serverless:1亿次请求 × $0.0002/次 = $20,000
- **结论**:高频持续场景,传统服务器更省钱
**场景3:潮汐流量**
- 传统服务器:为了应对峰值,需要$100/月的服务器(平时资源利用率只有10%)
- Serverless:峰值时$20,平时几乎$0
- **结论**:潮汐流量场景,Serverless节省成本
**启示**:不要盲目上Serverless,要根据实际流量特征做成本测算。
:::
### 5.4 Serverless 技术栈与平台
| 类别 | 技术/平台 | 特点 |
| ------------ | ------------------------ | ---------------------------- |
| **FaaS平台** | AWS Lambda | 最早的FaaS服务,生态最成熟 |
| | Azure Functions | 微软云集成度高,.NET友好 |
| | Google Cloud Functions | 与GCP服务深度集成 |
| | 阿里云函数计算 | 国内生态完善,冷启动优化好 |
| | 腾讯云云函数 | 与微信生态整合 |
| | Vercel/Netlify Functions | 前端开发者友好,边缘部署 |
| **BaaS服务** | Firebase | Google的移动端后端方案 |
| | Supabase | PostgreSQL的Firebase开源替代 |
| | AWS Amplify | AWS的移动和Web应用开发平台 |
| **部署工具** | Serverless Framework | 多云部署,社区活跃 |
| | Terraform | 基础设施即代码 |
| | Pulumi | 用编程语言定义基础设施 |
---
## 5. 各架构阶段对比与选型指南
## 6. 各架构阶段对比与选型指南
### 5.1 架构演进全景对比
### 6.1 架构演进全景对比
| 维度 | 物理服务器 | 单体架构 | 微服务+容器 | Serverless |
|------|-----------|---------|------------|-------------|
| **团队规模** | 1-5人 | 5-50人 | 50-500人 | 1-20人 |
| **部署复杂度** | 极高 | 低 | 极高 | 极低 |
| **运维成本** | 高 | 中 | 很高 | 低 |
| **扩展性** | 差 | 垂直扩展有限 | 水平扩展优秀 | 自动扩展 |
| **技术栈灵活性** | 无 | 单一 | 多样化 | 受限 |
| **冷启动** | 无 | 无 | 容器启动时间 | 有延迟 |
| **适用场景** | 遗留系统、特殊合规要求 | 初创公司、业务简单 | 大型互联网公司、复杂业务 | 快速验证、事件驱动 |
<ArchitectureComparisonDemo />
### 5.2 技术选型决策树
| 维度 | 物理服务器 | 单体架构 | 微服务+容器 | Serverless |
| ---------------- | ---------------------- | ------------------ | ------------------------ | ------------------ |
| **团队规模** | 1-5人 | 5-50人 | 50-500人 | 1-20人 |
| **部署复杂度** | 极高 | 低 | 极高 | 极低 |
| **运维成本** | 高 | 中 | 很高 | 低 |
| **扩展性** | 差 | 垂直扩展有限 | 水平扩展优秀 | 自动扩展 |
| **技术栈灵活性** | 无 | 单一 | 多样化 | 受限 |
| **冷启动** | 无 | 无 | 容器启动时间 | 有延迟 |
| **适用场景** | 遗留系统、特殊合规要求 | 初创公司、业务简单 | 大型互联网公司、复杂业务 | 快速验证、事件驱动 |
### 6.2 技术选型决策树
```
开始选型
├─ 团队有专业运维人员
├─ 团队有专业运维人员?
│ ├─ 是 → 考虑微服务或物理机
│ └─ 否 → 继续判断
├─ 需要快速上线验证想法
├─ 需要快速上线验证想法?
│ ├─ 是 → Serverless 或单体
│ └─ 否 → 继续判断
├─ 团队规模 > 50人
├─ 团队规模 > 50人?
│ ├─ 是 → 考虑微服务
│ └─ 否 → 继续判断
├─ 流量有明显峰谷特征
├─ 流量有明显峰谷特征?
│ ├─ 是 → Serverless
│ └─ 否 → 单体架构推荐初创
│ └─ 否 → 单体架构(推荐初创)
└─ 特殊要求合规、遗留系统)?
└─ 特殊要求(合规、遗留系统)?
└─ 是 → 物理服务器
```
### 5.3 不同场景下的推荐架构
::: tip 🎯 初学者选型建议
**如果你是个开发者或小团队:**
#### 场景一:独立开发者/兼职项目
- **推荐架构**Serverless (Vercel/Netlify) 或 单体应用
- **理由**:几乎零运维成本,按需付费,快速上线
- **示例技术栈**Next.js + Vercel + Supabase
1. **阶段0 (学习)**:本地跑单体应用,理解HTTP、数据库、基本架构
2. **阶段1 (MVP)**:部署单体应用到云服务器(如阿里云ECS、AWS EC2)
3. **阶段2 (增长)**:当团队>10人、业务变复杂,考虑拆分出1-2个微服务
4. **阶段3 (成熟)**:当团队>50人、流量百万级,全面微服务化
#### 场景二:初创公司 MVP 验证
- **推荐架构**:单体架构 + 云服务器
- **理由**:开发速度快,团队可以专注于业务逻辑而非基础设施
- **示例技术栈**Spring Boot / Django / Rails + RDS + ECS
**关键原则**:不要一开始就上微服务,那是"过早优化"。让架构随业务成长而演进。
:::
#### 场景三:成长型公司(10-50人团队)
- **推荐架构**:模块化单体 或 轻量级微服务
- **理由**:开始面临代码耦合问题,但还不需要完整的微服务复杂度
- **示例技术栈**Spring Cloud / Go Micro + Kubernetes
### 6.3 不同场景下的推荐架构
#### 场景四:大型互联网公司
- **推荐架构**:微服务 + 服务网格 + 中台架构
- **理由**:团队规模大,业务复杂,需要独立的发布节奏和技术栈
- **示例技术栈**:自研 RPC 框架 + Istio + 自建 PaaS 平台
#### 场景一:独立开发者/兼职项目
#### 场景五:事件驱动/潮汐流量应用
- **推荐架构**Serverless + 事件总线
- **理由**:流量波动大,需要极致的成本优化和自动扩缩容
- **示例技术栈**AWS Lambda + API Gateway + EventBridge
- **推荐架构**:Serverless (Vercel/Netlify) 或 单体应用
- **理由**:几乎零运维成本,按需付费,快速上线
- **示例技术栈**:Next.js + Vercel + Supabase
#### 场景二:初创公司MVP验证
- **推荐架构**:单体架构 + 云服务器
- **理由**:开发速度快,团队可以专注于业务逻辑而非基础设施
- **示例技术栈**:Spring Boot / Django / Rails + RDS + ECS
#### 场景三:成长型公司(10-50人团队)
- **推荐架构**:模块化单体 或 轻量级微服务
- **理由**:开始面临代码耦合问题,但还不需要完整的微服务复杂度
- **示例技术栈**:Spring Cloud / Go Micro + Kubernetes
#### 场景四:大型互联网公司
- **推荐架构**:微服务 + 服务网格 + 中台架构
- **理由**:团队规模大,业务复杂,需要独立的发布节奏和技术栈
- **示例技术栈**:自研RPC框架 + Istio + 自建PaaS平台
#### 场景五:事件驱动/潮汐流量应用
- **推荐架构**:Serverless + 事件总线
- **理由**:流量波动大,需要极致的成本优化和自动扩缩容
- **示例技术栈**:AWS Lambda + API Gateway + EventBridge
---
## 6. 总结与学习路线
## 7. 总结与学习路线
后端架构的演进,本质上是在做**加法**和**减法**:
### 7.1 核心要点
| 时代 | 架构 | 开发者要做的事 | 运维要做的事 |
| :--- | :----- | :--------------- | :-------------------- |
| **物理时代** | 单机 | 写脚本、手动部署 | 维护机房与硬件 |
| **单体时代** | 一整块 | 写所有业务逻辑 | 维护几台大服务器 |
| **微服务时代** | 拆分 | 关注单一业务 | 维护 K8s 集群 (很累!) |
| **Serverless** | 函数 | 只写核心函数 | 喝茶 (云厂商全包了) |
后端架构的演进,本质上是在做**加法**和**减法**:
**下一步建议**
| 时代 | 架构 | 开发者要做的事 | 运维要做的事 |
| :------------- | :----- | :--------------- | :----------------- |
| **物理时代** | 单机 | 写脚本、手动部署 | 维护机房与硬件 |
| **单体时代** | 一整块 | 写所有业务逻辑 | 维护几台大服务器 |
| **微服务时代** | 拆分 | 关注单一业务 | 维护K8s集群(很累!) |
| **Serverless** | 函数 | 只写核心函数 | 喝茶(云厂商全包了) |
- 想打基础:学会 HTTP、数据库、缓存、消息队列
- 想上手实践:用 Docker 跑一个小项目,再部署到云端
- 想更专业:了解 K8s、监控体系、CI/CD 流水线
**关键洞察**:
未来的后端开发,将越来越像"搭积木"——你只需要关注**业务逻辑**,底层的脏活累活,全部交给云。
- 架构演进不是"新技术取代旧技术",而是**适用场景的变化**
- 没有银弹,每个架构都有其适用的边界
- 选择架构要考虑:团队规模、业务复杂度、流量特征、运维能力
### 6.2 学习路线建议
### 7.2 学习路线建议
根据你的职业阶段推荐以下学习路径
根据你的职业阶段,推荐以下学习路径:
#### 阶段一打好基础0-1年
**目标**:理解后端核心概念,能独立开发单体应用
#### 阶段一:打好基础(0-1年)
- 掌握一门后端语言(Java/Python/Go 任选其一)
- 学习 HTTP 协议和 RESTful API 设计
- 掌握关系型数据库(MySQL/PostgreSQL
- 了解缓存基础(Redis
- 学习 Git 和基础 Linux 命令
- **实践项目**:用单体架构完成一个 CRUD 应用(如博客系统、待办事项)
**目标**:理解后端核心概念,能独立开发单体应用
#### 阶段二:扩展能力(1-3年)
**目标**:理解分布式系统,能参与微服务开发
- 掌握一门后端语言(Java/Python/Go任选其一)
- 学习HTTP协议和RESTful API设计
- 掌握关系型数据库(MySQL/PostgreSQL)
- 了解缓存基础(Redis)
- 学习Git和基础Linux命令
- **实践项目**:用单体架构完成一个CRUD应用(如博客系统、待办事项)
#### 阶段二:扩展能力(1-3年)
**目标**:理解分布式系统,能参与微服务开发
- 深入学习微服务架构和拆分策略
- 掌握 DockerKubernetes 基础
- 学习消息队列Kafka/RabbitMQ
- 掌握DockerKubernetes基础
- 学习消息队列(Kafka/RabbitMQ)
- 了解分布式事务和一致性
- 掌握监控和日志Prometheus/ELK
- **实践项目**将单体应用拆分为 3-5 个微服务使用 Docker 部署
- 掌握监控和日志(Prometheus/ELK)
- **实践项目**:将单体应用拆分为3-5个微服务,使用Docker部署
#### 阶段三专业深化3-5年
**目标**:能设计大型系统,具备技术选型能力
#### 阶段三:专业深化(3-5年)
- 深入理解云原生架构(Service Mesh、Serverless
**目标**:能设计大型系统,具备技术选型能力
- 深入理解云原生架构(Service Mesh、Serverless)
- 掌握容量规划和性能调优
- 了解多活架构和灾备设计
- 学习 DDD领域驱动设计
- 学习DDD(领域驱动设计)
- 培养技术判断力和架构思维
- **实践项目**设计一个支持百万级用户的系统架构包含高可用、弹性伸缩等方案
- **实践项目**:设计一个支持百万级用户的系统架构,包含高可用、弹性伸缩等方案
#### 持续学习资源推荐
### 7.3 持续学习资源推荐
**书籍**
- 《设计数据密集型应用》(DDIA)- 分布式系统必读
**书籍**:
- 《设计数据密集型应用》(DDIA)- 分布式系统必读
- 《云原生模式》
- 《微服务设计》
- 《领域驱动设计》
**在线资源**
**在线资源**:
- AWS/Azure/阿里云官方架构文档
- CNCF云原生计算基金会项目文档
- 各大公司技术博客Netflix Tech Blog、阿里技术公众号等
### 6.3 架构选型的核心原则
记住以下原则,帮助你在实际工作中做出正确的选择:
1. **没有银弹**:不存在最好的架构,只有最适合当前场景的架构
2. **演进优于完美**:先让系统跑起来,再逐步优化,不要过度设计
3. **团队能力优先**:选择团队熟悉和能驾驭的技术,而不是最新最酷的技术
4. **成本意识**:计算总体拥有成本(TCO),包括开发、运维、培训等
5. **可回退性**:设计时考虑回退方案,微服务可以合并回单体,但很难拆分
- CNCF(云原生计算基金会)项目文档
- 各大公司技术博客(Netflix Tech Blog、阿里技术公众号等)
---
## 7. 名词速查表 (Glossary)
## 8. 名词速查表(Glossary)
| 名词 | 全称 | 解释 |
| :---------------- | :-------------------------------- | :--------------------------------------------------- |
| **Backend** | - | 服务器端系统负责处理业务逻辑、数据存储和对外接口 |
| **CGI** | Common Gateway Interface | 早期动态网页技术通过脚本处理请求并返回结果 |
| **Monolith** | - | 单体架构把所有业务逻辑打包在同一个应用中 |
| **Microservices** | - | 微服务架构把业务拆分成多个独立服务 |
| **Container** | - | 容器化技术把应用和依赖打包成可移植单元 |
| **K8s** | Kubernetes | 容器编排平台用于调度、扩缩容和治理容器 |
| **Service Mesh** | - | 服务网格负责微服务间通信治理、观测与安全 |
| **Serverless** | - | 无服务计算开发者只写函数平台自动运行与扩缩容 |
| **BaaS** | Backend as a Service | 即插即用的后端云服务认证、数据库、支付等 |
| **CI/CD** | Continuous Integration / Delivery | 持续集成与持续交付自动化测试与部署流程 |
| **Observability** | - | 可观测性利用日志/指标/追踪理解系统运行状态 |
| 名词 | 全称 | 解释 |
| :---------------- | :-------------------------------- | :------------------------------------------------ |
| **Backend** | - | 服务器端系统,负责处理业务逻辑、数据存储和对外接口 |
| **CGI** | Common Gateway Interface | 早期动态网页技术,通过脚本处理请求并返回结果 |
| **Monolith** | - | 单体架构,把所有业务逻辑打包在同一个应用中 |
| **Microservices** | - | 微服务架构,把业务拆分成多个独立服务 |
| **Container** | - | 容器化技术,把应用和依赖打包成可移植单元 |
| **K8s** | Kubernetes | 容器编排平台,用于调度、扩缩容和治理容器 |
| **Service Mesh** | - | 服务网格,负责微服务间通信治理、观测与安全 |
| **Serverless** | - | 无服务计算,开发者只写函数,平台自动运行与扩缩容 |
| **BaaS** | Backend as a Service | 即插即用的后端云服务(认证、数据库、支付等) |
| **CI/CD** | Continuous Integration / Delivery | 持续集成与持续交付,自动化测试与部署流程 |
| **Observability** | - | 可观测性,利用日志/指标/追踪理解系统运行状态 |
File diff suppressed because it is too large Load Diff
+154 -571
View File
@@ -1,94 +1,112 @@
# Canvas 2D 入门从像素到动画(交互式教程)
# Canvas 2D 入门:从像素到动画
> **学习指南**:本章节无需深厚的前端基础,通过交互式演示带你掌握 Canvas 2D 的核心原理和实践技巧。我们将从最基础的绘制开始,一直到构建复杂的交互式图形应用。
::: tip 🎯 核心问题
**如何在网页上画图、做动画、甚至开发游戏?** Canvas 提供了一个强大的 2D 绘图能力,让你用代码创造视觉内容。
:::
## 0. 引言:Canvas 是什么
---
Canvas(画布)是 HTML5 提供的一个通过 JavaScript 绘制 2D 图形的元素。你可以把它想象成一张**数字画布**,上面可以用代码"画"出任何东西:简单的形状、复杂的图表、流畅的动画,甚至是完整的游戏。
## 1. 为什么要学 Canvas?
### 0.1 Canvas vs SVG:有什么区别?
### 1.1 Canvas 是什么?
在 Web 开发中,绘制图形主要有两种方式:Canvas 和 SVGScalable Vector Graphics)。它们各有优劣:
**Canvas (画布)** 是 HTML5 提供的一个通过 JavaScript 绘制 2D 图形的元素。
你可以把它想象成一张**数字画布**:
- 🖌️ 你可以用代码"画笔"在上面作画
- 🎨 可以画任何东西: 简单的形状、复杂的图表、流畅的动画
- 🎮 甚至可以做成完整的游戏
::: tip 💡 Canvas vs SVG:有什么区别?
在 Web 开发中,绘制图形主要有两种方式:
| 特性 | Canvas | SVG |
| :------- | :------------------- | :-------------------- |
| **类型** | 位图光栅图形 | 矢量图形 |
| -------- | -------------------- | --------------------- |
| **类型** | 位图(光栅图形) | 矢量图形 |
| **DOM** | 单个 `<canvas>` 元素 | 每个图形都是 DOM 元素 |
| **交互** | 需要手动计算碰撞 | 天然支持事件绑定 |
| **性能** | 适合大量对象 | 适合少量复杂对象 |
| **缩放** | 放大会失真 | 无限缩放不失真 |
| **应用** | 游戏、数据可视化 | 图标、插画 |
**简单总结**
**简单总结**:
- **Canvas** = 像素画画完就变成像素性能好但交互麻烦
- **SVG** = 矢量图每个图形都是对象交互方便但对象多了会慢
- **Canvas** = 像素画,画完就变成像素,性能好但交互麻烦
- **SVG** = 矢量图,每个图形都是对象,交互方便但对象多了会慢
:::
### 0.2 Canvas 的应用场景
### 1.2 Canvas 的应用场景
Canvas 的用途非常广泛你可能在很多地方都见过它:
Canvas 的用途非常广泛,你可能每天都在用:
1. **数据可视化**:折线图、饼图、热力图(如 ECharts、Chart.js
2. **游戏开发**网页游戏如 Phaser.js 引擎
3. **图像处理**图片裁剪、滤镜、拼图如 Fabric.js
4. **创意效果**粒子特效、动画背景(如 Three.js 的 2D 渲染)
5. **工程绘图**CAD、流程图、思维导图
1. **数据可视化**: ECharts、Chart.js 的图表
2. **游戏开发**: 网页游戏(如 Phaser.js 引擎)
3. **图像处理**: 图片裁剪、滤镜、拼图(如 Fabric.js)
4. **创意效果**: 粒子特效、动画背景
5. **工程绘图**: CAD、流程图、思维导图
---
## 1. Canvas 基础
## 2. Canvas 基础
### 1.1 Canvas 元素和上下文
### 2.1 Canvas 元素和上下文
使用 Canvas 的第一步是在 HTML 中创建一个 `<canvas>` 元素
使用 Canvas 的第一步是在 HTML 中创建一个 `<canvas>` 元素:
```html
<canvas id="myCanvas" width="600" height="400"></canvas>
```
然后通过 JavaScript 获取**渲染上下文Rendering Context**
然后通过 JavaScript 获取**渲染上下文 (Rendering Context)**:
```javascript
const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d') // 获取 2D 上下文
```
**关键概念**
::: tip 💡 关键概念
- `canvas` 是 DOM 元素控制画布的大小和位置
- `ctx` 是绘图工具所有的绘制操作都通过它完成
- `'2d'` 表示使用 2D 渲染上下文WebGL 使用 `'webgl'`
- **canvas** 是 DOM 元素,控制画布的大小和位置
- **ctx** 是绘图工具,所有的绘制操作都通过它完成
- **`"2d"`** 表示使用 2D 渲染上下文(WebGL 使用 `"webgl"`)
:::
> 🕹️ **交互演示**:点击下方按钮,体验 Canvas 的基本绘图操作。
### 2.2 坐标系统:Canvas 的"地图规则"
<CanvasBasicsDemo />
Canvas 使用的是**屏幕坐标系**,这与传统数学坐标系有所不同:
### 1.2 坐标系统
Canvas 使用的是**屏幕坐标系**,这与传统数学坐标系有所不同:
- **原点 (0, 0)**:在左上角(不是中心)
- **X 轴**:向右为正方向
- **Y 轴**:向下为正方向(注意:数学坐标系中 Y 轴向上)
- **单位**:像素(px
- **原点 (0, 0)**: 在**左上角**(不是中心)
- **X 轴**: 向右为正方向
- **Y 轴**: **向下**为正方向(注意: 数学坐标系中 Y 轴向上)
- **单位**: 像素 (px)
```javascript
// 在左上角绘制一个
// 在左上角绘制一个矩形
ctx.fillRect(0, 0, 10, 10)
// 在右下角绘制一个
// 在右下角绘制一个矩形
ctx.fillRect(canvas.width - 10, canvas.height - 10, 10, 10)
```
> 🕹️ **交互演示**:拖动下方的点,感受 Canvas 的坐标系统。
::: tip 💡 记忆技巧
<CoordinateSystemDemo />
想象你在看**屏幕**:
### 1.3 绘制基本形状
- 向右移 → X 增加 ✅
- 向下移(滚动页面) → Y 增加 ✅
- 向左移 → X 减少
- 向上移(向上滚动) → Y 减少
Canvas 提供了几种绘制基本形状的方法:
这就是 Canvas 的坐标规则。
:::
#### 矩形
### 2.3 绘制基本形状
Canvas 提供了几种绘制基本形状的方法:
**矩形**:
```javascript
// 填充矩形
@@ -104,7 +122,7 @@ ctx.strokeRect(x, y, width, height)
ctx.clearRect(x, y, width, height)
```
#### 圆形
**圆形**:
```javascript
ctx.beginPath()
@@ -112,18 +130,16 @@ ctx.arc(x, y, radius, startAngle, endAngle)
ctx.fill() // 或 ctx.stroke()
```
**参数说明**
**参数说明**:
- `x, y`圆心坐标
- `radius`半径
- `startAngle, endAngle`起始和结束角度弧度制
- **x, y**: 圆心坐标
- **radius**: 半径
- **startAngle, endAngle**: 起始和结束角度(弧度制)
- `0` = 3 点钟方向
- `Math.PI / 2` = 6 点钟方向
- `Math.PI` = 9 点钟方向
- `Math.PI * 1.5` = 12 点钟方向
- `Math.PI * 2` = 回到 3 点钟方向
#### 线条
**线条**:
```javascript
ctx.beginPath()
@@ -132,15 +148,15 @@ ctx.lineTo(x2, y2) // 终点
ctx.stroke()
```
### 1.4 颜色和渐变
### 2.4 颜色和样式
Canvas 支持多种颜色设置方式
Canvas 支持多种颜色设置方式:
```javascript
// 纯色
ctx.fillStyle = '#3498db' // 十六进制
ctx.fillStyle = 'rgb(52, 152, 219)' // RGB
ctx.fillStyle = 'rgba(52, 152, 219, 0.5)' // RGBA带透明度
ctx.fillStyle = 'rgba(52, 152, 219, 0.5)' // RGBA(带透明度)
// 线性渐变
const gradient = ctx.createLinearGradient(x1, y1, x2, y2)
@@ -157,32 +173,32 @@ ctx.fillStyle = radialGradient
---
## 2. 路径与形状
## 3. 路径:Canvas 的"笔画"
### 2.1 路径 (Path) 的概念
### 3.1 什么是路径?
**路径**是 Canvas 中的核心概念,它是由一系列点连接成的"轨迹"。你可以把它想象成用笔画线的过程
**路径 (Path)** 是 Canvas 中的核心概念。你可以把它想象成用笔画线的过程:
1. `beginPath()` - 开始新路径拿起笔
2. `moveTo()` - 移动到起点不画线
3. `lineTo()` / `arc()` / `curveTo()` - 绘制线条或曲线
4. `closePath()` - 闭合路径可选
5. `fill()` / `stroke()` - 填充或描边
1. **`beginPath()`** - 开始新路径(拿起笔)
2. **`moveTo()`** - 移动到起点(不画线)
3. **`lineTo()` / `arc()`** - 绘制线条或曲线
4. **`closePath()`** - 闭合路径(可选)
5. **`fill()` / `stroke()`** - 填充或描边
```javascript
ctx.beginPath()
ctx.moveTo(100, 100) // 移动到起点
ctx.lineTo(200, 100) // 画横线
ctx.lineTo(150, 150) // 画斜线
ctx.closePath() // 闭合路径回到起点
ctx.closePath() // 闭合路径(回到起点)
ctx.fill() // 填充
```
### 2.2 绘制复杂形状
### 3.2 绘制复杂形状
通过组合路径可以绘制任意复杂的形状
通过组合路径,可以绘制任意复杂的形状
#### 三角形
**三角形**:
```javascript
ctx.beginPath()
@@ -194,114 +210,17 @@ ctx.fillStyle = '#e74c3c'
ctx.fill()
```
#### 星形
```javascript
function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius) {
let rot = (Math.PI / 2) * 3
let x = cx
let y = cy
const step = Math.PI / spikes
ctx.beginPath()
ctx.moveTo(cx, cy - outerRadius)
for (let i = 0; i < spikes; i++) {
x = cx + Math.cos(rot) * outerRadius
y = cy + Math.sin(rot) * outerRadius
ctx.lineTo(x, y)
rot += step
x = cx + Math.cos(rot) * innerRadius
y = cy + Math.sin(rot) * innerRadius
ctx.lineTo(x, y)
rot += step
}
ctx.lineTo(cx, cy - outerRadius)
ctx.closePath()
ctx.fill()
}
drawStar(ctx, 150, 150, 5, 50, 25)
```
---
## 3. 文本与图片
### 3.1 绘制文本
Canvas 也可以绘制文本:
```javascript
// 填充文本
ctx.font = '30px Arial'
ctx.fillStyle = '#2c3e50'
ctx.fillText('Hello Canvas', x, y)
// 描边文本
ctx.font = 'bold 40px Arial'
ctx.strokeStyle = '#e74c3c'
ctx.lineWidth = 2
ctx.strokeText('Hello Canvas', x, y)
// 文本对齐
ctx.textAlign = 'center' // left, center, right
ctx.textBaseline = 'middle' // top, middle, bottom
ctx.fillText('Centered', canvas.width / 2, canvas.height / 2)
```
### 3.2 加载和绘制图片
```javascript
const img = new Image()
img.src = 'image.png'
img.onload = () => {
// 绘制图片
ctx.drawImage(img, x, y)
// 缩放图片
ctx.drawImage(img, x, y, width, height)
// 裁剪图片
ctx.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
}
```
**参数说明**
- `sx, sy, sWidth, sHeight`:源图像的裁剪区域
- `dx, dy, dWidth, dHeight`:目标画布的绘制区域
### 3.3 裁剪与合成
```javascript
// 裁剪区域
ctx.save()
ctx.beginPath()
ctx.arc(x, y, radius, 0, Math.PI * 2)
ctx.clip() // 之后的所有绘制都只会显示在圆形内
ctx.drawImage(img, 0, 0)
ctx.restore()
// 全局合成操作
ctx.globalCompositeOperation = 'source-over' // 默认
ctx.globalCompositeOperation = 'destination-over' // 绘制在现有内容后面
ctx.globalCompositeOperation = 'source-in' // 只保留重叠部分
```
---
## 4. 动画基础
### 4.1 requestAnimationFrame
### 4.1 动画循环
在 Canvas 中创建动画核心是使用 `requestAnimationFrame` 方法。它是浏览器专门为动画优化的 API
在 Canvas 中创建动画,核心是使用 **`requestAnimationFrame`** 方法。
```javascript
function animate() {
// 1. 清除画布或绘制半透明背景产生拖尾效果
// 1. 清除画布(或绘制半透明背景产生拖尾效果)
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 2. 更新状态
@@ -318,93 +237,35 @@ function animate() {
animate()
```
**为什么用 requestAnimationFrame 而不是 setInterval**
::: tip 💡 为什么用 requestAnimationFrame 而不是 setInterval?
- 自动优化通常为 60FPS每秒 60 帧
- 页面不可见时自动暂停节省资源
- 与浏览器刷新周期同步避免画面撕裂
- 自动优化,通常为 60FPS(每秒 60 帧)
- 页面不可见时自动暂停,节省资源
- 与浏览器刷新周期同步,避免画面撕裂
:::
> 🕹️ **交互演示**:点击播放,观察不同类型的动画效果。
### 4.2 动画的本质
<AnimationLoopDemo />
动画的本质是**快速连续绘制静态画面**。每帧需要:
### 4.2 清除与重绘
动画的本质是**快速连续绘制静态画面**。每帧需要:
1. **清除旧画面**`ctx.clearRect()` 或用半透明背景覆盖
2. **更新状态**:计算新位置、新角度等
3. **绘制新画面**:重新绘制所有对象
1. **清除旧画面**: `ctx.clearRect()` 或用半透明背景覆盖
2. **更新状态**: 计算新位置、新角度等
3. **绘制新画面**: 重新绘制所有对象
```javascript
// 方法1:完全清除
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 方法2半透明背景产生拖尾效果
// 半透明背景(产生拖尾效果)
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 方法3:只清除变化区域(脏矩形优化)
objects.forEach((obj) => {
if (obj.moved) {
ctx.clearRect(obj.oldX, obj.oldY, obj.size, obj.size)
obj.draw(ctx)
}
})
```
### 4.3 动画循环
一个完整的动画循环示例:
```javascript
let ball = {
x: 300,
y: 200,
vx: 2,
vy: 3,
radius: 20
}
function update() {
// 更新位置
ball.x += ball.vx
ball.y += ball.vy
// 边界碰撞
if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
ball.vx = -ball.vx
}
if (ball.y + ball.radius > canvas.height || ball.y - ball.radius < 0) {
ball.vy = -ball.vy
}
}
function draw() {
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 绘制球
ctx.beginPath()
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2)
ctx.fillStyle = '#3498db'
ctx.fill()
}
function animate() {
update()
draw()
requestAnimationFrame(animate)
}
animate()
```
---
## 5. 事件处理
Canvas 只是一个 DOM 元素不像 SVG 那样每个图形都是独立的 DOM 元素。因此我们需要**手动处理交互事件**。
Canvas 只是一个 DOM 元素,不像 SVG 那样每个图形都是独立的 DOM 元素。因此,我们需要**手动处理交互事件**。
### 5.1 鼠标事件
@@ -466,168 +327,15 @@ canvas.addEventListener('mouseup', () => {
})
```
### 5.3 键盘事件
```javascript
canvas.tabIndex = 0 // 使 canvas 可以获取焦点
canvas.focus()
canvas.addEventListener('keydown', (e) => {
const step = 10
switch (e.key) {
case 'ArrowUp':
selectedObject.y -= step
break
case 'ArrowDown':
selectedObject.y += step
break
case 'ArrowLeft':
selectedObject.x -= step
break
case 'ArrowRight':
selectedObject.x += step
break
case 'Delete':
objects = objects.filter((obj) => obj !== selectedObject)
break
}
draw()
})
```
> 🕹️ **交互演示**:尝试在下方的 Canvas 中点击、拖拽、悬停,体验不同的事件处理方式。
<EventHandlingDemo />
---
## 6. 实战案例
## 6. 性能优化
### 6.1 绘制折线图
随着绘制的对象增多,Canvas 性能会下降。以下是一些常用的优化技巧:
```javascript
const data = [10, 50, 30, 80, 60, 90, 40]
### 6.1 离屏 Canvas (Offscreen Canvas)
function drawLineChart(ctx, data) {
const padding = 50
const chartWidth = canvas.width - padding * 2
const chartHeight = canvas.height - padding * 2
const maxValue = Math.max(...data)
// 绘制坐标轴
ctx.beginPath()
ctx.moveTo(padding, padding)
ctx.lineTo(padding, canvas.height - padding)
ctx.lineTo(canvas.width - padding, canvas.height - padding)
ctx.strokeStyle = '#2c3e50'
ctx.stroke()
// 绘制折线
ctx.beginPath()
data.forEach((value, index) => {
const x = padding + (index / (data.length - 1)) * chartWidth
const y = canvas.height - padding - (value / maxValue) * chartHeight
if (index === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y)
}
})
ctx.strokeStyle = '#3498db'
ctx.lineWidth = 2
ctx.stroke()
// 绘制数据点
data.forEach((value, index) => {
const x = padding + (index / (data.length - 1)) * chartWidth
const y = canvas.height - padding - (value / maxValue) * chartHeight
ctx.beginPath()
ctx.arc(x, y, 5, 0, Math.PI * 2)
ctx.fillStyle = '#e74c3c'
ctx.fill()
})
}
```
### 6.2 简单粒子系统
粒子系统是游戏和特效中常见的技术,它由大量小粒子组成,每个粒子有独立的位置、速度、生命周期等属性。
```javascript
class Particle {
constructor(x, y) {
this.x = x
this.y = y
this.vx = (Math.random() - 0.5) * 4
this.vy = (Math.random() - 0.5) * 4
this.life = 1.0
this.color = `hsl(${Math.random() * 360}, 70%, 50%)`
}
update() {
this.x += this.vx
this.y += this.vy
this.life -= 0.02
}
draw(ctx) {
ctx.globalAlpha = this.life
ctx.fillStyle = this.color
ctx.beginPath()
ctx.arc(this.x, this.y, 3, 0, Math.PI * 2)
ctx.fill()
ctx.globalAlpha = 1.0
}
isDead() {
return this.life <= 0
}
}
// 动画循环
let particles = []
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 更新和绘制粒子
particles = particles.filter((p) => !p.isDead())
particles.forEach((p) => {
p.update()
p.draw(ctx)
})
requestAnimationFrame(animate)
}
// 鼠标移动产生粒子
canvas.addEventListener('mousemove', (e) => {
const { x, y } = getMousePos(e)
for (let i = 0; i < 3; i++) {
particles.push(new Particle(x, y))
}
})
animate()
```
> 🕹️ **交互演示**:在下方 Canvas 中移动鼠标,体验不同的粒子效果。
<ParticleSystemDemo />
---
## 7. 性能优化
随着绘制的对象增多,Canvas 性能会下降。以下是一些常用的优化技巧:
### 7.1 离屏 Canvas (Offscreen Canvas)
预渲染静态内容到离屏 Canvas,减少每帧的绘制操作:
预渲染静态内容到离屏 Canvas,减少每帧的绘制操作:
```javascript
// 创建离屏 Canvas
@@ -640,7 +348,6 @@ offscreenCanvas.height = 400
function drawBackground(ctx) {
ctx.fillStyle = '#f0f0f0'
ctx.fillRect(0, 0, 600, 400)
// 绘制网格...
}
drawBackground(offscreenCtx)
@@ -654,33 +361,9 @@ function draw() {
}
```
### 7.2 图层管理
### 6.2 减少重绘(脏矩形优化)
将静态背景和动态对象分层渲染:
```javascript
// 背景层(只绘制一次)
const backgroundLayer = document.createElement('canvas')
// ... 绘制静态背景
// 动态层(每帧重绘)
const dynamicLayer = canvas
function draw() {
// 清除动态层
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 绘制背景层
ctx.drawImage(backgroundLayer, 0, 0)
// 绘制动态对象
objects.forEach((obj) => obj.draw(ctx))
}
```
### 7.3 减少重绘
只重绘变化的部分(脏矩形优化):
只重绘变化的部分:
```javascript
function draw() {
@@ -703,9 +386,9 @@ function draw() {
}
```
### 7.4 批量渲染
### 6.3 批量渲染
减少状态切换fillStyle、strokeStyle 等):
减少状态切换(fillStyle、strokeStyle 等):
```javascript
// 按颜色分组
@@ -728,19 +411,15 @@ Object.keys(batches).forEach((color) => {
})
```
> 🕹️ **交互演示**:对比不同优化技术的性能差异。
<PerformanceDemo />
---
## 8. 常见库与框架
## 7. 常见库与框架
虽然原生 Canvas 已经很强大但在实际项目中使用成熟的库可以大大提高开发效率。
虽然原生 Canvas 已经很强大,但在实际项目中,使用成熟的库可以大大提高开发效率。
### 8.1 Fabric.js
### 7.1 Fabric.js
**特点**对象模型支持交互
**特点**: 对象模型,支持交互
```javascript
const canvas = new fabric.Canvas('c')
@@ -762,38 +441,11 @@ circle.on('click', () => {
})
```
**适用场景**图片编辑器、白板工具、图形设计工具
**适用场景**: 图片编辑器、白板工具、图形设计工具
### 8.2 Konva.js
### 7.2 PixiJS (WebGL)
**特点**:高性能,支持动画和滤镜
```javascript
const stage = new Konva.Stage({
container: 'container',
width: 600,
height: 400
})
const layer = new Konva.Layer()
stage.add(layer)
const circle = new Konva.Circle({
x: 300,
y: 200,
radius: 50,
fill: '#3498db',
draggable: true
})
layer.add(circle)
```
**适用场景**:复杂的图形应用、动画演示
### 8.3 PixiJS (WebGL)
**特点**:WebGL 加速,超高性能
**特点**: WebGL 加速,超高性能
```javascript
const app = new PIXI.Application({
@@ -810,60 +462,24 @@ graphics.endFill()
app.stage.addChild(graphics)
```
**适用场景**大型游戏、粒子系统、大量对象的场景
### 8.4 Three.js (3D)
虽然 Three.js 主要用于 3D,但也支持 2D 渲染:
```javascript
const scene = new THREE.Scene()
const camera = new THREE.OrthographicCamera(0, 600, 400, 0, 1, 1000)
const renderer = new THREE.WebGLRenderer()
renderer.setSize(600, 400)
document.body.appendChild(renderer.domElement)
const geometry = new THREE.CircleGeometry(50, 32)
const material = new THREE.MeshBasicMaterial({ color: 0x3498db })
const circle = new THREE.Mesh(geometry, material)
circle.position.set(300, 200, 0)
scene.add(circle)
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
}
animate()
```
**适用场景**2.5D 游戏、混合 2D/3D 应用
### 8.5 选择建议
| 库 | 优势 | 劣势 | 适用场景 |
| :-------------- | :----------------- | :--------- | :--------------- |
| **原生 Canvas** | 轻量、无依赖 | 开发效率低 | 学习、简单图形 |
| **Fabric.js** | 对象模型、交互友好 | 性能一般 | 图片编辑器、白板 |
| **Konva.js** | 高性能、API 简洁 | 体积较大 | 复杂图形应用 |
| **PixiJS** | 超高性能、WebGL | 学习曲线陡 | 大型游戏 |
| **Three.js** | 3D 能力强 | 过于重量级 | 2.5D 游戏 |
**适用场景**: 大型游戏、粒子系统、大量对象的场景
---
## 9. 总结与最佳实践
## 8. 总结与最佳实践
### 9.1 核心要点回顾
### 8.1 核心要点回顾
1. **Canvas 是位图画布**绘制后就是像素无法直接修改已有内容
2. **坐标系统**原点在左上角Y 轴向下为正
3. **路径系统**beginPath → moveTo → lineTo → fill/stroke
4. **动画原理**清除 → 更新 → 绘制 → requestAnimationFrame
5. **事件处理**需要手动计算碰撞检测
6. **性能优化**离屏 Canvas、脏矩形、批量渲染
1. **Canvas 是位图画布**: 绘制后就是像素,无法直接修改已有内容
2. **坐标系统**: 原点在左上角,Y 轴向下为正
3. **路径系统**: beginPath → moveTo → lineTo → fill/stroke
4. **动画原理**: 清除 → 更新 → 绘制 → requestAnimationFrame
5. **事件处理**: 需要手动计算碰撞
6. **性能优化**: 离屏 Canvas、脏矩形、批量渲染
### 9.2 最佳实践
### 8.2 最佳实践
#### 代码组织
**代码组织**:
```javascript
// 使用类封装对象
@@ -889,81 +505,48 @@ class GameObject {
}
```
#### 性能优化清单
**性能优化清单**:
- ✅ 使用 `requestAnimationFrame` 而不是 `setInterval`
- ✅ 减少状态切换按颜色分组绘制
- ✅ 减少状态切换(按颜色分组绘制)
- ✅ 使用离屏 Canvas 预渲染静态内容
- ✅ 只重绘变化的部分脏矩形
- ✅ 限制对象数量使用对象池
- ✅ 只重绘变化的部分(脏矩形)
- ✅ 限制对象数量,使用对象池
- ✅ 避免 `save()``restore()` 的频繁调用
#### 调试技巧
---
```javascript
// 绘制边界框(用于调试)
function drawBoundingBox(ctx, obj) {
ctx.strokeStyle = '#e74c3c'
ctx.lineWidth = 1
ctx.strokeRect(
obj.x - obj.radius,
obj.y - obj.radius,
obj.radius * 2,
obj.radius * 2
)
}
## 9. 名词速查表 (Glossary)
// 显示 FPS
let lastTime = performance.now()
let frameCount = 0
function showFPS() {
const now = performance.now()
frameCount++
if (now >= lastTime + 1000) {
console.log(`FPS: ${frameCount}`)
frameCount = 0
lastTime = now
}
requestAnimationFrame(showFPS)
}
showFPS()
```
### 9.3 学习路线
1. **入门**:掌握基本形状绘制和颜色
2. **进阶**:学习动画原理和事件处理
3. **实战**:制作小游戏(贪吃蛇、打砖块)
4. **优化**:学习性能优化技巧
5. **扩展**:尝试成熟的 Canvas 库(Fabric.js、Konva
| 名词 | 解释 |
| ------------------------- | ----------------------------------------------------------------------- |
| **Context / 上下文** | Canvas 的渲染环境,通过 `getContext("2d")` 获取,所有绘制操作都通过它完成 |
| **Path / 路径** | 由一系列点连接成的轨迹,是 Canvas 绘图的基础 |
| **Stroke / 描边** | 绘制路径的轮廓线 |
| **Fill / 填充** | 用颜色填充路径内部 |
| **requestAnimationFrame** | 浏览器提供的动画 API,在每次重绘前调用回调函数 |
| **Offscreen Canvas** | 离屏 Canvas,用于预渲染静态内容以提高性能 |
| **Dirty Rect** | 脏矩形优化,只重绘变化的部分 |
| **Collision Detection** | 碰撞检测,判断鼠标或对象是否点击了某个图形 |
| **Raster vs Vector** | 位图 vs 矢量图,Canvas 是位图,SVG 是矢量图 |
---
## 10. 名词速查表 (Glossary)
## 总结
| 名词 | 解释 |
| :------------------------ | :------------------------------------------------------------------------ |
| **Context / 上下文** | Canvas 的渲染环境,通过 `getContext('2d')` 获取,所有绘制操作都通过它完成 |
| **Path / 路径** | 由一系列点连接成的轨迹,是 Canvas 绘图的基础 |
| **Stroke / 描边** | 绘制路径的轮廓线 |
| **Fill / 填充** | 用颜色填充路径内部 |
| **requestAnimationFrame** | 浏览器提供的动画 API,在每次重绘前调用回调函数 |
| **Offscreen Canvas** | 离屏 Canvas,用于预渲染静态内容以提高性能 |
| **Dirty Rect** | 脏矩形优化,只重绘变化的部分 |
| **Particle System** | 粒子系统,由大量小粒子组成的特效系统 |
| **Collision Detection** | 碰撞检测,判断鼠标或对象是否点击了某个图形 |
| **Raster vs Vector** | 位图 vs 矢量图,Canvas 是位图,SVG 是矢量图 |
现在你已经掌握了 Canvas 2D 的核心概念:
---
- **基本绘图**: 矩形、圆形、线条
- **样式控制**: 颜色、渐变、阴影
- **动画制作**: requestAnimationFrame + 清除重绘
- **交互处理**: 鼠标事件、碰撞检测
- **性能优化**: 离屏 Canvas、批量渲染
**下一步建议**
**下一步建议**:
- 如果你想深入学习 Canvas 动画可以尝试制作一个**贪吃蛇游戏**或**打砖块游戏**
- 如果你对数据可视化感兴趣可以学习 **ECharts** 或 **D3.js**
- 如果你想做游戏开发可以尝试 **Phaser.js** 游戏引擎
- 如果你对 WebGL 感兴趣可以学习 **Three.js** 或 **PixiJS**
- 如果你想深入学习动画,可以尝试制作一个**贪吃蛇游戏**或**打砖块游戏**
- 如果你对数据可视化感兴趣,可以学习 **ECharts** 或 **D3.js**
- 如果你想做游戏开发,可以尝试 **Phaser.js** 游戏引擎
- 如果你对 WebGL 感兴趣,可以学习 **Three.js** 或 **PixiJS**
祝你学习愉快🎨
祝你学习愉快! 🎨
+101 -102
View File
@@ -36,12 +36,12 @@
想象一下,你们公司搬到了一栋新写字楼:
| 场景 | 没有 IAM 的做法 | 有 IAM 的做法 |
| :--- | :--- | :--- |
| 新员工入职 | 给他一把能开所有门的万能钥匙 | 给他一张门禁卡,只能刷他办公区域的门 |
| 员工离职 | 钥匙丢了就丢了,也不知道谁拿着 | 立即在系统里注销他的门禁卡,所有门都打不开了 |
| 外包人员 | 把钥匙借给他几天 | 发临时门禁卡,设置3天后自动失效 |
| 访客 | 前台配一把钥匙给他 | 发一次性访客码,只能进会议室 |
| 场景 | 没有 IAM 的做法 | 有 IAM 的做法 |
| :--------- | :----------------------------- | :------------------------------------------- |
| 新员工入职 | 给他一把能开所有门的万能钥匙 | 给他一张门禁卡,只能刷他办公区域的门 |
| 员工离职 | 钥匙丢了就丢了,也不知道谁拿着 | 立即在系统里注销他的门禁卡,所有门都打不开了 |
| 外包人员 | 把钥匙借给他几天 | 发临时门禁卡,设置3天后自动失效 |
| 访客 | 前台配一把钥匙给他 | 发一次性访客码,只能进会议室 |
**IAMIdentity and Access Management,身份与访问管理)**,就像是这套"智能门禁系统":
@@ -55,13 +55,13 @@
不同的云厂商都有自己的 IAM 实现:
| 云厂商 | 服务名称 | 核心概念 |
| :--- | :--- | :--- |
| **AWS** | IAM (Identity and Access Management) | User、Group、Role、Policy |
| **阿里云** | RAM (Resource Access Management) | 用户、用户组、角色、策略 |
| **腾讯云** | CAM (Cloud Access Management) | 用户、用户组、角色、策略 |
| **华为云** | IAM | 用户、用户组、委托、策略 |
| **Azure** | Azure AD + RBAC | User、Group、Role、RBAC |
| 云厂商 | 服务名称 | 核心概念 |
| :--------- | :----------------------------------- | :------------------------ |
| **AWS** | IAM (Identity and Access Management) | User、Group、Role、Policy |
| **阿里云** | RAM (Resource Access Management) | 用户、用户组、角色、策略 |
| **腾讯云** | CAM (Cloud Access Management) | 用户、用户组、角色、策略 |
| **华为云** | IAM | 用户、用户组、委托、策略 |
| **Azure** | Azure AD + RBAC | User、Group、Role、RBAC |
虽然名字不同,但**核心概念都是相通的**:
@@ -80,11 +80,11 @@
用一个办公室的场景来类比:
| 概念 | 类比 | 适用场景 | 特点 |
| :--- | :--- | :--- | :--- |
| **用户(User** | 正式员工,有自己的工位和门禁卡 | 长期、稳定的团队成员 | 有永久凭证(密码、AK/SK) |
| **用户组(Group** | 部门,如"技术部"、"销售部" | 批量管理权限 | 不能登录,只是权限容器 |
| **角色(Role** | 临时访客证、外包临时卡 | 临时授权、跨账号访问 | 没有永久凭证,靠"扮演"获取临时凭证 |
| 概念 | 类比 | 适用场景 | 特点 |
| :------------------ | :----------------------------- | :------------------- | :--------------------------------- |
| **用户(User** | 正式员工,有自己的工位和门禁卡 | 长期、稳定的团队成员 | 有永久凭证(密码、AK/SK) |
| **用户组(Group** | 部门,如"技术部"、"销售部" | 批量管理权限 | 不能登录,只是权限容器 |
| **角色(Role** | 临时访客证、外包临时卡 | 临时授权、跨账号访问 | 没有永久凭证,靠"扮演"获取临时凭证 |
### 2.2 真实案例:一个创业公司的权限演进
@@ -151,13 +151,13 @@ IAM Role 有两个核心组成部分:
用一个话剧表演的类比:
| 概念 | 类比 | 说明 |
| :--- | :--- | :--- |
| **Role(角色)** | 剧本里的"哈姆雷特" | 定义了要演什么戏(权限)|
| **Trust Policy** | 导演说"谁能演哈姆雷特" | 可能是"本剧团的演员"(本账号用户)、"隔壁剧团借来的演员"(跨账号)、"特邀嘉宾"(外部 IdP|
| **Permission Policy** | 剧本内容 | 哈姆雷特能做什么:说台词、决斗、发疯(具体权限)|
| **Assume Role** | 演员上台表演 | 小李被导演选中演哈姆雷特,上台后他就拥有了剧本里定义的所有权限 |
| **临时凭证** | 演出证 | 小李拿到一个"临时演出证",演出结束后就失效了 |
| 概念 | 类比 | 说明 |
| :-------------------- | :--------------------- | :----------------------------------------------------------------------------------------- |
| **Role(角色)** | 剧本里的"哈姆雷特" | 定义了要演什么戏(权限) |
| **Trust Policy** | 导演说"谁能演哈姆雷特" | 可能是"本剧团的演员"(本账号用户)、"隔壁剧团借来的演员"(跨账号)、"特邀嘉宾"(外部 IdP |
| **Permission Policy** | 剧本内容 | 哈姆雷特能做什么:说台词、决斗、发疯(具体权限) |
| **Assume Role** | 演员上台表演 | 小李被导演选中演哈姆雷特,上台后他就拥有了剧本里定义的所有权限 |
| **临时凭证** | 演出证 | 小李拿到一个"临时演出证",演出结束后就失效了 |
### 3.2 策略(Policy):权限的"语法"
@@ -174,11 +174,7 @@ IAM Policy 是一个 JSON 文档,定义了"谁能对什么资源做什么操
{
"Sid": "AllowS3ReadWrite",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::my-app-bucket/*",
"Condition": {
"StringEquals": {
@@ -201,15 +197,15 @@ IAM Policy 是一个 JSON 文档,定义了"谁能对什么资源做什么操
**关键字段解释**
| 字段 | 含义 | 示例 |
| :--- | :--- | :--- |
| **Version** | Policy 语法版本 | "2012-10-17" |
| **Statement** | 权限声明数组,可包含多个规则 | [...] |
| **Sid** | 声明 ID,可选,用于标识这条规则 | "AllowS3ReadWrite" |
| **Effect** | 效果:Allow(允许)或 Deny(拒绝) | "Allow" |
| **Action** | 允许/拒绝的操作,支持通配符 | "s3:GetObject", "s3:*" |
| **Resource** | 作用的资源,用 ARN 标识 | "arn:aws:s3:::bucket/*" |
| **Condition** | 可选,满足特定条件时才生效 | 区域限制、MFA 要求等 |
| 字段 | 含义 | 示例 |
| :------------ | :--------------------------------- | :----------------------- |
| **Version** | Policy 语法版本 | "2012-10-17" |
| **Statement** | 权限声明数组,可包含多个规则 | [...] |
| **Sid** | 声明 ID,可选,用于标识这条规则 | "AllowS3ReadWrite" |
| **Effect** | 效果:Allow(允许)或 Deny(拒绝) | "Allow" |
| **Action** | 允许/拒绝的操作,支持通配符 | "s3:GetObject", "s3:\*" |
| **Resource** | 作用的资源,用 ARN 标识 | "arn:aws:s3:::bucket/\*" |
| **Condition** | 可选,满足特定条件时才生效 | 区域限制、MFA 要求等 |
### 3.3 权限的优先级:Deny > Allow > 默认拒绝
@@ -246,6 +242,7 @@ IAM 的权限评估逻辑可以用一句话总结:**显式 Deny 永远赢,
```
**关键点**
- 开发者虽然有 `s3:*` 的 Allow 权限
- 但敏感目录有显式的 Deny 规则
- Deny 优先级更高,所以开发者无法访问敏感数据
@@ -261,10 +258,10 @@ IAM 的权限评估逻辑可以用一句话总结:**显式 Deny 永远赢,
Access Key(访问密钥)是云服务提供的一种长期凭证,用于程序化的 API 调用。它由两部分组成:
| 组成部分 | 名称 | 作用 | 类比 |
| :--- | :--- | :--- | :--- |
| **Access Key ID** | 访问密钥 ID | 标识你是谁(类似于用户名) | 银行卡号 |
| **Secret Access Key** | 秘密访问密钥 | 证明你是你(类似于密码) | 银行卡密码 |
| 组成部分 | 名称 | 作用 | 类比 |
| :-------------------- | :----------- | :------------------------- | :--------- |
| **Access Key ID** | 访问密钥 ID | 标识你是谁(类似于用户名) | 银行卡号 |
| **Secret Access Key** | 秘密访问密钥 | 证明你是你(类似于密码) | 银行卡密码 |
### 4.2 为什么 AK/SK 是"高危物品"
@@ -302,12 +299,12 @@ upload_file('./test.jpg', 'my-company-bucket', 'uploads/test.jpg')
**这个案例告诉我们什么?**
| 错误做法 | 正确做法 |
| :--- | :--- |
| 把 AK/SK 硬编码在代码中 | 使用 IAM Role,让程序自动获取临时凭证 |
| 把 AK/SK 提交到 Git 仓库 | 使用 `.gitignore` 忽略配置文件,使用密钥管理服务 |
| 长期使用同一个 AK/SK 不轮换 | 定期轮换 AK/SK,使用临时凭证替代长期凭证 |
| 给 AK/SK 分配过大权限 | 遵循最小权限原则,只授予必要的权限 |
| 错误做法 | 正确做法 |
| :-------------------------- | :----------------------------------------------- |
| 把 AK/SK 硬编码在代码中 | 使用 IAM Role,让程序自动获取临时凭证 |
| 把 AK/SK 提交到 Git 仓库 | 使用 `.gitignore` 忽略配置文件,使用密钥管理服务 |
| 长期使用同一个 AK/SK 不轮换 | 定期轮换 AK/SK,使用临时凭证替代长期凭证 |
| 给 AK/SK 分配过大权限 | 遵循最小权限原则,只授予必要的权限 |
### 4.3 AK/SK 的安全使用指南
@@ -356,7 +353,7 @@ jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # 关键:允许请求 OIDC token
id-token: write # 关键:允许请求 OIDC token
contents: read
steps:
- uses: actions/checkout@v3
@@ -374,13 +371,13 @@ jobs:
**总结:AK/SK 使用的安全层级**
| 安全等级 | 做法 | 适用场景 | 风险等级 |
| :--- | :--- | :--- | :--- |
| 最高 | 使用 IAM Role(无长期凭证) | EC2、Lambda、ECS、CI/CD | 极低 |
| 高 | 使用 OIDC Federation | GitHub Actions、GitLab CI | 低 |
| 中 | 使用密钥管理服务 | 本地开发、小团队 | 中 |
| 低 | 使用环境变量 | 快速原型、个人项目 | 高 |
| 极低 | 硬编码在代码中 | 任何场景都不推荐 | 极高 |
| 安全等级 | 做法 | 适用场景 | 风险等级 |
| :------- | :-------------------------- | :------------------------ | :------- |
| 最高 | 使用 IAM Role(无长期凭证) | EC2、Lambda、ECS、CI/CD | 极低 |
| 高 | 使用 OIDC Federation | GitHub Actions、GitLab CI | 低 |
| 中 | 使用密钥管理服务 | 本地开发、小团队 | 中 |
| 低 | 使用环境变量 | 快速原型、个人项目 | 高 |
| 极低 | 硬编码在代码中 | 任何场景都不推荐 | 极高 |
---
@@ -392,21 +389,21 @@ jobs:
MFAMulti-Factor Authentication,多因素认证),也叫 2FATwo-Factor Authentication,双因素认证),是一种安全机制,要求用户在登录时提供**两种或以上**不同类型的认证因素:
| 因素类型 | 是什么 | 例子 |
| :--- | :--- | :--- |
| **知识因素**(你知道什么) | 只有用户知道的信息 | 密码、PIN 码 |
| **持有因素**(你有什么) | 用户拥有的物理设备 | 手机、硬件密钥 |
| **生物因素**(你是什么) | 用户的生物特征 | 指纹、面部识别 |
| 因素类型 | 是什么 | 例子 |
| :------------------------- | :----------------- | :------------- |
| **知识因素**(你知道什么) | 只有用户知道的信息 | 密码、PIN 码 |
| **持有因素**(你有什么) | 用户拥有的物理设备 | 手机、硬件密钥 |
| **生物因素**(你是什么) | 用户的生物特征 | 指纹、面部识别 |
### 5.2 为什么 MFA 这么重要?
**真实数据告诉你答案**
| 攻击方式 | 没有 MFA 时的成功率 | 有 MFA 时的成功率 |
| :--- | :--- | :--- |
| 密码猜测/暴力破解 | 很高 | 极低(还需要第二因素) |
| 钓鱼攻击获取密码 | 很高 | 极低(钓鱼页面无法获取 MFA 码) |
| 密码泄露(其他网站泄露)| 很高 | 极低(不知道第二因素) |
| 攻击方式 | 没有 MFA 时的成功率 | 有 MFA 时的成功率 |
| :----------------------- | :------------------ | :------------------------------ |
| 密码猜测/暴力破解 | 很高 | 极低(还需要第二因素) |
| 钓鱼攻击获取密码 | 很高 | 极低(钓鱼页面无法获取 MFA 码) |
| 密码泄露(其他网站泄露) | 很高 | 极低(不知道第二因素) |
**微软安全报告(2020)**:启用 MFA 可以阻止 **99.9%** 的自动化攻击。
@@ -441,14 +438,14 @@ MFAMulti-Factor Authentication,多因素认证),也叫 2FATwo-Factor
随着业务增长,很多公司会使用**多账号架构**来隔离不同环境:
| 账号类型 | 用途 | 权限要求 |
| :--- | :--- | :--- |
| **Master Account** | 组织管理、账单结算 | 几乎不使用 |
| **Security Audit** | 集中收集所有账号的日志 | 只读访问其他账号 |
| **Shared Services** | 共享资源(镜像仓库等) | 其他账号只读访问 |
| **Development** | 开发环境 | 开发者完全权限 |
| **Staging** | 测试/预发布环境 | 测试人员权限 |
| **Production** | 生产环境 | 严格限制,需要审批 |
| 账号类型 | 用途 | 权限要求 |
| :------------------ | :--------------------- | :----------------- |
| **Master Account** | 组织管理、账单结算 | 几乎不使用 |
| **Security Audit** | 集中收集所有账号的日志 | 只读访问其他账号 |
| **Shared Services** | 共享资源(镜像仓库等) | 其他账号只读访问 |
| **Development** | 开发环境 | 开发者完全权限 |
| **Staging** | 测试/预发布环境 | 测试人员权限 |
| **Production** | 生产环境 | 严格限制,需要审批 |
**问题:Shared Services 账号里的镜像,怎么让 Production 账号的 EC2 拉取?**
@@ -498,6 +495,7 @@ MFAMulti-Factor Authentication,多因素认证),也叫 2FATwo-Factor
**步骤二:获取 Role ARN**
创建完成后,复制 Role 的 ARN:
```
arn:aws:iam::SHARED_SERVICES_ACCOUNT_ID:role/CrossAccountECRReadRole
```
@@ -691,38 +689,38 @@ aws ecr describe-repositories --registry-id SHARED_SERVICES_ACCOUNT_ID
### 8.1 十大 IAM 反模式
| # | 反模式 | 为什么不好 | 正确做法 |
| :--- | :--- | :--- | :--- |
| 1 | 使用根账号进行日常操作 | 根账号拥有所有权限,一旦泄露无法限制损害 | 创建 IAM 管理员账号,根账号仅在必要时使用 |
| 2 | 给所有人 AdministratorAccess | 违反最小权限原则,增加误操作和内部威胁风险 | 按角色分组,只授予必要的权限 |
| 3 | 在代码中硬编码 AK/SK | AK/SK 容易通过 GitHub 泄露,且难以轮换 | 使用 IAM Role、环境变量或密钥管理服务 |
| 4 | 长期不轮换 AK/SK | 增加凭证泄露后的风险敞口时间 | 设置 90 天轮换策略,或更好的——使用临时凭证 |
| 5 | 忽略 MFA | 密码泄露后账号直接沦陷 | 为所有 IAM 用户启用 MFA,尤其是高权限用户 |
| 6 | 不使用 CloudTrail | 无法审计谁做了什么操作,出事后无法溯源 | 启用 CloudTrail,并将日志存储到独立的审计账号 |
| 7 | IAM Policy 过于宽松 | 如 `Resource: "*"``Action: "*"`,增加攻击面 | 明确指定资源 ARN 和具体 Action |
| 8 | 不清理离职员工的 IAM User | 僵尸账号可能成为后门 | 建立离职流程,立即禁用并删除 IAM User |
| 9 | 不使用 IAM Access Analyzer | 无法发现过度宽松的资源策略(如公开 S3 bucket | 启用 IAM Access Analyzer,定期检查外部访问 |
| 10 | 不在测试环境验证 Policy | 直接在生产环境应用 Policy,可能导致服务中断 | 使用 IAM Policy Simulator 测试,先在测试环境验证 |
| # | 反模式 | 为什么不好 | 正确做法 |
| :-- | :--------------------------- | :--------------------------------------------- | :----------------------------------------------- |
| 1 | 使用根账号进行日常操作 | 根账号拥有所有权限,一旦泄露无法限制损害 | 创建 IAM 管理员账号,根账号仅在必要时使用 |
| 2 | 给所有人 AdministratorAccess | 违反最小权限原则,增加误操作和内部威胁风险 | 按角色分组,只授予必要的权限 |
| 3 | 在代码中硬编码 AK/SK | AK/SK 容易通过 GitHub 泄露,且难以轮换 | 使用 IAM Role、环境变量或密钥管理服务 |
| 4 | 长期不轮换 AK/SK | 增加凭证泄露后的风险敞口时间 | 设置 90 天轮换策略,或更好的——使用临时凭证 |
| 5 | 忽略 MFA | 密码泄露后账号直接沦陷 | 为所有 IAM 用户启用 MFA,尤其是高权限用户 |
| 6 | 不使用 CloudTrail | 无法审计谁做了什么操作,出事后无法溯源 | 启用 CloudTrail,并将日志存储到独立的审计账号 |
| 7 | IAM Policy 过于宽松 | 如 `Resource: "*"``Action: "*"`,增加攻击面 | 明确指定资源 ARN 和具体 Action |
| 8 | 不清理离职员工的 IAM User | 僵尸账号可能成为后门 | 建立离职流程,立即禁用并删除 IAM User |
| 9 | 不使用 IAM Access Analyzer | 无法发现过度宽松的资源策略(如公开 S3 bucket | 启用 IAM Access Analyzer,定期检查外部访问 |
| 10 | 不在测试环境验证 Policy | 直接在生产环境应用 Policy,可能导致服务中断 | 使用 IAM Policy Simulator 测试,先在测试环境验证 |
---
## 9. 名词对照表
| 英文术语 | 中文对照 | 解释 |
| :--- | :--- | :--- |
| **IAM (Identity and Access Management)** | 身份与访问管理 | 云服务中管理用户身份和访问权限的服务 |
| **RAM (Resource Access Management)** | 资源访问管理 | 阿里云的 IAM 服务名称 |
| **Root Account** | 根账号 | 注册云账号时创建的拥有者账号,拥有最高权限 |
| **IAM User** | IAM 用户/子账号 | 由根账号创建的子身份,用于日常操作 |
| **IAM Role** | IAM 角色 | 临时性权限载体,无长期凭证,需要被"扮演" |
| **IAM Policy** | IAM 策略 | JSON 格式的权限规则定义 |
| **ARN** | 亚马逊资源名称 | 全局唯一的资源标识符 |
| **AK/SK** | 访问密钥/密钥 | 程序访问云 API 的凭证 |
| **STS** | 安全令牌服务 | 提供临时安全凭证的服务 |
| **MFA** | 多因素认证 | 需要两个或以上因素的认证方式 |
| **SSO** | 单点登录 | 用户一次登录即可访问多个系统的认证方式 |
| **ExternalId** | 外部 ID | 用于防止困惑代理攻击的安全标识符 |
| **CloudTrail** | 云审计服务 | 记录云账号中所有 API 调用和操作的日志服务 |
| 英文术语 | 中文对照 | 解释 |
| :--------------------------------------- | :-------------- | :----------------------------------------- |
| **IAM (Identity and Access Management)** | 身份与访问管理 | 云服务中管理用户身份和访问权限的服务 |
| **RAM (Resource Access Management)** | 资源访问管理 | 阿里云的 IAM 服务名称 |
| **Root Account** | 根账号 | 注册云账号时创建的拥有者账号,拥有最高权限 |
| **IAM User** | IAM 用户/子账号 | 由根账号创建的子身份,用于日常操作 |
| **IAM Role** | IAM 角色 | 临时性权限载体,无长期凭证,需要被"扮演" |
| **IAM Policy** | IAM 策略 | JSON 格式的权限规则定义 |
| **ARN** | 亚马逊资源名称 | 全局唯一的资源标识符 |
| **AK/SK** | 访问密钥/密钥 | 程序访问云 API 的凭证 |
| **STS** | 安全令牌服务 | 提供临时安全凭证的服务 |
| **MFA** | 多因素认证 | 需要两个或以上因素的认证方式 |
| **SSO** | 单点登录 | 用户一次登录即可访问多个系统的认证方式 |
| **ExternalId** | 外部 ID | 用于防止困惑代理攻击的安全标识符 |
| **CloudTrail** | 云审计服务 | 记录云账号中所有 API 调用和操作的日志服务 |
---
@@ -756,6 +754,7 @@ aws ecr describe-repositories --registry-id SHARED_SERVICES_ACCOUNT_ID
---
> **延伸阅读**
>
> - [AWS IAM 官方文档](https://docs.aws.amazon.com/iam/)
> - [阿里云 RAM 官方文档](https://www.aliyun.com/product/ram)
> - [AWS IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)
+212 -122
View File
@@ -31,13 +31,13 @@
**核心区别一览**
| 维度 | 传统文件系统 | 对象存储 |
| :--- | :--- | :--- |
| **组织方式** | 层级目录树 | 扁平键值对 |
| **访问协议** | POSIX(本地文件操作) | HTTP/REST API |
| **扩展性** | 单机容量有限 | 近乎无限水平扩展 |
| **元数据** | 基础属性(大小、时间) | 丰富的自定义元数据 |
| **典型场景** | 本地办公文档 | 图片/视频/备份/静态资源 |
| 维度 | 传统文件系统 | 对象存储 |
| :----------- | :--------------------- | :---------------------- |
| **组织方式** | 层级目录树 | 扁平键值对 |
| **访问协议** | POSIX(本地文件操作) | HTTP/REST API |
| **扩展性** | 单机容量有限 | 近乎无限水平扩展 |
| **元数据** | 基础属性(大小、时间) | 丰富的自定义元数据 |
| **典型场景** | 本地办公文档 | 图片/视频/备份/静态资源 |
### 1.2 对象存储的核心概念
@@ -46,6 +46,7 @@
桶是对象存储的顶级容器,相当于一个独立的命名空间。所有对象都必须存放在某个桶中。
**命名规则**(以阿里云 OSS 为例):
- 全局唯一:在整个云厂商的所有用户中不能重复
- 只能包含小写字母、数字和短横线
- 必须以小写字母或数字开头和结尾
@@ -73,11 +74,11 @@
对象存储提供多层权限控制:
| 层级 | 控制方式 | 典型场景 |
| :--- | :--- | :--- |
| **桶级别** | Bucket Policy(资源策略) | 禁止所有外网访问、只允许特定 IP |
| **对象级别** | ACL(访问控制列表) | 公开图片、私有文档 |
| **临时授权** | STS(安全令牌服务) | 前端直传、移动端上传 |
| 层级 | 控制方式 | 典型场景 |
| :----------- | :------------------------ | :------------------------------ |
| **桶级别** | Bucket Policy(资源策略) | 禁止所有外网访问、只允许特定 IP |
| **对象级别** | ACL(访问控制列表) | 公开图片、私有文档 |
| **临时授权** | STS(安全令牌服务) | 前端直传、移动端上传 |
**安全红线**:永远不要把 AccessKey ID 和 AccessKey Secret 写在前端代码里!正确做法是:前端向你的后端申请临时 STS 凭证,后端验证身份后返回带过期时间的临时凭证。
@@ -102,11 +103,13 @@
#### 边缘节点:离用户最近的"快递站"
边缘节点是 CDN 网络中最接近用户的层级,通常部署在:
- 运营商机房(联通/电信/移动)
- 大城市互联网交换中心
- 重要交通枢纽
**中国主要 CDN 节点分布**
- 一线城市:北京、上海、广州、深圳
- 二线城市:杭州、南京、成都、武汉、西安
- 海外:香港、新加坡、东京、硅谷、法兰克福
@@ -116,11 +119,13 @@
#### 源站:内容的"总仓库"
源站是 CDN 回源获取内容的地方,可以是:
- 对象存储(OSS/COS/S3
- 自建服务器(ECS/物理机)
- 负载均衡(SLB/CLB
**关键配置**
- **回源 HOST**:CDN 节点访问源站时使用的域名/IP
- **回源协议**HTTP 还是 HTTPS
- **回源端口**80、443 还是自定义端口
@@ -128,10 +133,12 @@
#### 中间层节点:"区域分拨中心"
在边缘节点和源站之间,CDN 通常还有一层或多层中间节点:
- **汇聚节点**:聚合多个边缘节点的回源请求,减少源站压力
- **区域中心**:负责一个大区的内容分发和调度
这种分层架构的好处:
1. **降低源站压力**:1000 个边缘节点的请求,可能只需要向源站发起 10 次
2. **提高命中率**:热门内容在中间层就被拦截,不需要回源
3. **故障隔离**:某条链路出问题,可以自动切换到其他路径
@@ -143,14 +150,17 @@
<CachePolicyDemo />
**Step 1DNS 解析**(智能调度)
```
用户输入:cdn.example.com/image.jpg
DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4
```
这里的关键是**智能 DNS**:根据用户的运营商、地理位置、节点负载,返回最优的 CDN 节点 IP。
**Step 2:边缘节点查找**(缓存命中?)
```
请求到达北京联通 CDN 节点(1.2.3.4
@@ -160,6 +170,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4
```
**Step 3:回源获取**(层层向上)
```
边缘节点未命中
@@ -173,6 +184,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4
```
**Step 4:缓存并返回**(下次更快)
```
内容沿链路返回
@@ -198,17 +210,20 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4
```
**流程**
1. 用户选择文件,点击上传
2. 文件先上传到你的后端服务器
3. 后端接收完整文件后,再转上传到对象存储
4. 返回上传结果给用户
**优点**
- 实现简单,前后端都好控制
- 可以在后端做文件校验、格式转换
- 敏感操作可以记录日志、做权限校验
**缺点**
- **带宽双吃**:用户上传占用一次带宽,服务器转传又占用一次
- **服务器压力大**:大文件会占用大量内存和 CPU
- **上传慢**:相当于多了一道中转,用户感知到的上传时间更长
@@ -224,6 +239,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4
```
**流程**
1. 用户选择文件,前端先向后端申请"上传凭证"
2. 后端验证用户身份,向对象存储服务申请**临时 STS 凭证**(带过期时间)
3. 后端把临时凭证返回给前端
@@ -231,12 +247,14 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4
5. 对象存储返回上传结果,前端通知后端"上传完成"
**优点**
- **上传快**:少了中转环节,用户感知速度最快
- **服务器压力小**:只处理凭证签发,不处理文件流
- **带宽省**:只走一次上传流量
- **安全性高**:临时凭证有过期时间,泄露也危害有限
**缺点**
- 实现稍复杂,需要理解 STS、签名机制
- 前端需要处理分片上传、断点续传等逻辑
- 跨域(CORS)需要配置
@@ -261,21 +279,21 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4
**为什么需要分片?**
| 场景 | 不分片 | 分片 |
| :--- | :--- | :--- |
| **网络波动** | 传了 99% 断网,全部重传 | 只重传失败的分片 |
| **上传速度** | 单线程,速度慢 | 多线程并行,速度快 |
| **内存占用** | 需要缓存整个文件 | 只需缓存当前分片 |
| **进度显示** | 只有 0% 和 100% | 精确到每个分片的进度 |
| 场景 | 不分片 | 分片 |
| :----------- | :---------------------- | :------------------- |
| **网络波动** | 传了 99% 断网,全部重传 | 只重传失败的分片 |
| **上传速度** | 单线程,速度慢 | 多线程并行,速度快 |
| **内存占用** | 需要缓存整个文件 | 只需缓存当前分片 |
| **进度显示** | 只有 0% 和 100% | 精确到每个分片的进度 |
**主流云厂商的分片规格**
| 厂商 | 分片大小限制 | 最大分片数 | 最小分片大小 |
| :--- | :--- | :--- | :--- |
| **阿里云 OSS** | 100MB | 10000 | 100KB |
| **腾讯云 COS** | 5GB | 10000 | 1MB |
| **AWS S3** | 5GB | 10000 | 5MB(推荐) |
| **七牛云** | 100MB | 10000 | 4MB |
| 厂商 | 分片大小限制 | 最大分片数 | 最小分片大小 |
| :------------- | :----------- | :--------- | :----------- |
| **阿里云 OSS** | 100MB | 10000 | 100KB |
| **腾讯云 COS** | 5GB | 10000 | 1MB |
| **AWS S3** | 5GB | 10000 | 5MB(推荐) |
| **七牛云** | 100MB | 10000 | 4MB |
### 3.2 CDN 回源策略详解
@@ -284,6 +302,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4
#### 什么是"回源"
CDN 边缘节点缓存了源站的内容,但当:
- 用户请求的内容**第一次被访问**
- 缓存的内容**已过期(TTL 到期)**
- 缓存被**手动刷新/预热**
@@ -292,11 +311,11 @@ CDN 节点就需要向**源站**请求最新内容,这个过程就叫"回源"
#### 回源的三种模式
| 模式 | 原理 | 适用场景 | 优缺点 |
| :--- | :--- | :--- | :--- |
| **直接回源** | CDN 节点 → 源站 | 源站有公网 IP,且流量不大 | 简单直接,但源站压力大 |
| **中间源回源** | CDN 节点 → 中间层 → 源站 | 大型网站,多层缓存架构 | 分担源站压力,架构复杂 |
| ** OSS/COS 作为源站** | CDN 节点 → 对象存储 | 静态资源、图片、视频 | 最佳实践,成本低、性能好 |
| 模式 | 原理 | 适用场景 | 优缺点 |
| :-------------------- | :----------------------- | :------------------------ | :----------------------- |
| **直接回源** | CDN 节点 → 源站 | 源站有公网 IP,且流量不大 | 简单直接,但源站压力大 |
| **中间源回源** | CDN 节点 → 中间层 → 源站 | 大型网站,多层缓存架构 | 分担源站压力,架构复杂 |
| ** OSS/COS 作为源站** | CDN 节点 → 对象存储 | 静态资源、图片、视频 | 最佳实践,成本低、性能好 |
#### 回源配置实战
@@ -315,6 +334,7 @@ CDN 节点就需要向**源站**请求最新内容,这个过程就叫"回源"
```
关键配置项:
- **源站类型**OSS/COS 域名 或 自定义源站
- **回源协议**HTTP 还是 HTTPS(建议 HTTPS
- **回源 HOST**:访问源站时使用的 Host 头
@@ -332,6 +352,7 @@ CDN 边缘节点
```
主备模式:
```
CDN 边缘节点
├─ 主源站 A (健康时全部流量)
@@ -342,12 +363,13 @@ CDN 边缘节点
这里有个容易混淆的概念:
| 指标 | 定义 | 计费关系 |
| :--- | :--- | :--- |
| **CDN 下行带宽** | 从 CDN 节点到用户的流量 | 通常按流量计费的 CDN 费用 |
| **回源带宽** | 从源站到 CDN 节点的流量 | 通常对象存储或源站出流量费用 |
| 指标 | 定义 | 计费关系 |
| :--------------- | :---------------------- | :--------------------------- |
| **CDN 下行带宽** | 从 CDN 节点到用户的流量 | 通常按流量计费的 CDN 费用 |
| **回源带宽** | 从源站到 CDN 节点的流量 | 通常对象存储或源站出流量费用 |
**省钱技巧**
- 提高 CDN 命中率(让更多请求命中缓存,减少回源)
- 设置合理的缓存时间(TTL
- 使用预热功能,在用户访问前就缓存热点内容
@@ -362,10 +384,12 @@ CDN 边缘节点
CDN 如何判断两次请求是否应该返回同一个缓存副本?靠的就是**缓存键**。
**默认缓存键通常包括**
- URL 路径(不含查询参数)
- 例如:`/images/photo.jpg`
**问题场景**
```
用户 A 请求:/images/photo.jpg?w=100&h=100 (100x100 缩略图)
用户 B 请求:/images/photo.jpg?w=800&h=600 (800x600 大图)
@@ -375,14 +399,15 @@ CDN 如何判断两次请求是否应该返回同一个缓存副本?靠的就
**解决方案:自定义缓存键规则**
| 规则 | 示例 | 效果 |
| :--- | :--- | :--- |
| **保留指定查询参数** | 保留 `w``h` | 不同尺寸分别缓存 |
| **保留所有查询参数** | 保留全部 | 完全精确匹配 |
| 规则 | 示例 | 效果 |
| :------------------- | :------------------------ | :------------------------ |
| **保留指定查询参数** | 保留 `w``h` | 不同尺寸分别缓存 |
| **保留所有查询参数** | 保留全部 | 完全精确匹配 |
| **忽略特定查询参数** | 忽略 `token``timestamp` | 带时间戳的 URL 能命中缓存 |
| **包含请求头** | 包含 `Accept-Language` | 不同语言返回不同内容 |
| **包含请求头** | 包含 `Accept-Language` | 不同语言返回不同内容 |
**实战配置示例**(阿里云 CDN):
```
缓存键规则:
- URL 路径:/images/*
@@ -396,13 +421,13 @@ TTLTime To Live)决定了内容在 CDN 节点上缓存多久。设置太短
**按文件类型设置 TTL 的建议**:
| 文件类型 | 建议 TTL | 原因 |
| :--- | :--- | :--- |
| HTML 页面 | 0-5 分钟 | 内容频繁更新,需要实时 |
| 文件类型 | 建议 TTL | 原因 |
| :---------- | :---------------------- | :----------------------------- |
| HTML 页面 | 0-5 分钟 | 内容频繁更新,需要实时 |
| JS/CSS 文件 | 1 年(配合文件名 hash) | 内容不变,文件名变化即缓存失效 |
| 图片/视频 | 7-30 天 | 更新频率低,可长期缓存 |
| 字体文件 | 1 年 | 几乎不变 |
| API 响应 | 0-5 分钟(视业务) | 数据实时性要求高 |
| 图片/视频 | 7-30 天 | 更新频率低,可长期缓存 |
| 字体文件 | 1 年 | 几乎不变 |
| API 响应 | 0-5 分钟(视业务) | 数据实时性要求高 |
**前端工程化配合 CDN 的最佳实践**:
@@ -425,11 +450,11 @@ output: {
当你更新了源站内容,但 CDN 缓存还没过期,用户看到的还是旧内容:
| 刷新类型 | 效果 | 耗时 | 适用场景 |
| :--- | :--- | :--- | :--- |
| **URL 刷新** | 指定 URL 的缓存失效 | 5-10 分钟 | 单个文件更新 |
| **目录刷新** | 指定目录下所有内容失效 | 10-30 分钟 | 批量更新 |
| **全站刷新** | 整个域名的缓存全部失效 | 30 分钟以上 | 紧急回滚 |
| 刷新类型 | 效果 | 耗时 | 适用场景 |
| :----------- | :--------------------- | :---------- | :----------- |
| **URL 刷新** | 指定 URL 的缓存失效 | 5-10 分钟 | 单个文件更新 |
| **目录刷新** | 指定目录下所有内容失效 | 10-30 分钟 | 批量更新 |
| **全站刷新** | 整个域名的缓存全部失效 | 30 分钟以上 | 紧急回滚 |
**重要提醒**:刷新只是让缓存失效,下次请求会回源拉取新内容。不要在高峰期大批量刷新,否则可能导致源站被打爆。
@@ -458,12 +483,14 @@ output: {
### 4.1 智能 DNS 调度
传统 DNS 解析:
```
用户问:cdn.example.com 的 IP 是什么?
DNS 答:1.2.3.4(固定的)
```
智能 DNS 解析:
```
用户(北京联通)问:cdn.example.com 的 IP 是什么?
智能 DNS:让我查查... 北京联通的 CDN 节点是 1.2.3.4
@@ -486,6 +513,7 @@ DNS 答:1.2.3.4(固定的)
传统 DNS 有个问题:**DNS 劫持和解析延迟**。
**HTTP DNS 方案**
```
客户端 → 绕过系统 DNS → 直接问 HTTP DNS 服务(如 223.5.5.5:80
@@ -495,11 +523,13 @@ DNS 答:1.2.3.4(固定的)
```
优势:
- 防劫持:不走运营商 DNS
- 更精准:可以按客户端网络质量选择 IP
- 实时性:故障切换更快
**实战建议**
- 移动端 APP 强烈建议接入 HTTP DNS
- Web 端可以使用 CDN 提供的 CNAME 调度
- 关键业务可以做多 IP 容灾(一个域名返回多个 IP)
@@ -513,6 +543,7 @@ DNS 答:1.2.3.4(固定的)
### 5.1 为什么 CDN 上 HTTPS 很重要?
**场景对比**
```
无 HTTPS
用户访问 http://cdn.example.com/image.jpg
@@ -539,14 +570,15 @@ HTTP/2 多路复用生效
#### 证书管理
| 方案 | 说明 | 成本 | 适用场景 |
| :--- | :--- | :--- | :--- |
| **云厂商免费证书** | 阿里云/腾讯云等提供 | 免费 | 单域名,快速上手 |
| **Let's Encrypt** | 社区免费证书 | 免费 | 自动化部署 |
| 方案 | 说明 | 成本 | 适用场景 |
| :--------------------- | :-------------------- | :------------- | :--------------- |
| **云厂商免费证书** | 阿里云/腾讯云等提供 | 免费 | 单域名,快速上手 |
| **Let's Encrypt** | 社区免费证书 | 免费 | 自动化部署 |
| **商业 DV/OV/EV 证书** | 赛门铁克、GeoTrust 等 | ¥几百-几万/年 | 企业级、需要绿条 |
| **泛域名证书** | *.example.com | ¥几千/年 | 多子域名 |
| **泛域名证书** | \*.example.com | ¥几千/年 | 多子域名 |
**实战建议**
- 测试环境:Let's Encrypt 或云厂商免费证书
- 生产环境:泛域名证书(省事)或单域名 OV 证书(省钱)
- 注意证书过期时间,设置自动续期提醒
@@ -554,18 +586,21 @@ HTTP/2 多路复用生效
#### HTTPS 优化配置
**TLS 版本选择**
```
推荐配置:仅 TLS 1.2 和 TLS 1.3
兼容配置:TLS 1.1 + TLS 1.2 + TLS 1.3(兼容老旧浏览器)
```
**密码套件**
```
推荐:ECDHE 密钥交换 + AES-GCM 加密
禁用:DES、RC4、MD5、SHA1
```
**OCSP Stapling**
```
功能:CDN 节点预获取证书吊销状态
效果:减少客户端验证时间 200-500ms
@@ -573,6 +608,7 @@ HTTP/2 多路复用生效
```
**TLS 会话复用**
```
Session ID 复用:客户端带着上次 Session ID,服务端恢复会话
Session Ticket 复用:服务端把会话状态加密发给客户端,下次带来
@@ -582,6 +618,7 @@ Session Ticket 复用:服务端把会话状态加密发给客户端,下次
### 5.3 HTTP/2 与 HTTP/3 在 CDN 上的应用
**HTTP/2 多路复用**
```
HTTP/1.1
请求 1 (index.html) ────────────────→
@@ -603,6 +640,7 @@ HTTP/2
```
**HTTP/2 服务端推送**
```
场景:用户请求 index.html,里面引用了 style.css 和 script.js
@@ -620,6 +658,7 @@ HTTP/2 推送:
```
**HTTP/3 (QUIC)**
```
HTTP/2 的问题:基于 TCP,队头阻塞
→ 一个 TCP 包丢失,整个连接等待重传
@@ -654,6 +693,7 @@ CDN 带宽 = 所有边缘节点的出流量总和
```
**带宽与流量的关系**
```
1 Mbps 带宽持续跑 1 小时 = 450 MB 流量
(计算:1,000,000 bps × 3600s ÷ 8 ÷ 1024 ÷ 1024 ≈ 429 MB
@@ -689,13 +729,13 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
**命中率低的常见原因**
| 原因 | 现象 | 解决方案 |
| :--- | :--- | :--- |
| 缓存时间太短 | TTL 只有几分钟 | 根据文件类型调整 TTL |
| 查询参数变化 | URL 带随机数 | 配置忽略特定参数 |
| 缓存键设置不当 | 不该区分的被区分了 | 优化缓存键规则 |
| 内容更新频繁 | 文件经常被覆盖 | 使用版本号或 hash 文件名 |
| 首次访问多 | 新内容或新节点 | 提前预热 |
| 原因 | 现象 | 解决方案 |
| :------------- | :----------------- | :----------------------- |
| 缓存时间太短 | TTL 只有几分钟 | 根据文件类型调整 TTL |
| 查询参数变化 | URL 带随机数 | 配置忽略特定参数 |
| 缓存键设置不当 | 不该区分的被区分了 | 优化缓存键规则 |
| 内容更新频繁 | 文件经常被覆盖 | 使用版本号或 hash 文件名 |
| 首次访问多 | 新内容或新节点 | 提前预热 |
### 6.2 日志分析与问题排查
@@ -712,18 +752,19 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
关键字段解释:
| 字段 | 说明 | 分析价值 |
| :--- | :--- | :--- |
| `cache_status` | 缓存状态 | HIT(命中)、MISS(未命中)、EXPIRED(过期) |
| `response_time` | 响应时间(ms) | 判断用户体验,>500ms 需优化 |
| `http_status` | HTTP 状态码 | 404/500 错误排查 |
| `bytes_sent` | 发送字节数 | 带宽统计 |
| 字段 | 说明 | 分析价值 |
| :-------------- | :------------- | :------------------------------------------- |
| `cache_status` | 缓存状态 | HIT(命中)、MISS(未命中)、EXPIRED(过期) |
| `response_time` | 响应时间(ms) | 判断用户体验,>500ms 需优化 |
| `http_status` | HTTP 状态码 | 404/500 错误排查 |
| `bytes_sent` | 发送字节数 | 带宽统计 |
#### 常见问题排查
**问题 1:用户反映访问慢**
排查步骤:
```
1. 看日志 response_time
- 如果很大(>500ms):检查是缓存 MISS 还是源站慢
@@ -739,6 +780,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
**问题 2:缓存不生效,每次都回源**
排查清单:
```
□ 源站响应头是否有 Cache-Control: no-cache / private
□ URL 是否带随机参数(如 ?_=123456)?
@@ -750,6 +792,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
**问题 3:费用暴涨**
排查方向:
```
1. 看账单明细
- CDN 流量费高:检查是否有大文件被频繁访问,或被盗链
@@ -848,6 +891,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
#### 对象存储配置
**存储桶规划**
```
Bucket: myapp-images-prod
├─ 目录结构:
@@ -872,6 +916,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
```
**CORS 跨域配置**
```xml
<CORSConfiguration>
<CORSRule>
@@ -890,6 +935,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
#### CDN 加速配置
**缓存策略配置**
```
全局默认规则:
├─ 缓存键:URL 路径 + 保留 w、h、format 查询参数
@@ -916,6 +962,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
```
**HTTPS 优化配置**
```
证书配置:
├─ 证书类型:泛域名证书 *.myapp.com
@@ -1031,14 +1078,14 @@ rules:
```yaml
# 带宽封顶配置
bandwidth_cap:
daily_limit: 500 # Mbps,日峰值超过则自动停用 CDN
monthly_limit: 10000 # GB,月流量超过则停用
daily_limit: 500 # Mbps,日峰值超过则自动停用 CDN
monthly_limit: 10000 # GB,月流量超过则停用
# 告警阈值
alerts:
- threshold: 70% # 达到 70% 发告警
- threshold: 70% # 达到 70% 发告警
channels: [sms, email]
- threshold: 90% # 达到 90% 打电话
- threshold: 90% # 达到 90% 打电话
channels: [phone]
```
@@ -1049,12 +1096,14 @@ bandwidth_cap:
### 8.1 架构设计原则
**原则 1:动静分离**
```
动态内容(API、HTML)→ 走源站或边缘函数
静态内容(图片、JS、CSS、视频)→ 走 CDN + 对象存储
```
**原则 2:就近服务**
```
用户在哪里,内容就缓存到哪里
→ 选择覆盖广的 CDN 服务商
@@ -1063,6 +1112,7 @@ bandwidth_cap:
```
**原则 3:分层缓存**
```
浏览器本地缓存(最强)
@@ -1074,6 +1124,7 @@ CDN 中间层/区域节点(兜底)
```
**原则 4:成本与体验的平衡**
```
存储分级:热数据标准存储,冷数据归档存储
缓存策略:高频内容长 TTL,低频内容短 TTL
@@ -1084,24 +1135,28 @@ CDN 中间层/区域节点(兜底)
### 8.2 避坑清单
**存储桶命名与权限**
- [ ] 桶名全局唯一,避免被占用
- [ ] 私有文件不要设置为公共读
- [ ] AccessKey 不要写在前端代码里,用 STS 临时凭证
- [ ] 启用服务端加密(SSE)保护敏感数据
**CDN 缓存配置**
- [ ] HTML 文件 TTL 不要太长(建议 < 5 分钟)
- [ ] JS/CSS 建议用带 hash 的文件名,TTL 设为 1 年
- [ ] 缓存键要合理,不要把用户信息等变量放进去
- [ ] 重要更新后记得刷新缓存或预热
**HTTPS 安全**
- [ ] 证书不要过期,设置自动续期
- [ ] 最低 TLS 版本建议 1.2
- [ ] 开启 HSTS 防止降级攻击
- [ ] 敏感 Cookie 设置 Secure 和 HttpOnly
**成本控制**
- [ ] 开启带宽封顶告警,防止异常流量
- [ ] 低频/归档存储有最小存储时间和提前删除费,注意规则
- [ ] 回源流量费也很贵,努力提高 CDN 命中率
@@ -1170,7 +1225,12 @@ class DirectUploader {
const formData = new FormData()
// 构建表单字段(不同厂商字段名不同)
const formFields = this._buildFormFields(credentials, fileKey, file.type, options)
const formFields = this._buildFormFields(
credentials,
fileKey,
file.type,
options
)
Object.entries(formFields).forEach(([key, value]) => {
formData.append(key, value)
})
@@ -1210,7 +1270,11 @@ class DirectUploader {
const fileKey = this._generateFileKey(file, options.directory)
// 1. 初始化分片上传
const uploadId = await this._initMultipartUpload(credentials, fileKey, file.type)
const uploadId = await this._initMultipartUpload(
credentials,
fileKey,
file.type
)
// 2. 计算分片
const parts = []
@@ -1232,21 +1296,30 @@ class DirectUploader {
// 支持断点续传:检查哪些分片已上传
if (options.resume) {
const existingParts = await this._listParts(credentials, fileKey, uploadId)
const existingParts = await this._listParts(
credentials,
fileKey,
uploadId
)
for (const part of existingParts) {
uploadedParts.push(part)
}
}
// 过滤出未上传的分片
const pendingParts = parts.filter(p =>
!uploadedParts.some(up => up.partNumber === p.number)
const pendingParts = parts.filter(
(p) => !uploadedParts.some((up) => up.partNumber === p.number)
)
// 并发上传
const uploadPart = async (part) => {
try {
const etag = await this._uploadPart(credentials, fileKey, uploadId, part)
const etag = await this._uploadPart(
credentials,
fileKey,
uploadId,
part
)
return { partNumber: part.number, etag }
} catch (error) {
failedParts.push({ part, error })
@@ -1271,11 +1344,18 @@ class DirectUploader {
// 检查是否所有分片都上传成功
if (uploadedParts.length !== totalParts) {
throw new Error(`Upload incomplete: ${uploadedParts.length}/${totalParts} parts uploaded`)
throw new Error(
`Upload incomplete: ${uploadedParts.length}/${totalParts} parts uploaded`
)
}
// 4. 完成分片上传(合并分片)
await this._completeMultipartUpload(credentials, fileKey, uploadId, uploadedParts)
await this._completeMultipartUpload(
credentials,
fileKey,
uploadId,
uploadedParts
)
return {
url: this._getFileUrl(fileKey),
@@ -1316,7 +1396,11 @@ class DirectUploader {
_getFileUrl(key) {
return `https://${this.bucket}.${this.provider === 'oss' ? 'oss' : 'cos'}-${this.region}.${
this.provider === 'oss' ? 'aliyuncs.com' : this.provider === 'cos' ? 'myqcloud.com' : 'amazonaws.com'
this.provider === 'oss'
? 'aliyuncs.com'
: this.provider === 'cos'
? 'myqcloud.com'
: 'amazonaws.com'
}/${key}`
}
@@ -1385,7 +1469,9 @@ async function uploadVideo(file) {
parallel: 3, // 3 个并发
resume: true, // 支持断点续传
onProgress: (progress) => {
console.log(`上传进度: ${progress.percent}%, 已传 ${progress.loaded}/${progress.total}`)
console.log(
`上传进度: ${progress.percent}%, 已传 ${progress.loaded}/${progress.total}`
)
},
onPartComplete: (part) => {
console.log(`分片 ${part.number} 上传完成`)
@@ -1464,9 +1550,7 @@ router.post('/credentials', async (req, res) => {
'oss:AbortMultipartUpload',
'oss:ListParts'
],
Resource: [
`acs:oss:*:*:${config.oss.bucket}/${prefix}*`
]
Resource: [`acs:oss:*:*:${config.oss.bucket}/${prefix}*`]
}
],
Version: '1'
@@ -1495,7 +1579,13 @@ router.post('/credentials', async (req, res) => {
prefix: prefix, // 文件路径前缀
// 安全限制
maxSize: 100 * 1024 * 1024, // 最大 100MB
allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4']
allowedTypes: [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'video/mp4'
]
}
}
})
@@ -1565,27 +1655,27 @@ module.exports = router
const refererConfig = {
// 白名单模式:只允许以下 Referer 访问
allowList: [
'*.myapp.com', // 主站
'*.myapp.cn', // 国内站
'localhost:*', // 本地开发
'*.myapp.com', // 主站
'*.myapp.cn', // 国内站
'localhost:*', // 本地开发
'127.0.0.1:*'
],
// 黑名单模式(可选):禁止以下 Referer
blockList: [
'*. competitor.com', // 竞争对手
'*. competitor.com', // 竞争对手
'spam-site.com'
],
// 空 Referer 处理:是否允许直接访问(浏览器地址栏输入 URL)
allowEmptyReferer: false // 生产环境建议 false,测试环境可 true
allowEmptyReferer: false // 生产环境建议 false,测试环境可 true
}
// 2. URL 鉴权(更安全的防盗链,带时间戳和签名)
class URLAuth {
constructor(config) {
this.key = config.key // 鉴权密钥,只在服务端保存
this.expireTime = config.expireTime || 3600 // 默认 1 小时有效期
this.key = config.key // 鉴权密钥,只在服务端保存
this.expireTime = config.expireTime || 3600 // 默认 1 小时有效期
}
/**
@@ -1645,11 +1735,14 @@ class URLAuth {
// 使用示例
const auth = new URLAuth({
key: 'your-secret-key-only-known-by-server',
expireTime: 3600 // 1 小时有效期
expireTime: 3600 // 1 小时有效期
})
// 服务端生成带签名的 URL
const signedUrl = auth.sign('https://cdn.example.com/private/document.pdf', 7200)
const signedUrl = auth.sign(
'https://cdn.example.com/private/document.pdf',
7200
)
// 结果:https://cdn.example.com/private/document.pdf?sign=xxxxx&t=1699123456
// CDN 边缘或源站验证
@@ -1662,15 +1755,12 @@ if (!result.valid) {
const ipConfig = {
// 只允许特定 IP 访问(适合内部系统)
whiteList: [
'192.168.1.0/24', // 内网网段
'192.168.1.0/24', // 内网网段
'10.0.0.0/8'
],
// 禁止特定 IP 访问(封禁攻击者)
blackList: [
'1.2.3.4',
'5.6.7.8'
]
blackList: ['1.2.3.4', '5.6.7.8']
}
// 4. UAUser-Agent)黑白名单
@@ -1687,7 +1777,7 @@ const uaConfig = {
// 只允许浏览器访问(严格模式)
whiteList: [
'Mozilla/*', // 现代浏览器
'Mozilla/*', // 现代浏览器
'AppleWebKit/*'
]
}
@@ -1697,28 +1787,28 @@ const uaConfig = {
## 10. 名词对照表
| 英文术语 | 中文对照 | 解释 |
| :--- | :--- | :--- |
| **Object Storage** | 对象存储 | 一种数据存储架构,将数据作为对象管理,而非文件系统层级结构。适合存储图片、视频、备份等非结构化数据。 |
| **Bucket** | 存储桶 | 对象存储中的顶级容器,用于组织和隔离数据。每个桶有独立的权限控制和配置。 |
| **Object** | 对象/文件对象 | 对象存储的基本单元,包含数据本身、元数据(Metadata)和全局唯一键(Key)。 |
| **CDN** | 内容分发网络 | Content Delivery Network,通过在全球部署边缘节点,将网站内容缓存到离用户最近的位置,加速访问速度。 |
| **Edge Node** | 边缘节点 | CDN 网络中部署在各地的缓存服务器,直接为用户提供内容访问服务。 |
| **Origin** | 源站 | CDN 回源获取内容的服务器,可以是对象存储、ECS 或自建服务器。 |
| **Cache Hit** | 缓存命中 | 用户请求的内容在 CDN 边缘节点已存在,直接返回,无需回源。 |
| **Cache Miss** | 缓存未命中 | 边缘节点没有请求的内容,需要回源获取。 |
| **Hit Ratio** | 命中率 | 缓存命中次数占总请求次数的比例。命中率越高,回源越少,成本越低。 |
| **TTL** | 生存时间/缓存时间 | Time To Live,内容在 CDN 节点上缓存的有效期。过期后需要重新回源。 |
| **Back to Source** | 回源 | CDN 边缘节点向源站请求内容的过程。 |
| **Purge/Refresh** | 刷新缓存 | 强制使 CDN 缓存失效,下次请求回源获取最新内容。 |
| **Preheat** | 预热 | 在正式发布前,主动将内容推送到 CDN 节点,让用户第一次访问就能命中缓存。 |
| **CORS** | 跨域资源共享 | Cross-Origin Resource Sharing,浏览器的安全机制,控制不同域之间的资源访问。 |
| **Referer** | 来源页面 | HTTP 请求头字段,指示请求是从哪个页面链接过来的。用于防盗链。 |
| **STS** | 安全令牌服务 | Security Token Service,颁发临时访问凭证的服务,用于前端直传等场景。 |
| **Multipart Upload** | 分片上传 | 将大文件切分成多个小分片并行上传,支持断点续传,提高上传效率和可靠性。 |
| **ETag** | 实体标签 | HTTP 响应头,用于标识资源的特定版本,常用于缓存验证。 |
| **S3 API** | S3 兼容接口 | AWS S3 的对象存储 API 规范,多数云厂商的对象存储都兼容此接口。 |
| **Canonical Query String** | 规范查询字符串 | 签名字符串的一部分,用于计算请求签名,确保请求不被篡改。 |
| 英文术语 | 中文对照 | 解释 |
| :------------------------- | :---------------- | :--------------------------------------------------------------------------------------------------- |
| **Object Storage** | 对象存储 | 一种数据存储架构,将数据作为对象管理,而非文件系统层级结构。适合存储图片、视频、备份等非结构化数据。 |
| **Bucket** | 存储桶 | 对象存储中的顶级容器,用于组织和隔离数据。每个桶有独立的权限控制和配置。 |
| **Object** | 对象/文件对象 | 对象存储的基本单元,包含数据本身、元数据(Metadata)和全局唯一键(Key)。 |
| **CDN** | 内容分发网络 | Content Delivery Network,通过在全球部署边缘节点,将网站内容缓存到离用户最近的位置,加速访问速度。 |
| **Edge Node** | 边缘节点 | CDN 网络中部署在各地的缓存服务器,直接为用户提供内容访问服务。 |
| **Origin** | 源站 | CDN 回源获取内容的服务器,可以是对象存储、ECS 或自建服务器。 |
| **Cache Hit** | 缓存命中 | 用户请求的内容在 CDN 边缘节点已存在,直接返回,无需回源。 |
| **Cache Miss** | 缓存未命中 | 边缘节点没有请求的内容,需要回源获取。 |
| **Hit Ratio** | 命中率 | 缓存命中次数占总请求次数的比例。命中率越高,回源越少,成本越低。 |
| **TTL** | 生存时间/缓存时间 | Time To Live,内容在 CDN 节点上缓存的有效期。过期后需要重新回源。 |
| **Back to Source** | 回源 | CDN 边缘节点向源站请求内容的过程。 |
| **Purge/Refresh** | 刷新缓存 | 强制使 CDN 缓存失效,下次请求回源获取最新内容。 |
| **Preheat** | 预热 | 在正式发布前,主动将内容推送到 CDN 节点,让用户第一次访问就能命中缓存。 |
| **CORS** | 跨域资源共享 | Cross-Origin Resource Sharing,浏览器的安全机制,控制不同域之间的资源访问。 |
| **Referer** | 来源页面 | HTTP 请求头字段,指示请求是从哪个页面链接过来的。用于防盗链。 |
| **STS** | 安全令牌服务 | Security Token Service,颁发临时访问凭证的服务,用于前端直传等场景。 |
| **Multipart Upload** | 分片上传 | 将大文件切分成多个小分片并行上传,支持断点续传,提高上传效率和可靠性。 |
| **ETag** | 实体标签 | HTTP 响应头,用于标识资源的特定版本,常用于缓存验证。 |
| **S3 API** | S3 兼容接口 | AWS S3 的对象存储 API 规范,多数云厂商的对象存储都兼容此接口。 |
| **Canonical Query String** | 规范查询字符串 | 签名字符串的一部分,用于计算请求签名,确保请求不被篡改。 |
---
File diff suppressed because it is too large Load Diff
+373 -336
View File
@@ -1,424 +1,461 @@
# 前端开发入门:从"贴海报"到"搭乐高" (Interactive Intro)
# 前端开发演进史:从"贴海报"到"搭乐高"
> 💡 **学习指南**:本章节无需编程基础,通过交互式演示带你回顾前端开发的 20 年变迁。我们将从最基础的 HTML 讲起,一直到现代的 Vue/React 组件化开发。
::: tip 🎯 核心问题
**为什么网页越来越复杂?前端技术为什么要不断演进?** 这个问题会带你理解从简单网页到现代 Web 应用的技术演变之路。
:::
先把几个最常见的新名词说清楚(后面会反复出现):
---
- **HTML**:网页的"骨架",负责内容和结构(标题、段落、图片、按钮)。
- **CSS**:网页的"皮肤",负责样式(颜色、大小、布局、动画)。
- **JavaScript**:网页的"肌肉",负责交互与逻辑(点击、输入、请求数据)。
- **框架(Framework**:一套成熟的开发方式和工具,让你更高效地做复杂页面(比如 Vue/React)。
## 1. 为什么要关注前端演进史?
### 1.1 从"电子海报"到"桌面应用"
想象一下你在街上看到的**海报**:
- ✅ 有内容(文字、图片)
- ✅ 有设计(颜色、排版)
- ❌ 但你跟它说话,它不会回应
- ❌ 你点击某个地方,不会发生什么
**最早的网页**就是这样的"电子海报":只能看、不能改、内容固定。
**现代网页**完全不同了。它们像**桌面应用**(VS Code、Figma):
- ✅ 可以编辑文档、画图、玩游戏
- ✅ 实时响应你的每个操作
- ✅ 甚至可以离线工作
**这种转变的核心原因: 网页的功能越来越复杂,需要更高效的技术和开发方式。**
### 1.2 一个生活的比喻:盖房子
前端技术的演进,就像盖房子方式的进化:
| 时代 | 🏠 盖房比喻 | 实际特点 | 优缺点 |
| --------- | ------------------ | ---------------------------- | --------------------------- |
| **2000s** | **贴海报** | 静态网页,写好 HTML 就行 | ✅ 简单 ❌ 不能互动 |
| **2010s** | **请工人手动装修** | jQuery 时代,手动操作每个元素 | ✅ 能互动 ❌ 代码乱、难维护 |
| **2020s** | **用乐高搭房子** | Vue/React 时代,组件化开发 | ✅ 高效、可维护 ❌ 学习曲线 |
::: tip 💡 从表格中你能看到什么?
**阶段一 → 阶段二**: 从"不能动"到"能动"。这是质的飞跃——网页开始有交互,但代价是代码变得混乱。
**阶段二 → 阶段三**: 从"能用"到"好用"。组件化让代码像积木一样可复用,大幅提升开发效率。
**核心思想**: 技术演进不是"为了新而新",而是为了解决上一个阶段的痛点。
:::
---
## 2. 第一阶段:静态网页与"切图"(2000s)
<FrontendEvolutionDemo />
## 0. 引言:网页为什么越来越难做?
### 2.1 这个时代是什么样的?
最早的网页,只是**电子海报**——就像你在街上看到的纸质海报,只能看、不能互动。
**开发方式**:
现在的网页,是**桌面级应用** (如 VS Code, Figma)——可以编辑文档、画图、玩游戏,甚至剪辑视频。
- 写几个 HTML 文件
- 内嵌一些 CSS 和 JavaScript
- 直接把文件拖到浏览器就能看效果
- 上传文件夹到服务器就完成部署
为了支撑这种转变,前端技术经历了一场从 "手工作坊" 到 "工业化生产" 的革命。
**特点**:
### 一个生活的比喻
- ✅ **优点**: 简单直接,没有学习成本,写完就能跑
- ❌ **缺点**: 无法实现复杂交互,代码一多就乱
想象你要盖房子:
::: details 查看当时的项目结构
- **2000 年代(静态网页)**:就像**贴海报**。你画好一张图,贴到墙上就完事了,不能改动。
- **2010 年代(jQuery 时代)**:就像**请工人手动装修**。你需要亲自告诉工人:"把这块墙涂成蓝色"、"把那扇窗户打开"。工人很多、指令很杂,容易出错。
- **2020 年代(Vue/React 时代)**:就像**用乐高积木搭房子**。你先设计好"房子长什么样"(设计图),然后乐高积木(组件)会自动按设计图组装好,不需要你一块一块手动拼。
```
project/
├── index.html
├── login.html
├── css/
│ ├── bootstrap.css
│ └── custom.css
├── js/
│ ├── jquery.js
│ └── app.js
└── images/
```
**核心变化只有一点:页面越来越复杂,我们需要更高效的"组织方式"和"开发方式"。**
**遇到的问题**:
### 0.1 前端 vs 大前端(你到底在学什么?)
1. **全局变量污染**: 所有变量都在全局命名空间,容易互相覆盖
2. **依赖管理混乱**: 必须按正确顺序加载 JS 文件,否则会报错
3. **代码难以复用**: 想复用某个功能,只能复制粘贴
:::
很多人说"我学前端",但不同公司口径不一样。
### 2.2 "切图"是什么?
- **前端(Web Frontend**:主要指"在浏览器里跑的那部分"。典型产物是网站和 H5 页面。
- **大前端(Big Frontend**:泛指"所有用户界面相关的开发"。不只 Web,还包括小程序、App、桌面应用等。
<SliceRequestDemo />
这里的几个新词(后面也会用到):
你可能听说过"切图"这个词。它是早期前端的主要工作:
- **端**:平台/运行环境的意思,比如 Web 端、移动端、桌面端。
- **H5**:手机网页(本质也是 Web),通常用来做活动页/落地页,传播快、迭代快。
- **WebView**:App 里用来显示网页的"内置浏览器控件"。很多 App 的部分页面其实就是 WebView。
- **跨端**:用一套代码同时做多个端(比如同时做 iOS + Android)。
- **原生**:直接用平台官方语言/能力开发(iOS 的 Swift、Android 的 Kotlin)。
**什么是切图?**
<BigFrontendScopeDemo />
设计师用 Photoshop 设计好页面 → 前端把设计切成小图片 → 用 HTML 把图片拼成页面
**关键点**:大前端不是一个"新岗位名字",而是一种范围:把体验交付到更多平台。
**为什么这么慢?**
网页上的每张小图片,浏览器都要发一次**网络请求**。请求越多,加载越慢。
::: tip 💡 雪碧图(Sprite)
为了减少请求数,出现了"雪碧图"技术:把很多小图合成一张大图。
优点是请求数变少,缺点是制作和维护都很麻烦。
这个阶段的教训:**请求太多是性能大敌**。
:::
---
## 1. 第阶段:静态网页与切图 (2000s-)
## 3. 第阶段:jQuery 时代 - "手动搬砖"(2010s)
早期网页开发像做报纸:设计师把 UI 设计切成很多小图,前端把这些图片拼成页面。
这就叫**切图**。
### 3.1 为什么需要 jQuery?
这里的几个新词:
随着网页变复杂,原生 JavaScript 的问题暴露出来:
- **静态网页**:页面内容基本固定,打开就是一份 HTML 文件(不像现在很多页面是"数据驱动、可交互"的)。
- **UI**User Interface,用户界面。也就是你看到的按钮、颜色、布局。
- **API 繁琐**: 简单的操作也要写很多代码
- **浏览器兼容**: 不同浏览器的 API 不一样,要写很多兼容代码
- ❌ **选择器弱**: 找元素很麻烦
### 1.1 为什么会慢?
**jQuery** 诞生了。它让 JavaScript 变得简单:
网页上的每一张小图,浏览器都要发一次**网络请求**。
请求越多,加载越慢。
```javascript
// 原生 JavaScript (繁琐)
const element = document.getElementById('title')
想象一下你点外卖:
- 如果你一次性下单 10 道菜,餐厅可以一起做完送过来。
- 但如果你分 10 次下单,每次只点 1 道菜,骑手要跑 10 趟!
// jQuery (简洁)
const element = $('#title')
```
早期的网页就像"分 10 次下单",每张图片都要单独"下单"(发请求)。
### 3.2 jQuery 的思路:亲手改页面
<EvolutionSliceRequestDemo />
<JQueryVsStateDemo />
补充一个常见技巧:**雪碧图 (Sprite)**
把很多小图合成一张大图,这样请求数会变少(但制作和维护更麻烦)。
jQuery 的核心思路是**命令式**: 你告诉浏览器"怎么做"
**关键点**:早期网页慢,常见原因之一是"请求太多"。(图片、脚本、样式都会产生请求)
```javascript
// 找到标题元素
$('#title').text('新标题')
// 找到按钮并禁用
$('#submit-btn').attr('disabled', true)
// 找到列表并添加一项
$('ul').append('<li>新项目</li>')
```
**问题**: 你需要记住页面上有哪些元素,每次数据变化都要手动更新所有相关元素。
::: warning ⚠️ jQuery 的痛点
想象你在做一个购物车:
```javascript
// 用户点击"添加到购物车"
function addToCart() {
cartCount++ // 数据变化
// 你要手动更新所有相关地方
$('#cart-count').text(cartCount) // 右上角小红点
$('#cart-page-count').text(cartCount) // 购物车页面
$('#checkout-price').text(calculatePrice()) // 结算按钮
// 如果漏了一个地方,页面就不一致了!
}
```
**这就是"手动搬砖"的代价**: 容易出错,难以维护。
:::
### 3.3 移动端普及:响应式设计的出现
这个阶段还有一个重要变化:**手机和平板开始流行**。
<ResponsiveGridDemo />
网页必须适配不同屏幕。这需要**响应式布局**: 同一套 HTML/CSS,自动根据屏幕宽度变换布局。
**响应式布局的核心: 媒体查询 (Media Query)**
```css
/* 电脑屏幕(大于 640px) */
@media (min-width: 640px) {
.container {
display: flex;
}
}
/* 手机屏幕(小于 640px) */
@media (max-width: 640px) {
.container {
display: block;
}
}
```
::: tip 💡 响应式就像"智能相框"
想象你在不同房间看同一张照片:
- 在**大客厅**(电脑屏幕),照片可以摆大一些,旁边还能放其他装饰品
- 在**小卧室**(手机屏幕),照片需要缩小,其他装饰品要收起来
**响应式布局**就是"智能相框",它会自动根据房间大小调整展示方式。
:::
---
## 2. 第阶段:移动端普及与响应式布局 (2010s)
## 4. 第阶段:从"手动搬砖"到"数据驱动"(Vue/React)
手机和平板开始流行以后,网页必须适配不同屏幕。
这就需要**响应式布局**:同一套 HTML/CSS,自动根据屏幕宽度变换布局。
### 4.1 为什么需要新框架?
这里用到了**媒体查询 (Media Query)**
它是 CSS 里的"条件判断",比如"如果屏幕小于 640px,就用 1 列布局"。
jQuery 时代的问题积累到一定程度:
想象一下你在不同房间看同一张照片:
- **大客厅**(电脑屏幕),照片可以摆大一些,旁边还能放其他装饰品。
- **小卧室**(手机屏幕),照片需要缩小,其他装饰品要收起来,否则会挤不下。
- **代码一多就乱**: 到处都是 DOM 操作,难以维护
- **容易出 bug**: 漏更新一个地方,页面就不一致
- **协作困难**: 多人修改同一个文件,容易冲突
**响应式布局**就是"智能相框",它会自动根据房间大小调整展示方式
**Vue / React** 的核心思路:**只改数据,页面自动更新**
<EvolutionResponsiveGridDemo />
**关键点**:响应式让网页"会变形",不再只适配电脑。
---
## 3. 第三阶段:从"手动搬砖"到"数据驱动" (jQuery -> Vue/React)
网页开始像 App 一样复杂之后,最麻烦的事变成了:**同一份数据变化,要改很多地方**。
举个最常见的例子:购物车数量从 1 变成 2。
- 右上角小红点要变
- 购物车页面的数量要变
- 结算按钮的价格要变
下面这个可视化演示,专门用来解释:**什么是 jQuery(以及它为什么会累)**。
想象一下你在餐厅当服务员:
- **jQuery 时代**:客人点了一份牛排,你要亲自跑厨房告诉厨师、跑吧台拿饮料、跑收银台记账。客人加菜,你又要重新跑一遍。**你成了"跑腿王",累得半死**。
- **Vue/React 时代**:你只需在单子上写好"客人要什么"(数据),厨房、吧台、收银台会自动看单子做事。客人加菜,你只需改单子,其他地方**自动更新**。**你成了"指挥家",轻松优雅**。
<EvolutionJQueryVsStateDemo />
### 3.1 jQuery 的思路:我来"亲手改页面"
在 jQuery 时代(2005+),你通常会写很多"命令"去改页面:
"找到某个元素,把文字改掉;找到某个按钮,把它禁用……"
它的问题不是"写不出来",而是:**只要漏改一个地方,页面就会出现前后不一致的 bug**。
页面越大,这种 bug 越多。
就像你手动修一栋大楼:
- 你要记住"每个房间的长什么样"。
- 你要亲自"一间一间地修"。
- 如果你忘了修某间房,或者修错了,整栋楼就不一致了。
这里用到的新词(先解释清楚):
- **jQuery**:早期非常流行的 JavaScript 工具库,特点是"很方便地找元素、改元素"。
- **DOM**:浏览器里保存页面结构的一棵"树"(按钮、文字、图片都在这棵树上)。
- **ID**:HTML 元素的唯一名字(类似"身份证号"),方便你定位某一个元素。
- **div**HTML 里最常用的"盒子"标签,用来做布局和容器。
### 3.2 Vue/React 的思路:我只改"数据",页面自己变
后来大家意识到:与其到处改页面,不如只维护一份**状态 (State)**。
状态变了,页面自动刷新到正确的样子。
这就是"数据驱动 UI"的核心:
- **State(状态)**:页面的"数据",比如购物车数量、登录状态、输入框内容。
- **数据驱动**:你只改 State,不直接改 DOM;框架负责把界面同步到正确状态。
- **Vue/React**:现代前端框架,主要解决"状态变化 -> 界面自动更新"。
想象一下你在玩"模拟城市"游戏:
- 你不需要"手动画每一栋房子"(手动改 DOM)。
- 你只需要调整"城市数据"(比如人口、资金、政策),游戏画面**自动更新**。
**这就是数据驱动的魅力:你只关心"数据长什么样",不关心"页面怎么画"。**
### 3.3 什么是"命令式"和"声明式"
这就好比你要画一幅画:
- **命令式**:你告诉画家"拿起笔,蘸红颜料,在坐标(10,10)画一个圈"。
- **声明式**:你直接给画家一张照片,"给我画成这样"。
想象一下你点披萨:
- **命令式**:你亲自和面、撒料、烤披萨、切披萨。你要记住每一步,很累。
- **声明式**:你只需说"我要一个芝士披萨",披萨店自动做好。你只需"声明"你要什么,不关心"怎么做"。
### 3.4 交互演示:两种写法的区别
下方的演示展示了两种思维的巨大差异。
- **左边 (jQuery)**:你需要手动关注每一步 DOM 操作。忘了更新 DOM?界面就不对了。
- **右边 (Vue)**:你只管修改数据 `count`,界面自动变。
### 4.2 Vue/React 的思路:声明式 UI
<ImperativeVsDeclarativeDemo />
**关键点**:从 jQuery 到 Vue/React,变化的核心不是"语法",而是**思维方式**:从"我去改页面"变成"我只改数据"。
**jQuery (命令式)**:
### 3.5 Vue 和 React 怎么选?先把差异理解清楚
```javascript
// 你要告诉浏览器每一步怎么做
$('#title').text('新标题')
$('#title').css('color', 'red')
$('#title').show()
```
很多初学者会纠结:"我到底学 Vue 还是 React"
先别急着站队。你先把它们的"共同点"和"差异点"理解清楚,就不会被带节奏了。
**Vue (声明式)**:
**共同点(它们都在解决同一件事)**:
```javascript
// 你只需告诉浏览器"要显示什么"
data() {
return {
title: "新标题",
color: "red",
visible: true
}
}
```
- 都是为了解决:页面复杂时,如何可靠地管理状态、更新界面
- 都强调:组件化(把页面拆成积木)
::: tip 💡 命令式 vs 声明式
**差异点(你会在写代码时真实感受到)**:
就像画一幅画:
- **写 UI 的方式**Vue 常用 TemplateReact 常用 JSX
- **状态变化时怎么更新**Vue 更偏"依赖追踪"React 更偏"重新渲染组件函数"
- **命令式**: 你告诉画家"拿起笔,蘸红颜料,在坐标(10,10)画一个圈"
- **声明式**: 你直接给画家一张照片,"给我画成这样"
这里的几个新词(像课件一样解释清楚):
Vue/React 就是"声明式": 你描述"页面长什么样",框架负责"怎么把它画出来"。
:::
- **Template**:Vue 常见写法,用类似 HTML 的语法来写界面。
- **JSX**React 常见写法,用"像写 JS 一样"的方式写界面结构。
- **Hook**React 的一套函数式能力(比如 `useState`),用来保存状态、处理副作用。
- **SFC**Single File ComponentVue 常见的单文件组件(一个 `.vue` 文件里写模板/逻辑/样式)。
### 4.3 组件化:像搭乐高一样写页面
<VueReactComparisonDemo />
**Vue / React** 最强大的特性是**组件化**: 把页面拆成一个个独立的"积木"。
**关键点**:别死记名词。你只要记住一句话:它们都能做同样的产品,只是"写法和心智模型"不一样。
想象一下你在搭乐高:
---
- 你不需要"从头开始雕刻每一块积木"(从头写 HTML/CSS)
- 你只需要"按说明书把积木拼在一起"(把组件组合起来)
- 每个积木都是**独立的**,你可以在不同的套装里**重复使用**
## 4. 第四阶段:组件化(像搭积木一样写页面)
**组件的好处**:
解决了"怎么更新页面"的问题,接下来是"怎么组织代码"。
以前一个页面可能是一个超大的 HTML 文件,改一个按钮可能牵连全局。
- **复用**: 写一个"商品卡片"组件,可以用 100 次
- **封装**: 组件内部的状态不影响别人
- **维护**: 修改一个组件,所有用到它的地方都会更新
### 4.1 "积木"是什么?
现代前端把页面拆成了**组件**。
一个按钮、一个导航栏、一个商品卡片,都是独立的积木。
想象一下你在搭乐高:
- 你不需要"从头开始雕刻每一块积木"(从头写 HTML/CSS)。
- 你只需要"按说明书把积木拼在一起"(把组件组合起来)。
- 每个积木都是**独立的**,你可以在不同的套装里**重复使用**。
### 4.2 为什么组件能复用?
定义好一个"商品卡片"组件后,你可以由它生成 100 个实例。每个实例都有自己独立的状态(比如点赞状态),互不干扰。
想象一下你有一个"万能开关"组件:
- 你可以把这个开关放在客厅、卧室、厨房。
- 每个开关都是**独立的**:你按客厅的开关,不会影响到卧室的灯。
- 但它们都是**同一个组件**,你只需要设计一次"开关长什么样",就可以到处使用。
<ComponentReusabilityDemo />
**新名词解释**
- **组件 (Component)**:页面里的"积木块",可以单独复用。
- **封装**:组件内部的状态不影响别人。
- **复用**:同一个组件可以用很多次。
**关键点**:组件化让页面像搭积木一样搭出来。
---
## 5. 第五阶段:页面切换体验(MPA vs SPA)
用户不再想要"每点一次就刷新整页"的体验。
于是出现了**单页应用 (SPA)**:整个网站只加载一次,之后只是切换内容。
与之对应的是**多页应用 (MPA)**:每点一次都会重新加载整个页面。
这里的一个新词:**路由 (Routing)**。
简单理解:就是"从 A 页面切到 B 页面"的规则和过程。
再补两个新词(非常重要):
- **前端路由**:页面切换主要由浏览器里的 JavaScript 控制(常见于 SPA)。
- **后端路由**:页面切换主要由服务器决定"返回哪个页面"(常见于 MPA)。
想象一下你在看一本书:
- **MPA(多页应用)**:就像**翻书**。每翻一页,你都要把旧书合上、去书架上拿一本新书。慢,而且你之前在书上做的笔记(比如折页)都会消失。
- **SPA(单页应用)**:就像**在同一页纸上换内容**。你只需要擦掉旧内容、写上新内容,**纸还是那张纸**。快,而且你做的笔记一直都在。
### 4.4 SPA:单页应用的诞生
<RoutingModeDemo />
### 5.1 MPA 是什么?(多页应用)
**Vue / React** 时代还有一个重要变化:**从 MPA 到 SPA**。
MPA 的直觉很像"翻书"
**MPA (Multi-Page Application)**:
- 点"商品页" -> 浏览器向服务器要一个新的页面(新的 HTML)
- 旧页面被替换掉 -> 原来的输入、滚动位置、临时数据往往会消失
- 点一个链接 → 整页刷新 → 显示新页面
- 就像**翻书**: 每翻一页都要把旧书合上、去书架拿新书
想象一下你在逛商场:
- 每进一家店(点一个链接),你都要**走出商场、重新排队进门**(整页刷新)。
- 你在上一家店试过的衣服(输入的内容)、拿过的购物车,**全部清空**。
**SPA (Single-Page Application)**:
**优点(为什么很多网站仍在用)**
- 点一个链接 → 只刷新内容区域 → 页面不刷新
- 就像**同一本书里换章节**: 只擦掉旧内容、写上新内容
- 结构简单:服务器负责"出页面",浏览器负责"展示"
- SEO 友好:搜索引擎更容易直接看到页面内容
- 首屏容易快:因为服务器直接给了 HTML
**SPA 的优点**:
**缺点**
- 体验偏"跳":整页刷新会白一下、加载一下
- 复杂交互会变难:页面之间共享状态不方便
### 5.2 SPA 是什么?(单页应用)
SPA 更像"同一本书里换章节":
- 第一次打开:加载一个"外壳页面"HTML + CSS + JS
- 之后切换页面:通常只换内容区域,整页不刷新
想象一下你在用手机的 App
- 打开微信(第一次加载),之后你刷朋友圈、看聊天、进公众号,**页面不会重新加载**,只是内容在切换。
- 你输入了一半的消息、看到的滚动位置,**切换后再回来还在**。
**优点**
- 体验丝滑:页面切换快
- 状态好管理:同一个页面里,数据更容易共享(登录态、购物车等)
**缺点(也要知道)**
- 首次加载可能更慢:需要先下载一堆 JS
- SEO 要额外处理:通常需要 SSR/SSG 方案配合(后面第 7 阶段会讲)
### 5.3 交互演示:状态会不会丢?
下面这个小实验更直观:输入一段文字,然后切换页面再回来,看看有没有被清空。
想象一下你正在填写一张申请表:
- **MPA(翻书模式)**:你填到一半,去另一页查资料,回来发现**表格被清空了**,要重新填。
- **SPA(同一页模式)**:你填到一半,去另一页查资料,回来发现**表格还在**,继续填就行。
<SpaStatePreservationDemo />
**关键点**:从"整页刷新"到"局部更新",带来的不仅是速度,更是"状态能不能保留"的体验差异。
- ✅ **体验丝滑**: 页面切换快
- ✅ **状态好管理**: 输入的内容、滚动位置都在
- **首屏可能慢**: 需要先下载 JavaScript
- **SEO 要额外处理**: 搜索引擎可能抓不到内容(需要 SSR/SSG)
---
## 6. 第六阶段:工程化(从"手工作坊"到"现代化工厂"
前端项目越来越大,不能再靠手动引入脚本文件。
于是有了**打包工具**(Webpack/Vite):把多个文件和依赖打成一个或多个"优化后的包"。
想象一下你在整理行李:
- **以前(手动引入)**:你要出门,把衣服、裤子、袜子一件一件拿在手里,容易丢、容易乱。
- **现在(工程化打包)**:你把所有东西**打包进一个行李箱**,拉着就走,整齐又方便。
**依赖**就是你用到的第三方库,比如图表库、编辑器。
想象一下你在做饭:
- 你要做蛋糕,需要面粉、鸡蛋、糖(**依赖**)。
- 你不需要自己种小麦、养鸡(**自己写所有代码**),而是去超市买现成的(**使用第三方库**)。
- **工程化**就是"超市购物清单",帮你自动把所有需要的食材买齐、分类放好。
<BundlerSizeDemo />
这里的几个新词:
- **工程化**:用工具和规范把项目"像工程一样"管理(目录结构、构建、发布、代码规范等)。
- **Bundle(包)**:打包后的产物文件。
- **Tree Shaking**:把"没用到的代码"从包里摇掉,体积更小。
**关键点**:工程化让多人协作的大项目变得可控。
---
## 7. 第七阶段:渲染方式(CSR / SSR / SSG
为了更快的首屏、更好的搜索排名,渲染方式也在进化。
想象一下你在餐厅吃饭,有三种服务模式:
- **CSR(客户端渲染)**:服务员给你一个**半成品食材包**(JS 文件),你自己在桌上做饭。等菜时间长(要下载 JS),但做完后你可以随时"热更新"(交互流畅)。
- **SSR(服务端渲染)**:服务员在**厨房做好菜**(服务器渲染 HTML),直接端给你。上菜快(首屏快),但你想加辣(交互),还得等厨师(服务器响应)。
- **SSG(静态生成)**:餐厅**提前把所有菜做好**,放在保温柜里。你来点餐,立刻就能吃(最快)。但菜单是固定的(静态内容),不能临时加菜。
- **首屏**:用户打开网站时,最先看到的那一屏内容。
- **SEO**Search Engine Optimization,搜索引擎优化。让页面更容易被搜索到。
- **TTFB**Time To First Byte,浏览器收到"第一口数据"的时间(越小越快)。
- **TTI**Time To Interactive,页面变得"可以点、可以用"的时间。
## 5. 渲染策略:从 CSR 到 SSR/SSG
<RenderingStrategyDemo />
**关键点**:不同渲染策略适配不同业务场景。
## 6. 第四阶段:工程化与构建工具(2015s-2020s)
### 6.1 为什么需要"工程化"?
前端项目越来越大,不能再靠"手动引入脚本"。
**工程化**就是用工具和规范,让开发更高效、代码更可靠、协作更顺畅。
::: tip 💡 工程化 = 从"手工作坊"到"现代化工厂"
想象一下你在家做饭 vs 开餐厅:
- **在家做饭**: 想吃什么就做什么,很自由
- **开餐厅**: 需要标准化的菜谱、规范的操作流程、统一的原材料采购
前端开发也一样:
- **小项目**: 怎么写都行
- **大项目**: 需要统一的代码规范、自动化工具、标准化流程
:::
### 6.2 构建工具:Webpack → Vite
**Webpack** (传统):
- 工作方式:**先打包,后服务**
- 启动时: 打包所有代码 → 启动服务器
- 问题:**慢**。项目越大,启动越慢(可能要等 30 秒)
**Vite** (现代):
- 工作方式:**按需编译**
- 启动时: 不打包,直接启动服务器
- 浏览器请求哪个文件,就实时编译哪个
- 优势:**快**。通常 1 秒内启动
| 对比项 | Webpack | Vite | 提升 |
| -------- | ------- | ------ | ------------ |
| 冷启动 | 30s+ | <1s | **快 30 倍** |
| 热更新 | 3-5s | <100ms | **快 30 倍** |
| 配置文件 | 几百行 | 几十行 | **大幅简化** |
::: tip 💡 为什么 Vite 这么快?
**Webpack** 就像**整备家当搬家**:先把所有东西打包,再出门。
**Vite** 就像**轻装旅行**:只带必需品,用到什么再买什么。
在开发环境,大多数时候你只需要修改几个文件,Vite 只编译这几个文件,当然快。
:::
---
## 8. 小结与学习建议
## 7. 总结:演进的本质
前端技术的进化,本质上是在解决两个问题
前端技术的演进,本质上是在解决两个问题:
1. **效率**:从 手动操作 DOM -> 数据驱动 (MVVM)。
2. **规模**:从 巨型面条代码 -> 组件化 + 工程化。
### 7.1 效率:从手动到自动
**MVVM 是什么?**
简单理解:**Model(数据)变了,View(界面)自动变**。
| 时代 | 开发方式 | 效率 |
| --------- | ------------------------ | ---------- |
| **2000s** | 手写 HTML/CSS/JS | ⭐ |
| **2010s** | jQuery + 手动 DOM 操作 | ⭐⭐ |
| **2020s** | Vue/React + 数据驱动 | ⭐⭐⭐ |
| **现在** | 组件化 + 工程化 + 自动化 | ⭐⭐⭐⭐⭐ |
现在你可以把前端理解成三件事:
### 7.2 规模:从个人到团队
1. **写页面**HTML + CSS(结构与样式)
2. **让页面动起来**JavaScript(交互与状态)
3. **把复杂项目做好**:组件化 + 工程化(协作与质量)
| 时代 | 项目规模 | 协作方式 |
| --------- | ---------- | ----------------------- |
| **2000s** | 几个文件 | 单人就能维护 |
| **2010s** | 几十个文件 | 小团队,容易冲突 |
| **2020s** | 几百个文件 | 中团队,需要规范 |
| **现在** | 几千个文件 | 大团队,需要完整工程体系 |
如果你刚开始学,建议按这个顺序:
---
- 先把 HTML/CSS 写熟(布局、响应式)
- 再学 JavaScript 的基础(变量、函数、事件)
- 最后上手一个框架(Vue/React),理解"状态驱动 UI"
## 8. 学习路线图
### 8.1 如果你是零基础
**第 1 步: HTML/CSS/JavaScript 基础**
- 理解网页的三大基石
- 能写出简单的静态页面
**第 2 步: 学习一个框架(Vue 推荐)**
- 理解"数据驱动"的思想
- 掌握组件化开发
**第 3 步: 实战项目**
- 做一个完整的单页应用
- 熟悉路由、状态管理、API 调用
### 8.2 如果你有基础
**进阶方向**:
- **工程化**: 学习 Vite/Webpack,理解构建流程
- **性能优化**: 学习懒加载、代码分割、缓存策略
- **TypeScript**: 为代码加上类型,提升可靠性
- **服务端渲染**: 学习 Nuxt/Next.js,解决 SEO 和首屏问题
---
## 9. 名词速查表 (Glossary)
| 名词 | 全称 | 解释 |
| :----------------- | :----------------------------------------- | :--------------------------------------------------------------------------------------------- |
| **HTML** | HyperText Markup Language | 超文本标记语言。网页的骨架,定义内容和结构。 |
| **CSS** | Cascading Style Sheets | 层叠样式表。网页的皮肤,定义颜色、布局、动画。 |
| **JS** | JavaScript | 网页的肌肉,负责交互和逻辑。 |
| **DOM** | Document Object Model | 文档对象模型。浏览器内部表示页面结构的树形对象。 |
| **jQuery** | - | 早期流行的 JS 库,简化了 DOM 操作。 |
| **Vue/React** | - | 现代前端框架,采用数据驱动和组件化开发。 |
| **State** | - | 状态。组件或应用的数据,状态变化驱动 UI 更新。 |
| **组件** | Component | 可复用的 UI 单元,如按钮、卡片、导航栏。 |
| **MPA** | Multi-Page Application | 多页应用。每次跳转都重新加载整个页面。 |
| **SPA** | Single-Page Application | 单页应用。只加载一次,后续切换不刷新页面。 |
| **路由** | Routing | 管理页面之间切换的规则和过程。 |
| **CSR** | Client-Side Rendering | 客户端渲染。浏览器下载 JS 后执行生成页面。 |
| **SSR** | Server-Side Rendering | 服务端渲染。服务器生成 HTML 后发给浏览器。 |
| **SSG** | Static Site Generation | 静态站点生成。构建时预渲染页面为静态 HTML。 |
| **Bundle** | - | 包。打包工具将多个文件合并后的产物。 |
| **Tree Shaking** | - | 摇树优化。自动移除未使用的代码,减小包体积。 |
| **H5** | HTML5 | 通常指手机网页或基于 HTML5 的移动页面。 |
| **WebView** | - | 内嵌网页视图。App 中用于显示网页内容的组件。 |
| **跨端** | Cross-Platform | 一套代码运行在多个平台(iOS、Android、Web 等)。 |
| **原生** | Native | 使用平台官方语言和 API 开发的应用。 |
| **MVVM** | Model-View-ViewModel | 一种架构模式,实现数据(Model)和视图(View)的自动同步。 |
| **SEO** | Search Engine Optimization | 搜索引擎优化,提高网页在搜索结果中的排名。 |
| **TTFB** | Time To First Byte | 首字节时间,从请求到收到第一个字节数据的耗时。 |
| **TTI** | Time To Interactive | 可交互时间,页面变为完全可交互状态所需的时间。 |
| 名词 | 英文 | 用人话解释 |
| ---------------- | ----------------------- | --------------------------------------------- |
| **DOM** | Document Object Model | 文档对象模型。用对象树表示页面,可被 JS 读写。 |
| **jQuery** | - | 早期流行的 JS 库,简化了 DOM 操作。 |
| **Vue/React** | - | 现代前端框架,采用数据驱动和组件化开发。 |
| **组件** | Component | 可复用的 UI 单元,如按钮、卡片、导航栏。 |
| **MPA** | Multi-Page Application | 多页应用。每次跳转都重新加载整个页面。 |
| **SPA** | Single-Page Application | 单页应用。只加载一次,后续切换不刷新页面。 |
| **路由** | Routing | 管理页面之间切换的规则和过程。 |
| **SSR** | Server-Side Rendering | 服务端渲染。服务器生成 HTML 后发给浏览器。 |
| **SSG** | Static Site Generation | 静态站点生成。构建时预渲染页面为静态 HTML。 |
| **Webpack** | - | 传统打包工具,先打包后服务。 |
| **Vite** | - | 现代构建工具,按需编译,速度极快。 |
| **响应式** | Responsive Design | 页面自动适配不同屏幕尺寸的设计。 |
| **媒体查询** | Media Query | CSS 的条件判断,根据屏幕宽度应用不同样式。 |
| **命令式** | Imperative | 告诉程序"怎么做"。 |
| **声明式** | Declarative | 告诉程序"要什么"。 |
| **数据驱动** | Data-Driven | 只修改数据,界面自动更新。 |
| **Tree Shaking** | - | 摇树优化。自动移除未使用的代码,减小包体积。 |
| **代码分割** | Code Splitting | 把代码分成多个小块,按需加载。 |
---
## 总结
前端技术的演进,本质上是**从"手工"到"工业化"的进化**:
- **2000s**: 手工时代,简单直接
- **2010s**: 工具化时代,开始有框架
- **2020s**: 工业化时代,组件化 + 工程化
- **现在**: 智能化时代,AI 辅助开发
理解这个演进,你就能:
- 知道为什么要有 Vue/React
- 理解"数据驱动"的价值
- 明白工程化的必要性
- 快速上手新技术
**下一步建议**:
- 如果你想快速上手,学习 **Vue 3** (推荐) 或 **React**
- 如果你想深入理解,学习 **Vite** 构建流程
- 如果你想提升代码质量,学习 **TypeScript**
祝你学习愉快!
File diff suppressed because it is too large Load Diff
+292 -44
View File
@@ -1,18 +1,24 @@
# Git 版本控制:时间的后悔药
# Git 版本控制:代码世界的时光机
> 💡 **一句话解释**:Git 就是代码世界的**游戏存档管理器**。它能让你随时“读档”重来,也能让你和队友在各自的“平行宇宙”里互不干扰地开发。
::: tip 🎯 核心问题
**写代码时最怕什么?** 写错了想回退、改崩了想重来、多人同时改同一个文件...这些头疼的事,Git 都能帮你搞定!它就像是代码世界的"时光机",让你随时回到过去,又能和队友在各自的"平行宇宙"里安全开发。
:::
> ✅ **安全说明**:本页所有交互组件都是“模拟器”,不会对你电脑上的真实 Git 仓库执行任何操作;但真实项目里建议严格按步骤来,不要依赖“自动下一步”。
---
## 0. 最常用的 5 个场景直接照抄
## 0. 最常用的 5 个场景(直接照抄)
如果你只想立刻能用”,先把这块过一遍每个场景都是现实工作中最常见的 Git 流程。
如果你只想"立刻能用",先把这块过一遍:每个场景都是现实工作中最常见的 Git 流程。
<GitScenariosDemo />
## 1. 为什么我们需要它?
---
你是否经历过这种绝望?
## 1. 为什么要学 Git?三大痛点
### 1.1 痛点一:版本混乱
你是否经历过这种绝望?
```text
论文_初稿.doc
@@ -22,80 +28,322 @@
论文_绝对是最后一次修改版.doc
```
**Git 完美解决了三个问题**
**Git 的解决方案**:不需要复制副本,一个文件夹搞定所有历史版本。想回到哪次修改,一键恢复。
1. **版本混乱**:不需要复制副本,一个文件夹搞定所有历史版本。
2. **无法后悔**:删错了代码?一秒钟找回三天前的状态。
3. **协作冲突**:你改了 A 文件,我改了 B 文件,Git 帮我们自动合并。
### 1.2 痛点二:无法后悔
::: tip 💡 这个场景你一定遇到过
写代码写了 3 小时,突然发现之前的思路更好,但已经改不回去了...或者删错了一段代码,想找回原来的版本。
有了 Git,这种情况永远不会发生。每次重要节点都能"存档",出问题随时"读档"重来。
:::
### 1.3 痛点三:协作冲突
你和队友同时改同一个文件:
- 你改了 A 文件的第 10 行
- 队友改了 A 文件的第 15 行
- 怎么合并?谁覆盖谁?
**Git 的解决方案**:智能合并,自动处理大部分冲突。只有当你们真的改了同一行代码时,才需要手动决定用谁的。
---
## 2. 核心概念:三个箱子
## 2. 核心概念:三区模型
Git 的设计哲学其实很像**寄快递**。
<GitThreeAreasDemo />
- **工作区 (Working Dir)**:你的**书桌**。你正在这里写代码,想怎么乱改都行。
- **暂存区 (Staging Area)**:**快递盒**。你把写好的文件放进去(`git add`),准备打包。
- **仓库 (Repository)****快递柜**。一旦你封箱寄出(`git commit`),这个版本就被永久记录下来了。
### 2.1 三个区域是什么?
> 🔑 **关键点**:只有提交(Commit)到仓库的内容,才是安全的。工作区里没提交的内容,丢了就真丢了。
::: tip 📦 用快递理解 Git
想象你在寄快递:
- **工作区(Working Dir)** = 你的**书桌**。你在这里整理要寄的东西,想怎么乱改都行。
- **暂存区(Staging Area)** = **快递盒**。你把要寄的文件放进去(`git add`),准备打包。
- **仓库(Repository)** = **快递柜**。一旦你封箱寄出(`git commit`),这个版本就被永久记录下来了。
:::
| 区域 | 作用 | 对应命令 | 状态 |
| ---------- | ------------------ | --------------------- | ------------- |
| **工作区** | 你当前正在改的代码 | `git status` 查看修改 | 红色 = 未暂存 |
| **暂存区** | 准备提交的文件 | `git add` 添加 | 绿色 = 已暂存 |
| **仓库** | 永久保存的历史版本 | `git commit` 提交 | 只读,不能改 |
::: tip 💡 关键理解
只有提交到**仓库**的内容才是安全的。工作区里没提交的内容,丢了就真丢了。所以经常`git commit`是好习惯!
:::
---
## 3. 基础工作流存档三步走
## 3. 基础工作流:存档三步走
日常开发中你 90% 的时间都在重复这三个动作。
日常开发中,你 90% 的时间都在重复这三个动作。
<GitWorkflowDemo />
1. **修改代码**:在工作区写写画画。
2. **`git add`**:挑选你要保存的文件,放入暂存区。
3. **`git commit`**:给这次修改起个名字(比如“修复了登录 Bug”),永久存档
### 3.1 第一步:修改代码(工作区)
在工作区写写画画,想怎么改就怎么改。这时候修改只在你本地,还没记录
### 3.2 第二步:挑选文件(git add → 暂存区)
::: tip 🤔 为什么要先 add 再 commit?
你可能问:为什么不能直接 commit 所有修改?
**答案**:因为有时候你不想一次性提交所有改动。
- 今天改了 5 个文件,但只想提交其中 3 个(完成了一个功能)
- 另外 2 个文件还在调试中,不想现在提交
`git add` 让你有选择权:决定这次提交包含哪些文件。
:::
**常用命令**:
```bash
# 添加单个文件
git add index.html
# 添加所有修改
git add .
# 查看哪些文件被暂存了
git status
```
### 3.3 第三步:封箱提交(git commit → 仓库)
给这次修改起个名字(比如"修复了登录 Bug"),永久存档。
**重要:commit message 要写清楚!**
```bash
# ❌ 不好的写法
git commit -m "update"
# ✅ 好的写法
git commit -m "feat: 添加用户登录功能"
git commit -m "fix: 修复首页在 iOS 的显示问题"
git commit -m "docs: 更新 README 的部署说明"
```
::: tip 💡 commit message 规范
推荐用**类型+描述**的格式:
- `feat:` 新功能
- `fix:` 修复 bug
- `docs:` 文档更新
- `style:` 代码格式(不影响功能)
- `refactor:` 重构(不改变功能)
- `test:` 测试相关
- `chore:` 构建/工具相关
这样以后翻历史记录,一眼就知道每次提交做了什么。
:::
---
## 4. 平行宇宙分支 (Branch)
## 4. 平行宇宙:分支(Branch)的魔法
这是 Git 最强大的功能。想象你在玩游戏,前面有个大 Boss(上线新功能),你怕打不过导致游戏结束(系统崩溃)。
这是 Git 最强大的功能!
这时候,你可以开一个**分支 (Branch)**,相当于**复制了一个平行世界**。
::: tip 🌌 用游戏理解分支
想象你在玩游戏,前面有个大 Boss(上线新功能),你怕打不过导致游戏结束(系统崩溃)。
这时候,你可以开一个**分支(Branch)**,相当于**复制了一个平行世界**:
- 在**平行世界**(新分支)里打 Boss,输了也不怕,因为主世界(主分支)没影响
- 打赢了就把成果"合并"回主世界
- 多个队友可以在各自的平行世界开发,互不干扰
:::
<GitBranchMergeDemo />
- **主分支 (Main/Master)**:稳定的线上版本,只有测试通过的代码才能进来。
- **开发分支 (Feature)**:你的试验田。你在这里炸了地球也没关系,不会影响主分支。
- **合并 (Merge)**:你在试验田里测试成功了,就把改动“合并”回主分支。
### 4.1 主分支 vs 开发分支
| 分支类型 | 作用 | 特点 |
| ------------------- | -------------- | ------------------------------------ |
| **main/master** | 稳定的线上版本 | 只有测试通过的代码才能进来 |
| **dev/feature-xxx** | 你的试验田 | 这里炸了地球也没关系,不影响主分支 |
| **hotfix-xxx** | 紧急修复 | 生产出 bug 时,从 main 开分支快速修复 |
### 4.2 分支操作流程
**创建分支并切换**:
```bash
# 创建新分支
git branch feature-login
# 切换到新分支
git checkout feature-login
# 或者一步到位:创建并切换
git checkout -b feature-login
```
**在分支上开发**:
```bash
# 在 feature-login 分支上改代码...
git add .
git commit -m "feat: 添加登录表单"
```
**合并回主分支**:
```bash
# 切回主分支
git checkout main
# 合并 feature-login
git merge feature-login
# 删除已合并的分支(可选)
git branch -d feature-login
```
::: tip 💡 什么时候用分支?
**个人开发**:
- 要尝试新想法,不确定会不会搞崩现有代码 → 开分支
- 修一个复杂 bug,需要多次实验 → 开分支
**团队开发**:
- 每个功能一个分支,互不干扰
- 开发完提 Pull Request,队友 review 后再合并
:::
---
## 5. 常用命令速查
## 5. 常用命令速查
| 命令 | 作用 | 人话解释 |
| :-------------------- | :------------- | :------------------------------------- |
| `git init` | 初始化 | "我要在这里建个新仓库" |
| `git status` | 查看状态 | "现在书桌上乱不乱有没有东西没装箱?" |
| `git add .` | 添加所有 | "把桌上所有文件都扔进快递盒" |
| `git commit -m "..."` | 提交 | "封箱!贴上标签,写上这次改了啥" |
| `git log` | 查看历史 | "翻翻以前的日记" |
| `git checkout -b dev` | 以此创建新分支 | "我要去平行宇宙 dev 探险了" |
| `git checkout main` | 切换分支 | "回地球(主分支)看看" |
| `git merge dev` | 合并分支 | "把平行宇宙的成果带回地球" |
| 命令 | 作用 | 人话解释 | 使用频率 |
| ----------------------- | ---------- | ------------------------------ | --------------------- |
| `git init` | 初始化 | "在这里建个新仓库" | 项目开始时用一次 |
| `git status` | 查看状态 | "现在乱不乱?有没有东西没提交?" | ⭐⭐⭐⭐⭐ 极高频 |
| `git add .` | 添加所有 | "把桌上所有文件都扔进快递盒" | ⭐⭐⭐⭐⭐ 每次提交前 |
| `git add file.txt` | 添加单个 | "只要这个文件" | ⭐⭐⭐⭐ 选择性添加 |
| `git commit -m "..."` | 提交 | "封箱!贴上标签,写上这次改了啥" | ⭐⭐⭐⭐⭐ 完成功能时 |
| `git log` | 查看历史 | "翻翻以前的日记" | ⭐⭐⭐ 回顾历史 |
| `git checkout -b dev` | 创建新分支 | "我要去平行宇宙 dev 探险了" | ⭐⭐⭐⭐ 开新功能 |
| `git checkout main` | 切换分支 | "回地球(主分支)看看" | ⭐⭐⭐⭐ 切换任务 |
| `git merge dev` | 合并分支 | "把平行宇宙的成果带回地球" | ⭐⭐⭐ 完成功能 |
| `git branch` | 查看分支 | "现在有哪些平行世界?" | ⭐⭐⭐ 查看状态 |
| `git branch -d feature` | 删除分支 | "这个平行世界不需要了,删掉" | ⭐⭐ 合并后清理 |
| `git push` | 推送 | "把本地存档上传到云端" | ⭐⭐⭐⭐⭐ 团队协作 |
| `git pull` | 拉取 | "把云端最新存档下载到本地" | ⭐⭐⭐⭐⭐ 团队协作 |
---
## 6. 进阶解决冲突与远程协作
## 6. 进阶:解决冲突与远程协作
当你和队友同时修改了同一个文件的同一行代码,Git 就会懵逼:“我该听谁的?” 这就是**冲突 (Conflict)**。
### 6.1 冲突(Conflict)是什么?
当你和队友**同时修改了同一个文件的同一行代码**,Git 就会懵:"我该听谁的?"这就是**冲突(Conflict)**。
<GitConflictDemo />
此时你需要手动打开文件,保留需要的代码,删除 Git 自动生成的 `<<<<<<<` 标记,然后重新提交。
### 6.2 怎么解决冲突?
至于**远程仓库 (Remote)**(比如 GitHub/GitLab),它就是云端的备份中心。
**Step 1**:打开冲突文件,会看到这样的标记:
- `git push`:把本地存档上传到云端。
- `git pull`:把云端最新的存档拉取到本地。
```text
<<<<<<< HEAD
你的代码
=======
队友的代码
>>>>>>> feature-branch
```
**Step 2**:手动选择要保留的代码,或合并两者:
```text
# 保留你的代码 → 删除队友的部分和标记
# 保留队友的 → 删除你的部分和标记
# 合并两者 → 综合两边的代码
```
**Step 3**:删除所有标记,保存文件
**Step 4**:重新提交
```bash
git add 解决冲突的文件
git commit # Git 会自动生成合并提交
```
::: tip 💡 避免冲突的最佳实践
- **频繁沟通**:队友改同一个文件前,先打个招呼
- **小步提交**:不要攒着大量代码最后才提交,增加冲突概率
- **分支隔离**:不同功能用不同分支,减少直接冲突
- **用 Pull Request**:合并前让队友 review,提前发现问题
:::
### 6.3 远程仓库(Remote)
**远程仓库**(比如 GitHub/GitLab)就是**云端的备份中心**。
<GitRemoteDemo />
**核心操作**:
| 操作 | 命令 | 作用 |
| ------------ | ---------------------------------------------- | ------------------------ |
| **关联远程** | `git remote add origin https://github.com/...` | 第一次连接云端 |
| **推送** | `git push -u origin main` | 把本地存档上传 |
| **拉取** | `git pull` | 把云端最新存档下载并合并 |
| **克隆** | `git clone https://github.com/...` | 复制整个仓库到本地 |
::: tip 💡 push 和 pull 的区别
- **push**:你的本地代码 → 云端(你改了东西,要同步给队友)
- **pull**:云端代码 → 你的本地(队友改了东西,你要同步下来)
**最佳实践**:每天开始工作前先`git pull`,下班前`git push`,这样减少冲突。
:::
---
## 7. 总结:Git 的核心思想
Git 不是简单的"版本备份",而是一个**完整的代码协作系统**:
| 特性 | 解决的问题 | 生活类比 |
| ------------ | ------------------------------- | --------------------- |
| **版本管理** | 代码改错了怎么办? | 时光机,随时回退 |
| **分支** | 多人同时改同一个文件怎么办? | 平行宇宙,互不干扰 |
| **暂存区** | 这次提交不想包含所有修改怎么办? | 快递盒,挑拣要寄的东西 |
| **远程协作** | 怎么和队友共享代码? | 云备份,随时随地同步 |
| **冲突处理** | 真的改到同一行了怎么办? | 智能合并 + 手动协调 |
**学习建议**:
1. **先用起来**:不要等"完全理解"再用,一边用一边理解
2. **从简单开始**:个人项目先掌握`add/commit/push/pull`,团队项目再学分支
3. **看 Git 图形化工具**:SourceTree、GitHub Desktop 等,可视化帮助理解
4. **遇到问题不要慌**:Git 的设计就是为了让你能安全地尝试,大不了`git reset`
---
## 附录:名词速查表
| 名词 | 英文 | 用人话解释 |
| -------- | ---------- | ------------------------------------- |
| **仓库** | Repository | 存放所有版本历史的数据库 |
| **提交** | Commit | 一次完整的版本记录,像存档点 |
| **分支** | Branch | 独立的开发线,像平行宇宙 |
| **合并** | Merge | 把一个分支的改动整合到另一个分支 |
| **冲突** | Conflict | 同一行代码被修改多次,Git 不知道选哪个 |
| **暂存** | Stage | 把修改加入"准备提交"的列表 |
| **远程** | Remote | 云端的仓库副本(GitHub/GitLab) |
| **克隆** | Clone | 复制整个远程仓库到本地 |
| **推送** | Push | 本地 → 远程,上传代码 |
| **拉取** | Pull | 远程 → 本地,下载代码 |
| **检出** | Checkout | 切换到某个分支或版本 |
| **HEAD** | - | 当前所在的分支/版本的指针 |
+284 -380
View File
@@ -1,497 +1,401 @@
# 负载均衡与多实例部署示意图
# 负载均衡与多实例部署
> 💡 **学习指南**:本文将带你理解现代分布式系统中,如何通过负载均衡技术把流量"聪明地"分配到多个服务器实例上。我们会从四层/七层负载均衡讲起,逐步深入到健康检查、会话保持、自动扩缩容,最后到异地多活部署。建议你先阅读 [后端架构演进](./backend-evolution.md) 了解基本概念。
在开始之前,建议你先补充两块"基础砖":
- **网络基础**:可以先阅读 [网络基础概念](./network-basics.md) 了解 TCP/IP、HTTP 等协议。
- **容器与编排**:如果你还不熟悉 Docker 和 Kubernetes,可以先看 [容器化部署](./container-deployment.md)。
::: tip 🎯 核心问题
**当单台服务器扛不住时,如何把流量"聪明地"分配到多个服务器实例?** 负载均衡是现代分布式系统的"分发员"。本文通过真实案例(奶茶店收银、快递分拣、交通指挥)深入理解负载均衡的设计哲学和工程实践。
:::
---
## 0. 引言:当一台服务器扛不住的时候
## 1. 为什么要"负载均衡"?
<ClientOnly>
<LoadBalancerTypesDemo />
</ClientOnly>
### 1.1 从一个真实案例说起:某网站的架构演进
想象你开了一家网红奶茶店。刚开业时,店里只有一个收银台,顾客排队点单,一切井然有序。但随着口碑传播,排队的人越来越多,一个收银台根本应付不过来——顾客等得不耐烦,抱怨连连,甚至有人转身离开。
某创业公司在用户量快速增长时遇到了严重的性能问题:
**这时候你有两个选择:**
**场景还原:**
1. **换一台更快的收银机(垂直扩展)**:但再快的机器也有极限,而且贵得离谱。
2. **多开几个收银台,让顾客分流(水平扩展)**:每个收银台处理一部分顾客,整体效率大幅提升。
```
阶段一:单台服务器
用户 → 服务器(1核2G)
日活1000 → 活跃时间:1000人同时访问
问题:CPU 100%,响应慢,经常宕机
```
**负载均衡(Load Balancing)就是第二个方案的"总指挥"。** 它站在所有收银台前面,帮顾客决定:"你去1号台,你去2号台..." 确保每个收银台的 workload 相对平均,不让任何一个台累垮。
::: warning ⚠️ 单台服务器的致命问题
<ClientOnly>
<IntroProblemReasonSolution />
</ClientOnly>
- **性能瓶颈**: CPU 100%,响应时间> 5秒
- **单点故障**: 服务器挂了,整个网站不可用
- **扩展困难**: 只能垂直升级(加CPU、内存),贵且有限
:::
**改进后的架构(引入负载均衡):**
```
阶段二:多台服务器 + 负载均衡
用户 → 负载均衡器(Nginx)
├→ 服务器1 (1核2G)
├→ 服务器2 (1核2G)
└→ 服务器3 (1核2G)
```
::: tip ✨ 改进后的效果
- **性能提升**: 3台服务器并行处理,响应时间< 1秒
- **高可用**: 1台服务器挂了,其他服务器继续服务
- **水平扩展**: 需要更多性能?加服务器就行
:::
### 1.2 负载均衡的生活化比喻
**奶茶店收银台**
想象你开了一家网红奶茶店:
- **1个收银台**: 顾客排队,后面的人等不及,差评
- **3个收银台**: 员工分配顾客到不同收银台,效率提升3倍
**负载均衡就是"收银台分配员"**:
- **用户**(顾客) → 请求服务
- **负载均衡器**(分配员) → 把请求分配到不同服务器
- **服务器**(收银台) → 处理请求
<LoadBalancerTypesDemo />
---
## 1. 负载均衡器的"分层": L4 vs L7
## 2. 什么是负载均衡?
就像快递分拣有"只看邮编"和"检查包裹内容"两种策略,负载均衡也分不同"层次":
### 2.1 四层负载均衡(L4):只看门牌号
### 1.1 四层负载均衡(L4):"只看门牌号"
**工作在传输层(TCP/UDP)**,就像快递小哥只看你家的**门牌号(IP地址+端口号)**,不关心你家是做什么。
**工作在传输层(TCP/UDP)**,就像快递小哥只看你家的**门牌号(IP地址+端口号)**,不关心你家是做什么的。
**特点:**
**特点:**
- **速度超快**:只做简单的地址转发,不解析数据包内容
- **适用场景**:数据库连接、Redis缓存、长连接游戏服务器
- **代表产品**LVSLinux Virtual Server)、AWS NLB、Azure Load Balancer
- **速度超快**: 只做简单的地址转发,不解析数据包内容
- **适用场景**: 数据库连接、Redis缓存、长连接游戏服务器
- **代表产品**: LVS(Linux Virtual Server)、AWS NLB、Azure Load Balancer
**真实案例:电商大促的流量入口**
::: details 工作原理
某头部电商在双11期间,使用L4负载均衡处理每秒数百万的TCP连接。由于L4不解析HTTP内容,处理速度极快,确保用户在秒杀开始瞬间就能建立连接,不因为负载均衡本身的处理延迟而错过抢购。
```
客户端请求 → L4负载均衡器 → 后端服务器
只看IP + Port
快速转发(不解包内容)
```
### 1.2 七层负载均衡(L7):"检查包裹内容"
:::
**工作在应用层(HTTP/HTTPS)**,就像快递小哥不仅看门牌号,还会**打开包裹检查内容**,根据内容决定怎么送。
### 2.2 七层负载均衡(L7):检查包裹内容
**特点:**
- **智能路由**:可以根据URL路径、HTTP头、Cookie等做精细化路由
- **高级功能**:SSL卸载、内容缓存、压缩、安全WAF
- **适用场景**Web应用、API网关、微服务架构
- **代表产品**Nginx、HAProxy、AWS ALB、Envoy
**工作在应用层(HTTP/HTTPS)**,就像快递小哥不仅看门牌号,还会**打开包裹检查内容**,根据内容决定怎么送。
**真实案例:SaaS平台的多租户路由**
**特点:**
某SaaS公司使用Nginx作为L7负载均衡,根据HTTP Header中的`X-Tenant-ID`将不同租户的数据请求路由到对应的数据库集群。tenant-a 的请求去 db-cluster-1tenant-b 的请求去 db-cluster-2,实现了完全的数据隔离。
- **智能路由**: 可以根据URL路径、HTTP头、Cookie等做精细化路由
- **高级功能**: SSL卸载、内容缓存、压缩、安全WAF
- **适用场景**: Web应用、API网关、微服务架构
- **代表产品**: Nginx、HAProxy、AWS ALB、Envoy
### 1.3 L4 vs L7 对比一览
::: details 工作原理
| 维度 | 四层负载均衡 (L4) | 七层负载均衡 (L7) |
|:---|:---|:---|
| **工作层级** | 传输层 (TCP/UDP) | 应用层 (HTTP/HTTPS) |
| **决策依据** | IP地址 + 端口号 | URL、Header、Cookie、Body |
| **处理速度** | 极快(内核态处理) | 较快(用户态解析) |
| **功能丰富度** | 基础转发 | SSL卸载、缓存、压缩、WAF |
| **典型场景** | 数据库、游戏、长连接 | Web应用、API网关、微服务 |
| **代表产品** | LVS、AWS NLB | Nginx、HAProxy、AWS ALB |
```
客户端请求 → L7负载均衡器 → 解析HTTP内容
检查URL、Header、Cookie
智能路由到特定服务器
```
:::
### 2.3 L4 vs L7 对比一览
| 维度 | 四层负载均衡(L4) | 七层负载均衡(L7) |
| :------------- | :------------------- | :------------------------ |
| **工作层级** | 传输层(TCP/UDP) | 应用层(HTTP/HTTPS) |
| **决策依据** | IP地址 + 端口号 | URL、Header、Cookie、Body |
| **处理速度** | 极快(内核态处理) | 较快(用户态解析) |
| **功能丰富度** | 基础转发 | SSL卸载、缓存、压缩、WAF |
| **典型场景** | 数据库、游戏、长连接 | Web应用、API网关、微服务 |
| **代表产品** | LVS、AWS NLB | Nginx、HAProxy、AWS ALB |
---
## 2. 健康检查:别让"坏掉"的服务器继续接客
## 3. 核心问题一:如何避免"坏掉"的服务器继续接客?
想象一下,你的某个收银台突然坏了,但顾客不知道,还在源源不断地排过去。结果队伍越来越长,顾客怨声载道。
### 3.1 健康检查:别让"生病"的服务器拖累系统
**健康检查(Health Check)就是防止这种情况发生的"哨兵"。** 它定期"体检"每台服务器,发现"生病"的立即从队列中移除,等"康复"了再请回来
想象一下,你的某个收银台突然坏了,但分配员不知道,还在源源不断地把顾客分过去。结果队伍越来越长,顾客怨声载道
<ClientOnly>
<HealthCheckDemo />
</ClientOnly>
**健康检查(Health Check)就是防止这种情况发生的"哨兵"**。它定期"体检"每台服务器,发现"生病"的立即从队列中移除,等"康复"了再请回来。
### 2.1 主动健康检查 vs 被动健康检查
<!-- <HealthCheckDemo /> -->
**主动健康检查(Active Health Check**:负载均衡器主动"敲门"问服务器"你还在吗?"
- 定期发送探测请求(如 HTTP /health、TCP ping
### 3.2 主动健康检查 vs 被动健康检查
**主动健康检查(Active Health Check)**: 负载均衡器主动"敲门"问服务器"你还在吗?"
- 定期发送探测请求(如 HTTP /health、TCP ping)
- 响应超时或返回错误码则认为不健康
- **优点**检测结果准确可靠
- **缺点**产生额外的探测流量
- **优点**: 检测结果准确可靠
- **缺点**: 产生额外的探测流量
**被动健康检查(Passive Health Check)**: 负载均衡器"观察"真实业务流量的响应情况
**被动健康检查(Passive Health Check**:负载均衡器"观察"真实业务流量的响应情况
- 统计实际请求的响应时间、错误率
- 连续多次失败则认为不健康
- **优点**不产生额外流量
- **缺点**需要足够的流量样本才能判定
### 2.2 阈值设定:别让"小病"也触发告警
健康检查的阈值就像体温计:37度是正常,38度是低烧,40度是高烧。
**常见的阈值配置:**
- **优点**: 不产生额外流量
- **缺点**: 需要足够的流量样本才能判定
::: details 阈值设定表
| 指标 | 健康阈值 | 不健康阈值 | 说明 |
|:---|:---|:---|:---|
| **HTTP 状态码** | 200-399 | 400+ 或超时 | 4xx/5xx 都认为失败 |
| **TCP 连接** | 成功建立 | 连接超时 | 检查端口是否可达 |
| **响应时间** | < 500ms | > 2000ms | 超时时间通常设为2-5s |
| **HTTP状态码** | 200-399 | 400+或超时 | 4xx/5xx都认为失败 |
| **TCP连接** | 成功建立 | 连接超时 | 检查端口是否可达 |
| **响应时间** | < 500ms | > 2000ms | 超时时间通常设为2-5 |
| **连续失败次数** | - | 3次 | 避免单次抖动误判 |
| **检查间隔** | - | 5s | 太频繁会增加负载 |
**踩坑经验:阈值设置太"敏感"的教训**
::: tip 💡 踸见坑:阈值设置太"敏感"
某团队将健康检查的响应时间阈值设为100ms,而他们的应用平均响应时间在80-120ms之间波动。结果是服务器频繁被标记为"不健康",导致流量在健康和不健康之间反复横跳,系统整体可用率反而下降。
某团队将健康检查的响应时间阈值设为 100ms,而他们的应用平均响应时间在 80-120ms 之间波动。结果是服务器频繁被标记为"不健康",导致流量在健康和不健康之间反复横跳,系统整体可用率反而下降
**正确的做法:** 阈值应该设置为**P99 响应时间的 2-3 倍**,给正常波动留出足够的缓冲空间。
**正确的做法**: 阈值应该设置为**P99响应时间的2-3倍**,给正常波动留出足够的缓冲空间
:::
---
## 3. 会话保持:让"老顾客"一直找同一个"收银员"
## 4. 核心问题二:如何保证"老顾客"一直找同一个"收银员"?
想象你是奶茶店的常客,每次来都由同一个店员接待。她知道你的口味偏好(半糖、去冰),服务起来又快又贴心。但如果每次来都换一个新人,你得一遍遍重复同样的要求,效率大打折扣。
### 4.1 会话保持:让"老顾客"一直找同一个"收银员"
**会话保持(Session Persistence/Sticky Session** 就是解决这个问题的方法:确保同一个用户的请求,始终被路由到同一台后端服务器
想象你是奶茶店的常客,每次来都由同一个店员接待。她知道你的口味偏好(半糖、去冰),服务起来又快又贴心。但如果每次来都换一个新人,你得一遍遍重复同样的要求,效率大打折扣
<ClientOnly>
<SessionPersistenceDemo />
</ClientOnly>
**会话保持(Session Persistence/Sticky Session)** 就是解决这个问题的方法:确保同一个用户的请求,始终被路由到同一台后端服务器。
### 3.1 三种会话保持机制对比
<SessionPersistenceDemo />
| 机制 | 实现原理 | 优点 | 缺点 | 适用场景 |
|:---|:---|:---|:---|:---|
| **Cookie 插入** | LB在响应中插入Cookie,后续请求携带此Cookie | 不受IP变化影响,首次请求即可保持 | 客户端需支持Cookie,可能被禁用 | 电商购物车、登录态保持 |
| **IP 哈希** | 对客户端IP做哈希计算,映射到特定服务器 | 无需客户端支持,无状态 | IP变化会丢失会话,难以均匀分布 | 无Cookie环境、WebSocket |
| **粘性会话表** | LB维护会话到服务器的映射表 | 支持会话复制和故障转移 | 占用LB内存,需要额外同步 | 高可用要求严格的场景 |
### 4.2 三种会话保持机制对比
### 3.2 真实案例:电商大促期间的会话保持策略
| 机制 | 实现原理 | 优点 | 缺点 | 适用场景 |
| :------------- | :---------------------------------------- | :------------------------------ | :---------------------------- | :---------------------- |
| **Cookie插入** | LB在响应中插入Cookie,后续请求携带此Cookie | 不受IP变化影响,首次请求即可保持 | 客户端需支持Cookie,可能被禁用 | 电商购物车、登录态保持 |
| **IP哈希** | 对客户端IP做哈希计算,映射到特定服务器 | 无需客户端支持,无状态 | IP变化会丢失会话,难以均匀分布 | 无Cookie环境、WebSocket |
| **粘性会话表** | LB维护会话到服务器的映射表 | 支持会话复制和故障转移 | 占用LB内存,需要额外同步 | 高可用要求严格的场景 |
某电商平台在大促期间面临以下挑战:
::: tip 💡 使用建议
1. **购物车数据需要保持**:用户可能跨多个页面添加商品,需要保证请求都落在同一台服务器,购物车数据才能正确累计。
2. **秒杀场景下服务器动态扩容**:大促期间服务器数量从平时的10台动态扩展到50台。
3. **部分服务器可能故障**:需要能够快速剔除故障节点,同时不影响用户会话。
**他们的解决方案**
1. **采用 Cookie 插入机制**:负载均衡器(Nginx)在首次响应时设置 `SERVERID` Cookie,值为后端服务器的唯一标识。
2. **会话表持久化**:将会话映射表存储在 Redis 集群中,即使某台 Nginx 重启,也能从 Redis 恢复会话映射关系。
3. **故障转移策略**:当后端服务器健康检查失败时,将其从可用列表移除。对于已经绑定到该服务器的会话,下次请求时重新哈希分配到新的健康节点(牺牲一次会话保持,换取服务可用性)。
- **Cookie插入**: 优先推荐,兼容性好
- **IP哈希**: 只用于WebSocket等特殊场景
- **粘性会话表**: 配合Cookie,提供故障转移能力
:::
---
## 4. 部署策略:蓝绿部署与金丝雀发布
## 5. 核心问题三:如何实现零停机部署?
当新版本上线时,如何确保零停机?当新版本有 Bug 时,如何快速回滚?这涉及到两种经典的部署策略。
### 5.1 蓝绿部署:"一键切换"的零停机发布
### 4.1 蓝绿部署:"一键切换"的零停机发布
**核心思想**: 同时维护两套完全相同的生产环境(蓝环境和绿环境),但只有一个环境对外提供服务。
**核心思想**:同时维护两套完全相同的生产环境(蓝环境和绿环境),但只有一个环境对外提供服务。
<BlueGreenDeploymentDemo />
<ClientOnly>
<BlueGreenDeploymentDemo />
</ClientOnly>
**工作流程:**
**工作流程**
1. **初始状态**:蓝环境运行 v1.0(生产),绿环境待命
2. **部署新版本**:在绿环境部署 v1.1,进行内部冒烟测试
3. **切换流量**:将负载均衡器指向绿环境,流量瞬间切换到 v1.1。
4. **监控观察**:观察绿环境运行状态,确认无异常。
5. **保留旧版本**:蓝环境保持 v1.0 一段时间(如24小时),作为快速回滚的保险。
**优缺点分析**
1. **初始状态**: 蓝环境运行v1.0(生产),绿环境待命。
2. **部署新版本**: 在绿环境部署v1.1,进行内部冒烟测试。
3. **切换流量**: 将负载均衡器指向绿环境,流量瞬间切换到v1.1
4. **监控观察**: 观察绿环境运行状态,确认无异常。
5. **保留旧版本**: 蓝环境保持v1.0一段时间(如24小时),作为快速回滚的保险
::: tip ✨ 优缺点分析
| 优点 | 缺点 |
|:---|:---|
| ✅ 零停机时间切换在毫秒级完成 | ❌ 资源成本高需要同时维护两套环境 |
| ✅ 快速回滚发现问题立即切回原环境 | ❌ 数据库Schema变更时需要特别处理兼容性 |
| ✅ 新环境可完整测试后再接管流量 | ❌ 不适用于有状态服务如WebSocket长连接 |
| ✅ 零停机时间,切换在毫秒级完成 | ❌ 资源成本高,需要同时维护两套环境 |
| ✅ 快速回滚,发现问题立即切回原环境 | ❌ 数据库Schema变更时需要特别处理兼容性 |
| ✅ 新环境可完整测试后再接管流量 | ❌ 不适用于有状态服务(如WebSocket长连接) |
**适用场景**
- 对可用性要求极高的金融、电商核心交易系统
- 需要频繁发布但无法接受停机的 SaaS 服务
- 有充足的硬件/云资源预算
:::
### 4.2 金丝雀发布"小步快跑"的灰度策略
### 5.2 金丝雀发布:"小步快跑"的灰度策略
金丝雀发布得名于历史上的"煤矿金丝雀"——矿工带着金丝雀下井如果金丝雀出现异常说明有毒气体泄漏矿工立即撤离。在软件发布中金丝雀发布就是先让一小部分用户试用新版本观察没有问题后再逐步扩大范围。
金丝雀发布得名于历史上的"煤矿金丝雀"——矿工带着金丝雀下井,如果金丝雀出现异常,说明有毒气体泄漏,矿工立即撤离。在软件发布中,金丝雀发布就是先让一小部分用户试用新版本,观察没有问题后再逐步扩大范围。
<ClientOnly>
<CanaryReleaseDemo />
</ClientOnly>
<CanaryReleaseDemo />
**核心思想**
**核心思想:**
1. **小流量先行**先将 1% 的流量导入新版本服务器。
2. **观察指标**:持续监控错误率、延迟、业务关键指标
3. **逐步放量**:如果一切正常,逐步将比例提升到 5%、10%、25%、50%、100%。
4. **快速回滚**:一旦发现异常,立即将所有流量切回旧版本。
**金丝雀发布的优势:**
1. **小流量先行**: 先将1%的流量导入新版本服务器。
2. **观察指标**: 持续监控错误率、延迟、业务关键指标。
3. **逐步放量**: 如果一切正常,逐步将比例提升到5%、10%、25%、50%、100%
4. **快速回滚**: 一旦发现异常,立即将所有流量切回旧版本。
::: tip 💡 金丝雀发布的优势
| 优势 | 说明 |
|:---|:---|
| 🎯 **风险可控** | 即使新版本有严重 Bug也只影响少量用户 |
| 📊 **真实验证** | 在真实生产环境验证比测试环境更可靠 |
| 🎯 **风险可控** | 即使新版本有严重Bug,也只影响少量用户 |
| 📊 **真实验证** | 在真实生产环境验证,比测试环境更可靠 |
| 🚀 **快速迭代** | 团队可以更自信地频繁发布新功能 |
| 💰 **资源友好** | 不需要像蓝绿部署那样准备两套完整环境 |
**金丝雀发布的典型流量分配策略:**
```
阶段 1 (5分钟): 1% 新版本 → 99% 旧版本
阶段 2 (15分钟): 5% 新版本 → 95% 旧版本
阶段 3 (30分钟): 10% 新版本 → 90% 旧版本
阶段 4 (1小时): 25% 新版本 → 75% 旧版本
阶段 5 (2小时): 50% 新版本 → 50% 旧版本
阶段 6 (全量): 100%新版本
```
**注意**:每个阶段都需要持续监控关键指标,只有确认无异常后才进入下一阶段。
:::
---
## 5. 自动扩缩容:让系统自己"呼吸"
## 6. 核心问题四:如何让系统自己"呼吸"?
想象你开了一家餐厅。午餐高峰期需要10个服务员,但下午3点闲时只需要2个。如果一直维持10个人,人工成本爆炸;如果一直只有2个人,高峰期顾客等得不耐烦全跑了。
### 6.1 自动扩缩容:让系统像餐厅一样"灵活排班"
**自动扩缩容(Auto Scaling)就是让系统像餐厅一样"灵活排班"**——忙的时候自动加服务器,闲的时候自动减服务器。
想象你开了一家餐厅:
<ClientOnly>
<AutoScalingDemo />
</ClientOnly>
- **午餐高峰期**: 需要10个服务员,但下午3点闲时只需要2个
- 如果一直维持10个\*\*: 人工成本爆炸
- 如果一直只有2个: 高峰期顾客等不及,全跑了
### 5.1 扩容指标的选择
**自动扩缩容(Auto Scaling)** 就是让系统像餐厅一样"灵活排班"——忙的时候自动加服务器,闲的时候自动减服务器。
自动扩缩容的核心是回答一个问题:**什么时候该加机器?什么时候该减机器?**
<AutoScalingDemo />
常见的决策指标:
### 6.2 扩容指标的选择
| 指标 | 扩容阈值 | 缩容阈值 | 适用场景 |
|:---|:---|:---|:---|
| **CPU 使用率** | > 70% | < 30% | 计算密集型应用 |
| **内存使用率** | > 75% | < 40% | 内存密集型应用 |
| **QPS (每秒请求数)** | > 1000/s | < 400/s | API 网关、Web 服务 |
| **连接数** | > 5000 | < 1000 | 数据库、消息队列 |
| **自定义业务指标** | 视业务而定 | 视业务而定 | 特定业务场景 |
自动扩缩容的核心是回答一个问题:\*\* **什么时候该加机器?什么时候该减机器?**
### 5.2 扩容策略的"坑"与"解"
常见的决策指标:
**踩坑1:扩容反应太慢,流量洪峰已经把系统打挂了**
| 指标 | 扩容阈值 | 缩容阈值 | 适用场景 |
| :------------------ | :--------- | :--------- | :--------------- |
| **CPU使用率** | > 70% | < 30% | 计算密集型应用 |
| **内存使用率** | > 75% | < 40% | 内存密集型应用 |
| **QPS(每秒请求数)** | > 1000/s | < 400/s | API网关、Web服务 |
| **连接数** | > 5000 | < 1000 | 数据库、消息队列 |
| **自定义业务指标** | 视业务而定 | 视业务而定 | 特定业务场景 |
某电商大促期间,设置 CPU > 80% 触发扩容,但监控采集有1分钟延迟,新实例启动需要3分钟。结果流量来得太快,扩容还没完成,服务器已经被打挂。
::: tip 💡 扩容策略的"坑"与"解"
**解决方案**
- **提前扩容**:基于历史数据预测流量高峰,提前30分钟开始扩容
- **多级阈值**:设置 60% 预警(开始预热新实例)、70% 正式扩容、80% 紧急扩容
- **快速扩容**:使用容器化部署,新实例30秒内启动(相比虚拟机3-5分钟)
**坑1:扩容反应太慢,流量洪峰已经把系统打挂了**
**踩坑2:扩容太激进,成本爆炸**
某电商大促期间,设置CPU > 80%触发扩容,但监控采集有1分钟延迟,新实例启动需要3分钟。结果流量来得太快,扩容还没完成,服务器已经被打挂。
某创业公司设置了激进的自动扩容策略:CPU > 50% 就扩容。结果一个正常的业务波动就触发了扩容,服务器数量从5台膨胀到30台,月底云账单吓哭了 CTO。
**解决方案:**
**解决方案**
- **设置扩容冷却时间**:一次扩容后,至少等待5分钟才能再次扩容
- **设置最大实例数**:max = 当前实例数 × 2,防止无限膨胀
- **区分突刺和趋势**:只有连续3个周期都超过阈值才扩容,避免单点突刺触发
- **提前扩容**: 基于历史数据预测流量高峰,提前30分钟开始扩容
- **多级阈值**: 设置60%预警(开始预热新实例)、70%正式扩容、80%紧急扩容
- **快速扩容**: 使用容器化部署,新实例30秒内启动(相比虚拟机3-5分钟)
**踩坑3:缩容太快,刚扩容的机器马上就缩了**
**坑2:扩容太激进,成本爆炸**
团队设置了 CPU < 30% 缩容。扩容后流量还在消化,CPU 短暂回落到 25%,触发了缩容。刚缩完 CPU 又飙到 80%,又触发扩容——系统在"扩容-缩容-扩容"中疯狂震荡
创业公司设置了激进的自动扩容策略:CPU > 50%就扩容。结果一个正常的业务波动就触发了扩容,服务器数量从5台膨胀到30台,月底云账单吓哭了CTO
**解决方案**
- **缩容更保守**:扩容阈值 70%,缩容阈值 25%,中间有足够的缓冲带
- **容冷却时间更长**扩容后至少等待10分钟才能
- **渐进式缩容**:一次只缩 1 台,观察后再决定要不要继续缩
**解决方案:**
- **设置扩容冷却时间**: 一次扩容后,至少等待5分钟才能再次扩
- **设置最大实例数**: max = 当前实例数 × 2,防止无限膨胀
- **区分突刺和趋势**: 只有连续3个周期都超过阈值才扩容,避免单点突刺触发
**坑3:缩容太快,刚扩容的机器马上就缩了**
某团队设置了CPU < 30%缩容。扩容后流量还在消化,CPU短暂回落到25%,触发了缩容。刚缩完CPU又飙到80%,又触发扩容——系统在"扩容-缩容-扩容"中疯狂震荡。
**解决方案:**
- **缩容更保守**: 扩容阈值70%,缩容阈值25%,中间有足够的缓冲带
- **缩容冷却时间更长**: 扩容后至少等待10分钟才能缩容
- **渐进式缩容**: 一次只缩1台,观察后再决定要不要继续缩
:::
---
## 6. 多区域部署:当"灾难"来临时
## 7. 实战:如何选择负载均衡器?
想象你的奶茶店生意火爆,但你只有一个店面。某天突如其来的暴雨把店淹了,你得停业整修两周。这两周里,所有顾客都跑去竞争对手那里了,等你重新开业,客源已经流失大半。
### 7.1 主流负载均衡器对比
**单点故障是系统架构中的"阿喀琉斯之踵"**。多区域部署(Multi-Region Deployment)就是解决这个问题的方法:在不同地理位置部署多个数据中心,即使一个区域完全不可用,其他区域也能继续提供服务。
| 特性 | Nginx | HAProxy | Envoy | 云厂商负载均衡 |
| -------------- | ------------------------------- | --------------------- | -------------- | -------------- |
| **定位** | 高性能反向代理/负载均衡 | 开源负载均衡 | 云原生代理 | 托管负载均衡 |
| **性能** | 极高(C语言,事件驱动) | 高(事件驱动) | 高(C++/Rust) | 极高 |
| **功能丰富度** | 基础负载均衡、静态文件、缓存 | 丰富的负载均衡算法 | 高级路由、观测 | 功能全面 |
| **配置** | 配置文件(nginx.conf) | 配置文件(haproxy.cfg) | API/配置文件 | UI控制台 |
| **扩展** | C模块/Lua脚本 | Lua脚本 | WASM/Filter | 插件 |
| **适用场景** | 静态资源、七层负载均衡、SSL终结 | 七层负载均衡、高可用 | 服务网格、多云 | 快速上手 |
<ClientOnly>
<MultiRegionDemo />
</ClientOnly>
### 6.1 异地多活架构的核心概念
**主备模式(Active-Standby**
- 只有一个区域对外提供服务(主),其他区域待命(备)
- 备区实时同步数据,但不处理流量
- 主区故障时,手动或自动切换到备区
- **优点**:架构简单,数据一致性好
- **缺点**:备区资源利用率低,切换时有中断
**多活模式(Active-Active**
- 多个区域同时对外提供服务
- 用户请求被路由到最近的区域
- 区域之间实时同步数据
- **优点**:资源利用率高,故障影响小
- **缺点**:架构复杂,数据一致性挑战大
### 6.2 数据同步:多活架构的"阿喀琉斯之踵"
多活架构最大的挑战是**数据一致性**。当两个区域同时处理写入请求时,如何保证数据不会冲突?
**场景示例**
- 北京区域:用户A给账户充值 100 元,余额从 200 变为 300
- 上海区域:几乎同时,用户A消费 50 元,余额从 200 变为 150
如果两个区域分别执行后同步,最终余额应该是多少?300?150?还是其他值?
**解决方案对比:**
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|:---|:---|:---|:---|:---|
| **主从复制** | 只有一个主库可写,从库只读 | 实现简单,数据一致性好 | 主库单点,跨地域延迟大 | 读多写少,对一致性要求高 |
| **多主复制** | 多个主库可同时写,异步同步 | 写入性能高,就近写入 | 冲突解决复杂,可能丢数据 | 写入频繁,可接受短暂不一致 |
| **分布式事务** | 使用 2PC/3PC/TCC 等协议保证跨库事务 | 强一致性 | 性能开销大,复杂度高 | 金融交易等对一致性要求极高 |
| **CRDT(无冲突复制数据类型)** | 数学上保证无冲突的数据结构 | 自动合并,无需锁 | 数据类型受限,实现复杂 | 计数器、集合等特定场景 |
**真实案例:全球电商平台的订单系统**
某跨境电商在全球5个区域部署了数据中心。订单系统的架构设计如下:
- **订单创建**:使用"分区路由"策略,根据用户ID哈希确定主处理区域,该区域负责订单创建和初始状态变更,避免跨区域的写入冲突。
- **库存扣减**:使用分布式锁 + 乐观锁。库存数据以用户所在区域的副本为主,当跨区域访问时,先获取分布式锁,检查版本号,避免超卖。
- **最终一致性**:非关键数据(如推荐、统计)采用异步同步,允许秒级的延迟;关键数据(如支付状态)采用强同步,确保跨区一致性。
这套架构在实践中实现了 99.99% 的可用性,同时控制了跨区域同步的平均延迟在 100ms 以内。
---
## 7. 实战模板:从零搭建负载均衡架构
看完了理论,我们来动手实践。以下是一套可直接落地的架构方案。
### 7.1 中小型 Web 应用的推荐架构
**场景**:日活 10万 的电商平台,预算有限,团队规模 10人左右。
**架构方案:**
::: tip 💡 选型建议
**决策树:**
```
用户请求
[DNS 轮询] 多地域就近访问
[CDN] 静态资源缓存(图片、JS、CSS)
[L7 负载均衡 - Nginx] SSL卸载、URL路由、限流
[Web 服务器 - Node.js/Java] 业务逻辑处理
[缓存层 - Redis Cluster] 会话、热点数据
[数据库 - MySQL 主从] 读写分离
选择负载均衡器:
├─ 只需要基础的四层负载均衡?
│ ├─ 是 → LVS(开源免费)或 云厂商NLB
│ └─ 否 → 继续
├─ 需要服务网格、多云部署?
│ ├─ 是 → Envoy
│ └─ 否 → 继续
├─ 需要极其复杂的配置和插件?
│ ├─ 是 → HAProxy
│ └─ 否 → 继续
├─ 需要高性能+简单配置?
│ ├─ 是 → Nginx(首选)
│ └─ 继续
├─ 想要托管运维?
│ ├─ 是 → 云厂商负载均衡(AWS ALB、阿里SLB)
│ └─ Nginx自建
```
**关键配置:**
**Nginx 负载均衡配置示例:**
```nginx
upstream backend {
# 加权轮询,性能好的服务器权重更高
server 10.0.1.10 weight=5;
server 10.0.1.11 weight=3;
server 10.0.1.12 weight=2 backup; # backup 标记为备用
keepalive 32; # 长连接复用
}
server {
listen 80;
server_name api.example.com;
# 健康检查
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# 限流配置
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 会话保持配置(基于IP哈希)
# 注意:在 upstream 中配置 ip_hash; 代替加权轮询
}
# 静态资源缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
```
### 7.2 系统架构演进建议
**阶段一:起步期(日活 < 1万)**
- 单台服务器部署
- Nginx 做反向代理 + 负载均衡
- 重点关注:监控告警、日志收集
**阶段二:成长期(日活 1万-10万)**
- 横向扩展:2-3 台 Web 服务器
- 引入 Redis 做缓存和会话存储
- MySQL 主从复制,读写分离
- 引入 CDN 加速静态资源
**阶段三:成熟期(日活 10万-100万)**
- 多地域部署,就近访问
- 引入消息队列削峰填谷
- 数据库分库分表
- 自动化运维:CI/CD、自动扩缩容
:::
---
## 8. 名词对照表
## 8. 总结:负载均衡的核心思维
| 英文术语 | 中文对照 | 解释 |
|:---|:---|:---|
| **Load Balancer** | 负载均衡器 | 将流量分发到多个后端服务器的设备或软件 |
| **L4 Load Balancing** | 四层负载均衡 | 基于传输层(TCP/UDP)的负载均衡 |
| **L7 Load Balancing** | 七层负载均衡 | 基于应用层(HTTP/HTTPS)的负载均衡 |
| **Health Check** | 健康检查 | 定期检查后端服务器健康状态的机制 |
| **Session Persistence** | 会话保持 | 确保同一用户的请求始终路由到同一台服务器 |
| **Sticky Session** | 粘性会话 | 另一种称呼,同 Session Persistence |
| **Blue-Green Deployment** | 蓝绿部署 | 两套环境切换的零停机发布策略 |
| **Canary Release** | 金丝雀发布 | 小流量先行验证的灰度发布策略 |
| **Auto Scaling** | 自动扩缩容 | 根据负载自动增加或减少服务器数量 |
| **Horizontal Scaling** | 水平扩展 | 增加服务器数量来提升处理能力 |
| **Vertical Scaling** | 垂直扩展 | 提升单机配置(CPU、内存)来提升处理能力 |
| **Multi-Region** | 多区域 | 在多个地理区域部署服务 |
| **Active-Active** | 多活 | 多个区域同时对外提供服务 |
| **Active-Standby** | 主备 | 只有一个区域提供服务,其他待命 |
| **Data Replication** | 数据同步 | 跨区域的数据复制机制 |
| **RTO** | 恢复时间目标 | 系统故障后需要在多长时间内恢复 |
| **RPO** | 恢复点目标 | 系统故障后可以接受的数据丢失量 |
### 8.1 核心原则回顾
| 原则 | 含义 | 实践要点 |
| -------- | -------------------------- | ------------------------------------- |
| **分层** | L4处理"快递分拣"(快但简单) | L4处理数据库、游戏;L7处理Web、API |
| **冗余** | 单点故障是架构的敌人 | 通过多实例、多区域部署提升可用性 |
| **渐进** | 发布新版本不要"一刀切" | 蓝绿部署实现零停机;金丝雀实现风险可控 |
| **弹性** | 系统应该像生命体一样"呼吸" | 忙时自动扩容,闲时自动缩容 |
### 8.2 设计检查清单
在引入负载均衡前,问自己以下问题:
- [ ] 是否真的需要负载均衡?(单机性能是否真的不够)
- [ ] 选择L4还是L7?(根据业务场景)
- [ ] 如何处理会话保持?(Cookie、IP哈希、会话表)
- [ ] 如何实现健康检查?(主动、被动、阈值设置)
- [ ] 如何实现零停机?(蓝绿部署、金丝雀)
- [ ] 如何实现弹性?(扩缩指标、冷却时间、最大实例数)
---
## 总结:负载均衡的核心思维
## 9. 名词速查表
通过本文的学习,我们可以提炼出负载均衡设计的几个核心思维:
**1. 分层思维**
- L4 处理"快递分拣"(快但简单)
- L7 处理"内容检查"(慢但智能)
- 根据场景选择合适的层次
**2. 冗余思维**
- 单点故障是架构的敌人
- 通过多实例、多区域部署提升可用性
- 健康检查确保"坏节点"及时剔除
**3. 渐进思维**
- 发布新版本不要"一刀切"
- 蓝绿部署实现零停机
- 金丝雀发布实现风险可控
**4. 弹性思维**
- 系统应该像生命体一样"呼吸"
- 忙时自动扩容,闲时自动缩容
- 多区域部署实现就近服务和容灾
负载均衡不是简单的"流量分发",而是一套关于**高可用、高性能、高弹性**的系统工程思维。希望本文能帮助你在实际工作中做出更好的架构决策。
| 名词 | 英文 | 解释 |
| ---------------- | --------------------- | ---------------------------------------- | ------------------------------ |
| **负载均衡器** | Load Balancer | 将流量分发到多个后端服务器的设备或软件 |
| **四层负载均衡** | L4 Load Balancing | 基于传输层(TCP/UDP)的负载均衡 |
| **七层负载均衡** | L7 Load Balancing | 基于应用层(HTTP/HTTPS)的负载均衡 |
| **健康检查** | Health Check | 定期检查后端服务器的健康状态的机制 |
| **会话保持** | Session Persistence | 确保同一用户的请求始终路由到同一台服务器 |
| **粘性会话** | Sticky Session | 另一种称呼,同Session Persistence |
| **蓝绿部署** | Blue-Green Deployment | 两套环境切换的零停机发布策略 |
| **金丝雀发布** | Canary Release | 小流量先行验证的灰度发布策略 |
| **自动扩缩容** | Auto Scaling | 根据负载自动增加或减少服务器数量 |
| **水平扩展** | Horizontal Scaling | 增加服务器数量来提升处理能力 |
| **垂直扩展** | Vertical Scaling | 提升单机配置(CPU、内存)来提升处理能力 |
| **多区域** | Multi-Region | 在多个地理区域部署服务 |
| **多活** | Active-Active | 多个区域同时对外提供服务 |
| **主备** | Active-Standby | 只有一个区域提供服务,其他待命 |
| **数据同步** | Data Replication | 跨区域的数据复制机制 |
| **RTO** | RTO | 恢复时间目标 | 系统故障后需要在多长时间内恢复 |
| **RPO** | RPO | 恢复点目标 | 系统故障后可以接受的数据丢失量 |
File diff suppressed because it is too large Load Diff
+67 -54
View File
@@ -2,11 +2,6 @@
<script setup>
import UrlToBrowserQuickStart from '../../.vitepress/theme/components/appendix/url-to-browser/UrlToBrowserQuickStart.vue'
import UrlParserDemo from '../../.vitepress/theme/components/appendix/url-to-browser/UrlParserDemo.vue'
import DnsLookupDemo from '../../.vitepress/theme/components/appendix/url-to-browser/DnsLookupDemo.vue'
import TcpHandshakeDemo from '../../.vitepress/theme/components/appendix/url-to-browser/TcpHandshakeDemo.vue'
import HttpExchangeDemo from '../../.vitepress/theme/components/appendix/url-to-browser/HttpExchangeDemo.vue'
import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url-to-browser/BrowserRenderingDemo.vue'
</script>
> **学习指南**:本章节无需编程基础。我们将用**"网购"**的生活化比喻,配合**真实的技术过程**,带你一步步理解浏览器如何将一行网址变成丰富多彩的页面。
@@ -16,6 +11,7 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
## 0. 引言:当你按下回车键的那一刻
想象你正在进行一次**网购**。你需要:
1. **填写订单**(选好商品,确认收货地址)
2. **系统查找仓库**(根据店铺名找到具体的发货仓库)
3. **建立物流通道**(确保仓库正常营业且能发货)
@@ -35,6 +31,7 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
### 生活比喻:填写购物单
假设你只在订单上写"买鞋子",仓库肯定不知道发哪双。你需要写清楚:
- **店铺类型**(官方旗舰店/普通店)
- **店铺名称**Nike 官方店)
- **商品位置**(男鞋区/跑鞋系列)
@@ -45,14 +42,14 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
**URLUniform Resource Locator,统一资源定位符)**就是浏览器世界的"商品定位码"。当你在地址栏输入 `https://www.example.com:8080/path/page.html?id=123#section`,浏览器会立即拆解它:
| URL 部分 | 示例值 | 网购类比 | 技术作用 |
|----------|--------|-----------|----------|
| **协议** `https://` | 安全超文本传输协议 | **物流方式**:保密配送(HTTPS)vs 普通配送(HTTP) | 决定使用什么规则通信。`http` 是普通传输,`https` 是加密传输 |
| **域名** `www.example.com` | 服务器的人类可读名字 | **店铺名称**:京东超市 | 告诉浏览器要找哪台服务器。域名是为了让人记住,最终要转换成 IP 地址 |
| **端口** `:8080` | 服务器的具体"门牌号" | **柜台编号**:3号柜台(默认不写) | 服务器上可能有多个服务,端口指定访问哪一个。HTTP 默认 80,HTTPS 默认 443 |
| **路径** `/path/page.html` | 服务器上的文件位置 | **货架位置**:日用品区/第三排 | 指定服务器上的具体资源位置 |
| **查询参数** `?id=123` | 附加信息 | **订单备注**:红色、XL码 | 传递给服务器的额外数据,如搜索关键词、页码等 |
| **锚点** `#section` | 页面内的位置 | **说明书页码**:翻到第5页 | 页面加载后自动滚动到指定位置,不发送给服务器 |
| URL 部分 | 示例值 | 网购类比 | 技术作用 |
| -------------------------- | -------------------- | -------------------------------------------------- | ------------------------------------------------------------------------ |
| **协议** `https://` | 安全超文本传输协议 | **物流方式**:保密配送(HTTPS)vs 普通配送(HTTP) | 决定使用什么规则通信。`http` 是普通传输,`https` 是加密传输 |
| **域名** `www.example.com` | 服务器的人类可读名字 | **店铺名称**:京东超市 | 告诉浏览器要找哪台服务器。域名是为了让人记住,最终要转换成 IP 地址 |
| **端口** `:8080` | 服务器的具体"门牌号" | **柜台编号**3号柜台(默认不写) | 服务器上可能有多个服务,端口指定访问哪一个。HTTP 默认 80,HTTPS 默认 443 |
| **路径** `/path/page.html` | 服务器上的文件位置 | **货架位置**:日用品区/第三排 | 指定服务器上的具体资源位置 |
| **查询参数** `?id=123` | 附加信息 | **订单备注**:红色、XL码 | 传递给服务器的额外数据,如搜索关键词、页码等 |
| **锚点** `#section` | 页面内的位置 | **说明书页码**:翻到第5页 | 页面加载后自动滚动到指定位置,不发送给服务器 |
<UrlParserDemo />
@@ -65,6 +62,7 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
### 生活比喻:查仓库地址
你下单写的是"Nike 官方店",但物流系统不知道仓库在哪。它需要查地址簿:
1. 先查**常用地址**(最近买过这家吗)→ 浏览器缓存
2. 没有的话问**小区快递点**(他们知道大区域的分配)→ 本地 DNS 服务器
3. 问**总部调度中心**(知道.com类店铺归谁管)→ 根域名服务器
@@ -89,6 +87,7 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
```
**查询类型说明:**
- **递归查询(Recursive Query**:浏览器只发一次请求,本地 DNS 负责层层查询后返回结果
- **迭代查询(Iterative Query**:每一层只告诉下一层去哪查,浏览器需要多次查询
- **缓存机制**:查询结果会被缓存,下次直接返回,大大加速访问
@@ -104,6 +103,7 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
### 生活比喻:建立物流通道
假设物流车直接开到仓库,结果:
- 仓库关门了 → 白跑一趟
- 仓库爆仓不接单 → 无法发货
- 找不到卸货口 → 无法对接
@@ -135,6 +135,7 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
三次握手确保:**双方都能发、双方都能收** —— 四个条件都满足,才能可靠传输。
**TCP 还负责:**
- **数据分包**:大数据拆成小数据包传输
- **顺序重组**:确保数据包按正确顺序组装
- **错误重传**:丢包后自动重新发送
@@ -158,6 +159,7 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
**HTTPHyperText Transfer Protocol,超文本传输协议)**是浏览器和服务器之间的"对话规则"。通道建立后,浏览器发送**取货请求**,**核心目标是拿回网页的源代码(HTML 文件)**:
**HTTP 请求示例:**
```http
GET /index.html HTTP/1.1 ← 请求方法 + 路径 + 协议版本
Host: www.example.com ← 目标主机(支持虚拟主机,一台服务器可托管多个网站)
@@ -174,8 +176,9 @@ Cookie: session_id=abc123 ← 身份凭证
你平时写的 API 调用(`fetch` / `axios`)和浏览器访问网页,在 **HTTP 层面完全是同一个东西**
它们都是发送一个请求,服务器返回一段文本数据。
* 如果服务器给的是 **HTML**,浏览器就把它**画出来**(变成网页)。
* 如果服务器给的是 **JSON**你的代码就把它**存起来**用于逻辑处理)。
- 如果服务器给的是 **HTML**浏览器就把它**画出来**变成网页)。
- 如果服务器给的是 **JSON**,你的代码就把它**存起来**(用于逻辑处理)。
**根本就没有"两种"请求,只有同一种 HTTP 请求,只是返回的数据格式(Content-Type)不同而已。**
这也是为什么理解了 HTTP,你就理解了 90% 的后端 API 原理。
@@ -184,6 +187,7 @@ Cookie: session_id=abc123 ← 身份凭证
:::
**常见 HTTP 方法:**
- `GET`:获取资源(安全、幂等,可被缓存)
- `POST`:提交数据(创建资源,如注册、登录)
- `PUT`:更新资源(完整替换)
@@ -192,6 +196,7 @@ Cookie: session_id=abc123 ← 身份凭证
- `HEAD`:获取响应头(不返回主体,用于检查资源是否存在)
**服务器返回 HTTP 响应:**
```http
HTTP/1.1 200 OK ← 协议版本 + 状态码 + 状态描述
Date: Mon, 23 May 2025 12:00:00 GMT ← 服务器时间
@@ -205,18 +210,18 @@ Set-Cookie: user_id=xyz789 ← 设置 Cookie
**HTTP 状态码分类:**
| 状态码 | 类别 | 含义 | 生活类比 |
|--------|------|------|----------|
| **200** | 成功 | 请求成功处理 | "订单确认,马上发货" |
| **301/302** | 重定向 | 资源已移动 | "本店搬家了,请去新店下单" |
| **304** | 未修改 | 缓存仍有效 | "你上次买的还能用,不用重新发货" |
| **400** | 客户端错误 | 请求格式错误 | "订单填写模糊,看不懂" |
| **401** | 未授权 | 需要身份验证 | "请先出示会员卡" |
| **403** | 禁止访问 | 权限不足 | "非内部人员禁止入内" |
| **404** | 未找到 | 资源不存在 | "仓库里没这款商品" |
| **500** | 服务器错误 | 服务器内部错误 | "仓库起火了,暂时发不了货" |
| **502** | 网关错误 | 上游服务器无响应 | "总仓没货了,分仓也调不到" |
| **503** | 服务不可用 | 服务器过载或维护 | "爆单了,暂停接单" |
| 状态码 | 类别 | 含义 | 生活类比 |
| ----------- | ---------- | ---------------- | -------------------------------- |
| **200** | 成功 | 请求成功处理 | "订单确认,马上发货" |
| **301/302** | 重定向 | 资源已移动 | "本店搬家了,请去新店下单" |
| **304** | 未修改 | 缓存仍有效 | "你上次买的还能用,不用重新发货" |
| **400** | 客户端错误 | 请求格式错误 | "订单填写模糊,看不懂" |
| **401** | 未授权 | 需要身份验证 | "请先出示会员卡" |
| **403** | 禁止访问 | 权限不足 | "非内部人员禁止入内" |
| **404** | 未找到 | 资源不存在 | "仓库里没这款商品" |
| **500** | 服务器错误 | 服务器内部错误 | "仓库起火了,暂时发不了货" |
| **502** | 网关错误 | 上游服务器无响应 | "总仓没货了,分仓也调不到" |
| **503** | 服务不可用 | 服务器过载或维护 | "爆单了,暂停接单" |
<HttpExchangeDemo />
@@ -263,22 +268,29 @@ Document
浏览器解析所有的 CSS(内联、外部文件),构建**CSSOMCSS Object Model)树**。这就像理解说明书上的样式规则:
```css
.header { color: blue; font-size: 24px; } /* 标题要是蓝色的 */
.content { display: none; } /* 内容暂时隐藏 */
.header {
color: blue;
font-size: 24px;
} /* 标题要是蓝色的 */
.content {
display: none;
} /* 内容暂时隐藏 */
```
#### 步骤3:合并 → 渲染树 (准备组装)
DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**
关键点:**只有"可见"的元素才会在渲染树中**。
* `.header`:在渲染树中(可见)。
* `.content`**不在**渲染树中(因为 `display: none`,就像被扔掉的包装纸,不需要组装)。
- `.header`:在渲染树中(可见)。
- `.content`**不在**渲染树中(因为 `display: none`,就像被扔掉的包装纸,不需要组装)。
#### 步骤4:布局 (Layout / Reflow) —— 测量尺寸
浏览器计算渲染树中每个节点在屏幕上的**精确坐标和大小**。
* "这个标题框宽 100px,高 50px,放在屏幕左上角 (0,0) 位置。"
* 这个过程叫**重排 (Reflow)**。如果窗口大小变了(比如手机横屏),所有元素的位置都要重新计算,非常消耗性能。
- "这个标题框宽 100px,高 50px,放在屏幕左上角 (0,0) 位置。"
- 这个过程叫**重排 (Reflow)**。如果窗口大小变了(比如手机横屏),所有元素的位置都要重新计算,非常消耗性能。
#### 步骤5:绘制 (Paint) —— 上色
@@ -300,17 +312,18 @@ DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
让我们回顾整个旅程:
| 阶段 | 技术术语 | 网购类比 | 核心任务 | 关键技术 |
|------|----------|----------|----------|----------|
| **1. 解析** | URL 解析 | 填写订单 | 理解买家想买什么 | 协议、域名、端口、路径、参数 |
| **2. 查询** | DNS 查询 | 查仓库址 | 找到店铺的发货仓库 | 递归/迭代查询、缓存机制 |
| **3. 连接** | TCP 握手 | 建立通道 | 确保物流通畅 | 三次握手、序列号、流量控制 |
| **4. 对话** | HTTP 交换 | 仓库发货 | 提交订单并收货 | 请求方法、状态码、头部字段 |
| **5. 展示** | 浏览器渲染 | 拆箱组装 | 把商品展示出来 | DOM、CSSOM、渲染树、布局、绘制 |
| 阶段 | 技术术语 | 网购类比 | 核心任务 | 关键技术 |
| ----------- | ---------- | -------- | ------------------ | ------------------------------ |
| **1. 解析** | URL 解析 | 填写订单 | 理解买家想买什么 | 协议、域名、端口、路径、参数 |
| **2. 查询** | DNS 查询 | 查仓库址 | 找到店铺的发货仓库 | 递归/迭代查询、缓存机制 |
| **3. 连接** | TCP 握手 | 建立通道 | 确保物流通畅 | 三次握手、序列号、流量控制 |
| **4. 对话** | HTTP 交换 | 仓库发货 | 提交订单并收货 | 请求方法、状态码、头部字段 |
| **5. 展示** | 浏览器渲染 | 拆箱组装 | 把商品展示出来 | DOM、CSSOM、渲染树、布局、绘制 |
**整个过程通常在几百毫秒内完成** —— 想想这有多么不可思议!
你的浏览器在不到1秒的时间里:
- 解析了一个复杂的地址
- 查询了分布在全球的 DNS 服务器
- 和千里之外的服务器建立了可靠连接
@@ -323,20 +336,20 @@ DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
## 7. 名词速查表 (Glossary)
| 名词 | 全称 | 简单解释 |
|------|------|----------|
| **URL** | Uniform Resource Locator | **统一资源定位符**。网页的"地址",告诉浏览器去哪里找资源。 |
| **DNS** | Domain Name System | **域名系统**。互联网的"电话簿",把人类可读的域名转换成机器可读的 IP 地址。 |
| **IP 地址** | Internet Protocol Address | **互联网协议地址**。每台联网设备的唯一"门牌号",如 `192.168.1.1`。 |
| **TCP** | Transmission Control Protocol | **传输控制协议**。确保数据可靠传输的"规则",通过三次握手建立连接。 |
| **HTTP** | HyperText Transfer Protocol | **超文本传输协议**。浏览器和服务器"对话"的规则。 |
| **HTTPS** | HTTP Secure | **安全的 HTTP**。在 HTTP 基础上加了加密(TLS/SSL),保护数据安全。 |
| **HTML** | HyperText Markup Language | **超文本标记语言**。网页的"骨架",定义内容的结构。 |
| **CSS** | Cascading Style Sheets | **层叠样式表**。网页的"皮肤",定义内容的外观。 |
| **DOM** | Document Object Model | **文档对象模型**。浏览器把 HTML 转换成的树形结构,方便操作。 |
| **CSSOM** | CSS Object Model | **CSS 对象模型**。浏览器把 CSS 转换成的树形结构。 |
| **渲染** | Rendering | 浏览器把代码转换成屏幕像素的过程。 |
| **RTT** | Round Trip Time | **往返时间**。数据包从发送到接收确认的时间,影响网页加载速度。 |
| 名词 | 全称 | 简单解释 |
| ----------- | ----------------------------- | -------------------------------------------------------------------------- |
| **URL** | Uniform Resource Locator | **统一资源定位符**。网页的"地址",告诉浏览器去哪里找资源。 |
| **DNS** | Domain Name System | **域名系统**。互联网的"电话簿",把人类可读的域名转换成机器可读的 IP 地址。 |
| **IP 地址** | Internet Protocol Address | **互联网协议地址**。每台联网设备的唯一"门牌号",如 `192.168.1.1` |
| **TCP** | Transmission Control Protocol | **传输控制协议**。确保数据可靠传输的"规则",通过三次握手建立连接。 |
| **HTTP** | HyperText Transfer Protocol | **超文本传输协议**。浏览器和服务器"对话"的规则。 |
| **HTTPS** | HTTP Secure | **安全的 HTTP**。在 HTTP 基础上加了加密(TLS/SSL),保护数据安全。 |
| **HTML** | HyperText Markup Language | **超文本标记语言**。网页的"骨架",定义内容的结构。 |
| **CSS** | Cascading Style Sheets | **层叠样式表**。网页的"皮肤",定义内容的外观。 |
| **DOM** | Document Object Model | **文档对象模型**。浏览器把 HTML 转换成的树形结构,方便操作。 |
| **CSSOM** | CSS Object Model | **CSS 对象模型**。浏览器把 CSS 转换成的树形结构。 |
| **渲染** | Rendering | 浏览器把代码转换成屏幕像素的过程。 |
| **RTT** | Round Trip Time | **往返时间**。数据包从发送到接收确认的时间,影响网页加载速度。 |
---
File diff suppressed because it is too large Load Diff