feat(docs): add interactive demo components for technical appendices

Add placeholder Vue components for visualizing technical concepts across multiple domains including frontend routing, browser rendering, cache design, queue design, database principles, API design, cloud services, and backend evolution. These components provide interactive educational content for the documentation.

Update documentation structure to include new appendix sections and enhance existing content with visual components. Remove unused 'codex' dependency from package.json.
This commit is contained in:
sanbuphy
2026-02-06 03:34:50 +08:00
parent e8bba6f7c0
commit 7c70c37072
171 changed files with 69830 additions and 6689 deletions
@@ -0,0 +1,421 @@
<!--
ApiGatewayDemo.vue
API网关架构 - 统一入口/协议转换
-->
<template>
<div class="api-gateway-demo">
<div class="header">
<div class="title">🚪 API 网关系统的"统一大门"</div>
<div class="subtitle">想象成写字楼的前台所有访客都要先经过这里才能到达不同的办公室</div>
</div>
<div class="architecture-view">
<div class="layer client-layer">
<div class="layer-title">客户端 (来访者)</div>
<div class="clients">
<div class="client-item">📱 App</div>
<div class="client-item">💻 Web</div>
<div class="client-item">🔧 第三方</div>
</div>
</div>
<div class="arrow-down"> 统一入口</div>
<div class="layer gateway-layer">
<div class="layer-title">🚪 API 网关 (前台)</div>
<div class="gateway-box">
<div class="gateway-function" :class="{ active: activeFunc === 'auth' }" @click="setActive('auth')">
<span class="func-icon">🔐</span>
<span class="func-name">身份认证</span>
</div>
<div class="gateway-function" :class="{ active: activeFunc === 'rate' }" @click="setActive('rate')">
<span class="func-icon"></span>
<span class="func-name">限流熔断</span>
</div>
<div class="gateway-function" :class="{ active: activeFunc === 'route' }" @click="setActive('route')">
<span class="func-icon">🧭</span>
<span class="func-name">路由转发</span>
</div>
<div class="gateway-function" :class="{ active: activeFunc === 'transform' }" @click="setActive('transform')">
<span class="func-icon">🔄</span>
<span class="func-name">协议转换</span>
</div>
</div>
</div>
<div class="arrow-down"> 分发请求</div>
<div class="layer backend-layer">
<div class="layer-title"> 后端服务 (各个部门)</div>
<div class="services">
<div class="service-card">
<div class="service-icon">👤</div>
<div class="service-name">用户服务</div>
<div class="service-tech">/api/users</div>
</div>
<div class="service-card">
<div class="service-icon">📦</div>
<div class="service-name">订单服务</div>
<div class="service-tech">/api/orders</div>
</div>
<div class="service-card">
<div class="service-icon">💳</div>
<div class="service-name">支付服务</div>
<div class="service-tech">/api/pay</div>
</div>
</div>
</div>
</div>
<div class="function-detail" v-if="activeFunc">
<div class="detail-header">
<span class="detail-icon">{{ currentFunction.icon }}</span>
<span class="detail-name">{{ currentFunction.name }}</span>
</div>
<div class="detail-desc">{{ currentFunction.desc }}</div>
<div class="detail-example">
<div class="example-title">💡 实际场景</div>
<div class="example-content">{{ currentFunction.example }}</div>
</div>
</div>
<div class="comparison-table">
<div class="table-title">🤔 没有网关 vs 有网关的区别</div>
<table>
<thead>
<tr>
<th>功能需求</th>
<th>没有网关 (直接访问)</th>
<th> API 网关</th>
</tr>
</thead>
<tbody>
<tr>
<td>身份认证</td>
<td>每个服务都要写一遍登录校验</td>
<td> 统一在网关层校验 JWT</td>
</tr>
<tr>
<td>限流保护</td>
<td>每个服务自己实现限流</td>
<td> 网关统一限流保护后端</td>
</tr>
<tr>
<td>协议转换</td>
<td>HTTPgRPCWebSocket各自处理</td>
<td> 网关统一对外暴露 HTTP</td>
</tr>
<tr>
<td>灰度发布</td>
<td>需要改负载均衡器配置</td>
<td> 网关层按 Header 路由</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeFunc = ref('auth')
const functions = {
auth: {
icon: '🔐',
name: '身份认证',
desc: '统一校验用户身份,无需每个后端服务都写登录逻辑。支持 JWT、OAuth2、API Key 等多种认证方式。',
example: '用户请求携带 JWT Token,网关校验签名和过期时间,通过后把用户ID添加到请求头转发给后端服务。'
},
rate: {
icon: '⚡',
name: '限流熔断',
desc: '防止突发流量压垮后端服务。支持令牌桶、漏桶等算法,超过阈值时自动拒绝或排队。',
example: '设置每秒钟最多1000个请求,超过的返回 429 Too Many Requests,保护后端数据库不被打崩。'
},
route: {
icon: '🧭',
name: '路由转发',
desc: '根据 URL 路径、请求头、Query 参数等规则,将请求转发到不同的后端服务。',
example: '/api/users → 用户服务,/api/orders → 订单服务,/api/admin → 管理服务(需管理员权限)。'
},
transform: {
icon: '🔄',
name: '协议转换',
desc: '对外统一暴露 HTTP/HTTPS,内部可转换为 gRPC、GraphQL、WebSocket 等协议。',
example: '客户端用普通 HTTP POST 请求,网关转换为 gRPC 调用内部微服务,返回结果再转成 JSON。'
}
}
const currentFunction = computed(() => functions[activeFunc.value])
const setActive = (func) => {
activeFunc.value = func
}
</script>
<style scoped>
.api-gateway-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
text-align: center;
}
.title {
font-weight: 700;
font-size: 1.25rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
line-height: 1.5;
}
.mode-selector {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.mode-btn {
flex: 1;
min-width: 200px;
padding: 0.75rem 1rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
font-weight: 600;
font-size: 0.9rem;
}
.mode-btn:hover {
border-color: var(--vp-c-brand);
}
.mode-btn.active {
border-color: var(--vp-c-brand);
background: rgba(var(--vp-c-brand-rgb), 0.1);
}
.architecture-view {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.layer {
margin-bottom: 1rem;
}
.layer-title {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
}
.clients {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.client-item {
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
border: 2px solid #3b82f6;
border-radius: 10px;
padding: 0.75rem 1.25rem;
font-weight: 600;
font-size: 0.9rem;
}
.arrow-down {
text-align: center;
font-size: 1.5rem;
color: var(--vp-c-text-2);
margin: 0.5rem 0;
font-weight: 600;
}
.gateway-box {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.gateway-function {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
}
.gateway-function:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.gateway-function.active {
border-color: var(--vp-c-brand);
background: rgba(var(--vp-c-brand-rgb), 0.1);
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.2);
}
.func-icon {
font-size: 1.5rem;
}
.func-name {
font-weight: 600;
font-size: 0.9rem;
}
.services {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.service-card {
background: linear-gradient(135deg, #f3e8ff, #e9d5ff);
border: 2px solid #a855f7;
border-radius: 12px;
padding: 1rem 1.5rem;
text-align: center;
min-width: 100px;
}
.service-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.service-name {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
.service-tech {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.function-detail {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.detail-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.detail-icon {
font-size: 1.5rem;
}
.detail-name {
font-weight: 700;
font-size: 1.1rem;
}
.detail-desc {
color: var(--vp-c-text-2);
line-height: 1.7;
margin-bottom: 1rem;
}
.detail-example {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.example-title {
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.example-content {
color: var(--vp-c-text-2);
font-size: 0.9rem;
line-height: 1.6;
}
.comparison-table {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.table-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1rem;
text-align: center;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th, td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--vp-c-divider);
}
th {
font-weight: 600;
background: var(--vp-c-bg-soft);
}
@media (max-width: 768px) {
.gateway-box {
grid-template-columns: 1fr;
}
table {
font-size: 0.8rem;
}
th, td {
padding: 0.5rem;
}
}
</style>
@@ -0,0 +1,686 @@
<!--
AuthMiddlewareDemo.vue
认证中间件 - JWT/OAuth/签名验证
-->
<template>
<div class="auth-middleware-demo">
<div class="header">
<div class="title">🔐 认证中间件谁可以进大门</div>
<div class="subtitle">想象成写字楼门禁检查工牌验证身份没权限的人进不来</div>
</div>
<div class="auth-tabs">
<button
v-for="method in authMethods"
:key="method.id"
:class="['auth-tab', { active: currentAuth === method.id }]"
@click="currentAuth = method.id"
>
<span class="tab-icon">{{ method.icon }}</span>
<span class="tab-name">{{ method.name }}</span>
</button>
</div>
<div class="auth-flow">
<div class="flow-title">{{ currentAuthData.title }}</div>
<div class="flow-diagram">
<div class="flow-step" v-for="(step, index) in currentAuthData.steps" :key="index">
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-actor">{{ step.actor }}</div>
<div class="step-action">{{ step.action }}</div>
<div class="step-arrow" v-if="index < currentAuthData.steps.length - 1"></div>
</div>
</div>
</div>
<div class="token-display" v-if="currentAuth === 'jwt'">
<div class="token-header">🔑 JWT Token 结构Base64编码</div>
<div class="token-parts">
<div class="token-part header">
<div class="part-label">HEADER</div>
<div class="part-content">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9</div>
<div class="part-decoded">{ "alg": "HS256", "typ": "JWT" }</div>
</div>
<div class="token-separator">.</div>
<div class="token-part payload">
<div class="part-label">PAYLOAD</div>
<div class="part-content">eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ</div>
<div class="part-decoded">{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }</div>
</div>
<div class="token-separator">.</div>
<div class="token-part signature">
<div class="part-label">SIGNATURE</div>
<div class="part-content">SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c</div>
<div class="part-decoded">HMACSHA256(base64Url(header) + "." + base64Url(payload), secret)</div>
</div>
</div>
</div>
</div>
<div class="implementation-comparison">
<div class="section-title">🛠 三种方案实现对比</div>
<table class="comparison-table">
<thead>
<tr>
<th>对比维度</th>
<th>Session + Cookie</th>
<th>JWT</th>
<th>OAuth2.0</th>
</tr>
</thead>
<tbody>
<tr>
<td class="dim">存储位置</td>
<td>服务端存储 Session客户端存 Cookie</td>
<td>客户端存储 Token服务端无状态</td>
<td>授权服务器存储客户端存 Access Token</td>
</tr>
<tr>
<td class="dim">扩展性</td>
<td> 需要共享 Session扩展复杂</td>
<td> 无状态易于水平扩展</td>
<td> 分布式架构支持大规模系统</td>
</tr>
<tr>
<td class="dim">安全性</td>
<td> Cookie 可能被窃取需要 CSRF 防护</td>
<td> Token 泄露风险 HTTPS + 短期有效</td>
<td> 行业最佳实践支持多种安全机制</td>
</tr>
<tr>
<td class="dim">实现复杂度</td>
<td>🟢 简单开箱即用</td>
<td>🟡 中等需要 Token 管理</td>
<td>🔴 复杂需要授权服务器</td>
</tr>
<tr>
<td class="dim">适用场景</td>
<td>传统 Web 应用后台管理系统</td>
<td>SPA移动端 API微服务</td>
<td>第三方登录开放平台SSO</td>
</tr>
</tbody>
</table>
</div>
<div class="security-tips">
<div class="tips-title">🔒 网关层认证最佳实践</div>
<div class="tips-list">
<div class="tip-item">
<div class="tip-icon">1</div>
<div class="tip-content">
<div class="tip-heading">统一在网关层验证</div>
<div class="tip-desc">不要在每个微服务里重复写认证逻辑统一在网关层校验 JWT Session</div>
</div>
</div>
<div class="tip-item">
<div class="tip-icon">2</div>
<div class="tip-content">
<div class="tip-heading">HTTPS 强制</div>
<div class="tip-desc">网关层强制 HTTPS防止 Token 在传输过程中被窃取中间人攻击</div>
</div>
</div>
<div class="tip-item">
<div class="tip-icon">3</div>
<div class="tip-content">
<div class="tip-heading">Token 过期策略</div>
<div class="tip-desc">Access Token 短期有效15分钟配合 Refresh Token 实现无感知续期</div>
</div>
</div>
<div class="tip-item">
<div class="tip-icon">4</div>
<div class="tip-content">
<div class="tip-heading">黑名单机制</div>
<div class="tip-desc">用户登出或 Token 泄露时 Token 加入黑名单Redis 存储</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentAuth = ref('jwt')
const authMethods = [
{
id: 'jwt',
icon: '🔑',
name: 'JWT Token'
},
{
id: 'oauth',
icon: '🔐',
name: 'OAuth 2.0'
},
{
id: 'signature',
icon: '✍️',
name: '签名验证'
}
]
const authData = {
jwt: {
title: 'JWT (JSON Web Token) 认证流程',
steps: [
{ actor: '用户', action: '输入用户名密码,点击登录' },
{ actor: '网关/Nginx', action: '转发登录请求到认证服务' },
{ actor: '认证服务', action: '验证密码,生成 JWT Token(包含 Header、Payload、Signature' },
{ actor: '用户/客户端', action: '保存 TokenLocalStorage 或 Cookie' },
{ actor: '后续请求', action: '在 HTTP Header 中携带: Authorization: Bearer <Token>' },
{ actor: '网关/Nginx', action: '校验 Token 签名和过期时间,通过后转发请求' },
{ actor: '后端服务', action: '从 Token 中解析用户信息,处理业务逻辑' }
]
},
oauth: {
title: 'OAuth 2.0 第三方登录流程(以微信登录为例)',
steps: [
{ actor: '用户', action: '点击"微信登录"按钮' },
{ actor: '我们的应用', action: '重定向到微信授权页面,携带 client_id 和回调地址' },
{ actor: '微信/授权服务器', action: '展示授权页面,询问用户是否同意' },
{ actor: '用户', action: '确认授权(或扫码登录)' },
{ actor: '微信/授权服务器', action: '重定向回我们的应用,携带授权码 Code' },
{ actor: '我们的后端', action: '用 Code 换取 Access Token(对客户端不可见)' },
{ actor: '我们的后端', action: '用 Access Token 请求微信用户信息服务' },
{ actor: '微信/资源服务器', action: '返回用户基本信息(openid, nickname, avatar' },
{ actor: '我们的后端', action: '创建/关联本地用户,生成自己的 Session/JWT' },
{ actor: '用户', action: '登录成功,进入应用首页' }
]
},
signature: {
title: 'API 签名验证流程(常用于开放平台和支付接口)',
steps: [
{ actor: '开发者', action: '在开放平台申请 AppKey 和 AppSecret' },
{ actor: '发起请求前', action: '将所有参数按字典序排序,拼接成字符串' },
{ actor: '客户端', action: '用 AppSecret 对字符串进行 HMAC-SHA256 签名' },
{ actor: '请求参数', action: '携带 AppKey、签名(Sign)、时间戳(Timestamp)、随机数(Nonce)' },
{ actor: '网关/Nginx', action: '提取 AppKey,查询对应的 AppSecret' },
{ actor: '网关/Nginx', action: '用同样算法计算签名,对比是否一致' },
{ actor: '网关/Nginx', action: '检查时间戳(防重放攻击,通常5分钟内有效)' },
{ actor: '网关/Nginx', action: '检查随机数是否已使用(Redis 存储防重放)' },
{ actor: '验证通过', action: '转发请求到后端服务' },
{ actor: '验证失败', action: '返回 401/403,不暴露签名算法细节' }
]
}
}
const currentAuthData = computed(() => authData[currentAuth.value])
// 实现对比数据
const comparisonData = [
{
dimension: '存储位置',
session: '服务端存储 Session,客户端存 Cookie',
jwt: '客户端存储 Token,服务端无状态',
oauth: '授权服务器存储,客户端存 Access Token'
},
{
dimension: '扩展性',
session: '❌ 需要共享 Session,扩展复杂',
jwt: '✅ 无状态,易于水平扩展',
oauth: '✅ 分布式架构,支持大规模系统'
},
{
dimension: '安全性',
session: '⚠️ Cookie 可能被窃取,需要 CSRF 防护',
jwt: '⚠️ Token 泄露风险,需 HTTPS + 短期有效',
oauth: '✅ 行业最佳实践,支持多种安全机制'
},
{
dimension: '实现复杂度',
session: '🟢 简单,开箱即用',
jwt: '🟡 中等,需要 Token 管理',
oauth: '🔴 复杂,需要授权服务器'
},
{
dimension: '适用场景',
session: '传统 Web 应用、后台管理系统',
jwt: 'SPA、移动端 API、微服务',
oauth: '第三方登录、开放平台、SSO'
}
]
// Nginx 配置示例
const nginxConfigs = [
{
id: 'basic',
name: '基础限流',
config: `# 定义限流区域
# $binary_remote_addr: 按 IP 限流
# zone=mylimit:10m: 区域名称和大小
# rate=10r/s: 每秒最多10个请求
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
listen 80;
server_name api.example.com;
location / {
# 应用限流
# burst=20: 桶容量,允许突发20个请求
# nodelay: 不延迟处理突发请求
limit_req zone=mylimit burst=20 nodelay;
proxy_pass http://backend;
}
}`,
explanation: [
'limit_req_zone: 在 http 块中定义限流区域',
'$binary_remote_addr: 使用二进制 IP 地址作为限流键(省内存)',
'zone=mylimit:10m: 区域名称 mylimit,分配 10MB 内存',
'rate=10r/s: 每秒允许 10 个请求(漏桶算法)',
'burst=20: 桶的容量为 20,允许一定程度的突发流量',
'nodelay: 不延迟处理突发请求(立即处理或拒绝)'
]
},
{
id: 'connection',
name: '连接数限制',
config: `# 限制并发连接数
# zone=addr:10m: 区域名称为 addr,大小 10MB
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
listen 80;
server_name download.example.com;
location / {
# 每个 IP 最多 5 个并发连接
limit_conn addr 5;
# 同时应用限流:每秒 1 个请求
limit_req zone=mylimit rate=1r/s;
proxy_pass http://fileserver;
}
}`,
explanation: [
'limit_conn_zone: 定义连接数限制区域',
'limit_conn addr 5: 每个 IP 最多同时保持 5 个连接',
'适用于文件下载、视频流媒体等长连接场景',
'可以和 limit_req 同时使用(双重保护)',
'超过连接数限制时返回 503 Service Unavailable'
]
},
{
id: 'whiteblack',
name: '黑白名单',
config: `# 白名单 + 限流组合
# 公司内网 IP 不限流
geo $limit {
default 1;
10.0.0.0/8 0; # 内网网段
172.16.0.0/12 0; # 内网网段
192.168.0.0/16 0; # 内网网段
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
# 只有外网 IP 会触发限流
limit_req_zone $limit_key zone=sensitive:10m rate=1r/s;
server {
listen 80;
server_name api.example.com;
location /admin {
# 管理后台严格限流
limit_req zone=sensitive burst=5 nodelay;
# 拒绝特定 IP
deny 1.2.3.4;
deny 5.6.7.8;
proxy_pass http://backend;
}
}`,
explanation: [
'geo 模块:根据 IP 地址设置变量值',
'内网 IP 设置为 0,外网 IP 默认为 1',
'map 模块:将 0 映射为空字符串(不限流),1 映射为 IP 地址',
'只有外网 IP 会被限流,内网访问畅通无阻',
'deny 指令:直接拒绝特定 IP 访问',
'适用于管理后台、敏感接口的安全防护'
]
}
]
const currentNginxConfig = computed(() => nginxConfigs.find(c => c.id === currentConfig.value))
</script>
<style scoped>
.auth-middleware-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
text-align: center;
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.auth-tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.auth-tab {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1.25rem;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
}
.auth-tab:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.auth-tab.active {
border-color: var(--vp-c-brand);
background: rgba(var(--vp-c-brand-rgb), 0.1);
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.2);
}
.tab-icon {
font-size: 2rem;
}
.tab-name {
font-weight: 600;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.auth-flow {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.flow-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1.5rem;
text-align: center;
color: var(--vp-c-text-1);
}
.flow-diagram {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.flow-step {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.step-number {
width: 32px;
height: 32px;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.9rem;
flex-shrink: 0;
}
.step-content {
flex: 1;
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.step-actor {
font-weight: 700;
color: var(--vp-c-brand);
margin-bottom: 0.25rem;
}
.step-action {
color: var(--vp-c-text-2);
font-size: 0.95rem;
line-height: 1.5;
}
.step-arrow {
text-align: center;
font-size: 1.5rem;
color: var(--vp-c-text-2);
margin: 0.25rem 0;
}
.token-display {
margin-top: 1.5rem;
background: #1a1a2e;
border-radius: 12px;
padding: 1.5rem;
color: #eaeaea;
}
.token-header {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1rem;
color: #ffd700;
}
.token-parts {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.token-part {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 0.75rem;
}
.part-label {
font-weight: 700;
font-size: 0.75rem;
color: #ffd700;
margin-bottom: 0.25rem;
text-transform: uppercase;
}
.part-content {
font-family: monospace;
font-size: 0.8rem;
color: #a78bfa;
word-break: break-all;
margin-bottom: 0.5rem;
}
.part-decoded {
font-family: monospace;
font-size: 0.75rem;
color: #4ade80;
background: rgba(74, 222, 128, 0.1);
padding: 0.5rem;
border-radius: 4px;
}
.token-separator {
text-align: center;
font-size: 1.5rem;
font-weight: 700;
color: #ffd700;
}
.implementation-comparison {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.section-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1rem;
text-align: center;
color: var(--vp-c-text-1);
}
.comparison-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.comparison-table th,
.comparison-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--vp-c-divider);
vertical-align: top;
}
.comparison-table th {
font-weight: 600;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
white-space: nowrap;
}
.comparison-table td.dim {
font-weight: 600;
background: var(--vp-c-bg-soft);
white-space: nowrap;
}
.security-tips {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.1), rgba(34, 197, 94, 0.05));
border: 2px solid #22c55e;
border-radius: 12px;
padding: 1.5rem;
}
.tips-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1rem;
color: #15803d;
text-align: center;
}
.tips-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.tip-item {
display: flex;
gap: 1rem;
align-items: flex-start;
background: white;
border-radius: 8px;
padding: 1rem;
border: 1px solid #22c55e;
}
.tip-icon {
width: 32px;
height: 32px;
background: #22c55e;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
flex-shrink: 0;
}
.tip-content {
flex: 1;
}
.tip-heading {
font-weight: 700;
margin-bottom: 0.25rem;
color: var(--vp-c-text-1);
}
.tip-desc {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
@media (max-width: 768px) {
.auth-tabs {
grid-template-columns: 1fr;
}
.flow-step {
flex-direction: column;
gap: 0.5rem;
}
.step-content {
width: 100%;
}
.token-parts {
font-size: 0.75rem;
}
.comparison-table {
font-size: 0.75rem;
}
.comparison-table th,
.comparison-table td {
padding: 0.5rem;
}
}
</style>
@@ -0,0 +1,854 @@
<!--
LoadBalancingDemo.vue
负载均衡 - 轮询/加权/最少连接/IP哈希
-->
<template>
<div class="load-balancing-demo">
<div class="header">
<div class="title"> 负载均衡"压力"均匀分摊到多台服务器</div>
<div class="subtitle">想象成银行的取号系统把客户均匀分配到各个窗口避免某个窗口排长队</div>
</div>
<div class="strategy-selector">
<div class="selector-title">选择负载均衡策略</div>
<div class="strategy-tabs">
<button
v-for="strategy in strategies"
:key="strategy.id"
:class="['strategy-tab', { active: currentStrategy === strategy.id }]"
@click="changeStrategy(strategy.id)"
>
<span class="tab-icon">{{ strategy.icon }}</span>
<span class="tab-name">{{ strategy.name }}</span>
<span class="tab-badge" v-if="strategy.badge">{{ strategy.badge }}</span>
</button>
</div>
</div>
<div class="simulation-area">
<div class="sim-header">
<div class="sim-title">🎮 负载均衡模拟器</div>
<div class="sim-controls">
<button class="sim-btn" @click="startSimulation" :disabled="isSimulating">
{{ isSimulating ? '运行中...' : '▶ 开始模拟' }}
</button>
<button class="sim-btn reset" @click="resetSimulation"> 重置</button>
</div>
</div>
<div class="strategy-explanation">
<div class="exp-icon">💡</div>
<div class="exp-content">
<div class="exp-title">{{ currentStrategyData.name }} - {{ currentStrategyData.shortDesc }}</div>
<div class="exp-desc">{{ currentStrategyData.fullDesc }}</div>
</div>
</div>
<div class="servers-pool">
<div class="pool-header">
<div class="pool-title">🏢 后端服务器集群</div>
<div class="pool-config">
<label>服务器数量:</label>
<input type="range" v-model="serverCount" min="2" max="6" :disabled="isSimulating" />
<span>{{ serverCount }} </span>
</div>
</div>
<div class="servers-grid">
<div
v-for="server in servers"
:key="server.id"
:class="['server-card', { active: server.active, overloaded: server.load > 80 }]"
:style="{ borderColor: server.color }"
>
<div class="server-header">
<div class="server-icon">🖥</div>
<div class="server-name">{{ server.name }}</div>
<div class="server-status" :style="{ background: server.color }">
{{ server.load }}%
</div>
</div>
<div class="server-metrics">
<div class="metric">
<span class="metric-label">请求数:</span>
<span class="metric-value">{{ server.requests }}</span>
</div>
<div class="metric">
<span class="metric-label">权重:</span>
<input
v-if="currentStrategy === 'weighted'"
type="number"
v-model.number="server.weight"
min="1"
max="10"
:disabled="isSimulating"
class="weight-input"
/>
<span v-else>{{ server.weight }}</span>
</div>
</div>
<div class="load-bar">
<div class="load-fill" :style="{ width: server.load + '%', background: server.color }"></div>
</div>
<div class="recent-requests">
<div class="req-label">最近请求:</div>
<div class="req-list">
<span
v-for="(req, idx) in server.recentRequests"
:key="idx"
class="req-badge"
:style="{ background: req.color }"
>
{{ req.id }}
</span>
</div>
</div>
</div>
</div>
</div>
<div class="request-queue">
<div class="queue-header">
<div class="queue-title">📨 请求队列</div>
<div class="queue-stats">
<span>总请求: {{ totalRequests }}</span>
<span>待处理: {{ pendingRequests.length }}</span>
</div>
</div>
<div class="queue-items">
<div
v-for="req in displayedRequests"
:key="req.id"
:class="['queue-item', req.status]"
>
<span class="req-id">#{{ req.id }}</span>
<span class="req-arrow"></span>
<span v-if="req.assignedServer" class="req-target" :style="{ color: req.serverColor }">
{{ req.assignedServer }}
</span>
<span v-else class="req-status">{{ req.statusText }}</span>
</div>
</div>
</div>
<div class="strategy-stats">
<div class="stats-title">📊 负载分布统计</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{{ avgLoad }}%</div>
<div class="stat-label">平均负载</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ maxLoad }}%</div>
<div class="stat-label">最高负载</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ loadStdDev }}</div>
<div class="stat-label">负载标准差</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ mostBusyServer || '-' }}</div>
<div class="stat-label">最忙服务器</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, reactive, watch } from 'vue'
// 负载均衡策略
const strategies = [
{
id: 'roundrobin',
icon: '🔄',
name: '轮询',
badge: '默认',
shortDesc: '挨个分发,雨露均沾',
fullDesc: '按照服务器列表的顺序,依次将请求分配给每台服务器。就像银行叫号,1号窗口完事了到2号,2号完事了到3号,轮着来。'
},
{
id: 'weighted',
icon: '⚖️',
name: '加权轮询',
badge: '',
shortDesc: '性能好的多干活',
fullDesc: '给每台服务器设置一个权重值,性能强的服务器权重高,分配到的请求就多。就像团队里能力强的人多分担点任务。'
},
{
id: 'leastconn',
icon: '🔌',
name: '最少连接',
badge: '',
shortDesc: '谁闲找谁',
fullDesc: '将新请求分配给当前活跃连接数最少的服务器。就像食堂打饭,看哪个窗口排队的人少就去哪个。'
},
{
id: 'iphash',
icon: '🔢',
name: 'IP 哈希',
badge: '',
shortDesc: '同一用户永远去同一台',
fullDesc: '根据客户端 IP 地址计算哈希值,将同一 IP 的请求永远分配到同一台服务器。适用于需要保持会话状态的场景(如购物车)。'
}
]
const currentStrategy = ref('roundrobin')
const isSimulating = ref(false)
const serverCount = ref(4)
const currentIndex = ref(0)
const currentStrategyData = computed(() => strategies.find(s => s.id === currentStrategy.value))
// 生成服务器列表
const generateServers = (count) => {
const colors = ['#22c55e', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899']
const names = ['Server-A', 'Server-B', 'Server-C', 'Server-D', 'Server-E', 'Server-F']
return Array.from({ length: count }, (_, i) => ({
id: i,
name: names[i] || `Server-${i + 1}`,
color: colors[i % colors.length],
requests: 0,
load: Math.floor(Math.random() * 40) + 10,
weight: Math.floor(Math.random() * 5) + 1,
connections: Math.floor(Math.random() * 20),
active: false,
recentRequests: []
}))
}
const servers = ref(generateServers(serverCount.value))
// 请求队列
const requestQueue = ref([])
const totalRequests = ref(0)
const pendingRequests = computed(() => requestQueue.value.filter(r => r.status === 'pending'))
const displayedRequests = computed(() => requestQueue.value.slice(0, 10))
// 选择服务器的算法
const selectServer = (requestId, clientIP) => {
let selectedIndex = 0
switch (currentStrategy.value) {
case 'roundrobin':
selectedIndex = currentIndex.value % servers.value.length
currentIndex.value++
break
case 'weighted':
const totalWeight = servers.value.reduce((sum, s) => sum + s.weight, 0)
let random = Math.random() * totalWeight
for (let i = 0; i < servers.value.length; i++) {
random -= servers.value[i].weight
if (random <= 0) {
selectedIndex = i
break
}
}
break
case 'leastconn':
selectedIndex = servers.value.reduce((minIdx, s, i, arr) =>
s.connections < arr[minIdx].connections ? i : minIdx, 0)
break
case 'iphash':
const hash = clientIP.split('.').reduce((h, octet) => (h * 31 + parseInt(octet)) & 0xffffffff, 0)
selectedIndex = hash % servers.value.length
break
}
return servers.value[selectedIndex]
}
// 模拟请求
const simulateRequest = async () => {
const reqId = totalRequests.value + 1
const clientIP = `192.168.1.${Math.floor(Math.random() * 255) + 1}`
const request = {
id: reqId,
clientIP,
status: 'pending',
statusText: '等待分配...',
assignedServer: null,
serverColor: null
}
requestQueue.value.unshift(request)
totalRequests.value++
// 模拟分配延迟
await new Promise(resolve => setTimeout(resolve, 300))
const server = selectServer(reqId, clientIP)
request.assignedServer = server.name
request.serverColor = server.color
request.status = 'assigned'
request.statusText = '已分配'
// 更新服务器状态
server.requests++
server.connections++
server.load = Math.min(100, server.load + Math.floor(Math.random() * 10) + 5)
server.active = true
server.recentRequests.unshift({ id: reqId, color: '#22c55e' })
if (server.recentRequests.length > 5) server.recentRequests.pop()
setTimeout(() => {
server.connections = Math.max(0, server.connections - 1)
if (server.connections === 0) server.active = false
}, 2000)
}
// 开始模拟
const startSimulation = async () => {
isSimulating.value = true
for (let i = 0; i < 20; i++) {
if (!isSimulating.value) break
simulateRequest()
await new Promise(resolve => setTimeout(resolve, 400))
}
isSimulating.value = false
}
// 重置模拟
const resetSimulation = () => {
isSimulating.value = false
servers.value = generateServers(serverCount.value)
requestQueue.value = []
totalRequests.value = 0
currentIndex.value = 0
}
// 切换策略
const changeStrategy = (id) => {
currentStrategy.value = id
resetSimulation()
}
// 统计计算
const avgLoad = computed(() => {
if (servers.value.length === 0) return 0
return Math.round(servers.value.reduce((sum, s) => sum + s.load, 0) / servers.value.length)
})
const maxLoad = computed(() => {
if (servers.value.length === 0) return 0
return Math.max(...servers.value.map(s => s.load))
})
const loadStdDev = computed(() => {
if (servers.value.length === 0) return 0
const avg = avgLoad.value
const variance = servers.value.reduce((sum, s) => sum + Math.pow(s.load - avg, 2), 0) / servers.value.length
return Math.sqrt(variance).toFixed(1)
})
const mostBusyServer = computed(() => {
if (servers.value.length === 0) return null
return servers.value.reduce((max, s) => s.load > max.load ? s : max, servers.value[0]).name
})
// 监听服务器数量变化
watch(serverCount, (newVal) => {
if (!isSimulating.value) {
servers.value = generateServers(newVal)
}
})
</script>
<style scoped>
.load-balancing-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
text-align: center;
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.strategy-selector {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.selector-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1rem;
text-align: center;
color: var(--vp-c-text-1);
}
.strategy-tabs {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.strategy-tab {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.strategy-tab:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.strategy-tab.active {
border-color: var(--vp-c-brand);
background: rgba(var(--vp-c-brand-rgb), 0.1);
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.2);
}
.tab-icon {
font-size: 1.75rem;
}
.tab-name {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.tab-badge {
position: absolute;
top: -6px;
right: -6px;
background: #22c55e;
color: white;
font-size: 0.65rem;
font-weight: 700;
padding: 0.15rem 0.4rem;
border-radius: 999px;
}
.simulation-area {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.sim-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.sim-title {
font-weight: 700;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.sim-controls {
display: flex;
gap: 0.5rem;
}
.sim-btn {
padding: 0.6rem 1.25rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.sim-btn:hover:not(:disabled) {
background: var(--vp-c-brand-dark);
transform: translateY(-1px);
}
.sim-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.sim-btn.reset {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.strategy-explanation {
background: linear-gradient(135deg, rgba(var(--vp-c-brand-rgb), 0.1), rgba(var(--vp-c-brand-rgb), 0.05));
border: 2px solid var(--vp-c-brand);
border-radius: 10px;
padding: 1rem;
margin-bottom: 1.5rem;
display: flex;
gap: 1rem;
align-items: flex-start;
}
.exp-icon {
font-size: 1.5rem;
}
.exp-content {
flex: 1;
}
.exp-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 0.25rem;
color: var(--vp-c-text-1);
}
.exp-desc {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.servers-pool {
margin-bottom: 1.5rem;
}
.pool-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
flex-wrap: wrap;
gap: 0.75rem;
}
.pool-title {
font-weight: 700;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.pool-config {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.pool-config input[type="range"] {
width: 120px;
}
.pool-config span {
min-width: 50px;
font-weight: 600;
}
.servers-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}
.server-card {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
transition: all 0.3s;
}
.server-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.server-card.active {
box-shadow: 0 0 0 3px currentColor;
}
.server-card.overloaded {
background: #fef2f2;
border-color: #ef4444;
}
.server-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.server-icon {
font-size: 1.25rem;
}
.server-name {
flex: 1;
font-weight: 700;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.server-status {
padding: 0.25rem 0.5rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 700;
color: white;
}
.server-metrics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.metric {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
}
.metric-label {
color: var(--vp-c-text-2);
}
.metric-value {
font-weight: 700;
color: var(--vp-c-text-1);
}
.weight-input {
width: 50px;
padding: 0.1rem 0.25rem;
font-size: 0.8rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
.load-bar {
height: 8px;
background: var(--vp-c-bg-alt);
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.75rem;
}
.load-fill {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease;
}
.recent-requests {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
}
.req-label {
color: var(--vp-c-text-2);
white-space: nowrap;
}
.req-list {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
}
.req-badge {
padding: 0.1rem 0.3rem;
border-radius: 3px;
font-size: 0.7rem;
font-weight: 600;
color: white;
}
.request-queue {
background: var(--vp-c-bg-soft);
border-radius: 10px;
padding: 1rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.queue-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.queue-title {
font-weight: 700;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.queue-stats {
display: flex;
gap: 1rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.queue-items {
display: flex;
flex-direction: column;
gap: 0.25rem;
max-height: 200px;
overflow-y: auto;
}
.queue-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.6rem;
background: white;
border-radius: 5px;
font-size: 0.8rem;
border-left: 3px solid var(--vp-c-divider);
}
.queue-item.pending {
border-left-color: #f59e0b;
background: #fffbeb;
}
.queue-item.assigned {
border-left-color: #22c55e;
background: #f0fdf4;
}
.req-id {
font-weight: 700;
color: var(--vp-c-text-1);
min-width: 40px;
}
.req-arrow {
color: var(--vp-c-text-2);
}
.req-target {
font-weight: 700;
}
.req-status {
color: var(--vp-c-text-2);
font-style: italic;
}
.strategy-stats {
background: var(--vp-c-bg-soft);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.stats-title {
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 0.75rem;
text-align: center;
color: var(--vp-c-text-1);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 0.75rem;
text-align: center;
border: 1px solid var(--vp-c-divider);
}
.stat-value {
font-weight: 700;
font-size: 1.25rem;
color: var(--vp-c-brand);
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
@media (max-width: 768px) {
.strategy-tabs {
grid-template-columns: 1fr;
}
.auth-tabs {
grid-template-columns: 1fr;
}
.servers-grid {
grid-template-columns: 1fr;
}
.server-metrics {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,522 @@
<!--
NginxArchitectureDemo.vue
Nginx架构 - Master-Worker/事件驱动
-->
<template>
<div class="nginx-architecture-demo">
<div class="header">
<div class="title"> Nginx 架构揭秘为什么它能扛住百万并发</div>
<div class="subtitle">Master-Worker 进程模型 + 事件驱动 = 高性能的秘诀</div>
</div>
<div class="architecture-diagram">
<div class="diagram-title">Nginx 进程架构图</div>
<div class="process-layer master-layer">
<div class="process master">
<div class="process-icon">👑</div>
<div class="process-info">
<div class="process-name">Master 进程</div>
<div class="process-desc">管理所有 Worker负责配置加载平滑升级</div>
</div>
</div>
</div>
<div class="connections">
<div class="connection-line" v-for="n in workerCount" :key="n"></div>
</div>
<div class="process-layer worker-layer">
<div class="worker-controls">
<button class="control-btn" @click="decreaseWorker" :disabled="workerCount <= 1">-</button>
<span class="worker-count">{{ workerCount }} Worker</span>
<button class="control-btn" @click="increaseWorker" :disabled="workerCount >= 8">+</button>
</div>
<div class="workers">
<div
class="process worker"
v-for="n in workerCount"
:key="n"
:class="{ active: activeWorker === n, processing: processingWorkers.includes(n) }"
@click="activateWorker(n)"
>
<div class="process-icon"></div>
<div class="process-info">
<div class="process-name">Worker {{ n }}</div>
<div class="process-desc">处理 {{ requestCounts[n] || 0 }} 请求</div>
</div>
<div class="status-indicator"></div>
</div>
</div>
</div>
<div class="epoll-layer">
<div class="epoll-box">
<div class="epoll-title">📡 epoll (Linux) / kqueue (macOS)</div>
<div class="epoll-desc">事件驱动一个 Worker 同时处理数万个连接</div>
<div class="epoll-comparison">
<div class="compare-item old">
<div class="compare-title">传统 Apache</div>
<div class="compare-detail">一个连接 = 一个进程/线程</div>
<div class="compare-result"> C10K 问题</div>
</div>
<div class="vs">VS</div>
<div class="compare-item new">
<div class="compare-title">Nginx</div>
<div class="compare-detail">事件驱动 + 异步非阻塞</div>
<div class="compare-result"> 百万并发</div>
</div>
</div>
</div>
</div>
</div>
<div class="simulation-panel">
<div class="panel-title">🎮 模拟请求处理</div>
<div class="sim-controls">
<button class="sim-btn" @click="simulateRequests" :disabled="isSimulating">
{{ isSimulating ? '处理中...' : '发送 20 个并发请求' }}
</button>
<button class="sim-btn secondary" @click="resetSimulation">重置</button>
</div>
<div class="sim-stats" v-if="totalRequests > 0">
<div class="stat-item">
<div class="stat-value">{{ totalRequests }}</div>
<div class="stat-label">总请求数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ mostActiveWorker }}</div>
<div class="stat-label">最忙 Worker</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ avgRequests.toFixed(1) }}</div>
<div class="stat-label">平均/Worker</div>
</div>
</div>
</div>
<div class="config-tip">
<div class="tip-title">💡 生产环境建议</div>
<div class="tip-content">
<strong>Worker 数量 = CPU 核心数</strong>通常设置为 auto Nginx 自动检测
<br>
太多了上下文切换开销大太少了无法利用多核性能
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const workerCount = ref(4)
const activeWorker = ref(1)
const requestCounts = ref({})
const isSimulating = ref(false)
const processingWorkers = ref([])
const totalRequests = computed(() => {
return Object.values(requestCounts.value).reduce((a, b) => a + b, 0)
})
const mostActiveWorker = computed(() => {
let max = 0
let worker = '-'
Object.entries(requestCounts.value).forEach(([k, v]) => {
if (v > max) {
max = v
worker = k
}
})
return worker
})
const avgRequests = computed(() => {
if (workerCount.value === 0) return 0
return totalRequests.value / workerCount.value
})
const increaseWorker = () => {
if (workerCount.value < 8) {
workerCount.value++
}
}
const decreaseWorker = () => {
if (workerCount.value > 1) {
workerCount.value--
}
}
const activateWorker = (n) => {
activeWorker.value = n
}
const simulateRequests = async () => {
isSimulating.value = true
const requests = 20
for (let i = 0; i < requests; i++) {
const worker = Math.floor(Math.random() * workerCount.value) + 1
processingWorkers.value.push(worker)
await new Promise(resolve => setTimeout(resolve, 100))
requestCounts.value[worker] = (requestCounts.value[worker] || 0) + 1
processingWorkers.value = processingWorkers.value.filter(w => w !== worker)
}
isSimulating.value = false
}
const resetSimulation = () => {
requestCounts.value = {}
processingWorkers.value = []
isSimulating.value = false
}
</script>
<style scoped>
.nginx-architecture-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
text-align: center;
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.architecture-diagram {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.diagram-title {
font-weight: 700;
font-size: 1.1rem;
text-align: center;
margin-bottom: 1.5rem;
color: var(--vp-c-text-1);
}
.process-layer {
margin-bottom: 1.5rem;
}
.process {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.5rem;
border-radius: 12px;
transition: all 0.3s;
}
.process.master {
background: linear-gradient(135deg, #fef3c7, #fde68a);
border: 2px solid #f59e0b;
}
.process.worker {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
cursor: pointer;
}
.process.worker:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.process.worker.active {
border-color: var(--vp-c-brand);
background: rgba(var(--vp-c-brand-rgb), 0.1);
}
.process.worker.processing {
animation: pulse 0.5s ease-in-out;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.process-icon {
font-size: 2rem;
}
.process-info {
flex: 1;
}
.process-name {
font-weight: 700;
font-size: 1rem;
margin-bottom: 0.25rem;
}
.process-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #22c55e;
}
.worker-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-bottom: 1rem;
}
.control-btn {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
font-weight: 700;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.2s;
}
.control-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.control-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.worker-count {
font-weight: 600;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.workers {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem;
}
.epoll-layer {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 2px solid var(--vp-c-divider);
}
.epoll-box {
background: linear-gradient(135deg, rgba(var(--vp-c-brand-rgb), 0.1), rgba(var(--vp-c-brand-rgb), 0.05));
border: 2px solid var(--vp-c-brand);
border-radius: 12px;
padding: 1.5rem;
}
.epoll-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.epoll-desc {
color: var(--vp-c-text-2);
margin-bottom: 1.5rem;
}
.epoll-comparison {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1rem;
align-items: center;
}
.compare-item {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
text-align: center;
}
.compare-item.old {
border: 2px solid #ef4444;
}
.compare-item.new {
border: 2px solid #22c55e;
}
.compare-title {
font-weight: 700;
margin-bottom: 0.5rem;
}
.compare-detail {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.compare-result {
font-weight: 700;
font-size: 1.1rem;
}
.old .compare-result {
color: #ef4444;
}
.new .compare-result {
color: #22c55e;
}
.vs {
font-weight: 700;
font-size: 1.2rem;
color: var(--vp-c-text-2);
}
.simulation-panel {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.panel-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1rem;
text-align: center;
}
.sim-controls {
display: flex;
gap: 1rem;
justify-content: center;
margin-bottom: 1rem;
}
.sim-btn {
padding: 0.75rem 1.5rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.sim-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.3);
}
.sim-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.sim-btn.secondary {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.sim-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-top: 1rem;
}
.stat-item {
text-align: center;
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.stat-value {
font-weight: 700;
font-size: 1.5rem;
color: var(--vp-c-brand);
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.config-tip {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.1), rgba(34, 197, 94, 0.05));
border: 2px solid #22c55e;
border-radius: 12px;
padding: 1.25rem;
}
.tip-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.75rem;
color: #15803d;
}
.tip-content {
color: var(--vp-c-text-1);
line-height: 1.7;
}
@media (max-width: 768px) {
.epoll-comparison {
grid-template-columns: 1fr;
}
.vs {
text-align: center;
}
.sim-stats {
grid-template-columns: 1fr;
}
.workers {
grid-template-columns: 1fr;
}
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,378 @@
<!--
ReverseProxyDemo.vue
反向代理原理 - 正向代理 vs 反向代理
-->
<template>
<div class="reverse-proxy-demo">
<div class="header">
<div class="title">🔄 反向代理 vs 正向代理</div>
<div class="subtitle">一句话区分正向代理是"客户端的代理"反向代理是"服务器的代理"</div>
</div>
<div class="mode-selector">
<button
:class="['mode-btn', { active: mode === 'forward' }]"
@click="mode = 'forward'"
>
🔓 正向代理 (翻墙/隐藏身份)
</button>
<button
:class="['mode-btn', { active: mode === 'reverse' }]"
@click="mode = 'reverse'"
>
🛡 反向代理 (负载均衡/安全防护)
</button>
</div>
<div class="flow-container">
<div class="flow-row" v-if="mode === 'forward'">
<div class="flow-card client">
<div class="icon">👤</div>
<div class="label">用户 (想翻墙)</div>
</div>
<div class="arrow-box">
<div class="arrow"></div>
<div class="note">发给代理</div>
</div>
<div class="flow-card proxy forward">
<div class="icon">🔓</div>
<div class="label">正向代理 (VPN/SS)</div>
<div class="tag">代理客户端</div>
</div>
<div class="arrow-box">
<div class="arrow"></div>
<div class="note">转发请求</div>
</div>
<div class="flow-card target">
<div class="icon">🌐</div>
<div class="label">目标网站 (Google)</div>
</div>
</div>
<div class="flow-row" v-if="mode === 'reverse'">
<div class="flow-card client">
<div class="icon">👤</div>
<div class="label">用户 (浏览器)</div>
</div>
<div class="arrow-box">
<div class="arrow"></div>
<div class="note">访问域名</div>
</div>
<div class="flow-card proxy reverse">
<div class="icon">🛡</div>
<div class="label">反向代理 (Nginx)</div>
<div class="tag">代理服务器</div>
</div>
<div class="arrow-box">
<div class="arrow"></div>
<div class="note">负载均衡</div>
</div>
<div class="flow-card server">
<div class="icon"></div>
<div class="label">后端服务器集群</div>
<div class="sub-label">Web1 | Web2 | Web3</div>
</div>
</div>
</div>
<div class="detail-section">
<div class="detail-card">
<div class="detail-title">
{{ mode === 'forward' ? '🔓 正向代理特点' : '🛡️ 反向代理特点' }}
</div>
<ul class="detail-list">
<li v-for="(item, index) in currentFeatures" :key="index">{{ item }}</li>
</ul>
</div>
<div class="detail-card">
<div class="detail-title">💡 典型使用场景</div>
<ul class="detail-list">
<li v-for="(item, index) in currentScenarios" :key="index">{{ item }}</li>
</ul>
</div>
</div>
<div class="memory-trick">
<div class="trick-title">🧠 记忆口诀</div>
<div class="trick-content">
<p v-if="mode === 'forward'">
<strong>"正向代理 = 代理客户端"</strong> 客户端知情服务器只知道代理IP
</p>
<p v-else>
<strong>"反向代理 = 代理服务器"</strong> 客户端不知道真实服务器只知道域名
</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const mode = ref('reverse')
const forwardFeatures = [
'客户端需要主动配置代理服务器地址',
'服务端只知道代理IP,不知道真实客户端IP',
'主要用于翻墙、隐藏身份、突破网络限制',
'典型代表:VPN、Shadowsocks、V2Ray'
]
const reverseFeatures = [
'客户端无感知,只需要访问域名',
'隐藏真实服务器架构,统一对外接口',
'提供负载均衡、安全防护、SSL卸载等功能',
'典型代表:Nginx、HAProxy、AWS ELB'
]
const forwardScenarios = [
'访问被屏蔽的网站(Google、YouTube',
'隐藏真实IP地址,保护个人隐私',
'公司内部网络访问外部资源',
'爬虫程序使用代理池防止被封IP'
]
const reverseScenarios = [
'网站需要承载高并发流量(负载均衡)',
'统一HTTPS证书管理(SSL卸载)',
'防护DDoS攻击和SQL注入',
'灰度发布、A/B测试、蓝绿部署'
]
const currentFeatures = computed(() => mode.value === 'forward' ? forwardFeatures : reverseFeatures)
const currentScenarios = computed(() => mode.value === 'forward' ? forwardScenarios : reverseScenarios)
</script>
<style scoped>
.reverse-proxy-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
text-align: center;
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
line-height: 1.5;
}
.mode-selector {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.mode-btn {
flex: 1;
min-width: 200px;
padding: 1rem 1.5rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
font-weight: 600;
font-size: 0.95rem;
}
.mode-btn:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
}
.mode-btn.active {
border-color: var(--vp-c-brand);
background: rgba(var(--vp-c-brand-rgb), 0.1);
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.2);
}
.flow-container {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.flow-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
}
.flow-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1rem;
border-radius: 12px;
min-width: 100px;
text-align: center;
transition: all 0.3s;
}
.flow-card.client {
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
border: 2px solid #3b82f6;
}
.flow-card.proxy {
background: linear-gradient(135deg, #fef3c7, #fde68a);
border: 2px solid #f59e0b;
position: relative;
}
.flow-card.proxy.forward {
background: linear-gradient(135deg, #dcfce7, #bbf7d0);
border-color: #22c55e;
}
.flow-card.proxy.reverse {
background: linear-gradient(135deg, #fce7f3, #fbcfe8);
border-color: #ec4899;
}
.flow-card.target {
background: linear-gradient(135deg, #e0e7ff, #c7d2fe);
border: 2px solid #6366f1;
}
.flow-card.server {
background: linear-gradient(135deg, #f3e8ff, #e9d5ff);
border: 2px solid #a855f7;
}
.flow-card .icon {
font-size: 2rem;
}
.flow-card .label {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-1);
}
.flow-card .sub-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.flow-card .tag {
position: absolute;
top: -10px;
right: -10px;
background: var(--vp-c-brand);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 999px;
font-size: 0.7rem;
font-weight: 600;
}
.arrow-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.arrow {
font-size: 1.5rem;
color: var(--vp-c-text-2);
}
.arrow .miss-text {
font-size: 0.75rem;
color: #ef4444;
}
.note {
font-size: 0.75rem;
color: var(--vp-c-text-2);
text-align: center;
}
.detail-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1.5rem;
}
.detail-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1.25rem;
}
.detail-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 1rem;
color: var(--vp-c-text-1);
}
.detail-list {
margin: 0;
padding-left: 1.25rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
line-height: 1.8;
}
.memory-trick {
background: linear-gradient(135deg, rgba(var(--vp-c-brand-rgb), 0.1), rgba(var(--vp-c-brand-rgb), 0.05));
border: 2px solid var(--vp-c-brand);
border-radius: 12px;
padding: 1.25rem;
text-align: center;
}
.trick-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.75rem;
color: var(--vp-c-brand);
}
.trick-content {
color: var(--vp-c-text-1);
font-size: 1rem;
line-height: 1.6;
}
@media (max-width: 768px) {
.flow-row {
flex-direction: column;
gap: 1rem;
}
.detail-section {
grid-template-columns: 1fr;
}
.mode-btn {
min-width: 100%;
}
}
</style>
@@ -0,0 +1,570 @@
<!--
RoutingRulesDemo.vue
路由规则 - 路径匹配/重写/转发
-->
<template>
<div class="routing-rules-demo">
<div class="header">
<div class="title">🧭 路由规则如何把请求送到正确的服务</div>
<div class="subtitle">想象成快递分拣中心根据地址把包裹分配到不同的配送站</div>
</div>
<div class="playground">
<div class="playground-header">
<div class="playground-title">🎮 路由规则实验室</div>
<div class="playground-subtitle">输入一个 URL看看它会被路由到哪个服务</div>
</div>
<div class="input-section">
<div class="input-group">
<label>HTTP 方法</label>
<select v-model="request.method">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
</select>
</div>
<div class="input-group flex-2">
<label>URL 路径</label>
<input
type="text"
v-model="request.path"
placeholder="/api/users/123"
@keyup.enter="matchRoute"
/>
</div>
<div class="input-group">
<label>Header (可选)</label>
<input
type="text"
v-model="request.header"
placeholder="X-Version: v2"
/>
</div>
</div>
<button class="match-btn" @click="matchRoute" :disabled="isMatching">
{{ isMatching ? '匹配中...' : '🔍 开始匹配' }}
</button>
<div class="result-section" v-if="matchResult">
<div :class="['result-card', matchResult.found ? 'success' : 'fail']">
<div class="result-header">
<div class="result-icon">{{ matchResult.found ? '✅' : '❌' }}</div>
<div class="result-title">{{ matchResult.found ? '匹配成功' : '未找到匹配规则' }}</div>
</div>
<div class="result-detail" v-if="matchResult.found">
<div class="detail-row">
<span class="label">目标服务</span>
<span class="value service">{{ matchResult.service }}</span>
</div>
<div class="detail-row">
<span class="label">匹配规则</span>
<span class="value">{{ matchResult.rule }}</span>
</div>
<div class="detail-row">
<span class="label">重写后路径</span>
<span class="value path">{{ matchResult.rewrittenPath }}</span>
</div>
<div class="detail-row">
<span class="label">目标地址</span>
<span class="value url">{{ matchResult.targetUrl }}</span>
</div>
</div>
<div class="result-suggestion" v-else>
<p>💡 建议检查</p>
<ul>
<li>路径是否以 /api 开头</li>
<li>HTTP 方法是否匹配GET/POST</li>
<li>Header 条件是否满足</li>
</ul>
</div>
</div>
</div>
</div>
<div class="rules-table">
<div class="table-title">📋 当前路由规则表</div>
<table>
<thead>
<tr>
<th>优先级</th>
<th>匹配规则</th>
<th>目标服务</th>
<th>路径重写</th>
</tr>
</thead>
<tbody>
<tr v-for="(rule, index) in routingRules" :key="index"
:class="{ active: matchResult && matchResult.ruleIndex === index }">
<td>{{ index + 1 }}</td>
<td><code>{{ rule.match }}</code></td>
<td><span class="service-tag">{{ rule.service }}</span></td>
<td><code v-if="rule.rewrite">{{ rule.rewrite }}</code><span v-else class="no-rewrite"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="quick-presets">
<div class="presets-title">🚀 快速测试示例</div>
<div class="preset-buttons">
<button
v-for="preset in presets"
:key="preset.name"
class="preset-btn"
@click="applyPreset(preset)"
>
{{ preset.name }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const request = reactive({
method: 'GET',
path: '/api/users/123',
header: ''
})
const isMatching = ref(false)
const matchResult = ref(null)
const routingRules = [
{
match: 'Header: X-Version=v2',
service: '用户服务V2',
rewrite: null
},
{
match: 'Path: /api/users/*',
service: '用户服务',
rewrite: '/users/*'
},
{
match: 'Path: /api/orders/*',
service: '订单服务',
rewrite: '/orders/*'
},
{
match: 'Path: /api/pay/*',
service: '支付服务',
rewrite: '/payments/*'
},
{
match: 'Method: GET, Path: /health',
service: '健康检查',
rewrite: null
}
]
const presets = [
{ name: '👤 查询用户', method: 'GET', path: '/api/users/123', header: '' },
{ name: '📦 创建订单', method: 'POST', path: '/api/orders', header: '' },
{ name: '💳 发起支付', method: 'POST', path: '/api/pay/checkout', header: '' },
{ name: '🔍 健康检查', method: 'GET', path: '/health', header: '' },
{ name: '🆕 V2版本', method: 'GET', path: '/api/users/456', header: 'X-Version: v2' }
]
const matchRoute = async () => {
isMatching.value = true
matchResult.value = null
await new Promise(resolve => setTimeout(resolve, 500))
const path = request.path
const method = request.method
const header = request.header
let found = false
let matchedIndex = -1
let service = ''
let rule = ''
let rewrittenPath = path
let targetUrl = ''
if (header.includes('X-Version=v2')) {
found = true
matchedIndex = 0
service = '用户服务V2 (新版本)'
rule = 'Header: X-Version=v2'
targetUrl = 'http://user-service-v2:8080' + path
} else if (path.startsWith('/api/users/')) {
found = true
matchedIndex = 1
service = '用户服务'
rule = 'Path: /api/users/*'
rewrittenPath = path.replace('/api/users/', '/users/')
targetUrl = 'http://user-service:8080' + rewrittenPath
} else if (path.startsWith('/api/orders')) {
found = true
matchedIndex = 2
service = '订单服务'
rule = 'Path: /api/orders/*'
rewrittenPath = path.replace('/api/orders/', '/orders/')
targetUrl = 'http://order-service:8080' + rewrittenPath
} else if (path.startsWith('/api/pay/')) {
found = true
matchedIndex = 3
service = '支付服务'
rule = 'Path: /api/pay/*'
rewrittenPath = path.replace('/api/pay/', '/payments/')
targetUrl = 'http://payment-service:8080' + rewrittenPath
} else if (method === 'GET' && path === '/health') {
found = true
matchedIndex = 4
service = '健康检查'
rule = 'Method: GET, Path: /health'
targetUrl = 'http://health-check-service:8080/health'
}
matchResult.value = {
found,
service,
rule,
ruleIndex: matchedIndex,
originalPath: path,
rewrittenPath,
targetUrl
}
isMatching.value = false
}
const applyPreset = (preset) => {
request.method = preset.method
request.path = preset.path
request.header = preset.header
matchResult.value = null
}
</script>
<style scoped>
.routing-rules-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
text-align: center;
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.playground {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.playground-header {
margin-bottom: 1.5rem;
}
.playground-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.25rem;
color: var(--vp-c-text-1);
}
.playground-subtitle {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.input-section {
display: grid;
grid-template-columns: auto 2fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.input-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.input-group.flex-2 {
flex: 2;
}
.input-group label {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-2);
text-transform: uppercase;
}
.input-group input,
.input-group select {
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
font-size: 0.95rem;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.input-group input:focus,
.input-group select:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.match-btn {
width: 100%;
padding: 1rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.match-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(var(--vp-c-brand-rgb), 0.3);
}
.match-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.result-section {
margin-top: 1.5rem;
}
.result-card {
border-radius: 10px;
padding: 1.25rem;
border: 2px solid;
}
.result-card.success {
background: rgba(34, 197, 94, 0.1);
border-color: #22c55e;
}
.result-card.fail {
background: rgba(239, 68, 68, 0.1);
border-color: #ef4444;
}
.result-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.result-icon {
font-size: 1.5rem;
}
.result-title {
font-weight: 700;
font-size: 1.1rem;
}
.result-detail {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.detail-row {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.detail-row .label {
font-weight: 600;
color: var(--vp-c-text-2);
min-width: 100px;
}
.detail-row .value {
font-family: monospace;
background: var(--vp-c-bg-soft);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.9rem;
}
.detail-row .value.service {
background: rgba(34, 197, 94, 0.2);
color: #15803d;
}
.detail-row .value.path {
background: rgba(59, 130, 246, 0.2);
color: #1d4ed8;
}
.detail-row .value.url {
background: rgba(168, 85, 247, 0.2);
color: #7c3aed;
}
.result-suggestion {
color: var(--vp-c-text-2);
}
.result-suggestion ul {
margin: 0.5rem 0 0 0;
padding-left: 1.5rem;
}
.result-suggestion li {
margin: 0.25rem 0;
}
.rules-table {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.table-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1rem;
text-align: center;
color: var(--vp-c-text-1);
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
th, td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--vp-c-divider);
}
th {
font-weight: 600;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
tr:hover {
background: var(--vp-c-bg-soft);
}
tr.active {
background: rgba(34, 197, 94, 0.1);
}
.service-tag {
display: inline-block;
padding: 0.25rem 0.5rem;
background: var(--vp-c-brand);
color: white;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
code {
font-family: monospace;
background: var(--vp-c-bg-soft);
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-size: 0.85em;
}
.quick-presets {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.presets-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1rem;
text-align: center;
}
.preset-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: center;
}
.preset-btn {
padding: 0.6rem 1rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.9rem;
}
.preset-btn:hover {
border-color: var(--vp-c-brand);
background: rgba(var(--vp-c-brand-rgb), 0.1);
}
@media (max-width: 768px) {
.input-section {
grid-template-columns: 1fr;
}
.detail-row {
flex-direction: column;
align-items: flex-start;
}
.detail-row .label {
min-width: auto;
}
table {
font-size: 0.75rem;
}
th, td {
padding: 0.5rem;
}
}
</style>
@@ -0,0 +1,723 @@
<!--
SslTerminationDemo.vue
SSL终结 - HTTPS卸载/证书管理
-->
<template>
<div class="ssl-termination-demo">
<div class="header">
<div class="title">🔒 SSL 终结HTTPS 流量的"解密官"</div>
<div class="subtitle">想象成公司的前台接待对外使用正式头衔HTTPS对内用内部称呼HTTP负责"翻译"身份</div>
</div>
<div class="ssl-flow">
<div class="flow-title">🔐 HTTPS 流量解密流程</div>
<div class="flow-diagram">
<!-- 客户端 -->
<div class="flow-node client">
<div class="node-icon">👤</div>
<div class="node-label">客户端 (浏览器)</div>
<div class="node-detail">发起 HTTPS 请求</div>
</div>
<div class="flow-arrow encrypted">
<div class="arrow-line"></div>
<div class="arrow-label">
<span class="lock-icon">🔒</span>
<span>TLS 加密连接</span>
</div>
<div class="cert-info">
<div class="cert-item"><span class="cert-label">证书:</span> *.example.com</div>
<div class="cert-item"><span class="cert-label">算法:</span> TLS 1.3</div>
<div class="cert-item"><span class="cert-label">加密:</span> AES-256-GCM</div>
</div>
</div>
<!-- Nginx -->
<div class="flow-node nginx">
<div class="node-icon">🚪</div>
<div class="node-label">Nginx (SSL 终结)</div>
<div class="node-actions">
<div class="action"><span class="action-icon">📜</span> 校验证书</div>
<div class="action"><span class="action-icon">🔓</span> 解密流量</div>
<div class="action"><span class="action-icon">📝</span> 添加 X-Forwarded-*</div>
</div>
</div>
<div class="flow-arrow plain">
<div class="arrow-line"></div>
<div class="arrow-label">
<span class="unlock-icon">🔓</span>
<span>HTTP 明文</span>
</div>
<div class="headers-info">
<div class="header-item">X-Forwarded-For: 203.0.113.42</div>
<div class="header-item">X-Forwarded-Proto: https</div>
<div class="header-item">X-Real-IP: 203.0.113.42</div>
</div>
</div>
<!-- 后端服务 -->
<div class="flow-node backend">
<div class="node-icon"></div>
<div class="node-label">后端服务集群</div>
<div class="node-detail">专注于业务逻辑无需处理 TLS</div>
</div>
</div>
</div>
<div class="cert-management">
<div class="section-title">📜 SSL 证书管理</div>
<div class="cert-tabs">
<button
v-for="tab in certTabs"
:key="tab.id"
:class="['cert-tab', { active: currentCertTab === tab.id }]"
@click="currentCertTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<div class="cert-content">
<!-- 证书申请流程 -->
<div v-if="currentCertTab === 'apply'" class="apply-flow">
<div class="flow-steps">
<div v-for="(step, index) in certSteps" :key="index" class="cert-step">
<div class="step-badge">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-title">{{ step.title }}</div>
<div class="step-desc">{{ step.desc }}</div>
<div class="step-command" v-if="step.command">
<code>{{ step.command }}</code>
</div>
</div>
</div>
</div>
</div>
<!-- Nginx 配置 -->
<div v-if="currentCertTab === 'config'" class="nginx-config">
<pre class="config-block"><code>server {
listen 443 ssl http2;
server_name api.example.com;
# SSL 证书配置
ssl_certificate /etc/nginx/ssl/api.example.com.crt;
ssl_certificate_key /etc/nginx/ssl/api.example.com.key;
# SSL 协议和密码套件
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# SSL 会话缓存
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/chain.crt;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# 安全响应头
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
location / {
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;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# HTTP 重定向到 HTTPS
server {
listen 80;
server_name api.example.com;
return 301 https://$server_name$request_uri;
}</code></pre>
</div>
<!-- 最佳实践 -->
<div v-if="currentCertTab === 'bestpractice'" class="best-practices">
<div class="practices-grid">
<div v-for="practice in bestPractices" :key="practice.id" class="practice-card">
<div class="practice-header">
<span class="practice-icon">{{ practice.icon }}</span>
<span class="practice-title">{{ practice.title }}</span>
</div>
<div class="practice-content">{{ practice.content }}</div>
<div class="practice-code" v-if="practice.code">
<code>{{ practice.code }}</code>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="benefits-section">
<div class="section-title"> SSL 终结的核心优势</div>
<div class="benefits-grid">
<div class="benefit-card">
<div class="benefit-icon">🚀</div>
<div class="benefit-title">性能提升</div>
<div class="benefit-desc">TLS 握手和加密解密是 CPU 密集型操作集中在 Nginx 处理后端服务专注业务逻辑整体吞吐量提升 2-5 </div>
</div>
<div class="benefit-card">
<div class="benefit-icon">🔧</div>
<div class="benefit-title">简化运维</div>
<div class="benefit-desc">证书统一管理只需在 Nginx 配置一次无需在每个后端服务重复配置证书续期更换一键完成</div>
</div>
<div class="benefit-card">
<div class="benefit-icon">🛡</div>
<div class="benefit-title">集中安全</div>
<div class="benefit-desc">SSL/TLS 配置统一管控强制使用最新协议版本和密码套件统一添加安全响应头HSTSCSP </div>
</div>
<div class="benefit-card">
<div class="benefit-icon">📊</div>
<div class="benefit-title">统一监控</div>
<div class="benefit-desc">所有 HTTPS 流量经过 Nginx可以统一记录访问日志分析 SSL 握手性能监控证书有效期便于审计和排障</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
// 证书管理标签
const certTabs = [
{ id: 'apply', name: '证书申请' },
{ id: 'config', name: 'Nginx 配置' },
{ id: 'bestpractice', name: '最佳实践' }
]
const currentCertTab = ref('apply')
// 证书申请步骤
const certSteps = [
{
title: '生成私钥',
desc: '使用 OpenSSL 生成 RSA 私钥,这是证书的基础',
command: 'openssl genrsa -out private.key 2048'
},
{
title: '创建 CSR',
desc: '生成证书签名请求,包含域名和组织信息',
command: 'openssl req -new -key private.key -out csr.pem'
},
{
title: '域名验证',
desc: 'CA 机构验证域名所有权(DNS 记录或 HTTP 文件)',
command: '# 添加 DNS TXT 记录 或 上传验证文件到 /.well-known/'
},
{
title: '签发证书',
desc: '验证通过后,CA 签发证书文件',
command: '# 下载 certificate.crt 和 chain.crt'
},
{
title: '部署配置',
desc: '将证书配置到 Nginx 并测试',
command: 'nginx -t && systemctl reload nginx'
}
]
// 最佳实践
const bestPractices = [
{
id: 'protocol',
icon: '🔐',
title: '使用 TLS 1.2+',
content: '禁用 SSLv3、TLS 1.0/1.1 等老旧协议,仅启用 TLS 1.2 和 1.3',
code: 'ssl_protocols TLSv1.2 TLSv1.3;'
},
{
id: 'cipher',
icon: '🛡️',
title: '强密码套件',
content: '禁用弱加密算法,优先使用 ECDHE 和 AES-GCM',
code: 'ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;'
},
{
id: 'hsts',
icon: '🔒',
title: 'HSTS 头部',
content: '强制浏览器始终使用 HTTPS 访问,防止 SSL 剥离攻击',
code: 'add_header Strict-Transport-Security "max-age=63072000" always;'
},
{
id: 'ocsp',
icon: '✅',
title: 'OCSP Stapling',
content: '启用 OCSP 装订,加速 SSL 握手并保护用户隐私',
code: 'ssl_stapling on; ssl_stapling_verify on;'
}
]
</script>
<style scoped>
.ssl-termination-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
text-align: center;
}
.title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.ssl-flow {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.flow-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1.5rem;
text-align: center;
color: var(--vp-c-text-1);
}
.flow-diagram {
display: flex;
flex-direction: column;
gap: 1rem;
}
.flow-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
text-align: center;
}
.flow-node.client {
border-color: #3b82f6;
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
}
.flow-node.nginx {
border-color: #22c55e;
background: linear-gradient(135deg, #dcfce7, #bbf7d0);
}
.flow-node.backend {
border-color: #8b5cf6;
background: linear-gradient(135deg, #f3e8ff, #e9d5ff);
}
.node-icon {
font-size: 2rem;
}
.node-label {
font-weight: 700;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.node-detail {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.node-actions {
display: flex;
flex-direction: column;
gap: 0.25rem;
margin-top: 0.5rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.action {
display: flex;
align-items: center;
gap: 0.25rem;
}
.action-icon {
font-size: 0.9rem;
}
.flow-arrow {
position: relative;
padding: 0.5rem 0;
}
.arrow-line {
height: 2px;
background: var(--vp-c-divider);
position: relative;
}
.arrow-line::after {
content: '▼';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
color: var(--vp-c-divider);
font-size: 0.75rem;
}
.flow-arrow.encrypted .arrow-line {
background: linear-gradient(90deg, #22c55e, #3b82f6);
height: 3px;
}
.flow-arrow.encrypted .arrow-line::after {
color: #22c55e;
}
.arrow-label {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: var(--vp-c-bg);
padding: 0 0.5rem;
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-1);
display: flex;
align-items: center;
gap: 0.25rem;
white-space: nowrap;
}
.lock-icon {
color: #22c55e;
}
.unlock-icon {
color: #f59e0b;
}
.cert-info,
.headers-info {
position: absolute;
top: 15px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem;
font-size: 0.7rem;
font-family: monospace;
color: var(--vp-c-text-2);
white-space: nowrap;
z-index: 10;
}
.cert-info {
left: 0;
}
.headers-info {
right: 0;
}
.cert-item,
.header-item {
margin: 0.15rem 0;
}
.cert-label {
color: var(--vp-c-brand);
font-weight: 600;
}
.cert-management {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.section-title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1rem;
text-align: center;
color: var(--vp-c-text-1);
}
.cert-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 0.5rem;
}
.cert-tab {
padding: 0.5rem 1rem;
background: transparent;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.cert-tab:hover {
color: var(--vp-c-text-1);
background: var(--vp-c-bg-soft);
}
.cert-tab.active {
color: var(--vp-c-brand);
background: rgba(var(--vp-c-brand-rgb), 0.1);
}
.cert-content {
min-height: 200px;
}
.apply-flow {
padding: 1rem 0;
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 1rem;
}
.cert-step {
display: flex;
gap: 1rem;
align-items: flex-start;
background: var(--vp-c-bg-soft);
padding: 1rem;
border-radius: 8px;
border-left: 3px solid var(--vp-c-brand);
}
.step-badge {
width: 28px;
height: 28px;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.85rem;
flex-shrink: 0;
}
.step-content {
flex: 1;
}
.step-title {
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 0.25rem;
color: var(--vp-c-text-1);
}
.step-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.step-command {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
padding: 0.5rem;
font-family: monospace;
font-size: 0.8rem;
color: var(--vp-c-text-1);
}
.nginx-config {
background: var(--vp-c-bg-alt);
border-radius: 8px;
padding: 1rem;
overflow-x: auto;
}
.config-block {
margin: 0;
font-family: monospace;
font-size: 0.8rem;
line-height: 1.6;
color: var(--vp-c-text-1);
white-space: pre;
}
.best-practices {
padding: 1rem 0;
}
.practices-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}
.practice-card {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
}
.practice-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.practice-icon {
font-size: 1.25rem;
}
.practice-title {
font-weight: 700;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.practice-content {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.5;
margin-bottom: 0.5rem;
}
.practice-code {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
padding: 0.5rem;
font-family: monospace;
font-size: 0.75rem;
color: var(--vp-c-text-1);
}
.benefits-section {
background: var(--vp-c-bg);
border-radius: 12px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.benefits-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.benefit-card {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1.25rem;
text-align: center;
transition: all 0.3s;
}
.benefit-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
}
.benefit-icon {
font-size: 2.5rem;
margin-bottom: 0.75rem;
}
.benefit-title {
font-weight: 700;
font-size: 1rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.benefit-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
@media (max-width: 768px) {
.strategy-tabs {
grid-template-columns: 1fr;
}
.cert-tabs {
flex-direction: column;
gap: 0.25rem;
}
.pool-header {
flex-direction: column;
align-items: flex-start;
}
.servers-grid {
grid-template-columns: 1fr;
}
.flow-node {
padding: 0.75rem;
}
.cert-info,
.headers-info {
position: static;
margin-top: 0.5rem;
}
}
</style>