preface

Axios is a very small HTTP library, but it contains a lot of programming ideas worth learning. Let’s learn it today.

This article will be divided into the following sections to learn the AXIOS library step by step:

  1. axiosIntroduction to the
  2. axiosEngineering structures,
  3. axiosThe source code to write
  4. axiosPackaging releases

If learning this article has helped you, please give a thumbs-up.

Axios profile

Axios is a Promise-based HTTP library that can be used in browsers and Node.js.

features

  • Created from the browserXMLHttpRequests, fromnode.jscreatehttpRequests;
  • supportPromise API ;
  • Intercepting requests and responses;
  • Transform request data and response data;
  • Cancel the request;
  • Automatic conversionJSONData;
  • The client supports defenseXSRF 。

Axios engineering construction

Now I start to build my own project.

ES6+ syntax and Webpack packaging.

Initialization project:

1Create the lion-axios project on git2Git clone HTTPS://github.com/shiyou00/lion-axios.git
3Go to CD lion-axios/4Initialize project NPM init-yCopy the code

Webpack configuration:

Development environment configuration file: webpack.dev.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  mode: 'development'.// Development environment
  devtool: 'cheap-module-eval-source-map'.// sourceMap is used for error debugging
  devServer: {
    contentBase: './example'.// The server boot root is set to example
    open: true.// Automatically open the browser
    port: 8088./ / the port number
    hot: true / / open hot update, at the same time to configure the corresponding plug-in HotModuleReplacementPlugin
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html'.// Use a template file to view the effect
      inject:'head' // Insert into the head tag
    }),
    new webpack.HotModuleReplacementPlugin() // Hot update plugin]};Copy the code

Production environment configuration file: webpack.pro.js

module.exports = {
  mode: 'production' // Production environment
};
Copy the code

Common configuration file: webpack.common.js

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { merge } = require('webpack-merge');
const prodConfig = require("./webpack.pro"); // Import the production configuration
const devConfig = require("./webpack.dev"); // Import the development environment configuration

