Using typescript to implement lightweight Axios

Time:2021-6-9

Using typescript to implement lightweight Axios

catalogue

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

background

axiosyesYouyu RiverHighly recommended. There are several advantages

  • supportnodeClient and browser

    • alikeAPInodeAnd browser full support, platform switching without pressure
  • supportPromise

    • usePromiseManagement asynchronism, farewell to traditioncallbackmode
  • Rich configuration items

    • Automatic conversion of JSON data
    • Support request / response interceptor configuration
    • Support conversion request and response data
    • Cancel request supported

at workVueProjects are always usedaxiosOnly recently have I had some time to study the underlying ideas. On the one hand, the purpose of the research is to better control him, on the other hand, it is also the point of the interview.

The following will be from the use to simple implementation layer by layerAxios

Setting up the environment

The implementation of this first simple with the help ofcreate-react-appQuickly create projects that can be previewed quickly

npm i -g create-react-app
create-react-app axios --typescript

Build simple background to provide interface

Use at the same timeexpressBuild a local cooperationaxiosSimple backstage

npm i -g nodemon
yarn add express body-parser

Write in the root directoryserver.jsfile

// 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)

becausecreate-react-appThe default startup port is3000, usingexpressStart service port is8080, so it needs to be setcors, and provide ahttp://localhost:8080/getThe interface returns the parameters directly.

Install native Axios and use

Then install the nativeaxiosCheck out easy to use first

yarn add axios @types/axios qs @types/qs parse-headers

changesrc/index.tsxfile

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

stayVsCodeQuick print log plug invscode-extension-nidalee

View the effect

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

Browser accesshttp://localhost: 3000 / open the console to view the print results
Using typescript to implement lightweight Axios

Analysis of parameters and return values

seeaixos/index.d.tsThe file shows that the required parameters and return value types of Axios are defined as follows
Using typescript to implement lightweight Axios
Using typescript to implement lightweight Axios

Implementing Axios

By observing the source codeaxios/lib/axios.jsAnd its use, we can find thataxiosIt’s apromiseFunction and haveaxios.interceptors.requestInterceptor function.

createInstance

Here will simplify the source code, easy to understand

// 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

The implementation of source code is more ingenious

  • Exposure of entry filescreateInstanceFunction; Its internal core is mainlynewOneAxiosClass instancecontextAt the same time, it willAxiosMethod on Prototyperequest(main logic)thisAlways bind tocontext. The aim is to preventthisThere’s something wrong with it.
  • takeAxiosAll the properties and instances on the class prototypecontextCopy it to itbindNew functions generated afterinstance. The purpose is that s can be used inaxiosFunction is similar to the function of interceptoraxios.interceptors.requestConvenient for users to call.

type definition

fromAnalysis of parameters and return valuesThe type to be defined is shown in the screenshot of

Here will simplify the source code, easy to understand

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

The Axios class implements the get method

From the above type definition and usage, and with the help ofXMLHttpRequestTo actually send a request.

Steps are also familiar with the four steps

  • establishXMLHttpRequestexamplerequest
  • callrequest.open()to configuremethods,url
  • monitorrequest.onreadystatechange()Get response
  • callrequest.send()Send 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()
    })
  }
}

The above code can already meet the requirementsInstall native Axios and useNext, we will continue to expand other methods.

Type declaration episode

Due to the use of third-party librariesparse-headersNot at the moment@types/parse-headersSo TS error will be reported when using. On the one hand, due to the problem of time, I will not write a declaration file for this. On the other hand, the core of this project is implementationaxiosSo create a new project in the root directory of the current projecttypings/parse-headers.d.ts

// typings/parse-headers.d.ts
declare module 'parse-headers'

And then modify ittsconfig.jsonto configure

// tsconfig.json
"include": [
  "src",
  "typings" // +
]

Axios class implements post method

First, expand the interface on the server

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

Then replace the interface when you use it

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

Then extend the type

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

Finally, the core logic of sending request is extended

// 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)

Implement error handling mechanism

There are three main error scenarios

  • The network is abnormal. Disconnection
  • Timeout exception. The interface takes more time than configuredtimeout
  • Error status code.status < 200 || status >= 300
