对fetch函数请求的封装

788 阅读4分钟

封装的作用

  1. 简化 API 调用:提供 get、post、put、delete 等便捷方法。

  2. 统一错误处理:集中处理各种 HTTP 状态码。

  3. 自动处理认证:统一添加 token 到请求头。

  4. 请求超时控制:防止请求无限等待。

  5. 灵活的网关配置:支持多个 API 网关。

  6. 智能内容类型处理:自动处理不同类型的请求和响应数据。

1. 认证 Token 管理

let globalCustomToken: string | undefined;

const getAuthToken = () => {
  //开发环境
  if (process.env.NODE_ENV === 'development') {
    return globalCustomToken || '';
  }
  const token = localStorage.getItem('k2_portal_token');
  return token;
};

2. 请求头处理

// 设置请求头token
const setRequestHeaders = (requestOptions: any) => {
  const {headers, data} = requestOptions;
  const contentType = DEFAULT_TYPE;

  const token = getAuthToken();
  const obj = {
    'Content-Type': contentType,
    ...headers,
    Authorization: token || 'noToken'
  }
  // 检查是否有data属性,并且是FormData类型
  if (data instanceof FormData) {
    // FormData类型不需要设置Content-Type,浏览器会自动设置
    if (obj['Content-Type']) {
      delete obj['Content-Type'];
    }
  }

  return obj;
};

3. 响应处理


/**
 * 处理API响应
 * @param response 响应对象
 * @returns 处理后的数据
 */
const handleApiResponse = async (response: Response) => {
  // 获取响应状态
  const { status } = response;

  // 处理非JSON响应
  const contentType = response.headers.get('content-type');
  let responseData;

  if (contentType && contentType.includes(DEFAULT_TYPE)) {
    responseData = await response.json();
  } else {
    // 对于非JSON响应,返回文本内容
    responseData = { text: await response.text() };
  }


  const {body, message, data} = responseData;
  
  // 处理成功状态
  if (status >= 200 && status < 300) {

    return Promise.resolve({
      code: 200,
      data: body || data || 'noData',
      message: message || 'noMessage'
    });
  }

  // 错误处理
  const errorText = message || '请求错误';

  // 使用switch处理不同状态码
  switch (status) {
    case 400:
      console.error({
        message: `请求错误 ${status}`,
        description: errorText,
      });
      break;

    case 401:
      console.error({
        message: '登录已过期',
        description: '您的登录已过期,请重新登录',
        duration: 3,
      });

      // 延迟登出,给用户看到提示的时间
      setTimeout(() => {
        //TODO 登出
      }, 1500);
      break;

    case 500:
      console.error({
        message: `请求错误 ${status}`,
        description: errorText,
      });
      break;

    default:
      // 可以在这里处理其他状态码
      console.error({
        message: `请求错误 ${status}`,
        description: errorText,
      });
      break;
  }

  // 即使是错误状态,也返回数据,让调用者可以处理
  return Promise.reject({
    code: status,
    data: body || 'noData',
    message: message || 'noMessage'
  });
};

4. 请求数据处理

/**
 * 处理data数据
 * @param options 请求配置
 * @returns 处理后的data数据
 */
const handleData = (options: any) => {
  const {data, headers} = options;
  const type = headers['Content-Type'] || DEFAULT_TYPE
  // 如果没有data,返回undefined
  if (!data) {
    return undefined;
  }

  // 如果是FormData,直接返回
  if (data instanceof FormData) {
    return data;
  }

  if(type === DEFAULT_TYPE){
    return JSON.stringify(data);
  }else{
    return data
  }
};

5. 核心请求函数

/**
 * 封装的fetch请求
 * @param url 请求地址
 * @param options 请求配置
 * @param gatewayUrl 基础网关URL
 * @returns Promise 响应结果
 */
const request = async (url: string, options: RequestInit & { data?: any } = {}) => {
  const { method, ...rest } = options;
  
  // 设置请求头
  const headers = setRequestHeaders(options);

  // 处理data数据
  const body = handleData({...options, headers});

  try {
    // 添加超时控制
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 30000); // 30秒超时

    // 发起请求
    const response = await fetch(url, {
      ...rest,
      method,
      headers,
      body,
      signal: controller.signal,
    });

    clearTimeout(timeoutId);

    // 使用提取出的响应处理函数
    return await handleApiResponse(response);
  } catch (error) {
    // 添加错误日志以便调试
    console.error('Request failed:', url, error);
    return Promise.reject(error);
  }
};

6. API 方法创建

/**
 * 创建API方法集合
 * @param gatewayUrl 基础网关URL
 */
