pin_drop当前位置:知识文库 ❯ 图文
后端通信:RESTful API - 从入门到实践详解
教程简介
REST(Representational State Transfer)是当今最流行的Web API设计架构风格,几乎所有的现代Web应用都采用RESTful API进行前后端通信。理解REST的设计原则、资源与URI设计、HTTP方法语义、状态码选择、分页排序过滤、版本管理等知识,不仅对前端开发者调用API至关重要,也是全栈开发的核心技能。
本教程将全面讲解REST架构原则、资源与URI设计、HTTP方法语义映射、状态码选择、分页排序过滤、版本管理、HATEOAS以及RESTful最佳实践。
核心概念
1. REST架构原则
REST由Roy Fielding在2000年提出,包含六个核心约束:
统一接口是REST最重要的约束,包含四个子约束:
资源标识:通过URI标识资源
通过表示操作资源:通过资源的表示(JSON/XML)操作资源
自描述消息:每条消息包含足够的信息来描述如何处理
超媒体作为应用状态引擎(HATEOAS):响应包含相关操作的链接
2. 资源与URI设计
REST的核心概念是资源(Resource),URI是资源的唯一标识。
URI设计原则:
代码示例
// 好的设计 - 名词表示资源
GET /api/users // 用户集合
GET /api/users/1 // 单个用户
GET /api/users/1/orders // 用户的订单
GET /api/orders/101/items // 订单中的商品
// 不好的设计 - 动词表示操作
GET /api/getUsers
POST /api/createUser
PUT /api/updateUser/1
DELETE /api/deleteUser/1URI设计规范:
3. HTTP方法语义映射
非CRUD操作的处理:
代码示例
// 方式1:将操作视为资源
POST /users/1/activation // 激活用户
POST /orders/101/cancellation // 取消订单
// 方式2:使用子资源
POST /users/1/password-reset // 重置密码
POST /files/5/compression // 压缩文件
// 方式3:使用查询参数
POST /emails?action=send // 发送邮件4. 状态码选择
5. 分页、排序、过滤
代码示例
// 分页
GET /api/users?page=2&size=20
// 排序
GET /api/users?sort=created_at&order=desc
// 过滤
GET /api/users?role=admin&status=active
// 搜索
GET /api/users?q=张三
// 字段选择
GET /api/users?fields=id,name,email
// 组合使用
GET /api/users?role=admin&sort=created_at&order=desc&page=1&size=20分页响应格式:
代码示例
{
"data": [
{ "id": 1, "name": "张三" },
{ "id": 2, "name": "李四" }
],
"pagination": {
"page": 1,
"size": 20,
"total": 156,
"totalPages": 8
},
"links": {
"self": "/api/users?page=1&size=20",
"next": "/api/users?page=2&size=20",
"prev": null,
"first": "/api/users?page=1&size=20",
"last": "/api/users?page=8&size=20"
}
}6. 版本管理
代码示例
// 方式1:URL路径(最常用)
GET /api/v1/users
GET /api/v2/users
// 方式2:请求头
GET /api/users
Accept: application/vnd.myapi.v2+json
// 方式3:查询参数
GET /api/users?version=2URL路径版本化的优缺点:
7. HATEOAS
HATEOAS(Hypermedia as the Engine of Application State)要求API响应包含相关操作的链接:
代码示例
{
"id": 1,
"name": "张三",
"email": "zhangsan@example.com",
"_links": {
"self": { "href": "/api/v1/users/1" },
"orders": { "href": "/api/v1/users/1/orders" },
"update": { "href": "/api/v1/users/1", "method": "PUT" },
"delete": { "href": "/api/v1/users/1", "method": "DELETE" }
}
}8. RESTful最佳实践
请求格式:
代码示例
// POST /api/users - 创建用户
{
"name": "张三",
"email": "zhangsan@example.com",
"age": 25
}响应格式:
代码示例
// 成功响应
{
"code": 0,
"message": "success",
"data": {
"id": 1,
"name": "张三",
"email": "zhangsan@example.com",
"created_at": "2026-04-22T10:30:00Z"
}
}
// 错误响应
{
"code": 40001,
"message": "参数验证失败",
"errors": [
{ "field": "email", "message": "邮箱格式不正确" },
{ "field": "age", "message": "年龄必须大于0" }
]
}代码示例
示例1:RESTful API设计器
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RESTful API设计器</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
.container { max-width: 1000px; margin: 0 auto; }
h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
.card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
.resource-list { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
.resource-btn { padding: 8px 16px; border: 2px solid #e8eaed; border-radius: 8px; background: #fff; cursor: pointer; font-weight: 600; font-size: 13px; transition: all 0.2s; }
.resource-btn.active { background: #1a73e8; color: #fff; border-color: #1a73e8; }
.endpoint-table { width: 100%; border-collapse: collapse; }
.endpoint-table th, .endpoint-table td { padding: 12px 14px; text-align: left; border-bottom: 1px solid #e8eaed; font-size: 14px; }
.endpoint-table th { background: #f8f9fa; font-weight: 600; color: #555; }
.method-badge { display: inline-block; padding: 3px 10px; border-radius: 4px; font-weight: 700; font-size: 12px; color: #fff; }
.method-get { background: #34a853; }
.method-post { background: #ea4335; }
.method-put { background: #1a73e8; }
.method-patch { background: #ff9800; }
.method-delete { background: #9c27b0; }
.uri-text { font-family: 'Consolas', monospace; font-size: 13px; color: #555; }
.try-btn { padding: 4px 12px; background: #1a73e8; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 600; }
.response-area { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 13px; line-height: 1.7; max-height: 300px; overflow-y: auto; white-space: pre-wrap; margin-top: 16px; }
</style>
</head>
<body>
<div class="container">
<h1>RESTful API设计器</h1>
<div class="card">
<h2>选择资源</h2>
<div class="resource-list">
<div class="resource-btn active" onclick="selectResource('users')">Users 用户</div>
<div class="resource-btn" onclick="selectResource('orders')">Orders 订单</div>
<div class="resource-btn" onclick="selectResource('products')">Products 产品</div>
<div class="resource-btn" onclick="selectResource('articles')">Articles 文章</div>
</div>
</div>
<div class="card">
<h2>API端点</h2>
<table class="endpoint-table" id="endpointTable"></table>
</div>
<div class="card">
<h2>模拟响应</h2>
<div class="response-area" id="responseArea">点击端点表格中的"试一试"查看模拟响应</div>
</div>
</div>
<script>
const resources = {
users: {
name: '用户', fields: ['id', 'name', 'email', 'role', 'status', 'created_at'],
endpoints: [
{ method: 'GET', uri: '/api/v1/users', desc: '获取用户列表', status: 200, response: { code: 0, message: 'success', data: [{id:1,name:'张三',email:'zhangsan@example.com',role:'admin',status:'active',created_at:'2026-04-22T10:30:00Z'},{id:2,name:'李四',email:'lisi@example.com',role:'user',status:'active',created_at:'2026-04-21T08:00:00Z'}], pagination: {page:1,size:20,total:156,totalPages:8} }},
{ method: 'GET', uri: '/api/v1/users/1', desc: '获取单个用户', status: 200, response: { code: 0, message: 'success', data: {id:1,name:'张三',email:'zhangsan@example.com',role:'admin',status:'active',created_at:'2026-04-22T10:30:00Z'} }},
{ method: 'POST', uri: '/api/v1/users', desc: '创建用户', status: 201, response: { code: 0, message: '创建成功', data: {id:3,name:'王五',email:'wangwu@example.com',role:'user',status:'active',created_at:'2026-04-22T12:00:00Z'} }},
{ method: 'PUT', uri: '/api/v1/users/1', desc: '替换用户', status: 200, response: { code: 0, message: '更新成功', data: {id:1,name:'张三',email:'new@example.com',role:'admin',status:'active',created_at:'2026-04-22T10:30:00Z',updated_at:'2026-04-22T14:00:00Z'} }},
{ method: 'PATCH', uri: '/api/v1/users/1', desc: '部分更新', status: 200, response: { code: 0, message: '更新成功', data: {id:1,email:'new@example.com'} }},
{ method: 'DELETE', uri: '/api/v1/users/1', desc: '删除用户', status: 204, response: null },
{ method: 'GET', uri: '/api/v1/users?role=admin&status=active', desc: '过滤查询', status: 200, response: { code: 0, message: 'success', data: [{id:1,name:'张三',role:'admin',status:'active'}] }},
{ method: 'GET', uri: '/api/v1/users?sort=created_at&order=desc&page=1&size=10', desc: '分页排序', status: 200, response: { code: 0, data: [], pagination: {page:1,size:10,total:156,totalPages:16} }}
]
},
orders: {
name: '订单', fields: ['id', 'user_id', 'total', 'status', 'items', 'created_at'],
endpoints: [
{ method: 'GET', uri: '/api/v1/orders', desc: '获取订单列表', status: 200, response: { code: 0, data: [{id:101,user_id:1,total:299.9,status:'paid',created_at:'2026-04-22T10:00:00Z'}] }},
{ method: 'GET', uri: '/api/v1/orders/101', desc: '获取订单详情', status: 200, response: { code: 0, data: {id:101,user_id:1,total:299.9,status:'paid',items:[{product_id:1,quantity:2,price:149.95}],created_at:'2026-04-22T10:00:00Z'} }},
{ method: 'POST', uri: '/api/v1/orders', desc: '创建订单', status: 201, response: { code: 0, message: '创建成功', data: {id:102,user_id:1,total:599.8,status:'pending'} }},
{ method: 'POST', uri: '/api/v1/orders/101/cancellation', desc: '取消订单', status: 200, response: { code: 0, message: '订单已取消', data: {id:101,status:'cancelled'} }},
{ method: 'GET', uri: '/api/v1/users/1/orders', desc: '获取用户的订单', status: 200, response: { code: 0, data: [{id:101,total:299.9,status:'paid'}] }}
]
},
products: {
name: '产品', fields: ['id', 'name', 'price', 'category', 'stock'],
endpoints: [
{ method: 'GET', uri: '/api/v1/products', desc: '获取产品列表', status: 200, response: { code: 0, data: [{id:1,name:'商品A',price:99.9,category:'电子',stock:100}] }},
{ method: 'GET', uri: '/api/v1/products/1', desc: '获取产品详情', status: 200, response: { code: 0, data: {id:1,name:'商品A',price:99.9,category:'电子',stock:100,description:'优质商品'} }},
{ method: 'POST', uri: '/api/v1/products', desc: '创建产品', status: 201, response: { code: 0, data: {id:2,name:'商品B',price:199.9} }},
{ method: 'PATCH', uri: '/api/v1/products/1/stock', desc: '更新库存', status: 200, response: { code: 0, data: {id:1,stock:95} }}
]
},
articles: {
name: '文章', fields: ['id', 'title', 'content', 'author', 'tags', 'published_at'],
endpoints: [
{ method: 'GET', uri: '/api/v1/articles', desc: '获取文章列表', status: 200, response: { code: 0, data: [{id:1,title:'RESTful入门',author:'张三',published_at:'2026-04-22'}] }},
{ method: 'GET', uri: '/api/v1/articles/1', desc: '获取文章详情', status: 200, response: { code: 0, data: {id:1,title:'RESTful入门',content:'...',author:'张三',tags:['REST','API']} }},
{ method: 'POST', uri: '/api/v1/articles', desc: '创建文章', status: 201, response: { code: 0, data: {id:2,title:'新文章'} }},
{ method: 'POST', uri: '/api/v1/articles/1/publish', desc: '发布文章', status: 200, response: { code: 0, data: {id:1,status:'published',published_at:'2026-04-22T12:00:00Z'} }},
{ method: 'GET', uri: '/api/v1/articles?q=REST', desc: '搜索文章', status: 200, response: { code: 0, data: [{id:1,title:'RESTful入门'}] }}
]
}
};
let currentResource = 'users';
function selectResource(name) {
currentResource = name;
document.querySelectorAll('.resource-btn').forEach(b => b.classList.remove('active'));
event.currentTarget.classList.add('active');
renderEndpoints();
}
function renderEndpoints() {
const resource = resources[currentResource];
document.getElementById('endpointTable').innerHTML = `
<thead><tr><th>方法</th><th>URI</th><th>说明</th><th>状态码</th><th>操作</th></tr></thead>
<tbody>
${resource.endpoints.map((ep, i) => `
<tr>
<td><span class="method-badge method-${ep.method.toLowerCase()}">${ep.method}</span></td>
<td class="uri-text">${ep.uri}</td>
<td>${ep.desc}</td>
<td>${ep.status}</td>
<td><button class="try-btn" onclick="tryEndpoint('${currentResource}', ${i})">试一试</button></td>
</tr>
`).join('')}
</tbody>
`;
}
function tryEndpoint(resource, index) {
const ep = resources[resource].endpoints[index];
let output = `${ep.method} ${ep.uri}\nHTTP/1.1 ${ep.status}\nContent-Type: application/json\n\n`;
if (ep.response) {
output += JSON.stringify(ep.response, null, 2);
} else {
output += '(无响应体)';
}
document.getElementById('responseArea').textContent = output;
}
renderEndpoints();
</script>
</body>
</html>示例2:RESTful客户端模拟
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RESTful客户端模拟</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
.container { max-width: 1000px; margin: 0 auto; }
h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
.main-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.card { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
.full-width { grid-column: 1 / -1; }
.crud-bar { display: flex; gap: 8px; margin-bottom: 16px; }
.crud-btn { padding: 8px 16px; border: none; border-radius: 6px; color: #fff; font-weight: 700; font-size: 13px; cursor: pointer; }
.btn-get { background: #34a853; }
.btn-post { background: #ea4335; }
.btn-put { background: #1a73e8; }
.btn-delete { background: #9c27b0; }
.user-table { width: 100%; border-collapse: collapse; }
.user-table th, .user-table td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #e8eaed; font-size: 13px; }
.user-table th { background: #f8f9fa; font-weight: 600; }
.user-table tr:hover { background: #f8f9fa; }
.action-btn { padding: 4px 8px; border: none; border-radius: 4px; font-size: 11px; cursor: pointer; color: #fff; font-weight: 600; }
.form-group { margin-bottom: 10px; }
.form-group label { display: block; font-size: 13px; font-weight: 600; color: #555; margin-bottom: 4px; }
.form-group input { width: 100%; padding: 8px 12px; border: 1px solid #dadce0; border-radius: 6px; font-size: 14px; }
.log-area { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 12px; line-height: 1.8; max-height: 300px; overflow-y: auto; white-space: pre-wrap; }
.log-method { font-weight: 700; }
.log-get { color: #4caf50; } .log-post { color: #f44336; } .log-put { color: #2196f3; } .log-delete { color: #9c27b0; }
.log-uri { color: #ffd54f; } .log-status { color: #81c784; }
</style>
</head>
<body>
<div class="container">
<h1>RESTful客户端模拟</h1>
<div class="main-grid">
<div class="card">
<h2>用户资源</h2>
<div class="crud-bar">
<button class="crud-btn btn-get" onclick="listUsers()">GET /users</button>
<button class="crud-btn btn-post" onclick="showCreate()">POST /users</button>
</div>
<table class="user-table">
<thead><tr><th>ID</th><th>姓名</th><th>邮箱</th><th>操作</th></tr></thead>
<tbody id="userTableBody"><tr><td colspan="4" style="text-align:center;color:#999;">点击GET加载</td></tr></tbody>
</table>
</div>
<div class="card">
<h2>操作表单</h2>
<div id="formArea"><p style="color:#888;text-align:center;padding:30px;">选择操作</p></div>
</div>
<div class="card full-width">
<h2>请求日志</h2>
<div class="log-area" id="logArea">等待操作...</div>
</div>
</div>
</div>
<script>
let users = [
{ id: 1, name: '张三', email: 'zhangsan@example.com', role: 'admin' },
{ id: 2, name: '李四', email: 'lisi@example.com', role: 'user' },
{ id: 3, name: '王五', email: 'wangwu@example.com', role: 'user' }
];
let nextId = 4;
function logRequest(method, uri, status, data) {
const logArea = document.getElementById('logArea');
const cls = method.toLowerCase();
logArea.innerHTML = `<span class="log-method log-${cls}">${method}</span> <span class="log-uri">${uri}</span>\n → <span class="log-status">${status}</span> ${JSON.stringify(data).substring(0, 120)}\n\n` + logArea.innerHTML;
}
function listUsers() {
logRequest('GET', '/api/v1/users', '200 OK', users);
renderTable();
}
function renderTable() {
document.getElementById('userTableBody').innerHTML = users.map(u => `
<tr>
<td>${u.id}</td><td>${u.name}</td><td>${u.email}</td>
<td>
<button class="action-btn" style="background:#1a73e8" onclick="showEdit(${u.id})">PUT</button>
<button class="action-btn" style="background:#9c27b0" onclick="deleteUser(${u.id})">DELETE</button>
</td>
</tr>
`).join('');
}
function showCreate() {
document.getElementById('formArea').innerHTML = `
<h3 style="color:#ea4335;margin-bottom:12px;">POST /api/v1/users</h3>
<div class="form-group"><label>姓名</label><input id="f-name" placeholder="姓名"></div>
<div class="form-group"><label>邮箱</label><input id="f-email" placeholder="邮箱"></div>
<div class="form-group"><label>角色</label><input id="f-role" value="user"></div>
<button class="crud-btn btn-post" style="width:100%;margin-top:8px;" onclick="createUser()">POST 创建</button>
`;
}
function createUser() {
const name = document.getElementById('f-name').value;
const email = document.getElementById('f-email').value;
const role = document.getElementById('f-role').value || 'user';
if (!name || !email) return alert('请填写姓名和邮箱');
const user = { id: nextId++, name, email, role };
users.push(user);
logRequest('POST', '/api/v1/users', '201 Created', user);
renderTable();
}
function showEdit(id) {
const user = users.find(u => u.id === id);
if (!user) return;
document.getElementById('formArea').innerHTML = `
<h3 style="color:#1a73e8;margin-bottom:12px;">PUT /api/v1/users/${id}</h3>
<div class="form-group"><label>姓名</label><input id="f-name" value="${user.name}"></div>
<div class="form-group"><label>邮箱</label><input id="f-email" value="${user.email}"></div>
<div class="form-group"><label>角色</label><input id="f-role" value="${user.role}"></div>
<button class="crud-btn btn-put" style="width:100%;margin-top:8px;" onclick="updateUser(${id})">PUT 更新</button>
`;
}
function updateUser(id) {
const name = document.getElementById('f-name').value;
const email = document.getElementById('f-email').value;
const role = document.getElementById('f-role').value;
const index = users.findIndex(u => u.id === id);
if (index === -1) return;
users[index] = { id, name, email, role };
logRequest('PUT', `/api/v1/users/${id}`, '200 OK', users[index]);
renderTable();
}
function deleteUser(id) {
const deleted = users.find(u => u.id === id);
users = users.filter(u => u.id !== id);
logRequest('DELETE', `/api/v1/users/${id}`, '204 No Content', deleted);
renderTable();
}
</script>
</body>
</html>示例3:API设计规范检查器
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API设计规范检查器</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0f2f5; padding: 20px; color: #333; }
.container { max-width: 900px; margin: 0 auto; }
h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
.card { background: #fff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.card h2 { font-size: 18px; color: #1a73e8; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #e8eaed; }
textarea { width: 100%; padding: 14px; border: 1px solid #dadce0; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 13px; line-height: 1.6; resize: vertical; min-height: 150px; }
textarea:focus { outline: none; border-color: #1a73e8; }
button { padding: 12px 28px; background: #1a73e8; color: #fff; border: none; border-radius: 8px; font-weight: 700; cursor: pointer; font-size: 15px; margin-top: 12px; }
button:hover { background: #1557b0; }
.check-list { list-style: none; }
.check-item { padding: 12px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: flex-start; gap: 10px; }
.check-icon { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; flex-shrink: 0; margin-top: 2px; }
.icon-pass { background: #e8f5e9; color: #2e7d32; }
.icon-fail { background: #ffebee; color: #c62828; }
.icon-warn { background: #fff3e0; color: #ef6c00; }
.check-info { flex: 1; }
.check-name { font-weight: 600; font-size: 14px; }
.check-detail { font-size: 13px; color: #666; margin-top: 2px; }
.score-bar { height: 8px; background: #e8eaed; border-radius: 4px; overflow: hidden; margin: 16px 0; }
.score-fill { height: 100%; border-radius: 4px; transition: width 0.5s; }
</style>
</head>
<body>
<div class="container">
<h1>API设计规范检查器</h1>
<div class="card">
<h2>输入API端点列表</h2>
<textarea id="apiInput" placeholder="每行一个API端点,格式:METHOD URI
例如:
GET /api/v1/users
POST /api/v1/users
GET /api/v1/users/1
PUT /api/v1/users/1
DELETE /api/v1/users/1">GET /api/v1/users
POST /api/v1/users
GET /api/v1/users/1
PUT /api/v1/users/1
DELETE /api/v1/users/1
GET /api/v1/users?role=admin
POST /api/v1/users/1/activation</textarea>
<button onclick="checkAPI()">检查规范</button>
</div>
<div class="card" id="resultCard" style="display:none;">
<h2>检查结果</h2>
<div class="score-bar"><div class="score-fill" id="scoreFill"></div></div>
<ul class="check-list" id="checkList"></ul>
</div>
</div>
<script>
function checkAPI() {
const input = document.getElementById('apiInput').value.trim();
if (!input) return;
const lines = input.split('\n').filter(l => l.trim());
const endpoints = lines.map(l => {
const parts = l.trim().split(/\s+/);
return { method: parts[0], uri: parts.slice(1).join(' ') };
});
const checks = [];
// 检查1:URI使用名词
const verbPatterns = /\/(get|create|update|delete|list|find|search|add|remove|edit)\b/i;
endpoints.forEach(ep => {
if (verbPatterns.test(ep.uri)) {
checks.push({ pass: false, name: 'URI使用动词', detail: `${ep.method} ${ep.uri} - URI应使用名词而非动词,操作语义由HTTP方法表达` });
}
});
if (!endpoints.some(ep => verbPatterns.test(ep.uri))) {
checks.push({ pass: true, name: 'URI使用名词', detail: '所有URI使用名词表示资源,操作由HTTP方法表达' });
}
// 检查2:URI使用复数
const singularPattern = /\/user(?!s)|\/order(?!s)|\/product(?!s)|\/article(?!s)/;
endpoints.forEach(ep => {
if (singularPattern.test(ep.uri) && !ep.uri.match(/\/\d/)) {
checks.push({ pass: false, name: 'URI使用单数', detail: `${ep.method} ${ep.uri} - 集合资源应使用复数形式(如/users而非/user)` });
}
});
if (!endpoints.some(ep => singularPattern.test(ep.uri) && !ep.uri.match(/\/\d/))) {
checks.push({ pass: true, name: 'URI使用复数', detail: '集合资源URI使用复数形式' });
}
// 检查3:使用小写和连字符
const upperPattern = /[A-Z]|_/;
endpoints.forEach(ep => {
if (upperPattern.test(ep.uri)) {
checks.push({ pass: false, name: 'URI格式不规范', detail: `${ep.method} ${ep.uri} - URI应使用小写字母和连字符` });
}
});
if (!endpoints.some(ep => upperPattern.test(ep.uri))) {
checks.push({ pass: true, name: 'URI格式规范', detail: 'URI使用小写字母和连字符' });
}
// 检查4:版本号
const hasVersion = endpoints.some(ep => /\/v\d+/.test(ep.uri));
checks.push({ pass: hasVersion, name: 'API版本管理', detail: hasVersion ? 'URI中包含版本号' : '建议在URI中添加版本号(如/api/v1/users)' });
// 检查5:HTTP方法与操作匹配
const methods = new Set(endpoints.map(ep => ep.method));
checks.push({ pass: methods.has('GET'), name: 'GET方法', detail: methods.has('GET') ? '使用GET获取资源' : '缺少GET方法获取资源' });
checks.push({ pass: methods.has('POST'), name: 'POST方法', detail: methods.has('POST') ? '使用POST创建资源' : '缺少POST方法创建资源' });
// 检查6:嵌套层级
endpoints.forEach(ep => {
const depth = (ep.uri.match(/\//g) || []).length;
if (depth > 4) {
checks.push({ pass: false, name: 'URI嵌套过深', detail: `${ep.method} ${ep.uri} - 嵌套层级不超过3层` });
}
});
// 检查7:查询参数用于过滤
const hasQuery = endpoints.some(ep => ep.uri.includes('?'));
checks.push({ pass: true, name: '查询参数', detail: hasQuery ? '使用查询参数进行过滤和分页' : '建议使用查询参数进行过滤、排序和分页', warn: !hasQuery });
// 渲染结果
const passCount = checks.filter(c => c.pass).length;
const score = Math.round((passCount / checks.length) * 100);
document.getElementById('scoreFill').style.width = score + '%';
document.getElementById('scoreFill').style.background = score >= 80 ? '#34a853' : score >= 60 ? '#ff9800' : '#ea4335';
document.getElementById('checkList').innerHTML = checks.map(c => `
<li class="check-item">
<div class="check-icon ${c.pass ? 'icon-pass' : c.warn ? 'icon-warn' : 'icon-fail'}">${c.pass ? '' : c.warn ? '!' : ''}</div>
<div class="check-info">
<div class="check-name">${c.name}</div>
<div class="check-detail">${c.detail}</div>
</div>
</li>
`).join('');
document.getElementById('resultCard').style.display = 'block';
}
</script>
</body>
</html>浏览器兼容性
注意事项与最佳实践
1. URI设计
使用名词复数表示资源集合
使用小写字母和连字符
避免在URI中使用动词
嵌套层级不超过3层
使用查询参数进行过滤和分页
2. HTTP方法
严格遵循方法语义:GET获取、POST创建、PUT替换、PATCH更新、DELETE删除
GET和DELETE不应有请求体
PUT需要提供完整资源数据
PATCH只提供修改的字段
3. 状态码
使用最具体的状态码
201表示创建成功,204表示删除成功
400表示语法错误,422表示验证错误
401和403不要混淆
4. 版本管理
推荐URL路径版本化(/api/v1/)
版本变更时保持向后兼容
提供版本迁移指南
5. 错误处理
返回统一的错误格式
包含错误码、错误消息和详细信息
不要暴露服务器内部信息
代码规范示例
RESTful API客户端
代码示例
class RestClient {
constructor(baseURL, version = 'v1') {
this.baseURL = `${baseURL}/api/${version}`;
}
async request(method, path, options = {}) {
const url = `${this.baseURL}${path}`;
const config = {
method,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
...options.headers
}
};
if (options.params) {
const query = new URLSearchParams(options.params);
url += `?${query}`;
}
if (options.body) {
config.body = JSON.stringify(options.body);
}
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new ApiError(response.status, error);
}
if (response.status === 204) return null;
const result = await response.json();
return result.data !== undefined ? result.data : result;
}
list(resource, params) { return this.request('GET', `/${resource}`, { params }); }
get(resource, id) { return this.request('GET', `/${resource}/${id}`); }
create(resource, data) { return this.request('POST', `/${resource}`, { body: data }); }
update(resource, id, data) { return this.request('PUT', `/${resource}/${id}`, { body: data }); }
patch(resource, id, data) { return this.request('PATCH', `/${resource}/${id}`, { body: data }); }
delete(resource, id) { return this.request('DELETE', `/${resource}/${id}`); }
}
// 使用
const api = new RestClient('https://example.com');
const users = await api.list('users', { page: 1, size: 20 });
const user = await api.get('users', 1);
await api.create('users', { name: '张三' });
await api.update('users', 1, { name: '张三', email: 'new@example.com' });
await api.delete('users', 1);常见问题与解决方案
问题1:非CRUD操作如何设计
解决方案:将操作视为子资源:
代码示例
POST /users/1/activation // 激活
POST /orders/101/cancellation // 取消
POST /articles/1/publish // 发布问题2:批量操作如何设计
解决方案:
代码示例
// 批量删除
DELETE /users
Body: { "ids": [1, 2, 3] }
// 批量更新
PATCH /users
Body: { "updates": [{"id": 1, "role": "admin"}, {"id": 2, "role": "user"}] }问题3:关联资源的URI设计
解决方案:
代码示例
// 一对多关系
GET /users/1/orders // 用户的订单
GET /orders/101/items // 订单的商品
// 避免深层嵌套
// 不好:/users/1/orders/101/items/5
// 好:/order-items/5?order_id=101&user_id=1问题4:API响应格式不统一
解决方案:定义统一的响应格式规范:
代码示例
{
"code": 0,
"message": "success",
"data": {},
"pagination": {},
"links": {}
}总结
RESTful API是现代Web应用的标准接口设计方式:
REST原则:客户端-服务器、无状态、可缓存、统一接口、分层系统
URI设计:名词复数、小写连字符、层级表示关系、查询参数过滤
HTTP方法:GET获取、POST创建、PUT替换、PATCH更新、DELETE删除
状态码:200成功、201创建、204无内容、400参数错误、401未认证、403无权限、404未找到
分页排序:page/size/sort/order查询参数,响应包含分页元数据
版本管理:URL路径版本化(/api/v1/)最常用
HATEOAS:响应包含相关操作链接(高级特性)
最佳实践:统一响应格式、规范错误处理、合理设计非CRUD操作
掌握RESTful API的设计原则和使用方法,能够让你更好地设计、实现和消费Web API。
常见问题
非CRUD操作如何设计
请参考教程中的详细说明和代码示例。
批量操作如何设计
请参考教程中的详细说明和代码示例。
什么是RESTful API?
RESTful API是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。
如何学习RESTful API的实际应用?
教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。
RESTful API有哪些注意事项?
常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。
RESTful API适合初学者吗?
本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。
RESTful API的核心要点是什么?
核心要点包括教程简介等内容,建议按教程顺序逐步学习。
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别