axios 中一个请求取消的示例:

axios 取消请求的示例代码
import React, { useState, useEffect } from "react";
import axios, { AxiosResponse } from "axios";

export default function App() {
  const [index, setIndex] = useState(0);
  const [imgUrl, setImgUrl] = useState("");
  useEffect(() => {
    console.log(`loading ${index}`);
    const source = axios.CancelToken.source();
    axios
      .get("https://dog.ceo/api/breeds/image/random", {
        cancelToken: source.token
      })
      .then((res: AxiosResponse<{ message: string; status: string }>) => {
        console.log(`${index} done`);
        setImgUrl(res.data.message);
      })
      .catch(err => {
        if (axios.isCancel(source)) {
          console.log(err.message);
        }
      });

    return () => {
      console.log(`canceling ${index}`);
      source.cancel(`canceling ${index}`);
    };
  }, [index]);

  return (
    <div>
      <button
        onClick={() => {
          setIndex(index + 1);
        }}
      >
        click
      </button>
      <div>
        <img src={imgUrl} alt="" />
      </div>
    </div>
  );
}

axios 中一个请求取消的示例

axios 中一个请求取消的示例

通过解读其源码不难实现出一个自己的版本。Here we go…

Promise 链与拦截器

这个和请求的取消其实关系不大,但不妨先来了解一下,axios 中如何组织起来一个 Promise 链(Promise chain),从而实现在请求前后可执行一个拦截器(Interceptor)的。

简单来说,通过 axios 发起的请求,可在请求前后执行一些函数,来实现特定功能,比如请求前添加一些自定义的 header,请求后进行一些数据上的统一转换等。

用法

首先,通过 axios 实例配置需要执行的拦截器:

axios.interceptors.request.use(function (config) {
    console.log('before request')
    return config;
  }, function (error) {
    return Promise.reject(error);
  });

axios.interceptors.response.use(function (response) {
    console.log('after response');
    return response;
  }, function (error) {
    return Promise.reject(error);
  });

然后每次请求前后都会打印出相应信息,拦截器生效了。

axios({
    url: "https://dog.ceo/api/breeds/image/random",
    method: "GET"
}).then(res => {
    console.log("load success");
});

下面编写一个页面,放置一个按钮,点击后发起请求,后续示例中将一直使用该页面来测试。

import React from "react";
import axios from "axios";

export default function App() {
  const sendRequest = () => {
    axios.interceptors.request.use(
      config => {
        console.log("before request");
        return config;
      },
      function(error) {
        return Promise.reject(error);
      }
    );

    axios.interceptors.response.use(
      response => {
        console.log("after response");
        return response;
      },
      function(error) {
        return Promise.reject(error);
      }
    );

    axios({
      url: "https://dog.ceo/api/breeds/image/random",
      method: "GET"
    }).then(res => {
      console.log("load success");
    });
  };
  return (
    <div>
      <button onClick={sendRequest}>click me</button>
    </div>
  );
}

点击按钮后运行结果:

before request
after response
load success

拦截器机制的实现

实现分两步走,先看请求前的拦截器。

请求前拦截器的实现

Promise 的常规用法如下:

new Promise(resolve,reject);

假如我们封装一个类似 axios 的请求库,可以这么写:

interface Config {
  url: string;
  method: "GET" | "POST";
}

function request(config: Config) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(config.method, config.url);
    xhr.onload = () => {
      resolve(xhr.responseText);
    };
    xhr.onerror = err => {
      reject(err);
    };
    xhr.send();
  });
}

除了像上面那个直接 new 一个 Promise 外,其实任意对象值都可以形成一个 Promise,方法是调用 Promise.resolve

Promise.resolve(value).then(()=>{ /**... */ });

这种方式创建 Promise 的好处是,我们可以从 config 开始,创建一个 Promise 链,在真实的请求发出前,先执行一些函数,像这样:

function request(config: Config) {
  return Promise.resolve(config)
    .then(config => {
      console.log("interceptor 1");
      return config;
    })
    .then(config => {
      console.log("interceptor 2");
      return config;
    })
    .then(config => {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open(config.method, config.url);
        xhr.onload = () => {
          resolve(xhr.responseText);
        };
        xhr.onerror = err => {
          reject(err);
        };
        xhr.send();
      });
    });
}

将前面示例中 axios 替换为我们自己写的 request 函数,示例可以正常跑起来,输出如下:

interceptor 1
interceptor 2
load success

这里,已经实现了 axios 中请求前拦截器的功能。仔细观察,上面三个 then 当中的函数,形成了一个 Promise 链,在这个链中顺次执行,每一个都可以看成一个拦截器,即使是执行发送请求的那个 then

