directory

  • background
  • Set up the environment
    • Build a simple background to provide interface
    • Install native Axios and use it
    • See the effect
    • Analyze the pass parameters and return values
  • Implement Axios
    • createInstance
    • The type definition
    • The Axios class implements the GET method
    • Type declaration episode
    • The Axios class implements the POST method
  • Implement error handling mechanisms
    • Analog network exception
    • Analog timeout exception
    • Simulate error status code
    • The client invokes the timeout interface
  • Interceptor function
    • Use interceptors
    • Implementation interceptor
  • Merge configuration items
  • Implement request and response conversion
  • Cancel task function
    • Using cancel tasks
    • Implement cancel task
  • conclusion

The article was first published@careteen/axios(store all the codes mentioned below), reprint and indicate the source.

background

Axios is highly recommended by Rain Creek. There are several advantages

  • supportnodeSide and browser side
    • The sameAPI.nodeAnd browser full support, platform switching without pressure
  • supportPromise
    • usePromiseManage asynchrony, say goodbye to traditioncallbackway
  • Rich configuration items
    • Automatically convert JSON data
    • Support request/response interceptor configuration
    • Support for transforming request and response data
    • Support cancellation request

The Vue project at work has been using Axios for requests, and recently had time to explore the underlying thinking. On the one hand, the purpose of the research is to be able to control him better, on the other hand, it is also the point that the interview will examine (eager for quick success and instant benefit).

Next, peel back the layers of Axios from use to easy implementation.

Set up the environment

Create-react-app can be used to create a quick preview of the project

npm i -g create-react-app
create-react-app axios --typescript
Copy the code

Build a simple background to provide interface

At the same time, use Express to build a simple local backend that works with AxiOS

npm i -g nodemon
yarn add express body-parser
Copy the code

Write the server.js file in the root directory

// server.js
const express = require('express')
const bodyParser = require('body-parser')

const app = express()

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
  extended: true,}))// set cors
app.use((req, res, next) = > {
  res.set({
    'Access-Control-Allow-Origin': 'http://localhost:3000'.'Access-Control-Allow-Credentials': true.'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS'.'Access-Control-Allow-Headers': 'Content-Type',})if (req.method === 'OPTIONS') {
    return res.sendStatus(200)
  }
  next()
})

app.get('/get'.(req, res) = > {
  res.json(req.query)
})

app.listen(8080)

Copy the code

Because the create – react – app start the default port is 3000, start using express service port is 8080, so you need to set up cors, and provide an http://localhost:8080/get interface will pass first returned directly.

Install native Axios and use it

Then install native AxiOS first to view easy to use

yarn add axios @types/axios qs @types/qs parse-headers
Copy the code

Change the SRC /index.tsx file

// src/index.tsx
import axios, { AxiosResponse } from 'axios'

const BASE_URL = 'http://localhost:8080'

interface User {
  name: string;
  age: number;
}

const user: User = {
  name: 'Careteen'.age: 25,
}

axios({
  method: 'GET'.url: `${BASE_URL}/get`.params: user,
}).then((res: AxiosResponse) = > {
  console.log('res: ', res);
  return res.data
}).then((data: User) = > {
  console.log('data: ', data);
}).catch((err: any) = > {
  console.log('err: ', err);
})
Copy the code

Quickly print logs in VsCode using the vscode-extension-nidalee plug-in

See the effect

#1. Start the background service
yarn server
#2. Start the client
yarn start
Copy the code

Browser accesshttp://localhost:3000/Open the console to view the printed results

Analyze the pass parameters and return values

To viewaixos/index.d.tsThe file tells you that the parameters and return value types required by AXIos are defined as follows

Implement Axios

By observing the source axios/lib/axios. Js and its use, can be found axios is a promise function and axios. The interceptors. Request interceptor function.

createInstance

Here the source code is simplified for easy understanding

// axios/index.ts
import Axios from './Axios'
import { AxiosInstance } from './types'

