Categories
Uncategorized

Cancel learned from axios source code Promise chains with requests

One example of axios canceled request:

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 example a cancellation request in

It is not difficult to realize a version of their own by reading its source code. Here we go …

Promise chains with interceptors

Cancel this request and in fact has little to do, but first take a look at, axios how to organize a chain of Promise (Promise chain), enabling perform an interceptor (Interceptor) before and after the request.

Briefly, by requesting axios initiated the request may be performed before or after a number of functions to achieve a particular function, such as add a custom header of the request before, some unified data conversion on the request.

usage

First, configure the interceptor to be executed by axios example:

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);
  });

Before and after each request will then print out the appropriate information, the interceptor into effect.

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

A page write the following, placing a button click initiates the request, in the subsequent examples have been used to test the page.

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 mebutton>
    div>
  );
}

Click on the button operating results:

before request
after response
load success

Achieve interceptor mechanism

To achieve a two-step, look at the interceptor prior to the request.

That request before the interceptor

Promise conventional usage is as follows:

new Promise(resolve,reject);

If we can encapsulate a similar request axios library, you can write:

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();
  });
}

In addition to the above, as a new direct Promise, in fact, the value of any object can form a Promise, by calling Promise.resolve,

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

Promise benefit of creating this way, we can start from the config, create a Promise chain, before the real request is made to perform some functions, like this:

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();
      });
    });
}

The previous example axios replaced with our own written request function, examples can run up to normal, the output is as follows:

interceptor 1
interceptor 2
load success

Here, the function axios has been achieved in the previous request interceptors. Careful observation, then among the above three functions, the formation of a chain Promise, are sequentially performed in the chain, each of which can be seen as a blocker, then even if the transmission execution request.

So we can extract them into three functions, each function is a blocker.

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();
  });
}

The next thing, is from the head Promise.resolve Promise chain (config) starts, the above three functions string together. With Monkey patch it is not difficult to achieve:

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>;
}

Then, the hard-coded above wording stylized look, we realized before any request interceptor function.

Extended configuration, to receive blockers:

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

Create an array, a function execution request elements into them as the default, then the user profile is pressed into the front interceptor array, thus forming an array of interceptors. Finally traverse the array formed Promise chain.

function request<T = any>({ interceptors = [], ...config }: Config) {
  

// sends a request to interceptor default user profile is pressed into the front of the interceptor array

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>; }

use:

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

Results of the:

interceptor 2
interceptor 1
load success

Note that the order of incoming interceptor reverse order, but this is not important, can be controlled by passing sequentially.

After the response interceptor

The above implements a sequence of functions performed in intercepting a request before the same token, if the interceptor is pressed into the back of the array, i.e., behind the function execution request, the response will be achieved after the interceptor.

Configuration continues to expand, the interceptor separate request and response:

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

The method of updating request, the request interceptor front same logic, in response to the new interceptor by press-fitting push back the array:

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>;
}

Similarly interceptor1 interceptor2, two new interceptors in response to the execution,

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

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

Test code:

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

operation result:

interceptor 2
interceptor 1
interceptor 3
interceptor 4
load success

Easy to see, when we launch a axios request, in fact, launched a Promise chain, the chain function is performed sequentially.

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

Since there is no turning back the bow, the request is made, the subsequent operation can be canceled, but not the request itself, so the above Promise chain, need to implement the interceptor and the subsequent cancellation performed after the callback request.

request interceptor 1
request interceptor 2
...
request
# ? 后续操作不再执行
response interceptor 1
response interceptor 2
...

The cancellation request

Interrupt the chain of Promise

Promise interrupt the chain of execution, an exception can be achieved by throw.

Adding an intermediate function, the function execution request encapsulates, whether successful or not, the subsequent execution of the interrupt exception is thrown.

function adapter(config: Config) {
  return xmlHttpRequest(config).then(
    res => {
      throw "baddie!";
    },
    err => {
      throw "baddie!";
    }
  );
}

Update request function using the adapter instead of directly using xmlHttpRequest:

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

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

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

Execute its output again:

interceptor 2
interceptor 1
Uncaught (in promise) baddie!

That request cancellation

