介绍
axios是前端跟后端发送接口请求必备的一个库,掌握它的原理对使用这个库的时候更加能够游刃有余,所以就来手写一个axios,跟官方的API保持一致。包括我们常用的请求响应拦截器、取消ajax请求等。为了降低学习难度,会使用javascript来写。
为了阅读起来更加方便,首先说一下我的目录结构:
- src
- core
- helper
- axios.js
- index.js
- core文件夹下面核心的模块
- helper文件夹是一些帮助函数
- axios.js 里面负责产生axios实例
- index.js是总入口
基本功能
要实现基本的发送请求其实非常的简单,对 XMLHttpRequest 这个api稍微的封装一下就可以,这一小节看完后将实现下面这种方法的调用。
// get请求
axios({
method: "get",
url: "/xxx/get",
headers:{
//传递headers
},
params: {
xx:1
}
})
.then(res => console.log(res))
// post请求
axios({
method: "post",
url: "/xxx/post",
headers:{
//传递headers
},
data: {
xx:1
}
})
.then(res => console.log(res))
xhr的封装
用过axios的朋友都知道,在接收到响应的时候我们通过类似下面的代码来获取后端的数据:
axios({
url: '/xxx'
})
.then(res => {
console.log(res.data)
})
后端的数据都是放在res.data中的,那么res还有其他的属性吗?它完整的数据结构是这样的:
{
data:{},//存放后端返回的数据
status: 200//状态码
statusText: 'OK'//状态文本
headers: {}//响应headers
config:{}//config配置
request:{}//XMLHttpRequest对象
}
一定要谨记这个数据结构。
首先看看对XMLHttpRequest的封装,在src/core/xhr.js文件中:
import { parseResponseHeaders } from "../helper/headers"
// 参数config可以理解为在调用axios的时候传递的那个对象
export default function xhr(config) {
return new Promise((resolve, reject) => {
const { url, method = "get", data, headers, responseType } = config
const request = new XMLHttpRequest()
if (responseType) {
request.responseType = responseType
}
request.open(method, url, true)
// 必须在open之后调用setRequestHeader
Object.keys(headers).forEach(headerName => {
request.setRequestHeader(headerName, headers[headerName])
})
request.send(data)
request.onreadystatechange = function() {
if (request.readyState !== 4) return
const responseHeaders = parseResponseHeaders(
request.getAllResponseHeaders()
)
let responseData =
request.responseType === "text"
? request.responseText
: request.response
const response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
}
resolve(response)
}
})
}
代码很容易理解,函数整体返回一个promise,当成功接收到请求时调用resolve,resolve的参数就是上面说的那个数据结构。注意这里的 xhr.responseType 判断,如果用户在调用axios传递了{ responseType:'json' },那么xhr.response就是一个对象,如果没有传responseType,xhr.response就是一个字符串(我们会在下面的介绍中把它自动转为一个json对象)。
由于 request.getAllResponseHeaders 返回的不是对象,而是字符串,我们要转化一下,在src/helper/header.js中,新增一个方法:
export function parseResponseHeaders(headers) {
const parsed = {}
if (!headers) return parsed
headers.split("\r\n").forEach(line => {
if (!line) return
const [key, value] = line.split(":")
parsed[key.trim()] = value.trim()
})
return parsed
}
getAllResponseHeaders返回的字符串中每个响应头是回车分割的,所以用\r\n来区分每个响应头
get请求
实现的时候有几点需要注意一下:
- axios需要返回promise
- url要特殊处理(hash自动去掉、url传参时要进行编码、url本身就有 ? 和没有 ?的兼容处理)
- params参数要支持 对象形式,因为是get请求嘛,所以需要把对象形式转为字符串拼接到url后面
- 如果params传递的对象中的value是数组或日期需要特殊处理
get请求传递参数之params
由于get请求是通过url后面跟问号的方式来传参,来看看怎么把params参数拼接到url后面的,在src/helpers/url.js文件中:
export default function handleURL(url, params) {
if (!params) return url
const parts = []
Object.keys(params).forEach(key => {
let val = params[key]
if (val == null) return
let values = []
if (Array.isArray(val)) {
key += "[]"
values = val
} else {
values = [val]
}
values.forEach(value => {
//日期特殊处理
if (isDate(value)) {
value = value.toISOString()
} else if (isPlainObject(value)) {
//对象特殊处理
value = JSON.stringify(value)
}
parts.push(`${key}=${value}`)
})
// 去掉hash
if (url.includes("#")) {
url = url.slice(0, url.indexOf("#"))
}
// 处理url中有没有?的逻辑
const queryStr = parts.join("&")
if (url.includes("?")) {
url += "&" + queryStr
} else {
url += "?" + queryStr
}
})
return url
}
在src/core/dispatchRequest.js中真正的发送请求
import handleURL from "../helper/url"
function processConfig(config) {
const { url } = config
config.url = handleURL(url, params)
}
export default function dispatchRequest(config) {
processConfig(config)
return xhr(config)
}
dispathRequest是发送请求的入口,我们调用axios()传递的参数直接扔给dispatchRequest,对config.url处理以后丢给xhr方法。
请求头和数据的处理
实现的时候需要注意:
- 默认在HTTP请求头中带有 Content-Type: application/json,要不然后端解析不到body里的数据
- xhr.send()方法并不支持传递一个对象,而我们在调用axios的时候传递的data是一个对象,不能直接扔给send方法,怎么处理?
- 调用后端接口返回的数据是json格式的,但是是字符串的json,需要自动转成json对象
有了上面的基础代码,实现这些请求就很简单啦。首先,如果用户在调用axios的时候传递了data字段,我们就默认添加Content-Type: application/json,在src/helpers/headers.js中:
function normalizeHeaders(headers, normalizeHeaderName) {
Object.keys(headers).forEach(headerName => {
if (
headerName !== normalizeHeaderName &&
headerName.toUpperCase() === normalizeHeaderName.toUpperCase()
) {
headers[normalizeHeaderName] = headers[headerName]
delete headers[headerName]
}
})
return headers
}
export function processHeaders(headers, data) {
headers = normalizeHeaders(headers, "Content-Type")
if (!headers["Content-Type"] && data) {
headers["Content-Type"] = "application/json;charset=utf-8"
}
return headers
}
normalizeHeaders这个函数为了兼容请求头中大小写的问题,因为可能用户传递了小写的content-type,所以这个时候我们统一转为大写的Content-Type来处理。
由于xhr.send不支持传递对象,比如xhr.send({name:'xxx'})是不支持的,因为send方法支持的数据类型为Blob、BufferSource、FormData、URLSearchParams、ReadableStream、USVString,所以要将其用JSON.stringify转化成USVString类型的。在src/helper/data.js中:
import { isPlainObject } from "./utils"
export default function transformRequest(data) {
if (isPlainObject(data)) {
return JSON.stringify(data)
}
return data
}
那么对于后端的结果返回的是json字符串的问题,用JSON.parse转化一下就可以,在src/helper/data.js中增加一个 transformResponse 方法,代码如下:
export function transformResponse(data) {
try {
data = JSON.parse(data)
} catch (ex) {
// todo
}
return data
}
在src/core/dispatchRequest.js文件中:
import xhr from "./xhr"
import { processHeaders } from "../helper/headers"
import handleURL from "../helper/url"
import { transformRequest, transformResponse } from "../helper/data"
function processConfig(config) {
const { url, params, headers = {}, data } = config
config.url = handleURL(url, params)
// 处理headers
config.headers = processHeaders(headers, data)
// 处理data
config.data = transformRequest(data)
}
export default function dispatchRequest(config) {
processConfig(config)
return xhr(config).then(res => {
// json字符串转为json对象
res.data = transformResponse(res.data)
return res
})
}
需要注意的是:对请求结果的处理是要放在接收到后端请求后的,因为xhr方法返回promise,所以在then里面把json字符串转为json对象
post、delete等请求
以上已经完成了我们发送请求的逻辑,接下来就看看如何组织代码暴露axios实例可以真正的调用方法来发请求。
在src/core/Axios.js中定义一个Axios类,可以让使用者方便的通过HTTP动词的方法发送请求:
import dispatchRequest from "./dispatchRequest"
export default class Axios {
constructor() {}
request(config) {
return dispatchRequest(config)
}
get(url, config) {
return this._requestWithoutData(url, "get", config)
}
options(url, config) {
return this._requestWithoutData(url, "options", config)
}
head(url, config) {
return this._requestWithoutData(url, "head", config)
}
post(url, data, config) {
return this._requestWithData(url, "post", data, config)
}
put(url, data, config) {
return this._requestWithData(url, "put", data, config)
}
patch(url, data, config) {
return this._requestWithData(url, "patch", data, config)
}
delete(url, data, config) {
return this._requestWithData(url, "delete", data, config)
}
_requestWithoutData(url, method, config) {
return this.request({
...config,
url,
method
})
}
_requestWithData(url, method, data, config) {
return this.request({
...config,
url,
method,
data
})
}
}
这个类的总入口就是request方法(方法内部调用dispatchRequest发送请求),其他的方法都是直接或间接的调用了reqeust来发送请求。
还有一个点就是,我们在使用axios的时候可以把axios当作函数来用,而不总是调用axios.get等这种方法。
在在src/axios.js中:(注意这里是在src下的axios文件,上面的是src/core下的)
import Axios from "./core/axios"
function extend(instance, axios) {
const methods = Object.getOwnPropertyNames(Axios.prototype).filter(
method => method !== "constructor"
)
methods.forEach(method => {
instance[method] = axios[method]
})
}
function createInstance() {
const axios = new Axios()
// instance是一个方法
const instance = axios.request.bind(axios)
// 把axios实例下的所有方法拷贝到instance上
extend(instance, axios)
return instance
}
const axios = createInstance()
export default axios
extend方法的作用就是把Axios实例类的方法全部拷贝到instance实例中。这样我们使用axios的时候可以把axios当作函数来用,也可以当作对象来使用(调用对应的方法)。
使用axios
最后我们把axios暴露给用户就行,在src/index.js中,一行代码:
import axios from "./axios"
export default axios
错误处理
错误处理是使用第三方库很重要的一部分,把错误暴露出来让用户自己去处理是一个很重要的机制。
在使用axios的时候,错误可以分为几下3种:
- 请求超时
- 网络错误(断网)
- 返回了4xx、5xx的错误码
为了让使用者拿到更加详细的错误信息,我们定义一个错误类来组织代码:
class AxiosError extends Error {
// message是错误
// config是请求时的config配置
// code是错误码
// request是请求的xmlhttprequest对象
// response是响应
constructor(message, config, code, request, response) {
super(message)
this.config = config
this.code = code
this.request = request
this.response = response
this.isAxiosError = true
// 这句代码非常关键
// 一个类继承了Error类,这个类的实例 instanceOf 这个类 应该返回true,没有这个代码会返回false
// 这应该是js语言的一个bug
Object.setPrototypeOf(this, AxiosError.prototype)
}
}
export default function createError(message, config, code, request, response) {
return new AxiosError(message, config, code, request, response)
}
通过一个工厂函数createError来创建一个错误实例,使用的时候就不用new了。
继续修改我们的代码,来处理上面3种错误情况,修改src/core/xhr.js,增加错误的逻辑。
请求超时
if (timeout) {
request.timeout = timeout
}
request.ontimeout = function() {
reject(
new createError(
`Timeout of ${config.timeout} ms exceeded`,
config,
"ECONNABORTED",
request,
null
)
)
}
网络请求错误
request.onerror = function() {
reject(new createError("Network Error", config, null, request,null))
}
不管是超时错误还是网络请求错误 都 没有response,所以最后一个参数都是null
是4xx、5xx的错误
if (response.status >= 200 && response.status < 300) {
resolve(response)
} else {
reject(
createError(
`Request failed with status code ${response.status}`,
config,
null,
request,
response
)
)
}
这里是把response.status状态码 [200,300) 之间当成正确的响应,反之就是异常的。官网的axios是支持自定义错误函数,也就是可以根据自己的业务需要来指定response.status什么范围内是正常的,反之是异常的。
总结
今天的文章实现了axios的基础功能和错误处理,下一篇文章会实现axios的拦截器和取消请求的功能。