// axios/Axios.ts
//Handling network exceptions
request.onerror = () => {
  reject('net::ERR_INTERNET_DISCONNECTED')
}
//Handling timeout exception
if (timeout) {
  request.timeout = timeout
  request.ontimeout = () => {
    reject(`Error: timeout of ${timeout}ms exceeded`)
  }
}
//Processing 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}`)
    }
  }
}

Simulate network exception

Refresh the page to open the consoleNetworkIn 5 secondsOnlineChange toOfflineSimulate 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);

Errors can be captured normally
Using typescript to implement lightweight Axios

Simulation timeout exception

Extending server interface to add 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)
})
// 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)
})

Errors can be captured normally
Using typescript to implement lightweight Axios

Analog error status code

Add configuration error status code interface to extended server 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)
})

Client calls 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)
})

Errors can be captured normally
Using typescript to implement lightweight Axios

Interceptor function

Using interceptors

Server settingscorsWhen isAccess-Control-Allow-HeadersAdd an itemnameTo facilitate the subsequent use of the interceptor to set the request header.

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

Use in clientRequest and responseInterceptor

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

View request header and response body
Using typescript to implement lightweight Axios
Using typescript to implement lightweight Axios

The law of interceptor is obtained

  • Request interceptor added before execution
  • First add first execute first response interceptor

useaxios.interceptors.request.ejectCancels the specified interceptor

// 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)
  })
})
//+ pop up ` 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
})
//+ pop up ` interceptor_ response2`
axios.interceptors.response.eject(interceptor_response2)

2sView the request header and response body
Using typescript to implement lightweight Axios
Using typescript to implement lightweight Axios

Implement interceptor

By using interceptorsaxios.interceptors.request.useDerive the type definition.

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

Mainly the definitionAxiosInterceptorManagerClass anduse、ejectmethod.

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

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

export interface Interceptor<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
    }
  }
}

Pass the previous sectionUsing interceptorsThe interceptor structure defined by the user is shown in the figure below

Using typescript to implement lightweight Axios

// 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 - add before execute
    this.interceptors.request.interceptors.forEach((interceptor: Interceptor<AxiosRequestConfig> | null) => {
      interceptor && chain.unshift(interceptor)
    })
    //2. Response interceptor - add first, execute first
    this.interceptors.response.interceptors.forEach((interceptor: Interceptor<AxiosResponse<T>> | null) => {
      interceptor && chain.push(interceptor)
    })
    //3. Execute in the order after construction
    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
  }
}

As in the third step of the above steps, the constructed queue is executed in sequence, and asynchrony is supported at the same time.

Merge configuration items

byaxiosSet default configuration items, such asmethodsDefault toGETMethods 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])
          }
        }
      }
    }
    // ...
  }
}

To request headerheadersThe purpose of the treatment is topostStyle request is added by default'content-type': 'application/json'The merge configuration item distinguishes whether it is a request method or other request header configuration.

Realize the conversion of request and response

In ordinary workCommon problems of inconsistent naming caused by front-end parallel development or front-end first developmentThe solution is to map the attributes of objects or arrays. Similar solutions such as@careteen/match

The above solutions can be put intoaxiosProvidedtransformRequest/transformResponseIn the conversion function.

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

The implementation mode is before sending the requestThe first step of the request methodAfter sending the requestThe dispatchrequest method accepts the response bodyIt’s time to cut in.

// 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)
    }
    // ...
  }
}

Cancel task function

Use cancel task

In normal work requirements, in some scenarios (leave the page), the next expectation will not be completedpromiseperhapsXHR requestCancel it.

It can be observed firstaxiosUse of

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')

View the console to cancel the task

Using typescript to implement lightweight Axios

Implement cancel task

The implementation idea is similar toHow to terminate promiseThis article is easier to understand.

According to the use of backward type definition

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

Push the mount backward according to the usageCancelToken、isCancel

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

export default axios

newly buildcancel.tsFile implementation cancellation 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))
      }
    }
  }
}

Call at the right time (in the scenario specified by the user)source.cancelMethod)request.abort()And then cancel the task.

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

summary

Through the above simple code to achieve a short version availableaxiosIt is far from perfect.

The purpose is to use the third-party excellent library at the same timeUsageBackward pushBottom layer implementation ideasAnd then cooperateRead the source codeBetter control them.