20分钟,带你手写简易版Axios
Hello, 各位勇敢的小伙伴, 大家好, 我是你们的嘴强王者小五, 身体健康, 脑子没病.
本人有丰富的脱发技巧, 能让你一跃成为资深大咖.
一看就会一写就废是本人的主旨, 菜到抠脚是本人的特点, 卑微中透着一丝丝刚强, 傻人有傻福是对我最大的安慰.
欢迎来到 小五 的 随笔系列 之 手写简易版Axios.
前言
作为一个前端er,很难不和 axios 打交道,那你有曾去探索 axios 的具体实现吗?如果答案是否的话,不妨跟随笔者的步伐,共同实现一个简易版的 axios 吧
简易版 axios 代码 -- Github地址(在项目中使用命令 npm install deeruby-axios 安装体验)
tips:本文仅对 axios 的浏览器部分做简易实现,对 node 部分感兴趣的小伙伴可自行探索,借助适配器即可抹平环境差异,使用户无感知
基础拾遗
此部分内容为本文所需知识点,如需扩充,请各位看官自行查阅相关资料
XMLHttpRequest
axios 的浏览器环境围绕 XMLHttpRequest 为核心进行封装
👉 创建
const xhr = new XMLHttpRequest()
👉 open
用于初始化一个请求: open(method, url, async)
xhr.open('get', '/user', true)
👉 send
用于发送HTTP请求: send(data),若无 data 需传入 null
xhr.send(null)
👉 setRequestHeader
用于设置头部信息:setRequestHeader(key, value)
setRequestHeader('Authorization', `Bearer ${localStorage.getItem('access_token')}`)
👉 onreadystatechange
用于检测 readyState 值的变化,当 readyState === 4 时,表明已完成请求
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) return
// ... 执行xxx ...
}
👉 withCredentials
Boolean 类型,表示跨域请求时是否携带凭证信息,可阅读 HTTP请求方法 -> OPTIONS -> CORS:跨域资源共享 了解更多相关知识
👉 abort
用于在接收到响应前取消请求
xhr.abort()
👉 get & post
get 请求需将查询参数追加至 url 末尾
xhr.open('get', '/user?name=deeruby&age=18', true)
post 请求传递 data
const data = { name: 'deeruby', age: 18 }
xhr.open('post', '/user', true)
xhr.send(data)
对象处理
将 obj2 合并至 obj1
const mergeDeep = (obj1, obj2) => {
for (let key in obj2) {
obj1[key] = obj1[key] && isObject(obj1[key])
? mergeDeep(obj1[key], obj2[key])
: obj2[key]
}
return obj1
}
判断是否为对象
const isObject = (data) => {
return Object.prototype.toString.call(data) === '[object Object]'
}
同源判断
协议域名均相同即为同源
const urlParsingNode = document.createElement('a')
const resolveURL = url => {
urlParsingNode.setAttribute('href', url)
const { protocol, host } = urlParsingNode
return { protocol, host }
}
const isURLSameOrigin = url => {
const currentOrigin = resolveURL(window.location.href)
const parsedOrigin = resolveURL(url)
return (
parsedOrigin.protocol === currentOrigin.protocol &&
parsedOrigin.host === currentOrigin.host
)
}
响应头解析
const parseHeaders = (headers) => {
if (!headers) return {}
let parsed = {}
headers.split('\r\n').forEach(line => {
let [key, value] = line.split(':')
key = key.trim().toLowerCase()
if (!key) return
parsed[key] = value
})
return parsed
}
分析实战
这里只对一些关键步骤进行讲解,若需查看全部代码,请跳转 【Github - ajun568】手写axios,或通过 npm install deeruby-axios
安装体验,注意 post 的请求方式稍有不同,data 需放在 config 中传递
创建 Axios 类
Q1: 如何实现以下两种调用方式
- axios({ method: 'get', ... })
- axiox.get(...)
分析:
- 实例化 Axios 即可调用 get 方法
class Axios {
constructor() {}
request() {}
get() { this.request('get') }
}
const axios = new Axios()
axios.get()
- axios({ ... }) 可通过将方法暴露出来实现
const instance = axios.request
return instance
Q2: 如何对上述两种调用方式进行整合
分析:
我们通过 Axios 的原型找到 request 方法,并使用 bind 改变其 this 指向
const context = new Axios(initConfig)
const instance = Axios.prototype.request.bind(context)
return instance
发送真实请求
send 方法关键步骤如下
const send = (config) => {
const xhr = new XMLHttpRequest()
xhr.open(method.toLowerCase(), url, true)
xhr.send(data)
}
接下来我们处理 get 请求的参数,该参数通过 params 传递,取值后将其拼接至 url 中即可
buildUrl
let query = qs.stringify(params)
if (query) query = `?${query}`
return `${url}${query}`
处理好 get 请求参数后,再来处理 post 请求参数,该参数通过 data 传递
tips:这里我们直接将其放在 config 的 data 中,不进行其他处理
transformData
return JSON.stringify(data)
头部的默认值适配 get 及 post 两个方法,若 post 请求没有 Content-Type,则需追加默认值,逻辑如下
initializeHeaders
if (method.toLowerCase() !== 'post') return headers
if (headers['Content-Type']) return headers
headers['Content-Type'] = 'application/json;charset=utf-8'
return headers
最后,在 Axios 中增加 _dealRequest 方法整合以上操作
const { url, params, data, method, headers = {} } = config
config.url = buildUrl(url, params)
config.data = transformData(data)
config.headers = initializeHeaders(method, headers)
return send(config)
Q: 如何实现链式结构
分析: 借助 promise 即可实现链式结构,改造 send 方法,为其包裹一层 promise
return new Promise((resolve, reject) => {
// ... xxx ...
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) return
// ... xxx ...
resolve(response)
}
// ... xxx ...
})
拦截器
- 请求拦截器:用于在请求发送前统一执行某些操作,如在请求头中添加 token 字段
- 响应拦截器:用于在接收到服务器响应后统一执行某些操作,如统一处理 401 跳转登录逻辑
分析
-
拦截器是一个链式调用的过程,借助 promise 即可实现链式调用
-
请求拦截在请求前执行,响应拦截在请求后执行,故构造形式为 [...request.use..., request, ...response.use...] 的数组,依次执行即可
-
send 方法需传入 config,故以 Promise.resolve(config) 为 promise 起点
综上,Interceptor 类实现如下
class Interceptor {
interceptors = []
use(resolved, rejected) {
this.interceptors.push({
resolved,
rejected,
})
return this.interceptors.length - 1
}
forEach(fn) {
this.interceptors.forEach(interceptor => {
if (interceptor) fn(interceptor)
})
}
}
Axios 类中追加相关处理
constructor() {
this.interceptors = {
request: new Interceptor(),
response: new Interceptor(),
}
}
request(config) {
const chain = [
{
resolved: this._dealRequest,
rejected: undefined,
}
]
this.interceptors.request.forEach(interceptor => {
chain.unshift(interceptor)
})
this.interceptors.response.forEach(interceptor => {
chain.push(interceptor)
})
let promise = Promise.resolve(config)
while (chain.length) {
const { resolved, rejected } = chain.shift()
promise = promise.then(resolved, rejected)
}
return promise
}
取消请求
方式1:
方式2:
分析
-
取消请求通过实例化 CancelToken 实现
-
方式1中的 source.token 即为方式2中的 new CancelToken(c => { cancel = c })
-
方式1中的 source.cancel 即为方式2中的 cancel
-
综合第2条第3条,方式1中的 source 方法是对方式2的封装
Q: 如何实现方式2
分析
-
借助 promise 实现,定义 resolvePromise 保存 promise 的 resolve
-
调用 cancel 时触发 resolve
-
promise.then 中调用 xhr.abort() 实现取消请求
CancelToken 实现
class CancelToken {
promise
constructor(executor) {
let resolvePromise
this.promise = new Promise(resolve => {
resolvePromise = resolve
})
const paramFn = () => { resolvePromise() }
executor(paramFn)
}
static source() {
let cancel
const token = new CancelToken(c => { cancel = c })
return { token, cancel }
}
}
send 方法中追加逻辑
if (cancelToken) {
cancelToken.promise.then(reason => {
xhr.abort()
reject(reason)
})
.catch(() => {})
}
参考链接
【Lion】手写 axios 库并发布至 npm 线上完整过程
转载自:https://juejin.cn/post/7165653891818717221