const createApiMethods = (gatewayUrl: string) => {
  return {
    /**
     * GET请求
     * @param url 请求地址
     * @param options 其他配置
     */
    get: (url: string, options?: RequestInit) => {
      return request(`${gatewayUrl}${url}`, { method: 'GET', ...options });
    },

    /**
     * POST请求
     * @param url 请求地址
     * @param data 请求体数据
     * @param options 其他配置
     */
    post: (url: string, data?: any, options?: any) => {
      return request(`${gatewayUrl}${url}`, {
        data,
        ...options,
        method: 'POST',
      });
    },

    /**
     * PUT请求
     * @param url 请求地址
     * @param data 请求体数据
     * @param options 其他配置
     */
    put: (url: string, data?: any, options?: any) => {
      return request(`${gatewayUrl}${url}`, {
        data,
        ...options,
        method: 'PUT',
      });
    },

    /**
     * DELETE请求
     * @param url 请求地址
     * @param options 其他配置
     */
    delete: (url: string, options?: any) => {
      return request(`${gatewayUrl}${url}`, {  ...options ,method: 'DELETE',});
    },
  };
};

7. API 代理创建

/**
 * 创建API代理
 * @param getways 网关
 * @param customToken 开发环境传入的token
 * @returns 代理后的API对象
 */
export function createApiProxy(getways = {}, customToken: string = '') {
  // 如果提供了自定义token,更新全局变量
  if (customToken) {
    globalCustomToken = customToken;
  }

  // 创建API对象
  const apiBase: any = {
    getways: {
      gateway: '/bcf/api',
      ...getways,
    },
  };

  // 创建代理处理器
  const apiHandler = {
    get: (target: any, prop: string) => {
      // 如果属性已经存在,则返回它
      if (prop in target) {
        return target[prop];
      }

      // 否则,检查是否有对应的基础URL
      if (prop in target.getways) {
        // 为服务端点创建API方法并缓存,传入自定义的目标服务器地址
        target[prop] = createApiMethods(target.getways[prop]);
        return target[prop];
      }

      return undefined;
    },
  };

  // 使用代理创建API对象
  return new Proxy(apiBase, apiHandler);
}

8. 使用方式

// 默认网关使用
api.gateway.get('/users');
api.gateway.post('/users', { name: 'John', age: 30 });
api.gateway.put('/users/1', { name: 'Updated Name' });
api.gateway.delete('/users/1');

// 使用自定义网关
const customApi = createApiProxy({
  auth: '/auth/api',
  data: '/data/api'
});

customApi.auth.post('/login', { username: 'user', password: 'pass' });
customApi.data.get('/items');

// 开发环境使用自定义 token
const devApi = createApiProxy({}, 'dev-token-123');
devApi.gateway.get('/protected-resource');

9.Proxy 在这里的具体作用

JavaScript 的 Proxy 对象允许你创建一个对象的代理,拦截并自定义对该对象的基本操作(如属性查找、赋值、枚举、函数调用等)。

1. 延迟初始化(懒加载):
  • API 方法集合只有在首次访问特定网关时才会被创建。

  • 这种按需创建的方式提高了性能,避免了不必要的资源消耗。

2. 动态属性访问:
  • 允许通过 api.gateway.get() 这样的方式访问 API 方法。

  • 即使 gateway 属性最初并不存在于 api 对象上,Proxy 也能拦截这个访问并动态创建它。

3.属性查找拦截:
  • 当代码尝试访问 api.gateway 时,Proxy 的 get 陷阱会被触发。

  • 它会检查 gateway 是否是已知的网关名称(在 getways 对象中定义)。

  • 如果是,它会为该网关创建一套 API 方法(get、post、put、delete)。

4.结果缓存:
  • 一旦为特定网关创建了 API 方法集,它会被存储在原始对象中。

  • 这意味着下次访问同一网关时不需要重新创建方法集。

5. 使用示例解析

当你执行以下代码时:

api.gateway.get('/users');
  • 代码尝试访问 api.gateway。

  • Proxy 的 get 陷阱被触发,检查 gateway 是否存在。

  • 发现 gateway 在 getways 对象中定义为 /bcf/api。

  • 调用 createApiMethods('/bcf/api') 创建一套 API 方法。

  • 将这套方法存储在 api.gateway 中并返回。

  • 然后代码访问返回对象的 get 方法并调用它。

优势

  • 简洁的 API 设计:使用起来非常直观,如 api.gateway.post('/login', data)。

  • 灵活的网关配置:可以轻松添加和使用多个 API 网关。

  • 按需初始化:只有实际使用的网关才会创建对应的方法集。

  • 可扩展性:可以轻松添加新的网关或修改现有网关的 URL。

这种设计模式非常适合处理多服务架构的前端应用,让不同服务的 API 调用保持一致的接口同时又能清晰地区分。

10.总结

这个 Fetch 封装实现了以下核心功能:

  • 统一的请求/响应处理

  • 自动的认证管理

  • 灵活的错误处理

  • 请求超时控制

  • 多网关支持

  • 便捷的 HTTP 方法

  • 智能的内容类型处理

这种封装大大简化了 API 调用,提高了代码可维护性,并确保了请求处理的一致性。