// Public configuration
const commonConfig = {
  entry: './src/axios.js'.// Package the entry file
  output: {
    filename: 'axios.js'.// The output file name
    path: path.resolve(__dirname, 'dist'), // The absolute path to the output
    library: 'axios'.// The class library namespace, if introduced via a web page, can be accessed via window.axios
    globalObject: 'this'.// Define global variables, compatible with Node and browser running, avoid the "window is not defined" situation
    libraryTarget: "umd".Universal Module Definition is supported in CommonJS, AMD, and global variables
    libraryExport: 'default' // If you expose the default attribute, you can call the default attribute directly
  },
  module: {rules: [// Configure the parsing of Babel, and have the Babel configuration file in the project directory
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader"}},plugins: [
    new CleanWebpackPlugin(), // Clean the export (dist) folder every time you pack]};module.exports = (env) = >{
  // Determine the development environment or production environment according to the command to enable different configuration files
  if(env && env.production){
    return merge(commonConfig,prodConfig);
  }else{
    returnmerge(commonConfig,devConfig); }}Copy the code

Babelrc configuration:

{
  "plugins": [["@babel/plugin-proposal-class-properties",
      {
        "loose": true}], ["@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false."corejs": 2."helpers": true."regenerator": true."useESModules": false}]],"presets": [["@babel/preset-env"]]}Copy the code

Package. json add script command:

"scripts": {
  "dev": "webpack-dev-server --env.development --config webpack.common.js"."build": "webpack --env.production --config webpack.common.js"
},
Copy the code
  • Development-time executionnpm run devCommand;
  • Execution at package timenpm run buildCommand.

At this point, the basic configuration of the AXIos library project is ready.

The code quality

The above configuration ensures that we can properly package the project as a class library, but for enterprise projects, code quality control is also very important.

This project will use the following tools to control code quality:

  • editorConfigHelps maintain across multiple editors andIDEA consistent coding style for multiple developers working on the same project.
  • ESLint, it can be assembledJavaScript 和 JSXCheck tools.
  • husky ,gitThe commandhookDedicated configuration, which can be configured for executiongit commitEtc command to execute the hook.
  • lint-staged, can be in a specificgitPhases execute specific commands.
  • prettier, code unified formatting.
  • commitlint , git commit messageSpecification.

editorConfig

Editorconfig configuration:

root = true // Root configuration


[*] // Apply to all files
end_of_line = lf / / define a newline [lf | cr | CRLF]
insert_final_newline = true // Whether the file ends with a blank line

[*.{js,html,css}] // Function file
charset = utf-8 // Encoding format

[*.js] // Function file
indent_style = space // Indent type
indent_size = 2 // Indent size
Copy the code

ESLint

# installation NPM install eslint - D | | yarn add eslint - D # initialization eslint. / node_modules/bin/eslint - init// You can do it step by step

Copy the code

. Eslintrc. Js configuration:

module.exports = {
  env: { The env keyword specifies the environment you want to enable
    browser: true.es2021: true.node: true,},parser: "babel-eslint"./ / the parser
  extends: "eslint:recommended".// Inherited configuration rule set
  parserOptions: {
    ecmaVersion: 12.// Specify the version of ECMAScript you want to use
    sourceType: "module"./ / enable ESModule
  },
  rules: { // Rule "off" = off" WARN "= warning "error" = error
    strict: "off".// Strict mode, rule closed
    "no-console": "off".// Disable the console object method. The rule is closed
    "global-require": "off".// Require require() to appear in the top-level module scope, rule closed
    "require-yield": "off".// Require that generator functions contain yield and the rule is closed}};Copy the code

husky + lint-staged + prettier

Installation:

NPM install husky lint-staged prettier --save-dev Or YARN Add husky Lint-staged prettier --devCopy the code

Huskyrc configuration:

{
  "hooks": {
    "pre-commit": "lint-staged"}}Copy the code

This configuration means lint-staged execution before committing.

Lintstagedrc configuration:

{
  "*.{js,ts,jsx,tsx}": [
    "eslint --fix --quiet".// fix = automatically fixes, quiet = ESLint reports an error
    "prettier --write" // Format using prettier]."*.css": "prettier --write"."*.html": "prettier --write"."*.md": "prettier --write"."*.json": "prettier --write"
}
Copy the code

This configuration represents a lint-staged command set.

commitlint

Installation:

NPM install @commitlint/config-conventional @commitlint/cli --save-dev or YARN add @commitlint/config-conventional @commitlint/cli --devCopy the code

Commitlint. Config. Js configuration:

module.exports = {
  extends: ['@commitlint/config-conventional']};Copy the code

.huskyrc add configuration:

{
  "hooks": {+"commit-msg": "commitlint -E HUSKY_GIT_PARAMS".// CommitLint checks commit Message
    "pre-commit": "lint-staged"}}Copy the code

With everything configured, we can test this by adding the following code to axios.js:

const a = 12;
console.log(a);
Copy the code

Add. && git commit -m “test”

As you can see, the project does ESLint and CommitLint checks. The commit message is not written properly, so we can commit to the Git repository as usual.

Project directory structure

├ ─ ─ the README, md// Documentation├ ─ ─ node_modules// Dependency package folder├ ─ ─ package. Json ├ ─ ─. Babelrc// Babel configuration file├ ─ ─ commitlint. Config. Js// Commitlint configuration file├ ─ ─ editorconfig// editorConfig configuration file├ ─ ─ eslintrc. Js// ESLint configuration files├ ─ ─ huskyrc// Husky configuration file├ ─ ─ lintstagedrc// Lint-staged configuration files├ ─ ─ webpack.com mon. Js// Webpack common configuration├ ─ ─ webpack. Dev. Js// WebPack development environment configuration├ ─ ─ webpack. Pro. Js// WebPack production environment configuration├ ─ ─ gitignore// Git upload ignores configuration└ ─ ─ the SRC └ ─ ─ axios. Js// Library entry file
Copy the code

Click here to see the complete code for this summary

Axios source code

Many of axios’s design ideas are worth referencing, which is one of the main purposes of our handwritten Axios library.

Axios infrastructure

The first step in writing Axios is to set up a simple shelf. And then refine it step by step.

The axios library is called:

Get (POST, PUT, DELETE, etc.);'/user? ID=12345')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error); }); # call the axios request method to send the request axios.request({url:"/user? ID=12345"}) # You can create the request axios({method: 'post'.url: '/user/12345'.data: {
    firstName: 'Fred'.lastName: 'Flintstone'}}); You can create a new axiOS instance using a custom configurationconst instance = axios.create({
  baseURL: 'https://some-domain.com/api/'.timeout: 1000.headers: {'X-Custom-Header': 'foobar'}});Copy the code
  1. callaxiosgetMethod to send the request, it’s not hard to imagine that it hasPOST.PUT.DELETEHTTPProtocol supported methods.
  2. callaxiosrequestMethod to send a request.
  3. Can be accessed throughaxiosPass the configuration to create the request.
  4. You can create a new one using custom configurationaxiosInstance.

With these uses, we first build a Lion-Axios infrastructure that supports this use.

Create: SRC/axios. Js

Create the Axios class

class Axios {
  // Used to store configuration information.
  config = {};
  constructor(initConfig) {
  // A configuration message is received at instantiation time and saved to the config property.
    this.config = initConfig;
  }
  // This class has a request method, which can be used to send requests
  request(config){}}Copy the code

2, createInstance method

Use to instantiate the Axios class.

function createInstance(initConfig) {
  // Create an Axios instance
  const context = new Axios(initConfig);
  // The instance variable holds the request method on the Axios class and replaces this with the object instantiated in the previous step.
  const instance = Axios.prototype.request.bind(context);
  // Return the request method
  return instance;
}
Copy the code

3. Provide axiOS instances externally

Git is the default parameter object if the user does not pass method
const defaults = {
  method: 'get',}const axios = createInstance(defaults);

export default axios;
Copy the code

Use Axios

Axios:

  1. axios = createInstance(defaults);
  2. createInstance = Axios.prototype.request = request(config){} 

So it supports axios({… }); That’s called.

Support the axios.create method

axios.create = function (config) {
  // merge the default configuration with the configuration passed in by the user as createInstance.
  // Config merge is currently not implemented. Therefore, pass default directly to create the instance.
  // const initConfig = mergeConfig(config,defaults);
  return createInstance(defaults);
}
Copy the code

The create method supports passing in a configuration object to create a new Axios instance.

Calling axios is now supported like this:

constinstance = axios.create({... });Copy the code

6, support axios. Request and axios. Get | axios. Post | axios. Put the form such as call

New Axios class first get | post | delete etc

class Axios{
  config = {};
  constructor(initConfig) {
    this.config = initConfig;
  }
  request(config){}
  get(){}
  delete(){}
  head(){}
  options(){}
  post(){}
  put(){}
  patch(){}}Copy the code

Instance inherits the methods and attributes of the Axios class

Extend extends methods and attributes from Axios instances:

function extend(to, from, ctx) {
  // Inheritance method
  Object.getOwnPropertyNames( from ).forEach((key) = >{
    to[key] = from[key].bind(ctx);
  });
  // CTX inherits its own properties (not the properties on the prototype chain, so hasOwnProperty is required to determine)
  for(let val in ctx){
    if(ctx.hasOwnProperty(val)){ to[val] = ctx[val]; }}return to;
}
Copy the code

There is a pit for class instance method inheritance, see ES6 Iterate over class methods for details.

Call extend in createInstance:

function createInstance(initConfig) {
  const context = new Axios(initConfig);

  const instance = Axios.prototype.request.bind(context);
  // if you want to call axios.request and axios.get
  // Instance can inherit request, get, post, etc from context.
  extend(instance, context);	

  return instance;
}

Copy the code

Thus instance inherits the properties and methods defined on the Axios class. Now you can call it like this:

axios.get('/user? ID=12345')

axios.request({url:"/user? ID=12345"})
Copy the code

So far all four invocation methods have been supported. And the basic framework for the AXIos library is in place.

Click here to see the complete code for this summary

Send a real request

We can already call Axios in various ways, but that’s just calling it, not really sending a request to the background.

When we use axios({… }) and axios.request({}) are actually calling the request method on the axios class. Then we ask it to do the tasks in the background.

XHR

To send a request to the background on the browser side, you can use fetch or XMLHttpRequest. Since FETCH is not yet fully fledged, it does not even support canceling requests. So we select XMLHttpRequest on the browser side.

XMLHttpRequest

The XMLHttpRequest (XHR) object is used to interact with the server. With XMLHttpRequest, you can request a specific URL to retrieve data without refreshing the page. This allows the web page to update parts of the page without affecting the user’s actions. XMLHttpRequest is widely used in AJAX programming.

const xhr = (config) = >{
  Data is null if it is not passed. Method is get if it is not passed. Url is mandatory
  const {data = null,url,method = 'get'} = config;
  // Instantiate XMLHttpRequest
  const request = new XMLHttpRequest();
  // Initialize a request
  request.open(method.toUpperCase(),url,true);
  // Send the request
  request.send(data);
}

class Axios {
  config = {};
  constructor(initConfig) {
    this.config = initConfig;
  }
  request(config) {
    xhr(config)
  }
}
Copy the code

By simply implementing the XHR method and calling it in the Request method, we can send the actual request back to the background.

axios({
  method: "post".url: "https://reqres.in/api/users".data: {
    "name": "frankshi"."job": "FE"}});Copy the code

Reqres. in/ This site provides many public interfaces for us to use, so this article will use it to simulate interface requests.

Open the browser console and you can see that the request has been sent successfully.

Parse the param parameter

Demand analysis:

axios({
  method: 'get'.url: '/base/get'.params: {
    foo: ['bar'.'baz']}}) takes an array, and the final requested URL is => /base? foo[]=bar&foo[]=bazparams: {
  foo: {
    bar: 'baz'}} arguments are objects, and the final requested URL is => /base? foo=%7B%22:%22baz%22%7D

params: {
  date:new DateThe ()} argument is a date, and the final requested URL is => /base? date=2020-12-24T08:00:00.000z

params: {
  foo: '@ : $,} arguments are special characters and the final requested URL is => /base? Foo =@:$+ (Spaces converted to +)params: {
  foo: 'bar'.baz: null} arguments with a null value will be ignored and the final requested URL is => /base? foo=bar axios({method: 'get'.url: '/base/get#hash'.params: {
    foo: 'bar'}}) urls containing hash values will be discarded when requested. The final requested URL is => /base? foo=bar axios({method: 'get'.url: '/base/get? foo=bar'.params: {
    bar: 'baz'=> /base? => /base? foo=bar&bar=bazCopy the code

Code implementation:

1. Add a method to request to handle configuration uniformly

request(config) {
  // Process the incoming configuration
  processConfig(config);
  // Send the request
  xhr(config)
}
Copy the code

2. Convert URL parameters in processConfig

const buildURL = (url,params) = >{}

const transformURL = (config) = >{
  const { url, params } = config;
  return buildURL(url,params);
}

const processConfig = (config) = >{
  config.url = transformURL(config);
}
Copy the code

Params body function


// Determine whether the object is a Date object
function isDate(val) {
  return toString.call(val) === '[object Date]'
}
// Check whether the Object is an Object
function isPlainObject(val){
  return toString.call(val) === '[object Object]'
}
// Determine if URLSearchParams object is an instance
function isURLSearchParams(val) {
  return typeofval ! = ='undefined' && val instanceof URLSearchParams
}

const buildURL = (url,params) = >{
  // If the params parameter is empty, the original URL is returned
  if(! params) {return url
  }
  // Define a variable to hold the final concatenated parameters
  let serializedParams
  // Check whether params is the URLSearchParams object type
  if (isURLSearchParams(params)) {
    // If it is (for example: new URLSearchParams(topic= API&foo =bar)), params is serialized directly
    serializedParams = params.toString()
  } else {
    // If not, enter the main body to run
    // Define an array
    const parts = [];
    Keys can get an array of all the keys of an Object, traversed by forEach
    Object.keys(params).forEach(key= > {
      // Get the val of each key object
      const val = params[key]
      // If val is null, or undefined terminates the loop and enters the next loop, where null is ignored
      if (val === null || typeof val === 'undefined') {
        return
      }
      // Define an array
      let values = []
      // Check if val is an array type
      if (Array.isArray(val)) {
        // If yes, the values array is assigned to val, and the key concatenation is [].
        values = val
        key += '[]'
      } else {
        // If val is not an array, make it an array
        values = [val]
      }
      // Since the previous differences are smoothed out, this can be treated as an array
      values.forEach(val= > {
        If val is a date object,
        if (isDate(val)) {
          ToISOString Returns the Date object's standard date-time string format as a string
          val = val.toISOString()
          If val is an object type, serialize it directly
        } else if (isPlainObject(val)) {
          val = JSON.stringify(val)
        }
        // The result is pushed into the array
        parts.push(`${encode(key)}=${encode(val)}`)})})// Finally concatenate the array
    serializedParams = parts.join('&')}if (serializedParams) {
    // Handle the hash case
    const markIndex = url.indexOf(The '#')
    if(markIndex ! = = -1) {
      url = url.slice(0, markIndex)
    }
    If the pass already has an argument, concatenate it, otherwise add one manually.
    url += (url.indexOf('? ') = = = -1 ? '? ' : '&') + serializedParams
  }
  // Print the full URL
  return url

}
Copy the code

Parsing body data

Request. Send (data); . And data is the data we pass in:

axios({
  method: "post".url: baseURL,
  data: {
    "name": "frankshi"."job": "FE"}});Copy the code

The data that is passed in cannot be directly used as an input parameter to the send function. It needs to be converted to a JSON string.

For details, see MDN XMLHttpRequest Send

Implementation:

// This function serializes data
const transformRequest = (data) = >{
  if (isPlainObject(data)) {
    return JSON.stringify(data)
  }
  return data
}

// Define a function whose responsibility is to process the requested data
const transformRequestData = (config) = >{
  return transformRequest(config.data);
}
// processConfig is defined earlier to handle incoming configuration. It already handled the URL, adding processing data
const processConfig = (config) = >{
  config.url = transformURL(config);
  config.data = transformRequestData(config);
}
Copy the code

Parsing the headers

In an HTTP request, the header plays an important role as a bridge between the client and server to understand each other. So our library must also ensure that headers are passed and parsed correctly.

Content-type is added by default

axios({
  method: "post".url: baseURL,
  data: {
    "name": "frankshi"."job": "FE"}});Copy the code

When the front end sends a request like this, we need to tell the server the data type of the data we are sending so that the server can parse it correctly.

Therefore, this configuration is required:

axios({
  method: "post".url: baseURL,
  headers: {'content-type':"application/json; charset=utf-8"
  },
  data: {
    "name": "frankshi"."job": "FE"}});Copy the code

If the user does not add the Content-Type, we need to automatically add this configuration to it.

Let’s implement the logic for headers:

const normalizeHeaderName = (headers, normalizedName) = > {
  if(! headers) {return
  }
  // Traverse all headers
  Object.keys(headers).forEach(name= > {
    // If name is content-type and normalizedName is content-type, use content-type
    // Delete content-type.
    if(name ! == normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) { headers[normalizedName] = headers[name]delete headers[name]
    }
  })
}

 const processHeaders = (headers, data) = > {
  normalizeHeaderName(headers, 'Content-Type')
  // Set 'content-type' if data is an object
  if (isPlainObject(data)) {
    if(headers && ! headers['Content-Type']) {
      headers['Content-Type'] = 'application/json; charset=utf-8'}}return headers
}

const transformHeaders = (config) = >{
  const { headers = {}, data } = config;
  return processHeaders(headers,data);
}

const processConfig = (config) = >{
  config.url = transformURL(config);
  config.headers = transformHeaders(config);
  config.data = transformRequestData(config);
}

const xhr = (config) = >{
  let {data = null,url,method = 'get',headers={}} = config;
  const request = new XMLHttpRequest();
  request.open(method.toUpperCase(),url,true);
  // Iterate through all headers after processing
  Object.keys(headers).forEach(name= > {
    // Delete content-type if data is empty
    if (data === null && name.toLowerCase() === 'content-type') {
      delete headers[name]
    } else {
      // Set the header for the request
      request.setRequestHeader(name, headers[name])
    }
  })
  request.send(data);
}
Copy the code

Respond to the request

The above Request parses the param, body, and header of the Request. However, the client does not get the data of the background response. Let’s implement the Promise based data response.

Modify the XHR function

const xhr = (config) = >{
  // All implementation isomorphic new Promise wrapped around
  return new Promise((resolve, reject) = >{
    let {data = null,url,method = 'get',headers={}, responseType} = config;
    
    const request = new XMLHttpRequest();
    request.open(method.toUpperCase(),url,true);
    // Determine whether the user has set the return data type
    if (responseType) {
      request.responseType = responseType
    }
    // Listen to the onreadyStatechange function and receive the data returned by the background
    request.onreadystatechange = () = > {
      if(request.readyState ! = =4) {
        return
      }

      if (request.status === 0) {
        return
      }
      // The header returned is a string type that is parseHeaders parsed into an object type
      const responseHeaders = parseHeaders(request.getAllResponseHeaders());
      constresponseData = responseType && responseType ! = ='text' ? request.response : request.responseText
      const response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config,
        request
      }
      // Return data through resolve
      resolve(response);
    }

    // Iterate through all headers after processing. Request. Send (data); })}Copy the code

Once we’ve done that, we can write the Promise syntax when we provide it to the user.

Click here to see the complete code for this summary

Directory structure adjustment

One of the biggest problems is that all the code is concentrated in the axios.js file, and proper file directory division is very important for a professional library.

Optimized directory structure map:

└ ─ ─ the SRC └ ─ ─ axios. Js// Import file└ ─ ─ the core// Core folder└ ─ ─ Axios. Js// Store the Axios class└ ─ ─ dispatchRequest. Js// Trigger the request└ ─ ─ adapters// Adaptor folder, axios can adapt HTTP in Node, XHR in browser└ ─ ─ XHR. Js// Browser XHR request└ ─ ─ HTTP. Js// Node HTTP request, currently not implemented└ ─ ─ helpers// Store utility functions└ ─ ─ data. Js// Convert data related functions└ ─ ─ headers. Js// Handle header-related functions└ ─ ─ url. Js// Handle urL-related functions└ ─ ─ util. Js// Common utility functions
Copy the code

Click here to see the complete code for this summary

The adapter

To support different environments, Axios introduced adapters. The dispatchRequest.js file has been added to the directory in the previous section. Its main responsibility is to send requests, and this is the best place to write the adapter.

dispatchRequest.js

// The default adapter, to determine whether there is XMLHttpRequest, to determine whether the browser environment, to determine whether to choose XHR or HTTP
const getDefaultAdapter = () = > {
  let adapter;
  if (typeofXMLHttpRequest ! = ='undefined') {
    / / the browser
    adapter = require(".. /adapters/xhr");
  } else if (typeofprocess ! = ='undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // node.js
    adapter = require(".. /adapters/http");
  }
  return adapter;
}

const dispatchRequest = (config) = > {
  // If the user passes in an adapter, use the user's adapter, otherwise use the default adapter (the default configuration file will be extracted later)
  const adapter = config.adapter || getDefaultAdapter();

  // Process the incoming configuration
  processConfig(config);
  // Send the request
  return adapter(config).then((res) = > transformResponseData(res));
};
Copy the code

The principle of the adapter is not complicated, axios uses this function to smooth out the differences between the browser and node.js environment, so that the user does not notice. Adapter = require(“.. /adapters/*”), then use CommonJS require.

XHR/HTTP exports/module. Exports/module. Exports/module.

module.exports = function httpAdapter (config) {
  console.log("httpAdapter",config);
}
Copy the code

Why use require instead of import?

This is because CommonJS loads an object (the module.exports property) that is only generated when the script is finished running. An ES6 module is not an object, and its external interface is a static definition that is generated during the code static parsing phase.

Click here to see the complete code for this summary

Improve Axios class methods

There were a lot of methods defined in the Axios class, but none of them were implemented. Now that the Request method is implemented, other methods are relatively easy to implement.

Axios.js

import dispatchRequest from "./dispatchRequest";

class Axios {
  config = {};

  constructor(initConfig) {
    this.config = initConfig;
  }

  request(config) {
    return dispatchRequest(config);
  }

  get(url,config) {
    return this._requestMethodWithoutData('get',url,config)
  }

  delete(url,config) {
    return this._requestMethodWithoutData('delete', url, config)
  }

  head(url,config) {
    return this._requestMethodWithoutData('head', url, config)
  }

  options(url,config) {
    return this._requestMethodWithoutData('head', url, config)
  }

  post(url,data,config) {
    return this._requestMethodWithData('post', url, data, config)
  }

  put(url,data,config) {
    return this._requestMethodWithData('put', url, data, config)
  }

  patch(url,data,config) {
    return this._requestMethodWithData('patch', url, data, config)
  }
  // Call the require method without Data
  _requestMethodWithoutData(method, url, config) {
    return this.request(
      Object.assign(config || {}, {
        method,
          url
      })
    )
  }
  // Generically call methods with Data
  _requestMethodWithData(method, url, data, config) {
    // Merge parameters
    return this.request(
      Object.assign(config || {}, {
        method,
        url,
        data
      })
    )
  }
}

export default Axios;
Copy the code

Once this is done, we can call it axios.get(), axios.post(), etc.

Click here to see the complete code for this summary

The interceptor

Introduction to 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) {
    // What to do with the response data
    return response;
  }, function (error) {
    // Do something about the response error
    return Promise.reject(error);
  });
Copy the code
  • Request interceptor: This is used to uniformly perform certain operations, such as adding to the request header, before the request is senttokenField.
  • Response interceptor: It is used to perform certain actions upon receiving a server response, such as finding that the response status code is401, the login page is automatically displayed.

We can also remove an interceptor:

const myInterceptor = axios.interceptors.request.use(function () {/ *... * /});
axios.interceptors.request.eject(myInterceptor);
Copy the code

At this point, you can see that the interceptor is very much like the middleware of Redux and KOA2. It will execute the Request interceptor in the order you add it, then the real request, and then the Response interceptor.

Interceptor implementation

To create an interceptor management class: core/InterceptorManager. Js

export default class InterceptorManager{
  // Define an array to store interceptors
  interceptors = [];

  use(resolved, rejected) {
    // Push the interceptor object into the array
    this.interceptors.push({
      resolved,
      rejected
    })
    // Returns the interceptor index in the array
    return this.interceptors.length - 1
  }
  // go through the number group
  forEach(fn) {
    this.interceptors.forEach(interceptor= > {
      if(interceptor ! = =null) {
        fn(interceptor)
      }
    })
  }
  // Delete interceptors based on the index
  eject(id) {
    if (this.interceptors[id]) {
      this.interceptors[id] = null}}}Copy the code

The interceptor class is just an array and implements the use, forEach, and eject methods to manipulate it.

The next step is to implement the chain call to the interceptor: core/ axios.js

class Axios {
  config = {};
  // Define an interceptor object
  interceptors = {};

  constructor(initConfig) {
    this.config = initConfig;
    // Interceptor objects contain request interceptors and Response interceptors
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    }
  }

  request(config) {
    // Define an array of objects that will send real requests. Think of it as an interceptor
    const chain = [
      {
        resolved: dispatchRequest,
        rejected: undefined}]/ / when the user use axios. Interceptors. Request. Use (...). When multiple request interceptors are pushed
    / / this. Interceptors. Request what with multiple interceptors, by iterating through interceptors, insert the chain in the front of the array
    this.interceptors.request.forEach(interceptor= > {
      chain.unshift(interceptor)
    })
    / / when the user use axios. Interceptors. Response. Use (...). When multiple response interceptors are pushed
    / / this. Interceptors. The response in the multiple interceptors, by iterating through interceptors, inserted into the chain behind array
    this.interceptors.response.forEach(interceptor= > {
      chain.push(interceptor)
    })
    
    // The chain should look like this
    / * [{resolved: (config) = > {...}, / / the user custom request interceptor rejected (config) = > {...}}, {... resolved: dispatchRequest, rejected: Undefined}, {... resolved: (res) = > {...}, / / the user custom response interceptor rejected (res) = > {...}},] * /
		
    let promise = Promise.resolve(config)
    // If there are values in the chain array, the loop is iterated
    while (chain.length) {
      // Remove the first element from the array at a time
      const { resolved, rejected } = chain.shift();
      // Promise is copied to the next promise.then, implementing interceptor chain delivery
      promise = promise.then(resolved, rejected)
    }
    // Return the final execution result
    return promise
  }
}
Copy the code

The idea of the interceptor is a good one to learn from. Its implementation is not too complicated, but cleverly uses promise to deliver, giving Axios a “middleware” -like function.

Click here to see the complete code for this summary

The default configuration

This section describes how to use the default configuration

The global AXIOS default value

axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
Copy the code

Custom instance defaults

// Set config defaults when creating the instance
const instance = axios.create({
  baseURL: 'https://api.example.com'
});
// Alter defaults after instance has been created
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;
Copy the code

Default configuration implementation

Create defaults configuration files

src/defaults.js

const defaults = {
  method: 'get'.// If method is not passed by default, a GET method is given

  timeout: 0.// No timeout is set by default

  headers: {
    common: {
      Accept: 'application/json, text/plain, */*' // An Accept header is given by default}}}// Headers is null by default for 'delete', 'get', 'head', and 'options'
const methodsNoData = ['delete'.'get'.'head'.'options']

methodsNoData.forEach(method= > {
  defaults.headers[method] = {}
})

// Set a default content-type for 'post', 'put', and 'patch' requests
const methodsWithData = ['post'.'put'.'patch']

methodsWithData.forEach(method= > {
  defaults.headers[method] = {
    'Content-Type': 'application/x-www-form-urlencoded'}})export default defaults
Copy the code

Configuration merge in AXIOS

Axios.js

.import mergeConfig from "./mergeConfig";

class Axios {
  defaults = {};

  constructor(initConfig) {
    this.defaults = initConfig; . }request(url, config) {
    if (typeof url === "string") {
      if(! config) { config = {}; } config.url = url; }else {
      config = url;
    }
    // Merge the default configuration with the user-passed configuration
    config = mergeConfig(this.defaults, config) ... }}Copy the code

Assume that the following two configuration files need to be merged:

const defaults = {
  method: 'get'.timeout: 0.headers: {
    common: {
      Accept: 'application/json, text/plain, */*';
    }
    post: {
      'Content-Type': 'application/x-www-form-urlencoded'}}}const customConfig = {
  url: "/post".method: "post".data: {a:1},
  headers: {test: "123"}}Copy the code

Use the default configuration only if the configuration in customConfig takes precedence over the user’s own configuration.

Concrete implementation of the merged configuration: SRC /core/ mergeconfig.js

import { deepMerge, isPlainObject } from '.. /helpers/util'
// Create a policy object
const strats = Object.create(null)
// Default policy: val2. If the parameter is not empty, use the parameter. Otherwise, use the default parameter val1
function defaultStrat(val1, val2) {
  return typeofval2 ! = ='undefined' ? val2 : val1
}
// 'url', 'params', 'data'
function fromVal2Strat(val1, val2) {
  if (typeofval2 ! = ='undefined') {
    return val2
  }
}
// Deep merge configuration applies when the configuration itself is an object
// The deepMerge method is a deep copy of an object, which is not described here
function deepMergeStrat(val1, val2) {
  if (isPlainObject(val2)) {
    return deepMerge(val1, val2)
  } else if (typeofval2 ! = ='undefined') {
    return val2
  } else if (isPlainObject(val1)) {
    return deepMerge(val1)
  } else {
    return val1
  }
}
// Use three attributes configured by the user
const stratKeysFromVal2 = ['url'.'params'.'data']

stratKeysFromVal2.forEach(key= > {
  strats[key] = fromVal2Strat
})
// Two attributes that require a deep merge
const stratKeysDeepMerge = ['headers'.'auth']

stratKeysDeepMerge.forEach(key= > {
  strats[key] = deepMergeStrat
})

export default function mergeConfig(config1, config2) {
  if(! config2) { config2 = {} }// Create an empty object to store the final merged configuration file
  const config = Object.create(null)
  // The mergeField method is to select different policies according to the attributes and merge the configurations
  for (let key in config2) {
    mergeField(key)
  }
  // Traverses the default configuration, and the configuration does not appear in the user configuration
  for (let key in config1) {
    if(! config2[key]) { mergeField(key) } }// This is the "strategy mode" of design mode, which effectively removes an infinite number of if and else cases from the code
  function mergeField(key) {
    const strat = strats[key] || defaultStrat
    config[key] = strat(config1[key], config2[key])
  }
  // Export the final config
  return config
}

Copy the code

The code here is more complex and handles more cases, so to fully understand this code you need to actually run the code, understand the core idea here, and use the policy pattern to eliminate the if in the code… Else is the most important.

Click here to see the complete code for this summary

Cancel the function

Cancel request requirements analysis

To cancel a request using cancel Token, you can create a Cancel token using the canceltoken. source factory method, like this:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
     // Processing error}}); axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

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

CancelToken can also be created by passing an executor function to the CancelToken constructor:

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

Note: You can cancel multiple requests using the same Cancel token.

Cancel request implementation

Take a closer look at how it’s used:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

source.cancel('Operation canceled by the user.');
Copy the code

Perform source. Cancel (‘… ‘), you can cancel the request. The actual cancellation request can be implemented with the following code:

const request = new XMLHttpRequest();
request.abort();
Copy the code

In xhr.js, abort is invoked to cancel the request, but the Request object does not expose it. How do we do that?

We know by reading the source code, through the promise to achieve linkage. That is, when source.cancel(‘… ‘) to actually change the state of the promise.

In xhr.js, just listen for the state of the promise:

if (cancelToken) {
  cancelToken.promise
    .then(reason= > {
    request.abort()
    reject(reason)
  })
    .catch(() = >{})}Copy the code

To achieve its core classes now SRC/cancel/CancelToken js

class Cancel {
  message;

  constructor(message) {
    this.message = message
  }
}
// Determine whether the thrown error is an instance of Cancel
function isCancel(value) {
  return value instanceof Cancel
}

export default class CancelToken {
  promise;// Define the PROMISE variable to store the state
  reason; // Define error cause variables

  constructor(executor) {
    // Define an empty variable to store the resolve method of a Promise instance
    let resolvePromise;
    this.promise = new Promise(resolve= > {
      resolvePromise = resolve
    })

    const paramFn = message= > {
      if (this.reason) {
        return
      }
      this.reason = new Cancel(message)
      resolvePromise(this.reason)
    };
    // Executes the method passed in when instantiating, using paramFn as an argument
    executor(paramFn)
  }

  throwIfRequested() {
    if (this.reason) {
      throw this.reason
    }
  }
  // Define the source static method, export the CancelToken instance, and cancel the method
  static source() {
    let cancel;
    const token = new CancelToken(c= > {
      cancel = c
    })
    return {
      cancel,
      token
    }
  }
}
Copy the code

Finally, add the corresponding method to axios.js:

axios.CancelToken = CancelToken
axios.Cancel = Cancel
axios.isCancel = isCancel
Copy the code

Finally, we analyze the downsizing process:

Execute source.cancel(‘… ‘); Equivalent to executing the paramFn method.

2. When executing the paramFn method, use resolvePromise(this.reason) to change this. Promise to resolve.

In xhr.js, canceltoken.promise.then (reason => {request.abort()}) is used to monitor the promise state and cancel the request when it changes.

Click here to see the complete code for this summary

withCredentials

In the same-domain case, the cookie in the current domain is sent by default, but in the cross-domain case, the cookie in the request domain is not carried by default. If you want to carry the cookie, you only need to set the withCredentials of the XHR object to true.

This is very simple to implement, just add this code to xhr.js:

module.exports = function xhrAdapter(config) {
  return new Promise((resolve, reject) = > {
    let{... withCredentials } = config;const request = newXMLHttpRequest(); .// Set the withCredentials attribute of the XHR object
    if(withCredentials){ request.withCredentials = withCredentials; }... request.send(data); }); };Copy the code

Click here to see the complete code for this summary

CSRF defense

Cross-site Request Forgery, often shortened to CSRF or XSRF, is an attack method to trick a user into performing unintentional actions on a currently logged Web application.

A typical CSRF attack has the following flow:

  1. Victim logina.comAnd retained the login credentials (Cookie);
  2. The attacker lured the victim to visitb.com;
  3. b.coma.comA request was sent:a.com/act=xx. Browsers carry it by defaulta.comCookie;
  4. a.comAfter receiving the request, it verifies the request and confirms that it is the victim’s credentials, mistaking it as a request sent by the victim himself;
  5. a.comExecuted on behalf of the victimact=xx;
  6. The attack is complete, the attacker impersonates the victim without the victim’s knowledge, and letsa.comThe user-defined operation is performed.

If you are not familiar with CSRF defense, you can also read another article by the author: Front-end Security (same Origin Policy, XSS attack, CSRF attack).

How does Axios defend against CSRF

Token authentication is used to defend against CSRF attacks. The defense process is as follows:

  1. When the browser makes a request to the server, the server generates oneCSRF Token. And through theset-cookieTo the client.
  2. When a client sends a request fromcookieRead from the corresponding fieldtokenAnd then add to the requestheadersIn the.
  3. The server resolves the requestheadersAnd verify that thetoken.

Since this token is difficult to forge, it is possible to distinguish whether the request was properly initiated by the user.

So our axios library automatically does all of these things. Every time we send a request, we read the token value from the cookie and add it to the headers request. We allow the user to configure xsrfCookieName and xsrfHeaderName. XsrfCookieName indicates the cookie name of the token, and xsrfHeaderName indicates the header name of the token in the request headers.

axios({
  url:"/".xsrfCookieName: 'XSRF-TOKEN'.// Default configuration
  xsrfHeaderName: 'X-XSRF-TOKEN' // Default configuration
})
Copy the code

Axios implements CSRF defense

Its core implementation is in xhr.js:

module.exports = function xhrAdapter(config) {
  return new Promise((resolve, reject) = > {
    let{... withCredentials, xsrfCookieName, xsrfHeaderName } = config;const request = newXMLHttpRequest(); .// Set the withCredentials attribute of the XHR object
    if(withCredentials){
      request.withCredentials = withCredentials;
    }
    // Check if withCredentials are set to true, or if xsrfCookieName exists in a domain request
    // Add XSRF related fields to request HEADERS.
    if((withCredentials || isURLSameOrigin(url)) && xsrfCookieName ){
      // Read the corresponding xsrfHeaderName value by cookie
      const xsrfValue = cookie.read(xsrfCookieName);
      if(xsrfValue && xsrfHeaderName){ headers[xsrfHeaderName] = xsrfValue; }}... request.send(data); }); };Copy the code

The implementation of CSRF defense is not complicated, but by implementing it, we can thoroughly understand the cause of CSRF and how to defend against it.

Click here to see the complete code for this summary

At this point, the core of writing Axios is complete and basically covers the important axios points. I believe you and the author can learn a lot, next we will pack it and send it to NPM online.

Axios shipped it as a package

1. Since we have already written the Webpack configuration, we just need to execute the package command NPM run build directly. 2. Package. json optimization

{
  "name": "lion-axios"./ / package name
  "version": "1.0.0"./ / version
  "description": "lion-axios"./ / description
  "scripts": {
    "clean": "rimraf ./dist"."dev": "webpack-dev-server --env.development --config webpack.common.js"."build": "npm run clean && webpack --env.production --config webpack.common.js"."prepublishOnly": "npm run build" // NPM publish is automatically packaged before it is published
  },
  "repository": { // Code hosting information
    "type": "git"."url": "git+https://github.com/shiyou00/lion-axios.git"
  },
  "files": [
    "dist" // The files that the project uploads to the NPM server can be individual files, entire folders, or wildcard matched files.]."main": "dist/axios.js".// Import file
  "keywords": ["lion-axios"]./ / keywords
  "author": "Lion"./ / the author
  "license": "ISC"./ / agreement
  "bugs": {
    "url": "https://github.com/shiyou00/lion-axios/issues" // Mention the bug address
  },
  "homepage": "https://github.com/shiyou00/lion-axios#readme".// Home address
}
Copy the code

3. Log in and register NPM

# register NPM adduserUsername:shiyou
Email([email protected]) # Login to NPM login and fill in the registration informationCopy the code

4. Execute NPM publish

View the Lion-Axios packet line address

5. Use Lion – Axios

# install yarn add lion-axios or NPM install lion-axios --save #import axios from 'lion-axios'; # useconst baseURL = "https://reqres.in/api/users";
axios({
  method: "get".url: `${baseURL}? foo=bar`.params: {
    bar: "baz1",
  },
}).then((res) = > {
  console.log(res);
});
Copy the code

conclusion

  1. throughaxiosThe introduction makes us onaxiosHave a preliminary understanding, here or suggest you to go over the official documents.
  2. Through constructingaxiosProject engineering, you can learn how to build an enterprise – class library of engineering projects.
  3. By doing it step by stepaxiosSource code, you can learn a lot of subtle programming ideas, which is an important goal to achieve an open source library.
  4. Finally, we packaged up the library and published it tonpmOnline, complete the full life cycle of an open source library.

If your company needs you to manually build an open source library, you already know what to do.

If learning this article has helped you, give it a thumbs up.