“This is the first day of my participation in the First Challenge 2022. For details: First Challenge 2022”

Recently, I saw a source code interpretation article about Axios on the Internet, so I followed the article, studied the source code of Axios, and implemented mini-Axios myself.

Github:github.com/OUDUIDUI/mi…

Please click a Star if you like

Initialize the project

This project is packaged with Webpack. Let’s initialize the project:

#Initialize the
pnpm init -y

#Install webpack
pnpm i webpack-cli webpack -D
Copy the code

Then create webpack.config.js:

module.exports = {
  entry: './index.js'.output: {
    path: __dirname + '/dist/'.filename: 'axios.min.js'.library: 'axios'.libraryTarget: 'umd'.globalObject: 'this'}};Copy the code

Then create a new index.js file, click on the test code, and type webpack on the command line to pack.

Entrance to the file

First create a new lib folder under the directory as our source file path, and then create a new axios.js as the entry file.

We just created index.js in the root path, which actually exported axios.js:

// index.js
module.exports = require('./lib/axios');
Copy the code

In axios.js, the axios function is implemented.

Implement the AXIos function, which is created using a createInstance method. There are two points involved: defaultConfig, the default configuration, and the Axios class, which implements the main axios functionality.

// lib/axios
const Axios = require('./core/Axios');
const defaults = require('./defaults');

function createInstance(defaultConfig) {
  // Create an instance
  const context = new Axios(defaultConfig);
  return context;
}

/ / create axios
const axios = createInstance(defaults);

module.exports = axios;
Copy the code

The default configuration

When we create the AXIos method, we pass in a defaults object as the default configuration, which we will now implement.

Create new defaults.js in the lib directory.

// lib/defaults.js
const defaults = {};
module.exports = defaults;
Copy the code

Let’s implement the adapter first, and the rest of the configuration will come back later when needed.

What is an adapter? If you’ve ever used Axios, you know that it can be used for web requests as well as node environment requests. As we all know, on the web side we want to implement the request interface, which actually uses XMLHttpRequest, whereas in the Node environment we use HTTP or HTTPS modules.

The adapter will help you choose which modules to use in advance, depending on the environment you are using.

// lib/defaults.js

/** * get adapter */
function getDefaultAdapter() {
  let adapter;
  if (typeofXMLHttpRequest ! = ='undefined') {
    // Use XMLHttpRequest on the browser side
    adapter = require('./adapters/xhr');
  } else if (typeofprocess ! = ='undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // Node uses HTTP
    adapter = require('./adapters/http');
  }

  return adapter;
}

const defaults = {
  / / adapter
  adapter: getDefaultAdapter()
};
Copy the code

Also, instead of copying the XMLHttpRequest or HTTP modules directly to the adapter adapter, we’ll wrap them together in a factory pattern.

Create the Adapters folder under the lib folder and create xhr.js and http.js:

// xhr.js
module.exports = function xhrAdapter(config) {
  return new Promise((resolve, reject) = > {
    // TODO
  });
};

// http.js
module.exports = function httpAdapter(config) {
  return new Promise((resolve, reject) = > {
    // TODO
  });
};
Copy the code

We’ll implement them later, but let’s go back to Axios.

Axios class

The core Axios methods are implemented in the Axios class.

First, we will create a new core folder under the lib path, which will store our core code. Then create a new Axios file inside.

// lib/core/Axios.js

function Axios(instanceConfig) {
  // Save the default configuration
  this.defaults = instanceConfig;
}

module.exports = Axios;
Copy the code

One might ask why not just use ES6 classes. In fact, it is not impossible, but I will be inclined to imitate the implementation of source code, this way to find problems will also be convenient, by the way can learn about it. It depends on personal preference.

Axios has a core instance method called Request. It is used to send requests.

If you’ve ever used axios, you know that it takes two methods of passing arguments, axios(URL, options) and axios(options), so request takes two arguments. Then combine the parameters at the beginning.

// lib/core/Axios.js
Axios.prototype.request = function (configOrUrl, config) {
  // Both request(URL, options) and request(options) are supported
  if (typeof configOrUrl === 'string') {
    config = config || {};
    config.url = configOrUrl;
  } else {
    config = configOrUrl || {};
  }

  // TODO
};
Copy the code

refactoringcreateInstance

Now, if you’re careful, you’ll notice that when we create axios, it’s new Axios. To implement this, we need axios.request() to implement the request, but we used direct axios() to do just fine.

So we need to refine the createInstance method.

First we can make it clear that the axios method is essentially the axios.prototype. request method, so we can create an instance with new axios and then bind the instance to return a new method assigned to Axios.

