背景
- typescript写法比较麻烦,开发库的时候效果特别棒,对于各种逻辑组合而产生的结果可以进行有效预测,让写出来的库更好用。同时用ts书写也方便大家对于这个库使用的理解。
- 此次从手写axios入手,一步步实现。
准备工作
- 为了方便,直接使用react的ts脚手架,没安装的可以安装下
npm i create-react-app -g
,然后create-react-app 项目名 --typescript
- 另外还需要安装原版axios做对比,以及qs用来转换对象格式的,还有parse-headers转换请求头。
npm i axios @types/axios qs @types/qs parse-headers
,它可能会警告说axios的声明文件已经包含在axios库里了,不需要另外安装它的声明文件,那就@types/axios不装也没事,不会报错。到时候会优先从axios的index.d.ts里去找声明文件。
流程
一、包装ajax,实现axios基础功能
- 前面我写了个文章axios与fetch常见封装方法,里面发现这个axios实际就是用ajax发个请求,然后前面弄个数组传请求拦截方法后面弄个数组传响应拦截方法,再把方法写的好装卸一点,暴露api出去给大伙用,其实也没想象的多高级。
- 服务端用上面文章那个即可。
- 首先,需要做一个数据,比如用户名密码,使用原生axios发给服务器:
import axios,{AxiosResponse} from 'axios';
const baseURL = "http://localhost:8080";
interface User{
name:string
password: string
}
let user : User ={
name:'yehuo',
password:'123456'
}
axios({
method:'get',
url:baseURL+'/get',
params:user
}).then((response:AxiosResponse)=>{
console.log(response);
return response.data
}).catch(err=>{
console.log(err)
})
- 这个AxiosResponse是固定格式类型,就像你用某个api,它会返回固定格式的东西一样,这个格式也不是你决定的,是这个api封装的时候决定的,这个格式在声明文件里写了。
- 这个能成就说明原生axios调用ok,仿照这个调用方法写个自己的。
- 先建个目录,里面放3个文件,axios.tsx放axios类,type.tsx放声明,index.tsx导出axios实例给外界调用。
axios/types.tsx
export type Methods = 'get' | 'GET'
| 'delete' | 'DELETE'
| 'head' | 'HEAD'
| 'options' | 'OPTIONS'
| 'post' | 'POST'
| 'put' | 'PUT'
| 'patch' | 'PATCH'
| 'link' | 'LINK'
| 'unlink' | 'UNLINK'
//定义传入接口
export interface AxiosRequestConfig{
url:string
method:Methods
params:any
}
//定义实例方法
export interface AxiosInstance{
<T=any>(config:AxiosRequestConfig):Promise<AxiosResponse>
}
//定义返回接口
export interface AxiosResponse<T = any> {
data: T;
status: number;
statusText: string;
headers?: Record<string,any>;
config?: AxiosRequestConfig;
request?: XMLHttpRequest;
}
- 这个没什么说的,就是我要传入什么,返回什么,写出来即可。
axios/index.tsx
import Axios from './axios'
import {AxiosInstance}from './types'
function creatInstance():AxiosInstance{
let context:Axios = new Axios();
//相当于执行instance就执行Axios原型上request方法,指针是Axios实例
let instance :AxiosInstance = Axios.prototype.request.bind(context)
//把实例,原型,方法合并,可以拿到所有身上的方法
instance =Object.assign(instance,Axios.prototype,context)
return instance as AxiosInstance
}
let axios:AxiosInstance = creatInstance()
export default axios
export * from './types'
- 这里有个很骚的操作,弄了个实例后,拿去绑到类的方法上去,也变相相当于每次在实例上写了个方法,至于为啥这么做?
- 首先,我要保证每个axios都是不同的玩意,有人说我写个function(类),然后把配置项传进去不也行?其实这是不行的,因为你所有地方调的都是同一个方法,同步还好说,如果异步的话,那岂不是乱套了。
- 第二、如果是实例的话,它是个对象啊,它怎么才能像调用方法一样把一个配置对象传到另一个对象里?有人说Object.defineProperty?那个不行,只能让你变成实例点方法进行调用。所以源码就用上这个骚操作,把类里的方法绑到实例上套层函数,顺便把this指向改了,避免全是同样的this。有人会想,可以拿个函数包装一下,利用call函数,然后传入的配置项给类上的方法,其实跟这个原理差不多,跟这个没本质区别。
- 最后一个合并什么意思?就是把这个类原型上可枚举的方法加到自己身上,这样在原型上加方法每个实例上也有,至于为啥把实例上的方法加过来?我也没懂,有懂的说一下。源码是通过for循环一个个加上去的,实例也加的。
axios/axios.tsx
import {AxiosRequestConfig} from './types'
export default class Axios{
request<T>(config:AxiosRequestConfig):Promise<T>{
return this.dispatchRequest(config)
}
dispatchRequest<T>(config:AxiosRequestConfig):Promise<T>{
return new Promise<T>((resolve,reject)=>{
})
}
}
- 这个类没啥说的,基本骨架是这样。
- 下面在类里用promise包装ajax:
import {AxiosRequestConfig,AxiosResponse} from './types'
import qs from 'qs'
import parseHeaders from 'parse-headers'
export default class Axios{
request(config:AxiosRequestConfig):Promise<AxiosResponse>{
return this.dispatchRequest(config)
}
dispatchRequest(config:AxiosRequestConfig):Promise<AxiosResponse>{
return new Promise<AxiosResponse>((resolve,reject)=>{
let {method,url,params} = config
let request = new XMLHttpRequest();
if(params&&typeof params==='object'){
params=qs.stringify(params)
}
url += (url.includes('?')?'&':'?'+params)
request.open(method,url,true)
request.responseType='json'
request.onreadystatechange=function(){
//4完成
if(request.readyState===4){//
if(request.status>=200&&request.status<300){
let response:AxiosResponse={
data:request.response?request.response:request.responseText,
status:request.status,
statusText:request.statusText,
//响应头xxx=xxx转换成对象格式
headers:parseHeaders(request.getAllResponseHeaders()),
config,
request
}
resolve(response)
}else{
reject('请求失败')
}
}
}
request.send()
})
}
}
- 这个没啥说的,然后对传入类型进行限定,并且把方法改成post,写post方法
index.tsx
import axios,{AxiosResponse} from './axios';
const baseURL = "http://localhost:8080";
interface User{
name:string
password: string
}
let user : User ={
name:'yehuo',
password:'123456'
}
axios({
method:'post',
url:baseURL+'/post',
data:user,
headers:{
'Content-Type':'application/json'
}
}).then((response:AxiosResponse<User>)=>{
console.log(response);
return response.data
}).catch(err=>{
console.log(err)
})
axios/types.tsx
export type Methods = 'get' | 'GET'
| 'delete' | 'DELETE'
| 'head' | 'HEAD'
| 'options' | 'OPTIONS'
| 'post' | 'POST'
| 'put' | 'PUT'
| 'patch' | 'PATCH'
| 'link' | 'LINK'
| 'unlink' | 'UNLINK'
//定义传入接口
export interface AxiosRequestConfig{
url:string
method:Methods
params?:any,
data?:Record<string,any>,
headers?:Record<string,any>
}
//定义实例
export interface AxiosInstance{
<T=any>(config:AxiosRequestConfig):Promise<AxiosResponse<T>>
}
//定义返回接口
export interface AxiosResponse<T=any> {
data: T;
status: number;
statusText: string;
headers?: Record<string,any>;
config?: AxiosRequestConfig;
request?: XMLHttpRequest;
}
axios/index.tsx
import Axios from './axios'
import {AxiosInstance}from './types'
function creatInstance<T>():AxiosInstance{
let context:Axios<T> = new Axios();
//相当于执行instance就执行Axios原型上request方法,指针是Axios实例
let instance :AxiosInstance = Axios.prototype.request.bind(context)
//把实例,原型,方法合并,可以拿到所有身上的方法
instance =Object.assign(instance,Axios.prototype,context)
return instance as AxiosInstance
}
let axios:AxiosInstance = creatInstance()
export default axios
export * from './types'
axios/axios.tsx
import {AxiosRequestConfig,AxiosResponse} from './types'
import qs from 'qs'
import parseHeaders from 'parse-headers'
export default class Axios<T>{
request(config:AxiosRequestConfig):Promise<AxiosResponse<T>>{
return this.dispatchRequest(config)
}
dispatchRequest(config:AxiosRequestConfig):Promise<AxiosResponse<T>>{
return new Promise<AxiosResponse<T>>((resolve,reject)=>{
let {method,url,params,headers,data} = config
let request = new XMLHttpRequest();
let tmp:string|undefined;
if(params&&typeof params==='object'){
tmp = qs.stringify(params)
}
url += (url.includes('?')?'&':'?'+(tmp?tmp:''))
request.open(method,url,true)
request.responseType='json'
request.onreadystatechange=function(){
//4完成
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,
//响应头xxx=xxx转换成对象格式
headers:parseHeaders(request.getAllResponseHeaders()),
config,
request
}
resolve(response)
}else{
reject('请求失败')
}
}
}
if(headers){
for(let key in headers){
request.setRequestHeader(key,headers[key])
}
}
let body:string|null = null
if(data){
body=JSON.stringify(data)
}
request.send(body)
})
}
}
- 这个params和data分别对应不同发送方式,原版就是这样,如果只写params去发post是收不到data的。
二、错误处理
-
错误处理分为网络异常,超时异常,错误状态码。
-
网络异常只要配置request.onerror即可。
-
超时异常需要配置timeout,定义好类型后,给request配置ontimeout和timeout。另外超时错误码是0,把前面判断条件排除一下。
-
状态码错误就把请求错误那换一下即可。
-
axios/types.tsx加上声明
export interface AxiosRequestConfig{
url:string
method:Methods
params?:any,
data?:Record<string,any>,
headers?:Record<string,any>,
timeout?:number
}
axios/axios.tsx
import {AxiosRequestConfig,AxiosResponse} from './types'
import qs from 'qs'
import parseHeaders from 'parse-headers'
export default class Axios<T>{
request(config:AxiosRequestConfig):Promise<AxiosResponse<T>>{
return this.dispatchRequest(config)
}
dispatchRequest(config:AxiosRequestConfig):Promise<AxiosResponse<T>>{
return new Promise<AxiosResponse<T>>((resolve,reject)=>{
let {method,url,params,headers,data,timeout} = config
let request = new XMLHttpRequest();
let tmp:string|undefined;
if(params&&typeof params==='object'){
tmp = qs.stringify(params)
}
url += (url.includes('?')?'&':'?'+(tmp?tmp:''))
request.open(method,url,true)
request.responseType='json'
request.onreadystatechange=function(){
//4完成
if(request.readyState===4&&request.status!==0){
if(request.status>=200&&request.status<300){
let response:AxiosResponse<T>={
data:request.response?request.response:request.responseText,
status:request.status,
statusText:request.statusText,
//响应头xxx=xxx转换成对象格式
headers:parseHeaders(request.getAllResponseHeaders()),
config,
request
}
resolve(response)
}else{
reject(`超时错误码${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("网络不通")
}
if(timeout){
request.timeout=timeout
request.ontimeout=function(){
reject(`超过已配置时间${timeout}ms`)
}
}
request.send(body)
})
}
}
三、实现拦截器功能
- 先看下原版功能:
import axios,{AxiosResponse,AxiosRequestConfig} from 'axios';
const baseURL = "http://localhost:8080";
interface User{
name:string
password: string
}
let user : User ={
name:'yehuo',
password:'123456'
}
let request = axios.interceptors.request.use((config:AxiosRequestConfig):AxiosRequestConfig=>{
config.headers.name+='1'
return config
})
axios.interceptors.request.use((config:AxiosRequestConfig):AxiosRequestConfig=>{
config.headers.name+='2'
return config
})
axios.interceptors.response.use((response:AxiosResponse):AxiosResponse=>{
response.data.name+='3'
return response
})
let response = axios.interceptors.response.use((response:AxiosResponse):AxiosResponse=>{
response.data.name+='4'
return response
})
axios.interceptors.request.eject(request)
axios.interceptors.response.eject(response)
setTimeout(() => {
axios({
method:'post',
url:baseURL+'/post',
data:user,
headers:{
'Content-Type':'application/json',
'name':'bbbb'
},
timeout:1000
}).then((response:AxiosResponse<User>)=>{
console.log(response);
return response.data
}).catch(err=>{
console.log(err)
})
}, 3000);
- 其中use是可以添加拦截器,eject就是把拦截器删掉,打开网页可以验证一下。
- 这个headers的键不能瞎填,浏览器和服务端不认。
- 这个拦截器如果阻塞了,或者用promise异步then,都会等待走完,直到拿到返回值才继续走。
- 拦截器请求是先加后执行,响应是先加先执行,就是往中间靠的三明治一样的感觉,有新的往外层加,中间核心是发送请求。
- 如果拦截器报错,直接跳到axios的catch,请求也不会发。
- 再来分析下怎么实现:
- axios通过上面可以发现实际是个bind包的函数(这里叫实例有点误导性,源码是这么叫的),axios.intercepters就应该是配置在这个函数上的属性。可以通过intercepters点request或者点response说明这个结构应该是intercepters包着request和response,这个request和response里面还有2方法,添加就是use,删除就是eject。
- intercepters是啥玩意?对象有可能吗?可能的,如果对象包着request和response是可以的。函数可能吗?也是有可能的,函数有这2属性即可。
- 推一下request和response是啥玩意,request和response里面需要调添加和删除的方法,那么request和response里面应该要有个数组把拦截器存起来。每一个axios是bind包的函数,指向axios不同实例,那么这个request和response会跟着intercepters一起分给每个axios实例,所以这里的request和response如果写成函数,那么所有的request会共有一个数组,所有的response也会共有一个数组。所以这里request和response必然为类的实例。
- 最后推一下use和eject大概啥玩意,use里面有个2个回调函数,一个是成功的回调,一个是失败的回调,都是用户传的,然后这个use完有个返回值,eject通过拿返回值把数组里的回调删了。那么这个返回值比较容易想的就应该是这个数组的索引。use时候把成功和失败回调包成一个整体push进数组,这样索引就能确定。
- 基本结构理清,下面放代码:
axios/axios.tsx
import {AxiosRequestConfig,AxiosResponse} from './types'
import qs from 'qs'
import parseHeaders from 'parse-headers'
import AxiosInterceptorManager ,{Interceptor} from './axiosinterceptor';
export default class Axios<T>{
interceptors = {
request:new AxiosInterceptorManager<AxiosRequestConfig>(),
response:new AxiosInterceptorManager<AxiosResponse<T>>()
}
request(config:AxiosRequestConfig):Promise<AxiosResponse<T>|AxiosRequestConfig>{
let chain :any[]=[
{
onFulfilled:this.dispatchRequest,
onRejected:(err:any)=>err
}
]
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:Promise<AxiosRequestConfig|AxiosResponse<T>>=Promise.resolve(config)
while(chain.length){
const {onFulfilled,onRejected}=chain.shift()
promise = promise.then(onFulfilled,onRejected)
}
return promise
}
dispatchRequest(config:AxiosRequestConfig):Promise<AxiosResponse<T>>{
return new Promise<AxiosResponse<T>>((resolve,reject)=>{
let {method,url,params,headers,data,timeout} = config
let request = new XMLHttpRequest();
let tmp:string|undefined;
if(params&&typeof params==='object'){
tmp = qs.stringify(params)
}
url += (url.includes('?')?'&':'?'+(tmp?tmp:''))
request.open(method,url,true)
request.responseType='json'
request.onreadystatechange=function(){
//4完成
if(request.readyState===4&&request.status!==0){
if(request.status>=200&&request.status<300){
let response:AxiosResponse<T>={
data:request.response?request.response:request.responseText,
status:request.status,
statusText:request.statusText,
//响应头xxx=xxx转换成对象格式
headers:parseHeaders(request.getAllResponseHeaders()),
config,
request
}
resolve(response)
}else{
reject(`超时错误码${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("网络不通")
}
if(timeout){
request.timeout=timeout
request.ontimeout=function(){
reject(`超过已配置时间${timeout}ms`)
}
}
request.send(body)
})
}
}
axios/axiosinterceptor.tsx
interface OnFulfilled<T>{
(value:T):T|Promise<T>
}
interface OnRejected{
(err:any):any
}
export interface Interceptor<T>{
onFulfilled?:OnFulfilled<T>
onRejected?:OnRejected
}
export default class AxiosInterceptorManager<T>{
interceptors:Array<Interceptor<T>|null>=[]
use(onFulfilled:OnFulfilled<T>,onRejected?:OnRejected):number{
this.interceptors.push({
onFulfilled,
onRejected
})
return this.interceptors.length-1
}
eject(id:number){
if(this.interceptors[id]){
this.interceptors[id]=null
}
}
}
axios/types.tsx
import AxiosInterceptorManager from './axiosinterceptor'
export type Methods = 'get' | 'GET'
| 'delete' | 'DELETE'
| 'head' | 'HEAD'
| 'options' | 'OPTIONS'
| 'post' | 'POST'
| 'put' | 'PUT'
| 'patch' | 'PATCH'
| 'link' | 'LINK'
| 'unlink' | 'UNLINK'
//定义传入接口
export interface AxiosRequestConfig{
url:string
method:Methods
params?:any,
data?:Record<string,any>,
headers?:Record<string,any>,
timeout?:number
}
//定义实例
export interface AxiosInstance{
<T=any>(config:AxiosRequestConfig):Promise<AxiosResponse<T>>
interceptors:{
request:AxiosInterceptorManager<AxiosRequestConfig>
response:AxiosInterceptorManager<AxiosResponse >
}
}
//定义返回接口
export interface AxiosResponse<T=any> {
data: T;
status: number;
statusText: string;
headers?: Record<string,any>;
config?: AxiosRequestConfig;
request?: XMLHttpRequest;
}
axios/index.tsx
import Axios from './axios'
import {AxiosInstance}from './types'
function creatInstance<T>():AxiosInstance{
let context:Axios<T> = new Axios();
//相当于执行instance就执行Axios原型上request方法,指针是Axios实例
let instance = Axios.prototype.request.bind(context)
//把实例,原型,方法合并,可以拿到所有身上的方法
instance =Object.assign(instance,Axios.prototype,context)
return instance as AxiosInstance
}
let axios:AxiosInstance = creatInstance()
export default axios
export * from './types'
- 可以验证下拦截器是否有效,我已经试过ok。
- 里面还有个思路值得学习下,先做个promise传入配置项,这样后续的then的回调函数会把配置项给接上,从数组中shift回调函数,用while不停then。
四、实现默认配置功能
- 默认配置就是设置一遍,不配对应选项默认就是这个配置。
- 原版调用是用
axios.defaults
。 - 原理很简单,就是用default配置项去合并用户传入的配置项。不同之处在于请求头有些方法:
axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
- 也就是需要把方法给替换掉,如果key里有这些方法,那么就把这个方法里面的key给加到请求头上。
let defaults :AxiosRequestConfig={
method:'get',
timeout:0,
headers:{
common:{
'accept':'application/json',
'name':'34'
}
}
}
let getStyleMethod =['get','head','delete','option']
let postStyleMethod=['put','post','patch']
getStyleMethod.forEach((method)=>{
defaults.headers![method]={}
})
postStyleMethod.forEach((method)=>{
defaults.headers![method]={
'Content-Type':'application/json',
}
})
let allMethod = [...getStyleMethod,...postStyleMethod]
- 在axios类里定义default:
defaults:AxiosRequestConfig =defaults
- 类里的request方法里合并请求头
config.headers=Object.assign(this.defaults.headers,config.headers)
- 在dispatchRequest方法里把headers判断加上
if(headers){
for(let key in headers){
if(key==='common'||allMethod.includes(key)){//排除自定方法
if(key==='common'||key===config.method){//本次发送方法
for(let key2 in headers[key]){
request.setRequestHeader(key2,headers[key][key2])
}
}
}else{
request.setRequestHeader(key,headers[key])//正常请求头
}
}
}
- types.tsx里把类型加上
export interface AxiosInstance{
<T=any>(config:AxiosRequestConfig):Promise<AxiosResponse<T>>
interceptors:{
request:AxiosInterceptorManager<AxiosRequestConfig>
response:AxiosInterceptorManager<AxiosResponse >
},
defaults:Required<AxiosRequestConfig>
}
- 最后拿例子试验下,就完成了。
axios.defaults.headers.post['name']='324'
axios.defaults.headers.common['name']='213123'
五、实现转换请求响应
- 先看一下原版这玩意干啥的:
// `transformRequest` allows changes to the request data before it is sent to the server
// This is only applicable for request methods 'PUT', 'POST', 'PATCH' and 'DELETE'
// The last function in the array must return a string or an instance of Buffer, ArrayBuffer,
// FormData or Stream
// You may modify the headers object.
transformRequest: [function (data, headers) {
// Do whatever you want to transform the data
return data;
}],
// `transformResponse` allows changes to the response data to be made before
// it is passed to then/catch
transformResponse: [function (data) {
// Do whatever you want to transform the data
return data;
}],
- 这个东西是在用户传入参数里可以设置的,然后自己写个回调函数。
- 比如:
axios({
method:'post',
url:baseURL+'/post',
data:user,
headers:{
// 'Content-Type':'application/json',
// 'name':'bbbb'
},
timeout:1000,
transformRequest:(data,head)=>{
head['common']['name']='kcvxcvc'
return data
},
transformResponse:(response)=>{
console.log(response);
return response.data
}
}).then((response:AxiosResponse<User>)=>{
console.log(response);
return response.data
}).catch(err=>{
console.log(err)
})
- 原理大家肯定都猜到了,就是在路上设置个回调,如果有就进入回调,没有就不管。
- types.tsx加上类型
export interface AxiosRequestConfig{
url?:string
method?:Methods
params?:any,
data?:Record<string,any>,
headers?:Record<string,any>,
timeout?:number,
transformRequest?:(data:any,headers:any)=>any,
transformResponse?:(data:any)=>any
}
- request里加上回调,注意要在合并默认请求头之后,这样headers会拿到默认的配置,不然undefined
if(config.transformRequest&&config.data){
config.data=config.transformRequest(config.data,config.headers)
}
- dispatchRequest里加入响应回调,就是在resolve请求前
if(config.transformResponse){
response=config.transformResponse(response)
}
- 这样就完成了。
六、实现任务取消
- 任务取消用的还是比较多的,就是cancelToken。
- 先看用法:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
- 先理一下结构,axios里有个canceltoken的属性,这个属性里面有个source方法,执行后的返回值有个token属性,配置到配置项里,返回值还有个cancel方法,执行后会取消。
- 浏览器会显示source.cancel后面的字符串,可以发现是个Cancel类做的实例,里面有个message属性。而且这个log是catch里面打印出来的,也就是最后外层走promise的reject的。
- 首先想一下,每个axios是独立的,那么这个axios.CancelToken应该是在Axios类里的方法,里面source执行后有2返回值,而我们发现最后浏览器有个Cancel类,那么就是source的返回值里的cancel执行会new一个Cancel的实例出来。这个Cancel类也很简单,传个message即可,另外他还需要包进promise里。然后是另一个source返回值token,token最终是配置在配置项里的,如果有token那么发送时怎么工作呢?应该是通过token找到并执行和token一起配置的cancel,那么token如何才能执行到cancel?显然,需要个公有变量放到CancelToken里,结合前面cancel是包在promise里的,那么这个公有变量就应该是resolve,cancel存到resolve里,等用户调用直接改变promise状态,那么token就是个promise,如果token存在,就在这个promise处理完resolve后关闭请求(then),把外层大promise变成失败态走rejecet,这个resolve如果一直不执行,自然就一直不会触发后面的then,而执行就交给了cancel,只要用户调用cancel,就立马触发resolve。
- 这样思路就理清了,axios/cancel写下两个类
export class Cancel{
message:string;
constructor(message:string){
this.message=message
}
}
export class CancelToken{
resolve:any;
source(){
return{
token:new Promise((resolve)=>{
this.resolve=resolve
}),
cancel:(message:string)=>{
this.resolve(new Cancel(message))
},
}
}
}
- axios/types修改声明
export interface AxiosInstance{
<T=any>(config:AxiosRequestConfig):Promise<AxiosResponse<T>>
interceptors:{
request:AxiosInterceptorManager<AxiosRequestConfig>
response:AxiosInterceptorManager<AxiosResponse >
},
defaults:Required<AxiosRequestConfig>,
isCancel: (err:any)=>boolean,
CancelToken:CancelToken
}
- 把方法加到axios类上:
CancelToken:CancelToken=new CancelToken()
isCancel= (err:any)=>{
return err instanceof Cancel
}
- 发送请求前判断token
if(config.cancelToken){
config.cancelToken.then((message:string)=>{
request.abort()
reject(message)
})
}
- 这样一个简易的axios就重构完了。需要代码的去仓库自取。