const createInstance = (): AxiosInstance= > {
  const context = new Axios()
  let instance = Axios.prototype.request.bind(context)
  instance = Object.assign(instance, Axios.prototype, context)
  return instance as unknown as AxiosInstance
}

const axios = createInstance()

export default axios

Copy the code

The source code is implemented in a more ingenious way

  • Entry files are exposed outwardscreateInstanceFunctions; Its inner core is mainlynewaAxiosThe class instancecontextAt the same time, willAxiosThe prototype approachrequest(main logic)thisAlways bound tocontext. The purpose is to preventthisPoints to a problem.
  • willAxiosAll properties and instances on the class stereotypecontextCopy abovebindNew function generated afterinstance. The purpose is that S can be inaxiosThe mount object function is similar to the interceptor functionaxios.interceptors.requestConvenient for the user to call.

The type definition

You can see the type to define from analyzing the screenshot of the pass parameter and return value

Here the source code is simplified for easy understanding

// axios/types.ts
export type Methods = 
  | 'GET' | 'get'
  | 'POST' | 'post'
  | 'PUT' | 'put'
  | 'DELETE' | 'delete'
  | 'PATCH' | 'patch'
  | 'HEAD' | 'head'
  | 'OPTIONS' | 'options'

export interface AxiosRequestConfig {
  url: string; methods: Methods; params? : Record<string.any>;
}

export interface AxiosInstance {
  (config: AxiosRequestConfig): Promise<any>;
}

export interface AxiosResponse<T> {
  data: T;
  status: number;
  statusText: string;
  headers: any; config: AxiosRequestConfig; request? :any;
}

Copy the code

The Axios class implements the GET method

From the above type definition and usage, use XMLHttpRequest to implement the actual send request.

The steps are also known as the tetralogy

  • createXMLHttpRequestThe instancerequest
  • callrequest.open()configurationmethods,url
  • Listening to therequest.onreadystatechange()Get a response
  • callrequest.send()Send the request

Easy to understand without considering compatibility

// axios/Axios.ts
import qs from 'qs'
import parseHeaders from 'parse-headers'
import { AxiosRequestConfig, AxiosResponse } from './types'

export default class Axios {
  request(config: AxiosRequestConfig): Promise<any> {
    return this.dispatchRequest(config)
  }
  dispatchRequest(config: AxiosRequestConfig) {
    return new Promise((resolve, reject) = > {
      let {
        url,
        methods = 'GET',
        params
      } = config
      const request: XMLHttpRequest = new XMLHttpRequest()
      if (params) {
        const paramsStr = qs.stringify(params)
        if (url.indexOf('? ') = = = -1) {
          url += `?${paramsStr}`
        } else {
          url += ` &${paramsStr}`
        }
      }
      request.open(methods, url, true)
      request.responseType = 'json'
      request.onreadystatechange = () = > {
        if (request.readyState === 4) {
          if (request.status >= 200 && request.status < 300) {
            const response: AxiosResponse<any> = {
              data: request.response,
              status: request.status,
              statusText: request.statusText,
              headers: parseHeaders(request.getAllResponseHeaders()),
              config,
              request,
            }
            resolve(response)
          } else {
            reject(`Error: Request failed with status code ${request.status}`)
          }
        }
      }
      request.send()
    })
  }
}
Copy the code

The code above is sufficient for installing native Axios and using the section, and I’ll continue to extend the other methods below.

Type declaration episode

The third party library parse-headers does not currently have @types/parse-headers, so a TS error is reported when using it. On the one hand, I will not write a declaration file for this because of the time problem, and on the other hand, the core of this project is to implement AXIos, so I will create a new typings/parse-headers

// typings/parse-headers.d.ts
declare module 'parse-headers'
Copy the code

Then modify the tsconfig.json configuration

// tsconfig.json
"include": [
  "src"."typings" // +
]
Copy the code

The Axios class implements the POST method

