手写Axios源码二: 处理post类请求和串通拦截器
1. 添加POST请求
post请求和get请求再处理上一个比较大的区别就是需要处理请求的data数据,仅需要再昨天代码的dispatchRequest()方法的基础上添加如下代码即可:
// ... (这里只写出有变动的部分: dispatchRequest方法, 之后会将今天处理的完整的代码放到最后)
// 解构出data数据:
let { method, url, params, headers, data } = config;
// ...
// 处理data: 也就是将data的数据转化为字符串方法send方法中发送出去:
// 转化data为字符串
let body: string | null = null;
if (data) {
body = JSON.stringify(data)
}
request.send(body);
2. 处理Axios错误处理:
这里主要处理三类错误:
- 请求错误: 这个错误会在promise的catch中简单进行捕获后reject掉
- 网络请求错误: 监听XHR对象的onerror事件进行错误的捕获抛出
- 超时错误处理: 监听XHR对象的ontimeout事件,超时则reject出错误
// 请求出错: 及状态码有误等
// ...
request.onreadystatechange = function () {
// 指定状态变更监听函数
if (request.readyState === 4) {
if (request.status >= 200 && request.status < 300) {
let response: AxiosResponse<T> = {
data: request.response ? request.response : request.responseText,
status: request.status,
statusText: request.statusText,
headers: parseHeader(request.getAllResponseHeaders()),
config,
request
}
resolve(response)
} else {
// 状态码不对时reject出错误
reject(`Error: Request failed with status code ${request.status}`)
}
}
}
// ...
// 监听XHR对象的错误事件进行错误抛出:
request.onerror = function () {
reject('net::ERR_INTERNET_DISCONNECTED');
}
// ...
// 执行超时错误处理: 此时可以用户传递一个timeout配置超时时间,可以从options中结构出来
if (timeout) {
request.timeout = timeout;
request.ontimeout = function () {
reject(`Error: timeout of ${timeout}ms exceeded`);
}
}
// ...
3. 串通请求拦截器和响应拦截器
关于拦截器的内容,为了大家更好的理解,这里进行一下简单的介绍:
如上图所示, axios的拦截器分为请求拦截器和响应拦截器,可以通过interceptors.request和interceptors.response对其进行配置和使用,请求拦截器和响应拦截器的处理流程: axios会将请求拦截器和响应拦截器串联起来, 先依次执行请求拦截器,然后发送请求,获取到响应信心后进入响应拦截器进行处理。使用interceptors进行拦截器的注册时,可以传递两个函数参数,第一个参数时处理成功时执行的方法,如果一直成功,则会按照顺序依次走过成功的流程,如果一个环节出现错误,则进入到错误的处理方法,也就是第二个参数方法,并依序执行下去。
关于请求拦截器和响应拦截器的顺序这里单独说明一下: 请求拦截器是先使用use注册的后执行,后使用use注册的先执行; 而响应拦截器恰好相反: 先注册的限制性,后注册的后执行,整个是以dispatchRequest()发起请求为界限分割;
举个简单的拦截器使用例子:
// axios请求拦截器: 先添加的后执行,后添加的先执行
// 请求拦截器返回一个请求拦截器的句柄(类似于一个别名)
// 再添加请求拦截器之后可以通过请求拦截器返回的句柄将器从拦截栈中弹出
// 请求拦截器可以传递两个参数: 第一个参数是请求成功的回调,第二个是请求失败的回调
axios.interceptors.request.use((config: AxiosRequestConfig): AxiosRequestConfig | Promise<AxiosRequestConfig> => {
// 拦截器可以直接处理requetConfig配置上的内容,返回config
// config.headers && (config.headers.name += "3");
// return config;
// 拦截器中还可以返回一个Pormise
// 此时在拦截器中axios会等待promise完成后再继续往下执行
return new Promise(function (resolve) {
setTimeout(() => {
config.headers && (config.headers.name += "3");
resolve(config)
}, 3000)
})
// 如果遇到Promise失败,则直接跳过所有的拦截器和请求,直接进入失败的逻辑
// return Promise.reject("请求失败")
}, (error: any) => Promise.reject(error))
开始处理拦截器处理逻辑:
- 修改AxiosInstance实例的类型定义:
// 新建AxiosInterceptorManager.js文件
export interface AxiosInstance {
<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>; // 这里写成Promise<AxiosResponse<T>>是因为axios instance执行返回的是一个response对象
// 为axios instance类型添加拦截器类型: 这里的类型AxiosInterceptorManager是一个拦截器类
// 拦截器分为请求拦截器和响应拦截器分别定义
interceptors: {
request: AxiosInterceptorManager<AxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse>;
}
}
- 定义AxiosInterceptorManager拦截器类:
// 这里是拦截器处理成功方法的类型
interface OnFulfilled<V> {
(value: V): V | Promise<V> | undefined | null
}
// 这里的是拦截器处理失败时的方法类型定义
interface OnRejected {
(error: any): any
}
// 定义一个拦截器类型: 可以包含有onFulfilled成功处理方法和onRejected失败处理方法
export interface Interceptor<V> {
onFulfilled?: OnFulfilled<V>; // 成功的回调
onRejected?: OnRejected; // 失败的回调
}
// 定义拦截器类:
// 这里的泛型T可能是AxiosRequestConfig, 也可能是AxiosResponse
// AxiosRequestConfig对应请求拦截器, AxiosResponse对应响应拦截器
export default class AxiosInterceptorManager<V>{
// 拦截器数组,将会将这个数组中的拦截器内容和dispatchRepuest方法进行串联
public interceptors: Array<Interceptor<V> | null> = [];
// 当调用use时,可以向拦截管理器中添加一个
use(onFulfilled?: OnFulfilled<V>, onRejected?: OnRejected): number {
this.interceptors.push({
onFulfilled,
onRejected
})
return this.interceptors.length - 1; // 返回拦截器在管理器数组中的位置,用于使用eject去除拦截器
}
eject(id: number) {
if (this.interceptors[0]) {
this.interceptors[id] = null;
}
}
}
- 串联请求和拦截器:
// 在Axios类中进行处理:
// 1. 引入
import AxiosInterceptorManager, { Interceptor } from "./AxiosInterceptorManager"
export default class Axios<T> {
// 2. 添加拦截器属性:
public interceptors = {
request: new AxiosInterceptorManager<AxiosRequestConfig>(),
response: new AxiosInterceptorManager<AxiosResponse<T>>()
}
// 3. 处理串联逻辑,在request方法中进行处理:
request(config: AxiosRequestConfig): Promise<AxiosRequestConfig | AxiosResponse<T>> {
//return this.dispatchRequest(config) // 这是昨天的代码
// 串接拦截器:
// 构建一个数组,依次将请求拦截器和响应拦截器和请求方法放到数组中串接起来, 数组元素的类型就是拦截器的类型
// 创建数组的同时将dispatchRequet也作为一层加入到拦截器中,之后的请求拦截和响应拦截器就分别往两边扩展
const chain: Array<Interceptor<AxiosRequestConfig> | Interceptor<AxiosResponse<T>>> = [{
onFulfilled: this.dispatchRequest,
onRejected: (error: any) => error
}]
this.interceptors.request.interceptors.forEach((interceptor: Interceptor<AxiosRequestConfig> | null) => {
// 将请求拦截器添加到chain数组的头部:
interceptor && chain.unshift(interceptor)
})
this.interceptors.response.interceptors.forEach((interceptor: Interceptor<AxiosResponse<T>> | null) => {
// 将响应拦截器添加到数组尾部
interceptor && chain.push(interceptor)
})
// 使用Promise串联拦截器: Promise<AxiosRequestConfig | AxiosResponse<T>>
let promise: any = Promise.resolve(config);
while (chain.length) {
// 如果长度大于0,则向数组的头部取出元素并返回这个元素
const { onFulfilled, onRejected } = chain.shift()!;
promise = promise.then(onFulfilled, onRejected)
}
return promise;
}
}
至此, 拦截器的处理完毕
附上完整的typs.ts类型文件个Axios.ts文件代码,其他文件几乎无改动,AxiosInterceptor.ts文件上面已经给出了完整的代码,这里不再重复:
types.ts文件:
import AxiosInterceptorManager from "./AxiosInterceptorManager";
export type Methods = 'GET' | 'POST' | 'get' | 'post' | 'put' | 'PUT' | 'delete' | 'DELETE' | 'options' | 'OPTIONS'
export interface AxiosRequestConfig {
url?: string;
method?: Methods;
params?: any;
headers?: Record<string, any>;
data?: Record<string, any>;
timeout?: number
}
export interface AxiosInstance {
<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>;
interceptors: {
request: AxiosInterceptorManager<AxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse>;
}
}
// 泛型T代表响应体的类型
export interface AxiosResponse<T = any> {
data: T;
status: number;
statusText: string;
headers?: Record<string, any>;
config?: AxiosRequestConfig,
request?: XMLHttpRequest
}
Axios.ts文件:
import { AxiosRequestConfig, AxiosResponse } from "./types"
import AxiosInterceptorManager, { Interceptor } from "./AxiosInterceptorManager"
import qs from "qs";
import parseHeader from "parse-headers";
export default class Axios<T> {
public interceptors = {
request: new AxiosInterceptorManager<AxiosRequestConfig>(),
response: new AxiosInterceptorManager<AxiosResponse<T>>()
}
request(config: AxiosRequestConfig): Promise<AxiosRequestConfig | AxiosResponse<T>> {
const chain: Array<Interceptor<AxiosRequestConfig> | Interceptor<AxiosResponse<T>>> = [{
onFulfilled: this.dispatchRequest,
onRejected: (error: any) => error
}]
this.interceptors.request.interceptors.forEach((interceptor: Interceptor<AxiosRequestConfig> | null) => {
interceptor && chain.unshift(interceptor)
})
this.interceptors.response.interceptors.forEach((interceptor: Interceptor<AxiosResponse<T>> | null) => {
interceptor && chain.push(interceptor)
})
let promise: any = Promise.resolve(config);
while (chain.length) {
const { onFulfilled, onRejected } = chain.shift()!;
promise = promise.then(onFulfilled, onRejected)
}
return promise;
}
dispatchRequest<T>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return new Promise<AxiosResponse<T>>(function (resolve, reject) {
let { method, url, params, headers, data, timeout } = config;
let request = new XMLHttpRequest();
if (params) {
params = qs.stringify(params);
url += ((url!.indexOf('?') > 0 ? '&' : '?') + params);
}
request.open(method!, url!, true);
request.responseType = "json";
request.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.status >= 200 && request.status < 300) {
let response: AxiosResponse<T> = {
data: request.response ? request.response : request.responseText,
status: request.status,
statusText: request.statusText,
headers: parseHeader(request.getAllResponseHeaders()),
config,
request
}
resolve(response)
} else {
reject(`Error: Request failed with status code ${request.status}`)
}
}
}
if (headers) {
for (let key in headers) {
request.setRequestHeader(key, headers[key])
}
}
let body: string | null = null;
if (data) {
body = JSON.stringify(data)
}
request.onerror = function () {
reject('net::ERR_INTERNET_DISCONNECTED');
}
request.send(body);
})
}
}