目录
组件流程图
前置知识
AbortController使用场景探索
2个AbortController的使用场景:fetch和addEventListener
fetch:
const controller = new AbortController(); // 新建一个AbortController实例
let signal = controller.signal; // signal是AbortController实例的属性
//可以用作fetch的第二个参数中,控制请求取消
fetch(url, {signal}).then(function(response) {
//...
}).catch(function(e) {
console.error('e', e);
})
// 调用abort方法,取消请求
controller.abort();
addEventListener:
//还可以作为addEventListener第三个参数options上的可选属性:
//按照mdn的说法,当调用abort()后,监听器会被移除。其实这就相当于给我们内置了一个移除监听器的方式,可以不用把callback单独提取出成一个函数。
//之前创建以及取消监听
document.addEventListener('mousemove', callback3);
document.removeEventListener('mousemove', callback3);
//使用AbortController取消监听
const controller = new AbortController();
function callback (e) {
document.addEventListener('mousemove', (e) => {
},{
signal: controller.signal
});
}
document.addEventListener('mousedown', callback);
document.addEventListener('mouseup', controller.abort);
Axios取消请求方式:
//axios从v0.22版本后也支持fetch的这种方式了
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// 取消请求
controller.abort()
组件代码实现
http.ts
import Axios, { AxiosInstance, AxiosResponse } from 'axios';
import fetchAdapter from './fetch-adapter';
import * as Middleware from './middleware';
import type { RsponseType, XHRResponse } from './interface';
const CancelToken = Axios.CancelToken;
//基础配置
const config = {
baseURL: '/', //基础路径前缀
timeout: 60 * 1000, //设置超时时间
xhrMode: 'fetch', //请求made
adapter: fetchAdapter, //自定义处理请求函数
headers: {
Accept: 'application/json; charset=utf-8',
'Content-Type': 'application/json; charset=utf-8',
},
};
//创建axios实例
const httpInstance = Axios.create(config);
/**
* 请求之前拦截动作
*/
httpInstance.interceptors.request.use(
(response) => response,
(error) => console.error(error),
);
/**
* 请求之后拦截动作
*/
httpInstance.interceptors.response.use(
//请求成功后的处理函数
(response: AxiosResponse<any>) => {
if (Middleware.responseMiddleware.length === 0) {
return response.data;
} else {
//遍历注册方法的函数队列,并依次执行
Middleware.responseMiddleware.forEach((fn: (config: AxiosResponse<any>) => any) => (response = fn(response)));
return response;
}
},
//请求失败后的处理函数
function httpUtilErrorRequest(error) {
if (Middleware.responseErrorMiddleware.length !== 0) {
//遍历注册方法的函数队列,并依次执行
Middleware.responseErrorMiddleware.forEach((fn: (config: any) => any) => (error = fn(error)));
return Promise.reject(error);
}
if (!error.response) {
console.error(error);
return Promise.reject(error);
}
return Promise.reject(error.response);
},
);
//抛出去的函数
function http({ cancelHttp, ...newOptions }: RsponseType): Promise<any> {
let cancel;
const cancelToken = new CancelToken((c) => {
cancel = c;
if (cancelHttp) {
//将控制取消的c函数传出去
cancelHttp(cancel);
}
});
return httpInstance({ ...newOptions, cancelToken });
}
//和上面差不多,Content-Type不一样
const httpMultiPartInstance: AxiosInstance = Axios.create({
timeout: 10 * 60 * 1000,
adapter: fetchAdapter,
headers: {
'Content-Type': 'multipart/form-data',
},
});
httpMultiPartInstance.interceptors.response.use(
(response) => Promise.resolve(response.data),
(error) => Promise.reject(error),
);
//这个是个原生ajax,因为有些人不喜欢用fetch
function httpXMLInstance({ url, method = 'GET', data, headers, cancelHttp, isAsync = false }: XHRResponse): Promise<any> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
//ajax有自己的abort来处理取消请求
const cancel = () => xhr.abort();
if (cancelHttp) {
cancelHttp(cancel);
}
xhr.open(method, url, !isAsync);
if (headers) {
Object.keys(headers).forEach((key) => {
xhr.setRequestHeader(key, headers[key]);
});
}
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 304)) {
let data;
try {
data = JSON.parse(xhr.response);
} catch (e) {
data = xhr.response;
}
resolve(data);
}
if (xhr.readyState === 4 && !(xhr.status === 200 || xhr.status === 304)) {
reject(xhr);
}
};
xhr.send(data ? JSON.stringify(data) : null);
});
}
export { http as default, http, httpMultiPartInstance, httpXMLInstance };
fetch-adapter.ts(自定义处理请求)
/// <reference path="../../typings/global.d.ts"/>
import { AxiosRequestConfig, AxiosError, AxiosResponse, AxiosPromise } from 'axios';
import settle from 'axios/lib/core/settle';
import buildURL from 'axios/lib/helpers/buildURL';
import buildFullPath from 'axios/lib/core/buildFullPath';
import CanceledError from 'axios/lib/cancel/CanceledError';
import { isUndefined } from 'lodash-es';
export interface AxiosFetchRequestConfig extends AxiosRequestConfig<BodyInit> {
mode?: RequestMode;
body?: BodyInit;
cache?: RequestCache;
integrity?: string;
redirect?: RequestRedirect;
referrer?: string;
credentials?: RequestCredentials;
}
export default function fetchAdapter(config: AxiosFetchRequestConfig): AxiosPromise {
return new Promise((resolve, reject) => {
//创建实例
const controller = new AbortController();
const signal = controller.signal;
//创建一个Request对象
const request = createRequest(config, signal);
//数组:包含请求,和判断是否请求超时的promise
const promises = [getResponse(request, config)];
//只要不是永久的就需要判断是否超时,添加到数组中
if (config.timeout && config.timeout > 0) {
promises.push(timeoutHandle(request, controller, config));
}
//如果已经取消了,改变状态
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
reject(!cancel ? new CanceledError(null, config, request) : cancel);
controller.abort();
});
}
//Promise.race传入数组,会返回数组中第一个改变状态的结果,规定时间范围内请求成功则正常返回,否则取消请求,返回失败结果
return Promise.race(promises)
.then((data) => settle(resolve, reject, data))
.catch(reject);
});
}
//根据timeout创建定时器,改变状态
function timeoutHandle(request: Request, controller: AbortController, config: AxiosFetchRequestConfig): Promise<Error> {
return new Promise((resolve) => {
setTimeout(() => {
const message = config.timeoutErrorMessage ? config.timeoutErrorMessage : 'timeout of ' + config.timeout + 'ms exceeded';
resolve(createError(message, config, 'ECONNABORTED', request));
controller.abort();
}, config.timeout);
});
}
//获取Request实例
async function getResponse(request: Request, config: AxiosFetchRequestConfig) {
let stageOne;
try {
stageOne = await fetch(request);
} catch (e) {
return createError('Network Error', config, 'ERR_NETWORK', request);
}
const response = {
ok: stageOne.ok,
status: stageOne.status,
statusText: stageOne.statusText,
headers: new Headers(stageOne.headers),
config: config,
request,
} as unknown as AxiosResponse<any, any>;
if (stageOne.status >= 200 && stageOne.status !== 204) {
switch (config.responseType) {
case 'arraybuffer':
response.data = await stageOne.arrayBuffer();
break;
case 'blob':
response.data = await stageOne.blob();
break;
case 'json':
response.data = await stageOne.json();
break;
default:
response.data = await stageOne.text();
break;
}
}
return response;
}
function createRequest(config: AxiosFetchRequestConfig, signal: AbortSignal): Request {
const headers = new Headers(config.headers as any as Headers);
if (config.auth) {
const username = config.auth.username || '';
const password = config.auth.password ? decodeURI(encodeURIComponent(config.auth.password)) : '';
headers.set('Authorization', `Basic ${btoa(username + ':' + password)}`);
}
const method = config.method.toUpperCase();
const options: RequestInit = { headers, method, signal };
if (method !== 'GET' && method !== 'HEAD') {
options.body = config.data;
}
if (config.mode) {
options.mode = config.mode;
}
if (config.cache) {
options.cache = config.cache;
}
if (config.integrity) {
options.integrity = config.integrity;
}
if (config.redirect) {
options.redirect = config.redirect;
}
if (config.referrer) {
options.referrer = config.referrer;
}
if (!isUndefined(config.withCredentials)) {
options.credentials = config.withCredentials ? 'include' : 'omit';
}
const fullPath = buildFullPath(config.baseURL, config.url);
const url = buildURL(fullPath, config.params, config.paramsSerializer);
// Expected browser to throw error if there is any wrong configuration value
return new Request(url, options);
}
//创建axios错误实例
function createError(message: string, config: AxiosFetchRequestConfig, code: string, request: Request, response?: AxiosResponse): Error {
return new AxiosError(message, AxiosError[code], config, request, response);
}
middleware.ts(注册方法,方法数组)
import type { MiddleWareType } from './interface';
let _global = window as any;
//请求成功后调用方法的数组
export const responseMiddleware: Array<any> = _global.responseMiddleware || [];
//请求失败后调用的方法数组
export const responseErrorMiddleware: Array<any> = _global.responseErrorMiddleware || [];
//请求成功后调用方法的注册
export function registerResponseMiddleware(fn: MiddleWareType): void {
if (!responseMiddleware.includes(fn)) {
responseMiddleware.push(fn);
_global.responseMiddleware = responseMiddleware;
}
}
//请求失败后调用方法的注册
export function registerResponseErrorMiddleware(fn: MiddleWareType): void {
if (!responseErrorMiddleware.includes(fn)) {
responseErrorMiddleware.push(fn);
_global.responseErrorMiddleware = responseErrorMiddleware;
}
}
index.ts
import { http, httpMultiPartInstance, httpXMLInstance } from './http';
import type { IServiceInterface } from './interface';
import { registerResponseMiddleware, registerResponseErrorMiddleware } from './middleware';
const Service = {
http,
httpXMLInstance,
httpMultiPartInstance,
registerResponseMiddleware,
registerResponseErrorMiddleware,
} as IServiceInterface;
export default Service;
interface.d.ts(ts类型文件)
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
export type RsponseType = AxiosRequestConfig & { requestId?: string; cancelHttp?: (cancel: Function) => void; loggerIndex?: number };
export type XHRResponse = {
url: string;
method: string;
data?: any;
headers?: any;
cancelHttp?: any;
isAsync?: boolean;
requestId?: string;
};
export type MiddleWareType = (config: AxiosResponse<any>) => any;
export type IServiceInterface = {
http(options: RsponseType): Promise<any>;
httpXMLInstance(options: XHRResponse): Promise<any>;
httpMultiPartInstance: AxiosInstance;
registerResponseMiddleware(fn: MiddleWareType): void;
registerResponseErrorMiddleware(fn: MiddleWareType): void;
};
总结
使用就是流程图上那样,如果要实现快速切换路由的时候已经发出去的请求让它们取消掉,需要自己去划分请求和路由之间的关系然后做处理,每个请求都会返回一个属于自己的cancel方法,处理起来很麻烦,但是并不是每一个请求都需要去取消它,没有必要,这主要是处理一些几秒或几十秒都还在pendding的请求就够了。所以这个组件我认为已经满足了业务上的需求,如果一定要追求完美,其实可以再包一层,可以定义不同路由和不同type的映射关系,在组件内部根据type去分组处理一批请求?