First extend the interface on the server side

// server.js
app.post('/post'.(req, res) = > {
  res.json(req.body)
})
Copy the code

Then replace the interface when in use

// src/index.tsx
axios({
  method: 'POST'.url: `${BASE_URL}/post`.data: user,
  headers: {
    'Content-Type': 'application/json',
  },
}).then((res: AxiosResponse) = > {
  console.log('res: ', res);
  return res.data
}).then((data: User) = > {
  console.log('data: ', data);
}).catch((err: any) = > {
  console.log('err: ', err);
})
Copy the code

And then extend the type

export interface AxiosRequestConfig {
  // ...data? : Record<string.any>; headers? : Record<string.any>;
}
Copy the code

Finally extend the request core logic

// axios/Axios.ts
let {
  // ...
  data,
  headers,
} = config
// ...
if (headers) {
  for (const key in headers) {
    if (Object.prototype.hasOwnProperty.call(headers, key)) {
      request.setRequestHeader(key, headers[key])
    }
  }
}
let body: string | null = null;
if (data && typeof data === 'object') {
  body = JSON.stringify(data)
}
request.send(body)
Copy the code

Implement error handling mechanisms

There are three main error scenarios

  • The network is abnormal. Broken network
  • Timeout exception. The interface takes longer than the configured timetimeout
  • Error status code.status < 200 || status >= 300
// axios/Axios.ts
// Handle network exceptions
request.onerror = () = > {
  reject('net::ERR_INTERNET_DISCONNECTED')}// Handle timeout exceptions
if (timeout) {
  request.timeout = timeout
  request.ontimeout = () = > {
    reject(`Error: timeout of ${timeout}ms exceeded`)}}// Handle the error status code
request.onreadystatechange = () = > {
  if (request.readyState === 4) {
    if (request.status >= 200 && request.status < 300) {
      // ...
      resolve(response)
    } else {
      reject(`Error: Request failed with status code ${request.status}`)}}}Copy the code

Analog network exception

Refresh the page and open the console Network. Change Online to Offline within 5s to simulate disconnection.

// src/index.tsx
setTimeout(() = > {
  axios({
    method: 'POST'.url: `${BASE_URL}/post`.data: user,
    headers: {
      'Content-Type': 'application/json',
    },
  }).then((res: AxiosResponse) = > {
    console.log('res: ', res)
    return res.data
  }).then((data: User) = > {
    console.log('data: ', data)
  }).catch((err: any) = > {
    console.log('err: ', err)
  })
}, 5000);
Copy the code

Errors are normally caught

Analog timeout exception

Extended Server Interface Added a configuration timeout interface

// server.js
app.post('/post_timeout'.(req, res) = > {
  let { timeout } = req.body
  if (timeout) {
    timeout = parseInt(timeout, 10)}else {
    timeout = 0
  }
  setTimeout(() = > {
    res.json(req.body)
  }, timeout)
})
Copy the code
// src/index.tsx
axios({
  method: 'POST'.url: `${BASE_URL}/post_timeout`.data: {
    timeout: 3000,},timeout: 1000.headers: {
    'Content-Type': 'application/json',
  },
}).then((res: AxiosResponse) = > {
  console.log('res: ', res)
  return res.data
}).then((data: User) = > {
  console.log('data: ', data)
}).catch((err: any) = > {
  console.log('err: ', err)
})
Copy the code

Errors are normally caught

Simulate error status code

Extended Server Interface Added a configuration error status code interface

// server.js
app.post('/post_status'.(req, res) = > {
  let { code } = req.body
  if (code) {
    code = parseInt(code, 10)}else {
    code = 200
  }
  res.statusCode = code
  res.json(req.body)
})
Copy the code

The client invokes the error status code interface