According to the realization of ideas axios, to achieve the cancellation request, we need to create a token, by which token can call a cancel method; for token checks when initiating the request by the token passed to the configuration to determine whether the token whether executed canceled if it is the use of the above ideas, the Promise broken chain.

Construction token

It is easy to see, where the token, at least:

    There is a cancel method

    Whether there is a field recording cancel method is called too

Additionally,

    If there is a reason to cancel the recording field, then great.

From this we get such a class:

class CancelTokenSource {
  private _canceled = false;
  get canceled() {
    return this._canceled;
  }
  private _message = "unknown reason";
  get message() {
    return this._message;
  }

  cancel(reason?: string) {
    if (this.canceled) return;
    if (reason) {
      this._message = reason;
    }
    this._canceled = true;
  }
}

Add a token to the configuration

Extended configuration, to receive a token, to cancel:

interface Config {
  url: string;
  method: "GET" | "POST";
+  cancelToken?: CancelTokenSource;
  interceptors?: {
    request: Interceptor[];
    response: Interceptor[];
  };
}

Cancel request processing logic

At the same time update xmlHttpRequest function call to determine whether the state had canceled token, if the call is xhr.abort (), while adding onabort callback to reject out Promise:

function xmlHttpRequest(config: Config) {
  return new Promise((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.onabort = () => {
+      reject();
+    };
+    if (config.cancelToken) {
+      xhr.abort();
+    }
    xhr.send();
  });
}

Call canceled

The throw exception code at a plurality of extraction as a method to invoke, the adapter update logic, reject and return to normal without cancellation.

function throwIfCancelRequested(config: Config) {
  if (config.cancelToken && config.cancelToken.canceled) {
    throw config.cancelToken.message;
  }
}

function adapter(config: Config) {
  throwIfCancelRequested(config);
  return xmlHttpRequest(config).then(
    res => {
      throwIfCancelRequested(config);
      return res;
    },
    err => {
      throwIfCancelRequested(config);
      return Promise.reject(err);
    }
  );
}

Cancel the test request

Everything seems okay, the next wave of tests. The following code expects per-click button to initiate a request, the request before the previous request to cancel. In order to distinguish each different requests, add index variable, increment when the button is clicked.

import React, { useEffect, useState } from "react";

export default function App() {
  const [index, setIndex] = useState(0);

  useEffect(() => {
    const token = new CancelTokenSource();
    request({
      url: "https://dog.ceo/api/breeds/image/random",
      method: "GET",
      cancelToken: token,
      interceptors: {
        request: [interceptor1, interceptor2],
        response: [interceptor3, interceptor4]
      }
    })
      .then(res => {
        console.log(`load ${index} success`);
      })
      .catch(err => {
        console.log("outer catch ", err);
      });

    return () => {
      token.cancel(`just cancel ${index}`);
    };
  }, [index]);

  return (
    <div>
      <button
        onClick={() => {
          setIndex(index + 1);
        }}
      >
        click me
      </button>
    </div>
  );
}

Load test page, useEffect will first run after the page is loaded, it will trigger a full request process. Then click on the button twice in a row to cancel before the first two. operation result:

interceptor 2
interceptor 1
interceptor 3
interceptor 4
load 0 success

interceptor 2
interceptor 1

interceptor 2
interceptor 1
outer catch  just cancel 1
interceptor 3
interceptor 4
load 2 success

Implementation of existing problems

From the output perspective,

    The first part is the first time the request is a normal request.

    The second part is the implementation of the first click of the request interceptor.

    The third part is the second click, the first request was canceled, and then complete a full request.

And output requests from the network point of view, there are two problems:

    xhr.abort () does not take effect two consecutive clicks, the browser debugging tools will be two status request 200.

    The first request subsequent pullback indeed be canceled, but it was after waiting request is successful, the success callback is canceled, this can be canceled by adding a flag in function to view.

function throwIfCancelRequested(config: Config, flag?: number) {
  if (config.cancelToken && config.cancelToken.canceled) {
    console.log(flag);
    throw config.cancelToken.message;
  }
}

function adapter(config: Config) {
  throwIfCancelRequested(config, 1);
  return xmlHttpRequest(config).then(
    res => {
    

// ℹ subsequent output has proved that actually takes effect is here

throwIfCancelRequested(config, 2); return res; }, err => {

// ℹ rather than here, even if the action is to cancel the request process

throwIfCancelRequested(config, 3); return Promise.reject(err); } ); }

