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:
@@ -10,3 +10,7 @@ MULTI_LANGUAGE_PLAN.md
|
|||||||
scripts/collapse_code_blocks.py
|
scripts/collapse_code_blocks.py
|
||||||
.gitignore
|
.gitignore
|
||||||
scripts/verify.sh
|
scripts/verify.sh
|
||||||
|
REFACTORING_PLAN.md
|
||||||
|
.gitignore
|
||||||
|
.gitignore
|
||||||
|
REFACTORING_REPORT.md
|
||||||
|
|||||||
@@ -1,50 +1,496 @@
|
|||||||
|
<!--
|
||||||
|
DocumentationDemo.vue - API 文档演示组件
|
||||||
|
展示 API 文档的编写规范和最佳实践
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="demo-container">
|
<div class="demo">
|
||||||
<div class="demo-header">
|
<div class="header">
|
||||||
<h4>{{ title }}</h4>
|
<span class="icon">📚</span>
|
||||||
<p class="hint">{{ description }}</p>
|
<span class="title">API 文档:最好的 API 文档就是代码本身</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 class="demo-content">
|
|
||||||
<el-alert type="info" :closable="false">
|
|
||||||
文档演示组件占位符 - 待实现具体交互
|
|
||||||
</el-alert>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
const title = ref('API文档演示')
|
const tools = [
|
||||||
const description = ref('展示RESTful API文档的编写规范和最佳实践,包括Swagger、OpenAPI等工具的使用')
|
{
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 20px;
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-header {
|
||||||
|
padding: 16px;
|
||||||
background: var(--vp-c-bg-soft);
|
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 {
|
.tool-title {
|
||||||
margin-bottom: 20px;
|
font-weight: 700;
|
||||||
}
|
font-size: 16px;
|
||||||
|
|
||||||
.demo-header h4 {
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.tool-tags {
|
||||||
margin: 0;
|
display: flex;
|
||||||
font-size: 14px;
|
gap: 8px;
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
display: flex;
|
||||||
|
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;
|
flex-direction: column;
|
||||||
gap: 16px;
|
}
|
||||||
|
|
||||||
|
.feature-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,97 +1,379 @@
|
|||||||
|
<!--
|
||||||
|
ErrorHandlingDemo.vue - 错误处理演示组件
|
||||||
|
展示错误处理的正确和错误示例对比
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="demo-container">
|
<div class="demo">
|
||||||
<div class="demo-header">
|
<div class="header">
|
||||||
<h4>错误处理演示</h4>
|
<span class="icon">🚨</span>
|
||||||
<p class="hint">展示RESTful API中的错误处理机制</p>
|
<span class="title">错误处理:优雅地"拒绝"</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="demo-content">
|
|
||||||
<div class="error-types">
|
<div class="content">
|
||||||
<div class="error-item">
|
<div class="comparison-tabs">
|
||||||
<span class="code">400</span>
|
<button
|
||||||
<span class="name">Bad Request</span>
|
class="tab-btn bad"
|
||||||
<span class="desc">请求参数错误</span>
|
:class="{ active: selectedTab === 'bad' }"
|
||||||
|
@click="selectedTab = 'bad'"
|
||||||
|
>
|
||||||
|
❌ 错误示范
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab-btn good"
|
||||||
|
:class="{ active: selectedTab === 'good' }"
|
||||||
|
@click="selectedTab = 'good'"
|
||||||
|
>
|
||||||
|
✅ 正确示范
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="error-item">
|
|
||||||
<span class="code">401</span>
|
<!-- 错误示范 -->
|
||||||
<span class="name">Unauthorized</span>
|
<div v-if="selectedTab === 'bad'" class="comparison bad">
|
||||||
<span class="desc">未授权访问</span>
|
<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="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>
|
||||||
|
|
||||||
|
<!-- 正确示范 -->
|
||||||
|
<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="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 class="error-item">
|
|
||||||
<span class="code">404</span>
|
|
||||||
<span class="name">Not Found</span>
|
|
||||||
<span class="desc">资源不存在</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="error-item">
|
|
||||||
<span class="code">500</span>
|
|
||||||
<span class="name">Server Error</span>
|
|
||||||
<span class="desc">服务器内部错误</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<style scoped>
|
||||||
.demo-container {
|
.demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
|
margin: 24px 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.header {
|
||||||
margin-bottom: 20px;
|
padding: 16px 20px;
|
||||||
}
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
color: white;
|
||||||
.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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-types {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-item {
|
.icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
gap: 12px;
|
||||||
gap: 16px;
|
margin-bottom: 20px;
|
||||||
padding: 12px 16px;
|
}
|
||||||
|
|
||||||
|
.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);
|
background: var(--vp-c-bg);
|
||||||
border-radius: 8px;
|
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-family: monospace;
|
||||||
font-weight: 600;
|
font-size: 13px;
|
||||||
color: #f56c6c;
|
|
||||||
font-size: 16px;
|
|
||||||
min-width: 50px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-item .name {
|
.status-line.bad .code {
|
||||||
font-weight: 500;
|
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);
|
color: var(--vp-c-text-1);
|
||||||
min-width: 120px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-item .desc {
|
.problems, .highlights {
|
||||||
color: var(--vp-c-text-2);
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problems h4, .highlights h4 {
|
||||||
font-size: 14px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,50 +1,397 @@
|
|||||||
|
<!--
|
||||||
|
ResponseStructureDemo.vue - HTTP 响应结构演示组件
|
||||||
|
展示标准化 API 响应结构和分页响应
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="demo-container">
|
<div class="demo">
|
||||||
<div class="demo-header">
|
<div class="header">
|
||||||
<h4>{{ title }}</h4>
|
<span class="icon">📦</span>
|
||||||
<p class="hint">{{ description }}</p>
|
<span class="title">HTTP 响应结构:标准化的数据契约</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 class="demo-content">
|
|
||||||
<el-alert type="info" :closable="false">
|
|
||||||
响应结构演示组件占位符 - 待实现具体交互
|
|
||||||
</el-alert>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
const title = ref('HTTP响应结构演示')
|
const tabs = [
|
||||||
const description = ref('展示HTTP响应的结构,包括状态行、响应头和响应体')
|
{ 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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.demo-container {
|
.demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
|
||||||
background: var(--vp-c-bg-soft);
|
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;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header h4 {
|
.tab-btn {
|
||||||
margin: 0 0 8px 0;
|
padding: 8px 16px;
|
||||||
color: var(--vp-c-text-1);
|
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 {
|
.tab-btn:hover {
|
||||||
margin: 0;
|
transform: translateY(-1px);
|
||||||
font-size: 14px;
|
}
|
||||||
|
|
||||||
|
.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);
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,50 +1,413 @@
|
|||||||
|
<!--
|
||||||
|
VersioningStrategyDemo.vue - API 版本控制策略演示
|
||||||
|
展示 4 种版本控制策略的对比
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="demo-container">
|
<div class="demo">
|
||||||
<div class="demo-header">
|
<div class="header">
|
||||||
<h4>{{ title }}</h4>
|
<span class="icon">🔢</span>
|
||||||
<p class="hint">{{ description }}</p>
|
<span class="title">API 版本控制:向后兼容的艺术</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 class="demo-content">
|
|
||||||
<el-alert type="info" :closable="false">
|
|
||||||
版本策略演示组件占位符 - 待实现具体交互
|
|
||||||
</el-alert>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
const title = ref('版本策略演示')
|
const strategies = [
|
||||||
const description = ref('展示API版本控制的策略,包括URL版本、Header版本、内容协商等方式')
|
{
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.demo-container {
|
.demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
|
margin: 24px 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.header {
|
||||||
margin-bottom: 20px;
|
padding: 16px 20px;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header h4 {
|
.icon {
|
||||||
margin: 0 0 8px 0;
|
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);
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.strategy-stars {
|
||||||
margin: 0;
|
display: flex;
|
||||||
font-size: 14px;
|
gap: 2px;
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
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;
|
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>
|
</style>
|
||||||
|
|||||||
+47
-12
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="architecture-comparison-demo">
|
<div class="architecture-comparison-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>🏗️ 架构演进对比</h4>
|
<span class="icon">🏗️</span>
|
||||||
<p>四个时代的核心架构特征对比</p>
|
<span class="title">架构演进对比</span>
|
||||||
|
<span class="subtitle">四个时代的核心架构特征</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="comparison-grid">
|
<div class="comparison-grid">
|
||||||
@@ -50,6 +51,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>架构演进是为了解决上一个时代的痛点,但也带来了新的复杂度。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -108,24 +114,35 @@ const currentEra = computed(() => {
|
|||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.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 {
|
.demo-header .icon {
|
||||||
margin: 0 0 0.5rem 0;
|
font-size: 1.25rem;
|
||||||
font-size: 1.1rem;
|
}
|
||||||
|
|
||||||
|
.demo-header .title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header p {
|
.demo-header .subtitle {
|
||||||
margin: 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comparison-grid {
|
.comparison-grid {
|
||||||
@@ -152,8 +169,7 @@ const currentEra = computed(() => {
|
|||||||
|
|
||||||
.era-card.active {
|
.era-card.active {
|
||||||
border-color: var(--vp-c-brand);
|
border-color: var(--vp-c-brand);
|
||||||
background: rgba(102, 126, 234, 0.1);
|
background: var(--vp-c-brand-soft);
|
||||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.era-icon {
|
.era-icon {
|
||||||
@@ -271,6 +287,25 @@ const currentEra = computed(() => {
|
|||||||
color: var(--vp-c-text-2);
|
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) {
|
@media (max-width: 768px) {
|
||||||
.comparison-grid {
|
.comparison-grid {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
|||||||
+46
-10
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-docker-demo">
|
<div class="container-docker-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>🐳 Docker 容器化演示</h4>
|
<span class="icon">🐳</span>
|
||||||
<p>理解容器如何让应用"一次打包,到处运行"</p>
|
<span class="title">Docker 容器化演示</span>
|
||||||
|
<span class="subtitle">理解容器如何让应用"一次打包,到处运行"</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="docker-visualization">
|
<div class="docker-visualization">
|
||||||
@@ -46,6 +47,11 @@
|
|||||||
<div class="benefit-desc">{{ benefit.desc }}</div>
|
<div class="benefit-desc">{{ benefit.desc }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>容器化让应用"一次构建,到处运行",解决了环境一致性和快速部署的问题。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -69,24 +75,35 @@ const benefits = [
|
|||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.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 {
|
.demo-header .icon {
|
||||||
margin: 0 0 0.5rem 0;
|
font-size: 1.25rem;
|
||||||
font-size: 1.1rem;
|
}
|
||||||
|
|
||||||
|
.demo-header .title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header p {
|
.demo-header .subtitle {
|
||||||
margin: 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docker-visualization {
|
.docker-visualization {
|
||||||
@@ -268,4 +285,23 @@ const benefits = [
|
|||||||
grid-template-columns: 1fr;
|
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>
|
</style>
|
||||||
|
|||||||
+99
-62
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="evolution-intro-demo">
|
<div class="evolution-intro-demo">
|
||||||
<div class="intro-header">
|
<div class="demo-header">
|
||||||
<h3>后端架构进化之旅</h3>
|
<span class="icon">🏗️</span>
|
||||||
<p>用一个餐厅的成长历程,理解后端架构的 30 年变迁</p>
|
<span class="title">后端架构进化之旅</span>
|
||||||
|
<span class="subtitle">用餐厅比喻理解 30 年架构演进</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timeline-cards">
|
<div class="timeline-cards">
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stage-detail">
|
<div class="stage-detail" v-if="currentStage !== null">
|
||||||
<Transition name="fade" mode="out-in">
|
<Transition name="fade" mode="out-in">
|
||||||
<div :key="currentStage" class="detail-panel">
|
<div :key="currentStage" class="detail-panel">
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
@@ -46,6 +47,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>架构演进是为了解决上一个时代的痛点,但也带来了新的复杂度。没有最好的架构,只有最适合的架构。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -116,84 +122,95 @@ const stages = [
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.evolution-intro-demo {
|
.evolution-intro-demo {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 16px;
|
border-radius: 8px;
|
||||||
padding: 32px;
|
background: var(--vp-c-bg-soft);
|
||||||
color: #fff;
|
padding: 1rem;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
margin: 1rem 0;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro-header {
|
.demo-header {
|
||||||
text-align: center;
|
display: flex;
|
||||||
margin-bottom: 32px;
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro-header h3 {
|
.demo-header .icon {
|
||||||
font-size: 24px;
|
font-size: 1.25rem;
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro-header p {
|
.demo-header .title {
|
||||||
font-size: 14px;
|
font-weight: bold;
|
||||||
opacity: 0.9;
|
font-size: 1rem;
|
||||||
margin: 0;
|
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 {
|
.timeline-cards {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: 12px;
|
gap: 0.75rem;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-card {
|
.stage-card {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: var(--vp-c-bg);
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
padding: 16px 12px;
|
padding: 0.75rem 0.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-card:hover {
|
.stage-card:hover {
|
||||||
background: rgba(255, 255, 255, 0.15);
|
border-color: var(--vp-c-brand);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-card.active {
|
.stage-card.active {
|
||||||
background: rgba(255, 255, 255, 0.25);
|
border-color: var(--vp-c-brand);
|
||||||
border-color: rgba(255, 255, 255, 0.5);
|
background: var(--vp-c-brand-soft);
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-era {
|
.stage-era {
|
||||||
font-size: 11px;
|
font-size: 0.7rem;
|
||||||
opacity: 0.7;
|
color: var(--vp-c-text-3);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-icon {
|
.stage-icon {
|
||||||
font-size: 32px;
|
font-size: 1.5rem;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-name {
|
.stage-name {
|
||||||
font-size: 14px;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 2px;
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-arch {
|
.stage-arch {
|
||||||
font-size: 11px;
|
font-size: 0.7rem;
|
||||||
opacity: 0.7;
|
color: var(--vp-c-text-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-detail {
|
.stage-detail {
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: var(--vp-c-bg);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
padding: 24px;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-panel {
|
.detail-panel {
|
||||||
@@ -208,56 +225,76 @@ const stages = [
|
|||||||
.detail-header {
|
.detail-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 0.75rem;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 1rem;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 0.75rem;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-icon {
|
.detail-icon {
|
||||||
font-size: 32px;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-header h4 {
|
.detail-header h4 {
|
||||||
font-size: 20px;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-content {
|
.detail-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-section h5 {
|
.detail-section h5 {
|
||||||
font-size: 14px;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 0.5rem 0;
|
||||||
color: #ffd700;
|
color: var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-section p {
|
.detail-section p {
|
||||||
font-size: 13px;
|
font-size: 0.8rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin: 0;
|
margin: 0 0 0.5rem 0;
|
||||||
opacity: 0.9;
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-section ul {
|
.detail-section ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 18px;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-section li {
|
.detail-section li {
|
||||||
font-size: 13px;
|
font-size: 0.8rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 0.25rem;
|
||||||
opacity: 0.9;
|
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-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: all 0.4s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from {
|
.fade-enter-from {
|
||||||
@@ -275,8 +312,8 @@ const stages = [
|
|||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-detail {
|
.detail-content {
|
||||||
padding: 16px;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="monolith-demo">
|
<div class="monolith-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>🏢 单体架构演示</h4>
|
<span class="icon">🏢</span>
|
||||||
<p>观察单体应用如何处理请求,以及模块间的依赖关系</p>
|
<span class="title">单体架构演示</span>
|
||||||
|
<span class="subtitle">观察单体应用如何处理请求</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="monolith-diagram">
|
<div class="monolith-diagram">
|
||||||
@@ -47,13 +48,9 @@
|
|||||||
<button class="control-btn" @click="reset">重置</button>
|
<button class="control-btn" @click="reset">重置</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-explanation">
|
<div class="info-box">
|
||||||
<h5>💡 单体架构的特点</h5>
|
<span class="icon">💡</span>
|
||||||
<ul>
|
<strong>核心思想:</strong>所有模块在同一个进程中运行,内存共享,但一个模块崩溃可能导致整个进程挂掉(雪崩效应)。
|
||||||
<li><strong>共享进程空间</strong>:所有模块在同一个进程中运行,内存共享</li>
|
|
||||||
<li><strong>数据库耦合</strong>:所有模块共享同一个数据库,Schema变更影响全局</li>
|
|
||||||
<li><strong>级联故障</strong>:一个模块崩溃可能导致整个进程挂掉(雪崩效应)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -136,24 +133,35 @@ const reset = () => {
|
|||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.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 {
|
.demo-header .icon {
|
||||||
margin: 0 0 0.5rem 0;
|
font-size: 1.25rem;
|
||||||
font-size: 1.1rem;
|
}
|
||||||
|
|
||||||
|
.demo-header .title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header p {
|
.demo-header .subtitle {
|
||||||
margin: 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.monolith-diagram {
|
.monolith-diagram {
|
||||||
@@ -378,4 +386,23 @@ const reset = () => {
|
|||||||
flex-wrap: wrap;
|
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>
|
</style>
|
||||||
|
|||||||
+44
-31
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="physical-server-demo">
|
<div class="physical-server-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>🖥️ 物理服务器时代演示</h4>
|
<span class="icon">🖥️</span>
|
||||||
<p>点击"发送请求",观察早期 CGI 服务器的处理瓶颈</p>
|
<span class="title">物理服务器时代演示</span>
|
||||||
|
<span class="subtitle">观察早期 CGI 服务器的处理瓶颈</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-stage">
|
<div class="demo-stage">
|
||||||
@@ -85,27 +86,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-explanation">
|
<div class="info-box">
|
||||||
<h5>💡 早期的痛点在哪里?</h5>
|
<span class="icon">💡</span>
|
||||||
<ul>
|
<strong>核心思想:</strong>进程级隔离带来了稳定性,但也带来了巨大的性能开销。
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -192,24 +175,35 @@ const sendRequest = async () => {
|
|||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.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 {
|
.demo-header .icon {
|
||||||
margin: 0 0 0.5rem 0;
|
font-size: 1.25rem;
|
||||||
font-size: 1.1rem;
|
}
|
||||||
|
|
||||||
|
.demo-header .title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header p {
|
.demo-header .subtitle {
|
||||||
margin: 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-stage {
|
.demo-stage {
|
||||||
@@ -472,4 +466,23 @@ const sendRequest = async () => {
|
|||||||
height: 3px;
|
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>
|
</style>
|
||||||
|
|||||||
+24
-2
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tech-stack-timeline-demo">
|
<div class="tech-stack-timeline-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>📚 技术栈演进时间线</h4>
|
<span class="icon">📚</span>
|
||||||
<p>每个时代的主流技术栈</p>
|
<span class="title">技术栈演进时间线</span>
|
||||||
|
<span class="subtitle">每个时代的主流技术栈</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timeline">
|
<div class="timeline">
|
||||||
@@ -106,6 +107,8 @@ const eras = [
|
|||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.demo-header {
|
||||||
@@ -247,4 +250,23 @@ const eras = [
|
|||||||
grid-template-columns: 1fr;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -408,14 +408,15 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.animation-demo {
|
.animation-demo {
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 1.5rem;
|
||||||
background: #fafafa;
|
background: var(--vp-c-bg-soft);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-panel {
|
.control-panel {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playback-controls {
|
.playback-controls {
|
||||||
@@ -426,13 +427,16 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.play-btn,
|
.play-btn,
|
||||||
.reset-btn {
|
.reset-btn {
|
||||||
padding: 10px 20px;
|
padding: 0.625rem 1.25rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.25s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.play-btn {
|
.play-btn {
|
||||||
@@ -442,7 +446,8 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.play-btn:hover {
|
.play-btn:hover {
|
||||||
background: #27ae60;
|
background: #27ae60;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.reset-btn {
|
.reset-btn {
|
||||||
@@ -452,28 +457,35 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.reset-btn:hover {
|
.reset-btn:hover {
|
||||||
background: #7f8c8d;
|
background: #7f8c8d;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(149, 165, 166, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.animation-selector {
|
.animation-selector {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animation-selector label {
|
.animation-selector label {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
color: #2c3e50;
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animation-selector select {
|
.animation-selector select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px 12px;
|
padding: 0.5rem 0.75rem;
|
||||||
border: 2px solid #ddd;
|
border: 2px solid var(--vp-c-divider);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-selector select:hover {
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameters {
|
.parameters {
|
||||||
@@ -531,30 +543,35 @@ onUnmounted(() => {
|
|||||||
.canvas-container {
|
.canvas-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 20px 0;
|
margin: 1.5rem 0;
|
||||||
padding: 20px;
|
padding: 1.5rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
border: 2px solid var(--vp-c-divider);
|
||||||
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
border: 2px solid #ddd;
|
border: 3px solid var(--vp-c-divider);
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display {
|
.code-display {
|
||||||
margin-top: 20px;
|
margin-top: 1.5rem;
|
||||||
padding: 15px;
|
padding: 1.25rem;
|
||||||
background: #2c3e50;
|
background: #1e293b;
|
||||||
border-radius: 6px;
|
border-radius: 12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
border: 2px solid #334155;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display h4 {
|
.code-display h4 {
|
||||||
color: #ecf0f1;
|
color: #f8fafc;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 0.75rem 0;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display pre {
|
.code-display pre {
|
||||||
@@ -562,54 +579,60 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.code-display code {
|
.code-display code {
|
||||||
color: #ecf0f1;
|
color: #e2e8f0;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: var(--vp-font-family-mono);
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
line-height: 1.6;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation {
|
.explanation {
|
||||||
margin: 20px 0;
|
margin: 1.5rem 0;
|
||||||
padding: 15px;
|
padding: 1.25rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation h4 {
|
.explanation h4 {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 0.75rem 0;
|
||||||
color: #2c3e50;
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation ul {
|
.explanation ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 20px;
|
padding-left: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation li {
|
.explanation li {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
color: #555;
|
color: var(--vp-c-text-2);
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box {
|
.info-box {
|
||||||
margin-top: 15px;
|
margin-top: 1.5rem;
|
||||||
padding: 12px;
|
padding: 1rem 1.25rem;
|
||||||
background: #fff3cd;
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||||
border-left: 4px solid #ffc107;
|
border-radius: 12px;
|
||||||
border-radius: 4px;
|
border-left: 4px solid #f59e0b;
|
||||||
|
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box p {
|
.info-box p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
color: #856404;
|
color: #92400e;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box .icon {
|
.info-box .icon {
|
||||||
font-size: 16px;
|
font-size: 1.125rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,7 +13,14 @@
|
|||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="canvas-basics-demo">
|
<div class="canvas-basics-demo">
|
||||||
<div class="control-panel">
|
<div class="demo-header">
|
||||||
|
<span class="icon">🎨</span>
|
||||||
|
<span class="title">Canvas 基础</span>
|
||||||
|
<span class="subtitle">用代码画图(通俗说:编程画板)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-content">
|
||||||
|
<div class="controls">
|
||||||
<div class="shape-selector">
|
<div class="shape-selector">
|
||||||
<label>Shape / 形状</label>
|
<label>Shape / 形状</label>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
@@ -84,14 +91,11 @@
|
|||||||
<h4>Code / 代码</h4>
|
<h4>Code / 代码</h4>
|
||||||
<pre><code>{{ currentCode }}</code></pre>
|
<pre><code>{{ currentCode }}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<p>
|
|
||||||
<span class="icon">💡</span>
|
<span class="icon">💡</span>
|
||||||
<strong>提示:</strong>
|
<strong>核心思想:</strong>Canvas 是一个位图画布,所有绘制都是像素操作。绘制后无法修改已有内容,只能覆盖或清除重绘。
|
||||||
Canvas
|
|
||||||
是一个位图画布,所有绘制都是像素操作。绘制后无法修改已有内容,只能覆盖或清除重绘。
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -101,8 +105,8 @@ import { ref, computed, watch, onMounted } from 'vue'
|
|||||||
|
|
||||||
const canvasRef = ref(null)
|
const canvasRef = ref(null)
|
||||||
const currentShape = ref('rect')
|
const currentShape = ref('rect')
|
||||||
const fillColor = ref('#3498db')
|
const fillColor = ref('#3b82f6')
|
||||||
const strokeColor = ref('#2c3e50')
|
const strokeColor = ref('#1e293b')
|
||||||
const strokeWidth = ref(2)
|
const strokeWidth = ref(2)
|
||||||
const rectSize = ref(100)
|
const rectSize = ref(100)
|
||||||
const circleRadius = ref(50)
|
const circleRadius = ref(50)
|
||||||
@@ -231,145 +235,193 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.canvas-basics-demo {
|
.canvas-basics-demo {
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
background: var(--vp-c-bg-soft);
|
||||||
background: #fafafa;
|
padding: 1.5rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-panel {
|
.demo-header {
|
||||||
margin-bottom: 20px;
|
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 {
|
.shape-selector {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shape-selector label {
|
.shape-selector label {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.625rem;
|
||||||
color: #2c3e50;
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 0.625rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group button {
|
.button-group button {
|
||||||
padding: 8px 16px;
|
padding: 0.625rem 1.25rem;
|
||||||
border: 2px solid #ddd;
|
border: 2px solid var(--vp-c-divider);
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
transition: all 0.2s;
|
font-weight: 500;
|
||||||
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group button:hover {
|
.button-group button:hover {
|
||||||
border-color: #3498db;
|
border-color: var(--vp-c-brand);
|
||||||
background: #f0f8ff;
|
background: var(--vp-c-bg-soft);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group button.active {
|
.button-group button.active {
|
||||||
border-color: #3498db;
|
border-color: var(--vp-c-brand);
|
||||||
background: #3498db;
|
background: var(--vp-c-brand);
|
||||||
color: white;
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameters {
|
.parameters {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 12px;
|
gap: 0.75rem;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.param-row {
|
.param-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.param-row label {
|
.param-row label {
|
||||||
font-size: 13px;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #555;
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.param-row input[type='range'] {
|
.param-row input[type='range'] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
accent-color: var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.param-row input[type='color'] {
|
.param-row input[type='color'] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 36px;
|
height: 32px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draw-btn,
|
.draw-btn,
|
||||||
.clear-btn {
|
.clear-btn {
|
||||||
padding: 10px 20px;
|
padding: 0.5rem 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 14px;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 10px;
|
margin-right: 0.5rem;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draw-btn {
|
.draw-btn {
|
||||||
background: #3498db;
|
background: var(--vp-c-brand);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draw-btn:hover {
|
.draw-btn:hover {
|
||||||
background: #2980b9;
|
opacity: 0.9;
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-btn {
|
.clear-btn {
|
||||||
background: #e74c3c;
|
background: var(--vp-c-danger);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-btn:hover {
|
.clear-btn:hover {
|
||||||
background: #c0392b;
|
opacity: 0.9;
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-container {
|
.canvas-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 20px 0;
|
margin: 1.5rem 0;
|
||||||
padding: 20px;
|
padding: 1.5rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
border: 2px solid var(--vp-c-divider);
|
||||||
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
border: 2px solid #ddd;
|
border: 3px solid var(--vp-c-divider);
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
background: white;
|
background: #ffffff;
|
||||||
|
max-width: 100%;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display {
|
.code-display {
|
||||||
margin-top: 20px;
|
margin-top: 1.5rem;
|
||||||
padding: 15px;
|
padding: 1.25rem;
|
||||||
background: #2c3e50;
|
background: #1e293b;
|
||||||
border-radius: 6px;
|
border-radius: 12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
border: 2px solid #334155;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display h4 {
|
.code-display h4 {
|
||||||
color: #ecf0f1;
|
color: #f8fafc;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 0.75rem 0;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display pre {
|
.code-display pre {
|
||||||
@@ -377,31 +429,39 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.code-display code {
|
.code-display code {
|
||||||
color: #ecf0f1;
|
color: #e2e8f0;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: var(--vp-font-family-mono);
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
line-height: 1.6;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box {
|
.info-box {
|
||||||
margin-top: 15px;
|
margin-top: 1.5rem;
|
||||||
padding: 12px;
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||||
background: #fff3cd;
|
padding: 1rem 1.25rem;
|
||||||
border-left: 4px solid #ffc107;
|
border-radius: 12px;
|
||||||
border-radius: 4px;
|
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 {
|
.info-box p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
|
||||||
color: #856404;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
gap: 0.625rem;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box .icon {
|
.info-box .icon {
|
||||||
font-size: 16px;
|
font-size: 1.125rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-box strong {
|
||||||
|
color: #78350f;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -272,117 +272,140 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.coordinate-demo {
|
.coordinate-demo {
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 1.5rem;
|
||||||
background: #fafafa;
|
background: var(--vp-c-bg-soft);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-panel {
|
.control-panel {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-group {
|
.toggle-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 15px;
|
gap: 1rem;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-option {
|
.toggle-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
cursor: pointer;
|
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'] {
|
.toggle-option input[type='checkbox'] {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
accent-color: var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-display {
|
.info-display {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 10px;
|
gap: 0.75rem;
|
||||||
padding: 12px;
|
padding: 1rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item {
|
.info-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 13px;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item .label {
|
.info-item .label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #555;
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item .value {
|
.info-item .value {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: var(--vp-font-family-mono);
|
||||||
color: #2c3e50;
|
color: var(--vp-c-text-1);
|
||||||
background: #f0f0f0;
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 2px 8px;
|
padding: 0.25rem 0.75rem;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-container {
|
.canvas-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 20px 0;
|
margin: 1.5rem 0;
|
||||||
padding: 20px;
|
padding: 1.5rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
border: 2px solid var(--vp-c-divider);
|
||||||
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
border: 2px solid #ddd;
|
border: 3px solid var(--vp-c-divider);
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation {
|
.explanation {
|
||||||
margin: 20px 0;
|
margin: 1.5rem 0;
|
||||||
padding: 15px;
|
padding: 1.25rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation h4 {
|
.explanation h4 {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 0.75rem 0;
|
||||||
color: #2c3e50;
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation ul {
|
.explanation ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 20px;
|
padding-left: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation li {
|
.explanation li {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
color: #555;
|
color: var(--vp-c-text-2);
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display {
|
.code-display {
|
||||||
margin-top: 20px;
|
margin-top: 1.5rem;
|
||||||
padding: 15px;
|
padding: 1.25rem;
|
||||||
background: #2c3e50;
|
background: #1e293b;
|
||||||
border-radius: 6px;
|
border-radius: 12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
border: 2px solid #334155;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display h4 {
|
.code-display h4 {
|
||||||
color: #ecf0f1;
|
color: #f8fafc;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 0.75rem 0;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display pre {
|
.code-display pre {
|
||||||
@@ -390,31 +413,33 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.code-display code {
|
.code-display code {
|
||||||
color: #ecf0f1;
|
color: #e2e8f0;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: var(--vp-font-family-mono);
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
line-height: 1.6;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box {
|
.info-box {
|
||||||
margin-top: 15px;
|
margin-top: 1.5rem;
|
||||||
padding: 12px;
|
padding: 1rem 1.25rem;
|
||||||
background: #fff3cd;
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||||
border-left: 4px solid #ffc107;
|
border-radius: 12px;
|
||||||
border-radius: 4px;
|
border-left: 4px solid #f59e0b;
|
||||||
|
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box p {
|
.info-box p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
color: #856404;
|
color: #92400e;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box .icon {
|
.info-box .icon {
|
||||||
font-size: 16px;
|
font-size: 1.125rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -491,14 +491,15 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.event-demo {
|
.event-demo {
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 1.5rem;
|
||||||
background: #fafafa;
|
background: var(--vp-c-bg-soft);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-panel {
|
.control-panel {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-selector {
|
.mode-selector {
|
||||||
@@ -514,53 +515,60 @@ onMounted(() => {
|
|||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group button {
|
.button-group button {
|
||||||
padding: 8px 16px;
|
padding: 0.5rem 1rem;
|
||||||
border: 2px solid #ddd;
|
border: 2px solid var(--vp-c-divider);
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
transition: all 0.2s;
|
font-weight: 500;
|
||||||
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group button:hover {
|
.button-group button:hover {
|
||||||
border-color: #3498db;
|
border-color: var(--vp-c-brand);
|
||||||
background: #f0f8ff;
|
background: var(--vp-c-bg-soft);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group button.active {
|
.button-group button.active {
|
||||||
border-color: #3498db;
|
border-color: var(--vp-c-brand);
|
||||||
background: #3498db;
|
background: var(--vp-c-brand);
|
||||||
color: white;
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.instructions {
|
.instructions {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 1rem;
|
||||||
padding: 12px;
|
padding: 1rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.instructions h4 {
|
.instructions h4 {
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 0.5rem 0;
|
||||||
color: #2c3e50;
|
color: var(--vp-c-text-1);
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.instructions ul {
|
.instructions ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 20px;
|
padding-left: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.instructions li {
|
.instructions li {
|
||||||
margin-bottom: 6px;
|
margin-bottom: 0.375rem;
|
||||||
color: #555;
|
color: var(--vp-c-text-2);
|
||||||
font-size: 13px;
|
font-size: 0.813rem;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-log {
|
.event-log {
|
||||||
@@ -635,36 +643,41 @@ onMounted(() => {
|
|||||||
.canvas-container {
|
.canvas-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 20px 0;
|
margin: 1.5rem 0;
|
||||||
padding: 20px;
|
padding: 1.5rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
border: 2px solid var(--vp-c-divider);
|
||||||
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
border: 2px solid #ddd;
|
border: 3px solid var(--vp-c-divider);
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas:focus {
|
canvas:focus {
|
||||||
border-color: #3498db;
|
border-color: var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display {
|
.code-display {
|
||||||
margin-top: 20px;
|
margin-top: 1.5rem;
|
||||||
padding: 15px;
|
padding: 1.25rem;
|
||||||
background: #2c3e50;
|
background: #1e293b;
|
||||||
border-radius: 6px;
|
border-radius: 12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
border: 2px solid #334155;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display h4 {
|
.code-display h4 {
|
||||||
color: #ecf0f1;
|
color: #f8fafc;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 0.75rem 0;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display pre {
|
.code-display pre {
|
||||||
@@ -672,32 +685,36 @@ canvas:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.code-display code {
|
.code-display code {
|
||||||
color: #ecf0f1;
|
color: #e2e8f0;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: var(--vp-font-family-mono);
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
line-height: 1.6;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation {
|
.explanation {
|
||||||
margin: 20px 0;
|
margin: 1.5rem 0;
|
||||||
padding: 15px;
|
padding: 1.25rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation h4 {
|
.explanation h4 {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 0.75rem 0;
|
||||||
color: #2c3e50;
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation ul {
|
.explanation ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 20px;
|
padding-left: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation li {
|
.explanation li {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
color: #555;
|
color: var(--vp-c-text-2);
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -378,14 +378,15 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.particle-demo {
|
.particle-demo {
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 1.5rem;
|
||||||
background: #fafafa;
|
background: var(--vp-c-bg-soft);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-panel {
|
.control-panel {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.effect-selector {
|
.effect-selector {
|
||||||
@@ -401,29 +402,33 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group button {
|
.button-group button {
|
||||||
padding: 8px 16px;
|
padding: 0.5rem 1rem;
|
||||||
border: 2px solid #ddd;
|
border: 2px solid var(--vp-c-divider);
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
transition: all 0.2s;
|
font-weight: 500;
|
||||||
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group button:hover {
|
.button-group button:hover {
|
||||||
border-color: #3498db;
|
border-color: var(--vp-c-brand);
|
||||||
background: #f0f8ff;
|
background: var(--vp-c-bg-soft);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group button.active {
|
.button-group button.active {
|
||||||
border-color: #3498db;
|
border-color: var(--vp-c-brand);
|
||||||
background: #3498db;
|
background: var(--vp-c-brand);
|
||||||
color: white;
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameters {
|
.parameters {
|
||||||
@@ -499,31 +504,36 @@ onUnmounted(() => {
|
|||||||
.canvas-container {
|
.canvas-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 20px 0;
|
margin: 1.5rem 0;
|
||||||
padding: 20px;
|
padding: 1.5rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
border: 2px solid var(--vp-c-divider);
|
||||||
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
border: 2px solid #ddd;
|
border: 3px solid var(--vp-c-divider);
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display {
|
.code-display {
|
||||||
margin-top: 20px;
|
margin-top: 1.5rem;
|
||||||
padding: 15px;
|
padding: 1.25rem;
|
||||||
background: #2c3e50;
|
background: #1e293b;
|
||||||
border-radius: 6px;
|
border-radius: 12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
border: 2px solid #334155;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display h4 {
|
.code-display h4 {
|
||||||
color: #ecf0f1;
|
color: #f8fafc;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 0.75rem 0;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display pre {
|
.code-display pre {
|
||||||
@@ -531,54 +541,60 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.code-display code {
|
.code-display code {
|
||||||
color: #ecf0f1;
|
color: #e2e8f0;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: var(--vp-font-family-mono);
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
line-height: 1.6;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation {
|
.explanation {
|
||||||
margin: 20px 0;
|
margin: 1.5rem 0;
|
||||||
padding: 15px;
|
padding: 1.25rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation h4 {
|
.explanation h4 {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 0.75rem 0;
|
||||||
color: #2c3e50;
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation ul {
|
.explanation ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 20px;
|
padding-left: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation li {
|
.explanation li {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
color: #555;
|
color: var(--vp-c-text-2);
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box {
|
.info-box {
|
||||||
margin-top: 15px;
|
margin-top: 1.5rem;
|
||||||
padding: 12px;
|
padding: 1rem 1.25rem;
|
||||||
background: #fff3cd;
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||||
border-left: 4px solid #ffc107;
|
border-radius: 12px;
|
||||||
border-radius: 4px;
|
border-left: 4px solid #f59e0b;
|
||||||
|
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box p {
|
.info-box p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
color: #856404;
|
color: #92400e;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box .icon {
|
.info-box .icon {
|
||||||
font-size: 16px;
|
font-size: 1.125rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -553,14 +553,15 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.performance-demo {
|
.performance-demo {
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 1.5rem;
|
||||||
background: #fafafa;
|
background: var(--vp-c-bg-soft);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-panel {
|
.control-panel {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-selector {
|
.test-selector {
|
||||||
@@ -576,29 +577,33 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group button {
|
.button-group button {
|
||||||
padding: 8px 16px;
|
padding: 0.5rem 1rem;
|
||||||
border: 2px solid #ddd;
|
border: 2px solid var(--vp-c-divider);
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
transition: all 0.2s;
|
font-weight: 500;
|
||||||
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group button:hover {
|
.button-group button:hover {
|
||||||
border-color: #3498db;
|
border-color: var(--vp-c-brand);
|
||||||
background: #f0f8ff;
|
background: var(--vp-c-bg-soft);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group button.active {
|
.button-group button.active {
|
||||||
border-color: #3498db;
|
border-color: var(--vp-c-brand);
|
||||||
background: #3498db;
|
background: var(--vp-c-brand);
|
||||||
color: white;
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameters {
|
.parameters {
|
||||||
@@ -721,28 +726,34 @@ onUnmounted(() => {
|
|||||||
.canvas-container {
|
.canvas-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 20px 0;
|
margin: 1.5rem 0;
|
||||||
padding: 20px;
|
padding: 1.5rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
border: 2px solid var(--vp-c-divider);
|
||||||
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
border: 2px solid #ddd;
|
border: 3px solid var(--vp-c-divider);
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comparison {
|
.comparison {
|
||||||
margin: 20px 0;
|
margin: 1.5rem 0;
|
||||||
padding: 15px;
|
padding: 1.25rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comparison h4 {
|
.comparison h4 {
|
||||||
margin: 0 0 15px 0;
|
margin: 0 0 1rem 0;
|
||||||
color: #2c3e50;
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comparison-table {
|
.comparison-table {
|
||||||
@@ -756,29 +767,37 @@ canvas {
|
|||||||
|
|
||||||
.comparison-table th,
|
.comparison-table th,
|
||||||
.comparison-table td {
|
.comparison-table td {
|
||||||
padding: 10px;
|
padding: 0.625rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #e0e0e0;
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comparison-table th {
|
.comparison-table th {
|
||||||
background: #f8f9fa;
|
background: var(--vp-c-bg-soft);
|
||||||
font-weight: 600;
|
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 {
|
.code-display {
|
||||||
margin-top: 20px;
|
margin-top: 1.5rem;
|
||||||
padding: 15px;
|
padding: 1.25rem;
|
||||||
background: #2c3e50;
|
background: #1e293b;
|
||||||
border-radius: 6px;
|
border-radius: 12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
border: 2px solid #334155;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display h4 {
|
.code-display h4 {
|
||||||
color: #ecf0f1;
|
color: #f8fafc;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 0.75rem 0;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display pre {
|
.code-display pre {
|
||||||
@@ -786,32 +805,36 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.code-display code {
|
.code-display code {
|
||||||
color: #ecf0f1;
|
color: #e2e8f0;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: var(--vp-font-family-mono);
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
line-height: 1.6;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation {
|
.explanation {
|
||||||
margin: 20px 0;
|
margin: 1.5rem 0;
|
||||||
padding: 15px;
|
padding: 1.25rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation h4 {
|
.explanation h4 {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 0.75rem 0;
|
||||||
color: #2c3e50;
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation ul {
|
.explanation ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 20px;
|
padding-left: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation li {
|
.explanation li {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
color: #555;
|
color: var(--vp-c-text-2);
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="access-key-management-demo">
|
<div class="access-key-management-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>访问密钥(AK/SK)生命周期管理</h4>
|
<span class="icon">🔑</span>
|
||||||
<p class="intro-text">模拟 AK/SK 的创建、使用和轮换流程</p>
|
<span class="title">访问密钥管理</span>
|
||||||
|
<span class="subtitle">理解 AK/SK 生命周期和轮换流程</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-content">
|
<div class="demo-content">
|
||||||
@@ -88,7 +89,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<strong>💡 安全提示:</strong>访问密钥泄露是云安全事件的主要原因之一。建议优先使用 IAM 角色替代访问密钥,如果必须使用,请务必定期轮换。
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>访问密钥泄露是云安全事件的主要原因之一。建议优先使用 IAM 角色替代访问密钥,如果必须使用,请务必定期轮换。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="best-practices-demo">
|
<div class="best-practices-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>云账号权限管理最佳实践清单</h4>
|
<span class="icon">✅</span>
|
||||||
<p class="intro-text">点击查看详细的实施指南和代码示例</p>
|
<span class="title">权限管理最佳实践</span>
|
||||||
|
<span class="subtitle">理解云账号安全管理的核心原则</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-content">
|
<div class="demo-content">
|
||||||
@@ -52,7 +53,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<strong>💡 实施建议:</strong>按照优先级从 P0 开始逐步实施最佳实践。每个改进都能显著提升账号安全性,不要试图一次性完成所有改进。
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>按照优先级从 P0 开始逐步实施最佳实践。每个改进都能显著提升账号安全性,不要试图一次性完成所有改进。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="cross-account-access-demo">
|
<div class="cross-account-access-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>跨账号访问流程演示</h4>
|
<span class="icon">🔗</span>
|
||||||
<p class="intro-text">角色扮演(AssumeRole)获取临时凭证</p>
|
<span class="title">跨账号访问</span>
|
||||||
|
<span class="subtitle">理解跨账号访问的 AssumeRole 机制</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-content">
|
<div class="demo-content">
|
||||||
@@ -65,7 +66,8 @@ s3_client = boto3.client(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<strong>💡 跨账号访问优势:</strong>通过角色扮演实现跨账号访问,无需在每个账号创建 IAM 用户,临时凭证自动过期,更安全更易管理。
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>通过角色扮演实现跨账号访问,无需在每个账号创建 IAM 用户,临时凭证自动过期,更安全更易管理。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="iam-structure">
|
<div class="iam-structure">
|
||||||
|
<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 class="structure-layers">
|
||||||
<div
|
<div
|
||||||
v-for="(layer, index) in layers"
|
v-for="(layer, index) in layers"
|
||||||
@@ -31,6 +38,12 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>IAM 就像公司的门禁系统——根账号是老板(拥有所有钥匙),用户是员工(有特定权限),角色是临时访客证(有时效),策略是"谁能进哪些门"的规则。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -113,9 +126,39 @@ function selectLayer(index) {
|
|||||||
.iam-structure {
|
.iam-structure {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
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 {
|
.structure-layers {
|
||||||
@@ -221,4 +264,23 @@ function selectLayer(index) {
|
|||||||
.detail-examples li:last-child {
|
.detail-examples li:last-child {
|
||||||
margin-bottom: 0;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="iam-ram-comparison-demo">
|
<div class="iam-ram-comparison-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>AWS IAM vs 阿里云 RAM 对比</h4>
|
<span class="icon">🔐</span>
|
||||||
<p class="intro-text">点击各个模块查看详细对比</p>
|
<span class="title">IAM vs RAM 对比</span>
|
||||||
|
<span class="subtitle">理解不同云厂商的权限管理服务</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-content">
|
<div class="demo-content">
|
||||||
@@ -80,7 +81,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<strong>💡 提示:</strong>IAM 和 RAM 的核心概念基本一致,只是术语和实现细节略有不同。掌握一个平台后,可以快速迁移到另一个平台。
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>IAM 和 RAM 的核心概念基本一致,只是术语和实现细节略有不同。掌握一个平台后,可以快速迁移到另一个平台。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -163,31 +165,24 @@ function selectFeature(platform, index) {
|
|||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.demo-header {
|
||||||
margin-bottom: 1rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header h4 {
|
.demo-header .icon { font-size: 1.25rem; }
|
||||||
margin: 0 0 0.5rem 0;
|
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||||
font-weight: 800;
|
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||||
color: var(--vp-c-text-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-text {
|
.demo-content { margin-bottom: 0.75rem; }
|
||||||
margin: 0;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-content {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comparison-container {
|
.comparison-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -214,7 +209,7 @@ function selectFeature(platform, index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.platform-header.ram {
|
.platform-header.ram {
|
||||||
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
|
background: var(--vp-c-bg-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-header .logo {
|
.platform-header .logo {
|
||||||
@@ -234,9 +229,7 @@ function selectFeature(platform, index) {
|
|||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.features-list {
|
.features-list { padding: 0.75rem; }
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-item {
|
.feature-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -258,9 +251,7 @@ function selectFeature(platform, index) {
|
|||||||
transform: translateX(4px);
|
transform: translateX(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-icon {
|
.feature-icon { font-size: 1.2rem; }
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-content {
|
.feature-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -289,13 +280,11 @@ function selectFeature(platform, index) {
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-card {
|
.detail-card { text-align: center; }
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-card h6 {
|
.detail-card h6 {
|
||||||
margin: 0 0 1rem 0;
|
margin: 0 0 1rem 0;
|
||||||
font-size: 1.1rem;
|
font-size: 1rem;
|
||||||
color: var(--vp-c-brand-1);
|
color: var(--vp-c-brand-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,18 +313,13 @@ function selectFeature(platform, index) {
|
|||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aws-detail .label {
|
.aws-detail .label { color: var(--vp-c-brand-1); }
|
||||||
color: var(--vp-c-brand-1);
|
.ram-detail .label { color: var(--vp-c-brand-delta); }
|
||||||
}
|
|
||||||
|
|
||||||
.ram-detail .label {
|
|
||||||
color: var(--vp-c-brand-delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aws-detail p,
|
.aws-detail p,
|
||||||
.ram-detail p {
|
.ram-detail p {
|
||||||
margin: 0 0 0.5rem 0;
|
margin: 0 0 0.5rem 0;
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
@@ -347,7 +331,7 @@ function selectFeature(platform, index) {
|
|||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.65rem;
|
font-size: 0.6rem;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
@@ -363,19 +347,17 @@ function selectFeature(platform, index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.info-box {
|
.info-box {
|
||||||
padding: 0.75rem;
|
|
||||||
background: var(--vp-c-bg-alt);
|
background: var(--vp-c-bg-alt);
|
||||||
border: 1px solid var(--vp-c-divider);
|
padding: 0.75rem;
|
||||||
border-left: 4px solid var(--vp-c-brand);
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box strong {
|
.info-box .icon { flex-shrink: 0; }
|
||||||
color: var(--vp-c-text-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.comparison-container {
|
.comparison-container {
|
||||||
@@ -383,16 +365,8 @@ function selectFeature(platform, index) {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comparison-details {
|
.comparison-details { order: -1; }
|
||||||
order: -1;
|
.comparison-row { flex-direction: column; }
|
||||||
}
|
.vs-divider { padding: 0.5rem 0; }
|
||||||
|
|
||||||
.comparison-row {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vs-divider {
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="identity-provider-demo">
|
<div class="identity-provider-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>身份提供商(IdP)集成流程</h4>
|
<span class="icon">🔐</span>
|
||||||
<p class="intro-text">点击步骤查看 SSO 单点登录流程</p>
|
<span class="title">身份提供商集成</span>
|
||||||
|
<span class="subtitle">理解企业 SSO 单点登录流程</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-content">
|
<div class="demo-content">
|
||||||
@@ -38,7 +39,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<strong>💡 SSO 优势:</strong>通过企业 IdP 统一管理用户身份,避免在每个云平台单独创建账号,提高安全性和管理效率。
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>通过企业 IdP 统一管理用户身份,避免在每个云平台单独创建账号,提高安全性和管理效率。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mfa-security-demo">
|
<div class="mfa-security-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>MFA 多因素认证模拟</h4>
|
<span class="icon">🔐</span>
|
||||||
<p class="intro-text">体验 MFA 双因素认证流程</p>
|
<span class="title">多因素认证</span>
|
||||||
|
<span class="subtitle">理解 MFA 双因素认证流程</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-content">
|
<div class="demo-content">
|
||||||
@@ -50,7 +51,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<strong>💡 MFA 安全价值:</strong>启用 MFA 可降低 99.9% 的账号被盗风险。即使密码泄露,攻击者没有你的 MFA 设备也无法登录。
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>启用 MFA 可降低 99.9% 的账号被盗风险。即使密码泄露,攻击者没有你的 MFA 设备也无法登录。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="permission-hierarchy-demo">
|
<div class="permission-hierarchy-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>权限层级结构</h4>
|
<span class="icon">🏛️</span>
|
||||||
<p class="intro-text">点击层级查看详细权限范围</p>
|
<span class="title">权限层级结构</span>
|
||||||
|
<span class="subtitle">理解不同权限级别的范围差异</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-content">
|
<div class="demo-content">
|
||||||
@@ -61,7 +62,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<strong>💡 最小权限原则:</strong>始终授予用户完成工作所需的最小权限。从低权限开始,根据实际需求逐步提升,而不是一开始就授予高权限。
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>最小权限原则——始终授予用户完成工作所需的最小权限。从低权限开始,根据实际需求逐步提升,而不是一开始就授予高权限。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="policy-editor-demo">
|
<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-layout">
|
||||||
<div class="editor-panel">
|
<div class="editor-panel">
|
||||||
<div class="panel-title">策略编辑器</div>
|
<div class="panel-title">策略编辑器</div>
|
||||||
@@ -42,6 +48,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>策略由 Effect、Action、Resource、Condition 四个核心元素组成,理解这四个元素的作用是编写 IAM 策略的基础。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="role-policy-demo">
|
<div class="role-policy-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>角色与策略关系可视化</h4>
|
<span class="icon">🎭</span>
|
||||||
<p class="intro-text">拖动查看角色如何关联多个策略</p>
|
<span class="title">角色与策略</span>
|
||||||
|
<span class="subtitle">理解角色如何关联多个策略</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-content">
|
<div class="demo-content">
|
||||||
@@ -80,7 +81,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<strong>💡 策略叠加:</strong>一个角色可以附加多个策略,最终的权限是所有策略的叠加结果。Deny 策略优先级高于 Allow。
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>策略叠加——一个角色可以附加多个策略,最终的权限是所有策略的叠加结果。Deny 策略优先级高于 Allow。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+48
-15
@@ -1,14 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="demo-container">
|
<div class="access-analytics-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>{{ title }}</h4>
|
<span class="icon">📊</span>
|
||||||
<p class="hint">{{ description }}</p>
|
<span class="title">访问分析</span>
|
||||||
|
<span class="subtitle">理解 CDN 访问统计和日志分析</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="demo-content">
|
<div class="demo-content">
|
||||||
<el-alert type="info" :closable="false">
|
<el-alert type="info" :closable="false">
|
||||||
访问分析演示组件占位符 - 待实现具体交互
|
访问分析演示组件占位符 - 待实现具体交互
|
||||||
</el-alert>
|
</el-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>通过日志分析,可以了解谁在何时访问了什么资源,帮助发现异常访问模式和安全事件。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -20,31 +26,58 @@ const description = ref('展示CDN和对象存储的访问统计分析,包括
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.demo-container {
|
.access-analytics-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.demo-header {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header h4 {
|
.demo-header .icon {
|
||||||
margin: 0 0 8px 0;
|
font-size: 1.25rem;
|
||||||
color: var(--vp-c-text-1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.demo-header .title {
|
||||||
margin: 0;
|
font-weight: bold;
|
||||||
font-size: 14px;
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .subtitle {
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-content {
|
.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;
|
display: flex;
|
||||||
flex-direction: column;
|
gap: 0.25rem;
|
||||||
gap: 16px;
|
}
|
||||||
|
|
||||||
|
.info-box .icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box strong {
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="demo-container">
|
<div class="demo-container">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>{{ title }}</h4>
|
<span class="icon">⚙️</span>
|
||||||
<p class="hint">{{ description }}</p>
|
<span class="title">{{ title }}</span>
|
||||||
|
<span class="subtitle">{{ description }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="demo-content">
|
<div class="demo-content">
|
||||||
<el-alert type="info" :closable="false">
|
<el-alert type="info" :closable="false">
|
||||||
缓存策略演示组件占位符 - 待实现具体交互
|
缓存策略演示组件占位符 - 待实现具体交互
|
||||||
</el-alert>
|
</el-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>缓存策略平衡命中率和新鲜度,TTL 设置太短会导致频繁回源,太长会导致内容过期。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
+80
-44
@@ -4,9 +4,10 @@
|
|||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="cdn-acceleration-demo">
|
<div class="cdn-acceleration-demo">
|
||||||
<div class="header">
|
<div class="demo-header">
|
||||||
<div class="title">CDN 加速原理</div>
|
<span class="icon">🌐</span>
|
||||||
<div class="subtitle">边缘节点、源站与回源的协同工作</div>
|
<span class="title">CDN 加速原理</span>
|
||||||
|
<span class="subtitle">边缘节点、源站与回源的协同工作</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cdn-architecture">
|
<div class="cdn-architecture">
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="stat-label">命中</span>
|
<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 }}%
|
{{ node.hitRate }}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,25 +136,30 @@
|
|||||||
<div class="stats-title">📊 访问统计</div>
|
<div class="stats-title">📊 访问统计</div>
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card">
|
<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 class="stat-label">缓存命中</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<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 class="stat-label">缓存未命中</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<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 }}%
|
{{ stats.hitRate }}%
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-label">命中率</div>
|
<div class="stat-label">命中率</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<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 class="stat-label">平均响应</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>CDN就像在全球开了分店——用户访问最近的分店拿资源,不用都跑总店来,速度自然快。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -285,21 +291,32 @@ const resetDemo = () => {
|
|||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
margin: 1.5rem 0;
|
margin: 1.5rem 0;
|
||||||
font-family: var(--vp-font-family-base);
|
font-family: var(--vp-font-family-base);
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.demo-header .icon {
|
||||||
font-weight: 700;
|
font-size: 1.25rem;
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.demo-header .title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .subtitle {
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cdn-architecture {
|
.cdn-architecture {
|
||||||
@@ -338,27 +355,27 @@ const resetDemo = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.layer-status.hit {
|
.layer-status.hit {
|
||||||
background: #dcfce7;
|
background: var(--vp-c-brand-soft);
|
||||||
color: #166534;
|
color: var(--vp-c-brand-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-status.miss {
|
.layer-status.miss {
|
||||||
background: #fee2e2;
|
background: rgba(var(--vp-c-brand-delta-rgb), 0.15);
|
||||||
color: #991b1b;
|
color: var(--vp-c-brand-delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-status.active {
|
.layer-status.active {
|
||||||
background: #dbeafe;
|
background: var(--vp-c-brand-soft);
|
||||||
color: #1e40af;
|
color: var(--vp-c-brand-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 用户层 */
|
/* 用户层 */
|
||||||
.users-map {
|
.users-map {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 120px;
|
height: 120px;
|
||||||
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
|
background: var(--vp-c-bg-soft);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #bae6fd;
|
border: 1px solid var(--vp-c-divider);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,18 +413,18 @@ const resetDemo = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 50%;
|
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 {
|
.user-label {
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #0369a1;
|
color: var(--vp-c-brand-1);
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: var(--vp-c-bg);
|
||||||
padding: 0.1rem 0.4rem;
|
padding: 0.1rem 0.4rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
@@ -511,8 +528,8 @@ const resetDemo = () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
background: var(--vp-c-bg-soft);
|
||||||
border: 2px solid #f59e0b;
|
border: 2px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,12 +545,12 @@ const resetDemo = () => {
|
|||||||
.server-name {
|
.server-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: #92400e;
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-address {
|
.server-address {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #b45309;
|
color: var(--vp-c-text-2);
|
||||||
font-family: var(--vp-font-family-mono);
|
font-family: var(--vp-font-family-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,14 +560,14 @@ const resetDemo = () => {
|
|||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #15803d;
|
color: var(--vp-c-brand-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #22c55e;
|
background: var(--vp-c-brand-1);
|
||||||
animation: statusPulse 2s infinite;
|
animation: statusPulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,8 +577,8 @@ const resetDemo = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.back-to-source-flow {
|
.back-to-source-flow {
|
||||||
background: #fef2f2;
|
background: var(--vp-c-bg-soft);
|
||||||
border: 1px solid #fecaca;
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
@@ -571,7 +588,7 @@ const resetDemo = () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #dc2626;
|
color: var(--vp-c-brand-delta);
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,11 +606,11 @@ const resetDemo = () => {
|
|||||||
|
|
||||||
.flow-step {
|
.flow-step {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #991b1b;
|
color: var(--vp-c-text-1);
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
padding: 0.4rem 0.6rem;
|
padding: 0.4rem 0.6rem;
|
||||||
border-radius: 4px;
|
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 {
|
.control-btn.reset {
|
||||||
background: #fef2f2;
|
background: rgba(var(--vp-c-brand-delta-rgb), 0.1);
|
||||||
border-color: #fecaca;
|
border-color: var(--vp-c-brand-delta);
|
||||||
color: #dc2626;
|
color: var(--vp-c-brand-delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-btn.reset:hover {
|
.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;
|
font-size: 0.7rem;
|
||||||
color: var(--vp-c-text-2);
|
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>
|
</style>
|
||||||
|
|||||||
+48
-15
@@ -1,14 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="demo-container">
|
<div class="https-optimization-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>{{ title }}</h4>
|
<span class="icon">🔒</span>
|
||||||
<p class="hint">{{ description }}</p>
|
<span class="title">HTTPS 优化</span>
|
||||||
|
<span class="subtitle">理解 CDN 的 HTTPS 协议和证书管理</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="demo-content">
|
<div class="demo-content">
|
||||||
<el-alert type="info" :closable="false">
|
<el-alert type="info" :closable="false">
|
||||||
HTTPS 优化演示组件占位符 - 待实现具体交互
|
HTTPS 优化演示组件占位符 - 待实现具体交互
|
||||||
</el-alert>
|
</el-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>HTTPS 通过 TLS/SSL 加密数据传输,防止中间人攻击和数据泄露,是现代 Web 应用的安全基础。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -20,31 +26,58 @@ const description = ref('展示CDN的HTTPS优化技术,包括TLS握手优化
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.demo-container {
|
.https-optimization-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.demo-header {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header h4 {
|
.demo-header .icon {
|
||||||
margin: 0 0 8px 0;
|
font-size: 1.25rem;
|
||||||
color: var(--vp-c-text-1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.demo-header .title {
|
||||||
margin: 0;
|
font-weight: bold;
|
||||||
font-size: 14px;
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .subtitle {
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-content {
|
.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;
|
display: flex;
|
||||||
flex-direction: column;
|
gap: 0.25rem;
|
||||||
gap: 16px;
|
}
|
||||||
|
|
||||||
|
.info-box .icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box strong {
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+80
-167
@@ -1,12 +1,9 @@
|
|||||||
<!--
|
|
||||||
ObjectStorageDemo.vue
|
|
||||||
对象存储架构演示 - 展示桶、对象、元数据的核心概念
|
|
||||||
-->
|
|
||||||
<template>
|
<template>
|
||||||
<div class="object-storage-demo">
|
<div class="object-storage-demo">
|
||||||
<div class="header">
|
<div class="demo-header">
|
||||||
<div class="title">对象存储架构</div>
|
<span class="icon">🗄️</span>
|
||||||
<div class="subtitle">理解 Bucket、Object 和 Metadata 的关系</div>
|
<span class="title">对象存储架构</span>
|
||||||
|
<span class="subtitle">理解 Bucket、Object 和 Metadata 的关系</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="storage-architecture">
|
<div class="storage-architecture">
|
||||||
@@ -113,38 +110,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="architecture-summary">
|
<div class="info-box">
|
||||||
<div class="summary-title">架构要点总结</div>
|
<span class="icon">💡</span>
|
||||||
<div class="summary-grid">
|
<strong>核心思想:</strong>对象存储采用三层架构:Account(账户)→ Bucket(桶)→ Object(对象),每个对象都附带丰富的元数据用于检索和管理。理解这个层次结构是掌握对象存储的第一步。
|
||||||
<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 Policy、ACL、STS 临时凭证多层权限</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -262,69 +230,67 @@ const getFileIcon = (type) => {
|
|||||||
.object-storage-demo {
|
.object-storage-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
margin: 1.5rem 0;
|
margin: 1rem 0;
|
||||||
font-family: var(--vp-font-family-base);
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.demo-header {
|
||||||
margin-bottom: 1.5rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.demo-header .icon { font-size: 1.25rem; }
|
||||||
font-weight: 700;
|
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||||||
font-size: 1.2rem;
|
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.storage-architecture {
|
.storage-architecture {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-layer {
|
.account-layer {
|
||||||
background: linear-gradient(135deg, #e0e7ff, #c7d2fe);
|
background: var(--vp-c-brand-soft);
|
||||||
padding: 1rem;
|
padding: 0.75rem;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 2px solid #6366f1;
|
border: 2px solid var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-icon {
|
.account-icon {
|
||||||
font-size: 2rem;
|
font-size: 1.5rem;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-name {
|
.account-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.95rem;
|
font-size: 0.9rem;
|
||||||
color: #4338ca;
|
color: var(--vp-c-brand-1);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-desc {
|
.account-desc {
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
color: #6366f1;
|
color: var(--vp-c-text-2);
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connector {
|
.connector {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--vp-c-text-3);
|
color: var(--vp-c-text-3);
|
||||||
font-size: 1.25rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buckets-container {
|
.buckets-container {
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border: 2px solid var(--vp-c-divider);
|
border: 2px solid var(--vp-c-divider);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
@@ -332,13 +298,13 @@ const getFileIcon = (type) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.5rem;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-desc {
|
.section-desc {
|
||||||
font-size: 0.75rem;
|
font-size: 0.65rem;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@@ -346,17 +312,17 @@ const getFileIcon = (type) => {
|
|||||||
|
|
||||||
.buckets-row {
|
.buckets-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bucket-card {
|
.bucket-card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 140px;
|
min-width: 120px;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
border: 2px solid var(--vp-c-divider);
|
border: 2px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
padding: 0.75rem;
|
padding: 0.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
@@ -370,29 +336,26 @@ const getFileIcon = (type) => {
|
|||||||
.bucket-card.active {
|
.bucket-card.active {
|
||||||
border-color: var(--vp-c-brand);
|
border-color: var(--vp-c-brand);
|
||||||
background: var(--vp-c-brand-soft);
|
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 {
|
.bucket-icon { font-size: 1.5rem; margin-bottom: 0.25rem; }
|
||||||
font-size: 1.75rem;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bucket-name {
|
.bucket-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bucket-meta {
|
.bucket-meta {
|
||||||
font-size: 0.7rem;
|
font-size: 0.65rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bucket-size {
|
.bucket-size {
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
color: var(--vp-c-brand);
|
color: var(--vp-c-brand);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
@@ -401,30 +364,30 @@ const getFileIcon = (type) => {
|
|||||||
.objects-container {
|
.objects-container {
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border: 2px solid var(--vp-c-divider);
|
border: 2px solid var(--vp-c-divider);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 0.75rem;
|
||||||
min-height: 150px;
|
min-height: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.objects-list {
|
.objects-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.object-item {
|
.object-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.4rem 0.5rem;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.object-item:hover {
|
.object-item:hover {
|
||||||
background: var(--vp-c-bg-mute);
|
background: var(--vp-c-bg-alt);
|
||||||
}
|
}
|
||||||
|
|
||||||
.object-item.selected {
|
.object-item.selected {
|
||||||
@@ -432,9 +395,7 @@ const getFileIcon = (type) => {
|
|||||||
border: 1px solid var(--vp-c-brand);
|
border: 1px solid var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.object-icon {
|
.object-icon { font-size: 1rem; }
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.object-info {
|
.object-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -442,7 +403,7 @@ const getFileIcon = (type) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.object-key {
|
.object-key {
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -451,33 +412,35 @@ const getFileIcon = (type) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.object-meta {
|
.object-meta {
|
||||||
font-size: 0.7rem;
|
font-size: 0.65rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.object-arrow {
|
.object-arrow {
|
||||||
color: var(--vp-c-text-3);
|
color: var(--vp-c-text-3);
|
||||||
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.objects-placeholder {
|
.objects-placeholder,
|
||||||
|
.metadata-placeholder {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 1.5rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
font-size: 0.9rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-container {
|
.metadata-container {
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border: 2px solid var(--vp-c-divider);
|
border: 2px solid var(--vp-c-divider);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 0.75rem;
|
||||||
min-height: 150px;
|
min-height: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-content {
|
.metadata-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -488,30 +451,30 @@ const getFileIcon = (type) => {
|
|||||||
|
|
||||||
.metadata-section {
|
.metadata-section {
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
padding: 0.75rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-section-title {
|
.metadata-section-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.85rem;
|
font-size: 0.75rem;
|
||||||
color: var(--vp-c-brand);
|
color: var(--vp-c-brand);
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.4rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.4rem;
|
||||||
border-bottom: 1px solid var(--vp-c-divider);
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-list {
|
.metadata-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.4rem;
|
gap: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-item {
|
.metadata-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.1rem;
|
gap: 0.1rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-key {
|
.metadata-key {
|
||||||
@@ -525,66 +488,16 @@ const getFileIcon = (type) => {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-placeholder {
|
.info-box {
|
||||||
text-align: center;
|
background: var(--vp-c-bg-alt);
|
||||||
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;
|
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
background: var(--vp-c-bg-soft);
|
border-radius: 6px;
|
||||||
border-radius: 8px;
|
font-size: 0.85rem;
|
||||||
}
|
color: var(--vp-c-text-2);
|
||||||
|
margin-top: 0.75rem;
|
||||||
.summary-icon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-text {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-text strong {
|
.info-box .icon { flex-shrink: 0; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+48
-15
@@ -1,14 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="demo-container">
|
<div class="traffic-scheduling-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>{{ title }}</h4>
|
<span class="icon">🚦</span>
|
||||||
<p class="hint">{{ description }}</p>
|
<span class="title">流量调度</span>
|
||||||
|
<span class="subtitle">理解 CDN 智能调度和负载均衡</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="demo-content">
|
<div class="demo-content">
|
||||||
<el-alert type="info" :closable="false">
|
<el-alert type="info" :closable="false">
|
||||||
流量调度演示组件占位符 - 待实现具体交互
|
流量调度演示组件占位符 - 待实现具体交互
|
||||||
</el-alert>
|
</el-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>智能调度通过就近访问、负载均衡和故障切换,实现全球加速和高可用性。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -20,31 +26,58 @@ const description = ref('展示CDN的智能流量调度机制,包括负载均
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.demo-container {
|
.traffic-scheduling-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.demo-header {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header h4 {
|
.demo-header .icon {
|
||||||
margin: 0 0 8px 0;
|
font-size: 1.25rem;
|
||||||
color: var(--vp-c-text-1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.demo-header .title {
|
||||||
margin: 0;
|
font-weight: bold;
|
||||||
font-size: 14px;
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .subtitle {
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-content {
|
.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;
|
display: flex;
|
||||||
flex-direction: column;
|
gap: 0.25rem;
|
||||||
gap: 16px;
|
}
|
||||||
|
|
||||||
|
.info-box .icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box strong {
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="upload-process-demo">
|
<div class="upload-process-demo">
|
||||||
<div class="header">
|
<div class="demo-header">
|
||||||
<div class="title">文件上传流程</div>
|
<span class="icon">📤</span>
|
||||||
<div class="subtitle">直传 vs 分片上传 vs 断点续传</div>
|
<span class="title">文件上传流程</span>
|
||||||
|
<span class="subtitle">理解直传、分片、断点续传三种方式</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 上传方式选择 -->
|
<!-- 上传方式选择 -->
|
||||||
@@ -179,6 +180,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>大文件分片上传提高可靠性,网络中断可以从断点续传,避免重复上传整个文件。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
+92
-220
@@ -4,12 +4,13 @@
|
|||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="evolution-timeline">
|
<div class="evolution-timeline">
|
||||||
<div class="timeline-header">
|
<div class="demo-header">
|
||||||
<span class="header-icon">🚀</span>
|
<span class="icon">🚀</span>
|
||||||
<span class="header-title">前端开发演进时间线</span>
|
<span class="title">前端演进时间线</span>
|
||||||
<span class="header-subtitle">从"贴海报"到"搭乐高"的 20 年变迁</span>
|
<span class="subtitle">从"贴海报"到"搭乐高"的20年变迁</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-content">
|
||||||
<!-- 时间线 -->
|
<!-- 时间线 -->
|
||||||
<div class="timeline-container">
|
<div class="timeline-container">
|
||||||
<div
|
<div
|
||||||
@@ -38,41 +39,13 @@
|
|||||||
<div class="section-title">🔑 关键技术</div>
|
<div class="section-title">🔑 关键技术</div>
|
||||||
<div class="tech-tags">
|
<div class="tech-tags">
|
||||||
<span
|
<span
|
||||||
v-for="tech in era.technologies"
|
v-for="tech in era.technologies.slice(0, 5)"
|
||||||
:key="tech"
|
:key="tech"
|
||||||
class="tech-tag"
|
class="tech-tag"
|
||||||
>{{ tech }}</span>
|
>{{ tech }}</span>
|
||||||
</div>
|
</div>
|
||||||
</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="detail-section" v-if="era.metaphor">
|
||||||
<div class="section-title">💡 生活比喻</div>
|
<div class="section-title">💡 生活比喻</div>
|
||||||
<div class="metaphor-box">{{ era.metaphor }}</div>
|
<div class="metaphor-box">{{ era.metaphor }}</div>
|
||||||
@@ -82,22 +55,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 提示 -->
|
|
||||||
<div class="timeline-hint">
|
|
||||||
<span>👆</span>
|
|
||||||
<span>点击任意时代,查看详细信息</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 核心要点 -->
|
<div class="info-box">
|
||||||
<div class="key-takeaway">
|
<span class="icon">💡</span>
|
||||||
<span class="takeaway-icon">🎯</span>
|
<strong>核心思想:</strong>前端技术的演进,本质是为了解决两个问题:提升开发效率(从手动到自动化)和支撑更复杂的应用(从简单页面到桌面级应用)。
|
||||||
<div class="takeaway-content">
|
|
||||||
<strong>核心思想:</strong>
|
|
||||||
前端技术的演进,本质是为了解决两个问题:
|
|
||||||
<strong>提升开发效率</strong>(从手动到自动化)和
|
|
||||||
<strong>支撑更复杂的应用</strong>(从简单页面到桌面级应用)。
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -168,37 +130,39 @@ const eras = [
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.evolution-timeline {
|
.evolution-timeline {
|
||||||
border: 2px solid #e0e0e0;
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 16px;
|
border-radius: 8px;
|
||||||
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 24px;
|
padding: 1rem;
|
||||||
margin: 20px 0;
|
margin: 1rem 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-header {
|
.demo-header {
|
||||||
text-align: center;
|
display: flex;
|
||||||
margin-bottom: 32px;
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-icon {
|
.demo-header .icon {
|
||||||
font-size: 48px;
|
font-size: 1.25rem;
|
||||||
display: block;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.demo-header .title {
|
||||||
display: block;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
font-size: 1rem;
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-subtitle {
|
.demo-header .subtitle {
|
||||||
display: block;
|
color: var(--vp-c-text-2);
|
||||||
font-size: 14px;
|
font-size: 0.85rem;
|
||||||
color: #666;
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-content {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 时间线容器 */
|
/* 时间线容器 */
|
||||||
@@ -208,8 +172,8 @@ const eras = [
|
|||||||
|
|
||||||
.era-item {
|
.era-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 1rem;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
@@ -231,89 +195,88 @@ const eras = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
.era-dot {
|
.era-dot {
|
||||||
width: 56px;
|
width: 48px;
|
||||||
height: 56px;
|
height: 48px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
background: var(--vp-c-brand);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 28px;
|
font-size: 24px;
|
||||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.era-item:hover .era-dot {
|
.era-item:hover .era-dot {
|
||||||
transform: scale(1.1);
|
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 {
|
.era-line {
|
||||||
width: 4px;
|
width: 4px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: linear-gradient(180deg, #667eea, #e0e0e0);
|
background: var(--vp-c-divider);
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
min-height: 40px;
|
min-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 内容区域 */
|
/* 内容区域 */
|
||||||
.era-content {
|
.era-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 0.75rem;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid var(--vp-c-divider);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.era-item:hover .era-content {
|
.era-item:hover .era-content {
|
||||||
border-color: #667eea;
|
border-color: var(--vp-c-brand);
|
||||||
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.era-item.active .era-content {
|
.era-item.active .era-content {
|
||||||
border-color: #667eea;
|
border-color: var(--vp-c-brand);
|
||||||
background: linear-gradient(135deg, #f8f9ff, #ffffff);
|
background: var(--vp-c-bg-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.era-header {
|
.era-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 0.75rem;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.era-year {
|
.era-year {
|
||||||
padding: 4px 12px;
|
padding: 2px 10px;
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
background: var(--vp-c-brand);
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.era-name {
|
.era-name {
|
||||||
font-size: 18px;
|
font-size: 1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.era-brief {
|
.era-brief {
|
||||||
font-size: 14px;
|
font-size: 0.85rem;
|
||||||
color: #666;
|
color: var(--vp-c-text-2);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 详情展开 */
|
/* 详情展开 */
|
||||||
.era-detail {
|
.era-detail {
|
||||||
margin-top: 16px;
|
margin-top: 0.75rem;
|
||||||
padding-top: 16px;
|
padding-top: 0.75rem;
|
||||||
border-top: 2px dashed #e0e0e0;
|
border-top: 2px dashed var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-section {
|
.detail-section {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-section:last-child {
|
.detail-section:last-child {
|
||||||
@@ -321,124 +284,36 @@ const eras = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 13px;
|
font-size: 0.8rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #667eea;
|
color: var(--vp-c-brand);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 技术标签 */
|
/* 技术标签 */
|
||||||
.tech-tags {
|
.tech-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-tag {
|
.tech-tag {
|
||||||
padding: 4px 12px;
|
padding: 2px 10px;
|
||||||
background: #f0f4ff;
|
background: var(--vp-c-bg-soft);
|
||||||
color: #667eea;
|
color: var(--vp-c-brand);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
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 {
|
.metaphor-box {
|
||||||
background: linear-gradient(135deg, #fff7ed, #ffedd5);
|
background: var(--vp-c-bg-alt);
|
||||||
border-left: 4px solid #f97316;
|
border-left: 4px solid var(--vp-c-brand);
|
||||||
padding: 12px;
|
padding: 0.75rem;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
font-size: 13px;
|
font-size: 0.8rem;
|
||||||
color: #9a3412;
|
color: var(--vp-c-text-2);
|
||||||
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;
|
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,28 +332,25 @@ const eras = [
|
|||||||
|
|
||||||
.expand-enter-to,
|
.expand-enter-to,
|
||||||
.expand-leave-from {
|
.expand-leave-from {
|
||||||
max-height: 1000px;
|
max-height: 600px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式 */
|
.info-box {
|
||||||
@media (max-width: 768px) {
|
background: var(--vp-c-bg-alt);
|
||||||
.era-item {
|
padding: 0.75rem;
|
||||||
flex-direction: column;
|
border-radius: 6px;
|
||||||
gap: 12px;
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.era-marker {
|
.info-box .icon {
|
||||||
flex-direction: row;
|
flex-shrink: 0;
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.era-line {
|
.info-box strong {
|
||||||
width: 100%;
|
color: var(--vp-c-text-1);
|
||||||
height: 4px;
|
|
||||||
min-height: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+93
-40
@@ -1,19 +1,19 @@
|
|||||||
<!--
|
<!--
|
||||||
ImperativeVsDeclarativeDemo.vue
|
ImperativeVsDeclarativeDemo.vue - 命令式 vs 声明式编程对比
|
||||||
命令式 vs 声明式编程对比演示
|
用"画画的两种方式"来解释 jQuery vs Vue/React 的区别
|
||||||
|
|
||||||
用途:
|
|
||||||
通过并排的交互式计数器,直观展示 Imperative(jQuery)和 Declarative(Vue)
|
|
||||||
在代码量和心智负担上的差异。
|
|
||||||
|
|
||||||
交互功能:
|
|
||||||
- 两个可交互的计数器。
|
|
||||||
- 切换展示背后的代码实现。
|
|
||||||
- 高亮显示 jQuery 需要手动更新的多个 DOM 节点 vs Vue 的自动绑定。
|
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="imperative-declarative-demo">
|
<div class="imperative-declarative-demo">
|
||||||
|
<!-- 标题区 -->
|
||||||
<div class="demo-header">
|
<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">
|
<div class="toggle-group">
|
||||||
<button
|
<button
|
||||||
v-for="view in views"
|
v-for="view in views"
|
||||||
@@ -24,14 +24,13 @@
|
|||||||
{{ view.label }}
|
{{ view.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="comparison-container">
|
<div class="comparison-container">
|
||||||
<!-- Imperative Side (jQuery) -->
|
<!-- Imperative Side (jQuery) -->
|
||||||
<div class="side imperative-side">
|
<div class="side imperative-side">
|
||||||
<div class="side-header">
|
<div class="side-header">
|
||||||
<span class="badge imperative">jQuery / Imperative</span>
|
<span class="badge imperative">jQuery / 命令式</span>
|
||||||
<h4>"Tell me HOW"</h4>
|
<span class="sub-label">通俗说法: 告诉怎么做</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-area">
|
<div class="demo-area">
|
||||||
@@ -112,8 +111,8 @@
|
|||||||
<!-- Declarative Side (Vue) -->
|
<!-- Declarative Side (Vue) -->
|
||||||
<div class="side declarative-side">
|
<div class="side declarative-side">
|
||||||
<div class="side-header">
|
<div class="side-header">
|
||||||
<span class="badge declarative">Vue / Declarative</span>
|
<span class="badge declarative">Vue / 声明式</span>
|
||||||
<h4>"Tell me WHAT"</h4>
|
<span class="sub-label">通俗说法: 告诉要什么</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-area">
|
<div class="demo-area">
|
||||||
@@ -188,12 +187,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部控制 -->
|
||||||
<div class="demo-controls">
|
<div class="demo-controls">
|
||||||
<button class="toggle-btn" @click="showAnalysis = !showAnalysis">
|
<button class="toggle-btn" @click="showAnalysis = !showAnalysis">
|
||||||
{{ showAnalysis ? '隐藏' : '显示' }}对比分析
|
{{ showAnalysis ? '隐藏' : '显示' }}对比分析
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 信息框 -->
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>
|
||||||
|
命令式编程需要一步步告诉浏览器"怎么做",声明式编程只需告诉浏览器"要什么",框架会自动处理细节。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -224,13 +232,40 @@ function updateJq(change) {
|
|||||||
.imperative-declarative-demo {
|
.imperative-declarative-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 标题区 */
|
||||||
.demo-header {
|
.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 {
|
.toggle-group {
|
||||||
@@ -275,9 +310,10 @@ function updateJq(change) {
|
|||||||
|
|
||||||
.side-header {
|
.side-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.side-header .badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 0.75rem;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
@@ -285,14 +321,11 @@ function updateJq(change) {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge.imperative {
|
.side-header .sub-label {
|
||||||
background-color: rgba(7, 105, 173, 0.2);
|
display: block;
|
||||||
color: #0769ad;
|
font-size: 0.75rem;
|
||||||
}
|
color: var(--vp-c-text-2);
|
||||||
|
margin-top: 0.5rem;
|
||||||
.badge.declarative {
|
|
||||||
background-color: rgba(66, 184, 131, 0.2);
|
|
||||||
color: #2c8a5e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-header h4 {
|
.side-header h4 {
|
||||||
@@ -356,7 +389,7 @@ function updateJq(change) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-text.warning {
|
.status-text.warning {
|
||||||
color: #f87171;
|
color: var(--vp-c-warning);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,8 +439,8 @@ function updateJq(change) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.imperative-code {
|
.imperative-code {
|
||||||
background-color: #1e1e2e;
|
background: var(--vp-c-bg-alt);
|
||||||
color: #a6accd;
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.imperative-code code {
|
.imperative-code code {
|
||||||
@@ -415,8 +448,8 @@ function updateJq(change) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.declarative-code {
|
.declarative-code {
|
||||||
background-color: #1e1e2e;
|
background: var(--vp-c-bg-alt);
|
||||||
color: #a6accd;
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.declarative-code code {
|
.declarative-code code {
|
||||||
@@ -433,7 +466,7 @@ function updateJq(change) {
|
|||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: linear-gradient(135deg, #0769ad, #42b883);
|
background: var(--vp-c-brand);
|
||||||
color: white;
|
color: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -460,23 +493,43 @@ function updateJq(change) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pain-point {
|
.pain-point {
|
||||||
background-color: rgba(248, 113, 113, 0.1);
|
background: var(--vp-c-bg-alt);
|
||||||
color: #dc2626;
|
color: var(--vp-c-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.benefit {
|
.benefit {
|
||||||
background-color: rgba(74, 222, 128, 0.1);
|
background: var(--vp-c-bg-alt);
|
||||||
color: #16a34a;
|
color: var(--vp-c-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-controls {
|
.demo-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 1.5rem;
|
margin-top: 1rem;
|
||||||
padding-top: 1.5rem;
|
padding-top: 1rem;
|
||||||
border-top: 1px solid var(--vp-c-divider);
|
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) {
|
@media (max-width: 768px) {
|
||||||
.comparison-container {
|
.comparison-container {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
+159
-235
@@ -1,20 +1,18 @@
|
|||||||
<!--
|
<!--
|
||||||
JQueryVsStateDemo.vue - 餐厅账本对比
|
JQueryVsStateDemo.vue - 前端开发模式对比
|
||||||
用"手工记账 vs 智能管家"的比喻来解释 jQuery vs Vue/React
|
用"手工记账 vs 智能管家"的比喻来解释 jQuery vs Vue/React
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="restaurant-demo">
|
<div class="jquery-vs-state-demo">
|
||||||
<!-- 故事引入 -->
|
<!-- 标题区 -->
|
||||||
<div class="story-intro">
|
<div class="demo-header">
|
||||||
<div class="story-icon">👨🍳📒🤖</div>
|
<span class="icon">🔄</span>
|
||||||
<h3 class="story-title">老张的餐厅账本</h3>
|
<span class="title">前端开发模式</span>
|
||||||
<p class="story-desc">
|
<span class="subtitle">手动操作DOM vs 状态管理</span>
|
||||||
老张开了家餐厅,每天要点菜、做菜、算账。有两种记账方式:<br>
|
|
||||||
<strong>传统方式:老张手工记</strong>(jQuery 模式) vs <strong>智能方式:请个管家</strong>(Vue/React 模式)<br>
|
|
||||||
看看哪种更轻松?
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<div class="demo-content">
|
||||||
<!-- 模式选择 -->
|
<!-- 模式选择 -->
|
||||||
<div class="mode-tabs">
|
<div class="mode-tabs">
|
||||||
<button
|
<button
|
||||||
@@ -24,7 +22,7 @@
|
|||||||
>
|
>
|
||||||
<span class="tab-icon">✍️</span>
|
<span class="tab-icon">✍️</span>
|
||||||
<span class="tab-text">手工记账</span>
|
<span class="tab-text">手工记账</span>
|
||||||
<span class="tab-sub">jQuery 方式</span>
|
<span class="tab-sub">通俗说法: jQuery</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="tab-btn"
|
class="tab-btn"
|
||||||
@@ -33,7 +31,7 @@
|
|||||||
>
|
>
|
||||||
<span class="tab-icon">🤖</span>
|
<span class="tab-icon">🤖</span>
|
||||||
<span class="tab-text">智能管家</span>
|
<span class="tab-text">智能管家</span>
|
||||||
<span class="tab-sub">Vue/React 方式</span>
|
<span class="tab-sub">通俗说法: Vue/React</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -43,7 +41,7 @@
|
|||||||
<div class="scenario-panel">
|
<div class="scenario-panel">
|
||||||
<div class="scenario-header">
|
<div class="scenario-header">
|
||||||
<span class="scenario-icon">{{ mode === 'manual' ? '👨🍳' : '🤖' }}</span>
|
<span class="scenario-icon">{{ mode === 'manual' ? '👨🍳' : '🤖' }}</span>
|
||||||
<span class="scenario-title">{{ mode === 'manual' ? '老张手工记账' : '智能管家记账' }}</span>
|
<span class="scenario-title">{{ mode === 'manual' ? '手工记账' : '智能管家' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="scenario-content">
|
<div class="scenario-content">
|
||||||
@@ -119,34 +117,14 @@
|
|||||||
重新开始
|
重新开始
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 优缺点对比 -->
|
<!-- 信息框 -->
|
||||||
<div class="comparison-table">
|
<div class="info-box">
|
||||||
<div class="table-header">
|
<span class="icon">💡</span>
|
||||||
<div class="table-title">💡 两种方式对比</div>
|
<strong>核心思想:</strong>
|
||||||
</div>
|
<span v-if="mode === 'manual'">jQuery需要手动查找和修改DOM,就像手工记账,容易出错。</span>
|
||||||
<div class="table-content">
|
<span v-else>Vue/React通过状态自动更新界面,就像智能管家,改数据界面自动变。</span>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -237,53 +215,53 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.restaurant-demo {
|
.jquery-vs-state-demo {
|
||||||
border: 2px solid #e8e8e8;
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 16px;
|
border-radius: 8px;
|
||||||
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 24px;
|
padding: 1rem;
|
||||||
margin: 20px 0;
|
margin: 1rem 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 故事引入 */
|
/* 标题区 */
|
||||||
.story-intro {
|
.demo-header {
|
||||||
text-align: center;
|
display: flex;
|
||||||
margin-bottom: 24px;
|
align-items: center;
|
||||||
padding: 20px;
|
gap: 0.5rem;
|
||||||
background: linear-gradient(135deg, #fff8e1, #ffecb3);
|
margin-bottom: 0.75rem;
|
||||||
border-radius: 16px;
|
|
||||||
border: 2px dashed #ffc107;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-icon {
|
.demo-header .icon {
|
||||||
font-size: 48px;
|
font-size: 1.25rem;
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-title {
|
.demo-header .title {
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #e65100;
|
font-size: 1rem;
|
||||||
margin: 0 0 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-desc {
|
.demo-header .subtitle {
|
||||||
font-size: 14px;
|
color: var(--vp-c-text-2);
|
||||||
color: #666;
|
font-size: 0.85rem;
|
||||||
line-height: 1.6;
|
margin-left: 0.5rem;
|
||||||
margin: 0;
|
}
|
||||||
|
|
||||||
|
/* 主内容区 */
|
||||||
|
.demo-content {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 模式选项卡 */
|
/* 模式选项卡 */
|
||||||
.mode-tabs {
|
.mode-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 0.75rem;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 1rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
padding: 8px;
|
padding: 0.5rem;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-btn {
|
.tab-btn {
|
||||||
@@ -291,35 +269,36 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 0.25rem;
|
||||||
padding: 16px;
|
padding: 0.75rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.2s;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-btn:hover {
|
.tab-btn:hover {
|
||||||
background: #f5f5f5;
|
background: var(--vp-c-bg-alt);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-btn.active {
|
.tab-btn.active {
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
background: var(--vp-c-brand);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-icon {
|
.tab-icon {
|
||||||
font-size: 32px;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-text {
|
.tab-text {
|
||||||
font-size: 14px;
|
font-size: 0.85rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-sub {
|
.tab-sub {
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,8 +306,8 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
.comparison-showcase {
|
.comparison-showcase {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 20px;
|
gap: 1rem;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -339,67 +318,67 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
|
|
||||||
/* 场景面板 */
|
/* 场景面板 */
|
||||||
.scenario-panel {
|
.scenario-panel {
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 16px;
|
border-radius: 8px;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid var(--vp-c-divider);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-header {
|
.scenario-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 0.75rem;
|
||||||
padding: 16px;
|
padding: 0.75rem;
|
||||||
background: linear-gradient(135deg, #ffecb3, #ffe082);
|
background: var(--vp-c-bg-alt);
|
||||||
border-bottom: 2px solid #e0e0e0;
|
border-bottom: 2px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-icon {
|
.scenario-icon {
|
||||||
font-size: 28px;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-title {
|
.scenario-title {
|
||||||
font-size: 16px;
|
font-size: 0.9rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-content {
|
.scenario-content {
|
||||||
padding: 16px;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-list {
|
.step-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-item {
|
.step-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 0.75rem;
|
||||||
padding: 12px;
|
padding: 0.75rem;
|
||||||
background: #f5f5f5;
|
background: var(--vp-c-bg-alt);
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-item.active {
|
.step-item.active {
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
background: var(--vp-c-brand);
|
||||||
color: white;
|
color: white;
|
||||||
transform: translateX(8px);
|
transform: translateX(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-number {
|
.step-number {
|
||||||
width: 28px;
|
width: 24px;
|
||||||
height: 28px;
|
height: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
color: #333;
|
color: var(--vp-c-text-1);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,142 +388,147 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
}
|
}
|
||||||
|
|
||||||
.step-text {
|
.step-text {
|
||||||
font-size: 14px;
|
font-size: 0.85rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 账本面板 */
|
/* 账本面板 */
|
||||||
.ledger-panel {
|
.ledger-panel {
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 16px;
|
border-radius: 8px;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid var(--vp-c-divider);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ledger-header {
|
.ledger-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 0.75rem;
|
||||||
padding: 16px;
|
padding: 0.75rem;
|
||||||
background: linear-gradient(135deg, #c8e6c9, #a5d6a7);
|
background: var(--vp-c-bg-alt);
|
||||||
border-bottom: 2px solid #e0e0e0;
|
border-bottom: 2px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ledger-icon {
|
.ledger-icon {
|
||||||
font-size: 28px;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ledger-title {
|
.ledger-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 16px;
|
font-size: 0.9rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ledger-status {
|
.ledger-status {
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
padding: 4px 12px;
|
padding: 0.25rem 0.75rem;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
color: #666;
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ledger-status.manual {
|
.ledger-status.manual {
|
||||||
background: #fff3e0;
|
background: var(--vp-c-warning);
|
||||||
color: #e65100;
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ledger-status.smart {
|
||||||
|
background: var(--vp-c-success);
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ledger-content {
|
.ledger-content {
|
||||||
padding: 16px;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-list {
|
.order-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-item {
|
.order-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 12px;
|
padding: 0.75rem;
|
||||||
background: #f5f5f5;
|
background: var(--vp-c-bg-alt);
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-item.completed {
|
.order-item.completed {
|
||||||
background: #e8f5e9;
|
background: var(--vp-c-success);
|
||||||
border-left: 4px solid #4caf50;
|
border-left: 4px solid var(--vp-c-brand);
|
||||||
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-info {
|
.order-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-name {
|
.order-name {
|
||||||
font-size: 14px;
|
font-size: 0.85rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-price {
|
.order-price {
|
||||||
font-size: 13px;
|
font-size: 0.75rem;
|
||||||
color: #e65100;
|
color: var(--vp-c-brand);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-status {
|
.order-status {
|
||||||
font-size: 18px;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-section {
|
.total-section {
|
||||||
border-top: 2px dashed #e0e0e0;
|
border-top: 2px dashed var(--vp-c-divider);
|
||||||
padding-top: 12px;
|
padding-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-row {
|
.total-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 0;
|
padding: 0.5rem 0;
|
||||||
font-size: 14px;
|
font-size: 0.85rem;
|
||||||
color: #666;
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-row.total-final {
|
.total-row.total-final {
|
||||||
font-size: 16px;
|
font-size: 0.9rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: var(--vp-c-text-1);
|
||||||
border-top: 2px solid #e0e0e0;
|
border-top: 2px solid var(--vp-c-divider);
|
||||||
margin-top: 8px;
|
margin-top: 0.5rem;
|
||||||
padding-top: 12px;
|
padding-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-amount {
|
.total-amount {
|
||||||
color: #4caf50;
|
color: var(--vp-c-success);
|
||||||
font-size: 20px;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 操作按钮 */
|
/* 操作按钮 */
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12px;
|
gap: 0.75rem;
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 12px 24px;
|
padding: 0.5rem 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
font-size: 16px;
|
font-size: 0.85rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover:not(:disabled) {
|
.btn:hover:not(:disabled) {
|
||||||
@@ -558,91 +542,31 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
background: var(--vp-c-brand);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: #f5f5f5;
|
background: var(--vp-c-bg-alt);
|
||||||
color: #666;
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 对比表格 */
|
/* 信息框 */
|
||||||
.comparison-table {
|
.info-box {
|
||||||
background: white;
|
background: var(--vp-c-bg-alt);
|
||||||
border-radius: 16px;
|
padding: 0.75rem;
|
||||||
border: 2px solid #e0e0e0;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-header {
|
.info-box .icon {
|
||||||
padding: 16px;
|
flex-shrink: 0;
|
||||||
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
|
|
||||||
border-bottom: 2px solid #e0e0e0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-title {
|
.info-box strong {
|
||||||
font-size: 16px;
|
color: var(--vp-c-text-1);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,13 +4,21 @@
|
|||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="routing-demo">
|
<div class="routing-demo">
|
||||||
|
<!-- 标题区 -->
|
||||||
|
<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">
|
<div class="story-box">
|
||||||
<div class="story-emoji">📖📄✨</div>
|
|
||||||
<h4 class="story-title">小明看书记</h4>
|
|
||||||
<p class="story-text">
|
<p class="story-text">
|
||||||
小明喜欢看书。有两种看书方式:<br>
|
<strong>通俗说法:</strong>小明喜欢看书,有两种看书方式:<br>
|
||||||
<strong>MPA 方式:像翻书</strong>,每翻一页都要换一本书 <strong>SPA 方式:像换纸</strong>,在同一本书里换内容
|
<strong>MPA 方式(像翻书)</strong>:每翻一页都要换一本书<br>
|
||||||
|
<strong>SPA 方式(像换纸)</strong>:在同一本书里换内容
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -23,7 +31,7 @@
|
|||||||
>
|
>
|
||||||
<div class="mode-icon">📚</div>
|
<div class="mode-icon">📚</div>
|
||||||
<div class="mode-name">MPA 多页应用</div>
|
<div class="mode-name">MPA 多页应用</div>
|
||||||
<div class="mode-sub">像翻书:每次都换一本</div>
|
<div class="mode-sub">通俗说法: 像翻书</div>
|
||||||
<div class="mode-desc">每点一次链接,浏览器向服务器要新页面</div>
|
<div class="mode-desc">每点一次链接,浏览器向服务器要新页面</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -36,7 +44,7 @@
|
|||||||
>
|
>
|
||||||
<div class="mode-icon">📄</div>
|
<div class="mode-icon">📄</div>
|
||||||
<div class="mode-name">SPA 单页应用</div>
|
<div class="mode-name">SPA 单页应用</div>
|
||||||
<div class="mode-sub">像换纸:同一本书换内容</div>
|
<div class="mode-sub">通俗说法: 像换纸</div>
|
||||||
<div class="mode-desc">只加载一次,后续只切换内容</div>
|
<div class="mode-desc">只加载一次,后续只切换内容</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,10 +193,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 核心要点 -->
|
<!-- 核心要点 -->
|
||||||
<div class="key-takeaway">
|
<div class="info-box">
|
||||||
<div class="takeaway-icon">🎯</div>
|
<span class="icon">💡</span>
|
||||||
<div class="takeaway-content">
|
<strong>核心思想:</strong>
|
||||||
<strong>核心差异:</strong>
|
|
||||||
<strong>MPA</strong> 每次切换都要"整页刷新",像翻书,适合内容为主的网站;
|
<strong>MPA</strong> 每次切换都要"整页刷新",像翻书,适合内容为主的网站;
|
||||||
<strong>SPA</strong> 只加载一次,后续"局部更新",像换纸,适合交互复杂的应用。
|
<strong>SPA</strong> 只加载一次,后续"局部更新",像换纸,适合交互复杂的应用。
|
||||||
关键是:<strong>状态会不会丢</strong>。
|
关键是:<strong>状态会不会丢</strong>。
|
||||||
@@ -251,7 +258,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.routing-demo {
|
.routing-demo {
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid var(--vp-c-divider);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
|
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
@@ -303,7 +310,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-width: 280px;
|
max-width: 280px;
|
||||||
background: white;
|
background: white;
|
||||||
border: 3px solid #e0e0e0;
|
border: 3px solid var(--vp-c-divider);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -356,7 +363,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
.demo-area {
|
.demo-area {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid var(--vp-c-divider);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
@@ -493,7 +500,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
/* 阅读区 */
|
/* 阅读区 */
|
||||||
.reading-paper {
|
.reading-paper {
|
||||||
background: white;
|
background: white;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid var(--vp-c-divider);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
@@ -524,7 +531,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
.state-test {
|
.state-test {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
border-top: 2px dashed #e0e0e0;
|
border-top: 2px dashed var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-label {
|
.test-label {
|
||||||
@@ -537,7 +544,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
.test-input {
|
.test-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -572,7 +579,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
|
|
||||||
.nav-btn {
|
.nav-btn {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: white;
|
background: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -632,7 +639,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
.comparison-table {
|
.comparison-table {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid var(--vp-c-divider);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
@@ -655,7 +662,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|||||||
grid-template-columns: 1fr 1.5fr 1.5fr;
|
grid-template-columns: 1fr 1.5fr 1.5fr;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-bottom: 1px solid #e0e0e0;
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comparison-row:last-child {
|
.comparison-row:last-child {
|
||||||
|
|||||||
+165
-169
@@ -1,17 +1,24 @@
|
|||||||
<!--
|
<!--
|
||||||
SliceRequestDemo.vue - 搬家快递大作战
|
SliceRequestDemo.vue - HTTP请求优化对比
|
||||||
用"搬家打包"的比喻来解释 HTTP 请求优化(切图 vs 雪碧图)
|
用"搬家"的比喻来解释雪碧图 vs 切片请求
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="moving-game">
|
<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="demo-content">
|
||||||
<!-- 故事引入 -->
|
<!-- 故事引入 -->
|
||||||
<div class="story-box">
|
<div class="story-box">
|
||||||
<div class="story-emoji">📦🚚🏠</div>
|
|
||||||
<h4 class="story-title">小明搬家记</h4>
|
|
||||||
<p class="story-text">
|
<p class="story-text">
|
||||||
小明要搬 6 箱书到新房子。有两种搬家方式:<br>
|
<strong>通俗说法:</strong>就像搬家——<br>
|
||||||
<strong>A 方案:一箱一箱搬</strong>(切图模式) vs <strong>B 方案:一次性打包运走</strong>(雪碧图模式)<br>
|
<strong>切图模式</strong>:一箱一箱搬,需要6趟(6次HTTP请求)<br>
|
||||||
看看哪种更省时间?
|
<strong>雪碧图模式</strong>:打包一次性运走,只需1趟(1次HTTP请求)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -23,8 +30,8 @@
|
|||||||
@click="mode = 'separate'"
|
@click="mode = 'separate'"
|
||||||
>
|
>
|
||||||
<div class="mode-icon">🛵</div>
|
<div class="mode-icon">🛵</div>
|
||||||
<div class="mode-name">A 方案:一箱一趟</div>
|
<div class="mode-name">切图模式</div>
|
||||||
<div class="mode-desc">小面包车,一次拉一箱</div>
|
<div class="mode-desc">通俗说法: 一箱一趟</div>
|
||||||
<div class="mode-detail">需要 6 趟运输</div>
|
<div class="mode-detail">需要 6 趟运输</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -36,8 +43,8 @@
|
|||||||
@click="mode = 'packed'"
|
@click="mode = 'packed'"
|
||||||
>
|
>
|
||||||
<div class="mode-icon">🚚</div>
|
<div class="mode-icon">🚚</div>
|
||||||
<div class="mode-name">B 方案:打包一车拉</div>
|
<div class="mode-name">雪碧图模式</div>
|
||||||
<div class="mode-desc">大卡车,6箱一次运走</div>
|
<div class="mode-desc">通俗说法: 打包一车拉</div>
|
||||||
<div class="mode-detail">只需 1 趟运输</div>
|
<div class="mode-detail">只需 1 趟运输</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,22 +127,14 @@
|
|||||||
重置
|
重置
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
<!-- 信息框 -->
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -254,40 +253,56 @@ const resetStats = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.moving-game {
|
.slice-request-demo {
|
||||||
border: 2px solid #e8e8e8;
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 16px;
|
border-radius: 8px;
|
||||||
background: linear-gradient(135deg, #fafbfc 0%, #f0f4f8 100%);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 24px;
|
padding: 1rem;
|
||||||
margin: 20px 0;
|
margin: 1rem 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
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 {
|
.story-box {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 1rem;
|
||||||
padding: 20px;
|
padding: 0.75rem;
|
||||||
background: linear-gradient(135deg, #fff8e1, #ffecb3);
|
background: var(--vp-c-bg-alt);
|
||||||
border-radius: 16px;
|
border-radius: 6px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-text {
|
.story-text {
|
||||||
font-size: 14px;
|
font-size: 0.85rem;
|
||||||
color: #666;
|
color: var(--vp-c-text-2);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
@@ -297,67 +312,67 @@ const resetStats = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 16px;
|
gap: 0.75rem;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 1rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-card {
|
.mode-card {
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border: 3px solid #e0e0e0;
|
border: 2px solid var(--vp-c-divider);
|
||||||
border-radius: 16px;
|
border-radius: 8px;
|
||||||
padding: 20px;
|
padding: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.2s;
|
||||||
min-width: 200px;
|
min-width: 160px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 280px;
|
max-width: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-card:hover {
|
.mode-card:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-card.active {
|
.mode-card.active {
|
||||||
border-color: #4caf50;
|
border-color: var(--vp-c-brand);
|
||||||
background: #e8f5e9;
|
background: var(--vp-c-brand-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-icon {
|
.mode-icon {
|
||||||
font-size: 48px;
|
font-size: 2rem;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-name {
|
.mode-name {
|
||||||
font-size: 16px;
|
font-size: 0.9rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: var(--vp-c-text-1);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-desc {
|
.mode-desc {
|
||||||
font-size: 13px;
|
font-size: 0.75rem;
|
||||||
color: #666;
|
color: var(--vp-c-text-2);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-detail {
|
.mode-detail {
|
||||||
font-size: 14px;
|
font-size: 0.85rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #e65100;
|
color: var(--vp-c-brand);
|
||||||
padding: 4px 12px;
|
padding: 0.25rem 0.75rem;
|
||||||
background: #fff3e0;
|
background: var(--vp-c-bg-alt);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vs-divider {
|
.vs-divider {
|
||||||
font-size: 24px;
|
font-size: 1.25rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #999;
|
color: var(--vp-c-text-3);
|
||||||
padding: 0 8px;
|
padding: 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 动画演示区 */
|
/* 动画演示区 */
|
||||||
@@ -365,52 +380,52 @@ const resetStats = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 1rem;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 1rem;
|
||||||
padding: 20px;
|
padding: 1rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 16px;
|
border-radius: 8px;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.location {
|
.location {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
min-width: 100px;
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-icon {
|
.location-icon {
|
||||||
font-size: 40px;
|
font-size: 2rem;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-label {
|
.location-label {
|
||||||
font-size: 14px;
|
font-size: 0.85rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: var(--vp-c-text-1);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.boxes-remaining,
|
.boxes-remaining,
|
||||||
.boxes-delivered {
|
.boxes-delivered {
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
color: #666;
|
color: var(--vp-c-text-2);
|
||||||
padding: 4px 8px;
|
padding: 0.25rem 0.5rem;
|
||||||
background: #f5f5f5;
|
background: var(--vp-c-bg-alt);
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.count {
|
.count {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #e65100;
|
color: var(--vp-c-brand);
|
||||||
font-size: 16px;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.road {
|
.road {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 80px;
|
height: 60px;
|
||||||
background: linear-gradient(to bottom, #e8eaf6 0%, #c5cae9 100%);
|
background: var(--vp-c-bg-alt);
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,8 +437,8 @@ const resetStats = () => {
|
|||||||
height: 4px;
|
height: 4px;
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
#7986cb 0px,
|
var(--vp-c-brand) 0px,
|
||||||
#7986cb 20px,
|
var(--vp-c-brand) 20px,
|
||||||
transparent 20px,
|
transparent 20px,
|
||||||
transparent 40px
|
transparent 40px
|
||||||
);
|
);
|
||||||
@@ -441,84 +456,81 @@ const resetStats = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vehicle-body {
|
.vehicle-body {
|
||||||
font-size: 32px;
|
font-size: 1.5rem;
|
||||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
||||||
}
|
}
|
||||||
|
|
||||||
.vehicle-cargo {
|
.vehicle-cargo {
|
||||||
font-size: 12px;
|
font-size: 0.75rem;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
padding: 2px 6px;
|
padding: 0.125rem 0.375rem;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
margin-top: 2px;
|
margin-top: 0.125rem;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #e65100;
|
color: var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统计面板 */
|
/* 统计面板 */
|
||||||
.stats-panel {
|
.stats-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 16px;
|
gap: 1rem;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item {
|
.stat-item {
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 2px solid #e0e0e0;
|
border: 2px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 13px;
|
font-size: 0.75rem;
|
||||||
color: #666;
|
color: var(--vp-c-text-2);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 24px;
|
font-size: 1.25rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value.good {
|
.stat-value.good {
|
||||||
color: #4caf50;
|
color: var(--vp-c-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value.bad {
|
.stat-value.bad {
|
||||||
color: #f44336;
|
color: var(--vp-c-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value.excellent {
|
.stat-value.excellent {
|
||||||
color: #2196f3;
|
color: var(--vp-c-brand);
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value.good {
|
|
||||||
color: #4caf50;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value.poor {
|
.stat-value.poor {
|
||||||
color: #ff9800;
|
color: var(--vp-c-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 控制按钮 */
|
/* 控制按钮 */
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12px;
|
gap: 0.75rem;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 12px 24px;
|
padding: 0.5rem 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
font-size: 16px;
|
font-size: 0.9rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover:not(:disabled) {
|
.btn:hover:not(:disabled) {
|
||||||
@@ -532,48 +544,32 @@ const resetStats = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
background: var(--vp-c-brand);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: #f5f5f5;
|
background: var(--vp-c-bg-alt);
|
||||||
color: #666;
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 知识点总结 */
|
/* 信息框 */
|
||||||
.knowledge-box {
|
.info-box {
|
||||||
background: linear-gradient(135deg, #e3f2fd, #f3e5f5);
|
background: var(--vp-c-bg-alt);
|
||||||
border-radius: 12px;
|
padding: 0.75rem;
|
||||||
padding: 20px;
|
border-radius: 6px;
|
||||||
border-left: 4px solid #2196f3;
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-title {
|
.info-box .icon {
|
||||||
font-size: 16px;
|
flex-shrink: 0;
|
||||||
font-weight: bold;
|
|
||||||
color: #1565c0;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-content {
|
.info-box strong {
|
||||||
font-size: 14px;
|
color: var(--vp-c-text-1);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式 */
|
/* 响应式 */
|
||||||
@@ -588,7 +584,7 @@ const resetStats = () => {
|
|||||||
|
|
||||||
.animation-area {
|
.animation-area {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.road {
|
.road {
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="three-areas-demo">
|
<div class="three-areas-demo">
|
||||||
|
<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">
|
<div class="scene">
|
||||||
<!-- 1. Working Directory (Desk) -->
|
<!-- 1. Working Directory (Desk) -->
|
||||||
<div class="zone working">
|
<div class="zone working">
|
||||||
@@ -121,6 +128,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
@@ -132,6 +140,11 @@
|
|||||||
<pre class="mono"><code>{{ statusText }}</code></pre>
|
<pre class="mono"><code>{{ statusText }}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>Git 的三区就像餐厅——工作区是餐桌(随便放),暂存区是备菜盘(准备上菜),仓库是菜单(永久记录)。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -239,11 +252,40 @@ const commitFiles = () => {
|
|||||||
.three-areas-demo {
|
.three-areas-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
font-family: var(--vp-font-family-mono);
|
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 {
|
.scene {
|
||||||
@@ -641,4 +683,23 @@ const commitFiles = () => {
|
|||||||
grid-template-columns: 1fr;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,134 +1,136 @@
|
|||||||
<!--
|
<!--
|
||||||
GitWorkflowDemo.vue
|
GitWorkflowDemo.vue
|
||||||
Git 工作流演示 - 简洁版
|
Git 基础工作流演示 - 寄快递版
|
||||||
|
|
||||||
用途:展示 Git 的基本工作流程
|
展示 Git 的基本工作流程:修改 → 暂存 → 提交
|
||||||
交互:初始化、提交、创建分支、合并
|
高度控制:紧凑布局,确保在 600px 内
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="git-workflow-demo">
|
<div class="git-workflow-demo">
|
||||||
<!-- 控制面板 -->
|
<div class="demo-header">
|
||||||
<div class="control-panel">
|
<span class="icon">📦</span>
|
||||||
<button
|
<span class="title">Git 工作流</span>
|
||||||
@click="initRepo"
|
<span class="subtitle">修改 → 暂存 → 提交,三步走</span>
|
||||||
:disabled="inited || mergePending"
|
</div>
|
||||||
class="action-btn"
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 箭头 -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 暂存区 -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 箭头 -->
|
||||||
|
<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="action-panel">
|
||||||
|
<button
|
||||||
|
@click="modifyFile"
|
||||||
|
class="action-btn"
|
||||||
|
:disabled="allModified"
|
||||||
|
>
|
||||||
|
✏️ 修改文件
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="makeCommit"
|
@click="stageFiles"
|
||||||
:disabled="!inited || mergePending"
|
|
||||||
class="action-btn"
|
class="action-btn"
|
||||||
|
:disabled="!hasModified || allStaged"
|
||||||
>
|
>
|
||||||
✅ 提交
|
📌 暂存修改
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="createBranch"
|
@click="commitFiles"
|
||||||
:disabled="!inited || hasBranch || mergePending"
|
|
||||||
class="action-btn"
|
class="action-btn"
|
||||||
|
:disabled="!hasStaged"
|
||||||
>
|
>
|
||||||
🌿 创建分支
|
✅ 提交版本
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="prepareMerge"
|
@click="reset"
|
||||||
:disabled="!hasBranch || mergePending"
|
class="action-btn secondary"
|
||||||
class="action-btn"
|
|
||||||
>
|
>
|
||||||
🔀 准备合并
|
🔄 重置
|
||||||
</button>
|
</button>
|
||||||
<button @click="finishMerge" :disabled="!mergePending" class="action-btn">
|
|
||||||
✅ 完成合并
|
|
||||||
</button>
|
|
||||||
<button @click="reset" class="action-btn secondary">🔄 重置</button>
|
|
||||||
</div>
|
</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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 分支线 -->
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 合并线 -->
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 提交节点 -->
|
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- 说明 -->
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<p>
|
<span class="icon">💡</span>
|
||||||
<strong>💡 工作流程:</strong> 初始化 → 提交 → 创建分支 → 开发 → 合并
|
<strong>核心思想:</strong>工作区修改 → 暂存区挑选 → 仓库永久保存
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -136,56 +138,92 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
const inited = ref(false)
|
const files = ref([
|
||||||
const hasBranch = ref(false)
|
{ name: 'index.html', status: 'unmodified' },
|
||||||
const mergePending = ref(false)
|
{ name: 'app.js', status: 'unmodified' },
|
||||||
const mainCommits = ref([])
|
{ name: 'style.css', status: 'unmodified' }
|
||||||
const branchCommits = ref([])
|
])
|
||||||
|
|
||||||
const status = computed(() => {
|
const commits = ref([])
|
||||||
if (mergePending.value) return '准备合并:检查改动/解决冲突后再完成合并'
|
|
||||||
if (hasBranch) return '分支已创建'
|
const hasModified = computed(() =>
|
||||||
if (inited) return '已初始化'
|
files.value.some(f => f.status === 'modified')
|
||||||
return '未初始化'
|
)
|
||||||
|
|
||||||
|
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 getStatusText = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'modified': return '已修改'
|
||||||
|
case 'staged': return '已暂存'
|
||||||
|
case 'committed': return '已提交'
|
||||||
|
default: return '未修改'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 stageFiles = () => {
|
||||||
|
files.value.forEach(f => {
|
||||||
|
if (f.status === 'modified') {
|
||||||
|
f.status = 'staged'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const initRepo = () => {
|
|
||||||
inited.value = true
|
|
||||||
mainCommits.value = [{ hash: 'abc123' }]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeCommit = () => {
|
const commitFiles = () => {
|
||||||
if (inited.value) {
|
const staged = files.value.filter(f => f.status === 'staged')
|
||||||
mainCommits.value.push({ hash: Math.random().toString(16).substr(2, 6) })
|
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 createBranch = () => {
|
|
||||||
if (inited.value && !hasBranch.value) {
|
|
||||||
hasBranch.value = true
|
|
||||||
branchCommits.value = [{ hash: 'def456' }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const prepareMerge = () => {
|
|
||||||
if (!hasBranch.value) return
|
|
||||||
mergePending.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
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 reset = () => {
|
const reset = () => {
|
||||||
inited.value = false
|
files.value.forEach(f => {
|
||||||
hasBranch.value = false
|
f.status = 'unmodified'
|
||||||
mergePending.value = false
|
})
|
||||||
mainCommits.value = []
|
commits.value = []
|
||||||
branchCommits.value = []
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -193,94 +231,230 @@ const reset = () => {
|
|||||||
.git-workflow-demo {
|
.git-workflow-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
max-height: 550px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-panel {
|
.demo-header {
|
||||||
display: flex;
|
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;
|
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;
|
flex-wrap: wrap;
|
||||||
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
padding: 0.625rem 1.25rem;
|
padding: 0.5rem 1rem;
|
||||||
border: 2px solid var(--vp-c-brand);
|
background: var(--vp-c-brand);
|
||||||
background: var(--vp-c-bg);
|
color: white;
|
||||||
color: var(--vp-c-brand);
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 0.875rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn:hover:not(:disabled) {
|
.action-btn:hover:not(:disabled) {
|
||||||
background: var(--vp-c-brand);
|
opacity: 0.9;
|
||||||
color: var(--vp-c-bg);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn:disabled {
|
.action-btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.4;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
border-color: var(--vp-c-divider);
|
background: var(--vp-c-divider);
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn.secondary {
|
.action-btn.secondary {
|
||||||
border-color: var(--vp-c-divider);
|
background: transparent;
|
||||||
}
|
|
||||||
|
|
||||||
.visualization {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.graph-container {
|
|
||||||
background: var(--vp-c-bg);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
border: 1px solid var(--vp-c-divider);
|
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);
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-item .value {
|
.action-btn.secondary:hover:not(:disabled) {
|
||||||
font-weight: 600;
|
border-color: var(--vp-c-brand);
|
||||||
color: var(--vp-c-brand);
|
color: var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box {
|
.info-box {
|
||||||
padding: 1rem;
|
background: var(--vp-c-bg-alt);
|
||||||
background: var(--vp-c-bg);
|
padding: 0.75rem;
|
||||||
border-left: 4px solid var(--vp-c-brand);
|
border-radius: 6px;
|
||||||
border-radius: 4px;
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-box p {
|
.info-box .icon {
|
||||||
margin: 0;
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box strong {
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
line-height: 1.6;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="coupling-demo">
|
<div class="coupling-demo">
|
||||||
<div class="header">
|
<div class="demo-header">
|
||||||
<div class="title">系统解耦:从紧耦合到松耦合</div>
|
<span class="icon">🔗</span>
|
||||||
<div class="subtitle">观察同步调用与异步消息的区别</div>
|
<span class="title">系统解耦</span>
|
||||||
|
<span class="subtitle">从紧耦合到松耦合</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mode-switch">
|
<div class="mode-switch">
|
||||||
@@ -26,7 +27,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="demo-container">
|
<div class="demo-content">
|
||||||
<!-- 紧耦合模式 -->
|
<!-- 紧耦合模式 -->
|
||||||
<div v-if="!useAsync" class="synchronous-mode">
|
<div v-if="!useAsync" class="synchronous-mode">
|
||||||
<div class="scenario">
|
<div class="scenario">
|
||||||
@@ -65,7 +66,7 @@
|
|||||||
<div class="problem-list">
|
<div class="problem-list">
|
||||||
<div class="problem-item">
|
<div class="problem-item">
|
||||||
<span class="icon">⚠️</span>
|
<span class="icon">⚠️</span>
|
||||||
<span><strong>依赖性强:</strong>通知服务宕机,订单创建失败</span>
|
<span><strong>依赖性强:</strong>通知服务宕机,订单创建失败</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="problem-item">
|
<div class="problem-item">
|
||||||
<span class="icon">⚠️</span>
|
<span class="icon">⚠️</span>
|
||||||
@@ -111,7 +112,7 @@
|
|||||||
<div class="consumer-box" :class="{ failed: consumerFailed }">
|
<div class="consumer-box" :class="{ failed: consumerFailed }">
|
||||||
<div class="consumer-name">短信服务</div>
|
<div class="consumer-name">短信服务</div>
|
||||||
<div class="consumer-status">
|
<div class="consumer-status">
|
||||||
{{ consumerFailed ? '离线(不影响订单)' : '运行中' }}
|
{{ consumerFailed ? '离线(不影响订单)' : '运行中' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="consumer-box">
|
<div class="consumer-box">
|
||||||
@@ -122,10 +123,6 @@
|
|||||||
<div class="consumer-name">积分服务</div>
|
<div class="consumer-name">积分服务</div>
|
||||||
<div class="consumer-status">运行中</div>
|
<div class="consumer-status">运行中</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="consumer-box new">
|
|
||||||
<div class="consumer-name">数据分析</div>
|
|
||||||
<div class="consumer-status">新增 ✨</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -137,7 +134,7 @@
|
|||||||
<div class="benefit-item">
|
<div class="benefit-item">
|
||||||
<span class="icon">✅</span>
|
<span class="icon">✅</span>
|
||||||
<span
|
<span
|
||||||
><strong>响应快:</strong>订单服务只耗时 50ms(发送消息)</span
|
><strong>响应快:</strong>订单服务只耗时 50ms(发送消息)</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="benefit-item">
|
<div class="benefit-item">
|
||||||
@@ -153,41 +150,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="comparison-summary">
|
<div class="info-box">
|
||||||
<div class="summary-title">📊 对比总结</div>
|
<span class="icon">💡</span>
|
||||||
<div class="summary-table">
|
<strong>核心思想:</strong>同步调用强依赖、响应慢;异步消息解耦、响应快、易扩展
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -203,7 +168,7 @@ const messageInQueue = ref(false)
|
|||||||
const syncCalls = ref([
|
const syncCalls = ref([
|
||||||
{ id: 1, service: '调用库存服务', active: false, status: '处理中...' },
|
{ id: 1, service: '调用库存服务', active: false, status: '处理中...' },
|
||||||
{ id: 2, service: '调用积分服务', active: false, status: '处理中...' },
|
{ id: 2, service: '调用积分服务', active: false, status: '处理中...' },
|
||||||
{ id: 3, service: '调用通知服务', active: false, status: '失败!订单回滚' }
|
{ id: 3, service: '调用通知服务', active: false, status: '失败!订单回滚' }
|
||||||
])
|
])
|
||||||
|
|
||||||
const testSyncCall = () => {
|
const testSyncCall = () => {
|
||||||
@@ -232,42 +197,52 @@ const testAsyncCall = () => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.coupling-demo {
|
.coupling-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 8px;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
border-radius: 12px;
|
padding: 1rem;
|
||||||
padding: 1.5rem;
|
margin: 1rem 0;
|
||||||
margin: 1.5rem 0;
|
max-height: 600px;
|
||||||
font-family: var(--vp-font-family-base);
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.demo-header {
|
||||||
margin-bottom: 1rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.demo-header .icon {
|
||||||
font-weight: 700;
|
font-size: 1.25rem;
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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);
|
color: var(--vp-c-text-2);
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
margin-top: 0.25rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-switch {
|
.mode-switch {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-btn {
|
.mode-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border: 2px solid var(--vp-c-divider);
|
border: 2px solid var(--vp-c-divider);
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,18 +252,18 @@ const testAsyncCall = () => {
|
|||||||
|
|
||||||
.mode-btn.active {
|
.mode-btn.active {
|
||||||
background: var(--vp-c-brand);
|
background: var(--vp-c-brand);
|
||||||
color: #fff;
|
color: white;
|
||||||
border-color: var(--vp-c-brand);
|
border-color: var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-container {
|
.demo-content {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-title {
|
.scenario-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1rem;
|
font-size: 0.9rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.75rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,55 +271,55 @@ const testAsyncCall = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-box {
|
.service-box {
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
border: 2px solid var(--vp-c-brand);
|
border: 2px solid var(--vp-c-brand);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 0.75rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
min-width: 180px;
|
min-width: 140px;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-box.failed {
|
.service-box.failed {
|
||||||
border-color: #ef4444;
|
border-color: var(--vp-c-danger);
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: var(--vp-c-danger-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-name {
|
.service-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.95rem;
|
font-size: 0.85rem;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-desc {
|
.service-desc {
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-msg {
|
.error-msg {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
padding: 0.35rem 0.5rem;
|
padding: 0.35rem 0.5rem;
|
||||||
background: #ef4444;
|
background: var(--vp-c-danger);
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrows {
|
.arrows {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 300px;
|
max-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sync-call {
|
.sync-call {
|
||||||
@@ -357,7 +332,7 @@ const testAsyncCall = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sync-call.active {
|
.sync-call.active {
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: var(--vp-c-danger-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-line {
|
.call-line {
|
||||||
@@ -367,96 +342,91 @@ const testAsyncCall = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sync-call.active .call-line {
|
.sync-call.active .call-line {
|
||||||
background: #ef4444;
|
background: var(--vp-c-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-label {
|
.call-label {
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-status {
|
.call-status {
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
color: #ef4444;
|
color: var(--vp-c-danger);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mq-bridge {
|
.mq-bridge {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mq-box {
|
.mq-box {
|
||||||
background: rgba(59, 130, 246, 0.1);
|
background: var(--vp-c-brand-soft);
|
||||||
border: 2px solid var(--vp-c-brand);
|
border: 2px solid var(--vp-c-brand);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 0.75rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
min-width: 140px;
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mq-icon {
|
.mq-icon {
|
||||||
font-size: 2rem;
|
font-size: 1.5rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mq-label {
|
.mq-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-indicator {
|
.msg-indicator {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
padding: 0.35rem 0.5rem;
|
padding: 0.35rem 0.5rem;
|
||||||
background: #dcfce7;
|
background: var(--vp-c-success);
|
||||||
color: #166534;
|
color: white;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow-arrow {
|
.flow-arrow {
|
||||||
font-size: 1.5rem;
|
font-size: 1.25rem;
|
||||||
color: var(--vp-c-brand);
|
color: var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.consumers-group {
|
.consumers-group {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 500px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.consumer-box {
|
.consumer-box {
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
border: 2px solid var(--vp-c-brand);
|
border: 2px solid var(--vp-c-brand);
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
padding: 0.75rem;
|
padding: 0.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.consumer-box.failed {
|
.consumer-box.failed {
|
||||||
border-color: #f59e0b;
|
border-color: var(--vp-c-warning);
|
||||||
background: rgba(245, 158, 11, 0.1);
|
background: var(--vp-c-warning-soft);
|
||||||
}
|
|
||||||
|
|
||||||
.consumer-box.new {
|
|
||||||
border-color: #22c55e;
|
|
||||||
background: rgba(34, 197, 94, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.consumer-name {
|
.consumer-name {
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.consumer-status {
|
.consumer-status {
|
||||||
font-size: 0.7rem;
|
font-size: 0.65rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,8 +434,8 @@ const testAsyncCall = () => {
|
|||||||
.benefit-list {
|
.benefit-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problem-item,
|
.problem-item,
|
||||||
@@ -473,92 +443,66 @@ const testAsyncCall = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.75rem;
|
padding: 0.5rem;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.8rem;
|
||||||
line-height: 1.5;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problem-item {
|
.problem-item {
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: var(--vp-c-danger-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.benefit-item {
|
.benefit-item {
|
||||||
background: rgba(34, 197, 94, 0.1);
|
background: var(--vp-c-success-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
font-size: 1.2rem;
|
font-size: 1rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-btn {
|
.test-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem;
|
padding: 0.5rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-btn.fail {
|
.test-btn.fail {
|
||||||
background: #ef4444;
|
background: var(--vp-c-danger);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-btn.fail:hover {
|
.test-btn.fail:hover {
|
||||||
background: #dc2626;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-btn.success {
|
.test-btn.success {
|
||||||
background: #22c55e;
|
background: var(--vp-c-success);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-btn.success:hover {
|
.test-btn.success:hover {
|
||||||
background: #16a34a;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comparison-summary {
|
.info-box {
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg-alt);
|
||||||
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 {
|
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
text-align: left;
|
border-radius: 6px;
|
||||||
border-bottom: 1px solid var(--vp-c-divider);
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
.info-box .icon {
|
||||||
background: var(--vp-c-bg-soft);
|
flex-shrink: 0;
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bad {
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.good {
|
|
||||||
color: #16a34a;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,24 +4,26 @@
|
|||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="dlq-demo">
|
<div class="dlq-demo">
|
||||||
<div class="header">
|
<div class="demo-header">
|
||||||
<div class="title">死信队列:消息的"急救站"</div>
|
<span class="icon">🚑</span>
|
||||||
<div class="subtitle">处理无法消费的消息,避免阻塞队列</div>
|
<span class="title">死信队列</span>
|
||||||
|
<span class="subtitle">消息的"急救站" - 处理失败消息</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label>失败率:</label>
|
<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>
|
<span class="value">{{ failureRate }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label>最大重试次数:</label>
|
<label>最大重试:</label>
|
||||||
<input v-model="maxRetries" type="range" min="1" max="5" step="1" />
|
<input v-model.number="maxRetries" type="range" min="1" max="5" step="1" />
|
||||||
<span class="value">{{ maxRetries }}</span>
|
<span class="value">{{ maxRetries }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-content">
|
||||||
<div class="flow-container">
|
<div class="flow-container">
|
||||||
<div class="main-queue-section">
|
<div class="main-queue-section">
|
||||||
<div class="section-title">📦 主队列</div>
|
<div class="section-title">📦 主队列</div>
|
||||||
@@ -32,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="message-list">
|
<div class="message-list">
|
||||||
<div
|
<div
|
||||||
v-for="msg in mainQueue"
|
v-for="msg in mainQueue.slice(0, 3)"
|
||||||
:key="msg.id"
|
:key="msg.id"
|
||||||
class="message-item"
|
class="message-item"
|
||||||
:class="{ processing: msg.processing }"
|
:class="{ processing: msg.processing }"
|
||||||
@@ -43,6 +45,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="mainQueue.length === 0" class="empty">队列为空</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>
|
||||||
</div>
|
</div>
|
||||||
<button class="add-btn" @click="addMessage" :disabled="processing">
|
<button class="add-btn" @click="addMessage" :disabled="processing">
|
||||||
@@ -77,7 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="message-list">
|
<div class="message-list">
|
||||||
<div
|
<div
|
||||||
v-for="msg in deadLetterQueue"
|
v-for="msg in deadLetterQueue.slice(0, 2)"
|
||||||
:key="msg.id"
|
:key="msg.id"
|
||||||
class="message-item failed"
|
class="message-item failed"
|
||||||
>
|
>
|
||||||
@@ -87,6 +92,9 @@
|
|||||||
<div v-if="deadLetterQueue.length === 0" class="empty">
|
<div v-if="deadLetterQueue.length === 0" class="empty">
|
||||||
无失败消息
|
无失败消息
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="deadLetterQueue.length > 2" class="more">
|
||||||
|
还有 {{ deadLetterQueue.length - 2 }} 条...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -117,20 +125,11 @@
|
|||||||
<div class="stat-value">{{ successRate }}%</div>
|
<div class="stat-value">{{ successRate }}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="explanation">
|
<div class="info-box">
|
||||||
<div class="exp-title">💡 死信队列的作用</div>
|
<span class="icon">💡</span>
|
||||||
<div class="exp-content">
|
<strong>核心思想:</strong>失败消息进入死信队列,避免阻塞正常消息,可后续人工介入或自动重试
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -174,7 +173,7 @@ const processNext = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = mainQueue[0]
|
let msg = mainQueue.value[0]
|
||||||
msg.processing = true
|
msg.processing = true
|
||||||
processing.value = true
|
processing.value = true
|
||||||
currentMessage.value = msg
|
currentMessage.value = msg
|
||||||
@@ -188,7 +187,7 @@ const processNext = () => {
|
|||||||
msg.processing = false
|
msg.processing = false
|
||||||
|
|
||||||
if (msg.retries >= maxRetries.value) {
|
if (msg.retries >= maxRetries.value) {
|
||||||
// 超过最大重试次数,进入死信队列
|
// 超过最大重试次数,进入死信队列
|
||||||
mainQueue.value.shift()
|
mainQueue.value.shift()
|
||||||
deadLetterQueue.value.push({
|
deadLetterQueue.value.push({
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
@@ -202,7 +201,7 @@ const processNext = () => {
|
|||||||
// 重新入队
|
// 重新入队
|
||||||
lastResult.value = {
|
lastResult.value = {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: `⚠️ 消息 #${msg.id} 处理失败,重试 ${msg.retries}/${maxRetries.value}`
|
message: `⚠️ 消息 #${msg.id} 处理失败,重试 ${msg.retries}/${maxRetries.value}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,39 +261,48 @@ addMessage = addMessageWithAutoProcess
|
|||||||
.dlq-demo {
|
.dlq-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
margin: 1.5rem 0;
|
margin: 1rem 0;
|
||||||
font-family: var(--vp-font-family-base);
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.demo-header {
|
||||||
margin-bottom: 1rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.demo-header .icon {
|
||||||
font-weight: 700;
|
font-size: 1.25rem;
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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);
|
color: var(--vp-c-text-2);
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
margin-top: 0.25rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control {
|
.control {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control input[type='range'] {
|
.control input[type='range'] {
|
||||||
@@ -307,25 +315,29 @@ addMessage = addMessageWithAutoProcess
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.demo-content {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.flow-container {
|
.flow-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 0.85rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-box {
|
.queue-box {
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border: 2px solid var(--vp-c-divider);
|
border: 2px solid var(--vp-c-divider);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +346,7 @@ addMessage = addMessageWithAutoProcess
|
|||||||
}
|
}
|
||||||
|
|
||||||
.queue-box.dead-letter {
|
.queue-box.dead-letter {
|
||||||
border-color: #ef4444;
|
border-color: var(--vp-c-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-header {
|
.queue-header {
|
||||||
@@ -343,12 +355,12 @@ addMessage = addMessageWithAutoProcess
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-list {
|
.message-list {
|
||||||
max-height: 200px;
|
max-height: 150px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -361,17 +373,17 @@ addMessage = addMessageWithAutoProcess
|
|||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-bottom: 0.4rem;
|
margin-bottom: 0.4rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-item.processing {
|
.message-item.processing {
|
||||||
border: 1px solid #f59e0b;
|
border: 1px solid var(--vp-c-warning);
|
||||||
background: rgba(245, 158, 11, 0.1);
|
background: var(--vp-c-warning-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-item.failed {
|
.message-item.failed {
|
||||||
border: 1px solid #ef4444;
|
border: 1px solid var(--vp-c-danger);
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: var(--vp-c-danger-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-id {
|
.msg-id {
|
||||||
@@ -379,32 +391,32 @@ addMessage = addMessageWithAutoProcess
|
|||||||
}
|
}
|
||||||
|
|
||||||
.msg-retries {
|
.msg-retries {
|
||||||
font-size: 0.7rem;
|
font-size: 0.65rem;
|
||||||
color: #f59e0b;
|
color: var(--vp-c-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-error {
|
.msg-error {
|
||||||
font-size: 0.7rem;
|
font-size: 0.65rem;
|
||||||
color: #ef4444;
|
color: var(--vp-c-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty, .more {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1.5rem;
|
padding: 1rem 0.5rem;
|
||||||
color: var(--vp-c-text-3);
|
color: var(--vp-c-text-3);
|
||||||
font-size: 0.85rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-btn,
|
.add-btn,
|
||||||
.retry-btn {
|
.retry-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.6rem;
|
padding: 0.5rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.85rem;
|
font-size: 0.8rem;
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.5rem;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,8 +426,7 @@ addMessage = addMessageWithAutoProcess
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-btn:hover:not(:disabled) {
|
.add-btn:hover:not(:disabled) {
|
||||||
transform: translateY(-1px);
|
opacity: 0.9;
|
||||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-btn:disabled {
|
.add-btn:disabled {
|
||||||
@@ -424,21 +435,21 @@ addMessage = addMessageWithAutoProcess
|
|||||||
}
|
}
|
||||||
|
|
||||||
.retry-btn {
|
.retry-btn {
|
||||||
background: #f59e0b;
|
background: var(--vp-c-warning);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.retry-btn:hover:not(:disabled) {
|
.retry-btn:hover:not(:disabled) {
|
||||||
background: #d97706;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.processor-box {
|
.processor-box {
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border: 2px solid var(--vp-c-divider);
|
border: 2px solid var(--vp-c-divider);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
min-height: 200px;
|
min-height: 150px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -446,8 +457,8 @@ addMessage = addMessageWithAutoProcess
|
|||||||
}
|
}
|
||||||
|
|
||||||
.processor-icon {
|
.processor-icon {
|
||||||
font-size: 2.5rem;
|
font-size: 2rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.processor-icon.active {
|
.processor-icon.active {
|
||||||
@@ -455,97 +466,87 @@ addMessage = addMessageWithAutoProcess
|
|||||||
}
|
}
|
||||||
|
|
||||||
.processor-status {
|
.processor-status {
|
||||||
font-size: 0.9rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-msg {
|
.current-msg {
|
||||||
font-size: 0.85rem;
|
font-size: 0.75rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.last-result {
|
.last-result {
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.last-result.success {
|
.last-result.success {
|
||||||
background: #dcfce7;
|
background: var(--vp-c-success);
|
||||||
color: #166534;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.last-result.warning {
|
.last-result.warning {
|
||||||
background: rgba(245, 158, 11, 0.1);
|
background: var(--vp-c-warning-soft);
|
||||||
color: #d97706;
|
color: var(--vp-c-warning-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.last-result.error {
|
.last-result.error {
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: var(--vp-c-danger-soft);
|
||||||
color: #dc2626;
|
color: var(--vp-c-danger-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats {
|
.stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 0.75rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card.success {
|
.stat-card.success {
|
||||||
border-color: #22c55e;
|
border-color: var(--vp-c-success);
|
||||||
background: rgba(34, 197, 94, 0.05);
|
background: var(--vp-c-success-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card.warning {
|
.stat-card.warning {
|
||||||
border-color: #ef4444;
|
border-color: var(--vp-c-danger);
|
||||||
background: rgba(239, 68, 68, 0.05);
|
background: var(--vp-c-danger-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 0.8rem;
|
font-size: 0.7rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
margin-bottom: 0.35rem;
|
margin-bottom: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 1.3rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explanation {
|
.info-box {
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg-alt);
|
||||||
border-radius: 10px;
|
padding: 0.75rem;
|
||||||
padding: 1rem;
|
border-radius: 6px;
|
||||||
border: 1px solid var(--vp-c-divider);
|
font-size: 0.8rem;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box .icon {
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@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>
|
<template>
|
||||||
<div class="demo-container">
|
<div class="idempotence-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>{{ title }}</h4>
|
<span class="icon">🔄</span>
|
||||||
<p class="hint">{{ description }}</p>
|
<span class="title">幂等性演示</span>
|
||||||
|
<span class="subtitle">保证重复消费不会产生副作用</span>
|
||||||
</div>
|
</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">
|
<div class="demo-content">
|
||||||
<el-alert type="info" :closable="false">
|
<!-- 银行转账场景 -->
|
||||||
幂等性演示组件占位符 - 待实现具体交互
|
<div v-if="scenario === 'transfer'" class="transfer-scenario">
|
||||||
</el-alert>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.demo-container {
|
.idempotence-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-family: var(--vp-font-family-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.demo-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header h4 {
|
.demo-header .icon {
|
||||||
margin: 0 0 8px 0;
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.demo-header .subtitle {
|
||||||
margin: 0;
|
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;
|
font-size: 14px;
|
||||||
color: var(--vp-c-text-2);
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
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;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,13 +1,207 @@
|
|||||||
|
<!--
|
||||||
|
ReliabilityDemo.vue
|
||||||
|
消息可靠性演示 - 三道防线
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="demo-container">
|
<div class="reliability-demo">
|
||||||
<div class="demo-header">
|
<div class="demo-header">
|
||||||
<h4>{{ title }}</h4>
|
<span class="icon">🛡️</span>
|
||||||
<p class="hint">{{ description }}</p>
|
<span class="title">消息可靠性演示</span>
|
||||||
|
<span class="subtitle">三道防线保证消息不丢失</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 class="demo-content">
|
|
||||||
<el-alert type="info" :closable="false">
|
|
||||||
消息可靠性演示组件占位符 - 待实现具体交互
|
|
||||||
</el-alert>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -15,36 +209,500 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const title = ref('消息可靠性演示')
|
// 防线1: 生产者确认
|
||||||
const description = ref('展示消息队列如何保证消息的可靠传输,包括消息确认、持久化、重试机制等')
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.demo-container {
|
.reliability-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-family: var(--vp-font-family-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header {
|
.demo-header {
|
||||||
margin-bottom: 20px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-header h4 {
|
.demo-header .icon {
|
||||||
margin: 0 0 8px 0;
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.demo-header .subtitle {
|
||||||
margin: 0;
|
color: var(--vp-c-text-2);
|
||||||
font-size: 14px;
|
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);
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-content {
|
.message-flow {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
<!--
|
|
||||||
BigFrontendScopeDemo.vue
|
|
||||||
前端 vs 大前端(跨端)范围演示
|
|
||||||
-->
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bigfe-demo">
|
<div class="bigfe-demo">
|
||||||
<div class="header">
|
<div class="demo-header">
|
||||||
<div class="title">前端 vs 大前端:到底“前端”都包含什么?</div>
|
<span class="icon">🌐</span>
|
||||||
<div class="subtitle">点一下不同“端”,立刻看到它跑在哪里、怎么发布</div>
|
<span class="title">前端 vs 大前端</span>
|
||||||
|
<span class="subtitle">了解不同平台的运行环境和技术栈</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-content">
|
||||||
<div class="platforms">
|
<div class="platforms">
|
||||||
<button
|
<button
|
||||||
v-for="p in platforms"
|
v-for="p in platforms"
|
||||||
@@ -38,14 +36,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="skills">
|
<div class="skills">
|
||||||
<div class="skills-title">哪些能力是“共通的”?</div>
|
<div class="skills-title">哪些能力是"共通的"?</div>
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
<span v-for="t in commonSkills" :key="t" class="tag">{{ t }}</span>
|
<span v-for="t in commonSkills.slice(0, 6)" :key="t" class="tag">{{ t }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="skills-note">
|
|
||||||
大前端的核心不是“会更多框架”,而是:<strong>用同一套工程能力,把体验交付到不同平台</strong>。
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>大前端不是"会更多框架",而是用同一套工程能力,把体验交付到不同平台。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ const platforms = [
|
|||||||
{ key: 'web', label: 'Web网站', icon: '🌐' },
|
{ key: 'web', label: 'Web网站', icon: '🌐' },
|
||||||
{ key: 'h5', label: 'H5活动页', icon: '📱' },
|
{ key: 'h5', label: 'H5活动页', icon: '📱' },
|
||||||
{ key: 'miniapp', label: '小程序', icon: '🧩' },
|
{ key: 'miniapp', label: '小程序', icon: '🧩' },
|
||||||
{ key: 'native', label: 'App(原生)', icon: '📲' },
|
{ key: 'native', label: '原生App', icon: '📲' },
|
||||||
{ key: 'cross', label: '跨端App', icon: '🧱' },
|
{ key: 'cross', label: '跨端App', icon: '🧱' },
|
||||||
{ key: 'desktop', label: '桌面应用', icon: '🖥️' }
|
{ key: 'desktop', label: '桌面应用', icon: '🖥️' }
|
||||||
]
|
]
|
||||||
@@ -112,32 +113,45 @@ const commonSkills = [
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.bigfe-demo {
|
.bigfe-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 8px;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
border-radius: 12px;
|
padding: 1rem;
|
||||||
padding: 1.5rem;
|
margin: 1rem 0;
|
||||||
margin: 1.5rem 0;
|
max-height: 600px;
|
||||||
font-family: var(--vp-font-family-base);
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.demo-header {
|
||||||
margin-bottom: 1rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.demo-header .icon {
|
||||||
font-weight: 700;
|
font-size: 1.25rem;
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.demo-header .title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header .subtitle {
|
||||||
color: var(--vp-c-text-2);
|
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 {
|
.platforms {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 0.75rem 0 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform {
|
.platform {
|
||||||
@@ -150,22 +164,25 @@ const commonSkills = [
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform:hover {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform.active {
|
.platform.active {
|
||||||
border-color: #3b82f6;
|
border-color: var(--vp-c-brand);
|
||||||
color: #1d4ed8;
|
color: var(--vp-c-brand);
|
||||||
background: rgba(59, 130, 246, 0.12);
|
background: var(--vp-c-brand-soft);
|
||||||
}
|
font-weight: 600;
|
||||||
|
|
||||||
.icon {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cards {
|
.cards {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
@@ -185,17 +202,19 @@ const commonSkills = [
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.skills {
|
.skills {
|
||||||
margin-top: 1rem;
|
|
||||||
border-top: 1px dashed var(--vp-c-divider);
|
border-top: 1px dashed var(--vp-c-divider);
|
||||||
padding-top: 1rem;
|
padding-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skills-title {
|
.skills-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
@@ -208,14 +227,26 @@ const commonSkills = [
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
padding: 0.2rem 0.55rem;
|
padding: 0.2rem 0.55rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(34, 197, 94, 0.12);
|
background: var(--vp-c-bg-alt);
|
||||||
color: #15803d;
|
color: var(--vp-c-text-1);
|
||||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
border: 1px solid var(--vp-c-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.skills-note {
|
.info-box {
|
||||||
margin-top: 0.75rem;
|
background: var(--vp-c-bg-alt);
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--vp-c-text-2);
|
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>
|
</style>
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="box-demo">
|
<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="controls">
|
||||||
<div class="control-item">
|
<div class="control-item">
|
||||||
<div class="control-header">
|
<div class="control-header">
|
||||||
@@ -67,6 +73,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>每个元素都是一个"盒子",从内到外依次是:内容区 → 内边距 → 边框 → 外边距。
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<div class="code-title">CSS 代码片段</div>
|
<div class="code-title">CSS 代码片段</div>
|
||||||
<div class="code-content">
|
<div class="code-content">
|
||||||
@@ -99,13 +110,37 @@ const total = computed(
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.box-demo {
|
.box-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 24px;
|
padding: 1rem;
|
||||||
margin: 24px 0;
|
margin: 1rem 0;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.controls {
|
||||||
@@ -310,4 +345,22 @@ input[type='range'] {
|
|||||||
.line {
|
.line {
|
||||||
white-space: pre;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
<!--
|
<!--
|
||||||
CssFlexbox.vue
|
CssFlexbox.vue
|
||||||
Flex 速学:三个按钮控制方向/对齐/换行,实时看盒子怎么排。
|
Flex 布局速学:三个按钮控制方向/对齐/换行,实时看盒子怎么排
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-demo">
|
<div class="flex-demo">
|
||||||
|
<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="controls">
|
||||||
<div class="control-item">
|
<div class="control-item">
|
||||||
<div class="control-header">
|
<div class="control-header">
|
||||||
@@ -54,7 +61,7 @@
|
|||||||
|
|
||||||
<div class="canvas-container">
|
<div class="canvas-container">
|
||||||
<div class="canvas" :style="boxStyle">
|
<div class="canvas" :style="boxStyle">
|
||||||
<div v-for="n in 8" :key="n" class="item">{{ n }}</div>
|
<div v-for="n in 6" :key="n" class="item">{{ n }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,6 +77,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>Flex 让元素自动排列,不用手动计算位置。就像书架上的书,会自动对齐。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -99,7 +112,7 @@ const boxStyle = computed(() => ({
|
|||||||
justifyContent: justify.value,
|
justifyContent: justify.value,
|
||||||
flexWrap: wrap.value,
|
flexWrap: wrap.value,
|
||||||
gap: '12px',
|
gap: '12px',
|
||||||
minHeight: '200px',
|
minHeight: '180px',
|
||||||
padding: '16px'
|
padding: '16px'
|
||||||
}))
|
}))
|
||||||
</script>
|
</script>
|
||||||
@@ -107,50 +120,76 @@ const boxStyle = computed(() => ({
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.flex-demo {
|
.flex-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 24px;
|
padding: 1rem;
|
||||||
margin: 24px 0;
|
margin: 1rem 0;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: 24px;
|
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 {
|
.controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 16px;
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-item {
|
.control-item {
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 0.75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-header label {
|
.control-header label {
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
font-size: 13px;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chips {
|
.chips {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip {
|
.chip {
|
||||||
padding: 6px 12px;
|
padding: 0.25rem 0.75rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
background: var(--vp-c-bg-alt);
|
background: var(--vp-c-bg-alt);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 0.8rem;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,77 +200,60 @@ const boxStyle = computed(() => ({
|
|||||||
.chip.active {
|
.chip.active {
|
||||||
border-color: var(--vp-c-brand);
|
border-color: var(--vp-c-brand);
|
||||||
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;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-container {
|
.canvas-container {
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
padding: 4px; /* Tiny padding for the inner canvas */
|
padding: 0.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas {
|
.canvas {
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
/* border: 1px dashed var(--vp-c-divider); */
|
|
||||||
background-image: radial-gradient(var(--vp-c-divider) 1px, transparent 1px);
|
background-image: radial-gradient(var(--vp-c-divider) 1px, transparent 1px);
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
width: 60px;
|
width: 50px;
|
||||||
height: 60px;
|
height: 50px;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
background: linear-gradient(135deg, #0ea5e9, #10b981);
|
background: var(--vp-c-brand);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
box-shadow:
|
transition: all 0.3s;
|
||||||
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);
|
|
||||||
flex-shrink: 0;
|
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 {
|
.code-block {
|
||||||
background: var(--vp-c-bg-alt);
|
background: var(--vp-c-bg-alt);
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-title {
|
.code-title {
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.5rem;
|
||||||
font-size: 13px;
|
font-size: 0.8rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-content {
|
.code-content {
|
||||||
background: #0b1221;
|
background: var(--vp-c-bg);
|
||||||
color: #e5e7eb;
|
color: var(--vp-c-text-1);
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
padding: 16px;
|
padding: 0.75rem;
|
||||||
font-family: var(--vp-font-family-mono);
|
font-family: var(--vp-font-family-mono);
|
||||||
font-size: 13px;
|
font-size: 0.75rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
@@ -239,4 +261,22 @@ input[type='range'] {
|
|||||||
.line {
|
.line {
|
||||||
white-space: pre;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
<!--
|
<!--
|
||||||
DomManipulator.vue
|
DomManipulator.vue
|
||||||
DOM 速体验:输入标题+切换高亮类,直观看到文本和样式变化。
|
DOM 操作速体验:输入标题+切换高亮类,直观看到文本和样式变化
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="dom-demo">
|
<div class="dom-demo">
|
||||||
|
<div class="demo-header">
|
||||||
|
<span class="icon">🎯</span>
|
||||||
|
<span class="title">DOM 操作</span>
|
||||||
|
<span class="subtitle">网页内容的动态修改(通俗说:用代码改页面)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-content">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>改个标题</label>
|
<label>改个标题</label>
|
||||||
<input v-model="title" placeholder="输入新标题" />
|
<input v-model="title" placeholder="输入新标题" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field checkbox">
|
<div class="field checkbox">
|
||||||
<label
|
<label><input type="checkbox" v-model="highlight" /> 高亮模式 (class="highlight")</label>
|
||||||
><input type="checkbox" v-model="highlight" /> 高亮模式
|
|
||||||
(class="highlight")</label
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,6 +35,12 @@ titleEl.textContent = '{{ title }}'
|
|||||||
const card = document.querySelector('.card')
|
const card = document.querySelector('.card')
|
||||||
card.classList.toggle('highlight', {{ highlight }})</code></pre>
|
card.classList.toggle('highlight', {{ highlight }})</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>DOM 是网页的"乐高积木",JavaScript 可以随时添加、删除、修改这些积木。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -48,73 +58,154 @@ const toggleText = () => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.dom-demo {
|
.dom-demo {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 16px;
|
padding: 1rem;
|
||||||
margin: 20px 0;
|
margin: 1rem 0;
|
||||||
display: flex;
|
max-height: 600px;
|
||||||
flex-direction: column;
|
overflow-y: auto;
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
.controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 10px;
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 10px;
|
padding: 0.75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
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'] {
|
input[type='checkbox'] {
|
||||||
accent-color: var(--vp-c-brand);
|
accent-color: var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 1rem;
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.highlight {
|
.card.highlight {
|
||||||
border-color: #f59e0b;
|
border-color: var(--vp-c-warning);
|
||||||
box-shadow: 0 8px 18px rgba(245, 158, 11, 0.2);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
background: #fff7ed;
|
background: var(--vp-c-bg-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card h2 {
|
.card h2 {
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card p {
|
.card p {
|
||||||
margin: 0 0 12px 0;
|
margin: 0 0 0.75rem 0;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card button {
|
.card button {
|
||||||
background: var(--vp-c-brand);
|
background: var(--vp-c-brand);
|
||||||
color: #fff;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
padding: 8px 12px;
|
padding: 0.5rem 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card button:hover {
|
||||||
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code {
|
.code {
|
||||||
background: #0b1221;
|
background: var(--vp-c-bg-alt);
|
||||||
color: #e5e7eb;
|
color: var(--vp-c-text-1);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 12px;
|
padding: 0.75rem;
|
||||||
font-family: var(--vp-font-family-mono);
|
font-family: var(--vp-font-family-mono);
|
||||||
font-size: 13px;
|
font-size: 0.75rem;
|
||||||
overflow-x: auto;
|
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>
|
</style>
|
||||||
|
|||||||
+94
-40
@@ -1,11 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="imperative-declarative-demo">
|
<div class="imperative-declarative-demo">
|
||||||
|
<div class="demo-header">
|
||||||
|
<span class="icon">🔄</span>
|
||||||
|
<span class="title">命令式 vs 声明式</span>
|
||||||
|
<span class="subtitle">两种编程思维的对比(通俗说:手动操作 vs 自动响应)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-content">
|
||||||
<div class="demo-grid">
|
<div class="demo-grid">
|
||||||
<!-- Imperative (jQuery Style) -->
|
<!-- Imperative (jQuery Style) -->
|
||||||
<div class="panel imperative">
|
<div class="panel imperative">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="badge yellow">Imperative (命令式)</span>
|
<span class="badge yellow">命令式 (Imperative)</span>
|
||||||
<span class="sub-text">jQuery Style</span>
|
<span class="sub-text">jQuery Style - 手动操作</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="code-preview">
|
<div class="code-preview">
|
||||||
<code>
|
<code>
|
||||||
@@ -39,13 +46,13 @@
|
|||||||
<!-- Declarative (Vue Style) -->
|
<!-- Declarative (Vue Style) -->
|
||||||
<div class="panel declarative">
|
<div class="panel declarative">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="badge green">Declarative (声明式)</span>
|
<span class="badge green">声明式 (Declarative)</span>
|
||||||
<span class="sub-text">Vue/React Style</span>
|
<span class="sub-text">Vue/React Style - 自动响应</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="code-preview">
|
<div class="code-preview">
|
||||||
<code>
|
<code v-pre>
|
||||||
// 只需要绑定数据<br />
|
// 只需要绑定数据
|
||||||
{{ '{' + '{ count }' + '}' }}<br />
|
{{ count }}
|
||||||
<div v-if="count > 5">...</div>
|
<div v-if="count > 5">...</div>
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,6 +73,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="icon">💡</span>
|
||||||
|
<strong>核心思想:</strong>命令式像"手把手教电脑怎么做",声明式像"告诉电脑要什么,它自己搞定"。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -119,12 +132,40 @@ const decIncrement = () => {
|
|||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 1rem 0;
|
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 {
|
.demo-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 1.5rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
@@ -143,7 +184,7 @@ const decIncrement = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-header {
|
.panel-header {
|
||||||
padding: 0.8rem;
|
padding: 0.75rem;
|
||||||
border-bottom: 1px solid var(--vp-c-divider);
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -152,48 +193,51 @@ const decIncrement = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 2px 6px;
|
padding: 2px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge.yellow {
|
.badge.yellow {
|
||||||
background: #f59e0b;
|
background: var(--vp-c-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge.green {
|
.badge.green {
|
||||||
background: #10b981;
|
background: var(--vp-c-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-text {
|
.sub-text {
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-preview {
|
.code-preview {
|
||||||
background: #1e1e2e;
|
background: var(--vp-c-bg-alt);
|
||||||
padding: 0.8rem;
|
padding: 0.75rem;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
color: #a6accd;
|
color: var(--vp-c-text-1);
|
||||||
height: 80px;
|
height: 70px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.interactive-area {
|
.interactive-area {
|
||||||
padding: 1rem;
|
padding: 0.75rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.output-box {
|
.output-box {
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
padding: 1rem;
|
padding: 0.75rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
min-height: 80px;
|
min-height: 70px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -201,10 +245,9 @@ const decIncrement = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.warning-msg {
|
.warning-msg {
|
||||||
color: #ef4444;
|
color: var(--vp-c-danger);
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
animation: popIn 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
@@ -217,42 +260,53 @@ const decIncrement = () => {
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border: 1px solid var(--vp-c-divider);
|
border: 1px solid var(--vp-c-divider);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: white;
|
background: var(--vp-c-bg);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover:not(:disabled) {
|
.btn:hover:not(:disabled) {
|
||||||
background: #f3f4f6;
|
background: var(--vp-c-bg-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.primary {
|
.btn.primary {
|
||||||
background: #3b82f6;
|
background: var(--vp-c-brand);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.primary:hover {
|
.btn.primary:hover {
|
||||||
background: #2563eb;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-log {
|
.status-log {
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
color: var(--vp-c-text-3);
|
color: var(--vp-c-text-2);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
min-height: 1.2em;
|
min-height: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes popIn {
|
.info-box {
|
||||||
0% {
|
background: var(--vp-c-bg-alt);
|
||||||
transform: scale(0.8);
|
padding: 0.75rem;
|
||||||
opacity: 0;
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
.info-box .icon {
|
||||||
opacity: 1;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-box strong {
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -303,6 +303,8 @@ const handleBtnClick = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* New Config Panel Styles */
|
/* New Config Panel Styles */
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ import MessageQueueComponentsDemo from './components/appendix/queue-design/Messa
|
|||||||
import PointToPointVsPubSubDemo from './components/appendix/queue-design/PointToPointVsPubSubDemo.vue'
|
import PointToPointVsPubSubDemo from './components/appendix/queue-design/PointToPointVsPubSubDemo.vue'
|
||||||
import MessageQueueComparisonDemo from './components/appendix/queue-design/MessageQueueComparisonDemo.vue'
|
import MessageQueueComparisonDemo from './components/appendix/queue-design/MessageQueueComparisonDemo.vue'
|
||||||
import CouplingDemo from './components/appendix/queue-design/CouplingDemo.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 PubSubDemo from './components/appendix/queue-design/PubSubDemo.vue'
|
||||||
import DeadLetterQueueDemo from './components/appendix/queue-design/DeadLetterQueueDemo.vue'
|
import DeadLetterQueueDemo from './components/appendix/queue-design/DeadLetterQueueDemo.vue'
|
||||||
import DelayedMessageDemo from './components/appendix/queue-design/DelayedMessageDemo.vue'
|
import DelayedMessageDemo from './components/appendix/queue-design/DelayedMessageDemo.vue'
|
||||||
@@ -693,6 +694,7 @@ export default {
|
|||||||
app.component('PointToPointVsPubSubDemo', PointToPointVsPubSubDemo)
|
app.component('PointToPointVsPubSubDemo', PointToPointVsPubSubDemo)
|
||||||
app.component('MessageQueueComparisonDemo', MessageQueueComparisonDemo)
|
app.component('MessageQueueComparisonDemo', MessageQueueComparisonDemo)
|
||||||
app.component('CouplingDemo', CouplingDemo)
|
app.component('CouplingDemo', CouplingDemo)
|
||||||
|
app.component('DecouplingDemo', DecouplingDemo)
|
||||||
app.component('PubSubDemo', PubSubDemo)
|
app.component('PubSubDemo', PubSubDemo)
|
||||||
app.component('DeadLetterQueueDemo', DeadLetterQueueDemo)
|
app.component('DeadLetterQueueDemo', DeadLetterQueueDemo)
|
||||||
app.component('DelayedMessageDemo', DelayedMessageDemo)
|
app.component('DelayedMessageDemo', DelayedMessageDemo)
|
||||||
|
|||||||
+280
-715
File diff suppressed because it is too large
Load Diff
@@ -152,7 +152,7 @@ _很多人以为 AI 直接处理"声音",但实际上 AI 处理的是**数字
|
|||||||
### 4.1 自回归 vs 非自回归
|
### 4.1 自回归 vs 非自回归
|
||||||
|
|
||||||
| 特性 | 自回归 (AR) | 非自回归 (NAR) | 流匹配 (Flow) |
|
| 特性 | 自回归 (AR) | 非自回归 (NAR) | 流匹配 (Flow) |
|
||||||
|------|------------|---------------|--------------|
|
| -------- | ----------- | -------------- | ------------- |
|
||||||
| 生成方式 | 逐个时间步 | 一次性生成 | 流匹配路径 |
|
| 生成方式 | 逐个时间步 | 一次性生成 | 流匹配路径 |
|
||||||
| 速度 | 慢 | 快 | 很快 |
|
| 速度 | 慢 | 快 | 很快 |
|
||||||
| 音质 | 高 | 中高 | 高 |
|
| 音质 | 高 | 中高 | 高 |
|
||||||
@@ -286,7 +286,7 @@ GST (Global Style Token) 是一种从参考音频中提取风格特征的方法
|
|||||||
## 附录:常用术语表 (Vocabulary)
|
## 附录:常用术语表 (Vocabulary)
|
||||||
|
|
||||||
| 术语 | 英文 | 解释 |
|
| 术语 | 英文 | 解释 |
|
||||||
| :------------- | :--------------------------- | :------------------------------------------- |
|
| :--------------- | :--------------------------- | :------------------------------------------- |
|
||||||
| **采样率** | Sample Rate | 每秒采集的音频样本数(如 44.1kHz)。 |
|
| **采样率** | Sample Rate | 每秒采集的音频样本数(如 44.1kHz)。 |
|
||||||
| **梅尔频谱** | Mel-Spectrogram | 模拟人耳感知的频谱表示,音频 AI 的核心输入。 |
|
| **梅尔频谱** | Mel-Spectrogram | 模拟人耳感知的频谱表示,音频 AI 的核心输入。 |
|
||||||
| **声码器** | Vocoder | 将频谱图还原为音频波形的模型。 |
|
| **声码器** | Vocoder | 将频谱图还原为音频波形的模型。 |
|
||||||
|
|||||||
@@ -1,168 +1,311 @@
|
|||||||
# 后端架构演进:从单机到云原生
|
# 后端架构演进:从单机到云原生
|
||||||
|
|
||||||
> 💡 **学习指南**:本章节无需编程基础,通过交互式演示带你回顾后端架构的 30 年变迁。我们将从最原始的物理服务器讲起,一直到现代的 Serverless 云计算。理解架构演进的历史,能帮助你在面对技术选型时做出更明智的决策。
|
::: tip 🎯 核心问题
|
||||||
|
**代码写好了,怎么让全世界的人都能访问?** 这就像问:你是想开一家路边小摊,还是经营一家跨国连锁餐厅?后端架构的选择,决定了你的"餐厅"能服务多少顾客。
|
||||||
<EvolutionIntroDemo />
|
:::
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0. 引言:为什么要了解架构演进?
|
## 1. 为什么要了解架构演进?
|
||||||
|
|
||||||
想象一下,你正在规划一次长途旅行。你可以选择骑自行车、开私家车、坐高铁,或者乘飞机。每种方式都有其适用的场景:自行车适合短距离且想锻炼身体的情况,飞机则适合跨越大陆的长途旅行。
|
想象一下,你正在规划一次长途旅行。你可以选择骑自行车、开私家车、坐高铁,或者乘飞机。每种方式都有其适用的场景:自行车适合短距离且想锻炼身体的情况,飞机则适合跨越大陆的长途旅行。
|
||||||
|
|
||||||
**后端架构的选择也是如此。**
|
**后端架构的选择也是如此。**
|
||||||
|
|
||||||
从互联网诞生到现在,后端架构经历了多次重大变革。每一次变革都不是为了"追新潮",而是为了解决当时面临的特定问题:
|
从互联网诞生到现在,后端架构经历了多次重大变革。每一次变革都不是为了"追新潮",而是为了解决当时面临的特定问题:
|
||||||
|
|
||||||
| 年代 | 核心问题 | 架构演进 |
|
| 年代 | 核心问题 | 架构演进 |
|
||||||
|------|---------|---------|
|
| ----- | ------------------------ | ------------------- |
|
||||||
| 1990s | 如何把网站跑起来 | 物理服务器 |
|
| 1990s | 如何把网站跑起来 | 物理服务器 |
|
||||||
| 2000s | 代码越来越乱怎么维护 | 单体架构 + MVC |
|
| 2000s | 代码越来越乱怎么维护 | 单体架构 + MVC |
|
||||||
| 2010s | 系统太大怎么扩展和协作 | 微服务 + 容器化 |
|
| 2010s | 系统太大怎么扩展和协作 | 微服务 + 容器化 |
|
||||||
| 2020s | 如何降低运维成本和复杂性 | Serverless + 云原生 |
|
| 2020s | 如何降低运维成本和复杂性 | Serverless + 云原生 |
|
||||||
|
|
||||||
**了解架构演进的意义在于:**
|
::: tip 📊 从表格中你能看到什么?
|
||||||
|
让我们逐行解读这张表:
|
||||||
|
|
||||||
1. **避免重复造轮子**:很多"新"概念其实早在几十年前就有雏形,了解历史能让你站在巨人的肩膀上
|
**1990s → 2000s**:从"能跑就行"到"需要维护"。网站从静态页面变成动态应用,代码量激增,需要更好的组织方式。
|
||||||
2. **做出合理的技术选型**:没有最好的架构,只有最适合当前阶段的架构
|
|
||||||
3. **理解技术背后的权衡**:每一次架构演进都是在**开发效率**、**系统性能**、**运维复杂度**之间做取舍
|
|
||||||
4. **预判技术趋势**:历史总是押韵的,理解过去的演进规律有助于把握未来方向
|
|
||||||
|
|
||||||
<ArchitectureComparisonDemo />
|
**2000s → 2010s**:从"单机"到"分布式"。用户量爆炸式增长,单台服务器扛不住了,需要拆分系统,水平扩展。
|
||||||
|
|
||||||
|
**2010s → 2020s**:从"自己运维"到"云服务"。容器和微服务虽然强大,但运维成本太高,Serverless 让开发者只关注业务逻辑。
|
||||||
|
|
||||||
|
**核心启示**:架构演进不是技术选型的游戏,而是**解决实际问题**的过程。每个阶段都有其适用的场景,没有"最好的架构",只有"最适合的架构"。
|
||||||
|
:::
|
||||||
|
|
||||||
|
**了解架构演进的意义在于:**
|
||||||
|
|
||||||
|
1. **避免重复造轮子**:很多"新"概念其实早在几十年前就有雏形,了解历史能让你站在巨人的肩膀上
|
||||||
|
2. **做出合理的技术选型**:没有最好的架构,只有最适合当前阶段的架构
|
||||||
|
3. **理解技术背后的权衡**:每一次架构演进都是在**开发效率**、**系统性能**、**运维复杂度**之间做取舍
|
||||||
|
4. **预判技术趋势**:历史总是押韵的,理解过去的演进规律有助于把握未来方向
|
||||||
|
|
||||||
|
<EvolutionIntroDemo />
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 物理服务器时代 (1990s)
|
## 2. 物理服务器时代 (1990s)
|
||||||
|
|
||||||
在互联网刚起步时,后端就是一台放在机房里的**物理服务器**(一台真实的电脑)。
|
### 2.1 什么是物理服务器?
|
||||||
|
|
||||||
|
在互联网刚起步时,后端就是一台放在机房里的**物理服务器**(一台真实的电脑)。
|
||||||
|
|
||||||
|
::: tip 💡 通俗解释
|
||||||
|
**物理服务器**就像你家里的台式机,但它:
|
||||||
|
|
||||||
|
- 7×24小时不关机
|
||||||
|
- 放在专门的数据中心(有空调、UPS电源、消防系统)
|
||||||
|
- 有更快的网络带宽(企业级光纤)
|
||||||
|
- 有固定的公网IP地址(全世界都能访问)
|
||||||
|
|
||||||
|
这就好比你家 vs 餐厅:你家只是偶尔做饭,餐厅则是专业厨房,全天候营业,设备更专业。
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 2.2 核心特点
|
||||||
|
|
||||||
|
- **单机部署**:所有应用运行在一台物理机上
|
||||||
|
- **手动运维**:需要人工上架、布线、安装系统
|
||||||
|
- **垂直扩展**:性能不够时只能买更强的机器
|
||||||
|
|
||||||
|
::: details 🔧 垂直扩展 vs 水平扩展
|
||||||
|
**垂直扩展**(Scale Up):升级单台服务器的配置(更多CPU、更大内存、更快硬盘)。
|
||||||
|
|
||||||
|
**水平扩展**(Scale Out):增加更多服务器,让它们一起工作。
|
||||||
|
|
||||||
|
**比喻**:
|
||||||
|
|
||||||
|
- 垂直扩展:把小餐厅改成大餐厅,装修更豪华,但只有一个厨师
|
||||||
|
- 水平扩展:开连锁店,每个店规模不大,但有100家分店
|
||||||
|
|
||||||
|
**优缺点**:
|
||||||
|
|
||||||
|
- 垂直扩展简单,但有上限(顶级服务器很贵,且有限制)
|
||||||
|
- 水平扩展理论上无限,但需要解决数据一致性问题
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 2.3 痛点
|
||||||
|
|
||||||
|
- **慢**:每次改代码都要手动上传,然后重启服务器
|
||||||
|
- **贵**:扩容只能买更大的机器(垂直扩展)
|
||||||
|
- **难扩展**:一台机器顶住所有请求,CPU满载时就只能排队
|
||||||
|
|
||||||
<PhysicalServerDemo />
|
<PhysicalServerDemo />
|
||||||
|
|
||||||
### 1.1 核心特点
|
### 2.4 物理服务器时代的优缺点
|
||||||
|
|
||||||
- **单机部署**:所有应用运行在一台物理机上
|
|
||||||
- **手动运维**:需要人工上架、布线、安装系统
|
|
||||||
- **垂直扩展**:性能不够时只能买更强的机器
|
|
||||||
|
|
||||||
### 1.2 痛点
|
|
||||||
|
|
||||||
- **慢**:每次改代码都要手动上传,然后重启服务器
|
|
||||||
- **贵**:扩容只能买更大的机器(垂直扩展)
|
|
||||||
- **难扩展**:一台机器顶住所有请求,CPU 满载时就只能排队
|
|
||||||
|
|
||||||
### 1.3 扩展策略
|
|
||||||
|
|
||||||
<ScalingStrategyDemo />
|
|
||||||
|
|
||||||
### 1.4 物理服务器时代的优缺点
|
|
||||||
|
|
||||||
| 维度 | 评价 |
|
| 维度 | 评价 |
|
||||||
|------|------|
|
| ------------ | ------------------------------------------------------------ |
|
||||||
| **优点** | 完全掌控硬件,性能可预测;没有虚拟化开销;数据物理隔离,安全性高 |
|
| **优点** | 完全掌控硬件,性能可预测;没有虚拟化开销;数据物理隔离,安全性高 |
|
||||||
| **缺点** | 采购周期长(数周);前期投入大(CapEx);资源利用率低;扩容困难 |
|
| **缺点** | 采购周期长(数周);前期投入大(CapEx);资源利用率低;扩容困难 |
|
||||||
| **适用场景** | 金融核心系统、政府涉密系统、对数据主权有严格要求的场景 |
|
| **适用场景** | 金融核心系统、政府涉密系统、对数据主权有严格要求的场景 |
|
||||||
|
|
||||||
|
::: tip 💡 CapEx vs OpEx
|
||||||
|
**CapEx**(Capital Expenditure):资本性支出,一次性投入大量资金购买硬件。
|
||||||
|
|
||||||
|
**OpEx**(Operating Expenditure):运营性支出,按使用量付费(如云服务器)。
|
||||||
|
|
||||||
|
**比喻**:
|
||||||
|
|
||||||
|
- CapEx:买房,一次性付几百万,之后每月只需交物业费
|
||||||
|
- OpEx:租房,每月交房租,不用一次性掏大钱
|
||||||
|
|
||||||
|
**云时代**的启示:Serverless 和云服务让更多公司从 CapEx 转向 OpEx,降低创业门槛。
|
||||||
|
:::
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 单体架构时代 (2000s)
|
## 3. 单体架构时代 (2000s)
|
||||||
|
|
||||||
随着框架的出现(Rails / Django / Spring),大家把所有功能都塞进一个应用里。
|
### 3.1 什么是单体架构?
|
||||||
|
|
||||||
|
随着框架的出现(Rails / Django / Spring),大家把所有功能都塞进一个应用里。
|
||||||
|
|
||||||
|
::: tip 💡 通俗解释
|
||||||
|
**单体架构**(Monolith)就像一个超级商场:
|
||||||
|
|
||||||
|
- 服装区、食品区、电器区都在同一栋楼里
|
||||||
|
- 所有员工在一个管理系统里工作
|
||||||
|
- 如果整栋楼停电,所有区域都停止营业
|
||||||
|
|
||||||
|
对比微服务就像商业街:每家店独立运营,一家店关门不影响其他店。
|
||||||
|
:::
|
||||||
|
|
||||||
<MonolithDemo />
|
<MonolithDemo />
|
||||||
|
|
||||||
### 2.1 核心特点
|
### 3.2 核心特点
|
||||||
|
|
||||||
- **单一代码库**:所有功能模块在同一个项目中
|
- **单一代码库**:所有功能模块在同一个项目中
|
||||||
- **共享数据库**:所有模块共用同一个数据库
|
- **共享数据库**:所有模块共用同一个数据库
|
||||||
- **统一部署**:整个应用作为一个整体打包部署
|
- **统一部署**:整个应用作为一个整体打包部署
|
||||||
|
|
||||||
### 2.2 优点
|
### 3.3 优点
|
||||||
|
|
||||||
- **开发简单**:一个项目搞定所有功能
|
- **开发简单**:一个项目搞定所有功能
|
||||||
- **部署方便**:把一个大包扔到服务器上就行
|
- **部署方便**:把一个大包扔到服务器上就行
|
||||||
- **调试容易**:本地启动就能调试所有功能
|
- **调试容易**:本地启动就能调试所有功能
|
||||||
|
|
||||||
### 2.3 痛点:雪崩效应
|
### 3.4 痛点:雪崩效应
|
||||||
|
|
||||||
想象一下,如果"切菜"的师傅不小心切到了手(代码出了 Bug),整个后厨都要停下来处理伤口,导致所有客人都吃不上饭。
|
想象一下,如果"切菜"的师傅不小心切到了手(代码出了Bug),整个后厨都要停下来处理伤口,导致所有客人都吃不上饭。
|
||||||
|
|
||||||
这就是单体架构最大的风险:**隔离性差**。
|
这就是单体架构最大的风险:**隔离性差**。
|
||||||
|
|
||||||
### 2.4 单体架构的优缺点与适用场景
|
::: details 🚨 真实的雪崩案例
|
||||||
|
某电商公司双十一大促:
|
||||||
|
|
||||||
|
- 订单服务因为某个商品的价格计算错误,抛出异常
|
||||||
|
- 异常没有被正确捕获,导致线程池耗尽
|
||||||
|
- 所有后续请求(包括商品浏览、搜索、用户登录)都被阻塞
|
||||||
|
- 整个网站彻底瘫痪,持续1小时
|
||||||
|
|
||||||
|
**如果用微服务**:
|
||||||
|
|
||||||
|
- 订单服务挂了,但商品浏览、搜索、用户登录仍然可用
|
||||||
|
- 用户至少可以继续浏览商品,损失降到最低
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 3.5 单体架构的优缺点与适用场景
|
||||||
|
|
||||||
| 维度 | 评价 |
|
| 维度 | 评价 |
|
||||||
|------|------|
|
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **优点** | 开发简单,无需考虑分布式复杂性;调试方便,本地启动即可调试全功能;部署简单,一个包即可运行;事务管理容易,单机数据库即可保证 ACID |
|
| **优点** | 开发简单,无需考虑分布式复杂性;调试方便,本地启动即可调试全功能;部署简单,一个包即可运行;事务管理容易,单机数据库即可保证ACID |
|
||||||
| **缺点** | 代码耦合度高,随着业务增长代码膨胀;技术栈单一,难以局部升级;扩展困难,只能整体扩容;故障隔离差,一个模块故障影响全局;团队协作效率低,多人改同一套代码 |
|
| **缺点** | 代码耦合度高,随着业务增长代码膨胀;技术栈单一,难以局部升级;扩展困难,只能整体扩容;故障隔离差,一个模块故障影响全局;团队协作效率低,多人改同一套代码 |
|
||||||
| **适用场景** | 初创公司 MVP 验证、小型团队(<10人)、业务相对简单、对交付速度要求高于扩展性的场景 |
|
| **适用场景** | 初创公司MVP验证、小型团队(<10人)、业务相对简单、对交付速度要求高于扩展性的场景 |
|
||||||
| **不适用场景** | 大型团队并行开发、需要频繁发布不同模块、某些模块需要独立扩容的场景 |
|
| **不适用场景** | 大型团队并行开发、需要频繁发布不同模块、某些模块需要独立扩容的场景 |
|
||||||
|
|
||||||
### 2.5 部署流程演进
|
::: tip 🎯 初学者建议
|
||||||
|
如果你正在学习后端开发,**强烈建议从单体架构开始**:
|
||||||
|
|
||||||
<DeploymentFlowDemo />
|
1. **先学会走路**:理解HTTP、数据库、基本的MVC架构
|
||||||
|
2. **再考虑跑步**:当项目真的遇到扩展性问题,再考虑微服务
|
||||||
|
3. **避免过度设计**:很多公司的"微服务"其实是"分布式单体",更难维护
|
||||||
|
|
||||||
### 2.6 单体架构的技术栈
|
**学习路径**:
|
||||||
|
|
||||||
在单体架构时代,开发者通常使用以下技术栈:
|
- 阶段1:用 Spring Boot / Django / Rails 写一个完整的单体应用
|
||||||
|
- 阶段2:遇到性能瓶颈时,尝试拆分出1-2个服务
|
||||||
|
- 阶段3:当团队规模>50人,系统真的复杂了,再全面微服务化
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 3.6 单体架构的技术栈
|
||||||
|
|
||||||
| 语言/框架 | 特点 | 代表企业 |
|
| 语言/框架 | 特点 | 代表企业 |
|
||||||
|---------|------|---------|
|
| -------------------------- | ---------------------------- | --------------------- |
|
||||||
| **Java + Spring** | 企业级开发首选,生态完善 | 阿里巴巴、京东 |
|
| **Java + Spring** | 企业级开发首选,生态完善 | 阿里巴巴、京东 |
|
||||||
| **PHP + Laravel/ThinkPHP** | 快速开发,适合中小型项目 | 早期 Facebook、微博 |
|
| **PHP + Laravel/ThinkPHP** | 快速开发,适合中小型项目 | 早期 Facebook、微博 |
|
||||||
| **Python + Django/Flask** | 开发效率高,适合快速原型 | Instagram、Pinterest |
|
| **Python + Django/Flask** | 开发效率高,适合快速原型 | Instagram、Pinterest |
|
||||||
| **Ruby on Rails** | 约定优于配置,初创公司最爱 | GitHub、Twitter(早期) |
|
| **Ruby on Rails** | 约定优于配置,初创公司最爱 | GitHub、Twitter(早期) |
|
||||||
| **Node.js + Express** | 前后端统一语言,I/O 密集型场景 | Netflix、Uber |
|
| **Node.js + Express** | 前后端统一语言,I/O密集型场景 | Netflix、Uber |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 容器化与微服务 (2010s)
|
## 4. 容器化与微服务 (2010s)
|
||||||
|
|
||||||
### 3.1 Docker 容器化
|
### 4.1 为什么需要微服务?
|
||||||
|
|
||||||
|
单体架构的痛点在2010年代集中爆发:
|
||||||
|
|
||||||
|
- **代码太庞大**:一个项目几百万行代码,新人入职要花一个月才能看懂
|
||||||
|
- **部署太慢**:构建一次要30分钟,发布一次要小心翼翼
|
||||||
|
- **协作太难**:100个开发者改同一个项目,代码冲突每天发生
|
||||||
|
- **扩展太贵**:只需要扩展"聊天服务",却要复制整个应用
|
||||||
|
|
||||||
|
**微服务的核心思想**:把大应用拆成多个小服务,每个服务:
|
||||||
|
|
||||||
|
- 独立开发、独立部署
|
||||||
|
- 有自己的数据库
|
||||||
|
- 通过API通信
|
||||||
|
|
||||||
<ContainerDockerDemo />
|
<ContainerDockerDemo />
|
||||||
|
|
||||||
Docker 就像是**集装箱**,它把每个小服务连同它的锅碗瓢盆(依赖库)一起打包。
|
::: tip 💡 Docker是什么?
|
||||||
|
**Docker**就像是"集装箱":
|
||||||
|
|
||||||
无论运到哪里(哪台服务器),打开集装箱就能直接开工,不用再重新安装环境。
|
- 每个集装箱里有独立的货物(代码 + 依赖库 + 运行环境)
|
||||||
|
- 无论运到哪里(哪台服务器),打开集装箱就能直接开工
|
||||||
|
- 不用担心"我这台机器没有Python 3.9"、"那个机器缺少某个库"
|
||||||
|
|
||||||
### 3.2 技术栈时间线
|
**比喻**:
|
||||||
|
|
||||||
|
- 没有 Docker:每次搬家,要把家具、电器、衣服一件件搬上卡车,到了新家再一件件摆好
|
||||||
|
- 有 Docker:所有东西打包进集装箱,卡车直接运走,到了新家放下就能用
|
||||||
|
|
||||||
|
**核心价值**:"一次构建,到处运行"。
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 4.2 技术栈时间线
|
||||||
|
|
||||||
<TechStackTimelineDemo />
|
<TechStackTimelineDemo />
|
||||||
|
|
||||||
### 3.3 微服务架构
|
### 4.3 微服务架构
|
||||||
|
|
||||||
<MicroservicesDemo />
|
为了解决单体的问题,我们把大厨房拆成了很多个小厨房(服务):
|
||||||
|
|
||||||
为了解决单体的问题,我们把大厨房拆成了很多个小厨房(服务):
|
|
||||||
|
|
||||||
- 专门负责用户的服务
|
- 专门负责用户的服务
|
||||||
- 专门负责订单的服务
|
- 专门负责订单的服务
|
||||||
- 专门负责支付的服务
|
- 专门负责支付的服务
|
||||||
|
|
||||||
### 3.4 Kubernetes 编排
|
<MicroservicesDemo />
|
||||||
|
|
||||||
|
### 4.4 Kubernetes 编排
|
||||||
|
|
||||||
|
当集装箱数量到达成百上千,就需要一个"港口调度系统":
|
||||||
|
|
||||||
|
- **Kubernetes (K8s)**:负责把容器安排到合适的机器上(调度、扩缩容、滚动更新)
|
||||||
|
- **Service Mesh**:负责服务之间的交通规则(熔断、限流、重试、可观测)
|
||||||
|
|
||||||
<KubernetesDemo />
|
<KubernetesDemo />
|
||||||
|
|
||||||
当集装箱数量到达成百上千,就需要一个"港口调度系统":
|
::: tip 💡 什么是"编排"?
|
||||||
|
**编排**(Orchestration)是指自动管理大量容器的系统。
|
||||||
|
|
||||||
- **Kubernetes (K8s)**:负责把容器安排到合适的机器上(调度、扩缩容、滚动更新)
|
**比喻**:
|
||||||
- **Service Mesh**:负责服务之间的交通规则(熔断、限流、重试、可观测)
|
|
||||||
|
|
||||||
**关键点**:微服务不是"拆开就好",真正的难点在于**治理和运维**。
|
- 没有 K8s:你手动管理100个容器,哪个挂了要手动重启,哪个流量大了要手动加机器
|
||||||
|
- 有 K8s:你告诉它"我要这个服务一直有10个实例运行",它会自动完成:
|
||||||
|
- 哪台服务器资源充足,就把容器调度到那里
|
||||||
|
- 容器挂了,自动重启
|
||||||
|
- 流量大了,自动扩容到20个实例
|
||||||
|
- 更新代码时,滚动更新(先停1个旧实例,启动1个新实例,逐个替换)
|
||||||
|
|
||||||
### 3.5 微服务与容器化的优缺点
|
**关键点**:微服务不是"拆开就好",真正的难点在于**治理和运维**。
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 4.5 微服务与容器化的优缺点
|
||||||
|
|
||||||
| 维度 | 评价 |
|
| 维度 | 评价 |
|
||||||
|------|------|
|
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **优点** | 服务独立部署,技术栈可异构;故障隔离,单个服务崩溃不影响全局;按需扩展,热点服务单独扩容;团队协作友好,不同团队负责不同服务;代码库更小,易于理解和维护 |
|
| **优点** | 服务独立部署,技术栈可异构;故障隔离,单个服务崩溃不影响全局;按需扩展,热点服务单独扩容;团队协作友好,不同团队负责不同服务;代码库更小,易于理解和维护 |
|
||||||
| **缺点** | 分布式复杂性高(网络延迟、分布式事务、服务发现);运维成本高,需要专业的 DevOps 团队;调试困难,问题可能需要跨多个服务追踪;数据一致性难以保证;部署和监控基础设施要求复杂 |
|
| **缺点** | 分布式复杂性高(网络延迟、分布式事务、服务发现);运维成本高,需要专业的DevOps团队;调试困难,问题可能需要跨多个服务追踪;数据一致性难以保证;部署和监控基础设施要求复杂 |
|
||||||
| **适用场景** | 大型团队(>50人)、业务复杂需要分模块独立演进、某些模块需要独立扩容、需要多语言技术栈、对可用性要求高的系统 |
|
| **适用场景** | 大型团队(>50人)、业务复杂需要分模块独立演进、某些模块需要独立扩容、需要多语言技术栈、对可用性要求高的系统 |
|
||||||
| **不适用场景** | 小型团队、业务简单、流量小且稳定、没有专业运维团队的情况 |
|
| **不适用场景** | 小型团队、业务简单、流量小且稳定、没有专业运维团队的情况 |
|
||||||
|
|
||||||
### 3.6 微服务技术栈
|
::: details ⚠️ 微服务的陷阱
|
||||||
|
**陷阱1:分布式单体**
|
||||||
|
|
||||||
|
拆了10个微服务,但它们之间紧密耦合:
|
||||||
|
|
||||||
|
- 服务A调用服务B,服务B调用服务C,服务C又调用服务A
|
||||||
|
- 改一个功能,要同时改5个服务
|
||||||
|
- 部署时,必须按顺序依次部署,否则系统报错
|
||||||
|
|
||||||
|
**这比单体更糟糕**:你拥有了单体的复杂性,又没有享受到微服务的独立部署好处。
|
||||||
|
|
||||||
|
**陷阱2:过度拆分**
|
||||||
|
|
||||||
|
把只有100行代码的功能也拆成一个独立服务:
|
||||||
|
|
||||||
|
- 10个服务,每个只有100行代码
|
||||||
|
- 服务间通信的开销(网络序列化/反序列化)比实际业务逻辑还重
|
||||||
|
- 运维成本爆炸:要部署、监控、日志收集10个服务
|
||||||
|
|
||||||
|
**正确做法**:从功能内聚的角度拆分,一个微服务应该是一个完整的业务能力(如"订单服务",而不是"订单创建服务"、"订单查询服务")。
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 4.6 微服务技术栈
|
||||||
|
|
||||||
| 类别 | 技术/工具 | 作用 |
|
| 类别 | 技术/工具 | 作用 |
|
||||||
|------|---------|------|
|
| ------------ | ---------------------------------- | -------------------- |
|
||||||
| **容器化** | Docker, containerd | 应用打包与隔离 |
|
| **容器化** | Docker, containerd | 应用打包与隔离 |
|
||||||
| **编排调度** | Kubernetes, Docker Swarm | 容器管理与自动扩缩容 |
|
| **编排调度** | Kubernetes, Docker Swarm | 容器管理与自动扩缩容 |
|
||||||
| **服务发现** | Consul, etcd, ZooKeeper | 服务注册与发现 |
|
| **服务发现** | Consul, etcd, ZooKeeper | 服务注册与发现 |
|
||||||
@@ -174,79 +317,124 @@ Docker 就像是**集装箱**,它把每个小服务连同它的锅碗瓢盆(
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Serverless 与云原生时代 (2020s+)
|
## 5. Serverless 与云原生时代 (2020s+)
|
||||||
|
|
||||||
微服务虽然好,但维护几十个小厨房还是很累。你需要担心:
|
### 5.1 为什么需要 Serverless?
|
||||||
|
|
||||||
- 厨房够不够大?(服务器扩容)
|
微服务虽然好,但维护几十个小厨房还是很累。你需要担心:
|
||||||
- 停电了怎么办?(高可用)
|
|
||||||
- 容器太多怎么管?(运维成本)
|
- 厨房够不够大?(服务器扩容)
|
||||||
|
- 停电了怎么办?(高可用)
|
||||||
|
- 容器太多怎么管?(运维成本)
|
||||||
|
|
||||||
<ServerlessDemo />
|
<ServerlessDemo />
|
||||||
|
|
||||||
### 4.1 什么是 Serverless?
|
::: tip 💡 Serverless 不是真的"没有服务器"
|
||||||
|
**Serverless**的意思是"你不需要管理服务器",而不是真的没有服务器。
|
||||||
|
|
||||||
Serverless 并不是"没有服务器",而是**"你不需要管理服务器"**。
|
**比喻**:
|
||||||
|
|
||||||
就像你现在不想自己做饭,也不想开饭馆,而是直接叫**外卖**。
|
- **物理服务器时代**:你买地、盖房、装修、雇厨师、买食材...全部自己来
|
||||||
|
- **云服务器时代**:你租一个已经装修好的餐厅,但自己雇厨师、管理运营
|
||||||
|
- **Serverless时代**:你只需要设计菜单,云端有共享厨房,有专业厨师,你下单他们做,按次付费
|
||||||
|
|
||||||
- 你只需要写代码(下单)
|
**核心变化**:
|
||||||
- 云厂商(美团)负责准备机器、运行代码、自动扩容
|
|
||||||
- **按次付费**:代码跑了 100 毫秒,就收 100 毫秒的钱。没人访问就不收钱
|
|
||||||
|
|
||||||
### 4.2 适用场景
|
- 以前:买服务器 → 配环境 → 部署代码 → 监控 → 扩容 → 维护
|
||||||
|
- 现在:写代码 → 上传 → 按使用量付费
|
||||||
|
|
||||||
Serverless 特别适合:
|
**就像外卖**:你不需要厨房,只需要设计菜单,有人帮你做。
|
||||||
|
:::
|
||||||
|
|
||||||
- **潮汐流量**:比如外卖软件,中午流量大,半夜没人。Serverless 会自动在中午为你分配 1000 台机器,半夜缩减到 0 台
|
### 5.2 什么是 Serverless?
|
||||||
- **事件驱动**:比如"用户上传图片后,自动压缩图片"
|
|
||||||
- **快速验证**:小团队、MVP、黑客松项目
|
|
||||||
|
|
||||||
### 4.3 BaaS 组合拳
|
**Serverless = FaaS + BaaS**
|
||||||
|
|
||||||
Serverless 的真正力量来自于 **BaaS (Backend as a Service)**:
|
**FaaS**(Function as a Service,函数即服务):
|
||||||
|
|
||||||
- 登录 -> Auth0 / Supabase Auth
|
- 你只写函数(如"用户注册时发送欢迎邮件")
|
||||||
- 支付 -> Stripe
|
- 云厂商负责运行这个函数,自动扩缩容
|
||||||
- 数据库 -> Supabase / Firebase / DynamoDB
|
- 典型代表:AWS Lambda、阿里云函数计算
|
||||||
- 消息 -> Kafka / SQS
|
|
||||||
|
|
||||||
**关键点**:Serverless 让后端越来越像"搭积木"。
|
**BaaS**(Backend as a Service,后端即服务):
|
||||||
|
|
||||||
### 4.4 Serverless 与云原生的优缺点
|
- 登录 → Auth0 / Supabase Auth
|
||||||
|
- 支付 → Stripe
|
||||||
|
- 数据库 → Supabase / Firebase / DynamoDB
|
||||||
|
- 消息 → Kafka / SQS
|
||||||
|
|
||||||
|
::: tip 🎯 Serverless 适用场景
|
||||||
|
**最佳场景**:
|
||||||
|
|
||||||
|
1. **潮汐流量**:外卖软件,中午流量大,半夜没人。Serverless会自动在中午分配1000台机器,半夜缩减到0台
|
||||||
|
2. **事件驱动**:"用户上传图片后,自动压缩图片"
|
||||||
|
3. **快速验证**:小团队、MVP、黑客松项目
|
||||||
|
|
||||||
|
**不适合场景**:
|
||||||
|
|
||||||
|
1. **长时间运行的任务**:视频转码(可能跑1小时,函数最大执行时间通常只有15分钟)
|
||||||
|
2. **需要低延迟的应用**:高频交易(冷启动延迟可能几十毫秒到几秒)
|
||||||
|
3. **需要精细控制底层**:操作系统内核调优、GPU直接访问
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 5.3 Serverless 与云原生的优缺点
|
||||||
|
|
||||||
| 维度 | 评价 |
|
| 维度 | 评价 |
|
||||||
|------|------|
|
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **优点** | 零运维成本,开发者只需关注业务代码;自动扩缩容,完美应对流量峰值;按需付费,无流量时成本接近零;快速上线,几分钟即可部署全球;高可用内置,云服务自动处理故障转移 |
|
| **优点** | 零运维成本,开发者只需关注业务代码;自动扩缩容,完美应对流量峰值;按需付费,无流量时成本接近零;快速上线,几分钟即可部署全球;高可用内置,云服务自动处理故障转移 |
|
||||||
| **缺点** | 冷启动延迟(几百毫秒到数秒);运行时长限制(通常5-15分钟);调试困难,本地难以完全模拟云环境;供应商锁定风险;不适合长时间运行或计算密集型任务;成本在高频持续流量下可能反超传统方案 |
|
| **缺点** | 冷启动延迟(几百毫秒到数秒);运行时长限制(通常5-15分钟);调试困难,本地难以完全模拟云环境;供应商锁定风险;不适合长时间运行或计算密集型任务;成本在高频持续流量下可能反超传统方案 |
|
||||||
| **适用场景** | 事件驱动处理(图片处理、消息通知);潮汐流量应用(活动页、促销);快速原型验证和MVP;低频API或后台任务;无专职运维团队的小团队 |
|
| **适用场景** | 事件驱动处理(图片处理、消息通知);潮汐流量应用(活动页、促销);快速原型验证和MVP;低频API或后台任务;无专职运维团队的小团队 |
|
||||||
| **不适用场景** | 需要持续低延迟的应用;长时间计算任务;对冷启动敏感的场景(高频交易);需要精细控制底层基础设施的场景 |
|
| **不适用场景** | 需要持续低延迟的应用;长时间计算任务;对冷启动敏感的场景(高频交易);需要精细控制底层基础设施的场景 |
|
||||||
|
|
||||||
### 4.5 Serverless 技术栈与平台
|
::: 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 服务,生态最成熟 |
|
| **FaaS平台** | AWS Lambda | 最早的FaaS服务,生态最成熟 |
|
||||||
| | Azure Functions | 微软云集成度高,.NET 友好 |
|
| | Azure Functions | 微软云集成度高,.NET友好 |
|
||||||
| | Google Cloud Functions | 与GCP服务深度集成 |
|
| | Google Cloud Functions | 与GCP服务深度集成 |
|
||||||
| | 阿里云函数计算 | 国内生态完善,冷启动优化好 |
|
| | 阿里云函数计算 | 国内生态完善,冷启动优化好 |
|
||||||
| | 腾讯云云函数 | 与微信生态整合 |
|
| | 腾讯云云函数 | 与微信生态整合 |
|
||||||
| | Vercel/Netlify Functions | 前端开发者友好,边缘部署 |
|
| | Vercel/Netlify Functions | 前端开发者友好,边缘部署 |
|
||||||
| **BaaS服务** | Firebase | Google的移动端后端方案 |
|
| **BaaS服务** | Firebase | Google的移动端后端方案 |
|
||||||
| | Supabase | PostgreSQL的Firebase开源替代 |
|
| | Supabase | PostgreSQL的Firebase开源替代 |
|
||||||
| | AWS Amplify | AWS的移动和Web应用开发平台 |
|
| | AWS Amplify | AWS的移动和Web应用开发平台 |
|
||||||
| **部署工具** | Serverless Framework | 多云部署,社区活跃 |
|
| **部署工具** | Serverless Framework | 多云部署,社区活跃 |
|
||||||
| | Terraform | 基础设施即代码 |
|
| | Terraform | 基础设施即代码 |
|
||||||
| | Pulumi | 用编程语言定义基础设施 |
|
| | Pulumi | 用编程语言定义基础设施 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 各架构阶段对比与选型指南
|
## 6. 各架构阶段对比与选型指南
|
||||||
|
|
||||||
### 5.1 架构演进全景对比
|
### 6.1 架构演进全景对比
|
||||||
|
|
||||||
|
<ArchitectureComparisonDemo />
|
||||||
|
|
||||||
| 维度 | 物理服务器 | 单体架构 | 微服务+容器 | Serverless |
|
| 维度 | 物理服务器 | 单体架构 | 微服务+容器 | Serverless |
|
||||||
|------|-----------|---------|------------|-------------|
|
| ---------------- | ---------------------- | ------------------ | ------------------------ | ------------------ |
|
||||||
| **团队规模** | 1-5人 | 5-50人 | 50-500人 | 1-20人 |
|
| **团队规模** | 1-5人 | 5-50人 | 50-500人 | 1-20人 |
|
||||||
| **部署复杂度** | 极高 | 低 | 极高 | 极低 |
|
| **部署复杂度** | 极高 | 低 | 极高 | 极低 |
|
||||||
| **运维成本** | 高 | 中 | 很高 | 低 |
|
| **运维成本** | 高 | 中 | 很高 | 低 |
|
||||||
@@ -255,150 +443,161 @@ Serverless 的真正力量来自于 **BaaS (Backend as a Service)**:
|
|||||||
| **冷启动** | 无 | 无 | 容器启动时间 | 有延迟 |
|
| **冷启动** | 无 | 无 | 容器启动时间 | 有延迟 |
|
||||||
| **适用场景** | 遗留系统、特殊合规要求 | 初创公司、业务简单 | 大型互联网公司、复杂业务 | 快速验证、事件驱动 |
|
| **适用场景** | 遗留系统、特殊合规要求 | 初创公司、业务简单 | 大型互联网公司、复杂业务 | 快速验证、事件驱动 |
|
||||||
|
|
||||||
### 5.2 技术选型决策树
|
### 6.2 技术选型决策树
|
||||||
|
|
||||||
```
|
```
|
||||||
开始选型
|
开始选型
|
||||||
│
|
│
|
||||||
├─ 团队有专业运维人员?
|
├─ 团队有专业运维人员?
|
||||||
│ ├─ 是 → 考虑微服务或物理机
|
│ ├─ 是 → 考虑微服务或物理机
|
||||||
│ └─ 否 → 继续判断
|
│ └─ 否 → 继续判断
|
||||||
│
|
│
|
||||||
├─ 需要快速上线验证想法?
|
├─ 需要快速上线验证想法?
|
||||||
│ ├─ 是 → Serverless 或单体
|
│ ├─ 是 → Serverless 或单体
|
||||||
│ └─ 否 → 继续判断
|
│ └─ 否 → 继续判断
|
||||||
│
|
│
|
||||||
├─ 团队规模 > 50人?
|
├─ 团队规模 > 50人?
|
||||||
│ ├─ 是 → 考虑微服务
|
│ ├─ 是 → 考虑微服务
|
||||||
│ └─ 否 → 继续判断
|
│ └─ 否 → 继续判断
|
||||||
│
|
│
|
||||||
├─ 流量有明显峰谷特征?
|
├─ 流量有明显峰谷特征?
|
||||||
│ ├─ 是 → Serverless
|
│ ├─ 是 → Serverless
|
||||||
│ └─ 否 → 单体架构(推荐初创)
|
│ └─ 否 → 单体架构(推荐初创)
|
||||||
│
|
│
|
||||||
└─ 特殊要求(合规、遗留系统)?
|
└─ 特殊要求(合规、遗留系统)?
|
||||||
└─ 是 → 物理服务器
|
└─ 是 → 物理服务器
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.3 不同场景下的推荐架构
|
::: tip 🎯 初学者选型建议
|
||||||
|
**如果你是个开发者或小团队:**
|
||||||
|
|
||||||
#### 场景一:独立开发者/兼职项目
|
1. **阶段0 (学习)**:本地跑单体应用,理解HTTP、数据库、基本架构
|
||||||
- **推荐架构**:Serverless (Vercel/Netlify) 或 单体应用
|
2. **阶段1 (MVP)**:部署单体应用到云服务器(如阿里云ECS、AWS EC2)
|
||||||
- **理由**:几乎零运维成本,按需付费,快速上线
|
3. **阶段2 (增长)**:当团队>10人、业务变复杂,考虑拆分出1-2个微服务
|
||||||
- **示例技术栈**:Next.js + Vercel + Supabase
|
4. **阶段3 (成熟)**:当团队>50人、流量百万级,全面微服务化
|
||||||
|
|
||||||
#### 场景二:初创公司 MVP 验证
|
**关键原则**:不要一开始就上微服务,那是"过早优化"。让架构随业务成长而演进。
|
||||||
- **推荐架构**:单体架构 + 云服务器
|
:::
|
||||||
- **理由**:开发速度快,团队可以专注于业务逻辑而非基础设施
|
|
||||||
- **示例技术栈**:Spring Boot / Django / Rails + RDS + ECS
|
|
||||||
|
|
||||||
#### 场景三:成长型公司(10-50人团队)
|
### 6.3 不同场景下的推荐架构
|
||||||
- **推荐架构**:模块化单体 或 轻量级微服务
|
|
||||||
- **理由**:开始面临代码耦合问题,但还不需要完整的微服务复杂度
|
|
||||||
- **示例技术栈**:Spring Cloud / Go Micro + Kubernetes
|
|
||||||
|
|
||||||
#### 场景四:大型互联网公司
|
#### 场景一:独立开发者/兼职项目
|
||||||
- **推荐架构**:微服务 + 服务网格 + 中台架构
|
|
||||||
- **理由**:团队规模大,业务复杂,需要独立的发布节奏和技术栈
|
|
||||||
- **示例技术栈**:自研 RPC 框架 + Istio + 自建 PaaS 平台
|
|
||||||
|
|
||||||
#### 场景五:事件驱动/潮汐流量应用
|
- **推荐架构**:Serverless (Vercel/Netlify) 或 单体应用
|
||||||
- **推荐架构**:Serverless + 事件总线
|
- **理由**:几乎零运维成本,按需付费,快速上线
|
||||||
- **理由**:流量波动大,需要极致的成本优化和自动扩缩容
|
- **示例技术栈**:Next.js + Vercel + Supabase
|
||||||
- **示例技术栈**:AWS Lambda + API Gateway + EventBridge
|
|
||||||
|
#### 场景二:初创公司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集群(很累!) |
|
| **微服务时代** | 拆分 | 关注单一业务 | 维护K8s集群(很累!) |
|
||||||
| **Serverless** | 函数 | 只写核心函数 | 喝茶(云厂商全包了) |
|
| **Serverless** | 函数 | 只写核心函数 | 喝茶(云厂商全包了) |
|
||||||
|
|
||||||
**下一步建议**:
|
**关键洞察**:
|
||||||
|
|
||||||
- 想打基础:学会 HTTP、数据库、缓存、消息队列
|
- 架构演进不是"新技术取代旧技术",而是**适用场景的变化**
|
||||||
- 想上手实践:用 Docker 跑一个小项目,再部署到云端
|
- 没有银弹,每个架构都有其适用的边界
|
||||||
- 想更专业:了解 K8s、监控体系、CI/CD 流水线
|
- 选择架构要考虑:团队规模、业务复杂度、流量特征、运维能力
|
||||||
|
|
||||||
未来的后端开发,将越来越像"搭积木"——你只需要关注**业务逻辑**,底层的脏活累活,全部交给云。
|
### 7.2 学习路线建议
|
||||||
|
|
||||||
### 6.2 学习路线建议
|
根据你的职业阶段,推荐以下学习路径:
|
||||||
|
|
||||||
根据你的职业阶段,推荐以下学习路径:
|
#### 阶段一:打好基础(0-1年)
|
||||||
|
|
||||||
#### 阶段一:打好基础(0-1年)
|
**目标**:理解后端核心概念,能独立开发单体应用
|
||||||
**目标**:理解后端核心概念,能独立开发单体应用
|
|
||||||
|
|
||||||
- 掌握一门后端语言(Java/Python/Go 任选其一)
|
- 掌握一门后端语言(Java/Python/Go任选其一)
|
||||||
- 学习HTTP协议和RESTful API设计
|
- 学习HTTP协议和RESTful API设计
|
||||||
- 掌握关系型数据库(MySQL/PostgreSQL)
|
- 掌握关系型数据库(MySQL/PostgreSQL)
|
||||||
- 了解缓存基础(Redis)
|
- 了解缓存基础(Redis)
|
||||||
- 学习Git和基础Linux命令
|
- 学习Git和基础Linux命令
|
||||||
- **实践项目**:用单体架构完成一个 CRUD 应用(如博客系统、待办事项)
|
- **实践项目**:用单体架构完成一个CRUD应用(如博客系统、待办事项)
|
||||||
|
|
||||||
#### 阶段二:扩展能力(1-3年)
|
#### 阶段二:扩展能力(1-3年)
|
||||||
**目标**:理解分布式系统,能参与微服务开发
|
|
||||||
|
**目标**:理解分布式系统,能参与微服务开发
|
||||||
|
|
||||||
- 深入学习微服务架构和拆分策略
|
- 深入学习微服务架构和拆分策略
|
||||||
- 掌握Docker和Kubernetes基础
|
- 掌握Docker和Kubernetes基础
|
||||||
- 学习消息队列(Kafka/RabbitMQ)
|
- 学习消息队列(Kafka/RabbitMQ)
|
||||||
- 了解分布式事务和一致性
|
- 了解分布式事务和一致性
|
||||||
- 掌握监控和日志(Prometheus/ELK)
|
- 掌握监控和日志(Prometheus/ELK)
|
||||||
- **实践项目**:将单体应用拆分为 3-5 个微服务,使用 Docker 部署
|
- **实践项目**:将单体应用拆分为3-5个微服务,使用Docker部署
|
||||||
|
|
||||||
#### 阶段三:专业深化(3-5年)
|
#### 阶段三:专业深化(3-5年)
|
||||||
**目标**:能设计大型系统,具备技术选型能力
|
|
||||||
|
|
||||||
- 深入理解云原生架构(Service Mesh、Serverless)
|
**目标**:能设计大型系统,具备技术选型能力
|
||||||
|
|
||||||
|
- 深入理解云原生架构(Service Mesh、Serverless)
|
||||||
- 掌握容量规划和性能调优
|
- 掌握容量规划和性能调优
|
||||||
- 了解多活架构和灾备设计
|
- 了解多活架构和灾备设计
|
||||||
- 学习 DDD(领域驱动设计)
|
- 学习DDD(领域驱动设计)
|
||||||
- 培养技术判断力和架构思维
|
- 培养技术判断力和架构思维
|
||||||
- **实践项目**:设计一个支持百万级用户的系统架构,包含高可用、弹性伸缩等方案
|
- **实践项目**:设计一个支持百万级用户的系统架构,包含高可用、弹性伸缩等方案
|
||||||
|
|
||||||
#### 持续学习资源推荐
|
### 7.3 持续学习资源推荐
|
||||||
|
|
||||||
**书籍**
|
**书籍**:
|
||||||
- 《设计数据密集型应用》(DDIA)- 分布式系统必读
|
|
||||||
|
- 《设计数据密集型应用》(DDIA)- 分布式系统必读
|
||||||
- 《云原生模式》
|
- 《云原生模式》
|
||||||
- 《微服务设计》
|
- 《微服务设计》
|
||||||
- 《领域驱动设计》
|
- 《领域驱动设计》
|
||||||
|
|
||||||
**在线资源**
|
**在线资源**:
|
||||||
|
|
||||||
- AWS/Azure/阿里云官方架构文档
|
- AWS/Azure/阿里云官方架构文档
|
||||||
- CNCF(云原生计算基金会)项目文档
|
- CNCF(云原生计算基金会)项目文档
|
||||||
- 各大公司技术博客(Netflix Tech Blog、阿里技术公众号等)
|
- 各大公司技术博客(Netflix Tech Blog、阿里技术公众号等)
|
||||||
|
|
||||||
### 6.3 架构选型的核心原则
|
|
||||||
|
|
||||||
记住以下原则,帮助你在实际工作中做出正确的选择:
|
|
||||||
|
|
||||||
1. **没有银弹**:不存在最好的架构,只有最适合当前场景的架构
|
|
||||||
2. **演进优于完美**:先让系统跑起来,再逐步优化,不要过度设计
|
|
||||||
3. **团队能力优先**:选择团队熟悉和能驾驭的技术,而不是最新最酷的技术
|
|
||||||
4. **成本意识**:计算总体拥有成本(TCO),包括开发、运维、培训等
|
|
||||||
5. **可回退性**:设计时考虑回退方案,微服务可以合并回单体,但很难拆分
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 名词速查表 (Glossary)
|
## 8. 名词速查表(Glossary)
|
||||||
|
|
||||||
| 名词 | 全称 | 解释 |
|
| 名词 | 全称 | 解释 |
|
||||||
| :---------------- | :-------------------------------- | :--------------------------------------------------- |
|
| :---------------- | :-------------------------------- | :------------------------------------------------ |
|
||||||
| **Backend** | - | 服务器端系统,负责处理业务逻辑、数据存储和对外接口 |
|
| **Backend** | - | 服务器端系统,负责处理业务逻辑、数据存储和对外接口 |
|
||||||
| **CGI** | Common Gateway Interface | 早期动态网页技术,通过脚本处理请求并返回结果 |
|
| **CGI** | Common Gateway Interface | 早期动态网页技术,通过脚本处理请求并返回结果 |
|
||||||
| **Monolith** | - | 单体架构,把所有业务逻辑打包在同一个应用中 |
|
| **Monolith** | - | 单体架构,把所有业务逻辑打包在同一个应用中 |
|
||||||
| **Microservices** | - | 微服务架构,把业务拆分成多个独立服务 |
|
| **Microservices** | - | 微服务架构,把业务拆分成多个独立服务 |
|
||||||
| **Container** | - | 容器化技术,把应用和依赖打包成可移植单元 |
|
| **Container** | - | 容器化技术,把应用和依赖打包成可移植单元 |
|
||||||
| **K8s** | Kubernetes | 容器编排平台,用于调度、扩缩容和治理容器 |
|
| **K8s** | Kubernetes | 容器编排平台,用于调度、扩缩容和治理容器 |
|
||||||
| **Service Mesh** | - | 服务网格,负责微服务间通信治理、观测与安全 |
|
| **Service Mesh** | - | 服务网格,负责微服务间通信治理、观测与安全 |
|
||||||
| **Serverless** | - | 无服务计算,开发者只写函数,平台自动运行与扩缩容 |
|
| **Serverless** | - | 无服务计算,开发者只写函数,平台自动运行与扩缩容 |
|
||||||
| **BaaS** | Backend as a Service | 即插即用的后端云服务(认证、数据库、支付等) |
|
| **BaaS** | Backend as a Service | 即插即用的后端云服务(认证、数据库、支付等) |
|
||||||
| **CI/CD** | Continuous Integration / Delivery | 持续集成与持续交付,自动化测试与部署流程 |
|
| **CI/CD** | Continuous Integration / Delivery | 持续集成与持续交付,自动化测试与部署流程 |
|
||||||
| **Observability** | - | 可观测性,利用日志/指标/追踪理解系统运行状态 |
|
| **Observability** | - | 可观测性,利用日志/指标/追踪理解系统运行状态 |
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+154
-571
@@ -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 和 SVG(Scalable Vector Graphics)。它们各有优劣:
|
**Canvas (画布)** 是 HTML5 提供的一个通过 JavaScript 绘制 2D 图形的元素。
|
||||||
|
|
||||||
|
你可以把它想象成一张**数字画布**:
|
||||||
|
|
||||||
|
- 🖌️ 你可以用代码"画笔"在上面作画
|
||||||
|
- 🎨 可以画任何东西: 简单的形状、复杂的图表、流畅的动画
|
||||||
|
- 🎮 甚至可以做成完整的游戏
|
||||||
|
|
||||||
|
::: tip 💡 Canvas vs SVG:有什么区别?
|
||||||
|
|
||||||
|
在 Web 开发中,绘制图形主要有两种方式:
|
||||||
|
|
||||||
| 特性 | Canvas | SVG |
|
| 特性 | Canvas | SVG |
|
||||||
| :------- | :------------------- | :-------------------- |
|
| -------- | -------------------- | --------------------- |
|
||||||
| **类型** | 位图(光栅图形) | 矢量图形 |
|
| **类型** | 位图(光栅图形) | 矢量图形 |
|
||||||
| **DOM** | 单个 `<canvas>` 元素 | 每个图形都是 DOM 元素 |
|
| **DOM** | 单个 `<canvas>` 元素 | 每个图形都是 DOM 元素 |
|
||||||
| **交互** | 需要手动计算碰撞 | 天然支持事件绑定 |
|
| **交互** | 需要手动计算碰撞 | 天然支持事件绑定 |
|
||||||
| **性能** | 适合大量对象 | 适合少量复杂对象 |
|
| **性能** | 适合大量对象 | 适合少量复杂对象 |
|
||||||
| **缩放** | 放大会失真 | 无限缩放不失真 |
|
| **缩放** | 放大会失真 | 无限缩放不失真 |
|
||||||
| **应用** | 游戏、数据可视化 | 图标、插画 |
|
| **应用** | 游戏、数据可视化 | 图标、插画 |
|
||||||
|
|
||||||
**简单总结**:
|
**简单总结**:
|
||||||
|
|
||||||
- **Canvas** = 像素画,画完就变成像素,性能好但交互麻烦
|
- **Canvas** = 像素画,画完就变成像素,性能好但交互麻烦
|
||||||
- **SVG** = 矢量图,每个图形都是对象,交互方便但对象多了会慢
|
- **SVG** = 矢量图,每个图形都是对象,交互方便但对象多了会慢
|
||||||
|
:::
|
||||||
|
|
||||||
### 0.2 Canvas 的应用场景
|
### 1.2 Canvas 的应用场景
|
||||||
|
|
||||||
Canvas 的用途非常广泛,你可能在很多地方都见过它:
|
Canvas 的用途非常广泛,你可能每天都在用:
|
||||||
|
|
||||||
1. **数据可视化**:折线图、饼图、热力图(如 ECharts、Chart.js)
|
1. **数据可视化**: ECharts、Chart.js 的图表
|
||||||
2. **游戏开发**:网页游戏(如 Phaser.js 引擎)
|
2. **游戏开发**: 网页游戏(如 Phaser.js 引擎)
|
||||||
3. **图像处理**:图片裁剪、滤镜、拼图(如 Fabric.js)
|
3. **图像处理**: 图片裁剪、滤镜、拼图(如 Fabric.js)
|
||||||
4. **创意效果**:粒子特效、动画背景(如 Three.js 的 2D 渲染)
|
4. **创意效果**: 粒子特效、动画背景
|
||||||
5. **工程绘图**:CAD、流程图、思维导图
|
5. **工程绘图**: CAD、流程图、思维导图
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Canvas 基础
|
## 2. Canvas 基础
|
||||||
|
|
||||||
### 1.1 Canvas 元素和上下文
|
### 2.1 Canvas 元素和上下文
|
||||||
|
|
||||||
使用 Canvas 的第一步是在 HTML 中创建一个 `<canvas>` 元素:
|
使用 Canvas 的第一步是在 HTML 中创建一个 `<canvas>` 元素:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<canvas id="myCanvas" width="600" height="400"></canvas>
|
<canvas id="myCanvas" width="600" height="400"></canvas>
|
||||||
```
|
```
|
||||||
|
|
||||||
然后通过 JavaScript 获取**渲染上下文(Rendering Context)**:
|
然后通过 JavaScript 获取**渲染上下文 (Rendering Context)**:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const canvas = document.getElementById('myCanvas')
|
const canvas = document.getElementById('myCanvas')
|
||||||
const ctx = canvas.getContext('2d') // 获取 2D 上下文
|
const ctx = canvas.getContext('2d') // 获取 2D 上下文
|
||||||
```
|
```
|
||||||
|
|
||||||
**关键概念**:
|
::: tip 💡 关键概念
|
||||||
|
|
||||||
- `canvas` 是 DOM 元素,控制画布的大小和位置
|
- **canvas** 是 DOM 元素,控制画布的大小和位置
|
||||||
- `ctx` 是绘图工具,所有的绘制操作都通过它完成
|
- **ctx** 是绘图工具,所有的绘制操作都通过它完成
|
||||||
- `'2d'` 表示使用 2D 渲染上下文(WebGL 使用 `'webgl'`)
|
- **`"2d"`** 表示使用 2D 渲染上下文(WebGL 使用 `"webgl"`)
|
||||||
|
:::
|
||||||
|
|
||||||
> 🕹️ **交互演示**:点击下方按钮,体验 Canvas 的基本绘图操作。
|
### 2.2 坐标系统:Canvas 的"地图规则"
|
||||||
|
|
||||||
<CanvasBasicsDemo />
|
Canvas 使用的是**屏幕坐标系**,这与传统数学坐标系有所不同:
|
||||||
|
|
||||||
### 1.2 坐标系统
|
- **原点 (0, 0)**: 在**左上角**(不是中心)
|
||||||
|
- **X 轴**: 向右为正方向
|
||||||
Canvas 使用的是**屏幕坐标系**,这与传统数学坐标系有所不同:
|
- **Y 轴**: **向下**为正方向(注意: 数学坐标系中 Y 轴向上)
|
||||||
|
- **单位**: 像素 (px)
|
||||||
- **原点 (0, 0)**:在左上角(不是中心)
|
|
||||||
- **X 轴**:向右为正方向
|
|
||||||
- **Y 轴**:向下为正方向(注意:数学坐标系中 Y 轴向上)
|
|
||||||
- **单位**:像素(px)
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 在左上角绘制一个点
|
// 在左上角绘制一个矩形
|
||||||
ctx.fillRect(0, 0, 10, 10)
|
ctx.fillRect(0, 0, 10, 10)
|
||||||
|
|
||||||
// 在右下角绘制一个点
|
// 在右下角绘制一个矩形
|
||||||
ctx.fillRect(canvas.width - 10, canvas.height - 10, 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
|
```javascript
|
||||||
// 填充矩形
|
// 填充矩形
|
||||||
@@ -104,7 +122,7 @@ ctx.strokeRect(x, y, width, height)
|
|||||||
ctx.clearRect(x, y, width, height)
|
ctx.clearRect(x, y, width, height)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 圆形
|
**圆形**:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
@@ -112,18 +130,16 @@ ctx.arc(x, y, radius, startAngle, endAngle)
|
|||||||
ctx.fill() // 或 ctx.stroke()
|
ctx.fill() // 或 ctx.stroke()
|
||||||
```
|
```
|
||||||
|
|
||||||
**参数说明**:
|
**参数说明**:
|
||||||
|
|
||||||
- `x, y`:圆心坐标
|
- **x, y**: 圆心坐标
|
||||||
- `radius`:半径
|
- **radius**: 半径
|
||||||
- `startAngle, endAngle`:起始和结束角度(弧度制)
|
- **startAngle, endAngle**: 起始和结束角度(弧度制)
|
||||||
- `0` = 3 点钟方向
|
- `0` = 3 点钟方向
|
||||||
- `Math.PI / 2` = 6 点钟方向
|
- `Math.PI / 2` = 6 点钟方向
|
||||||
- `Math.PI` = 9 点钟方向
|
- `Math.PI` = 9 点钟方向
|
||||||
- `Math.PI * 1.5` = 12 点钟方向
|
|
||||||
- `Math.PI * 2` = 回到 3 点钟方向
|
|
||||||
|
|
||||||
#### 线条
|
**线条**:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
@@ -132,15 +148,15 @@ ctx.lineTo(x2, y2) // 终点
|
|||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1.4 颜色和渐变
|
### 2.4 颜色和样式
|
||||||
|
|
||||||
Canvas 支持多种颜色设置方式:
|
Canvas 支持多种颜色设置方式:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 纯色
|
// 纯色
|
||||||
ctx.fillStyle = '#3498db' // 十六进制
|
ctx.fillStyle = '#3498db' // 十六进制
|
||||||
ctx.fillStyle = 'rgb(52, 152, 219)' // RGB
|
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)
|
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()` - 开始新路径(拿起笔)
|
1. **`beginPath()`** - 开始新路径(拿起笔)
|
||||||
2. `moveTo()` - 移动到起点(不画线)
|
2. **`moveTo()`** - 移动到起点(不画线)
|
||||||
3. `lineTo()` / `arc()` / `curveTo()` - 绘制线条或曲线
|
3. **`lineTo()` / `arc()`** - 绘制线条或曲线
|
||||||
4. `closePath()` - 闭合路径(可选)
|
4. **`closePath()`** - 闭合路径(可选)
|
||||||
5. `fill()` / `stroke()` - 填充或描边
|
5. **`fill()` / `stroke()`** - 填充或描边
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(100, 100) // 移动到起点
|
ctx.moveTo(100, 100) // 移动到起点
|
||||||
ctx.lineTo(200, 100) // 画横线
|
ctx.lineTo(200, 100) // 画横线
|
||||||
ctx.lineTo(150, 150) // 画斜线
|
ctx.lineTo(150, 150) // 画斜线
|
||||||
ctx.closePath() // 闭合路径(回到起点)
|
ctx.closePath() // 闭合路径(回到起点)
|
||||||
ctx.fill() // 填充
|
ctx.fill() // 填充
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.2 绘制复杂形状
|
### 3.2 绘制复杂形状
|
||||||
|
|
||||||
通过组合路径,可以绘制任意复杂的形状:
|
通过组合路径,可以绘制任意复杂的形状。
|
||||||
|
|
||||||
#### 三角形
|
**三角形**:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
@@ -194,114 +210,17 @@ ctx.fillStyle = '#e74c3c'
|
|||||||
ctx.fill()
|
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. 动画基础
|
||||||
|
|
||||||
### 4.1 requestAnimationFrame
|
### 4.1 动画循环
|
||||||
|
|
||||||
在 Canvas 中创建动画,核心是使用 `requestAnimationFrame` 方法。它是浏览器专门为动画优化的 API:
|
在 Canvas 中创建动画,核心是使用 **`requestAnimationFrame`** 方法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function animate() {
|
function animate() {
|
||||||
// 1. 清除画布(或绘制半透明背景产生拖尾效果)
|
// 1. 清除画布(或绘制半透明背景产生拖尾效果)
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
// 2. 更新状态
|
// 2. 更新状态
|
||||||
@@ -318,93 +237,35 @@ function animate() {
|
|||||||
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
|
```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.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
// 绘制球
|
// 半透明背景(产生拖尾效果)
|
||||||
ctx.beginPath()
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'
|
||||||
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2)
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
ctx.fillStyle = '#3498db'
|
|
||||||
ctx.fill()
|
|
||||||
}
|
|
||||||
|
|
||||||
function animate() {
|
|
||||||
update()
|
|
||||||
draw()
|
|
||||||
requestAnimationFrame(animate)
|
|
||||||
}
|
|
||||||
|
|
||||||
animate()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 事件处理
|
## 5. 事件处理
|
||||||
|
|
||||||
Canvas 只是一个 DOM 元素,不像 SVG 那样每个图形都是独立的 DOM 元素。因此,我们需要**手动处理交互事件**。
|
Canvas 只是一个 DOM 元素,不像 SVG 那样每个图形都是独立的 DOM 元素。因此,我们需要**手动处理交互事件**。
|
||||||
|
|
||||||
### 5.1 鼠标事件
|
### 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
|
### 6.1 离屏 Canvas (Offscreen Canvas)
|
||||||
const data = [10, 50, 30, 80, 60, 90, 40]
|
|
||||||
|
|
||||||
function drawLineChart(ctx, data) {
|
预渲染静态内容到离屏 Canvas,减少每帧的绘制操作:
|
||||||
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,减少每帧的绘制操作:
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 创建离屏 Canvas
|
// 创建离屏 Canvas
|
||||||
@@ -640,7 +348,6 @@ offscreenCanvas.height = 400
|
|||||||
function drawBackground(ctx) {
|
function drawBackground(ctx) {
|
||||||
ctx.fillStyle = '#f0f0f0'
|
ctx.fillStyle = '#f0f0f0'
|
||||||
ctx.fillRect(0, 0, 600, 400)
|
ctx.fillRect(0, 0, 600, 400)
|
||||||
// 绘制网格...
|
|
||||||
}
|
}
|
||||||
drawBackground(offscreenCtx)
|
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
|
```javascript
|
||||||
function draw() {
|
function draw() {
|
||||||
@@ -703,9 +386,9 @@ function draw() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7.4 批量渲染
|
### 6.3 批量渲染
|
||||||
|
|
||||||
减少状态切换(fillStyle、strokeStyle 等):
|
减少状态切换(fillStyle、strokeStyle 等):
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 按颜色分组
|
// 按颜色分组
|
||||||
@@ -728,19 +411,15 @@ Object.keys(batches).forEach((color) => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
> 🕹️ **交互演示**:对比不同优化技术的性能差异。
|
|
||||||
|
|
||||||
<PerformanceDemo />
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 常见库与框架
|
## 7. 常见库与框架
|
||||||
|
|
||||||
虽然原生 Canvas 已经很强大,但在实际项目中,使用成熟的库可以大大提高开发效率。
|
虽然原生 Canvas 已经很强大,但在实际项目中,使用成熟的库可以大大提高开发效率。
|
||||||
|
|
||||||
### 8.1 Fabric.js
|
### 7.1 Fabric.js
|
||||||
|
|
||||||
**特点**:对象模型,支持交互
|
**特点**: 对象模型,支持交互
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const canvas = new fabric.Canvas('c')
|
const canvas = new fabric.Canvas('c')
|
||||||
@@ -762,38 +441,11 @@ circle.on('click', () => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
**适用场景**:图片编辑器、白板工具、图形设计工具
|
**适用场景**: 图片编辑器、白板工具、图形设计工具
|
||||||
|
|
||||||
### 8.2 Konva.js
|
### 7.2 PixiJS (WebGL)
|
||||||
|
|
||||||
**特点**:高性能,支持动画和滤镜
|
**特点**: 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 加速,超高性能
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const app = new PIXI.Application({
|
const app = new PIXI.Application({
|
||||||
@@ -810,60 +462,24 @@ graphics.endFill()
|
|||||||
app.stage.addChild(graphics)
|
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 是位图画布**:绘制后就是像素,无法直接修改已有内容
|
1. **Canvas 是位图画布**: 绘制后就是像素,无法直接修改已有内容
|
||||||
2. **坐标系统**:原点在左上角,Y 轴向下为正
|
2. **坐标系统**: 原点在左上角,Y 轴向下为正
|
||||||
3. **路径系统**:beginPath → moveTo → lineTo → fill/stroke
|
3. **路径系统**: beginPath → moveTo → lineTo → fill/stroke
|
||||||
4. **动画原理**:清除 → 更新 → 绘制 → requestAnimationFrame
|
4. **动画原理**: 清除 → 更新 → 绘制 → requestAnimationFrame
|
||||||
5. **事件处理**:需要手动计算碰撞检测
|
5. **事件处理**: 需要手动计算碰撞
|
||||||
6. **性能优化**:离屏 Canvas、脏矩形、批量渲染
|
6. **性能优化**: 离屏 Canvas、脏矩形、批量渲染
|
||||||
|
|
||||||
### 9.2 最佳实践
|
### 8.2 最佳实践
|
||||||
|
|
||||||
#### 代码组织
|
**代码组织**:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 使用类封装对象
|
// 使用类封装对象
|
||||||
@@ -889,81 +505,48 @@ class GameObject {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 性能优化清单
|
**性能优化清单**:
|
||||||
|
|
||||||
- ✅ 使用 `requestAnimationFrame` 而不是 `setInterval`
|
- ✅ 使用 `requestAnimationFrame` 而不是 `setInterval`
|
||||||
- ✅ 减少状态切换(按颜色分组绘制)
|
- ✅ 减少状态切换(按颜色分组绘制)
|
||||||
- ✅ 使用离屏 Canvas 预渲染静态内容
|
- ✅ 使用离屏 Canvas 预渲染静态内容
|
||||||
- ✅ 只重绘变化的部分(脏矩形)
|
- ✅ 只重绘变化的部分(脏矩形)
|
||||||
- ✅ 限制对象数量,使用对象池
|
- ✅ 限制对象数量,使用对象池
|
||||||
- ✅ 避免 `save()` 和 `restore()` 的频繁调用
|
- ✅ 避免 `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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示 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)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. 名词速查表 (Glossary)
|
## 9. 名词速查表 (Glossary)
|
||||||
|
|
||||||
| 名词 | 解释 |
|
| 名词 | 解释 |
|
||||||
| :------------------------ | :------------------------------------------------------------------------ |
|
| ------------------------- | ----------------------------------------------------------------------- |
|
||||||
| **Context / 上下文** | Canvas 的渲染环境,通过 `getContext('2d')` 获取,所有绘制操作都通过它完成 |
|
| **Context / 上下文** | Canvas 的渲染环境,通过 `getContext("2d")` 获取,所有绘制操作都通过它完成 |
|
||||||
| **Path / 路径** | 由一系列点连接成的轨迹,是 Canvas 绘图的基础 |
|
| **Path / 路径** | 由一系列点连接成的轨迹,是 Canvas 绘图的基础 |
|
||||||
| **Stroke / 描边** | 绘制路径的轮廓线 |
|
| **Stroke / 描边** | 绘制路径的轮廓线 |
|
||||||
| **Fill / 填充** | 用颜色填充路径内部 |
|
| **Fill / 填充** | 用颜色填充路径内部 |
|
||||||
| **requestAnimationFrame** | 浏览器提供的动画 API,在每次重绘前调用回调函数 |
|
| **requestAnimationFrame** | 浏览器提供的动画 API,在每次重绘前调用回调函数 |
|
||||||
| **Offscreen Canvas** | 离屏 Canvas,用于预渲染静态内容以提高性能 |
|
| **Offscreen Canvas** | 离屏 Canvas,用于预渲染静态内容以提高性能 |
|
||||||
| **Dirty Rect** | 脏矩形优化,只重绘变化的部分 |
|
| **Dirty Rect** | 脏矩形优化,只重绘变化的部分 |
|
||||||
| **Particle System** | 粒子系统,由大量小粒子组成的特效系统 |
|
| **Collision Detection** | 碰撞检测,判断鼠标或对象是否点击了某个图形 |
|
||||||
| **Collision Detection** | 碰撞检测,判断鼠标或对象是否点击了某个图形 |
|
| **Raster vs Vector** | 位图 vs 矢量图,Canvas 是位图,SVG 是矢量图 |
|
||||||
| **Raster vs Vector** | 位图 vs 矢量图,Canvas 是位图,SVG 是矢量图 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**下一步建议**:
|
## 总结
|
||||||
|
|
||||||
- 如果你想深入学习 Canvas 动画,可以尝试制作一个**贪吃蛇游戏**或**打砖块游戏**
|
现在你已经掌握了 Canvas 2D 的核心概念:
|
||||||
- 如果你对数据可视化感兴趣,可以学习 **ECharts** 或 **D3.js**
|
|
||||||
- 如果你想做游戏开发,可以尝试 **Phaser.js** 游戏引擎
|
|
||||||
- 如果你对 WebGL 感兴趣,可以学习 **Three.js** 或 **PixiJS**
|
|
||||||
|
|
||||||
祝你学习愉快!🎨
|
- **基本绘图**: 矩形、圆形、线条
|
||||||
|
- **样式控制**: 颜色、渐变、阴影
|
||||||
|
- **动画制作**: requestAnimationFrame + 清除重绘
|
||||||
|
- **交互处理**: 鼠标事件、碰撞检测
|
||||||
|
- **性能优化**: 离屏 Canvas、批量渲染
|
||||||
|
|
||||||
|
**下一步建议**:
|
||||||
|
|
||||||
|
- 如果你想深入学习动画,可以尝试制作一个**贪吃蛇游戏**或**打砖块游戏**
|
||||||
|
- 如果你对数据可视化感兴趣,可以学习 **ECharts** 或 **D3.js**
|
||||||
|
- 如果你想做游戏开发,可以尝试 **Phaser.js** 游戏引擎
|
||||||
|
- 如果你对 WebGL 感兴趣,可以学习 **Three.js** 或 **PixiJS**
|
||||||
|
|
||||||
|
祝你学习愉快! 🎨
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
想象一下,你们公司搬到了一栋新写字楼:
|
想象一下,你们公司搬到了一栋新写字楼:
|
||||||
|
|
||||||
| 场景 | 没有 IAM 的做法 | 有 IAM 的做法 |
|
| 场景 | 没有 IAM 的做法 | 有 IAM 的做法 |
|
||||||
| :--- | :--- | :--- |
|
| :--------- | :----------------------------- | :------------------------------------------- |
|
||||||
| 新员工入职 | 给他一把能开所有门的万能钥匙 | 给他一张门禁卡,只能刷他办公区域的门 |
|
| 新员工入职 | 给他一把能开所有门的万能钥匙 | 给他一张门禁卡,只能刷他办公区域的门 |
|
||||||
| 员工离职 | 钥匙丢了就丢了,也不知道谁拿着 | 立即在系统里注销他的门禁卡,所有门都打不开了 |
|
| 员工离职 | 钥匙丢了就丢了,也不知道谁拿着 | 立即在系统里注销他的门禁卡,所有门都打不开了 |
|
||||||
| 外包人员 | 把钥匙借给他几天 | 发临时门禁卡,设置3天后自动失效 |
|
| 外包人员 | 把钥匙借给他几天 | 发临时门禁卡,设置3天后自动失效 |
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
不同的云厂商都有自己的 IAM 实现:
|
不同的云厂商都有自己的 IAM 实现:
|
||||||
|
|
||||||
| 云厂商 | 服务名称 | 核心概念 |
|
| 云厂商 | 服务名称 | 核心概念 |
|
||||||
| :--- | :--- | :--- |
|
| :--------- | :----------------------------------- | :------------------------ |
|
||||||
| **AWS** | IAM (Identity and Access Management) | User、Group、Role、Policy |
|
| **AWS** | IAM (Identity and Access Management) | User、Group、Role、Policy |
|
||||||
| **阿里云** | RAM (Resource Access Management) | 用户、用户组、角色、策略 |
|
| **阿里云** | RAM (Resource Access Management) | 用户、用户组、角色、策略 |
|
||||||
| **腾讯云** | CAM (Cloud Access Management) | 用户、用户组、角色、策略 |
|
| **腾讯云** | CAM (Cloud Access Management) | 用户、用户组、角色、策略 |
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
用一个办公室的场景来类比:
|
用一个办公室的场景来类比:
|
||||||
|
|
||||||
| 概念 | 类比 | 适用场景 | 特点 |
|
| 概念 | 类比 | 适用场景 | 特点 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :------------------ | :----------------------------- | :------------------- | :--------------------------------- |
|
||||||
| **用户(User)** | 正式员工,有自己的工位和门禁卡 | 长期、稳定的团队成员 | 有永久凭证(密码、AK/SK) |
|
| **用户(User)** | 正式员工,有自己的工位和门禁卡 | 长期、稳定的团队成员 | 有永久凭证(密码、AK/SK) |
|
||||||
| **用户组(Group)** | 部门,如"技术部"、"销售部" | 批量管理权限 | 不能登录,只是权限容器 |
|
| **用户组(Group)** | 部门,如"技术部"、"销售部" | 批量管理权限 | 不能登录,只是权限容器 |
|
||||||
| **角色(Role)** | 临时访客证、外包临时卡 | 临时授权、跨账号访问 | 没有永久凭证,靠"扮演"获取临时凭证 |
|
| **角色(Role)** | 临时访客证、外包临时卡 | 临时授权、跨账号访问 | 没有永久凭证,靠"扮演"获取临时凭证 |
|
||||||
@@ -152,7 +152,7 @@ IAM Role 有两个核心组成部分:
|
|||||||
用一个话剧表演的类比:
|
用一个话剧表演的类比:
|
||||||
|
|
||||||
| 概念 | 类比 | 说明 |
|
| 概念 | 类比 | 说明 |
|
||||||
| :--- | :--- | :--- |
|
| :-------------------- | :--------------------- | :----------------------------------------------------------------------------------------- |
|
||||||
| **Role(角色)** | 剧本里的"哈姆雷特" | 定义了要演什么戏(权限) |
|
| **Role(角色)** | 剧本里的"哈姆雷特" | 定义了要演什么戏(权限) |
|
||||||
| **Trust Policy** | 导演说"谁能演哈姆雷特" | 可能是"本剧团的演员"(本账号用户)、"隔壁剧团借来的演员"(跨账号)、"特邀嘉宾"(外部 IdP) |
|
| **Trust Policy** | 导演说"谁能演哈姆雷特" | 可能是"本剧团的演员"(本账号用户)、"隔壁剧团借来的演员"(跨账号)、"特邀嘉宾"(外部 IdP) |
|
||||||
| **Permission Policy** | 剧本内容 | 哈姆雷特能做什么:说台词、决斗、发疯(具体权限) |
|
| **Permission Policy** | 剧本内容 | 哈姆雷特能做什么:说台词、决斗、发疯(具体权限) |
|
||||||
@@ -174,11 +174,7 @@ IAM Policy 是一个 JSON 文档,定义了"谁能对什么资源做什么操
|
|||||||
{
|
{
|
||||||
"Sid": "AllowS3ReadWrite",
|
"Sid": "AllowS3ReadWrite",
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": [
|
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
|
||||||
"s3:GetObject",
|
|
||||||
"s3:PutObject",
|
|
||||||
"s3:DeleteObject"
|
|
||||||
],
|
|
||||||
"Resource": "arn:aws:s3:::my-app-bucket/*",
|
"Resource": "arn:aws:s3:::my-app-bucket/*",
|
||||||
"Condition": {
|
"Condition": {
|
||||||
"StringEquals": {
|
"StringEquals": {
|
||||||
@@ -202,13 +198,13 @@ IAM Policy 是一个 JSON 文档,定义了"谁能对什么资源做什么操
|
|||||||
**关键字段解释**:
|
**关键字段解释**:
|
||||||
|
|
||||||
| 字段 | 含义 | 示例 |
|
| 字段 | 含义 | 示例 |
|
||||||
| :--- | :--- | :--- |
|
| :------------ | :--------------------------------- | :----------------------- |
|
||||||
| **Version** | Policy 语法版本 | "2012-10-17" |
|
| **Version** | Policy 语法版本 | "2012-10-17" |
|
||||||
| **Statement** | 权限声明数组,可包含多个规则 | [...] |
|
| **Statement** | 权限声明数组,可包含多个规则 | [...] |
|
||||||
| **Sid** | 声明 ID,可选,用于标识这条规则 | "AllowS3ReadWrite" |
|
| **Sid** | 声明 ID,可选,用于标识这条规则 | "AllowS3ReadWrite" |
|
||||||
| **Effect** | 效果:Allow(允许)或 Deny(拒绝) | "Allow" |
|
| **Effect** | 效果:Allow(允许)或 Deny(拒绝) | "Allow" |
|
||||||
| **Action** | 允许/拒绝的操作,支持通配符 | "s3:GetObject", "s3:*" |
|
| **Action** | 允许/拒绝的操作,支持通配符 | "s3:GetObject", "s3:\*" |
|
||||||
| **Resource** | 作用的资源,用 ARN 标识 | "arn:aws:s3:::bucket/*" |
|
| **Resource** | 作用的资源,用 ARN 标识 | "arn:aws:s3:::bucket/\*" |
|
||||||
| **Condition** | 可选,满足特定条件时才生效 | 区域限制、MFA 要求等 |
|
| **Condition** | 可选,满足特定条件时才生效 | 区域限制、MFA 要求等 |
|
||||||
|
|
||||||
### 3.3 权限的优先级:Deny > Allow > 默认拒绝
|
### 3.3 权限的优先级:Deny > Allow > 默认拒绝
|
||||||
@@ -246,6 +242,7 @@ IAM 的权限评估逻辑可以用一句话总结:**显式 Deny 永远赢,
|
|||||||
```
|
```
|
||||||
|
|
||||||
**关键点**:
|
**关键点**:
|
||||||
|
|
||||||
- 开发者虽然有 `s3:*` 的 Allow 权限
|
- 开发者虽然有 `s3:*` 的 Allow 权限
|
||||||
- 但敏感目录有显式的 Deny 规则
|
- 但敏感目录有显式的 Deny 规则
|
||||||
- Deny 优先级更高,所以开发者无法访问敏感数据
|
- Deny 优先级更高,所以开发者无法访问敏感数据
|
||||||
@@ -262,7 +259,7 @@ IAM 的权限评估逻辑可以用一句话总结:**显式 Deny 永远赢,
|
|||||||
Access Key(访问密钥)是云服务提供的一种长期凭证,用于程序化的 API 调用。它由两部分组成:
|
Access Key(访问密钥)是云服务提供的一种长期凭证,用于程序化的 API 调用。它由两部分组成:
|
||||||
|
|
||||||
| 组成部分 | 名称 | 作用 | 类比 |
|
| 组成部分 | 名称 | 作用 | 类比 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :-------------------- | :----------- | :------------------------- | :--------- |
|
||||||
| **Access Key ID** | 访问密钥 ID | 标识你是谁(类似于用户名) | 银行卡号 |
|
| **Access Key ID** | 访问密钥 ID | 标识你是谁(类似于用户名) | 银行卡号 |
|
||||||
| **Secret Access Key** | 秘密访问密钥 | 证明你是你(类似于密码) | 银行卡密码 |
|
| **Secret Access Key** | 秘密访问密钥 | 证明你是你(类似于密码) | 银行卡密码 |
|
||||||
|
|
||||||
@@ -303,7 +300,7 @@ upload_file('./test.jpg', 'my-company-bucket', 'uploads/test.jpg')
|
|||||||
**这个案例告诉我们什么?**
|
**这个案例告诉我们什么?**
|
||||||
|
|
||||||
| 错误做法 | 正确做法 |
|
| 错误做法 | 正确做法 |
|
||||||
| :--- | :--- |
|
| :-------------------------- | :----------------------------------------------- |
|
||||||
| 把 AK/SK 硬编码在代码中 | 使用 IAM Role,让程序自动获取临时凭证 |
|
| 把 AK/SK 硬编码在代码中 | 使用 IAM Role,让程序自动获取临时凭证 |
|
||||||
| 把 AK/SK 提交到 Git 仓库 | 使用 `.gitignore` 忽略配置文件,使用密钥管理服务 |
|
| 把 AK/SK 提交到 Git 仓库 | 使用 `.gitignore` 忽略配置文件,使用密钥管理服务 |
|
||||||
| 长期使用同一个 AK/SK 不轮换 | 定期轮换 AK/SK,使用临时凭证替代长期凭证 |
|
| 长期使用同一个 AK/SK 不轮换 | 定期轮换 AK/SK,使用临时凭证替代长期凭证 |
|
||||||
@@ -375,7 +372,7 @@ jobs:
|
|||||||
**总结:AK/SK 使用的安全层级**
|
**总结:AK/SK 使用的安全层级**
|
||||||
|
|
||||||
| 安全等级 | 做法 | 适用场景 | 风险等级 |
|
| 安全等级 | 做法 | 适用场景 | 风险等级 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :------- | :-------------------------- | :------------------------ | :------- |
|
||||||
| 最高 | 使用 IAM Role(无长期凭证) | EC2、Lambda、ECS、CI/CD | 极低 |
|
| 最高 | 使用 IAM Role(无长期凭证) | EC2、Lambda、ECS、CI/CD | 极低 |
|
||||||
| 高 | 使用 OIDC Federation | GitHub Actions、GitLab CI | 低 |
|
| 高 | 使用 OIDC Federation | GitHub Actions、GitLab CI | 低 |
|
||||||
| 中 | 使用密钥管理服务 | 本地开发、小团队 | 中 |
|
| 中 | 使用密钥管理服务 | 本地开发、小团队 | 中 |
|
||||||
@@ -393,7 +390,7 @@ jobs:
|
|||||||
MFA(Multi-Factor Authentication,多因素认证),也叫 2FA(Two-Factor Authentication,双因素认证),是一种安全机制,要求用户在登录时提供**两种或以上**不同类型的认证因素:
|
MFA(Multi-Factor Authentication,多因素认证),也叫 2FA(Two-Factor Authentication,双因素认证),是一种安全机制,要求用户在登录时提供**两种或以上**不同类型的认证因素:
|
||||||
|
|
||||||
| 因素类型 | 是什么 | 例子 |
|
| 因素类型 | 是什么 | 例子 |
|
||||||
| :--- | :--- | :--- |
|
| :------------------------- | :----------------- | :------------- |
|
||||||
| **知识因素**(你知道什么) | 只有用户知道的信息 | 密码、PIN 码 |
|
| **知识因素**(你知道什么) | 只有用户知道的信息 | 密码、PIN 码 |
|
||||||
| **持有因素**(你有什么) | 用户拥有的物理设备 | 手机、硬件密钥 |
|
| **持有因素**(你有什么) | 用户拥有的物理设备 | 手机、硬件密钥 |
|
||||||
| **生物因素**(你是什么) | 用户的生物特征 | 指纹、面部识别 |
|
| **生物因素**(你是什么) | 用户的生物特征 | 指纹、面部识别 |
|
||||||
@@ -403,7 +400,7 @@ MFA(Multi-Factor Authentication,多因素认证),也叫 2FA(Two-Factor
|
|||||||
**真实数据告诉你答案**:
|
**真实数据告诉你答案**:
|
||||||
|
|
||||||
| 攻击方式 | 没有 MFA 时的成功率 | 有 MFA 时的成功率 |
|
| 攻击方式 | 没有 MFA 时的成功率 | 有 MFA 时的成功率 |
|
||||||
| :--- | :--- | :--- |
|
| :----------------------- | :------------------ | :------------------------------ |
|
||||||
| 密码猜测/暴力破解 | 很高 | 极低(还需要第二因素) |
|
| 密码猜测/暴力破解 | 很高 | 极低(还需要第二因素) |
|
||||||
| 钓鱼攻击获取密码 | 很高 | 极低(钓鱼页面无法获取 MFA 码) |
|
| 钓鱼攻击获取密码 | 很高 | 极低(钓鱼页面无法获取 MFA 码) |
|
||||||
| 密码泄露(其他网站泄露) | 很高 | 极低(不知道第二因素) |
|
| 密码泄露(其他网站泄露) | 很高 | 极低(不知道第二因素) |
|
||||||
@@ -442,7 +439,7 @@ MFA(Multi-Factor Authentication,多因素认证),也叫 2FA(Two-Factor
|
|||||||
随着业务增长,很多公司会使用**多账号架构**来隔离不同环境:
|
随着业务增长,很多公司会使用**多账号架构**来隔离不同环境:
|
||||||
|
|
||||||
| 账号类型 | 用途 | 权限要求 |
|
| 账号类型 | 用途 | 权限要求 |
|
||||||
| :--- | :--- | :--- |
|
| :------------------ | :--------------------- | :----------------- |
|
||||||
| **Master Account** | 组织管理、账单结算 | 几乎不使用 |
|
| **Master Account** | 组织管理、账单结算 | 几乎不使用 |
|
||||||
| **Security Audit** | 集中收集所有账号的日志 | 只读访问其他账号 |
|
| **Security Audit** | 集中收集所有账号的日志 | 只读访问其他账号 |
|
||||||
| **Shared Services** | 共享资源(镜像仓库等) | 其他账号只读访问 |
|
| **Shared Services** | 共享资源(镜像仓库等) | 其他账号只读访问 |
|
||||||
@@ -498,6 +495,7 @@ MFA(Multi-Factor Authentication,多因素认证),也叫 2FA(Two-Factor
|
|||||||
**步骤二:获取 Role ARN**
|
**步骤二:获取 Role ARN**
|
||||||
|
|
||||||
创建完成后,复制 Role 的 ARN:
|
创建完成后,复制 Role 的 ARN:
|
||||||
|
|
||||||
```
|
```
|
||||||
arn:aws:iam::SHARED_SERVICES_ACCOUNT_ID:role/CrossAccountECRReadRole
|
arn:aws:iam::SHARED_SERVICES_ACCOUNT_ID:role/CrossAccountECRReadRole
|
||||||
```
|
```
|
||||||
@@ -692,7 +690,7 @@ aws ecr describe-repositories --registry-id SHARED_SERVICES_ACCOUNT_ID
|
|||||||
### 8.1 十大 IAM 反模式
|
### 8.1 十大 IAM 反模式
|
||||||
|
|
||||||
| # | 反模式 | 为什么不好 | 正确做法 |
|
| # | 反模式 | 为什么不好 | 正确做法 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :-- | :--------------------------- | :--------------------------------------------- | :----------------------------------------------- |
|
||||||
| 1 | 使用根账号进行日常操作 | 根账号拥有所有权限,一旦泄露无法限制损害 | 创建 IAM 管理员账号,根账号仅在必要时使用 |
|
| 1 | 使用根账号进行日常操作 | 根账号拥有所有权限,一旦泄露无法限制损害 | 创建 IAM 管理员账号,根账号仅在必要时使用 |
|
||||||
| 2 | 给所有人 AdministratorAccess | 违反最小权限原则,增加误操作和内部威胁风险 | 按角色分组,只授予必要的权限 |
|
| 2 | 给所有人 AdministratorAccess | 违反最小权限原则,增加误操作和内部威胁风险 | 按角色分组,只授予必要的权限 |
|
||||||
| 3 | 在代码中硬编码 AK/SK | AK/SK 容易通过 GitHub 泄露,且难以轮换 | 使用 IAM Role、环境变量或密钥管理服务 |
|
| 3 | 在代码中硬编码 AK/SK | AK/SK 容易通过 GitHub 泄露,且难以轮换 | 使用 IAM Role、环境变量或密钥管理服务 |
|
||||||
@@ -709,7 +707,7 @@ aws ecr describe-repositories --registry-id SHARED_SERVICES_ACCOUNT_ID
|
|||||||
## 9. 名词对照表
|
## 9. 名词对照表
|
||||||
|
|
||||||
| 英文术语 | 中文对照 | 解释 |
|
| 英文术语 | 中文对照 | 解释 |
|
||||||
| :--- | :--- | :--- |
|
| :--------------------------------------- | :-------------- | :----------------------------------------- |
|
||||||
| **IAM (Identity and Access Management)** | 身份与访问管理 | 云服务中管理用户身份和访问权限的服务 |
|
| **IAM (Identity and Access Management)** | 身份与访问管理 | 云服务中管理用户身份和访问权限的服务 |
|
||||||
| **RAM (Resource Access Management)** | 资源访问管理 | 阿里云的 IAM 服务名称 |
|
| **RAM (Resource Access Management)** | 资源访问管理 | 阿里云的 IAM 服务名称 |
|
||||||
| **Root Account** | 根账号 | 注册云账号时创建的拥有者账号,拥有最高权限 |
|
| **Root Account** | 根账号 | 注册云账号时创建的拥有者账号,拥有最高权限 |
|
||||||
@@ -756,6 +754,7 @@ aws ecr describe-repositories --registry-id SHARED_SERVICES_ACCOUNT_ID
|
|||||||
---
|
---
|
||||||
|
|
||||||
> **延伸阅读**:
|
> **延伸阅读**:
|
||||||
|
>
|
||||||
> - [AWS IAM 官方文档](https://docs.aws.amazon.com/iam/)
|
> - [AWS IAM 官方文档](https://docs.aws.amazon.com/iam/)
|
||||||
> - [阿里云 RAM 官方文档](https://www.aliyun.com/product/ram)
|
> - [阿里云 RAM 官方文档](https://www.aliyun.com/product/ram)
|
||||||
> - [AWS IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)
|
> - [AWS IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
**核心区别一览**:
|
**核心区别一览**:
|
||||||
|
|
||||||
| 维度 | 传统文件系统 | 对象存储 |
|
| 维度 | 传统文件系统 | 对象存储 |
|
||||||
| :--- | :--- | :--- |
|
| :----------- | :--------------------- | :---------------------- |
|
||||||
| **组织方式** | 层级目录树 | 扁平键值对 |
|
| **组织方式** | 层级目录树 | 扁平键值对 |
|
||||||
| **访问协议** | POSIX(本地文件操作) | HTTP/REST API |
|
| **访问协议** | POSIX(本地文件操作) | HTTP/REST API |
|
||||||
| **扩展性** | 单机容量有限 | 近乎无限水平扩展 |
|
| **扩展性** | 单机容量有限 | 近乎无限水平扩展 |
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
桶是对象存储的顶级容器,相当于一个独立的命名空间。所有对象都必须存放在某个桶中。
|
桶是对象存储的顶级容器,相当于一个独立的命名空间。所有对象都必须存放在某个桶中。
|
||||||
|
|
||||||
**命名规则**(以阿里云 OSS 为例):
|
**命名规则**(以阿里云 OSS 为例):
|
||||||
|
|
||||||
- 全局唯一:在整个云厂商的所有用户中不能重复
|
- 全局唯一:在整个云厂商的所有用户中不能重复
|
||||||
- 只能包含小写字母、数字和短横线
|
- 只能包含小写字母、数字和短横线
|
||||||
- 必须以小写字母或数字开头和结尾
|
- 必须以小写字母或数字开头和结尾
|
||||||
@@ -74,7 +75,7 @@
|
|||||||
对象存储提供多层权限控制:
|
对象存储提供多层权限控制:
|
||||||
|
|
||||||
| 层级 | 控制方式 | 典型场景 |
|
| 层级 | 控制方式 | 典型场景 |
|
||||||
| :--- | :--- | :--- |
|
| :----------- | :------------------------ | :------------------------------ |
|
||||||
| **桶级别** | Bucket Policy(资源策略) | 禁止所有外网访问、只允许特定 IP |
|
| **桶级别** | Bucket Policy(资源策略) | 禁止所有外网访问、只允许特定 IP |
|
||||||
| **对象级别** | ACL(访问控制列表) | 公开图片、私有文档 |
|
| **对象级别** | ACL(访问控制列表) | 公开图片、私有文档 |
|
||||||
| **临时授权** | STS(安全令牌服务) | 前端直传、移动端上传 |
|
| **临时授权** | STS(安全令牌服务) | 前端直传、移动端上传 |
|
||||||
@@ -102,11 +103,13 @@
|
|||||||
#### 边缘节点:离用户最近的"快递站"
|
#### 边缘节点:离用户最近的"快递站"
|
||||||
|
|
||||||
边缘节点是 CDN 网络中最接近用户的层级,通常部署在:
|
边缘节点是 CDN 网络中最接近用户的层级,通常部署在:
|
||||||
|
|
||||||
- 运营商机房(联通/电信/移动)
|
- 运营商机房(联通/电信/移动)
|
||||||
- 大城市互联网交换中心
|
- 大城市互联网交换中心
|
||||||
- 重要交通枢纽
|
- 重要交通枢纽
|
||||||
|
|
||||||
**中国主要 CDN 节点分布**:
|
**中国主要 CDN 节点分布**:
|
||||||
|
|
||||||
- 一线城市:北京、上海、广州、深圳
|
- 一线城市:北京、上海、广州、深圳
|
||||||
- 二线城市:杭州、南京、成都、武汉、西安
|
- 二线城市:杭州、南京、成都、武汉、西安
|
||||||
- 海外:香港、新加坡、东京、硅谷、法兰克福
|
- 海外:香港、新加坡、东京、硅谷、法兰克福
|
||||||
@@ -116,11 +119,13 @@
|
|||||||
#### 源站:内容的"总仓库"
|
#### 源站:内容的"总仓库"
|
||||||
|
|
||||||
源站是 CDN 回源获取内容的地方,可以是:
|
源站是 CDN 回源获取内容的地方,可以是:
|
||||||
|
|
||||||
- 对象存储(OSS/COS/S3)
|
- 对象存储(OSS/COS/S3)
|
||||||
- 自建服务器(ECS/物理机)
|
- 自建服务器(ECS/物理机)
|
||||||
- 负载均衡(SLB/CLB)
|
- 负载均衡(SLB/CLB)
|
||||||
|
|
||||||
**关键配置**:
|
**关键配置**:
|
||||||
|
|
||||||
- **回源 HOST**:CDN 节点访问源站时使用的域名/IP
|
- **回源 HOST**:CDN 节点访问源站时使用的域名/IP
|
||||||
- **回源协议**:HTTP 还是 HTTPS
|
- **回源协议**:HTTP 还是 HTTPS
|
||||||
- **回源端口**:80、443 还是自定义端口
|
- **回源端口**:80、443 还是自定义端口
|
||||||
@@ -128,10 +133,12 @@
|
|||||||
#### 中间层节点:"区域分拨中心"
|
#### 中间层节点:"区域分拨中心"
|
||||||
|
|
||||||
在边缘节点和源站之间,CDN 通常还有一层或多层中间节点:
|
在边缘节点和源站之间,CDN 通常还有一层或多层中间节点:
|
||||||
|
|
||||||
- **汇聚节点**:聚合多个边缘节点的回源请求,减少源站压力
|
- **汇聚节点**:聚合多个边缘节点的回源请求,减少源站压力
|
||||||
- **区域中心**:负责一个大区的内容分发和调度
|
- **区域中心**:负责一个大区的内容分发和调度
|
||||||
|
|
||||||
这种分层架构的好处:
|
这种分层架构的好处:
|
||||||
|
|
||||||
1. **降低源站压力**:1000 个边缘节点的请求,可能只需要向源站发起 10 次
|
1. **降低源站压力**:1000 个边缘节点的请求,可能只需要向源站发起 10 次
|
||||||
2. **提高命中率**:热门内容在中间层就被拦截,不需要回源
|
2. **提高命中率**:热门内容在中间层就被拦截,不需要回源
|
||||||
3. **故障隔离**:某条链路出问题,可以自动切换到其他路径
|
3. **故障隔离**:某条链路出问题,可以自动切换到其他路径
|
||||||
@@ -143,14 +150,17 @@
|
|||||||
<CachePolicyDemo />
|
<CachePolicyDemo />
|
||||||
|
|
||||||
**Step 1:DNS 解析**(智能调度)
|
**Step 1:DNS 解析**(智能调度)
|
||||||
|
|
||||||
```
|
```
|
||||||
用户输入:cdn.example.com/image.jpg
|
用户输入:cdn.example.com/image.jpg
|
||||||
↓
|
↓
|
||||||
DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
||||||
```
|
```
|
||||||
|
|
||||||
这里的关键是**智能 DNS**:根据用户的运营商、地理位置、节点负载,返回最优的 CDN 节点 IP。
|
这里的关键是**智能 DNS**:根据用户的运营商、地理位置、节点负载,返回最优的 CDN 节点 IP。
|
||||||
|
|
||||||
**Step 2:边缘节点查找**(缓存命中?)
|
**Step 2:边缘节点查找**(缓存命中?)
|
||||||
|
|
||||||
```
|
```
|
||||||
请求到达北京联通 CDN 节点(1.2.3.4)
|
请求到达北京联通 CDN 节点(1.2.3.4)
|
||||||
↓
|
↓
|
||||||
@@ -160,6 +170,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Step 3:回源获取**(层层向上)
|
**Step 3:回源获取**(层层向上)
|
||||||
|
|
||||||
```
|
```
|
||||||
边缘节点未命中
|
边缘节点未命中
|
||||||
↓
|
↓
|
||||||
@@ -173,6 +184,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Step 4:缓存并返回**(下次更快)
|
**Step 4:缓存并返回**(下次更快)
|
||||||
|
|
||||||
```
|
```
|
||||||
内容沿链路返回
|
内容沿链路返回
|
||||||
↓
|
↓
|
||||||
@@ -198,17 +210,20 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**流程**:
|
**流程**:
|
||||||
|
|
||||||
1. 用户选择文件,点击上传
|
1. 用户选择文件,点击上传
|
||||||
2. 文件先上传到你的后端服务器
|
2. 文件先上传到你的后端服务器
|
||||||
3. 后端接收完整文件后,再转上传到对象存储
|
3. 后端接收完整文件后,再转上传到对象存储
|
||||||
4. 返回上传结果给用户
|
4. 返回上传结果给用户
|
||||||
|
|
||||||
**优点**:
|
**优点**:
|
||||||
|
|
||||||
- 实现简单,前后端都好控制
|
- 实现简单,前后端都好控制
|
||||||
- 可以在后端做文件校验、格式转换
|
- 可以在后端做文件校验、格式转换
|
||||||
- 敏感操作可以记录日志、做权限校验
|
- 敏感操作可以记录日志、做权限校验
|
||||||
|
|
||||||
**缺点**:
|
**缺点**:
|
||||||
|
|
||||||
- **带宽双吃**:用户上传占用一次带宽,服务器转传又占用一次
|
- **带宽双吃**:用户上传占用一次带宽,服务器转传又占用一次
|
||||||
- **服务器压力大**:大文件会占用大量内存和 CPU
|
- **服务器压力大**:大文件会占用大量内存和 CPU
|
||||||
- **上传慢**:相当于多了一道中转,用户感知到的上传时间更长
|
- **上传慢**:相当于多了一道中转,用户感知到的上传时间更长
|
||||||
@@ -224,6 +239,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**流程**:
|
**流程**:
|
||||||
|
|
||||||
1. 用户选择文件,前端先向后端申请"上传凭证"
|
1. 用户选择文件,前端先向后端申请"上传凭证"
|
||||||
2. 后端验证用户身份,向对象存储服务申请**临时 STS 凭证**(带过期时间)
|
2. 后端验证用户身份,向对象存储服务申请**临时 STS 凭证**(带过期时间)
|
||||||
3. 后端把临时凭证返回给前端
|
3. 后端把临时凭证返回给前端
|
||||||
@@ -231,12 +247,14 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
|||||||
5. 对象存储返回上传结果,前端通知后端"上传完成"
|
5. 对象存储返回上传结果,前端通知后端"上传完成"
|
||||||
|
|
||||||
**优点**:
|
**优点**:
|
||||||
|
|
||||||
- **上传快**:少了中转环节,用户感知速度最快
|
- **上传快**:少了中转环节,用户感知速度最快
|
||||||
- **服务器压力小**:只处理凭证签发,不处理文件流
|
- **服务器压力小**:只处理凭证签发,不处理文件流
|
||||||
- **带宽省**:只走一次上传流量
|
- **带宽省**:只走一次上传流量
|
||||||
- **安全性高**:临时凭证有过期时间,泄露也危害有限
|
- **安全性高**:临时凭证有过期时间,泄露也危害有限
|
||||||
|
|
||||||
**缺点**:
|
**缺点**:
|
||||||
|
|
||||||
- 实现稍复杂,需要理解 STS、签名机制
|
- 实现稍复杂,需要理解 STS、签名机制
|
||||||
- 前端需要处理分片上传、断点续传等逻辑
|
- 前端需要处理分片上传、断点续传等逻辑
|
||||||
- 跨域(CORS)需要配置
|
- 跨域(CORS)需要配置
|
||||||
@@ -262,7 +280,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
|||||||
**为什么需要分片?**
|
**为什么需要分片?**
|
||||||
|
|
||||||
| 场景 | 不分片 | 分片 |
|
| 场景 | 不分片 | 分片 |
|
||||||
| :--- | :--- | :--- |
|
| :----------- | :---------------------- | :------------------- |
|
||||||
| **网络波动** | 传了 99% 断网,全部重传 | 只重传失败的分片 |
|
| **网络波动** | 传了 99% 断网,全部重传 | 只重传失败的分片 |
|
||||||
| **上传速度** | 单线程,速度慢 | 多线程并行,速度快 |
|
| **上传速度** | 单线程,速度慢 | 多线程并行,速度快 |
|
||||||
| **内存占用** | 需要缓存整个文件 | 只需缓存当前分片 |
|
| **内存占用** | 需要缓存整个文件 | 只需缓存当前分片 |
|
||||||
@@ -271,7 +289,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
|||||||
**主流云厂商的分片规格**:
|
**主流云厂商的分片规格**:
|
||||||
|
|
||||||
| 厂商 | 分片大小限制 | 最大分片数 | 最小分片大小 |
|
| 厂商 | 分片大小限制 | 最大分片数 | 最小分片大小 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :------------- | :----------- | :--------- | :----------- |
|
||||||
| **阿里云 OSS** | 100MB | 10000 | 100KB |
|
| **阿里云 OSS** | 100MB | 10000 | 100KB |
|
||||||
| **腾讯云 COS** | 5GB | 10000 | 1MB |
|
| **腾讯云 COS** | 5GB | 10000 | 1MB |
|
||||||
| **AWS S3** | 5GB | 10000 | 5MB(推荐) |
|
| **AWS S3** | 5GB | 10000 | 5MB(推荐) |
|
||||||
@@ -284,6 +302,7 @@ DNS 服务器返回:北京联通 CDN 节点 IP(1.2.3.4)
|
|||||||
#### 什么是"回源"?
|
#### 什么是"回源"?
|
||||||
|
|
||||||
CDN 边缘节点缓存了源站的内容,但当:
|
CDN 边缘节点缓存了源站的内容,但当:
|
||||||
|
|
||||||
- 用户请求的内容**第一次被访问**
|
- 用户请求的内容**第一次被访问**
|
||||||
- 缓存的内容**已过期(TTL 到期)**
|
- 缓存的内容**已过期(TTL 到期)**
|
||||||
- 缓存被**手动刷新/预热**
|
- 缓存被**手动刷新/预热**
|
||||||
@@ -293,7 +312,7 @@ CDN 节点就需要向**源站**请求最新内容,这个过程就叫"回源"
|
|||||||
#### 回源的三种模式
|
#### 回源的三种模式
|
||||||
|
|
||||||
| 模式 | 原理 | 适用场景 | 优缺点 |
|
| 模式 | 原理 | 适用场景 | 优缺点 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :-------------------- | :----------------------- | :------------------------ | :----------------------- |
|
||||||
| **直接回源** | CDN 节点 → 源站 | 源站有公网 IP,且流量不大 | 简单直接,但源站压力大 |
|
| **直接回源** | CDN 节点 → 源站 | 源站有公网 IP,且流量不大 | 简单直接,但源站压力大 |
|
||||||
| **中间源回源** | CDN 节点 → 中间层 → 源站 | 大型网站,多层缓存架构 | 分担源站压力,架构复杂 |
|
| **中间源回源** | CDN 节点 → 中间层 → 源站 | 大型网站,多层缓存架构 | 分担源站压力,架构复杂 |
|
||||||
| ** OSS/COS 作为源站** | CDN 节点 → 对象存储 | 静态资源、图片、视频 | 最佳实践,成本低、性能好 |
|
| ** OSS/COS 作为源站** | CDN 节点 → 对象存储 | 静态资源、图片、视频 | 最佳实践,成本低、性能好 |
|
||||||
@@ -315,6 +334,7 @@ CDN 节点就需要向**源站**请求最新内容,这个过程就叫"回源"
|
|||||||
```
|
```
|
||||||
|
|
||||||
关键配置项:
|
关键配置项:
|
||||||
|
|
||||||
- **源站类型**:OSS/COS 域名 或 自定义源站
|
- **源站类型**:OSS/COS 域名 或 自定义源站
|
||||||
- **回源协议**:HTTP 还是 HTTPS(建议 HTTPS)
|
- **回源协议**:HTTP 还是 HTTPS(建议 HTTPS)
|
||||||
- **回源 HOST**:访问源站时使用的 Host 头
|
- **回源 HOST**:访问源站时使用的 Host 头
|
||||||
@@ -332,6 +352,7 @@ CDN 边缘节点
|
|||||||
```
|
```
|
||||||
|
|
||||||
主备模式:
|
主备模式:
|
||||||
|
|
||||||
```
|
```
|
||||||
CDN 边缘节点
|
CDN 边缘节点
|
||||||
├─ 主源站 A (健康时全部流量)
|
├─ 主源站 A (健康时全部流量)
|
||||||
@@ -343,11 +364,12 @@ CDN 边缘节点
|
|||||||
这里有个容易混淆的概念:
|
这里有个容易混淆的概念:
|
||||||
|
|
||||||
| 指标 | 定义 | 计费关系 |
|
| 指标 | 定义 | 计费关系 |
|
||||||
| :--- | :--- | :--- |
|
| :--------------- | :---------------------- | :--------------------------- |
|
||||||
| **CDN 下行带宽** | 从 CDN 节点到用户的流量 | 通常按流量计费的 CDN 费用 |
|
| **CDN 下行带宽** | 从 CDN 节点到用户的流量 | 通常按流量计费的 CDN 费用 |
|
||||||
| **回源带宽** | 从源站到 CDN 节点的流量 | 通常对象存储或源站出流量费用 |
|
| **回源带宽** | 从源站到 CDN 节点的流量 | 通常对象存储或源站出流量费用 |
|
||||||
|
|
||||||
**省钱技巧**:
|
**省钱技巧**:
|
||||||
|
|
||||||
- 提高 CDN 命中率(让更多请求命中缓存,减少回源)
|
- 提高 CDN 命中率(让更多请求命中缓存,减少回源)
|
||||||
- 设置合理的缓存时间(TTL)
|
- 设置合理的缓存时间(TTL)
|
||||||
- 使用预热功能,在用户访问前就缓存热点内容
|
- 使用预热功能,在用户访问前就缓存热点内容
|
||||||
@@ -362,10 +384,12 @@ CDN 边缘节点
|
|||||||
CDN 如何判断两次请求是否应该返回同一个缓存副本?靠的就是**缓存键**。
|
CDN 如何判断两次请求是否应该返回同一个缓存副本?靠的就是**缓存键**。
|
||||||
|
|
||||||
**默认缓存键通常包括**:
|
**默认缓存键通常包括**:
|
||||||
|
|
||||||
- URL 路径(不含查询参数)
|
- URL 路径(不含查询参数)
|
||||||
- 例如:`/images/photo.jpg`
|
- 例如:`/images/photo.jpg`
|
||||||
|
|
||||||
**问题场景**:
|
**问题场景**:
|
||||||
|
|
||||||
```
|
```
|
||||||
用户 A 请求:/images/photo.jpg?w=100&h=100 (100x100 缩略图)
|
用户 A 请求:/images/photo.jpg?w=100&h=100 (100x100 缩略图)
|
||||||
用户 B 请求:/images/photo.jpg?w=800&h=600 (800x600 大图)
|
用户 B 请求:/images/photo.jpg?w=800&h=600 (800x600 大图)
|
||||||
@@ -376,13 +400,14 @@ CDN 如何判断两次请求是否应该返回同一个缓存副本?靠的就
|
|||||||
**解决方案:自定义缓存键规则**
|
**解决方案:自定义缓存键规则**
|
||||||
|
|
||||||
| 规则 | 示例 | 效果 |
|
| 规则 | 示例 | 效果 |
|
||||||
| :--- | :--- | :--- |
|
| :------------------- | :------------------------ | :------------------------ |
|
||||||
| **保留指定查询参数** | 保留 `w`、`h` | 不同尺寸分别缓存 |
|
| **保留指定查询参数** | 保留 `w`、`h` | 不同尺寸分别缓存 |
|
||||||
| **保留所有查询参数** | 保留全部 | 完全精确匹配 |
|
| **保留所有查询参数** | 保留全部 | 完全精确匹配 |
|
||||||
| **忽略特定查询参数** | 忽略 `token`、`timestamp` | 带时间戳的 URL 能命中缓存 |
|
| **忽略特定查询参数** | 忽略 `token`、`timestamp` | 带时间戳的 URL 能命中缓存 |
|
||||||
| **包含请求头** | 包含 `Accept-Language` | 不同语言返回不同内容 |
|
| **包含请求头** | 包含 `Accept-Language` | 不同语言返回不同内容 |
|
||||||
|
|
||||||
**实战配置示例**(阿里云 CDN):
|
**实战配置示例**(阿里云 CDN):
|
||||||
|
|
||||||
```
|
```
|
||||||
缓存键规则:
|
缓存键规则:
|
||||||
- URL 路径:/images/*
|
- URL 路径:/images/*
|
||||||
@@ -397,7 +422,7 @@ TTL(Time To Live)决定了内容在 CDN 节点上缓存多久。设置太短
|
|||||||
**按文件类型设置 TTL 的建议**:
|
**按文件类型设置 TTL 的建议**:
|
||||||
|
|
||||||
| 文件类型 | 建议 TTL | 原因 |
|
| 文件类型 | 建议 TTL | 原因 |
|
||||||
| :--- | :--- | :--- |
|
| :---------- | :---------------------- | :----------------------------- |
|
||||||
| HTML 页面 | 0-5 分钟 | 内容频繁更新,需要实时 |
|
| HTML 页面 | 0-5 分钟 | 内容频繁更新,需要实时 |
|
||||||
| JS/CSS 文件 | 1 年(配合文件名 hash) | 内容不变,文件名变化即缓存失效 |
|
| JS/CSS 文件 | 1 年(配合文件名 hash) | 内容不变,文件名变化即缓存失效 |
|
||||||
| 图片/视频 | 7-30 天 | 更新频率低,可长期缓存 |
|
| 图片/视频 | 7-30 天 | 更新频率低,可长期缓存 |
|
||||||
@@ -426,7 +451,7 @@ output: {
|
|||||||
当你更新了源站内容,但 CDN 缓存还没过期,用户看到的还是旧内容:
|
当你更新了源站内容,但 CDN 缓存还没过期,用户看到的还是旧内容:
|
||||||
|
|
||||||
| 刷新类型 | 效果 | 耗时 | 适用场景 |
|
| 刷新类型 | 效果 | 耗时 | 适用场景 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :----------- | :--------------------- | :---------- | :----------- |
|
||||||
| **URL 刷新** | 指定 URL 的缓存失效 | 5-10 分钟 | 单个文件更新 |
|
| **URL 刷新** | 指定 URL 的缓存失效 | 5-10 分钟 | 单个文件更新 |
|
||||||
| **目录刷新** | 指定目录下所有内容失效 | 10-30 分钟 | 批量更新 |
|
| **目录刷新** | 指定目录下所有内容失效 | 10-30 分钟 | 批量更新 |
|
||||||
| **全站刷新** | 整个域名的缓存全部失效 | 30 分钟以上 | 紧急回滚 |
|
| **全站刷新** | 整个域名的缓存全部失效 | 30 分钟以上 | 紧急回滚 |
|
||||||
@@ -458,12 +483,14 @@ output: {
|
|||||||
### 4.1 智能 DNS 调度
|
### 4.1 智能 DNS 调度
|
||||||
|
|
||||||
传统 DNS 解析:
|
传统 DNS 解析:
|
||||||
|
|
||||||
```
|
```
|
||||||
用户问:cdn.example.com 的 IP 是什么?
|
用户问:cdn.example.com 的 IP 是什么?
|
||||||
DNS 答:1.2.3.4(固定的)
|
DNS 答:1.2.3.4(固定的)
|
||||||
```
|
```
|
||||||
|
|
||||||
智能 DNS 解析:
|
智能 DNS 解析:
|
||||||
|
|
||||||
```
|
```
|
||||||
用户(北京联通)问:cdn.example.com 的 IP 是什么?
|
用户(北京联通)问:cdn.example.com 的 IP 是什么?
|
||||||
智能 DNS:让我查查... 北京联通的 CDN 节点是 1.2.3.4
|
智能 DNS:让我查查... 北京联通的 CDN 节点是 1.2.3.4
|
||||||
@@ -486,6 +513,7 @@ DNS 答:1.2.3.4(固定的)
|
|||||||
传统 DNS 有个问题:**DNS 劫持和解析延迟**。
|
传统 DNS 有个问题:**DNS 劫持和解析延迟**。
|
||||||
|
|
||||||
**HTTP DNS 方案**:
|
**HTTP DNS 方案**:
|
||||||
|
|
||||||
```
|
```
|
||||||
客户端 → 绕过系统 DNS → 直接问 HTTP DNS 服务(如 223.5.5.5:80)
|
客户端 → 绕过系统 DNS → 直接问 HTTP DNS 服务(如 223.5.5.5:80)
|
||||||
↓
|
↓
|
||||||
@@ -495,11 +523,13 @@ DNS 答:1.2.3.4(固定的)
|
|||||||
```
|
```
|
||||||
|
|
||||||
优势:
|
优势:
|
||||||
|
|
||||||
- 防劫持:不走运营商 DNS
|
- 防劫持:不走运营商 DNS
|
||||||
- 更精准:可以按客户端网络质量选择 IP
|
- 更精准:可以按客户端网络质量选择 IP
|
||||||
- 实时性:故障切换更快
|
- 实时性:故障切换更快
|
||||||
|
|
||||||
**实战建议**:
|
**实战建议**:
|
||||||
|
|
||||||
- 移动端 APP 强烈建议接入 HTTP DNS
|
- 移动端 APP 强烈建议接入 HTTP DNS
|
||||||
- Web 端可以使用 CDN 提供的 CNAME 调度
|
- Web 端可以使用 CDN 提供的 CNAME 调度
|
||||||
- 关键业务可以做多 IP 容灾(一个域名返回多个 IP)
|
- 关键业务可以做多 IP 容灾(一个域名返回多个 IP)
|
||||||
@@ -513,6 +543,7 @@ DNS 答:1.2.3.4(固定的)
|
|||||||
### 5.1 为什么 CDN 上 HTTPS 很重要?
|
### 5.1 为什么 CDN 上 HTTPS 很重要?
|
||||||
|
|
||||||
**场景对比**:
|
**场景对比**:
|
||||||
|
|
||||||
```
|
```
|
||||||
无 HTTPS:
|
无 HTTPS:
|
||||||
用户访问 http://cdn.example.com/image.jpg
|
用户访问 http://cdn.example.com/image.jpg
|
||||||
@@ -540,13 +571,14 @@ HTTP/2 多路复用生效
|
|||||||
#### 证书管理
|
#### 证书管理
|
||||||
|
|
||||||
| 方案 | 说明 | 成本 | 适用场景 |
|
| 方案 | 说明 | 成本 | 适用场景 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :--------------------- | :-------------------- | :------------- | :--------------- |
|
||||||
| **云厂商免费证书** | 阿里云/腾讯云等提供 | 免费 | 单域名,快速上手 |
|
| **云厂商免费证书** | 阿里云/腾讯云等提供 | 免费 | 单域名,快速上手 |
|
||||||
| **Let's Encrypt** | 社区免费证书 | 免费 | 自动化部署 |
|
| **Let's Encrypt** | 社区免费证书 | 免费 | 自动化部署 |
|
||||||
| **商业 DV/OV/EV 证书** | 赛门铁克、GeoTrust 等 | ¥几百-几万/年 | 企业级、需要绿条 |
|
| **商业 DV/OV/EV 证书** | 赛门铁克、GeoTrust 等 | ¥几百-几万/年 | 企业级、需要绿条 |
|
||||||
| **泛域名证书** | *.example.com | ¥几千/年 | 多子域名 |
|
| **泛域名证书** | \*.example.com | ¥几千/年 | 多子域名 |
|
||||||
|
|
||||||
**实战建议**:
|
**实战建议**:
|
||||||
|
|
||||||
- 测试环境:Let's Encrypt 或云厂商免费证书
|
- 测试环境:Let's Encrypt 或云厂商免费证书
|
||||||
- 生产环境:泛域名证书(省事)或单域名 OV 证书(省钱)
|
- 生产环境:泛域名证书(省事)或单域名 OV 证书(省钱)
|
||||||
- 注意证书过期时间,设置自动续期提醒
|
- 注意证书过期时间,设置自动续期提醒
|
||||||
@@ -554,18 +586,21 @@ HTTP/2 多路复用生效
|
|||||||
#### HTTPS 优化配置
|
#### HTTPS 优化配置
|
||||||
|
|
||||||
**TLS 版本选择**:
|
**TLS 版本选择**:
|
||||||
|
|
||||||
```
|
```
|
||||||
推荐配置:仅 TLS 1.2 和 TLS 1.3
|
推荐配置:仅 TLS 1.2 和 TLS 1.3
|
||||||
兼容配置:TLS 1.1 + TLS 1.2 + TLS 1.3(兼容老旧浏览器)
|
兼容配置:TLS 1.1 + TLS 1.2 + TLS 1.3(兼容老旧浏览器)
|
||||||
```
|
```
|
||||||
|
|
||||||
**密码套件**:
|
**密码套件**:
|
||||||
|
|
||||||
```
|
```
|
||||||
推荐:ECDHE 密钥交换 + AES-GCM 加密
|
推荐:ECDHE 密钥交换 + AES-GCM 加密
|
||||||
禁用:DES、RC4、MD5、SHA1
|
禁用:DES、RC4、MD5、SHA1
|
||||||
```
|
```
|
||||||
|
|
||||||
**OCSP Stapling**:
|
**OCSP Stapling**:
|
||||||
|
|
||||||
```
|
```
|
||||||
功能:CDN 节点预获取证书吊销状态
|
功能:CDN 节点预获取证书吊销状态
|
||||||
效果:减少客户端验证时间 200-500ms
|
效果:减少客户端验证时间 200-500ms
|
||||||
@@ -573,6 +608,7 @@ HTTP/2 多路复用生效
|
|||||||
```
|
```
|
||||||
|
|
||||||
**TLS 会话复用**:
|
**TLS 会话复用**:
|
||||||
|
|
||||||
```
|
```
|
||||||
Session ID 复用:客户端带着上次 Session ID,服务端恢复会话
|
Session ID 复用:客户端带着上次 Session ID,服务端恢复会话
|
||||||
Session Ticket 复用:服务端把会话状态加密发给客户端,下次带来
|
Session Ticket 复用:服务端把会话状态加密发给客户端,下次带来
|
||||||
@@ -582,6 +618,7 @@ Session Ticket 复用:服务端把会话状态加密发给客户端,下次
|
|||||||
### 5.3 HTTP/2 与 HTTP/3 在 CDN 上的应用
|
### 5.3 HTTP/2 与 HTTP/3 在 CDN 上的应用
|
||||||
|
|
||||||
**HTTP/2 多路复用**:
|
**HTTP/2 多路复用**:
|
||||||
|
|
||||||
```
|
```
|
||||||
HTTP/1.1:
|
HTTP/1.1:
|
||||||
请求 1 (index.html) ────────────────→
|
请求 1 (index.html) ────────────────→
|
||||||
@@ -603,6 +640,7 @@ HTTP/2:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**HTTP/2 服务端推送**:
|
**HTTP/2 服务端推送**:
|
||||||
|
|
||||||
```
|
```
|
||||||
场景:用户请求 index.html,里面引用了 style.css 和 script.js
|
场景:用户请求 index.html,里面引用了 style.css 和 script.js
|
||||||
|
|
||||||
@@ -620,6 +658,7 @@ HTTP/2 推送:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**HTTP/3 (QUIC)**:
|
**HTTP/3 (QUIC)**:
|
||||||
|
|
||||||
```
|
```
|
||||||
HTTP/2 的问题:基于 TCP,队头阻塞
|
HTTP/2 的问题:基于 TCP,队头阻塞
|
||||||
→ 一个 TCP 包丢失,整个连接等待重传
|
→ 一个 TCP 包丢失,整个连接等待重传
|
||||||
@@ -654,6 +693,7 @@ CDN 带宽 = 所有边缘节点的出流量总和
|
|||||||
```
|
```
|
||||||
|
|
||||||
**带宽与流量的关系**:
|
**带宽与流量的关系**:
|
||||||
|
|
||||||
```
|
```
|
||||||
1 Mbps 带宽持续跑 1 小时 = 450 MB 流量
|
1 Mbps 带宽持续跑 1 小时 = 450 MB 流量
|
||||||
(计算:1,000,000 bps × 3600s ÷ 8 ÷ 1024 ÷ 1024 ≈ 429 MB)
|
(计算:1,000,000 bps × 3600s ÷ 8 ÷ 1024 ÷ 1024 ≈ 429 MB)
|
||||||
@@ -690,7 +730,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
|||||||
**命中率低的常见原因**:
|
**命中率低的常见原因**:
|
||||||
|
|
||||||
| 原因 | 现象 | 解决方案 |
|
| 原因 | 现象 | 解决方案 |
|
||||||
| :--- | :--- | :--- |
|
| :------------- | :----------------- | :----------------------- |
|
||||||
| 缓存时间太短 | TTL 只有几分钟 | 根据文件类型调整 TTL |
|
| 缓存时间太短 | TTL 只有几分钟 | 根据文件类型调整 TTL |
|
||||||
| 查询参数变化 | URL 带随机数 | 配置忽略特定参数 |
|
| 查询参数变化 | URL 带随机数 | 配置忽略特定参数 |
|
||||||
| 缓存键设置不当 | 不该区分的被区分了 | 优化缓存键规则 |
|
| 缓存键设置不当 | 不该区分的被区分了 | 优化缓存键规则 |
|
||||||
@@ -713,7 +753,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
|||||||
关键字段解释:
|
关键字段解释:
|
||||||
|
|
||||||
| 字段 | 说明 | 分析价值 |
|
| 字段 | 说明 | 分析价值 |
|
||||||
| :--- | :--- | :--- |
|
| :-------------- | :------------- | :------------------------------------------- |
|
||||||
| `cache_status` | 缓存状态 | HIT(命中)、MISS(未命中)、EXPIRED(过期) |
|
| `cache_status` | 缓存状态 | HIT(命中)、MISS(未命中)、EXPIRED(过期) |
|
||||||
| `response_time` | 响应时间(ms) | 判断用户体验,>500ms 需优化 |
|
| `response_time` | 响应时间(ms) | 判断用户体验,>500ms 需优化 |
|
||||||
| `http_status` | HTTP 状态码 | 404/500 错误排查 |
|
| `http_status` | HTTP 状态码 | 404/500 错误排查 |
|
||||||
@@ -724,6 +764,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
|||||||
**问题 1:用户反映访问慢**
|
**问题 1:用户反映访问慢**
|
||||||
|
|
||||||
排查步骤:
|
排查步骤:
|
||||||
|
|
||||||
```
|
```
|
||||||
1. 看日志 response_time
|
1. 看日志 response_time
|
||||||
- 如果很大(>500ms):检查是缓存 MISS 还是源站慢
|
- 如果很大(>500ms):检查是缓存 MISS 还是源站慢
|
||||||
@@ -739,6 +780,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
|||||||
**问题 2:缓存不生效,每次都回源**
|
**问题 2:缓存不生效,每次都回源**
|
||||||
|
|
||||||
排查清单:
|
排查清单:
|
||||||
|
|
||||||
```
|
```
|
||||||
□ 源站响应头是否有 Cache-Control: no-cache / private?
|
□ 源站响应头是否有 Cache-Control: no-cache / private?
|
||||||
□ URL 是否带随机参数(如 ?_=123456)?
|
□ URL 是否带随机参数(如 ?_=123456)?
|
||||||
@@ -750,6 +792,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
|||||||
**问题 3:费用暴涨**
|
**问题 3:费用暴涨**
|
||||||
|
|
||||||
排查方向:
|
排查方向:
|
||||||
|
|
||||||
```
|
```
|
||||||
1. 看账单明细
|
1. 看账单明细
|
||||||
- CDN 流量费高:检查是否有大文件被频繁访问,或被盗链
|
- CDN 流量费高:检查是否有大文件被频繁访问,或被盗链
|
||||||
@@ -848,6 +891,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
|||||||
#### 对象存储配置
|
#### 对象存储配置
|
||||||
|
|
||||||
**存储桶规划**:
|
**存储桶规划**:
|
||||||
|
|
||||||
```
|
```
|
||||||
Bucket: myapp-images-prod
|
Bucket: myapp-images-prod
|
||||||
├─ 目录结构:
|
├─ 目录结构:
|
||||||
@@ -872,6 +916,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
|||||||
```
|
```
|
||||||
|
|
||||||
**CORS 跨域配置**:
|
**CORS 跨域配置**:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<CORSConfiguration>
|
<CORSConfiguration>
|
||||||
<CORSRule>
|
<CORSRule>
|
||||||
@@ -890,6 +935,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
|||||||
#### CDN 加速配置
|
#### CDN 加速配置
|
||||||
|
|
||||||
**缓存策略配置**:
|
**缓存策略配置**:
|
||||||
|
|
||||||
```
|
```
|
||||||
全局默认规则:
|
全局默认规则:
|
||||||
├─ 缓存键:URL 路径 + 保留 w、h、format 查询参数
|
├─ 缓存键:URL 路径 + 保留 w、h、format 查询参数
|
||||||
@@ -916,6 +962,7 @@ CDN QPS = 所有边缘节点每秒处理的 HTTP 请求总数
|
|||||||
```
|
```
|
||||||
|
|
||||||
**HTTPS 优化配置**:
|
**HTTPS 优化配置**:
|
||||||
|
|
||||||
```
|
```
|
||||||
证书配置:
|
证书配置:
|
||||||
├─ 证书类型:泛域名证书 *.myapp.com
|
├─ 证书类型:泛域名证书 *.myapp.com
|
||||||
@@ -1049,12 +1096,14 @@ bandwidth_cap:
|
|||||||
### 8.1 架构设计原则
|
### 8.1 架构设计原则
|
||||||
|
|
||||||
**原则 1:动静分离**
|
**原则 1:动静分离**
|
||||||
|
|
||||||
```
|
```
|
||||||
动态内容(API、HTML)→ 走源站或边缘函数
|
动态内容(API、HTML)→ 走源站或边缘函数
|
||||||
静态内容(图片、JS、CSS、视频)→ 走 CDN + 对象存储
|
静态内容(图片、JS、CSS、视频)→ 走 CDN + 对象存储
|
||||||
```
|
```
|
||||||
|
|
||||||
**原则 2:就近服务**
|
**原则 2:就近服务**
|
||||||
|
|
||||||
```
|
```
|
||||||
用户在哪里,内容就缓存到哪里
|
用户在哪里,内容就缓存到哪里
|
||||||
→ 选择覆盖广的 CDN 服务商
|
→ 选择覆盖广的 CDN 服务商
|
||||||
@@ -1063,6 +1112,7 @@ bandwidth_cap:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**原则 3:分层缓存**
|
**原则 3:分层缓存**
|
||||||
|
|
||||||
```
|
```
|
||||||
浏览器本地缓存(最强)
|
浏览器本地缓存(最强)
|
||||||
↓
|
↓
|
||||||
@@ -1074,6 +1124,7 @@ CDN 中间层/区域节点(兜底)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**原则 4:成本与体验的平衡**
|
**原则 4:成本与体验的平衡**
|
||||||
|
|
||||||
```
|
```
|
||||||
存储分级:热数据标准存储,冷数据归档存储
|
存储分级:热数据标准存储,冷数据归档存储
|
||||||
缓存策略:高频内容长 TTL,低频内容短 TTL
|
缓存策略:高频内容长 TTL,低频内容短 TTL
|
||||||
@@ -1084,24 +1135,28 @@ CDN 中间层/区域节点(兜底)
|
|||||||
### 8.2 避坑清单
|
### 8.2 避坑清单
|
||||||
|
|
||||||
**存储桶命名与权限**
|
**存储桶命名与权限**
|
||||||
|
|
||||||
- [ ] 桶名全局唯一,避免被占用
|
- [ ] 桶名全局唯一,避免被占用
|
||||||
- [ ] 私有文件不要设置为公共读
|
- [ ] 私有文件不要设置为公共读
|
||||||
- [ ] AccessKey 不要写在前端代码里,用 STS 临时凭证
|
- [ ] AccessKey 不要写在前端代码里,用 STS 临时凭证
|
||||||
- [ ] 启用服务端加密(SSE)保护敏感数据
|
- [ ] 启用服务端加密(SSE)保护敏感数据
|
||||||
|
|
||||||
**CDN 缓存配置**
|
**CDN 缓存配置**
|
||||||
|
|
||||||
- [ ] HTML 文件 TTL 不要太长(建议 < 5 分钟)
|
- [ ] HTML 文件 TTL 不要太长(建议 < 5 分钟)
|
||||||
- [ ] JS/CSS 建议用带 hash 的文件名,TTL 设为 1 年
|
- [ ] JS/CSS 建议用带 hash 的文件名,TTL 设为 1 年
|
||||||
- [ ] 缓存键要合理,不要把用户信息等变量放进去
|
- [ ] 缓存键要合理,不要把用户信息等变量放进去
|
||||||
- [ ] 重要更新后记得刷新缓存或预热
|
- [ ] 重要更新后记得刷新缓存或预热
|
||||||
|
|
||||||
**HTTPS 安全**
|
**HTTPS 安全**
|
||||||
|
|
||||||
- [ ] 证书不要过期,设置自动续期
|
- [ ] 证书不要过期,设置自动续期
|
||||||
- [ ] 最低 TLS 版本建议 1.2
|
- [ ] 最低 TLS 版本建议 1.2
|
||||||
- [ ] 开启 HSTS 防止降级攻击
|
- [ ] 开启 HSTS 防止降级攻击
|
||||||
- [ ] 敏感 Cookie 设置 Secure 和 HttpOnly
|
- [ ] 敏感 Cookie 设置 Secure 和 HttpOnly
|
||||||
|
|
||||||
**成本控制**
|
**成本控制**
|
||||||
|
|
||||||
- [ ] 开启带宽封顶告警,防止异常流量
|
- [ ] 开启带宽封顶告警,防止异常流量
|
||||||
- [ ] 低频/归档存储有最小存储时间和提前删除费,注意规则
|
- [ ] 低频/归档存储有最小存储时间和提前删除费,注意规则
|
||||||
- [ ] 回源流量费也很贵,努力提高 CDN 命中率
|
- [ ] 回源流量费也很贵,努力提高 CDN 命中率
|
||||||
@@ -1170,7 +1225,12 @@ class DirectUploader {
|
|||||||
const formData = new FormData()
|
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]) => {
|
Object.entries(formFields).forEach(([key, value]) => {
|
||||||
formData.append(key, value)
|
formData.append(key, value)
|
||||||
})
|
})
|
||||||
@@ -1210,7 +1270,11 @@ class DirectUploader {
|
|||||||
const fileKey = this._generateFileKey(file, options.directory)
|
const fileKey = this._generateFileKey(file, options.directory)
|
||||||
|
|
||||||
// 1. 初始化分片上传
|
// 1. 初始化分片上传
|
||||||
const uploadId = await this._initMultipartUpload(credentials, fileKey, file.type)
|
const uploadId = await this._initMultipartUpload(
|
||||||
|
credentials,
|
||||||
|
fileKey,
|
||||||
|
file.type
|
||||||
|
)
|
||||||
|
|
||||||
// 2. 计算分片
|
// 2. 计算分片
|
||||||
const parts = []
|
const parts = []
|
||||||
@@ -1232,21 +1296,30 @@ class DirectUploader {
|
|||||||
|
|
||||||
// 支持断点续传:检查哪些分片已上传
|
// 支持断点续传:检查哪些分片已上传
|
||||||
if (options.resume) {
|
if (options.resume) {
|
||||||
const existingParts = await this._listParts(credentials, fileKey, uploadId)
|
const existingParts = await this._listParts(
|
||||||
|
credentials,
|
||||||
|
fileKey,
|
||||||
|
uploadId
|
||||||
|
)
|
||||||
for (const part of existingParts) {
|
for (const part of existingParts) {
|
||||||
uploadedParts.push(part)
|
uploadedParts.push(part)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤出未上传的分片
|
// 过滤出未上传的分片
|
||||||
const pendingParts = parts.filter(p =>
|
const pendingParts = parts.filter(
|
||||||
!uploadedParts.some(up => up.partNumber === p.number)
|
(p) => !uploadedParts.some((up) => up.partNumber === p.number)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 并发上传
|
// 并发上传
|
||||||
const uploadPart = async (part) => {
|
const uploadPart = async (part) => {
|
||||||
try {
|
try {
|
||||||
const etag = await this._uploadPart(credentials, fileKey, uploadId, part)
|
const etag = await this._uploadPart(
|
||||||
|
credentials,
|
||||||
|
fileKey,
|
||||||
|
uploadId,
|
||||||
|
part
|
||||||
|
)
|
||||||
return { partNumber: part.number, etag }
|
return { partNumber: part.number, etag }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
failedParts.push({ part, error })
|
failedParts.push({ part, error })
|
||||||
@@ -1271,11 +1344,18 @@ class DirectUploader {
|
|||||||
|
|
||||||
// 检查是否所有分片都上传成功
|
// 检查是否所有分片都上传成功
|
||||||
if (uploadedParts.length !== totalParts) {
|
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. 完成分片上传(合并分片)
|
// 4. 完成分片上传(合并分片)
|
||||||
await this._completeMultipartUpload(credentials, fileKey, uploadId, uploadedParts)
|
await this._completeMultipartUpload(
|
||||||
|
credentials,
|
||||||
|
fileKey,
|
||||||
|
uploadId,
|
||||||
|
uploadedParts
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: this._getFileUrl(fileKey),
|
url: this._getFileUrl(fileKey),
|
||||||
@@ -1316,7 +1396,11 @@ class DirectUploader {
|
|||||||
|
|
||||||
_getFileUrl(key) {
|
_getFileUrl(key) {
|
||||||
return `https://${this.bucket}.${this.provider === 'oss' ? 'oss' : 'cos'}-${this.region}.${
|
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}`
|
}/${key}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1385,7 +1469,9 @@ async function uploadVideo(file) {
|
|||||||
parallel: 3, // 3 个并发
|
parallel: 3, // 3 个并发
|
||||||
resume: true, // 支持断点续传
|
resume: true, // 支持断点续传
|
||||||
onProgress: (progress) => {
|
onProgress: (progress) => {
|
||||||
console.log(`上传进度: ${progress.percent}%, 已传 ${progress.loaded}/${progress.total}`)
|
console.log(
|
||||||
|
`上传进度: ${progress.percent}%, 已传 ${progress.loaded}/${progress.total}`
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onPartComplete: (part) => {
|
onPartComplete: (part) => {
|
||||||
console.log(`分片 ${part.number} 上传完成`)
|
console.log(`分片 ${part.number} 上传完成`)
|
||||||
@@ -1464,9 +1550,7 @@ router.post('/credentials', async (req, res) => {
|
|||||||
'oss:AbortMultipartUpload',
|
'oss:AbortMultipartUpload',
|
||||||
'oss:ListParts'
|
'oss:ListParts'
|
||||||
],
|
],
|
||||||
Resource: [
|
Resource: [`acs:oss:*:*:${config.oss.bucket}/${prefix}*`]
|
||||||
`acs:oss:*:*:${config.oss.bucket}/${prefix}*`
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
Version: '1'
|
Version: '1'
|
||||||
@@ -1495,7 +1579,13 @@ router.post('/credentials', async (req, res) => {
|
|||||||
prefix: prefix, // 文件路径前缀
|
prefix: prefix, // 文件路径前缀
|
||||||
// 安全限制
|
// 安全限制
|
||||||
maxSize: 100 * 1024 * 1024, // 最大 100MB
|
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'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1649,7 +1739,10 @@ const auth = new URLAuth({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 服务端生成带签名的 URL
|
// 服务端生成带签名的 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
|
// 结果:https://cdn.example.com/private/document.pdf?sign=xxxxx&t=1699123456
|
||||||
|
|
||||||
// CDN 边缘或源站验证
|
// CDN 边缘或源站验证
|
||||||
@@ -1667,10 +1760,7 @@ const ipConfig = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
// 禁止特定 IP 访问(封禁攻击者)
|
// 禁止特定 IP 访问(封禁攻击者)
|
||||||
blackList: [
|
blackList: ['1.2.3.4', '5.6.7.8']
|
||||||
'1.2.3.4',
|
|
||||||
'5.6.7.8'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. UA(User-Agent)黑白名单
|
// 4. UA(User-Agent)黑白名单
|
||||||
@@ -1698,7 +1788,7 @@ const uaConfig = {
|
|||||||
## 10. 名词对照表
|
## 10. 名词对照表
|
||||||
|
|
||||||
| 英文术语 | 中文对照 | 解释 |
|
| 英文术语 | 中文对照 | 解释 |
|
||||||
| :--- | :--- | :--- |
|
| :------------------------- | :---------------- | :--------------------------------------------------------------------------------------------------- |
|
||||||
| **Object Storage** | 对象存储 | 一种数据存储架构,将数据作为对象管理,而非文件系统层级结构。适合存储图片、视频、备份等非结构化数据。 |
|
| **Object Storage** | 对象存储 | 一种数据存储架构,将数据作为对象管理,而非文件系统层级结构。适合存储图片、视频、备份等非结构化数据。 |
|
||||||
| **Bucket** | 存储桶 | 对象存储中的顶级容器,用于组织和隔离数据。每个桶有独立的权限控制和配置。 |
|
| **Bucket** | 存储桶 | 对象存储中的顶级容器,用于组织和隔离数据。每个桶有独立的权限控制和配置。 |
|
||||||
| **Object** | 对象/文件对象 | 对象存储的基本单元,包含数据本身、元数据(Metadata)和全局唯一键(Key)。 |
|
| **Object** | 对象/文件对象 | 对象存储的基本单元,包含数据本身、元数据(Metadata)和全局唯一键(Key)。 |
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,424 +1,461 @@
|
|||||||
# 前端开发入门:从"贴海报"到"搭乐高" (Interactive Intro)
|
# 前端开发演进史:从"贴海报"到"搭乐高"
|
||||||
|
|
||||||
> 💡 **学习指南**:本章节无需编程基础,通过交互式演示带你回顾前端开发的 20 年变迁。我们将从最基础的 HTML 讲起,一直到现代的 Vue/React 组件化开发。
|
::: tip 🎯 核心问题
|
||||||
|
**为什么网页越来越复杂?前端技术为什么要不断演进?** 这个问题会带你理解从简单网页到现代 Web 应用的技术演变之路。
|
||||||
|
:::
|
||||||
|
|
||||||
先把几个最常见的新名词说清楚(后面会反复出现):
|
---
|
||||||
|
|
||||||
- **HTML**:网页的"骨架",负责内容和结构(标题、段落、图片、按钮)。
|
## 1. 为什么要关注前端演进史?
|
||||||
- **CSS**:网页的"皮肤",负责样式(颜色、大小、布局、动画)。
|
|
||||||
- **JavaScript**:网页的"肌肉",负责交互与逻辑(点击、输入、请求数据)。
|
### 1.1 从"电子海报"到"桌面应用"
|
||||||
- **框架(Framework)**:一套成熟的开发方式和工具,让你更高效地做复杂页面(比如 Vue/React)。
|
|
||||||
|
想象一下你在街上看到的**海报**:
|
||||||
|
|
||||||
|
- ✅ 有内容(文字、图片)
|
||||||
|
- ✅ 有设计(颜色、排版)
|
||||||
|
- ❌ 但你跟它说话,它不会回应
|
||||||
|
- ❌ 你点击某个地方,不会发生什么
|
||||||
|
|
||||||
|
**最早的网页**就是这样的"电子海报":只能看、不能改、内容固定。
|
||||||
|
|
||||||
|
**现代网页**完全不同了。它们像**桌面应用**(VS Code、Figma):
|
||||||
|
|
||||||
|
- ✅ 可以编辑文档、画图、玩游戏
|
||||||
|
- ✅ 实时响应你的每个操作
|
||||||
|
- ✅ 甚至可以离线工作
|
||||||
|
|
||||||
|
**这种转变的核心原因: 网页的功能越来越复杂,需要更高效的技术和开发方式。**
|
||||||
|
|
||||||
|
### 1.2 一个生活的比喻:盖房子
|
||||||
|
|
||||||
|
前端技术的演进,就像盖房子方式的进化:
|
||||||
|
|
||||||
|
| 时代 | 🏠 盖房比喻 | 实际特点 | 优缺点 |
|
||||||
|
| --------- | ------------------ | ---------------------------- | --------------------------- |
|
||||||
|
| **2000s** | **贴海报** | 静态网页,写好 HTML 就行 | ✅ 简单 ❌ 不能互动 |
|
||||||
|
| **2010s** | **请工人手动装修** | jQuery 时代,手动操作每个元素 | ✅ 能互动 ❌ 代码乱、难维护 |
|
||||||
|
| **2020s** | **用乐高搭房子** | Vue/React 时代,组件化开发 | ✅ 高效、可维护 ❌ 学习曲线 |
|
||||||
|
|
||||||
|
::: tip 💡 从表格中你能看到什么?
|
||||||
|
|
||||||
|
**阶段一 → 阶段二**: 从"不能动"到"能动"。这是质的飞跃——网页开始有交互,但代价是代码变得混乱。
|
||||||
|
|
||||||
|
**阶段二 → 阶段三**: 从"能用"到"好用"。组件化让代码像积木一样可复用,大幅提升开发效率。
|
||||||
|
|
||||||
|
**核心思想**: 技术演进不是"为了新而新",而是为了解决上一个阶段的痛点。
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 第一阶段:静态网页与"切图"(2000s)
|
||||||
|
|
||||||
<FrontendEvolutionDemo />
|
<FrontendEvolutionDemo />
|
||||||
|
|
||||||
## 0. 引言:网页为什么越来越难做?
|
### 2.1 这个时代是什么样的?
|
||||||
|
|
||||||
最早的网页,只是**电子海报**——就像你在街上看到的纸质海报,只能看、不能互动。
|
**开发方式**:
|
||||||
|
|
||||||
现在的网页,是**桌面级应用** (如 VS Code, Figma)——可以编辑文档、画图、玩游戏,甚至剪辑视频。
|
- 写几个 HTML 文件
|
||||||
|
- 内嵌一些 CSS 和 JavaScript
|
||||||
|
- 直接把文件拖到浏览器就能看效果
|
||||||
|
- 上传文件夹到服务器就完成部署
|
||||||
|
|
||||||
为了支撑这种转变,前端技术经历了一场从 "手工作坊" 到 "工业化生产" 的革命。
|
**特点**:
|
||||||
|
|
||||||
### 一个生活的比喻
|
- ✅ **优点**: 简单直接,没有学习成本,写完就能跑
|
||||||
|
- ❌ **缺点**: 无法实现复杂交互,代码一多就乱
|
||||||
|
|
||||||
想象你要盖房子:
|
::: details 查看当时的项目结构
|
||||||
|
|
||||||
- **2000 年代(静态网页)**:就像**贴海报**。你画好一张图,贴到墙上就完事了,不能改动。
|
```
|
||||||
- **2010 年代(jQuery 时代)**:就像**请工人手动装修**。你需要亲自告诉工人:"把这块墙涂成蓝色"、"把那扇窗户打开"。工人很多、指令很杂,容易出错。
|
project/
|
||||||
- **2020 年代(Vue/React 时代)**:就像**用乐高积木搭房子**。你先设计好"房子长什么样"(设计图),然后乐高积木(组件)会自动按设计图组装好,不需要你一块一块手动拼。
|
├── 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 页面。
|
<SliceRequestDemo />
|
||||||
- **大前端(Big Frontend)**:泛指"所有用户界面相关的开发"。不只 Web,还包括小程序、App、桌面应用等。
|
|
||||||
|
|
||||||
这里的几个新词(后面也会用到):
|
你可能听说过"切图"这个词。它是早期前端的主要工作:
|
||||||
|
|
||||||
- **端**:平台/运行环境的意思,比如 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 文件(不像现在很多页面是"数据驱动、可交互"的)。
|
- ❌ **API 繁琐**: 简单的操作也要写很多代码
|
||||||
- **UI**:User Interface,用户界面。也就是你看到的按钮、颜色、布局。
|
- ❌ **浏览器兼容**: 不同浏览器的 API 不一样,要写很多兼容代码
|
||||||
|
- ❌ **选择器弱**: 找元素很麻烦
|
||||||
|
|
||||||
### 1.1 为什么会慢?
|
**jQuery** 诞生了。它让 JavaScript 变得简单:
|
||||||
|
|
||||||
网页上的每一张小图,浏览器都要发一次**网络请求**。
|
```javascript
|
||||||
请求越多,加载越慢。
|
// 原生 JavaScript (繁琐)
|
||||||
|
const element = document.getElementById('title')
|
||||||
|
|
||||||
想象一下你点外卖:
|
// jQuery (简洁)
|
||||||
- 如果你一次性下单 10 道菜,餐厅可以一起做完送过来。
|
const element = $('#title')
|
||||||
- 但如果你分 10 次下单,每次只点 1 道菜,骑手要跑 10 趟!
|
```
|
||||||
|
|
||||||
早期的网页就像"分 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)
|
||||||
|
|
||||||
手机和平板开始流行以后,网页必须适配不同屏幕。
|
### 4.1 为什么需要新框架?
|
||||||
这就需要**响应式布局**:同一套 HTML/CSS,自动根据屏幕宽度变换布局。
|
|
||||||
|
|
||||||
这里用到了**媒体查询 (Media Query)**:
|
jQuery 时代的问题积累到一定程度:
|
||||||
它是 CSS 里的"条件判断",比如"如果屏幕小于 640px,就用 1 列布局"。
|
|
||||||
|
|
||||||
想象一下你在不同房间看同一张照片:
|
- **代码一多就乱**: 到处都是 DOM 操作,难以维护
|
||||||
- 在**大客厅**(电脑屏幕),照片可以摆大一些,旁边还能放其他装饰品。
|
- **容易出 bug**: 漏更新一个地方,页面就不一致
|
||||||
- 在**小卧室**(手机屏幕),照片需要缩小,其他装饰品要收起来,否则会挤不下。
|
- **协作困难**: 多人修改同一个文件,容易冲突
|
||||||
|
|
||||||
**响应式布局**就是"智能相框",它会自动根据房间大小调整展示方式。
|
**Vue / React** 的核心思路:**只改数据,页面自动更新**。
|
||||||
|
|
||||||
<EvolutionResponsiveGridDemo />
|
### 4.2 Vue/React 的思路:声明式 UI
|
||||||
|
|
||||||
**关键点**:响应式让网页"会变形",不再只适配电脑。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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`,界面自动变。
|
|
||||||
|
|
||||||
<ImperativeVsDeclarativeDemo />
|
<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 常用 Template;React 常用 JSX
|
- **命令式**: 你告诉画家"拿起笔,蘸红颜料,在坐标(10,10)画一个圈"
|
||||||
- **状态变化时怎么更新**:Vue 更偏"依赖追踪";React 更偏"重新渲染组件函数"
|
- **声明式**: 你直接给画家一张照片,"给我画成这样"
|
||||||
|
|
||||||
这里的几个新词(像课件一样解释清楚):
|
Vue/React 就是"声明式": 你描述"页面长什么样",框架负责"怎么把它画出来"。
|
||||||
|
:::
|
||||||
|
|
||||||
- **Template**:Vue 常见写法,用类似 HTML 的语法来写界面。
|
### 4.3 组件化:像搭乐高一样写页面
|
||||||
- **JSX**:React 常见写法,用"像写 JS 一样"的方式写界面结构。
|
|
||||||
- **Hook**:React 的一套函数式能力(比如 `useState`),用来保存状态、处理副作用。
|
|
||||||
- **SFC**:Single File Component,Vue 常见的单文件组件(一个 `.vue` 文件里写模板/逻辑/样式)。
|
|
||||||
|
|
||||||
<VueReactComparisonDemo />
|
**Vue / React** 最强大的特性是**组件化**: 把页面拆成一个个独立的"积木"。
|
||||||
|
|
||||||
**关键点**:别死记名词。你只要记住一句话:它们都能做同样的产品,只是"写法和心智模型"不一样。
|
想象一下你在搭乐高:
|
||||||
|
|
||||||
---
|
- 你不需要"从头开始雕刻每一块积木"(从头写 HTML/CSS)
|
||||||
|
- 你只需要"按说明书把积木拼在一起"(把组件组合起来)
|
||||||
|
- 每个积木都是**独立的**,你可以在不同的套装里**重复使用**
|
||||||
|
|
||||||
## 4. 第四阶段:组件化(像搭积木一样写页面)
|
**组件的好处**:
|
||||||
|
|
||||||
解决了"怎么更新页面"的问题,接下来是"怎么组织代码"。
|
- **复用**: 写一个"商品卡片"组件,可以用 100 次
|
||||||
以前一个页面可能是一个超大的 HTML 文件,改一个按钮可能牵连全局。
|
- **封装**: 组件内部的状态不影响别人
|
||||||
|
- **维护**: 修改一个组件,所有用到它的地方都会更新
|
||||||
|
|
||||||
### 4.1 "积木"是什么?
|
### 4.4 SPA:单页应用的诞生
|
||||||
|
|
||||||
现代前端把页面拆成了**组件**。
|
|
||||||
一个按钮、一个导航栏、一个商品卡片,都是独立的积木。
|
|
||||||
|
|
||||||
想象一下你在搭乐高:
|
|
||||||
- 你不需要"从头开始雕刻每一块积木"(从头写 HTML/CSS)。
|
|
||||||
- 你只需要"按说明书把积木拼在一起"(把组件组合起来)。
|
|
||||||
- 每个积木都是**独立的**,你可以在不同的套装里**重复使用**。
|
|
||||||
|
|
||||||
### 4.2 为什么组件能复用?
|
|
||||||
|
|
||||||
定义好一个"商品卡片"组件后,你可以由它生成 100 个实例。每个实例都有自己独立的状态(比如点赞状态),互不干扰。
|
|
||||||
|
|
||||||
想象一下你有一个"万能开关"组件:
|
|
||||||
- 你可以把这个开关放在客厅、卧室、厨房。
|
|
||||||
- 每个开关都是**独立的**:你按客厅的开关,不会影响到卧室的灯。
|
|
||||||
- 但它们都是**同一个组件**,你只需要设计一次"开关长什么样",就可以到处使用。
|
|
||||||
|
|
||||||
<ComponentReusabilityDemo />
|
|
||||||
|
|
||||||
**新名词解释**:
|
|
||||||
|
|
||||||
- **组件 (Component)**:页面里的"积木块",可以单独复用。
|
|
||||||
- **封装**:组件内部的状态不影响别人。
|
|
||||||
- **复用**:同一个组件可以用很多次。
|
|
||||||
|
|
||||||
**关键点**:组件化让页面像搭积木一样搭出来。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 第五阶段:页面切换体验(MPA vs SPA)
|
|
||||||
|
|
||||||
用户不再想要"每点一次就刷新整页"的体验。
|
|
||||||
于是出现了**单页应用 (SPA)**:整个网站只加载一次,之后只是切换内容。
|
|
||||||
|
|
||||||
与之对应的是**多页应用 (MPA)**:每点一次都会重新加载整个页面。
|
|
||||||
|
|
||||||
这里的一个新词:**路由 (Routing)**。
|
|
||||||
简单理解:就是"从 A 页面切到 B 页面"的规则和过程。
|
|
||||||
|
|
||||||
再补两个新词(非常重要):
|
|
||||||
|
|
||||||
- **前端路由**:页面切换主要由浏览器里的 JavaScript 控制(常见于 SPA)。
|
|
||||||
- **后端路由**:页面切换主要由服务器决定"返回哪个页面"(常见于 MPA)。
|
|
||||||
|
|
||||||
想象一下你在看一本书:
|
|
||||||
- **MPA(多页应用)**:就像**翻书**。每翻一页,你都要把旧书合上、去书架上拿一本新书。慢,而且你之前在书上做的笔记(比如折页)都会消失。
|
|
||||||
- **SPA(单页应用)**:就像**在同一页纸上换内容**。你只需要擦掉旧内容、写上新内容,**纸还是那张纸**。快,而且你做的笔记一直都在。
|
|
||||||
|
|
||||||
<RoutingModeDemo />
|
<RoutingModeDemo />
|
||||||
|
|
||||||
### 5.1 MPA 是什么?(多页应用)
|
**Vue / React** 时代还有一个重要变化:**从 MPA 到 SPA**。
|
||||||
|
|
||||||
MPA 的直觉很像"翻书":
|
**MPA (Multi-Page Application)**:
|
||||||
|
|
||||||
- 点"商品页" -> 浏览器向服务器要一个新的页面(新的 HTML)
|
- 点一个链接 → 整页刷新 → 显示新页面
|
||||||
- 旧页面被替换掉 -> 原来的输入、滚动位置、临时数据往往会消失
|
- 就像**翻书**: 每翻一页都要把旧书合上、去书架拿新书
|
||||||
|
|
||||||
想象一下你在逛商场:
|
**SPA (Single-Page Application)**:
|
||||||
- 每进一家店(点一个链接),你都要**走出商场、重新排队进门**(整页刷新)。
|
|
||||||
- 你在上一家店试过的衣服(输入的内容)、拿过的购物车,**全部清空**。
|
|
||||||
|
|
||||||
**优点(为什么很多网站仍在用)**:
|
- 点一个链接 → 只刷新内容区域 → 页面不刷新
|
||||||
|
- 就像**同一本书里换章节**: 只擦掉旧内容、写上新内容
|
||||||
|
|
||||||
- 结构简单:服务器负责"出页面",浏览器负责"展示"
|
**SPA 的优点**:
|
||||||
- SEO 友好:搜索引擎更容易直接看到页面内容
|
|
||||||
- 首屏容易快:因为服务器直接给了 HTML
|
|
||||||
|
|
||||||
**缺点**:
|
- ✅ **体验丝滑**: 页面切换快
|
||||||
|
- ✅ **状态好管理**: 输入的内容、滚动位置都在
|
||||||
- 体验偏"跳":整页刷新会白一下、加载一下
|
- ❌ **首屏可能慢**: 需要先下载 JavaScript
|
||||||
- 复杂交互会变难:页面之间共享状态不方便
|
- ❌ **SEO 要额外处理**: 搜索引擎可能抓不到内容(需要 SSR/SSG)
|
||||||
|
|
||||||
### 5.2 SPA 是什么?(单页应用)
|
|
||||||
|
|
||||||
SPA 更像"同一本书里换章节":
|
|
||||||
|
|
||||||
- 第一次打开:加载一个"外壳页面"(HTML + CSS + JS)
|
|
||||||
- 之后切换页面:通常只换内容区域,整页不刷新
|
|
||||||
|
|
||||||
想象一下你在用手机的 App:
|
|
||||||
- 打开微信(第一次加载),之后你刷朋友圈、看聊天、进公众号,**页面不会重新加载**,只是内容在切换。
|
|
||||||
- 你输入了一半的消息、看到的滚动位置,**切换后再回来还在**。
|
|
||||||
|
|
||||||
**优点**:
|
|
||||||
|
|
||||||
- 体验丝滑:页面切换快
|
|
||||||
- 状态好管理:同一个页面里,数据更容易共享(登录态、购物车等)
|
|
||||||
|
|
||||||
**缺点(也要知道)**:
|
|
||||||
|
|
||||||
- 首次加载可能更慢:需要先下载一堆 JS
|
|
||||||
- SEO 要额外处理:通常需要 SSR/SSG 方案配合(后面第 7 阶段会讲)
|
|
||||||
|
|
||||||
### 5.3 交互演示:状态会不会丢?
|
|
||||||
|
|
||||||
下面这个小实验更直观:输入一段文字,然后切换页面再回来,看看有没有被清空。
|
|
||||||
|
|
||||||
想象一下你正在填写一张申请表:
|
|
||||||
- **MPA(翻书模式)**:你填到一半,去另一页查资料,回来发现**表格被清空了**,要重新填。
|
|
||||||
- **SPA(同一页模式)**:你填到一半,去另一页查资料,回来发现**表格还在**,继续填就行。
|
|
||||||
|
|
||||||
<SpaStatePreservationDemo />
|
|
||||||
|
|
||||||
**关键点**:从"整页刷新"到"局部更新",带来的不仅是速度,更是"状态能不能保留"的体验差异。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 第六阶段:工程化(从"手工作坊"到"现代化工厂")
|
## 5. 渲染策略:从 CSR 到 SSR/SSG
|
||||||
|
|
||||||
前端项目越来越大,不能再靠手动引入脚本文件。
|
|
||||||
于是有了**打包工具**(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,页面变得"可以点、可以用"的时间。
|
|
||||||
|
|
||||||
<RenderingStrategyDemo />
|
<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)。
|
### 7.1 效率:从手动到自动
|
||||||
2. **规模**:从 巨型面条代码 -> 组件化 + 工程化。
|
|
||||||
|
|
||||||
**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 写熟(布局、响应式)
|
## 8. 学习路线图
|
||||||
- 再学 JavaScript 的基础(变量、函数、事件)
|
|
||||||
- 最后上手一个框架(Vue/React),理解"状态驱动 UI"
|
### 8.1 如果你是零基础
|
||||||
|
|
||||||
|
**第 1 步: HTML/CSS/JavaScript 基础**
|
||||||
|
|
||||||
|
- 理解网页的三大基石
|
||||||
|
- 能写出简单的静态页面
|
||||||
|
|
||||||
|
**第 2 步: 学习一个框架(Vue 推荐)**
|
||||||
|
|
||||||
|
- 理解"数据驱动"的思想
|
||||||
|
- 掌握组件化开发
|
||||||
|
|
||||||
|
**第 3 步: 实战项目**
|
||||||
|
|
||||||
|
- 做一个完整的单页应用
|
||||||
|
- 熟悉路由、状态管理、API 调用
|
||||||
|
|
||||||
|
### 8.2 如果你有基础
|
||||||
|
|
||||||
|
**进阶方向**:
|
||||||
|
|
||||||
|
- **工程化**: 学习 Vite/Webpack,理解构建流程
|
||||||
|
- **性能优化**: 学习懒加载、代码分割、缓存策略
|
||||||
|
- **TypeScript**: 为代码加上类型,提升可靠性
|
||||||
|
- **服务端渲染**: 学习 Nuxt/Next.js,解决 SEO 和首屏问题
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 名词速查表 (Glossary)
|
## 9. 名词速查表 (Glossary)
|
||||||
|
|
||||||
| 名词 | 全称 | 解释 |
|
| 名词 | 英文 | 用人话解释 |
|
||||||
| :----------------- | :----------------------------------------- | :--------------------------------------------------------------------------------------------- |
|
| ---------------- | ----------------------- | --------------------------------------------- |
|
||||||
| **HTML** | HyperText Markup Language | 超文本标记语言。网页的骨架,定义内容和结构。 |
|
| **DOM** | Document Object Model | 文档对象模型。用对象树表示页面,可被 JS 读写。 |
|
||||||
| **CSS** | Cascading Style Sheets | 层叠样式表。网页的皮肤,定义颜色、布局、动画。 |
|
| **jQuery** | - | 早期流行的 JS 库,简化了 DOM 操作。 |
|
||||||
| **JS** | JavaScript | 网页的肌肉,负责交互和逻辑。 |
|
| **Vue/React** | - | 现代前端框架,采用数据驱动和组件化开发。 |
|
||||||
| **DOM** | Document Object Model | 文档对象模型。浏览器内部表示页面结构的树形对象。 |
|
| **组件** | Component | 可复用的 UI 单元,如按钮、卡片、导航栏。 |
|
||||||
| **jQuery** | - | 早期流行的 JS 库,简化了 DOM 操作。 |
|
|
||||||
| **Vue/React** | - | 现代前端框架,采用数据驱动和组件化开发。 |
|
|
||||||
| **State** | - | 状态。组件或应用的数据,状态变化驱动 UI 更新。 |
|
|
||||||
| **组件** | Component | 可复用的 UI 单元,如按钮、卡片、导航栏。 |
|
|
||||||
| **MPA** | Multi-Page Application | 多页应用。每次跳转都重新加载整个页面。 |
|
| **MPA** | Multi-Page Application | 多页应用。每次跳转都重新加载整个页面。 |
|
||||||
| **SPA** | Single-Page Application | 单页应用。只加载一次,后续切换不刷新页面。 |
|
| **SPA** | Single-Page Application | 单页应用。只加载一次,后续切换不刷新页面。 |
|
||||||
| **路由** | Routing | 管理页面之间切换的规则和过程。 |
|
| **路由** | Routing | 管理页面之间切换的规则和过程。 |
|
||||||
| **CSR** | Client-Side Rendering | 客户端渲染。浏览器下载 JS 后执行生成页面。 |
|
|
||||||
| **SSR** | Server-Side Rendering | 服务端渲染。服务器生成 HTML 后发给浏览器。 |
|
| **SSR** | Server-Side Rendering | 服务端渲染。服务器生成 HTML 后发给浏览器。 |
|
||||||
| **SSG** | Static Site Generation | 静态站点生成。构建时预渲染页面为静态 HTML。 |
|
| **SSG** | Static Site Generation | 静态站点生成。构建时预渲染页面为静态 HTML。 |
|
||||||
| **Bundle** | - | 包。打包工具将多个文件合并后的产物。 |
|
| **Webpack** | - | 传统打包工具,先打包后服务。 |
|
||||||
| **Tree Shaking** | - | 摇树优化。自动移除未使用的代码,减小包体积。 |
|
| **Vite** | - | 现代构建工具,按需编译,速度极快。 |
|
||||||
| **H5** | HTML5 | 通常指手机网页或基于 HTML5 的移动页面。 |
|
| **响应式** | Responsive Design | 页面自动适配不同屏幕尺寸的设计。 |
|
||||||
| **WebView** | - | 内嵌网页视图。App 中用于显示网页内容的组件。 |
|
| **媒体查询** | Media Query | CSS 的条件判断,根据屏幕宽度应用不同样式。 |
|
||||||
| **跨端** | Cross-Platform | 一套代码运行在多个平台(iOS、Android、Web 等)。 |
|
| **命令式** | Imperative | 告诉程序"怎么做"。 |
|
||||||
| **原生** | Native | 使用平台官方语言和 API 开发的应用。 |
|
| **声明式** | Declarative | 告诉程序"要什么"。 |
|
||||||
| **MVVM** | Model-View-ViewModel | 一种架构模式,实现数据(Model)和视图(View)的自动同步。 |
|
| **数据驱动** | Data-Driven | 只修改数据,界面自动更新。 |
|
||||||
| **SEO** | Search Engine Optimization | 搜索引擎优化,提高网页在搜索结果中的排名。 |
|
| **Tree Shaking** | - | 摇树优化。自动移除未使用的代码,减小包体积。 |
|
||||||
| **TTFB** | Time To First Byte | 首字节时间,从请求到收到第一个字节数据的耗时。 |
|
| **代码分割** | Code Splitting | 把代码分成多个小块,按需加载。 |
|
||||||
| **TTI** | Time To Interactive | 可交互时间,页面变为完全可交互状态所需的时间。 |
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
前端技术的演进,本质上是**从"手工"到"工业化"的进化**:
|
||||||
|
|
||||||
|
- **2000s**: 手工时代,简单直接
|
||||||
|
- **2010s**: 工具化时代,开始有框架
|
||||||
|
- **2020s**: 工业化时代,组件化 + 工程化
|
||||||
|
- **现在**: 智能化时代,AI 辅助开发
|
||||||
|
|
||||||
|
理解这个演进,你就能:
|
||||||
|
|
||||||
|
- 知道为什么要有 Vue/React
|
||||||
|
- 理解"数据驱动"的价值
|
||||||
|
- 明白工程化的必要性
|
||||||
|
- 快速上手新技术
|
||||||
|
|
||||||
|
**下一步建议**:
|
||||||
|
|
||||||
|
- 如果你想快速上手,学习 **Vue 3** (推荐) 或 **React**
|
||||||
|
- 如果你想深入理解,学习 **Vite** 构建流程
|
||||||
|
- 如果你想提升代码质量,学习 **TypeScript**
|
||||||
|
|
||||||
|
祝你学习愉快!
|
||||||
|
|||||||
+416
-1294
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,24 @@
|
|||||||
# Git 版本控制:时间的后悔药
|
# Git 版本控制:代码世界的时光机
|
||||||
|
|
||||||
> 💡 **一句话解释**:Git 就是代码世界的**游戏存档管理器**。它能让你随时“读档”重来,也能让你和队友在各自的“平行宇宙”里互不干扰地开发。
|
::: tip 🎯 核心问题
|
||||||
|
**写代码时最怕什么?** 写错了想回退、改崩了想重来、多人同时改同一个文件...这些头疼的事,Git 都能帮你搞定!它就像是代码世界的"时光机",让你随时回到过去,又能和队友在各自的"平行宇宙"里安全开发。
|
||||||
|
:::
|
||||||
|
|
||||||
> ✅ **安全说明**:本页所有交互组件都是“模拟器”,不会对你电脑上的真实 Git 仓库执行任何操作;但真实项目里建议严格按步骤来,不要依赖“自动下一步”。
|
---
|
||||||
|
|
||||||
## 0. 最常用的 5 个场景(直接照抄)
|
## 0. 最常用的 5 个场景(直接照抄)
|
||||||
|
|
||||||
如果你只想“立刻能用”,先把这块过一遍:每个场景都是现实工作中最常见的 Git 流程。
|
如果你只想"立刻能用",先把这块过一遍:每个场景都是现实工作中最常见的 Git 流程。
|
||||||
|
|
||||||
<GitScenariosDemo />
|
<GitScenariosDemo />
|
||||||
|
|
||||||
## 1. 为什么我们需要它?
|
---
|
||||||
|
|
||||||
你是否经历过这种绝望?
|
## 1. 为什么要学 Git?三大痛点
|
||||||
|
|
||||||
|
### 1.1 痛点一:版本混乱
|
||||||
|
|
||||||
|
你是否经历过这种绝望?
|
||||||
|
|
||||||
```text
|
```text
|
||||||
论文_初稿.doc
|
论文_初稿.doc
|
||||||
@@ -22,80 +28,322 @@
|
|||||||
论文_绝对是最后一次修改版.doc
|
论文_绝对是最后一次修改版.doc
|
||||||
```
|
```
|
||||||
|
|
||||||
**Git 完美解决了三个问题**:
|
**Git 的解决方案**:不需要复制副本,一个文件夹搞定所有历史版本。想回到哪次修改,一键恢复。
|
||||||
|
|
||||||
1. **版本混乱**:不需要复制副本,一个文件夹搞定所有历史版本。
|
### 1.2 痛点二:无法后悔
|
||||||
2. **无法后悔**:删错了代码?一秒钟找回三天前的状态。
|
|
||||||
3. **协作冲突**:你改了 A 文件,我改了 B 文件,Git 帮我们自动合并。
|
::: tip 💡 这个场景你一定遇到过
|
||||||
|
写代码写了 3 小时,突然发现之前的思路更好,但已经改不回去了...或者删错了一段代码,想找回原来的版本。
|
||||||
|
|
||||||
|
有了 Git,这种情况永远不会发生。每次重要节点都能"存档",出问题随时"读档"重来。
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 1.3 痛点三:协作冲突
|
||||||
|
|
||||||
|
你和队友同时改同一个文件:
|
||||||
|
|
||||||
|
- 你改了 A 文件的第 10 行
|
||||||
|
- 队友改了 A 文件的第 15 行
|
||||||
|
- 怎么合并?谁覆盖谁?
|
||||||
|
|
||||||
|
**Git 的解决方案**:智能合并,自动处理大部分冲突。只有当你们真的改了同一行代码时,才需要手动决定用谁的。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 核心概念:三个箱子
|
## 2. 核心概念:三区模型
|
||||||
|
|
||||||
Git 的设计哲学其实很像**寄快递**。
|
Git 的设计哲学其实很像**寄快递**。
|
||||||
|
|
||||||
<GitThreeAreasDemo />
|
<GitThreeAreasDemo />
|
||||||
|
|
||||||
- **工作区 (Working Dir)**:你的**书桌**。你正在这里写代码,想怎么乱改都行。
|
### 2.1 三个区域是什么?
|
||||||
- **暂存区 (Staging Area)**:**快递盒**。你把写好的文件放进去(`git add`),准备打包。
|
|
||||||
- **仓库 (Repository)**:**快递柜**。一旦你封箱寄出(`git commit`),这个版本就被永久记录下来了。
|
|
||||||
|
|
||||||
> 🔑 **关键点**:只有提交(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 />
|
<GitWorkflowDemo />
|
||||||
|
|
||||||
1. **修改代码**:在工作区写写画画。
|
### 3.1 第一步:修改代码(工作区)
|
||||||
2. **`git add`**:挑选你要保存的文件,放入暂存区。
|
|
||||||
3. **`git commit`**:给这次修改起个名字(比如“修复了登录 Bug”),永久存档。
|
在工作区写写画画,想怎么改就怎么改。这时候修改只在你本地,还没记录。
|
||||||
|
|
||||||
|
### 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 />
|
<GitBranchMergeDemo />
|
||||||
|
|
||||||
- **主分支 (Main/Master)**:稳定的线上版本,只有测试通过的代码才能进来。
|
### 4.1 主分支 vs 开发分支
|
||||||
- **开发分支 (Feature)**:你的试验田。你在这里炸了地球也没关系,不会影响主分支。
|
|
||||||
- **合并 (Merge)**:你在试验田里测试成功了,就把改动“合并”回主分支。
|
| 分支类型 | 作用 | 特点 |
|
||||||
|
| ------------------- | -------------- | ------------------------------------ |
|
||||||
|
| **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 init` | 初始化 | "在这里建个新仓库" | 项目开始时用一次 |
|
||||||
| `git status` | 查看状态 | "现在书桌上乱不乱?有没有东西没装箱?" |
|
| `git status` | 查看状态 | "现在乱不乱?有没有东西没提交?" | ⭐⭐⭐⭐⭐ 极高频 |
|
||||||
| `git add .` | 添加所有 | "把桌上所有文件都扔进快递盒" |
|
| `git add .` | 添加所有 | "把桌上所有文件都扔进快递盒" | ⭐⭐⭐⭐⭐ 每次提交前 |
|
||||||
| `git commit -m "..."` | 提交 | "封箱!贴上标签,写上这次改了啥" |
|
| `git add file.txt` | 添加单个 | "只要这个文件" | ⭐⭐⭐⭐ 选择性添加 |
|
||||||
| `git log` | 查看历史 | "翻翻以前的日记" |
|
| `git commit -m "..."` | 提交 | "封箱!贴上标签,写上这次改了啥" | ⭐⭐⭐⭐⭐ 完成功能时 |
|
||||||
| `git checkout -b dev` | 以此创建新分支 | "我要去平行宇宙 dev 探险了" |
|
| `git log` | 查看历史 | "翻翻以前的日记" | ⭐⭐⭐ 回顾历史 |
|
||||||
| `git checkout main` | 切换分支 | "回地球(主分支)看看" |
|
| `git checkout -b dev` | 创建新分支 | "我要去平行宇宙 dev 探险了" | ⭐⭐⭐⭐ 开新功能 |
|
||||||
| `git merge 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 />
|
<GitConflictDemo />
|
||||||
|
|
||||||
此时你需要手动打开文件,保留需要的代码,删除 Git 自动生成的 `<<<<<<<` 标记,然后重新提交。
|
### 6.2 怎么解决冲突?
|
||||||
|
|
||||||
至于**远程仓库 (Remote)**(比如 GitHub/GitLab),它就是云端的备份中心。
|
**Step 1**:打开冲突文件,会看到这样的标记:
|
||||||
|
|
||||||
- `git push`:把本地存档上传到云端。
|
```text
|
||||||
- `git pull`:把云端最新的存档拉取到本地。
|
<<<<<<< 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 />
|
<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** | - | 当前所在的分支/版本的指针 |
|
||||||
|
|||||||
@@ -1,497 +1,401 @@
|
|||||||
# 负载均衡与多实例部署示意图
|
# 负载均衡与多实例部署
|
||||||
|
|
||||||
> 💡 **学习指南**:本文将带你理解现代分布式系统中,如何通过负载均衡技术把流量"聪明地"分配到多个服务器实例上。我们会从四层/七层负载均衡讲起,逐步深入到健康检查、会话保持、自动扩缩容,最后到异地多活部署。建议你先阅读 [后端架构演进](./backend-evolution.md) 了解基本概念。
|
::: tip 🎯 核心问题
|
||||||
|
**当单台服务器扛不住时,如何把流量"聪明地"分配到多个服务器实例?** 负载均衡是现代分布式系统的"分发员"。本文通过真实案例(奶茶店收银、快递分拣、交通指挥)深入理解负载均衡的设计哲学和工程实践。
|
||||||
在开始之前,建议你先补充两块"基础砖":
|
:::
|
||||||
|
|
||||||
- **网络基础**:可以先阅读 [网络基础概念](./network-basics.md) 了解 TCP/IP、HTTP 等协议。
|
|
||||||
- **容器与编排**:如果你还不熟悉 Docker 和 Kubernetes,可以先看 [容器化部署](./container-deployment.md)。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0. 引言:当一台服务器扛不住的时候
|
## 1. 为什么要"负载均衡"?
|
||||||
|
|
||||||
|
### 1.1 从一个真实案例说起:某网站的架构演进
|
||||||
|
|
||||||
|
某创业公司在用户量快速增长时遇到了严重的性能问题:
|
||||||
|
|
||||||
|
**场景还原:**
|
||||||
|
|
||||||
|
```
|
||||||
|
阶段一:单台服务器
|
||||||
|
用户 → 服务器(1核2G)
|
||||||
|
↓
|
||||||
|
日活1000 → 活跃时间:1000人同时访问
|
||||||
|
↓
|
||||||
|
问题:CPU 100%,响应慢,经常宕机
|
||||||
|
```
|
||||||
|
|
||||||
|
::: warning ⚠️ 单台服务器的致命问题
|
||||||
|
|
||||||
|
- **性能瓶颈**: CPU 100%,响应时间> 5秒
|
||||||
|
- **单点故障**: 服务器挂了,整个网站不可用
|
||||||
|
- **扩展困难**: 只能垂直升级(加CPU、内存),贵且有限
|
||||||
|
:::
|
||||||
|
|
||||||
|
**改进后的架构(引入负载均衡):**
|
||||||
|
|
||||||
|
```
|
||||||
|
阶段二:多台服务器 + 负载均衡
|
||||||
|
用户 → 负载均衡器(Nginx)
|
||||||
|
↓
|
||||||
|
├→ 服务器1 (1核2G)
|
||||||
|
├→ 服务器2 (1核2G)
|
||||||
|
└→ 服务器3 (1核2G)
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip ✨ 改进后的效果
|
||||||
|
|
||||||
|
- **性能提升**: 3台服务器并行处理,响应时间< 1秒
|
||||||
|
- **高可用**: 1台服务器挂了,其他服务器继续服务
|
||||||
|
- **水平扩展**: 需要更多性能?加服务器就行
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 1.2 负载均衡的生活化比喻
|
||||||
|
|
||||||
|
**奶茶店收银台**
|
||||||
|
|
||||||
|
想象你开了一家网红奶茶店:
|
||||||
|
|
||||||
|
- **1个收银台**: 顾客排队,后面的人等不及,差评
|
||||||
|
- **3个收银台**: 员工分配顾客到不同收银台,效率提升3倍
|
||||||
|
|
||||||
|
**负载均衡就是"收银台分配员"**:
|
||||||
|
|
||||||
|
- **用户**(顾客) → 请求服务
|
||||||
|
- **负载均衡器**(分配员) → 把请求分配到不同服务器
|
||||||
|
- **服务器**(收银台) → 处理请求
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<LoadBalancerTypesDemo />
|
<LoadBalancerTypesDemo />
|
||||||
</ClientOnly>
|
|
||||||
|
|
||||||
想象你开了一家网红奶茶店。刚开业时,店里只有一个收银台,顾客排队点单,一切井然有序。但随着口碑传播,排队的人越来越多,一个收银台根本应付不过来——顾客等得不耐烦,抱怨连连,甚至有人转身离开。
|
|
||||||
|
|
||||||
**这时候你有两个选择:**
|
|
||||||
|
|
||||||
1. **换一台更快的收银机(垂直扩展)**:但再快的机器也有极限,而且贵得离谱。
|
|
||||||
2. **多开几个收银台,让顾客分流(水平扩展)**:每个收银台处理一部分顾客,整体效率大幅提升。
|
|
||||||
|
|
||||||
**负载均衡(Load Balancing)就是第二个方案的"总指挥"。** 它站在所有收银台前面,帮顾客决定:"你去1号台,你去2号台..." 确保每个收银台的 workload 相对平均,不让任何一个台累垮。
|
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<IntroProblemReasonSolution />
|
|
||||||
</ClientOnly>
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 负载均衡器的"分层": L4 vs L7
|
## 2. 什么是负载均衡?
|
||||||
|
|
||||||
就像快递分拣有"只看邮编"和"检查包裹内容"两种策略,负载均衡也分不同"层次":
|
### 2.1 四层负载均衡(L4):只看门牌号
|
||||||
|
|
||||||
### 1.1 四层负载均衡(L4):"只看门牌号"
|
**工作在传输层(TCP/UDP)**,就像快递小哥只看你家的**门牌号(IP地址+端口号)**,不关心你家是做什么。
|
||||||
|
|
||||||
**工作在传输层(TCP/UDP)**,就像快递小哥只看你家的**门牌号(IP地址+端口号)**,不关心你家是做什么的。
|
**特点:**
|
||||||
|
|
||||||
**特点:**
|
- **速度超快**: 只做简单的地址转发,不解析数据包内容
|
||||||
- **速度超快**:只做简单的地址转发,不解析数据包内容
|
- **适用场景**: 数据库连接、Redis缓存、长连接游戏服务器
|
||||||
- **适用场景**:数据库连接、Redis缓存、长连接游戏服务器
|
- **代表产品**: LVS(Linux Virtual Server)、AWS NLB、Azure Load Balancer
|
||||||
- **代表产品**: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):检查包裹内容
|
||||||
|
|
||||||
**特点:**
|
**工作在应用层(HTTP/HTTPS)**,就像快递小哥不仅看门牌号,还会**打开包裹检查内容**,根据内容决定怎么送。
|
||||||
- **智能路由**:可以根据URL路径、HTTP头、Cookie等做精细化路由
|
|
||||||
- **高级功能**:SSL卸载、内容缓存、压缩、安全WAF
|
|
||||||
- **适用场景**:Web应用、API网关、微服务架构
|
|
||||||
- **代表产品**:Nginx、HAProxy、AWS ALB、Envoy
|
|
||||||
|
|
||||||
**真实案例:SaaS平台的多租户路由**
|
**特点:**
|
||||||
|
|
||||||
某SaaS公司使用Nginx作为L7负载均衡,根据HTTP Header中的`X-Tenant-ID`将不同租户的数据请求路由到对应的数据库集群。tenant-a 的请求去 db-cluster-1,tenant-b 的请求去 db-cluster-2,实现了完全的数据隔离。
|
- **智能路由**: 可以根据URL路径、HTTP头、Cookie等做精细化路由
|
||||||
|
- **高级功能**: SSL卸载、内容缓存、压缩、安全WAF
|
||||||
|
- **适用场景**: Web应用、API网关、微服务架构
|
||||||
|
- **代表产品**: Nginx、HAProxy、AWS ALB、Envoy
|
||||||
|
|
||||||
### 1.3 L4 vs L7 对比一览
|
::: details 工作原理
|
||||||
|
|
||||||
|
```
|
||||||
|
客户端请求 → L7负载均衡器 → 解析HTTP内容
|
||||||
|
↓
|
||||||
|
检查URL、Header、Cookie
|
||||||
|
↓
|
||||||
|
智能路由到特定服务器
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 2.3 L4 vs L7 对比一览
|
||||||
|
|
||||||
| 维度 | 四层负载均衡(L4) | 七层负载均衡(L7) |
|
| 维度 | 四层负载均衡(L4) | 七层负载均衡(L7) |
|
||||||
|:---|:---|:---|
|
| :------------- | :------------------- | :------------------------ |
|
||||||
| **工作层级** | 传输层(TCP/UDP) | 应用层(HTTP/HTTPS) |
|
| **工作层级** | 传输层(TCP/UDP) | 应用层(HTTP/HTTPS) |
|
||||||
| **决策依据** | IP地址 + 端口号 | URL、Header、Cookie、Body |
|
| **决策依据** | IP地址 + 端口号 | URL、Header、Cookie、Body |
|
||||||
| **处理速度** | 极快(内核态处理) | 较快(用户态解析) |
|
| **处理速度** | 极快(内核态处理) | 较快(用户态解析) |
|
||||||
| **功能丰富度** | 基础转发 | SSL卸载、缓存、压缩、WAF |
|
| **功能丰富度** | 基础转发 | SSL卸载、缓存、压缩、WAF |
|
||||||
| **典型场景** | 数据库、游戏、长连接 | Web应用、API网关、微服务 |
|
| **典型场景** | 数据库、游戏、长连接 | Web应用、API网关、微服务 |
|
||||||
| **代表产品** | LVS、AWS NLB | Nginx、HAProxy、AWS ALB |
|
| **代表产品** | LVS、AWS NLB | Nginx、HAProxy、AWS ALB |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 健康检查:别让"坏掉"的服务器继续接客
|
## 3. 核心问题一:如何避免"坏掉"的服务器继续接客?
|
||||||
|
|
||||||
想象一下,你的某个收银台突然坏了,但顾客不知道,还在源源不断地排过去。结果队伍越来越长,顾客怨声载道。
|
### 3.1 健康检查:别让"生病"的服务器拖累系统
|
||||||
|
|
||||||
**健康检查(Health Check)就是防止这种情况发生的"哨兵"。** 它定期"体检"每台服务器,发现"生病"的立即从队列中移除,等"康复"了再请回来。
|
想象一下,你的某个收银台突然坏了,但分配员不知道,还在源源不断地把顾客分过去。结果队伍越来越长,顾客怨声载道。
|
||||||
|
|
||||||
<ClientOnly>
|
**健康检查(Health Check)就是防止这种情况发生的"哨兵"**。它定期"体检"每台服务器,发现"生病"的立即从队列中移除,等"康复"了再请回来。
|
||||||
<HealthCheckDemo />
|
|
||||||
</ClientOnly>
|
|
||||||
|
|
||||||
### 2.1 主动健康检查 vs 被动健康检查
|
<!-- <HealthCheckDemo /> -->
|
||||||
|
|
||||||
**主动健康检查(Active Health Check)**:负载均衡器主动"敲门"问服务器"你还在吗?"
|
### 3.2 主动健康检查 vs 被动健康检查
|
||||||
- 定期发送探测请求(如 HTTP /health、TCP ping)
|
|
||||||
|
**主动健康检查(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都认为失败 |
|
| **HTTP状态码** | 200-399 | 400+或超时 | 4xx/5xx都认为失败 |
|
||||||
| **TCP连接** | 成功建立 | 连接超时 | 检查端口是否可达 |
|
| **TCP连接** | 成功建立 | 连接超时 | 检查端口是否可达 |
|
||||||
| **响应时间** | < 500ms | > 2000ms | 超时时间通常设为2-5s |
|
| **响应时间** | < 500ms | > 2000ms | 超时时间通常设为2-5秒 |
|
||||||
| **连续失败次数** | - | 3次 | 避免单次抖动误判 |
|
| **连续失败次数** | - | 3次 | 避免单次抖动误判 |
|
||||||
| **检查间隔** | - | 5s | 太频繁会增加负载 |
|
| **检查间隔** | - | 5s | 太频繁会增加负载 |
|
||||||
|
|
||||||
**踩坑经验:阈值设置太"敏感"的教训**
|
::: tip 💡 踸见坑:阈值设置太"敏感"
|
||||||
|
某团队将健康检查的响应时间阈值设为100ms,而他们的应用平均响应时间在80-120ms之间波动。结果是服务器频繁被标记为"不健康",导致流量在健康和不健康之间反复横跳,系统整体可用率反而下降。
|
||||||
|
|
||||||
某团队将健康检查的响应时间阈值设为 100ms,而他们的应用平均响应时间在 80-120ms 之间波动。结果是服务器频繁被标记为"不健康",导致流量在健康和不健康之间反复横跳,系统整体可用率反而下降。
|
**正确的做法**: 阈值应该设置为**P99响应时间的2-3倍**,给正常波动留出足够的缓冲空间。
|
||||||
|
:::
|
||||||
**正确的做法:** 阈值应该设置为**P99 响应时间的 2-3 倍**,给正常波动留出足够的缓冲空间。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 会话保持:让"老顾客"一直找同一个"收银员"
|
## 4. 核心问题二:如何保证"老顾客"一直找同一个"收银员"?
|
||||||
|
|
||||||
想象你是奶茶店的常客,每次来都由同一个店员接待。她知道你的口味偏好(半糖、去冰),服务起来又快又贴心。但如果每次来都换一个新人,你得一遍遍重复同样的要求,效率大打折扣。
|
### 4.1 会话保持:让"老顾客"一直找同一个"收银员"
|
||||||
|
|
||||||
**会话保持(Session Persistence/Sticky Session)** 就是解决这个问题的方法:确保同一个用户的请求,始终被路由到同一台后端服务器。
|
想象你是奶茶店的常客,每次来都由同一个店员接待。她知道你的口味偏好(半糖、去冰),服务起来又快又贴心。但如果每次来都换一个新人,你得一遍遍重复同样的要求,效率大打折扣。
|
||||||
|
|
||||||
|
**会话保持(Session Persistence/Sticky Session)** 就是解决这个问题的方法:确保同一个用户的请求,始终被路由到同一台后端服务器。
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<SessionPersistenceDemo />
|
<SessionPersistenceDemo />
|
||||||
</ClientOnly>
|
|
||||||
|
|
||||||
### 3.1 三种会话保持机制对比
|
### 4.2 三种会话保持机制对比
|
||||||
|
|
||||||
| 机制 | 实现原理 | 优点 | 缺点 | 适用场景 |
|
| 机制 | 实现原理 | 优点 | 缺点 | 适用场景 |
|
||||||
|:---|:---|:---|:---|:---|
|
| :------------- | :---------------------------------------- | :------------------------------ | :---------------------------- | :---------------------- |
|
||||||
| **Cookie 插入** | LB在响应中插入Cookie,后续请求携带此Cookie | 不受IP变化影响,首次请求即可保持 | 客户端需支持Cookie,可能被禁用 | 电商购物车、登录态保持 |
|
| **Cookie插入** | LB在响应中插入Cookie,后续请求携带此Cookie | 不受IP变化影响,首次请求即可保持 | 客户端需支持Cookie,可能被禁用 | 电商购物车、登录态保持 |
|
||||||
| **IP 哈希** | 对客户端IP做哈希计算,映射到特定服务器 | 无需客户端支持,无状态 | IP变化会丢失会话,难以均匀分布 | 无Cookie环境、WebSocket |
|
| **IP哈希** | 对客户端IP做哈希计算,映射到特定服务器 | 无需客户端支持,无状态 | IP变化会丢失会话,难以均匀分布 | 无Cookie环境、WebSocket |
|
||||||
| **粘性会话表** | LB维护会话到服务器的映射表 | 支持会话复制和故障转移 | 占用LB内存,需要额外同步 | 高可用要求严格的场景 |
|
| **粘性会话表** | LB维护会话到服务器的映射表 | 支持会话复制和故障转移 | 占用LB内存,需要额外同步 | 高可用要求严格的场景 |
|
||||||
|
|
||||||
### 3.2 真实案例:电商大促期间的会话保持策略
|
::: tip 💡 使用建议
|
||||||
|
|
||||||
某电商平台在大促期间面临以下挑战:
|
- **Cookie插入**: 优先推荐,兼容性好
|
||||||
|
- **IP哈希**: 只用于WebSocket等特殊场景
|
||||||
1. **购物车数据需要保持**:用户可能跨多个页面添加商品,需要保证请求都落在同一台服务器,购物车数据才能正确累计。
|
- **粘性会话表**: 配合Cookie,提供故障转移能力
|
||||||
|
:::
|
||||||
2. **秒杀场景下服务器动态扩容**:大促期间服务器数量从平时的10台动态扩展到50台。
|
|
||||||
|
|
||||||
3. **部分服务器可能故障**:需要能够快速剔除故障节点,同时不影响用户会话。
|
|
||||||
|
|
||||||
**他们的解决方案**:
|
|
||||||
|
|
||||||
1. **采用 Cookie 插入机制**:负载均衡器(Nginx)在首次响应时设置 `SERVERID` Cookie,值为后端服务器的唯一标识。
|
|
||||||
|
|
||||||
2. **会话表持久化**:将会话映射表存储在 Redis 集群中,即使某台 Nginx 重启,也能从 Redis 恢复会话映射关系。
|
|
||||||
|
|
||||||
3. **故障转移策略**:当后端服务器健康检查失败时,将其从可用列表移除。对于已经绑定到该服务器的会话,下次请求时重新哈希分配到新的健康节点(牺牲一次会话保持,换取服务可用性)。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 部署策略:蓝绿部署与金丝雀发布
|
## 5. 核心问题三:如何实现零停机部署?
|
||||||
|
|
||||||
当新版本上线时,如何确保零停机?当新版本有 Bug 时,如何快速回滚?这涉及到两种经典的部署策略。
|
### 5.1 蓝绿部署:"一键切换"的零停机发布
|
||||||
|
|
||||||
### 4.1 蓝绿部署:"一键切换"的零停机发布
|
**核心思想**: 同时维护两套完全相同的生产环境(蓝环境和绿环境),但只有一个环境对外提供服务。
|
||||||
|
|
||||||
**核心思想**:同时维护两套完全相同的生产环境(蓝环境和绿环境),但只有一个环境对外提供服务。
|
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<BlueGreenDeploymentDemo />
|
<BlueGreenDeploymentDemo />
|
||||||
</ClientOnly>
|
|
||||||
|
|
||||||
**工作流程**:
|
**工作流程:**
|
||||||
|
|
||||||
1. **初始状态**:蓝环境运行 v1.0(生产),绿环境待命。
|
1. **初始状态**: 蓝环境运行v1.0(生产),绿环境待命。
|
||||||
|
2. **部署新版本**: 在绿环境部署v1.1,进行内部冒烟测试。
|
||||||
2. **部署新版本**:在绿环境部署 v1.1,进行内部冒烟测试。
|
3. **切换流量**: 将负载均衡器指向绿环境,流量瞬间切换到v1.1。
|
||||||
|
4. **监控观察**: 观察绿环境运行状态,确认无异常。
|
||||||
3. **切换流量**:将负载均衡器指向绿环境,流量瞬间切换到 v1.1。
|
5. **保留旧版本**: 蓝环境保持v1.0一段时间(如24小时),作为快速回滚的保险。
|
||||||
|
|
||||||
4. **监控观察**:观察绿环境运行状态,确认无异常。
|
|
||||||
|
|
||||||
5. **保留旧版本**:蓝环境保持 v1.0 一段时间(如24小时),作为快速回滚的保险。
|
|
||||||
|
|
||||||
**优缺点分析**:
|
|
||||||
|
|
||||||
|
::: tip ✨ 优缺点分析
|
||||||
| 优点 | 缺点 |
|
| 优点 | 缺点 |
|
||||||
|:---|:---|
|
|:---|:---|
|
||||||
| ✅ 零停机时间,切换在毫秒级完成 | ❌ 资源成本高,需要同时维护两套环境 |
|
| ✅ 零停机时间,切换在毫秒级完成 | ❌ 资源成本高,需要同时维护两套环境 |
|
||||||
| ✅ 快速回滚,发现问题立即切回原环境 | ❌ 数据库Schema变更时需要特别处理兼容性 |
|
| ✅ 快速回滚,发现问题立即切回原环境 | ❌ 数据库Schema变更时需要特别处理兼容性 |
|
||||||
| ✅ 新环境可完整测试后再接管流量 | ❌ 不适用于有状态服务(如WebSocket长连接) |
|
| ✅ 新环境可完整测试后再接管流量 | ❌ 不适用于有状态服务(如WebSocket长连接) |
|
||||||
|
|
||||||
**适用场景**:
|
:::
|
||||||
- 对可用性要求极高的金融、电商核心交易系统
|
|
||||||
- 需要频繁发布但无法接受停机的 SaaS 服务
|
|
||||||
- 有充足的硬件/云资源预算
|
|
||||||
|
|
||||||
### 4.2 金丝雀发布:"小步快跑"的灰度策略
|
### 5.2 金丝雀发布:"小步快跑"的灰度策略
|
||||||
|
|
||||||
金丝雀发布得名于历史上的"煤矿金丝雀"——矿工带着金丝雀下井,如果金丝雀出现异常,说明有毒气体泄漏,矿工立即撤离。在软件发布中,金丝雀发布就是先让一小部分用户试用新版本,观察没有问题后再逐步扩大范围。
|
金丝雀发布得名于历史上的"煤矿金丝雀"——矿工带着金丝雀下井,如果金丝雀出现异常,说明有毒气体泄漏,矿工立即撤离。在软件发布中,金丝雀发布就是先让一小部分用户试用新版本,观察没有问题后再逐步扩大范围。
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<CanaryReleaseDemo />
|
<CanaryReleaseDemo />
|
||||||
</ClientOnly>
|
|
||||||
|
|
||||||
**核心思想**:
|
**核心思想:**
|
||||||
|
|
||||||
1. **小流量先行**:先将 1% 的流量导入新版本服务器。
|
1. **小流量先行**: 先将1%的流量导入新版本服务器。
|
||||||
|
2. **观察指标**: 持续监控错误率、延迟、业务关键指标。
|
||||||
2. **观察指标**:持续监控错误率、延迟、业务关键指标。
|
3. **逐步放量**: 如果一切正常,逐步将比例提升到5%、10%、25%、50%、100%。
|
||||||
|
4. **快速回滚**: 一旦发现异常,立即将所有流量切回旧版本。
|
||||||
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)就是让系统像餐厅一样"灵活排班"**——忙的时候自动加服务器,闲的时候自动减服务器。
|
想象你开了一家餐厅:
|
||||||
|
|
||||||
|
- **午餐高峰期**: 需要10个服务员,但下午3点闲时只需要2个
|
||||||
|
- 如果一直维持10个\*\*: 人工成本爆炸
|
||||||
|
- 如果一直只有2个: 高峰期顾客等不及,全跑了
|
||||||
|
|
||||||
|
**自动扩缩容(Auto Scaling)** 就是让系统像餐厅一样"灵活排班"——忙的时候自动加服务器,闲的时候自动减服务器。
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<AutoScalingDemo />
|
<AutoScalingDemo />
|
||||||
</ClientOnly>
|
|
||||||
|
|
||||||
### 5.1 扩容指标的选择
|
### 6.2 扩容指标的选择
|
||||||
|
|
||||||
自动扩缩容的核心是回答一个问题:**什么时候该加机器?什么时候该减机器?**
|
自动扩缩容的核心是回答一个问题:\*\* **什么时候该加机器?什么时候该减机器?**
|
||||||
|
|
||||||
常见的决策指标:
|
常见的决策指标:
|
||||||
|
|
||||||
| 指标 | 扩容阈值 | 缩容阈值 | 适用场景 |
|
| 指标 | 扩容阈值 | 缩容阈值 | 适用场景 |
|
||||||
|:---|:---|:---|:---|
|
| :------------------ | :--------- | :--------- | :--------------- |
|
||||||
| **CPU使用率** | > 70% | < 30% | 计算密集型应用 |
|
| **CPU使用率** | > 70% | < 30% | 计算密集型应用 |
|
||||||
| **内存使用率** | > 75% | < 40% | 内存密集型应用 |
|
| **内存使用率** | > 75% | < 40% | 内存密集型应用 |
|
||||||
| **QPS(每秒请求数)** | > 1000/s | < 400/s | API网关、Web服务 |
|
| **QPS(每秒请求数)** | > 1000/s | < 400/s | API网关、Web服务 |
|
||||||
| **连接数** | > 5000 | < 1000 | 数据库、消息队列 |
|
| **连接数** | > 5000 | < 1000 | 数据库、消息队列 |
|
||||||
| **自定义业务指标** | 视业务而定 | 视业务而定 | 特定业务场景 |
|
| **自定义业务指标** | 视业务而定 | 视业务而定 | 特定业务场景 |
|
||||||
|
|
||||||
### 5.2 扩容策略的"坑"与"解"
|
::: tip 💡 扩容策略的"坑"与"解"
|
||||||
|
|
||||||
**踩坑1:扩容反应太慢,流量洪峰已经把系统打挂了**
|
**坑1:扩容反应太慢,流量洪峰已经把系统打挂了**
|
||||||
|
|
||||||
某电商大促期间,设置 CPU > 80% 触发扩容,但监控采集有1分钟延迟,新实例启动需要3分钟。结果流量来得太快,扩容还没完成,服务器已经被打挂。
|
某电商大促期间,设置CPU > 80%触发扩容,但监控采集有1分钟延迟,新实例启动需要3分钟。结果流量来得太快,扩容还没完成,服务器已经被打挂。
|
||||||
|
|
||||||
**解决方案**:
|
**解决方案:**
|
||||||
- **提前扩容**:基于历史数据预测流量高峰,提前30分钟开始扩容
|
|
||||||
- **多级阈值**:设置 60% 预警(开始预热新实例)、70% 正式扩容、80% 紧急扩容
|
|
||||||
- **快速扩容**:使用容器化部署,新实例30秒内启动(相比虚拟机3-5分钟)
|
|
||||||
|
|
||||||
**踩坑2:扩容太激进,成本爆炸**
|
- **提前扩容**: 基于历史数据预测流量高峰,提前30分钟开始扩容
|
||||||
|
- **多级阈值**: 设置60%预警(开始预热新实例)、70%正式扩容、80%紧急扩容
|
||||||
|
- **快速扩容**: 使用容器化部署,新实例30秒内启动(相比虚拟机3-5分钟)
|
||||||
|
|
||||||
某创业公司设置了激进的自动扩容策略:CPU > 50% 就扩容。结果一个正常的业务波动就触发了扩容,服务器数量从5台膨胀到30台,月底云账单吓哭了 CTO。
|
**坑2:扩容太激进,成本爆炸**
|
||||||
|
|
||||||
**解决方案**:
|
某创业公司设置了激进的自动扩容策略:CPU > 50%就扩容。结果一个正常的业务波动就触发了扩容,服务器数量从5台膨胀到30台,月底云账单吓哭了CTO。
|
||||||
- **设置扩容冷却时间**:一次扩容后,至少等待5分钟才能再次扩容
|
|
||||||
- **设置最大实例数**:max = 当前实例数 × 2,防止无限膨胀
|
|
||||||
- **区分突刺和趋势**:只有连续3个周期都超过阈值才扩容,避免单点突刺触发
|
|
||||||
|
|
||||||
**踩坑3:缩容太快,刚扩容的机器马上就缩了**
|
**解决方案:**
|
||||||
|
|
||||||
某团队设置了 CPU < 30% 缩容。扩容后流量还在消化,CPU 短暂回落到 25%,触发了缩容。刚缩完 CPU 又飙到 80%,又触发扩容——系统在"扩容-缩容-扩容"中疯狂震荡。
|
- **设置扩容冷却时间**: 一次扩容后,至少等待5分钟才能再次扩容
|
||||||
|
- **设置最大实例数**: max = 当前实例数 × 2,防止无限膨胀
|
||||||
|
- **区分突刺和趋势**: 只有连续3个周期都超过阈值才扩容,避免单点突刺触发
|
||||||
|
|
||||||
**解决方案**:
|
**坑3:缩容太快,刚扩容的机器马上就缩了**
|
||||||
- **缩容更保守**:扩容阈值 70%,缩容阈值 25%,中间有足够的缓冲带
|
|
||||||
- **缩容冷却时间更长**:扩容后至少等待10分钟才能缩容
|
某团队设置了CPU < 30%缩容。扩容后流量还在消化,CPU短暂回落到25%,触发了缩容。刚缩完CPU又飙到80%,又触发扩容——系统在"扩容-缩容-扩容"中疯狂震荡。
|
||||||
- **渐进式缩容**:一次只缩 1 台,观察后再决定要不要继续缩
|
|
||||||
|
**解决方案:**
|
||||||
|
|
||||||
|
- **缩容更保守**: 扩容阈值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>
|
::: tip 💡 选型建议
|
||||||
<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人左右。
|
|
||||||
|
|
||||||
**架构方案:**
|
|
||||||
|
|
||||||
```
|
```
|
||||||
用户请求
|
选择负载均衡器:
|
||||||
↓
|
│
|
||||||
[DNS 轮询] 多地域就近访问
|
├─ 只需要基础的四层负载均衡?
|
||||||
↓
|
│ ├─ 是 → LVS(开源免费)或 云厂商NLB
|
||||||
[CDN] 静态资源缓存(图片、JS、CSS)
|
│ └─ 否 → 继续
|
||||||
↓
|
│
|
||||||
[L7 负载均衡 - Nginx] SSL卸载、URL路由、限流
|
├─ 需要服务网格、多云部署?
|
||||||
↓
|
│ ├─ 是 → Envoy
|
||||||
[Web 服务器 - Node.js/Java] 业务逻辑处理
|
│ └─ 否 → 继续
|
||||||
↓
|
│
|
||||||
[缓存层 - Redis Cluster] 会话、热点数据
|
├─ 需要极其复杂的配置和插件?
|
||||||
↓
|
│ ├─ 是 → HAProxy
|
||||||
[数据库 - MySQL 主从] 读写分离
|
│ └─ 否 → 继续
|
||||||
|
│
|
||||||
|
├─ 需要高性能+简单配置?
|
||||||
|
│ ├─ 是 → 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. 总结:负载均衡的核心思维
|
||||||
|
|
||||||
| 英文术语 | 中文对照 | 解释 |
|
### 8.1 核心原则回顾
|
||||||
|:---|:---|:---|
|
|
||||||
| **Load Balancer** | 负载均衡器 | 将流量分发到多个后端服务器的设备或软件 |
|
| 原则 | 含义 | 实践要点 |
|
||||||
| **L4 Load Balancing** | 四层负载均衡 | 基于传输层(TCP/UDP)的负载均衡 |
|
| -------- | -------------------------- | ------------------------------------- |
|
||||||
| **L7 Load Balancing** | 七层负载均衡 | 基于应用层(HTTP/HTTPS)的负载均衡 |
|
| **分层** | L4处理"快递分拣"(快但简单) | L4处理数据库、游戏;L7处理Web、API |
|
||||||
| **Health Check** | 健康检查 | 定期检查后端服务器健康状态的机制 |
|
| **冗余** | 单点故障是架构的敌人 | 通过多实例、多区域部署提升可用性 |
|
||||||
| **Session Persistence** | 会话保持 | 确保同一用户的请求始终路由到同一台服务器 |
|
| **渐进** | 发布新版本不要"一刀切" | 蓝绿部署实现零停机;金丝雀实现风险可控 |
|
||||||
| **Sticky Session** | 粘性会话 | 另一种称呼,同 Session Persistence |
|
| **弹性** | 系统应该像生命体一样"呼吸" | 忙时自动扩容,闲时自动缩容 |
|
||||||
| **Blue-Green Deployment** | 蓝绿部署 | 两套环境切换的零停机发布策略 |
|
|
||||||
| **Canary Release** | 金丝雀发布 | 小流量先行验证的灰度发布策略 |
|
### 8.2 设计检查清单
|
||||||
| **Auto Scaling** | 自动扩缩容 | 根据负载自动增加或减少服务器数量 |
|
|
||||||
| **Horizontal Scaling** | 水平扩展 | 增加服务器数量来提升处理能力 |
|
在引入负载均衡前,问自己以下问题:
|
||||||
| **Vertical Scaling** | 垂直扩展 | 提升单机配置(CPU、内存)来提升处理能力 |
|
|
||||||
| **Multi-Region** | 多区域 | 在多个地理区域部署服务 |
|
- [ ] 是否真的需要负载均衡?(单机性能是否真的不够)
|
||||||
| **Active-Active** | 多活 | 多个区域同时对外提供服务 |
|
- [ ] 选择L4还是L7?(根据业务场景)
|
||||||
| **Active-Standby** | 主备 | 只有一个区域提供服务,其他待命 |
|
- [ ] 如何处理会话保持?(Cookie、IP哈希、会话表)
|
||||||
| **Data Replication** | 数据同步 | 跨区域的数据复制机制 |
|
- [ ] 如何实现健康检查?(主动、被动、阈值设置)
|
||||||
| **RTO** | 恢复时间目标 | 系统故障后需要在多长时间内恢复 |
|
- [ ] 如何实现零停机?(蓝绿部署、金丝雀)
|
||||||
| **RPO** | 恢复点目标 | 系统故障后可以接受的数据丢失量 |
|
- [ ] 如何实现弹性?(扩缩指标、冷却时间、最大实例数)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 总结:负载均衡的核心思维
|
## 9. 名词速查表
|
||||||
|
|
||||||
通过本文的学习,我们可以提炼出负载均衡设计的几个核心思维:
|
| 名词 | 英文 | 解释 |
|
||||||
|
| ---------------- | --------------------- | ---------------------------------------- | ------------------------------ |
|
||||||
**1. 分层思维**
|
| **负载均衡器** | Load Balancer | 将流量分发到多个后端服务器的设备或软件 |
|
||||||
- L4 处理"快递分拣"(快但简单)
|
| **四层负载均衡** | L4 Load Balancing | 基于传输层(TCP/UDP)的负载均衡 |
|
||||||
- L7 处理"内容检查"(慢但智能)
|
| **七层负载均衡** | L7 Load Balancing | 基于应用层(HTTP/HTTPS)的负载均衡 |
|
||||||
- 根据场景选择合适的层次
|
| **健康检查** | Health Check | 定期检查后端服务器的健康状态的机制 |
|
||||||
|
| **会话保持** | Session Persistence | 确保同一用户的请求始终路由到同一台服务器 |
|
||||||
**2. 冗余思维**
|
| **粘性会话** | Sticky Session | 另一种称呼,同Session Persistence |
|
||||||
- 单点故障是架构的敌人
|
| **蓝绿部署** | Blue-Green Deployment | 两套环境切换的零停机发布策略 |
|
||||||
- 通过多实例、多区域部署提升可用性
|
| **金丝雀发布** | Canary Release | 小流量先行验证的灰度发布策略 |
|
||||||
- 健康检查确保"坏节点"及时剔除
|
| **自动扩缩容** | Auto Scaling | 根据负载自动增加或减少服务器数量 |
|
||||||
|
| **水平扩展** | Horizontal Scaling | 增加服务器数量来提升处理能力 |
|
||||||
**3. 渐进思维**
|
| **垂直扩展** | Vertical Scaling | 提升单机配置(CPU、内存)来提升处理能力 |
|
||||||
- 发布新版本不要"一刀切"
|
| **多区域** | Multi-Region | 在多个地理区域部署服务 |
|
||||||
- 蓝绿部署实现零停机
|
| **多活** | Active-Active | 多个区域同时对外提供服务 |
|
||||||
- 金丝雀发布实现风险可控
|
| **主备** | Active-Standby | 只有一个区域提供服务,其他待命 |
|
||||||
|
| **数据同步** | Data Replication | 跨区域的数据复制机制 |
|
||||||
**4. 弹性思维**
|
| **RTO** | RTO | 恢复时间目标 | 系统故障后需要在多长时间内恢复 |
|
||||||
- 系统应该像生命体一样"呼吸"
|
| **RPO** | RPO | 恢复点目标 | 系统故障后可以接受的数据丢失量 |
|
||||||
- 忙时自动扩容,闲时自动缩容
|
|
||||||
- 多区域部署实现就近服务和容灾
|
|
||||||
|
|
||||||
负载均衡不是简单的"流量分发",而是一套关于**高可用、高性能、高弹性**的系统工程思维。希望本文能帮助你在实际工作中做出更好的架构决策。
|
|
||||||
|
|||||||
+257
-595
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,6 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import UrlToBrowserQuickStart from '../../.vitepress/theme/components/appendix/url-to-browser/UrlToBrowserQuickStart.vue'
|
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>
|
</script>
|
||||||
|
|
||||||
> **学习指南**:本章节无需编程基础。我们将用**"网购"**的生活化比喻,配合**真实的技术过程**,带你一步步理解浏览器如何将一行网址变成丰富多彩的页面。
|
> **学习指南**:本章节无需编程基础。我们将用**"网购"**的生活化比喻,配合**真实的技术过程**,带你一步步理解浏览器如何将一行网址变成丰富多彩的页面。
|
||||||
@@ -16,6 +11,7 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
|
|||||||
## 0. 引言:当你按下回车键的那一刻
|
## 0. 引言:当你按下回车键的那一刻
|
||||||
|
|
||||||
想象你正在进行一次**网购**。你需要:
|
想象你正在进行一次**网购**。你需要:
|
||||||
|
|
||||||
1. **填写订单**(选好商品,确认收货地址)
|
1. **填写订单**(选好商品,确认收货地址)
|
||||||
2. **系统查找仓库**(根据店铺名找到具体的发货仓库)
|
2. **系统查找仓库**(根据店铺名找到具体的发货仓库)
|
||||||
3. **建立物流通道**(确保仓库正常营业且能发货)
|
3. **建立物流通道**(确保仓库正常营业且能发货)
|
||||||
@@ -35,6 +31,7 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
|
|||||||
### 生活比喻:填写购物单
|
### 生活比喻:填写购物单
|
||||||
|
|
||||||
假设你只在订单上写"买鞋子",仓库肯定不知道发哪双。你需要写清楚:
|
假设你只在订单上写"买鞋子",仓库肯定不知道发哪双。你需要写清楚:
|
||||||
|
|
||||||
- **店铺类型**(官方旗舰店/普通店)
|
- **店铺类型**(官方旗舰店/普通店)
|
||||||
- **店铺名称**(Nike 官方店)
|
- **店铺名称**(Nike 官方店)
|
||||||
- **商品位置**(男鞋区/跑鞋系列)
|
- **商品位置**(男鞋区/跑鞋系列)
|
||||||
@@ -46,7 +43,7 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
|
|||||||
**URL(Uniform Resource Locator,统一资源定位符)**就是浏览器世界的"商品定位码"。当你在地址栏输入 `https://www.example.com:8080/path/page.html?id=123#section`,浏览器会立即拆解它:
|
**URL(Uniform Resource Locator,统一资源定位符)**就是浏览器世界的"商品定位码"。当你在地址栏输入 `https://www.example.com:8080/path/page.html?id=123#section`,浏览器会立即拆解它:
|
||||||
|
|
||||||
| URL 部分 | 示例值 | 网购类比 | 技术作用 |
|
| URL 部分 | 示例值 | 网购类比 | 技术作用 |
|
||||||
|----------|--------|-----------|----------|
|
| -------------------------- | -------------------- | -------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||||
| **协议** `https://` | 安全超文本传输协议 | **物流方式**:保密配送(HTTPS)vs 普通配送(HTTP) | 决定使用什么规则通信。`http` 是普通传输,`https` 是加密传输 |
|
| **协议** `https://` | 安全超文本传输协议 | **物流方式**:保密配送(HTTPS)vs 普通配送(HTTP) | 决定使用什么规则通信。`http` 是普通传输,`https` 是加密传输 |
|
||||||
| **域名** `www.example.com` | 服务器的人类可读名字 | **店铺名称**:京东超市 | 告诉浏览器要找哪台服务器。域名是为了让人记住,最终要转换成 IP 地址 |
|
| **域名** `www.example.com` | 服务器的人类可读名字 | **店铺名称**:京东超市 | 告诉浏览器要找哪台服务器。域名是为了让人记住,最终要转换成 IP 地址 |
|
||||||
| **端口** `:8080` | 服务器的具体"门牌号" | **柜台编号**:3号柜台(默认不写) | 服务器上可能有多个服务,端口指定访问哪一个。HTTP 默认 80,HTTPS 默认 443 |
|
| **端口** `:8080` | 服务器的具体"门牌号" | **柜台编号**:3号柜台(默认不写) | 服务器上可能有多个服务,端口指定访问哪一个。HTTP 默认 80,HTTPS 默认 443 |
|
||||||
@@ -65,6 +62,7 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
|
|||||||
### 生活比喻:查仓库地址
|
### 生活比喻:查仓库地址
|
||||||
|
|
||||||
你下单写的是"Nike 官方店",但物流系统不知道仓库在哪。它需要查地址簿:
|
你下单写的是"Nike 官方店",但物流系统不知道仓库在哪。它需要查地址簿:
|
||||||
|
|
||||||
1. 先查**常用地址**(最近买过这家吗)→ 浏览器缓存
|
1. 先查**常用地址**(最近买过这家吗)→ 浏览器缓存
|
||||||
2. 没有的话问**小区快递点**(他们知道大区域的分配)→ 本地 DNS 服务器
|
2. 没有的话问**小区快递点**(他们知道大区域的分配)→ 本地 DNS 服务器
|
||||||
3. 问**总部调度中心**(知道.com类店铺归谁管)→ 根域名服务器
|
3. 问**总部调度中心**(知道.com类店铺归谁管)→ 根域名服务器
|
||||||
@@ -89,6 +87,7 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
|
|||||||
```
|
```
|
||||||
|
|
||||||
**查询类型说明:**
|
**查询类型说明:**
|
||||||
|
|
||||||
- **递归查询(Recursive Query)**:浏览器只发一次请求,本地 DNS 负责层层查询后返回结果
|
- **递归查询(Recursive Query)**:浏览器只发一次请求,本地 DNS 负责层层查询后返回结果
|
||||||
- **迭代查询(Iterative Query)**:每一层只告诉下一层去哪查,浏览器需要多次查询
|
- **迭代查询(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 还负责:**
|
**TCP 还负责:**
|
||||||
|
|
||||||
- **数据分包**:大数据拆成小数据包传输
|
- **数据分包**:大数据拆成小数据包传输
|
||||||
- **顺序重组**:确保数据包按正确顺序组装
|
- **顺序重组**:确保数据包按正确顺序组装
|
||||||
- **错误重传**:丢包后自动重新发送
|
- **错误重传**:丢包后自动重新发送
|
||||||
@@ -158,6 +159,7 @@ import BrowserRenderingDemo from '../../.vitepress/theme/components/appendix/url
|
|||||||
**HTTP(HyperText Transfer Protocol,超文本传输协议)**是浏览器和服务器之间的"对话规则"。通道建立后,浏览器发送**取货请求**,**核心目标是拿回网页的源代码(HTML 文件)**:
|
**HTTP(HyperText Transfer Protocol,超文本传输协议)**是浏览器和服务器之间的"对话规则"。通道建立后,浏览器发送**取货请求**,**核心目标是拿回网页的源代码(HTML 文件)**:
|
||||||
|
|
||||||
**HTTP 请求示例:**
|
**HTTP 请求示例:**
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /index.html HTTP/1.1 ← 请求方法 + 路径 + 协议版本
|
GET /index.html HTTP/1.1 ← 请求方法 + 路径 + 协议版本
|
||||||
Host: www.example.com ← 目标主机(支持虚拟主机,一台服务器可托管多个网站)
|
Host: www.example.com ← 目标主机(支持虚拟主机,一台服务器可托管多个网站)
|
||||||
@@ -174,8 +176,9 @@ Cookie: session_id=abc123 ← 身份凭证
|
|||||||
你平时写的 API 调用(`fetch` / `axios`)和浏览器访问网页,在 **HTTP 层面完全是同一个东西**。
|
你平时写的 API 调用(`fetch` / `axios`)和浏览器访问网页,在 **HTTP 层面完全是同一个东西**。
|
||||||
|
|
||||||
它们都是发送一个请求,服务器返回一段文本数据。
|
它们都是发送一个请求,服务器返回一段文本数据。
|
||||||
* 如果服务器给的是 **HTML**,浏览器就把它**画出来**(变成网页)。
|
|
||||||
* 如果服务器给的是 **JSON**,你的代码就把它**存起来**(用于逻辑处理)。
|
- 如果服务器给的是 **HTML**,浏览器就把它**画出来**(变成网页)。
|
||||||
|
- 如果服务器给的是 **JSON**,你的代码就把它**存起来**(用于逻辑处理)。
|
||||||
|
|
||||||
**根本就没有"两种"请求,只有同一种 HTTP 请求,只是返回的数据格式(Content-Type)不同而已。**
|
**根本就没有"两种"请求,只有同一种 HTTP 请求,只是返回的数据格式(Content-Type)不同而已。**
|
||||||
这也是为什么理解了 HTTP,你就理解了 90% 的后端 API 原理。
|
这也是为什么理解了 HTTP,你就理解了 90% 的后端 API 原理。
|
||||||
@@ -184,6 +187,7 @@ Cookie: session_id=abc123 ← 身份凭证
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
**常见 HTTP 方法:**
|
**常见 HTTP 方法:**
|
||||||
|
|
||||||
- `GET`:获取资源(安全、幂等,可被缓存)
|
- `GET`:获取资源(安全、幂等,可被缓存)
|
||||||
- `POST`:提交数据(创建资源,如注册、登录)
|
- `POST`:提交数据(创建资源,如注册、登录)
|
||||||
- `PUT`:更新资源(完整替换)
|
- `PUT`:更新资源(完整替换)
|
||||||
@@ -192,6 +196,7 @@ Cookie: session_id=abc123 ← 身份凭证
|
|||||||
- `HEAD`:获取响应头(不返回主体,用于检查资源是否存在)
|
- `HEAD`:获取响应头(不返回主体,用于检查资源是否存在)
|
||||||
|
|
||||||
**服务器返回 HTTP 响应:**
|
**服务器返回 HTTP 响应:**
|
||||||
|
|
||||||
```http
|
```http
|
||||||
HTTP/1.1 200 OK ← 协议版本 + 状态码 + 状态描述
|
HTTP/1.1 200 OK ← 协议版本 + 状态码 + 状态描述
|
||||||
Date: Mon, 23 May 2025 12:00:00 GMT ← 服务器时间
|
Date: Mon, 23 May 2025 12:00:00 GMT ← 服务器时间
|
||||||
@@ -206,7 +211,7 @@ Set-Cookie: user_id=xyz789 ← 设置 Cookie
|
|||||||
**HTTP 状态码分类:**
|
**HTTP 状态码分类:**
|
||||||
|
|
||||||
| 状态码 | 类别 | 含义 | 生活类比 |
|
| 状态码 | 类别 | 含义 | 生活类比 |
|
||||||
|--------|------|------|----------|
|
| ----------- | ---------- | ---------------- | -------------------------------- |
|
||||||
| **200** | 成功 | 请求成功处理 | "订单确认,马上发货" |
|
| **200** | 成功 | 请求成功处理 | "订单确认,马上发货" |
|
||||||
| **301/302** | 重定向 | 资源已移动 | "本店搬家了,请去新店下单" |
|
| **301/302** | 重定向 | 资源已移动 | "本店搬家了,请去新店下单" |
|
||||||
| **304** | 未修改 | 缓存仍有效 | "你上次买的还能用,不用重新发货" |
|
| **304** | 未修改 | 缓存仍有效 | "你上次买的还能用,不用重新发货" |
|
||||||
@@ -263,22 +268,29 @@ Document
|
|||||||
浏览器解析所有的 CSS(内联、外部文件),构建**CSSOM(CSS Object Model)树**。这就像理解说明书上的样式规则:
|
浏览器解析所有的 CSS(内联、外部文件),构建**CSSOM(CSS Object Model)树**。这就像理解说明书上的样式规则:
|
||||||
|
|
||||||
```css
|
```css
|
||||||
.header { color: blue; font-size: 24px; } /* 标题要是蓝色的 */
|
.header {
|
||||||
.content { display: none; } /* 内容暂时隐藏 */
|
color: blue;
|
||||||
|
font-size: 24px;
|
||||||
|
} /* 标题要是蓝色的 */
|
||||||
|
.content {
|
||||||
|
display: none;
|
||||||
|
} /* 内容暂时隐藏 */
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 步骤3:合并 → 渲染树 (准备组装)
|
#### 步骤3:合并 → 渲染树 (准备组装)
|
||||||
|
|
||||||
DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
|
DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
|
||||||
关键点:**只有"可见"的元素才会在渲染树中**。
|
关键点:**只有"可见"的元素才会在渲染树中**。
|
||||||
* `.header`:在渲染树中(可见)。
|
|
||||||
* `.content`:**不在**渲染树中(因为 `display: none`,就像被扔掉的包装纸,不需要组装)。
|
- `.header`:在渲染树中(可见)。
|
||||||
|
- `.content`:**不在**渲染树中(因为 `display: none`,就像被扔掉的包装纸,不需要组装)。
|
||||||
|
|
||||||
#### 步骤4:布局 (Layout / Reflow) —— 测量尺寸
|
#### 步骤4:布局 (Layout / Reflow) —— 测量尺寸
|
||||||
|
|
||||||
浏览器计算渲染树中每个节点在屏幕上的**精确坐标和大小**。
|
浏览器计算渲染树中每个节点在屏幕上的**精确坐标和大小**。
|
||||||
* "这个标题框宽 100px,高 50px,放在屏幕左上角 (0,0) 位置。"
|
|
||||||
* 这个过程叫**重排 (Reflow)**。如果窗口大小变了(比如手机横屏),所有元素的位置都要重新计算,非常消耗性能。
|
- "这个标题框宽 100px,高 50px,放在屏幕左上角 (0,0) 位置。"
|
||||||
|
- 这个过程叫**重排 (Reflow)**。如果窗口大小变了(比如手机横屏),所有元素的位置都要重新计算,非常消耗性能。
|
||||||
|
|
||||||
#### 步骤5:绘制 (Paint) —— 上色
|
#### 步骤5:绘制 (Paint) —— 上色
|
||||||
|
|
||||||
@@ -301,7 +313,7 @@ DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
|
|||||||
让我们回顾整个旅程:
|
让我们回顾整个旅程:
|
||||||
|
|
||||||
| 阶段 | 技术术语 | 网购类比 | 核心任务 | 关键技术 |
|
| 阶段 | 技术术语 | 网购类比 | 核心任务 | 关键技术 |
|
||||||
|------|----------|----------|----------|----------|
|
| ----------- | ---------- | -------- | ------------------ | ------------------------------ |
|
||||||
| **1. 解析** | URL 解析 | 填写订单 | 理解买家想买什么 | 协议、域名、端口、路径、参数 |
|
| **1. 解析** | URL 解析 | 填写订单 | 理解买家想买什么 | 协议、域名、端口、路径、参数 |
|
||||||
| **2. 查询** | DNS 查询 | 查仓库址 | 找到店铺的发货仓库 | 递归/迭代查询、缓存机制 |
|
| **2. 查询** | DNS 查询 | 查仓库址 | 找到店铺的发货仓库 | 递归/迭代查询、缓存机制 |
|
||||||
| **3. 连接** | TCP 握手 | 建立通道 | 确保物流通畅 | 三次握手、序列号、流量控制 |
|
| **3. 连接** | TCP 握手 | 建立通道 | 确保物流通畅 | 三次握手、序列号、流量控制 |
|
||||||
@@ -311,6 +323,7 @@ DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
|
|||||||
**整个过程通常在几百毫秒内完成** —— 想想这有多么不可思议!
|
**整个过程通常在几百毫秒内完成** —— 想想这有多么不可思议!
|
||||||
|
|
||||||
你的浏览器在不到1秒的时间里:
|
你的浏览器在不到1秒的时间里:
|
||||||
|
|
||||||
- 解析了一个复杂的地址
|
- 解析了一个复杂的地址
|
||||||
- 查询了分布在全球的 DNS 服务器
|
- 查询了分布在全球的 DNS 服务器
|
||||||
- 和千里之外的服务器建立了可靠连接
|
- 和千里之外的服务器建立了可靠连接
|
||||||
@@ -324,7 +337,7 @@ DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
|
|||||||
## 7. 名词速查表 (Glossary)
|
## 7. 名词速查表 (Glossary)
|
||||||
|
|
||||||
| 名词 | 全称 | 简单解释 |
|
| 名词 | 全称 | 简单解释 |
|
||||||
|------|------|----------|
|
| ----------- | ----------------------------- | -------------------------------------------------------------------------- |
|
||||||
| **URL** | Uniform Resource Locator | **统一资源定位符**。网页的"地址",告诉浏览器去哪里找资源。 |
|
| **URL** | Uniform Resource Locator | **统一资源定位符**。网页的"地址",告诉浏览器去哪里找资源。 |
|
||||||
| **DNS** | Domain Name System | **域名系统**。互联网的"电话簿",把人类可读的域名转换成机器可读的 IP 地址。 |
|
| **DNS** | Domain Name System | **域名系统**。互联网的"电话簿",把人类可读的域名转换成机器可读的 IP 地址。 |
|
||||||
| **IP 地址** | Internet Protocol Address | **互联网协议地址**。每台联网设备的唯一"门牌号",如 `192.168.1.1`。 |
|
| **IP 地址** | Internet Protocol Address | **互联网协议地址**。每台联网设备的唯一"门牌号",如 `192.168.1.1`。 |
|
||||||
|
|||||||
+683
-326
File diff suppressed because it is too large
Load Diff
Generated
+522
-1500
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vitepress dev docs",
|
"dev": "vitepress dev docs",
|
||||||
"build": "vitepress build docs",
|
"build": "vitepress build docs --force",
|
||||||
"preview": "vitepress preview docs",
|
"preview": "vitepress preview docs",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"verify": "bash scripts/verify.sh"
|
"verify": "bash scripts/verify.sh"
|
||||||
|
|||||||
Reference in New Issue
Block a user