4. RESTful API设计与客户端调用
REST 不是某个具体框架或库,而是一组围绕“资源”组织网络接口的架构约束。理解这些约束,能帮助我们在设计接口时保持路径清晰、语义一致,也能让 React Native 客户端在调用时更容易判断请求是否成功、失败原因是什么、下一步该做什么。本节从 REST 原则出发,落到资源路径设计、HTTP 状态码语义,再完整走一遍在 React Native 中调用 REST 接口并渲染列表的实现过程。
REST 的核心原则与资源路径设计
REST(Representational State Transfer,表述性状态转移)强调用统一接口操作资源。落到 HTTP API 上,最关键的几条约束是:客户端与服务器分离、无状态、可缓存、统一接口、分层系统。其中对日常开发影响最大的是“无状态”和“统一接口”。无状态意味着每个请求都应携带服务器完成处理所需的全部信息,服务器不在两次请求之间保存客户端会话上下文;统一接口则意味着用同一套方法语义操作不同资源,而不是为每个动作发明一个新路径。
资源路径设计应遵循“名词表示资源、HTTP 方法表示动作”的原则。例如文章资源用 /articles 表示集合,用 /articles/42 表示单个资源。获取列表用 GET /articles,获取单篇用 GET /articles/42,创建用 POST /articles,整体更新用 PUT /articles/42,局部更新用 PATCH /articles/42,删除用 DELETE /articles/42。路径中应避免出现动词,如 /getArticleList 或 /deleteArticleById,这类写法把动作塞进路径,破坏了统一接口,也让缓存和权限控制更难统一处理。
资源之间有关联时,用嵌套路径表达从属关系,例如 /articles/42/comments 表示文章 42 的评论集合。但嵌套不宜过深,通常不超过两层;如果评论本身可以独立寻址,也可以直接暴露 /comments?articleId=42。过滤、排序、分页属于对集合的“视图”控制,应放在查询参数中,例如 /articles?page=1&pageSize=20&sort=-createdAt,其中 -createdAt 表示按创建时间倒序。这样路径保持稳定,查询条件可组合、可缓存。
HTTP 状态码语义与客户端判断
状态码是服务器对请求结果的标准化表达,客户端应依据状态码而不是响应体中的自定义字段来判断请求结果。常见语义如下:200 OK 表示请求成功并返回内容;201 Created 表示资源创建成功,通常配合 Location 响应头指向新资源;204 No Content 表示成功但无响应体,常用于删除或更新;400 Bad Request 表示请求参数或格式有误;401 Unauthorized 表示未认证;403 Forbidden 表示已认证但无权限;404 Not Found 表示资源不存在;409 Conflict 表示与当前资源状态冲突,如重复创建;422 Unprocessable Entity 表示语义校验失败;500 Internal Server Error 表示服务器内部错误。
在客户端,fetch 只有在网络层失败时才会 reject,HTTP 4xx 和 5xx 并不会自动进入 catch。因此必须显式检查 response.ok 或 response.status。一个稳健的调用流程是:先判断网络是否可达,再判断状态码区间,最后解析响应体。2xx 视为成功,4xx 视为客户端可修正的问题,5xx 视为服务端问题并适合重试或提示稍后再试。把状态码语义映射为客户端可执行的分支,是 REST 调用中非常关键的一步。
在 React Native 中调用 REST 接口
React Native 内置 fetch,无需额外安装网络库即可发起 HTTP 请求。下面以文章列表接口 GET /articles?page=1&pageSize=20 为例,展示一个包含状态码判断、错误分类和超时控制的调用封装。示例中的接口地址、返回数据和状态码均为教学假设,实际项目应替换为真实服务端地址与字段。
const BASE_URL = 'https://api.example.com';
async function fetchArticles(page = 1, pageSize = 20) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 8000);
try {
const res = await fetch(
`${BASE_URL}/articles?page=${page}&pageSize=${pageSize}`,
{
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer demo-token',
},
signal: controller.signal,
}
);
if (res.status === 204) return { items: [], total: 0 };
if (!res.ok) {
if (res.status === 401) throw new Error('UNAUTHORIZED');
if (res.status === 404) throw new Error('NOT_FOUND');
if (res.status >= 500) throw new Error('SERVER_ERROR');
throw new Error(`HTTP_${res.status}`);
}
const json = await res.json();
return { items: json.data ?? [], total: json.total ?? 0 };
} finally {
clearTimeout(timer);
}
}
这段代码体现了几个关键点:用 AbortController 设置 8 秒超时,避免请求悬挂;先处理 204 无响应体的情况,再判断 res.ok;把 401、404、5xx 映射为不同的错误标识,便于 UI 层分别处理;解析 JSON 时用空值合并运算符兜底,防止字段缺失导致渲染崩溃。
渲染列表与状态管理
拿到数据后,用 FlatList 渲染列表。列表组件需要处理四种状态:加载中、加载成功、空列表、加载失败。下面是一个完整的函数组件示例,包含下拉刷新和分页加载。
import React, { useEffect, useState, useCallback } from 'react';
import { View, Text, FlatList, ActivityIndicator, RefreshControl } from 'react-native';
export default function ArticleList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState(null);
const load = useCallback(async (targetPage, isRefresh = false) => {
if (isRefresh) setRefreshing(true);
else setLoading(true);
setError(null);
try {
const { items: list } = await fetchArticles(targetPage, 20);
setItems(prev => (isRefresh || targetPage === 1 ? list : [...prev, ...list]));
setPage(targetPage);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
useEffect(() => { load(1); }, [load]);
if (loading && items.length === 0) {
return <ActivityIndicator size="large" style={{ marginTop: 40 }} />;
}
if (error && items.length === 0) {
return <Text style={{ textAlign: 'center', marginTop: 40 }}>加载失败:{error}</Text>;
}
return (
<FlatList
data={items}
keyExtractor={(item) => String(item.id)}
renderItem={({ item }) => (
<View style={{ padding: 16, borderBottomWidth: 1, borderColor: '#eee' }}>
<Text style={{ fontSize: 16, fontWeight: '600' }}>{item.title}</Text>
<Text style={{ color: '#666', marginTop: 4 }}>{item.summary}</Text>
</View>
)}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={() => load(1, true)} />
}
onEndReachedThreshold={0.3}
onEndReached={() => { if (!loading) load(page + 1); }}
ListEmptyComponent={<Text style={{ textAlign: 'center', marginTop: 40 }}>暂无文章</Text>}
/>
);
}
这段实现中,keyExtractor 使用服务端返回的稳定 id,避免用数组下标导致列表项复用错乱;onEndReached 触发下一页加载,并用 loading 防止重复请求;RefreshControl 实现下拉刷新,刷新时把页码重置为 1 并替换列表;ListEmptyComponent 处理空数据状态。错误信息通过 error 状态展示,若已有数据则保留旧列表,避免一次失败清空整个页面。
code: 0 或 success: true 替代 HTTP 状态码作为唯一判断依据。状态码是 HTTP 协议层的标准语义,代理、网关、缓存和监控系统都依赖它;自定义业务码可以作为补充,但不应取代状态码。另外,fetch 不会因 4xx/5xx 自动抛错,忘记检查 res.ok 会把错误响应当成正常数据解析,导致难以定位的渲染异常。把以上环节串起来:设计阶段用名词路径和 HTTP 方法表达资源与动作,用查询参数表达过滤分页;服务端用状态码准确表达结果;客户端用 fetch 发起请求,显式判断状态码并映射为可处理的错误类型;最后用 FlatList 渲染列表,覆盖加载、成功、空、失败四种状态。这样一条链路既符合 REST 约束,也能让 React Native 页面在真实网络环境下保持稳定和可维护。