At the same time, there are essentially other instance methods of Axios, namely instance methods of various request methods, such as axios.get() and axios.post(), which call the Request method at their core. So we also need to assign Axios’s instance methods and instance properties to Axios.

Let’s look at the implementation:

// lib/axios.js
function createInstance(defaultConfig) {
  // Create an instance
  const context = new Axios(defaultConfig);
  // Implement axios methods
  const instance = Axios.prototype.request.bind(context);

  // Bind the instance properties and instance methods of Axios
  Object.keys(context).forEach((key) = > {
    if (typeof context[key] === 'function') {
      instance[key] = context[key].bind(context);
    } else{ instance[key] = context[key]; }});return instance;
}
Copy the code

Axios.get () and other instance methods mentioned above are not implemented here, essentially calling the request method, interested friends to have a look at the source code

Let’s continue to refine the request method.

Merge options

Request then merges the defaults and the options passed in.

// lib/core/Axios.js
const mergeConfig = require('./mergeConfig');

Axios.prototype.request = function (configOrUrl, config) {
  // ...

  // Merge options
  config = mergeConfig(this.defaults, config);

  // TODO
};
Copy the code

The mergeConfig method is used here, so we’ll create a new mergeconfig.js in lib/core to implement the mergeConfig method.

The logic of merging options is pretty simple and won’t be covered here. Only the options that mini-Axios uses are listed here, some of which will be covered later.

/** * Merge processing options *@param config1
 * @param config2* /
module.exports = function mergeConfig(config1, config2) {
  config2 = config2 || {};

  const mergeMap = {
    url: config2.url || config1.url, / / interface
    method: config2.method || config1.method, // Request method
    data: config2.data || config1.data, // Request data
    params: config2.params || config1.params, // Request parameters
    adapter: config2.adapter || config1.adapter, / / adapter
    transformRequest: config2.transformRequest || config1.transformRequest, // Request data conversion
    transformResponse: config2.transformResponse || config1.transformResponse, // Response data conversion
    cancelToken: config2.cancelToken || config1.cancelToken, // Cancel the request
    validateStatus: config2.validateStatus || config1.validateStatus // Valid status code
  };

  const config = {};
  // remove the no-value option
  Object.keys(mergeMap).forEach((key) = > {
    if(mergeMap[key] ! = =undefined) { config[key] = mergeMap[key]; }});return config;
};
Copy the code

Delegate requests

Once the options are processed, it’s time to request the interface.

Instead, a method of dispatchRequest is implemented that sends the request, and then invokes it using a promise.

// lib/core/Axios.js
const dispatchRequest = require('./dispatchRequest');

Axios.prototype.request = function (configOrUrl, config) {
  // ...

  // Send the request
  let promise = Promise.resolve(config).then(dispatchRequest);

  return promise;
};
Copy the code

Now the request method is almost complete.

dispatchRequestDelegate requests

So earlier we talked about request and then we wrapped dispatchRequest with a promise, and then we made a chain call to dispatch the request. You can see from the above code that dispatchRequest receives the config parameter.

Next, let’s implement dispatchRequest. We will create a dispatchRequest.js file in lib/core.

// lib/core/dispatchRequest.js
/** * Send request *@param config* /
module.exports = function dispatchRequest(config) {
  // TODO
};
Copy the code

First we initialize headers, since subsequent request data conversions will use headers.

// lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
  config.headers = config.headers || {};

  // TODO
};
Copy the code

The next step is what we call request data conversion, which basically means unified processing of the request data, and the default handler is implemented in defaults.js.

Second, the transformRequest option is an array that is merged with the default array if passed in by the user. (EMM, I chose mergeConfig).

Here we just implement some simple data conversion.

// lib/defaults.js
const defaults = {
  // ...
  transformRequest: [
    function transformRequest(data, headers) {
      // The default header argument
      headers['Accept'] = 'application/json, text/plain, */*';

      if(! data) {return data;
      }

      // Set content-type based on the header type
      if (typeof data === 'object') {
        headers['Content-Type'] = 'application/json';
        return JSON.stringify(data);
      }
      returndata; }};Copy the code

Next, we need to use another function transformData to walk through the transformRequest array and call it one by one to transform the data.

So we create a new transformdata.js under lib/core that takes three parameters: data request data, header request header, and FNS data set.

// lib/core/transformData.js
const defaults = require('.. /defaults');

