Logo
Published on

Fetch封装

Authors
  • avatar
    Name
    Monster Cone
    Twitter

随着浏览器的更新迭代,对 fetch 和 Promise 的支持也越来越好,在一些需求小或者无兼容需求的项目引入请求库无疑是浪费资源的,而原生支持的 fetch 就为我们提供了另一条路,下面针对 fetch 和 API 规范做封装

请求基本配置

const BASE_URL = 'https://xxxx.com/xxx' // 请求地址
const TIME_OUT = 5000 // 请求超时时间,单位毫秒

先看我们的主要请求体

const fetchApi = async (url, opts) => {
  const controller = new AbortController()

  url = base_url + url
  const options = {
    headers: {
      Authorization: `Bearer ${getAccessToken()}`,
      'content-type': 'application/json',
    },
    ...opts,
    signal: controller.signal,
  }

  const timer = setTimeout(() => {
    controller.abort()
  }, TIME_OUT)
  try {
    return await fetch(url, options)
      .then(async (res) => {
        if (res.ok === true) {
          const data = await res.clone().json()
          return Promise.resolve(data)
        } else {
          if (res.status === 401) {
            let params = {
              refresh_token: getRefreshToken(),
            }
            let { access_token } = await refreshToken(params)
            setAccessToken(access_token)
            return await fetchApi(url, options)
          } else {
            return Promise.reject(res)
          }
        }
      })
      .catch((err) => {
        return Promise.reject(err)
      })
      .finally((_) => {
        clearTimeout(timer)
      })
  } catch (err) {
    return Promise.reject(err)
  }
}
  • fetchApi 参数 1. 请求地址 2. 初始化配置对象, 可选
  • 4 行 - 11 行,拼接 url 同时初始化请求参数,初始化一般都是携带自定义的请求头
  • 2、10、13-15 行,我们通过延时器和 AbortController 控制器取消请求来处理请求时间过长的问题,同时在请求完成后不要忘记清除延时器 39 行
  • 为了统一处理 Promise 都是用 await 来接收结果,在外层用 try catch 捕获所有错误
  • 17 行 - 40 行就是请求主体了,判断请求状态返回请求结果,如果 token 过期则刷新 token 再次请求
  • 通常在 21 行处还需要判断请求成功,但因为其他原因导致失败的状态码,一般都和服务端进行协商约定,这个封装因为后端是自己的项目错误情况都不会是 200 状态码所以省略了这一步

为了便于调用,我们在原型方法上挂载请求方法函数

fetchApi.get = async (url, params = {}) => {
  if (JSON.stringify(params) !== '{}') {
    url += (url.indexOf('?') > -1 ? '&' : '?') + paramsStringify(params)
  }

  return await fetchApi(url, {
    method: 'GET',
  })
}

fetchApi.post = async (url, data) => {
  return await fetchApi(url, {
    method: 'POST',
    body: JSON.stringify(data),
  })
}

fetchApi.put = async (url, data) => {
  return await fetchApi(url, {
    method: 'PUT',
    body: JSON.stringify(data),
  })
}

fetchApi.delete = async (url) => {
  return await fetchApi(url, {
    method: 'DELETE',
  })
}

get 方法和 delete 方法没有 body 所以我们需要自己处理参数

封装中用到的其他方法如下:

const paramsStringify = (params) => {
  return Object.keys(params)
    .map((key) => {
      return `${key}=${encodeURI(params[key])}`
    })
    .join('&')
}
const setAccessToken = (access_token, options) => {
  return Cookies.set('access_token', access_token, options)
}
const getAccessToken = () => {
  return Cookies.get('access_token')
}
const getRefreshToken = () => {
  return Cookies.get('refresh_token')
}
const refreshToken = (params) => fetchApi.get(`${base_url}/auth/refresh_token`, params)

完整的代码如下

const base_url = '/api'
const TIME_OUT = 30000

const setAccessToken = (access_token, options) => {
  return Cookies.set('access_token', access_token, options)
}

const getAccessToken = () => {
  return Cookies.get('access_token')
}

const getRefreshToken = () => {
  return Cookies.get('refresh_token')
}

const paramsStringify = (params) => {
  return Object.keys(params)
    .map((key) => {
      return `${key}=${encodeURI(params[key])}`
    })
    .join('&')
}

const refreshToken = (params) => fetchApi.get(`/auth/refresh_token`, params)

const fetchApi = async (url, opts) => {
  const controller = new AbortController()

  url = base_url + url
  const options = {
    headers: {
      Authorization: `Bearer ${getAccessToken()}`,
    },
    mode: 'cors',
    ...opts,
    signal: controller.signal,
  }

  const timer = setTimeout(() => {
    controller.abort()
  }, TIME_OUT)
  try {
    return await fetch(url, options)
      .then(async (res) => {
        if (res.ok === true) {
          const data = await res.clone().json()
          return Promise.resolve(data)
        } else {
          if (res.status === 401) {
            let params = {
              refresh_token: getRefreshToken(),
            }
            let { access_token } = await refreshToken(params)
            setAccessToken(access_token)
            return await fetchApi(url, options)
          } else {
            return Promise.reject(res)
          }
        }
      })
      .catch((err) => {
        return Promise.reject(err)
      })
      .finally((_) => {
        clearTimeout(timer)
      })
  } catch (err) {
    return Promise.reject(err)
  }
}

fetchApi.get = async (url, params = {}) => {
  if (JSON.stringify(params) !== '{}') {
    url += (url.indexOf('?') > -1 ? '&' : '?') + paramsStringify(params)
  }

  return await fetchApi(url, {
    method: 'GET',
  })
}

fetchApi.post = async (url, data) => {
  return await fetchApi(url, {
    method: 'POST',
    body: JSON.stringify(data),
  })
}

export default fetchApi

使用方法

fetchApi('/api/login', { username: 'monster', password: 123456 }).then((res) => xxx)

总结

为了简洁,一个方法可以放在 utils 工具文件中进行复用,此处为了展示完整代码所以搬到了一个文件中。响应可以根据自己的需求进行扩展。因为是原生支持还是存在不同浏览器的兼容问题需要使用 polyfill 插件来解决。虽然提供了 mode 来支持跨域,但还是需要服务端来配合使用才行。