Skip to content
大纲

封装请求

原生的 XMLHttpRequest 不方便直接使用,需要封装

封装 XHR

js
// callback 回调版本
function ajaxCallback(url, options = {}) {
  const noop = () => {}
  const {
    method = 'get',
    success = noop,
    fail = noop,
    complete = noop
  } = options
  // 1. 创建 ajax 对象
  let xhr = null
  try {
    xhr = new XMLHttpRequest()
  } catch (error) {
    // eslint-disable-next-line no-undef
    xhr = new ActiveXObject('Microsoft.XMLHTTP')
  }

  // 可以设置一些配置
  xhr.timeout = 10000

  xhr.ontimeout = (e) => {
    console.log(e)
    // options.onTimeout?.(e)
  }

  // 2. 等待数据响应
  // 必须在调用 open() 方法之前指定 onreadystatechange 事件处理程序才能确保跨域浏览器兼容性                //问题
  // 只要 readyState 属性的值有变化,就会触发 readystatechange 事件
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) {
      // TODO: 这里到底该怎么设计好?
      // jQuery.ajax 以及 axios 是怎么考虑设计的,为什么?
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
        try {
          // TODO:  xhr.response vs req.responeText
          const result = JSON.parse(xhr.responseText)
          success(result)
        } catch (err) {
          fail(err)
        }
      } else {
        fail('Error:' + xhr.status)
      }
    }
  }

  // 3. 调用open(默认为 true 表示异步, false 同步)
  xhr.open(method, url, true)

  // 4. 设置 HTTP 请求头的值。必须在 open() 之后、send() 之前调用
  // xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded')

  // 5. 调用send
  xhr.send()

  return xhr
}
js
// promise 版本
function ajaxPromise(url, options = {}) {
  const { method = 'get' } = options
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()

    // If specified, responseType must be empty string or "text"
    xhr.responseType = 'text'

    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        // if(req.status >= '200' && req.status <= 300){
        if (xhr.status === 200) {
          console.log(xhr)
          resolve(xhr.response)
        } else {
          reject(xhr)
        }
      }
    }
    // 第三个参数,默认为 true,指示是否异步执行操作
    // xhr.open(method, url, async)
    xhr.open(method, url)
    xhr.send(options.data)
  })
}

针对请求

针对请求(xhr, fetch)实现以下诉求

  1. 并发控制
  2. 超时取消
  3. 手动取消
  4. 失败重试 N 次
  5. session 失效自动更新
js
// 请求基础封装
function myAjax(url, options = {})
function myFetch(url, options = {})

// 可使用 sleep 模拟请求
const sleep = (...rest) => new Promise(s => setTimeout(s, ...rest))

并发控制

js
// 同 p-limit
import pLimit from 'p-limit'

const limit = pLimit(2)
const request1 = pLimit(() => sleep(2000, 'api_1'))
const request2 = pLimit(() => sleep(1500, 'api_2'))
const request3 = pLimit(() => sleep(1000, 'api_3'))

Promise.all([request1, request2, request3]).then((res) => {
  console.log(res)
})

手写实现请求并发控制

js

超时取消

  • xhr 可以通过配置项 timeout 取消请求
  • fetch 没有超时取消控制,可以通过 race 竞争,实现计时控制。
js
const url = 'https://baidu.com'
// 对于 xhr
const xhr = new XMLHttpRequest()
xhr.open('GET', url)

xhr.timeout = 5000
xhr.onload = () => {
  // Request finished. Do processing here.
}
xhr.ontimeout = (e) => {
  // XMLHttpRequest timed out. Do something here.
}

xhr.send()

// 对于 fetch
const sleep = (...rest) => new Promise((s) => setTimeout(s, ...rest))
const getDetail = (params) => {
  return sleep(2000, { ok: true, code: 0, data: {} })
  // return fetch('/api/getDetail', {})
}
const timeout = (time) => new Promise((s, r) => setTimeout(r, time, 'timeout'))
Promise.race([getDetail(), timeout(1000)])
  .then((res) => {
    console.log('res', res)
  })
  .catch((err) => {
    console.log('err', err)
  })

手动取消

  • xhr 可以通过 xhr.abort() 方法取消请求
  • fetch 可以通过 AbortController 来控制取消请求
    • controller.signal 传入 fetch 配置项
    • 通过 controller.abort() 方法取消请求
js
// 对于 xhr
const url = 'https://baidu.com'
const xhr = new XMLHttpRequest()
xhr.open('GET', url)

xhr.send()

// 手动取消
xhr.abort()

// 对于 fetch
const controller = new AbortController()
const signal = controller.signal

fetch(url, { signal })

// 手动取消
controller.abort()

失败重试 N 次

无论 xhr 还是 fetch,该功能默认都没有,需要我们自己扩展支持

js
const url = 'https://baidu.com'
const maxTryTimes = 3
myAjax(url)
  .then((res) => {})
  .catch((err) => {
    if (!err.ok) {
      const { options } = err
      options.tryTimes ??= 0
      options.tryTimes++
      if (options.tryTimes < maxTryTimes) {
        return ajaxPromise(url, options)
      } else {
        throw err
      }
    }
  })

相比而言,axios 通过高阶封装,更容易实现该能力扩展

如下,通过 axios-retry 拦截器直接支持该功能了

js
import axios from 'axios'
import axiosRetry from 'axios-retry'

axiosRetry(axios, { retries: 3 })

axios.get('https://baidu.com')

session 失效自动更新

这是个业务功能扩展,是请求出错重试的定制版

当发生 session 过期错误时,通过封装特定逻辑来更新 session,然后再重试前面失败的请求

js

面试题

手写 fetch 请求,请求失败重试三次,且 10 秒超时自动取消

这是个综合题,同时要求失败重试和超时取消功能

js
const url = 'https://baidu.com'
const options = {
  url,
  maxTryTimes = 3
}
const request = (options) => {
  const controller = new AbortController()
  const signal = controller.signal

  const result = fetch({
    maxTryTimes: 3,
    signal,
    ...options,
  }).catch(err => {
    if (!err.ok) {
      const { options: opts } = err
      opts.tryTimes ??= 0
      opts.tryTimes++
      if (opts.tryTimes < opts.maxTryTimes) {
        return request(opts)
      } else {
        throw err
      }
    }
  })

  return {
    controller,
    result,
  }
}

const timeout = (time) => new Promise((s, r) => setTimeout(r, time, 'timeout'))
Promise.race(request(options), timeout(5000)).then(res=>{
  console.log('res', res)
}).catch(err=>{
  console.log('err', err)
})