0eba9e87e9
- Disable formatting rules (handled by Prettier) - Relaxed strict Vue/JS rules for demo code compatibility - Fix syntax errors in ApiPlayground and VoiceCloningDemo - Fix duplicate else-if condition in ApiPlayground - Fix Promise executor async pattern in AutoregressiveAudioDemo - Add TypeScript file support to ESLint config Warnings reduced from 295 to 251 problems. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
383 lines
8.9 KiB
Vue
383 lines
8.9 KiB
Vue
<template>
|
||
<div class="restful-design-demo">
|
||
<div class="header">
|
||
<div class="title">
|
||
RESTful API 设计核心原则
|
||
</div>
|
||
<div class="subtitle">
|
||
REST(Representational 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>
|