于是我们可以将他们抽取成三个函数,每个函数就是一个拦截器

function interceptor1(config: Config) {
  console.log("interceptor 1");
  return config;
}
function interceptor2(config: Config) {
  console.log("interceptor 2");
  return config;
}

function xmlHttpRequest<T>(config: Config) {
  return new Promise<T>((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(config.method, config.url);
    xhr.onload = () => {
      resolve(xhr.responseText as any);
    };
    xhr.onerror = err => {
      reject(err);
    };
    xhr.send();
  });
}

接下来要做的,就是从 Promise 链的头部 Promise.resolve(config) 开始,将上面三个函数串起来。借助 Monkey patch 这不难实现:

function request<T = any>(config: Config) {
  let chain: Promise<any> = Promise.resolve(config);
  chain = chain.then(interceptor1);
  chain = chain.then(interceptor2);
  chain = chain.then(xmlHttpRequest);
  return chain as Promise<T>;
}

然后,将上面硬编码的写法程式化一下,就实现了任意个请求前拦截器的功能。

扩展配置,以接收拦截器:

interface Config {
  url: string;
  method: "GET" | "POST";
  interceptors?: Interceptor<Config>[];
}

创建一个数组,将执行请求的函数做为默认的元素放进去,然后将用户配置的拦截器压入数组前面,这样形成了一个拦截器的数组。最后再遍历这个数组形成 Promise 链。

function request<T = any>({ interceptors = [], ...config }: Config) {
  // 发送请求的拦截器为默认,用户配置的拦截器压入数组前面
  const tmpInterceptors: Interceptor<any>[] = [xmlHttpRequest];
  interceptors.forEach(interceptor => {
    tmpInterceptors.unshift(interceptor);
  });
  let chain: Promise<any> = Promise.resolve(config);
  tmpInterceptors.forEach(interceptor => (chain = chain.then(interceptor)));
  return chain as Promise<T>;
}

使用:

request({
    url: "https://dog.ceo/api/breeds/image/random",
    method: "GET",
    interceptors: [interceptor1, interceptor2]
}).then(res => {
    console.log("load success");
});

执行结果:

interceptor 2
interceptor 1
load success

注意这里顺序为传入的拦截器的反序,不过这不重要,可通过传递的顺序来控制。

响应后拦截器

上面实现了在请求前执行一序列拦截函数,同理,如果将拦截器压入到数组后面,即执行请求那个函数的后面,便实现了响应后的拦截器。

继续扩展配置,将请求与响应的拦截器分开:

interface Config {
  url: string;
  method: "GET" | "POST";
  interceptors?: {
    request: Interceptor<Config>[];
    response: Interceptor<any>[];
  };
}

更新 request 方法,请求前拦截器的逻辑不变,将新增的响应拦截器通过 push 压入数组后面:

function request<T = any>({
  interceptors = { request: [], response: [] },
  ...config
}: Config) {
  const tmpInterceptors: Interceptor<any>[] = [xmlHttpRequest];
  interceptors.request.forEach(interceptor => {
    tmpInterceptors.unshift(interceptor);
  });

  interceptors.response.forEach(interceptor => {
    tmpInterceptors.push(interceptor);
  });

  let chain: Promise<any> = Promise.resolve(config);
  tmpInterceptors.forEach(interceptor => (chain = chain.then(interceptor)));
  return chain as Promise<T>;
}

类似 interceptor1 interceptor2,新增两个拦截器用于响应后执行,

function interceptor3<T>(res: T) {
  console.log("interceptor 3");
  return res;
}

function interceptor4<T>(res: T) {
  console.log("interceptor 4");
  return res;
}

测试代码:

request({
    url: "https://dog.ceo/api/breeds/image/random",
    method: "GET",
    interceptors: {
    request: [interceptor1, interceptor2],
    response: [interceptor3, interceptor4]
    }
}).then(res => {
    console.log("load success");
});

运行结果:

interceptor 2
interceptor 1
interceptor 3
interceptor 4
load success

不难看出,当我们发起一次 axios 请求时,其实是发起了一次 Promise 链,链上的函数顺次执行。

request interceptor 1
request interceptor 2
...
request
response interceptor 1
response interceptor 2
...

因为拉弓没有回头箭,请求发出后,能够取消的是后续操作,而不是请求本身,所以上面的 Promise 链中,需要实现 request 之后的拦截器和后续回调的取消执行。

request interceptor 1
request interceptor 2
...
request
# 
版权声明:本文为Wayou原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/Wayou/p/axios_promise_chain_and_request_cancelation.html