Files
test-repo/docs/.vitepress/theme/components/appendix/api-design/RestfulDesignDemo.vue
T

366 lines
8.7 KiB
Vue
Raw Normal View History

<template>
<div class="restful-design-demo">
<div class="header">
<div class="title">RESTful API 设计核心原则</div>
<div class="subtitle">
RESTRepresentational State Transfer是一种架构风格让接口设计像自然资源一样直观
</div>
</div>
<div class="principles-grid">
<div
v-for="principle in principles"
:key="principle.id"
class="principle-card"
:class="{ active: selectedPrinciple === principle.id }"
@click="selectedPrinciple = principle.id"
>
<div class="principle-icon">{{ principle.icon }}</div>
<div class="principle-name">{{ principle.name }}</div>
<div class="principle-brief">{{ principle.brief }}</div>
</div>
</div>
<div class="detail-panel">
<div class="detail-header">
<span class="detail-title">{{ activePrinciple.name }}</span>
<span class="detail-tag">{{ activePrinciple.tag }}</span>
</div>
<div class="detail-content">
<div class="explanation">
<h4>核心概念</h4>
<p>{{ activePrinciple.explanation }}</p>
</div>
<div class="comparison">
<h4>对比示例</h4>
<div class="code-comparison">
<div class="code-block bad">
<div class="code-label">传统方式不推荐</div>
<pre><code>{{ activePrinciple.badExample }}</code></pre>
</div>
<div class="code-block good">
<div class="code-label">RESTful 方式推荐</div>
<pre><code>{{ activePrinciple.goodExample }}</code></pre>
</div>
</div>
</div>
<div class="tips">
<h4>设计要点</h4>
<ul>
<li v-for="(tip, index) in activePrinciple.tips" :key="index">{{ tip }}</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const principles = [
{
id: 'resource',
name: '资源导向',
icon: '📦',
brief: 'URL 表示资源,而非动作',
tag: '核心原则',
explanation: '将系统中的实体抽象为资源(Resource),每个资源对应唯一的 URL。资源是名词,而不是动词或动作。',
badExample: `GET /getUserById?id=123
GET /deleteOrder?orderId=456
POST /createProduct`,
goodExample: `GET /users/123
DELETE /orders/456
POST /products`,
tips: [
'使用名词复数形式(/users 而非 /user',
'避免在 URL 中出现动词(get、create、delete 等)',
'资源层级用路径表示(/users/123/orders',
'资源名使用小写字母,多个单词用连字符(/order-items'
]
},
{
id: 'method',
name: 'HTTP 方法',
icon: '🎯',
brief: '用 HTTP 方法表达操作语义',
tag: '动作表达',
explanation: '使用标准的 HTTP 方法(GET、POST、PUT、DELETE 等)来表示对资源的操作类型,让接口语义更加清晰。',
badExample: `POST /users/query // 查询用户
POST /users/create // 创建用户
POST /users/update // 更新用户
POST /users/delete // 删除用户`,
goodExample: `GET /users // 查询用户列表
POST /users // 创建用户
GET /users/123 // 查询单个用户
PUT /users/123 // 全量更新用户
PATCH /users/123 // 部分更新用户
DELETE /users/123 // 删除用户`,
tips: [
'GET 用于获取资源,是幂等且安全的',
'POST 用于创建资源,返回 201 和新资源 URI',
'PUT 用于全量更新,替换整个资源',
'PATCH 用于部分更新,只修改指定字段',
'DELETE 用于删除资源,返回 204 或 200'
]
},
{
id: 'stateless',
name: '无状态',
icon: '🔄',
brief: '每个请求独立,服务端不保存会话',
tag: '可扩展性',
explanation: '服务端不保存客户端的上下文状态,每个请求都必须包含服务端处理该请求所需的全部信息。这让系统更容易水平扩展。',
badExample: `// 服务端维护会话状态
POST /login
→ 服务端创建 session,返回 session_id cookie
GET /profile
→ 服务端根据 session_id 查找用户
→ 如果会话过期,需要重新登录`,
goodExample: `// 无状态认证
POST /auth/token
→ 验证凭证,返回 JWT token
GET /profile
Authorization: Bearer <token>
→ 服务端验证 token,提取用户信息
→ 请求独立,可随时扩展到多台服务器`,
tips: [
'使用 JWT 或 API Key 进行无状态认证',
'避免在服务端存储会话状态',
'每个请求包含完整的认证信息',
'便于负载均衡和水平扩展',
'使用 Redis 等缓存共享必要的状态数据'
]
},
{
id: 'representation',
name: '统一表现',
icon: '📋',
brief: '使用标准数据格式',
tag: '数据交换',
explanation: '资源的表示(Representation)应该使用标准的数据格式,通常是 JSON。客户端可以通过 Accept 头部请求不同的表示格式。',
badExample: `// 混合格式,字段不一致
GET /users
{
"user_list": [...],
"total_count": 100
}
GET /orders
{
"data": [...],
"pagination": {
"total": 100,
"page": 1
}
}`,
goodExample: `// 统一的响应结构
GET /users
{
"code": 200,
"message": "success",
"data": {
"items": [...],
"pagination": {
"total": 100,
"page": 1,
"page_size": 20
}
},
"timestamp": "2024-01-15T10:30:00Z"
}`,
tips: [
'使用 JSON 作为默认数据格式',
'统一的响应结构(code、message、data',
'支持字段过滤(fields=id,name,email',
'日期使用 ISO 8601 格式',
'字段命名使用 camelCase 或 snake_case,保持一致'
]
}
]
const selectedPrinciple = ref('resource')
const activePrinciple = computed(() =>
principles.find(p => p.id === selectedPrinciple.value) || principles[0]
)
</script>
<style scoped>
.restful-design-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 1.5rem;
margin: 0.5rem 0;
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 800;
font-size: 1.25rem;
color: var(--vp-c-text-1);
}
.subtitle {
margin-top: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.principles-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.principle-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.principle-card:hover {
border-color: rgba(var(--vp-c-brand-rgb), 0.5);
}
.principle-card.active {
border-color: rgba(var(--vp-c-brand-rgb), 0.8);
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.15);
}
.principle-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.principle-name {
font-weight: 700;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.principle-brief {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.detail-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1.25rem;
}
.detail-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-title {
font-weight: 700;
font-size: 1.1rem;
color: var(--vp-c-text-1);
}
.detail-tag {
font-size: 0.75rem;
padding: 0.2rem 0.6rem;
border-radius: 999px;
background: rgba(var(--vp-c-brand-rgb), 0.1);
color: var(--vp-c-brand);
font-weight: 600;
}
.detail-content h4 {
font-weight: 700;
color: var(--vp-c-text-1);
margin: 1rem 0 0.5rem;
}
.explanation p {
color: var(--vp-c-text-2);
line-height: 1.7;
}
.code-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.code-block {
border-radius: 6px;
overflow: hidden;
}
.code-block.bad {
border: 1px solid #ef4444;
}
.code-block.good {
border: 1px solid #22c55e;
}
.code-label {
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
font-weight: 700;
color: white;
}
.code-block.bad .code-label {
background: #ef4444;
}
.code-block.good .code-label {
background: #22c55e;
}
.code-block pre {
margin: 0;
padding: 0.75rem;
background: var(--vp-c-bg-alt);
font-size: 0.8rem;
line-height: 1.5;
overflow-x: auto;
}
.tips ul {
margin: 0;
padding-left: 1.25rem;
color: var(--vp-c-text-2);
}
.tips li {
margin: 0.4rem 0;
line-height: 1.6;
}
@media (max-width: 768px) {
.principles-grid {
grid-template-columns: repeat(2, 1fr);
}
.code-comparison {
grid-template-columns: 1fr;
}
}
</style>