/** * Data conversion *@param Data Requests data *@param Headers Request header *@param Collection of FNS conversion methods *@return {*}* /
module.exports = function transformData(data, headers, fns) {
  const context = this || defaults;

  fns.forEach((fn) = > {
    data = fn.call(context, data, headers);
  });

  return data;
};
Copy the code

Finally we call at dispatchRequest:

// lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
  config.headers = config.headers || {};

  // Call the conversion function to process the data
  config.data = transformData.call(config, config.data, config.headers, config.transformRequest);

  // TODO
};
Copy the code

After processing the request data, we need to invoke the adapter to make the call request.

As you saw earlier when initializing the adapter, the adapter returns a promise at the end, so we can chain together the success or failure results after calling the adapter.

// lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
  // ...

  // Get the adapter
  const adapter = config.adapter || defaults.adapter;

  return adapter(config).then(
    // Successfully processed
    function (response) {
      return response;
    },
    // Error handling
    function (reason) {
      return Promise.reject(reason); }); };Copy the code

Next, let’s implement the adapter.

Adapter implementation

Let’s implement xhr.js first.

XHR adapter implementation

Most of the following implementations are familiar if you’ve ever written about Ajax:

  • Initializing the request:let request = new XMLHttpRequest();
  • Start request:request.open(method, url, async)
  • Set the request header:request.setRequestHeader
  • Listen for request status:request.onreadystatechange
  • Listen for termination requests:request.onabort
  • Listening interface error:request.onerror

Here the url to start the request needs to consider the params parameter splicing, the source code is to achieve their own splicing, I lazy to directly use the QS module (PNPM I QS).

// lib/adapters/xhr.js
/** * encapsulates XMLHttpRequest *@param config
 * @return {Promise<unknown>}* /
module.exports = function xhrAdapter(config) {
  return new Promise((resolve, reject) = > {
    const requestData = config.data;
    const requestHeaders = config.headers;

    let request = new XMLHttpRequest();

    // Start the request
    request.open(
      config.method.toUpperCase(),
      config.params ? config.url + '? ' + qs.stringify(config.params) : config.url,
      true
    );

    // Listen for request status
    request.onreadystatechange = function () {
      // Only when the request is complete (readyState === 4) will the request be processed
      if(! request || request.readyState ! = =4) {
        return;
      }

      // Note that if an XMLHttpRequest request fails, in most cases we can handle it by listening on onError,
      There is an exception, however, when the request uses the file protocol (file://). Most browsers will return a status code of 0 even if the request succeeds
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') = = =0)) {
        return;
      }

      // Asynchronous processing
      setTimeout(function onloadend() {
        if(! request)return;
        / / response headers
        const responseHeaders = request.getAllResponseHeaders();
        // Response data
        const responseData = request.response;
        const response = {
          data: responseData,
          status: request.status,
          statusText: request.statusText,
          headers: responseHeaders,
          config: config,
          request: request
        };

        // TODO response data processing
      });
    };

    // Listen for termination requests
    request.onabort = function () {
      if(! request)return;

      const error = new Error('Request aborted');
      error.code = 'ECONNABORTED';
      reject(error);

      request = null;
    };

    // An error occurred on the listening interface
    request.onerror = function () {
      reject(new Error('Network Error'));
      request = null;
    };

    // Set the request header
    if ('setRequestHeader' in request) {
      Object.keys(requestHeaders).forEach((key) = > {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
          delete requestHeaders[key];
        } else{ request.setRequestHeader(key, requestHeaders[key]); }}); }// Send the request
    request.send(requestData);
  });
};
Copy the code

The above basic functions are realized, and the rest after the request is successful response data processing, which is in the request. The onreadystatechange in listening.

Here we encapsulate a settle function to process the response uniformly and ensure that it returns in a certain format.

A core content will be implemented here, which is the status code judgment. Those of you who have used Axios will know that if a request succeeds but the status code is not 2xx or 3XX, it is returned with an error by default. This step is handled right here.

The default function to check the status code is configured in defaults.js.

// lib/defaults.js
const defaults = {
  // ...

  // A valid status code is rejected
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300; }};Copy the code

Next we implement the settle function and create a new set.js in the lib/core path.

// lib/core/settle.js
/** * Process the adapter response result *@param resolve
 * @param reject
 * @param response* /
module.exports = function settle(resolve, reject, response) {
  const validateStatus = response.config.validateStatus;
  // Verify the valid status code method
  if(! response.status || ! validateStatus || validateStatus(response.status)) { resolve(response); }else {
    // Error handling
    const error = new Error('Request failed with status code ' + response.status);
    error.response = response;
    error.toJSON = function () {
      return {
        message: this.message,
        status: this.response && this.response.status ? this.response.status : null}; }; reject(error); }};Copy the code

Next we continue to refine the code in the xhr.js adapter:

// lib/adapters/xhr.js
const settle = require('.. /core/settle');

/** * encapsulates XMLHttpRequest *@param config
 * @return {Promise<unknown>}* /
module.exports = function xhrAdapter(config) {
  return new Promise((resolve, reject) = > {
    // ...

    request.onreadystatechange = function () {
      // ...

      setTimeout(function onloadend() {
        // ...

        // Response data processing
        settle(resolve, reject, response);
      });
    };

    // ...
  });
};
Copy the code

At this time our webpage request function is basically realized.

HTTP adapter implementation

The implementation logic here is basically the same as the implementation of xhr.js, the main difference is the use of XHR and HTTP method is different, about the use of HTTP module can see the documentation, here is not to say.

So go straight to the code:

// lib/adapters/http.js
const url = require('url');
const http = require('http');
const https = require('https');
const settle = require('.. /core/settle');
const qs = require('qs');

const isHttps = /https:? /;

/** * encapsulates HTTP modules *@param config
 * @return {Promise<unknown>}* /
module.exports = function httpAdapter(config) {
  return new Promise((resolve, reject) = > {
    const data = config.data;
    const headers = config.headers;
    const headerNames = {}; // header name Mapping table

    Object.keys(headers).forEach((name) = > {
      headerNames[name.toLowerCase()] = name;
    });

    // Parse links
    const parsed = url.parse(config.url);
    const protocol = parsed.protocol || 'http:';

    // Check whether it is an HTTPS request
    const isHttpsRequest = isHttps.test(protocol);

    const options = {
      hostname: parsed.hostname,
      port: parsed.port,
      path: config.params ? parsed.path + '? ' + qs.stringify(config.params) : parsed.path,
      method: config.method.toUpperCase(),
      headers: headers
    };

    // Get the request adapter
    const transport = isHttpsRequest ? https : http;

    // Create the request
    const req = transport.request(options, function handleResponse(res) {
      if (req.aborted) return;

      const stream = res;

      const response = {
        status: res.statusCode,
        statusText: res.statusText,
        headers: res.headers,
        config: config
      };

      const responseBuffer = [];
      let totalResponseBytes = 0;
      // Respond to the listener
      stream.on('data'.(chunk) = > {
        responseBuffer.push(chunk);
        totalResponseBytes += chunk.length;
      });

      // Listen for cancellation requests
      stream.on('aborted'.() = > {
        / / destruction of flow
        stream.destroy();

        const error = new Error('Request aborted');
        error.code = 'ECONNABORTED';
        reject(error);
      });

      // Error listening
      stream.on('error'.(err) = > {
        reject(err);
      });

      // Respond to end the listening
      stream.on('end'.() = > {
        // Merge data
        response.data = responseBuffer.length === 1 ? responseBuffer[0] : Buffer.concat(responseBuffer);
        settle(resolve, reject, response);
      });
    });

    req.on('error'.(err) = > {
      reject(err);
    });

    // Send the request
    req.end(data);
  });
};
Copy the code

Response data conversion

The basic functions of the adapter are now implemented. We can test it out.

Let’s test the web first. The packaging is performed first, and the axios.min.js file is generated in dist.

You can then create a new index.html to test.

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Mini-axios Example</title>
  </head>
  <body>
    <script src=".. /dist/axios.min.js"></script>
    <script>
      axios({
        url: 'https://jsonplaceholder.typicode.com/posts'.method: 'GET'.params: {
          userId: 1
        }
      })
        .then((res) = > {
          console.log(' 'get success., res.data, res.status);
        })
        .catch((err) = > {
          console.log('get err', err);
        });

      axios({
        url: 'https://jsonplaceholder.typicode.com/posts'.method: 'POST'.data: {
          title: 'foo'.body: 'bar'.userId: 1
        },
        headers: {
          'Content-type': 'application/json'
        }
      })
        .then((res) = > {
          console.log('post success:, res.data, res.status);
        })
        .catch((err) = > {
          console.log('post err', err);
        });
    </script>
  </body>
</html>
Copy the code

Now let’s test the Node side:

const axios = require('.. /index');

axios({
  url: 'https://jsonplaceholder.typicode.com/posts'.method: 'GET'.params: {
    userId: 1
  }
})
  .then((res) = > {
    console.log(' 'get success., res.data, res.status);
  })
  .catch((err) = > {
    console.log('get err', err);
  });

axios({
  url: 'https://jsonplaceholder.typicode.com/posts'.method: 'POST'.data: {
    title: 'foo'.body: 'bar'.userId: 1
  },
  headers: {
    'Content-type': 'application/json'
  }
})
  .then((res) = > {
    console.log('post success:, res.data, res.status);
  })
  .catch((err) = > {
    console.log('post err', err);
  });
Copy the code

At this point, we find that the data we get is either a string or a Buffer, not json. So we need to do the response data conversion.

With our previous experience with request data conversion, we know that we can configure an array of response data conversion methods in defaults.js.

Next, how do we convert a string or Buffer to the DESIRED JSON by calling json.parse. So let’s implement:

// lib/defaults.js
const defaults = {
  // ...

  // The default response conversion function
  transformResponse: [
    function transformResponse(data) {
      try {
        return JSON.parse(data);
      } catch (e) {
        throwe; }}};Copy the code

According to the previous code implementation, we can know that dispatchRequest is the last place to contact the response data, so we can convert the response data at dispatchRequest.

So let’s improve the code:

// lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
  config.headers = config.headers || {};

  // Call the conversion function to process the data
  config.data = transformData.call(config, config.data, config.headers, config.transformRequest);

  // Get the adapter
  const adapter = config.adapter || defaults.adapter;

  return adapter(config).then(
    // Successfully processed
    function (response) {
      // The core of processing response data is to parse the string into an object
      response.data = transformData.call(config, response.data, response.headers, config.transformResponse);

      return response;
    },
    // Error handling
    function (reason) {
      if (reason && reason.response) {
        // The core of processing response data is to parse the string into an object
        reason.response.data = transformData.call(
          config,
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }

      return Promise.reject(reason); }); };Copy the code

Let’s take a look at the test:

At this point, the basic functionality of our Mini-Axios has been implemented.

Next, let’s implement two extensions — interceptors and cancel requests.

The interceptor

Let’s start with what the official documentation says about interceptors:

Intercepts requests or responses before they are processed by THEN or catch.

// Add request interceptor
axios.interceptors.request.use(
  function (config) {
    // What to do before sending the request
    return config;
  },
  function (error) {
    // What to do about the request error
    return Promise.reject(error); });// Add a response interceptor
axios.interceptors.response.use(
  function (response) {
    // all status codes in the 2xx range trigger this function.
    // What to do with the response data
    return response;
  },
  function (error) {
    // Any status code outside the 2xx range triggers this function.
    // Do something about the response error
    return Promise.reject(error); });Copy the code

In fact, this function is very simple to understand. And in terms of implementation, we have to sort of figure it out.

First, we need to implement the Interceptors object in Axios, which contains the Request and Response objects. They both have the same use method, which stores our interceptor methods, so we can use a class to implement it.

Next, the request interceptor needs to be triggered before the request is sent, and the response interceptor needs to be triggered before the result is returned, which we can use in conjunction with chained calls to queues and promises.

That’s it. So let’s implement PI.

First, we can create the Interceptors instance properties in the Axios class, after all, they will all be mounted to Axios later.

const InterceptorManager = require('./InterceptorManager');

// lib/core/Axios.js
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  / / the interceptor
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
Copy the code

Next we implement the InterceptorManager class. First create interceptorManager.js in lib/core.

// lib/core/InterceptorManager.js
/ / the interceptor
function InterceptorManager() {
  // Store the interceptor container
  this.handlers = [];
}

module.exports = InterceptorManager;
Copy the code

Next we implement the Use instance method, which is really just a function to store the interceptor handler passed in to the Handlers.

// lib/core/InterceptorManager.js
/** * Create an interceptor *@param fulfilled
 * @param rejected
 * @return {number}* /
InterceptorManager.prototype.use = function (fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });

  // use subscripts as id
  return this.handlers.length - 1;
};
Copy the code

We can also implement a sample method, forEach, that iterates through this.handler and calls the incoming callback.

This method is for subsequent extraction of all interceptors.

// lib/core/InterceptorManager.js
/** * traverses the call *@param fn* /
InterceptorManager.prototype.forEach = function (fn) {
  this.handlers.forEach((h) = > {
    if(h ! = =null) { fn(h); }}); };Copy the code

Finally, back to the axios.prototype. request method, we want to fire the request interceptor before sending the request, and the response interceptor before sending the request but returning the result.

So we can create a new queue, put the request interceptor first, the dispatch request in the middle, and the response interception last.

Finally we can use the promise.then().then() chain call to each.

This is a big pity, which is a pity. Then (fulfilled, rejected). Then (fulfilled, rejected).

So we can split the interceptor, such as [fulfilled, fulfilled, Fulfilled, Rejected], and call then two by two.

The problem is that if we insert dispatchRequest into the queue, then the queue length becomes an odd number, so we need to insert undefined at the end of dispatchRequest to ensure that the queue length is odd.

If it looks a little messy now, let’s go straight to the code:

// lib/core/Axios.js
Axios.prototype.request = function (configOrUrl, config) {
  // ...

  // Request interceptor queue
  const requestInterceptorChain = [];
  this.interceptors.request.forEach(function (interceptor) {
    // Put the request intercepting callback into the requestInterceptorChain in reverse order
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  const responseInterceptorChain = [];
  this.interceptors.response.forEach(function (interceptor) {
    // Place the response intercepting callback sequence into the responseInterceptorChain
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // Chain queues are used to store and manage actual requests and interceptors
  // undefined is used to keep the queue length even
  let chain = [dispatchRequest, undefined];
  // Put the request interceptor into the chain header
  Array.prototype.unshift.apply(chain, requestInterceptorChain);
  // Put the response interceptor at the end of the chain queue
  chain = chain.concat(responseInterceptorChain);

  // Initialize Promise
  let promise = Promise.resolve(config);
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};
Copy the code

Now that the interceptor is basically implemented, we can test the code:

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Mini-axios Example</title>
  </head>
  <body>
    <script src=".. /dist/axios.min.js"></script>
    <script>
      axios.interceptors.request.use(
        function (config) {
          console.log('interceptors request', config);
          return config;
        },
        function (error) {
          return Promise.reject(error); }); axios.interceptors.response.use(function (response) {
          console.log('interceptors response', response);
          return response;
        },
        function (error) {
          return Promise.reject(error); }); axios({url: 'https://jsonplaceholder.typicode.com/posts'.method: 'GET'.params: {
          userId: 1
        }
      })
        .then((res) = > {
          console.log(' 'get success., res.data, res.status);
        })
        .catch((err) = > {
          console.log('get err', err);
        });
    </script>
  </body>
</html>
Copy the code

const axios = require('.. /index');

axios.interceptors.request.use(
  function (config) {
    console.log('interceptors request', config);
    return config;
  },
  function (error) {
    return Promise.reject(error); }); axios.interceptors.response.use(function (response) {
    console.log('interceptors response', response);
    return response;
  },
  function (error) {
    return Promise.reject(error); }); axios({url: 'https://jsonplaceholder.typicode.com/posts'.method: 'GET'.params: {
    userId: 1
  }
})
  .then((res) = > {
    console.log(' 'get success., res.data, res.status);
  })
  .catch((err) = > {
    console.log('get err', err);
  });
Copy the code

Cancel the request

Axios implements cancellation requests in two ways:

/ / method
const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // The executor function takes a cancel function as an argumentcancel = c; })});// Cancel the request
cancel();
Copy the code
/ / method 2
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
});

// Cancel the request (the message argument is optional)
source.cancel('Operation canceled by the user.');
Copy the code

The core of both methods is to use the CancelToken class, then create an example binding to the CancelToken option in the Options option, then get the cancel function, and then call the Cancel function whenever you want to cancel the request.

The difference between method 1 and Method 2 is that method 1 is more suitable for setting different cancellation functions for different requests, so as to cancel corresponding requests more accurately. Method two is more suitable for cancelling all requests through a cancellation function, since they all use the same cancelToken.

Although the usage is different, the implementation is basically the same, except that the source method of method 2 does the operation of method 1 ahead of time.

Without further ado, let’s make it happen.

First we create a cancel path in lib and then a canceltoken.js. Next we initialize the CancelToken class, which receives an executor function.

// lib/cancel/CancelToken.js
function CancelToken(executor) {
  // TODO
}

module.exports = CancelToken;
Copy the code

Then we can go back and mount it on axios properties.

// lib/axios.js
const CancelToken = require('./cancel/CancelToken');

axios.CancelToken = CancelToken;
Copy the code

With the initialization done, we continue to complete the CancelToken function.

From the previous two examples, we saw that the Executor function receives a cancel function that takes a message cancellation reason.

So let’s implement:

// lib/cancel/CancelToken.js
function CancelToken(executor) {
  executor(function cancel(message) {
    // TODO
  });
}
Copy the code

In the axios source code, CancelToken has a Reason instance attribute, which stores the cancellation reason and determines whether the user has executed the cancellation function.

Furthermore, instead of assigning message directly to the Reason instance property, AixOS uses a Cancel class to create an instance, passes message, and then instantiates the assignment to Reason.

We can do that first, and then we can talk about why and how. Now lib/cancel creates a new cancer.js.

// lib/cancel/Cancel.js
function Cancel(message) {
  this.message = message;
}

module.exports = Cancel;
Copy the code

As you can see, the Cancel class is simple, but why bother creating a new class?

For those of you who are familiar with the Error class, every time we get an err, we print err.message to see what the Error message is.

So Axios is based on this idea. After all, we reject a request, reject(err), or throw err returns an error. There aren’t too many wrong requests to Cancel, so we can create a Cancel and then a Reason instance. When canceling a request, we can add reject(Reason) or throw Reason.

So we continue to refine CancelToken.

// lib/cancel/CancelToken.js
function CancelToken(executor) {
  const token = this;
  executor(function cancel(message) {
    // If there is an instance with reason, it is already cancelled
    if (token.reason) return;

    token.reason = new Cancel(message);
    // TODO
  });
}
Copy the code

All that remains is the implementation of the cancel request.

The CancelToken instance will first have an array of _listeners that store the CancelToken cancellation function bound to the CancelToken request. So we just need to traverse the _Listeners array and call cancellation functions one by one.

Here will use an asynchronous implementation, it can ensure that cancel the request to cancel the function has been deposited in the _listreners object, because this step is basically in the adapter operations, and through the front can be issued with the request in an asynchronous queue, so we need to put the cancel action also in asynchronous queue.

So we can create an instance of CancelToken, mount a promise on the instance, and then implement the cancellation while we expose resolve. So when we call cancel, we trigger the exposed resolve, which in turn triggers promise.then.

// lib/cancel/CancelToken.js
function CancelToken(executor) {
  let resolvePromise;

  // A Promise will be mounted on the instance when instantiated
  // The promise's resolve callback is exposed to the external method executor
  this.promise = new Promise((resolve) = > {
    resolvePromise = resolve;
  });

  const token = this;

  this.promise.then((cancel) = > {
    if(! token._listeners)return;

    for (let i = 0; i < token._listeners.length; i++) {
      token._listeners[i](cancel);
    }
    token._listeners = null;
  });

  executor(function cancel(message) {
    // If there is an instance with reason, it is already cancelled
    if (token.reason) {
      return;
    }

    token.reason = new Cancel(message);
    // Executing resolvePromise calls the this.promise.then method directly
    resolvePromise(token.reason);
  });
}
Copy the code

The listeners and unlisteners are simply inserted into the _listeners array and removed from the _listeners array.

// lib/cancel/CancelToken.js

/** * Subscribe unsubscribe request function *@param listener* /
CancelToken.prototype.subscribe = function (listener) {
  // If this.reason is present, the request has already been cancelled
  // Cancel immediately
  if (this.reason) {
    listener(this.reason);
    return;
  }

  // Stored on this._listeners
  if (this._listeners) {
    this._listeners.push(listener);
  } else {
    this._listeners = [listener]; }};/** * Unsubscribe, delete cancel request function *@param listener* /
CancelToken.prototype.unsubscribe = function (listener) {
  if (!this._listeners) return;

  const index = this._listeners.indexOf(listener);
  if(index ! = = -1) {}this._listeners.splice(index, 1);
};
Copy the code

Next, let’s go to the XHR adapter and implement the cancel request function. With XMLHttpRequest, you can cancel the request simply by calling abort().

Next comes subscription and unsubscription. Subscribe, no need to say, as soon as you initialize it. Unsubscribe, but after the request ends, we have to unsubscribe, so we can wrap a done method and call it when we finally process the response.

// lib/adapters/xhr.js
const settle = require('.. /core/settle');
const Cancel = require('.. /cancel/Cancel');

/** * encapsulates XMLHttpRequest *@param config
 * @return {Promise<unknown>}* /
module.exports = function xhrAdapter(config) {
  return new Promise((resolve, reject) = > {
    // ...

    // Cancel the request
    let onCanceled;

    // Request finalization handler
    function done() {
      if(config.cancelToken) { config.cancelToken.unsubscribe(onCanceled); }}// ...

    // Listen for request status
    request.onreadystatechange = function () {
      // ...

        // Process the response
        settle(
          (value) = > {
            resolve(value);
            done();
          },
          (err) = > {
            reject(err);
            done();
          },
          response
        );
    };

    // ...

    // Process the cancellation request
    if (config.cancelToken) {
      onCanceled = function (cancel) {
        reject(cancel || new Cancel('canceled'));
        request.abort();
        request = null;
      };

      config.cancelToken.subscribe(onCanceled);
    }

    // ...
  });
};
Copy the code

The same is true for HTTP adapters, which I won’t go into here.

// lib/adpters/http.js
const settle = require('.. /core/settle');
const Cancel = require('.. /cancel/Cancel');

/** * encapsulates HTTP modules *@param config
 * @return {Promise<unknown>}* /
module.exports = function httpAdapter(config) {
  return new Promise((resolvePromise, rejectPromise) = > {
    let onCanceled;

    // Request finalization handler
    function done() {
      if(config.cancelToken) { config.cancelToken.unsubscribe(onCanceled); }}Wrap the resolve function
    const resolve = function (value) {
      done();
      resolvePromise(value);
    };

    Wrap the reject function
    const reject = function (value) {
      done();
      rejectPromise(value);
    };

    // ...

    // Create the request
    const req = transport.request(options, function handleResponse(res) {
      // ...

      // Respond to end the listening
      stream.on('end'.() = > {
        // ...

        settle(resolve, reject, response);
      });
    });

    // ...

    // Initialize the cancel function
    if (config.cancelToken) {
      onCanceled = function (cancel) {
        // If the request has already been cancelled, there is no need to call it again
        if (req.aborted) return;

        // Call the cancel request
        req.abort();
        reject(cancel || new Cancel('canceled'));
      };

      // Subscribe unsubscribe function
      config.cancelToken.subscribe(onCanceled);
    }

    // ...
  });
};
Copy the code

Now the basic function of canceling the request has been implemented.

In the AXIos source code, it also checks for cancellation before and after the request is processed. We can also try it out with the implementation.

The basic are in dispatchRequest said above, we implement a throwIfCancellationRequested function, and then determine whether the configuration have cancelToken selected items, in any instance to determine whether there is a tiny instance attributes, Throw Reason () : throw reason (); throw reason (); throw reason (); throw reason ();

// lib/core/dispatchRequest.js

function throwIfCancellationRequested(config) {
  if(config.cancelToken) { config.cancelToken.throwIfRequest(); }}/** * Send request *@param config* /
module.exports = function dispatchRequest(config) {
  // Check whether the cancellation action has been triggered
  throwIfCancellationRequested(config);

  // ...

  return adapter(config).then(
    // Successfully processed
    function (response) {
      // Check whether the cancellation action has been triggered
      throwIfCancellationRequested(config);

      // ...
    },
    // Error handling
    function (reason) {
      // Check whether the cancellation action has been triggered
      throwIfCancellationRequested(config);

      // ...}); };Copy the code
// lib/cancel/CancelToken.js
/** ** cancel */ before request
CancelToken.prototype.throwIfRequest = function () {
  if (this.reason) {
    throw this.reason; }};Copy the code

