API设计实战:从规范到性能优化的完整指南
先看效果,再看代码
最近在重构一个老项目的数据请求层,发现很多组件的 API 调用方式五花八门:有的用原生 fetch,有的封装了 axios,还有的直接在组件里写死 URL。改起来头疼得不行。于是花了两天时间,统一搞了个轻量级但足够灵活的 API 调用方案。亲测有效,现在所有组件都用它,维护成本直降。
核心思路就一点:把 API 调用抽象成可配置、可复用、可拦截的函数。不是搞个巨复杂的类,而是用最简单的函数组合,搞定 90% 的场景。
先看最终调用方式,是不是很清爽:
// 获取用户信息
const user = await api.get('/user/123');
// 提交表单
await api.post('/submit', { name: '张三', email: 'xxx@xxx.com' });
// 带查询参数
const list = await api.get('/items', { params: { page: 1, size: 20 } });
看起来像 axios?但其实我自己写的,只有 50 行左右,完全可控。下面贴出核心实现。
核心代码就这几行
别被“API 设计”这个词吓到,其实真没那么复杂。我这个方案基于原生 fetch 封装,加了点糖,但保留了最大灵活性。
// api.js
const BASE_URL = 'https://jztheme.com/api';
const createApi = (baseURL) => {
const request = async (url, options = {}) => {
const config = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
// 如果有 token,这里统一加
// 'Authorization': Bearer ${getToken()}
},
...options
};
// 处理 GET 请求的 query 参数
if (config.params && config.method === 'GET') {
const searchParams = new URLSearchParams(config.params);
url += '?' + searchParams.toString();
delete config.params;
}
// 处理非 GET 请求的 body
if (config.body && typeof config.body === 'object') {
config.body = JSON.stringify(config.body);
}
try {
const res = await fetch(${baseURL}${url}, config);
if (!res.ok) {
throw new Error(HTTP error! status: ${res.status});
}
return await res.json();
} catch (error) {
console.error('API 请求失败:', error);
// 这里可以做统一错误处理,比如弹 toast
throw error;
}
};
return {
get: (url, config = {}) => request(url, { ...config, method: 'GET' }),
post: (url, data, config = {}) => request(url, { ...config, method: 'POST', body: data }),
put: (url, data, config = {}) => request(url, { ...config, method: 'PUT', body: data }),
delete: (url, config = {}) => request(url, { ...config, method: 'DELETE' })
};
};
export const api = createApi(BASE_URL);
就这么点代码,搞定日常所有 CRUD。关键在于:不依赖第三方库,逻辑清晰,扩展方便。你要是用 axios,还得研究它的 interceptors、defaults、cancelToken,一堆概念。而这个,一眼看懂。
这个场景最好用
我们有个动态表单组件,字段完全由后端配置返回。以前每次都要手动拼 URL 和参数,现在直接:
// 动态加载表单 schema
const loadFormSchema = async (formId) => {
return await api.get('/form/schema', { params: { id: formId } });
};
// 提交表单数据
const submitFormData = async (data) => {
return await api.post('/form/submit', data);
};
更爽的是,如果某天后端说所有接口要加个 ?v=2 的版本号,我只需要改一行:const BASE_URL = 'https://jztheme.com/api?v=2'; 全局生效。不用去每个组件里翻找。
另外,配合 TypeScript 的话,还能给 api 函数加上泛型,自动推导返回类型,开发体验直接拉满:
// 如果你用 TS
const user = await api.get<User>('/user/123');
// user 自动是 User 类型,不用再断言
踩坑提醒:这三点一定注意
我折腾这个方案的时候,踩了几个坑,分享出来帮你避雷:
- GET 请求的 body 问题:fetch 规范里,GET 请求理论上不能带 body,虽然部分浏览器允许,但 Node 环境或某些代理会直接报错。所以我的方案里,GET 只走 query 参数,绝不传 body。如果你看到有人写
api.get('/xxx', { body: {...} }),赶紧拦住他。 - 错误处理别只 log:早期我只在 catch 里
console.error,结果用户操作失败了毫无感知。后来加了全局错误提示(比如调用一个 toast 函数),但要注意别在 API 层直接操作 DOM,应该抛出错误,让调用方决定怎么处理。比如登录失败跳转到登录页,列表加载失败显示重试按钮。 - params 和 body 别混用:我见过有人同时传
params和body给 POST 请求,后端一脸懵。明确约定:GET 用 params,POST/PUT 用 body。我的封装里也做了隔离,避免误用。
还有一个小细节:fetch 默认不带 cookie,如果你们用 session 认证,记得加 credentials: 'include'。我一般在 config 里默认加上,除非是跨域且不需要认证的公开接口。
高级技巧:拦截器怎么加?
有人问:“你这没拦截器,怎么加 loading 或刷新 token?” 其实很简单,不用搞复杂,直接在 request 函数里加钩子就行:
const request = async (url, options = {}) => {
// 请求前:比如显示 loading
showLoading();
// 添加 token(伪代码)
if (hasToken()) {
options.headers = {
...options.headers,
'Authorization': Bearer ${getToken()}
};
}
try {
const res = await fetch(...);
// 请求成功
hideLoading();
return res.json();
} catch (error) {
// 请求失败
hideLoading();
if (error.status === 401) {
// 尝试刷新 token
await refreshToken();
// 重新发起原请求
return request(url, options);
}
throw error;
}
};
当然,token 刷新要防重入,不然可能无限循环。我一般是加个锁:
let isRefreshing = false;
let refreshSubscribers = [];
function refreshToken() {
if (isRefreshing) {
// 如果已经在刷新,把后续请求挂起
return new Promise((resolve) => {
refreshSubscribers.push(resolve);
});
}
isRefreshing = true;
// ...实际刷新逻辑
// 刷新成功后,resolve 所有挂起的请求
refreshSubscribers.forEach(cb => cb());
refreshSubscribers = [];
isRefreshing = false;
}
这部分代码我就不全贴了,但思路就是这样:简单、可控、不依赖黑盒。
结尾碎碎念
这个 API 设计方案,不是最优解,但绝对是最省事的。它没有覆盖 100% 的边缘情况(比如上传文件需要 FormData),但日常业务 95% 的场景都能 hold 住。剩下的 5%,单独写个 uploadFile 函数就行,没必要为了那 5% 把整个方案搞复杂。
以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,比如结合 SWR 做缓存,或者加个 retry 机制,后续会继续分享这类博客。
有更优的实现方式欢迎评论区交流,毕竟前端天天变,谁也不敢说自己方案就是最好的。

暂无评论