封装请求
原生的 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)实现以下诉求
- 并发控制
- 超时取消
- 手动取消
- 失败重试 N 次
- 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)
})