Service请求组件(可取消请求)

组件流程图

在这里插入图片描述

前置知识

AbortController使用场景探索

2个AbortController的使用场景:fetchaddEventListener

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去分组处理一批请求?

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值