// src/index.tsx
axios({
  method: 'POST'.url: `${BASE_URL}/post_status`.data: {
    code: 502,},headers: {
    'Content-Type': 'application/json',
  },
}).then((res: AxiosResponse) = > {
  console.log('res: ', res)
  return res.data
}).then((data: User) = > {
  console.log('data: ', data)
}).catch((err: any) = > {
  console.log('err: ', err)
})
Copy the code

Errors are normally caught

Interceptor function

Use interceptors

Add a name for access-Control-allow-headers when setting cORS on the server, so that the interceptor can be used to set request Headers later.

// server.js
app.use((req, res, next) = > {
  res.set({
    // ...
    'Access-Control-Allow-Headers': 'Content-Type, name',})// ...
})
Copy the code

Use request and Response interceptors on the client side

// src/index.tsx
axios.interceptors.request.use((config: AxiosRequestConfig): AxiosRequestConfig= > {
  config.headers.name += '1'
  return config
})
axios.interceptors.request.use((config: AxiosRequestConfig): AxiosRequestConfig= > {
  config.headers.name += '2'
  return config
})
axios.interceptors.request.use((config: AxiosRequestConfig): AxiosRequestConfig= > {
  config.headers.name += '3'
  return config
})

axios.interceptors.response.use((response: AxiosResponse): AxiosResponse= > {
  response.data.name += '1'
  return response
})
axios.interceptors.response.use((response: AxiosResponse): AxiosResponse= > {
  response.data.name += '2'
  return response
})
axios.interceptors.response.use((response: AxiosResponse): AxiosResponse= > {
  response.data.name += '3'
  return response
})

axios({
  method: 'GET'.url: `${BASE_URL}/get`.params: user,
  headers: {
    'Content-Type': 'application/json'.'name': 'Careteen',
  },
}).then((res: AxiosResponse) = > {
  console.log('res: ', res)
  return res.data
}).then((data: User) = > {
  console.log('data: ', data)
}).catch((err: any) = > {
  console.log('err: ', err)
})
Copy the code

View the request header and response body

The pattern for interceptors is

  • Request interceptors are added before they are executed
  • Responder interceptors added first are executed first

Using axios. Interceptors. Request. Eject cancel the interceptor specified

// src/index.tsx
axios.interceptors.request.use((config: AxiosRequestConfig): AxiosRequestConfig= > {
  config.headers.name += '1'
  return config
})
const interceptor_request2 = axios.interceptors.request.use((config: AxiosRequestConfig): AxiosRequestConfig= > {
  config.headers.name += '2'
  return config
})
// + change from synchronous to asynchronous
axios.interceptors.request.use((config: AxiosRequestConfig) = > {
  return new Promise((resolve) = > {
    setTimeout(() = > {
      config.headers.name += '3'
      resolve(config)
    }, 2000)})})// + interceptor_request2 '
axios.interceptors.request.eject(interceptor_request2)

axios.interceptors.response.use((response: AxiosResponse): AxiosResponse= > {
  response.data.name += '1'
  return response
})
const interceptor_response2 = axios.interceptors.response.use((response: AxiosResponse): AxiosResponse= > {
  response.data.name += '2'
  return response
})
axios.interceptors.response.use((response: AxiosResponse): AxiosResponse= > {
  response.data.name += '3'
  return response
})
// + interceptor_response2 '
axios.interceptors.response.eject(interceptor_response2)
Copy the code

2sThen view the request header and response body

Implementation interceptor

Through the use of the interceptor axios. Interceptors. Request. Use derived type definition.

// axios/types.ts
import AxiosInterceptorManager from "./AxiosInterceptorManager";
export interface AxiosInstance {
  (config: AxiosRequestConfig): Promise<any>;
  interceptors: {
    request: AxiosInterceptorManager<AxiosRequestConfig>;
    response: AxiosInterceptorManager<AxiosResponse>
  };
}
Copy the code

It mainly defines the AxiosInterceptorManager class and the use and eject methods.

// axios/AxiosInterceptorManager.ts
export interface OnFulfilled<V> {
  (value: V): V | PromiseLike<V> | undefined | null;
}

