API设计实战:从规范到性能优化的完整指南

司马思捷 组件 阅读 677
赞 8 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

最近在重构一个老项目的数据请求层,发现很多组件的 API 调用方式五花八门:有的用原生 fetch,有的封装了 axios,还有的直接在组件里写死 URL。改起来头疼得不行。于是花了两天时间,统一搞了个轻量级但足够灵活的 API 调用方案。亲测有效,现在所有组件都用它,维护成本直降。

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 别混用:我见过有人同时传 paramsbody 给 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 机制,后续会继续分享这类博客。

有更优的实现方式欢迎评论区交流,毕竟前端天天变,谁也不敢说自己方案就是最好的。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论