Output:

interceptor 2
interceptor 1
interceptor 2
interceptor 1
2
outer catch  just cancel 1
interceptor 3
interceptor 4
load 2 success

optimization

The following optimization required to solve the above problems. The method used is axios logic, but also a place to start looking at the source code will not quite understand.

In fact, external call cancel () the timing is uncertain, so the token on the object record field if it is canceled, when it is set to true is uncertain, therefore, we request the cancellation logic (xhr.abort ()) should It is a Promise in to complete.

Therefore, in CancelTokenSource class, create a Promise types of fields, it will fall to resolve when cancel () method is called.

The updated CancelTokenSource categories:

class CancelTokenSource {
  public promise: Promise<unknown>;
  private resolvePromise!: (value?: any) => void;
  constructor() {
    this.promise = new Promise(resolve => {
      this.resolvePromise = resolve;
    });
  }
  private _canceled = false;
  get canceled() {
    return this._canceled;
  }
  private _message = "unknown reason";
  get message() {
    return this._message;
  }

  cancel(reason?: string) {
    if (reason) {
      this._message = reason;
    }
    this._canceled = true;
    this.resolvePromise();
  }
}

Update logic field visit canceled after:

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.onabort = () => {
      reject();
    };
    if (config.cancelToken) {
      config.cancelToken.promise.then(() => {
        xhr.abort();
      });
    }
    xhr.send();
  });
}

After tests optimized version

Output:

interceptor 2
interceptor 1
interceptor 3
interceptor 4
load 0 success

interceptor 2
interceptor 1

interceptor 2
3
interceptor 1
outer catch  just cancel 1
interceptor 3
interceptor 4
load 2 success

Web browser debugging tools out there will be a rosy be abort requests, while the above output (where entry into force is 3 instead of 2) show the request being canceled out rightly reject.

The complete code

自己实现的请求取消机制完整代码

import React, { useState, useEffect } from "react";

class CancelTokenSource {
  public promise: Promise<unknown>;
  private resolvePromise!: (value?: any) => void;
  constructor() {
    this.promise = new Promise(resolve => {
      this.resolvePromise = resolve;
    });
  }
  private _canceled = false;
  get canceled() {
    return this._canceled;
  }
  private _message = "unknown reason";
  get message() {
    return this._message;
  }

  cancel(reason?: string) {
    if (reason) {
      this._message = reason;
    }
    this._canceled = true;
    this.resolvePromise();
  }
}

type Interceptor<T> = (value: T) => T | Promise<T>;

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

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

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

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

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.onabort = () => {
      reject();
    };
    if (config.cancelToken) {
      config.cancelToken.promise.then(() => {
        xhr.abort();
      });
    }
    xhr.send();
  });
}

function throwIfCancelRequested(config: Config, flag?: number) {
  if (config.cancelToken && config.cancelToken.canceled) {
    console.log(flag);
    throw config.cancelToken.message;
  }
}

function adapter(config: Config) {
  throwIfCancelRequested(config, 1);
  return xmlHttpRequest(config).then(
    res => {
      throwIfCancelRequested(config, 2);
      return res;
    },
    err => {
      throwIfCancelRequested(config, 3);
      return Promise.reject(err);
    }
  );
}

function request<T = any>({
  interceptors = { request: [], response: [] },
  ...config
}: Config) {
  const tmpInterceptors: Interceptor<any>[] = [adapter];
  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>;
}

export default function App() {
  const [index, setIndex] = useState(0);

  useEffect(() => {
    const token = new CancelTokenSource();
    request({
      url: "https://dog.ceo/api/breeds/image/random",
      method: "GET",
      cancelToken: token,
      interceptors: {
        request: [interceptor1, interceptor2],
        response: [interceptor3, interceptor4]
      }
    })
      .then(res => {
        console.log(`load ${index} success`);
      })
      .catch(err => {
        console.log("outer catch ", err);
      });

    return () => {
      token.cancel(`just cancel ${index}`);
    };
  }, [index]);

  return (
    <div>
      <button
        onClick={() => {
          setIndex(index + 1);
        }}
      >
        click me
      button>
    div>
  );
}

running result

related resources

  • axios

Leave a Reply