Finally, we implement canceltoken.source. That is, the implementation of the second cancel method, in fact, is to move the code in method one, and then return the instance and cancel function processing.

// lib/cancel/CancelToken.js
CancelToken.source = function () {
  let cancel;
  const token = new CancelToken(function (c) {
    cancel = c;
  });
  return {
    token: token, / / CancelToken instance
    cancel: cancel // Cancel the function
  };
};
Copy the code

And finally, finally, let’s test this out.

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Mini-axios Example</title>
  </head>
  <body>
    <script src=".. /dist/axios.min.js"></script>
    <script>
      let cancel;
      axios({
        url: 'https://jsonplaceholder.typicode.com/todos/1'.method: 'GET'.cancelToken: new axios.CancelToken(function executor(c) {
          // The executor function takes a cancel function as an argument
          cancel = c;
        })
      })
        .then((res) = > {
          console.log(' 'get success., res.data, res.status);
        })
        .catch((err) = > {
          console.log('get err', err);
        });

      setTimeout(() = > {
        cancel('Cancel request');
      });
    </script>
  </body>
</html>
Copy the code

const axios = require('.. /index');

let cancel;

axios({
  url: 'https://jsonplaceholder.typicode.com/todos/1'.method: 'GET'.cancelToken: new axios.CancelToken(function executor(c) {
    // The executor function takes a cancel function as an argument
    cancel = c;
  })
})
  .then((res) = > {
    console.log(' 'get success., res.data, res.status);
  })
  .catch((err) = > {
    console.log('get err', err);
  });

setTimeout(() = > {
  cancel('Cancel request');
});
Copy the code

The test was successful, and our Mini-Axios was implemented.

Github:github.com/OUDUIDUI/mi…

Please click a Star if you like