export interface OnRejected {
  (error: any) :any;
}

export interfaceInterceptor<V> { onFulfilled? : OnFulfilled<V>; onRejected? : OnRejected; }export default class AxiosInterceptorManager<V> {
  public interceptors: Array<Interceptor<V> | null> = [] use(onFulfilled? : OnFulfilled<V>, onRejected? : OnRejected):number {
    this.interceptors.push({
      onFulfilled,
      onRejected
    })
    return this.interceptors.length - 1
  }
  eject(id: number) {
    if (this.interceptors[id]) {
      this.interceptors[id] = null}}}Copy the code

Using the interceptor user defined in the previous section constructs the queue as shown below

// axios/Axios.ts
export default class Axios<T = any> {
  public interceptors = {
    request: new AxiosInterceptorManager<AxiosRequestConfig>(),
    response: new AxiosInterceptorManager<AxiosResponse<T>>(),
  }
  request(config: AxiosRequestConfig): Promise<any> {
    const chain: Array<Interceptor<AxiosRequestConfig> | Interceptor<AxiosResponse<T>>> = [
      {
        onFulfilled: this.dispatchRequest as unknown as OnFulfilled<AxiosRequestConfig>,
      }
    ]
    // 1. Request interceptor - first added and then executed
    this.interceptors.request.interceptors.forEach((interceptor: Interceptor<AxiosRequestConfig> | null) = > {
      interceptor && chain.unshift(interceptor)
    })
    // 2. Responder interceptor - first add, first execute
    this.interceptors.response.interceptors.forEach((interceptor: Interceptor<AxiosResponse<T>> | null) = > {
      interceptor && chain.push(interceptor)
    })
    // 3. Execute in the order constructed
    let promise: Promise<any> = Promise.resolve(config)
    while (chain.length) {
      const { onFulfilled, onRejected } = chain.shift()!
      promise = promise.then(onFulfilled  as unknown as OnFulfilled<AxiosRequestConfig>, onRejected)
    }
    return promise
  }
}
Copy the code

For example, the third step of the above step is to execute the constructed queue in sequence, while supporting asynchrony.

Merge configuration items

Set default configuration items for AXIOS, such as methods default to GET methods, and so on

// axios/Axios.ts
let defaultConfig: AxiosRequestConfig = {
  url: ' '.methods: 'GET'.timeout: 0.headers: {
    common: {
      accept: 'application/json',}}}const getStyleMethods: Methods[] = ['get'.'head'.'delete'.'options']
const postStyleMethods: Methods[] = ['put'.'post'.'patch']
const allMethods:  Methods[] = [...getStyleMethods, ...postStyleMethods]

getStyleMethods.forEach((method: Methods) = >{ defaultConfig.headers! [method] = {} }) postStyleMethods.forEach((method: Methods) = >{ defaultConfig.headers! [method] = {'content-type': 'application/json',}})export default class Axios<T = any> {
  public defaultConfig: AxiosRequestConfig = defaultConfig
  request() {
    // merge config
    config.headers = Object.assign(this.defaultConfig.headers, config.headers)
    // ...
  }
  dispatchRequest() {
    // ...
    if (headers) {
      for (const key in headers) {
        if (Object.prototype.hasOwnProperty.call(headers, key)) {
          if (key === 'common' || allMethods.includes(key as Methods)) {
            if (key === 'common' || key === config.methods.toLowerCase()) {
              for (const key2 in headers[key]) {
                if (Object.prototype.hasOwnProperty.call(headers[key], key2)) {
                  request.setRequestHeader(key2, headers[key][key2])
                }
              }
            }
          } else {
            request.setRequestHeader(key, headers[key])
          }
        }
      }
    }
    // ...}}Copy the code

The purpose of headers processing is to add ‘Content-type ‘: ‘application/json’ to post-style requests by default, and merge configuration items to distinguish whether it is a request method or another request header configuration.

Implement request and response conversion

In ordinary work, there are common problems of naming inconsistency caused by the parallel development of the front and back end or the first development of the front end. The solution is generally to map objects or array attributes. The solution is similar to @careteen/ Match.

The solution can be put into the axios transformRequest/transformResponse conversion function.

// axios/types.ts
export interface AxiosRequestConfig {
  // ...transformRequest? :(data: Record<string.any>, headers: Record<string.any>) = > any; transformResponse? :(data: any) = > any;
}
Copy the code

The implementation method is the first step of the request method before sending a request and the dispatchRequest method after sending a request when receiving the response body.

// axios/Axios.ts
let defaultConfig: AxiosRequestConfig = {
  // ...
  transformRequest: (data: Record<string.any>, headers: Record<string.any>) = > {
    headers['common'] ['content-type'] = 'application/x-www-form-urlencoded'
    return JSON.stringify(data)
  },
  transformResponse: (response: any) = > {
    return response.data
  },
}
export default class Axios<T = any> {
  request() {
    if (config.transformRequest && config.data) {
      config.data = config.transformRequest(config.data, config.headers = {})
    }
    // ...
  }
  dispatchRequest() {
    // ...
    request.onreadystatechange = () = > {
      if (config.transformResponse) {
        request.response.data = config.transformResponse(request.response.data)
      }
      resolve(request.response)
    }
    // ...}}Copy the code

Cancel task function

Using cancel tasks

Normal work requirements expect unfulfilled promises or XHR requests to be cancelled under certain scenarios (off the page).

Watch axios in action first

const CancelToken = axios.CancelToken
const source = CancelToken.source()
axios({
  method: 'POST'.url: `${BASE_URL}/post_timeout`.timeout: 3000.data: {
    timeout: 2000,},cancelToken: source.token,
}).then((res: AxiosResponse) = > {
  console.log('res: ', res)
  return res.data
}).then((data: User) = > {
  console.log('data: ', data)
}).catch((err: any) = > {
  if (axios.isCancel(err)) {
    console.log('cancel: ', err)
  } else {
    console.log('err: ', err)
  }
})
source.cancel('[cancel] : user cancel request')
Copy the code

View the console to cancel the task

Implement cancel task

The implementation idea is similar to how to terminate promises, but this article is easier to understand.

Based on the use of a backward type definition

// axios/types.ts
export interface AxiosRequestConfig {
  // ...cancelToken? :Promise<any>;
}
export interface AxiosInstance {
  // ...
  CancelToken: CancelToken;
  isCancel: (reaseon: any) = > boolean;
}
Copy the code

Based on CancelToken, isCancel using a backward mount

import { CancelToken, isCancel } from './cancel'
// ...
axios.CancelToken = new CancelToken()
axios.isCancel = isCancel

export default axios
Copy the code

Create a new cancel.ts file to cancel the function

// axios/cancel.ts
export class Cancel {
  public reason: string
  constructor(reason: string) {
    this.reason = reason
  }
}

export const isCancel = (reason: any) = > {
  return reason instanceof Cancel
}

export class CancelToken {
  public resolve: any
  source() {
    return {
      token: new Promise((resolve) = > {
        this.resolve = resolve
      }),
      cancel: (reason: string) = > {
        this.resolve(new Cancel(reason))
      }
    }
  }
}
Copy the code

Request.abort () is triggered when appropriate (the source.cancel method is invoked in the user-specified scenario) and the task is canceled.

export default class Axios<T = any> {
  dispatchRequest() {
    // ...
    if (config.cancelToken) {
      config.cancelToken.then((reason: string) = > {
        request.abort()
        reject(reason)
      })
    }
    request.send(body)
  }
}
Copy the code

conclusion

Using the simple code above to implement a simplified version of axiOS that works, it is far from perfect.

Purpose is also in the use of third-party excellent library at the same time, through the use of the way back to the bottom of the implementation of ideas, and then read the source code, better control them.