preface

The release of Vue3 helped popularize typescript, and now ts is becoming more widely used. Axios, by far the most popular HTTP library, lives in code developed by the Vue, React, and Angular front-end frameworks. In the current development environment, there are more and more opportunities for TS to work side by side with Axios. This article is all about taking advantage of TS features to use AXIOS in your code cleanly and efficiently.

Prior to the start

This article will encapsulate AXIOS from scratch and work your way up, allowing you to read it slowly or skip to the full code.

Interface to prepare

Now that you’re going to use AXIos, you definitely need to prepare the appropriate back-end environment. You can use your own back-end interface, Mock. Js, or an online Mock platform. I won’t go into the details here.

// GET /api/success
{
  "code": 0."message": "Request successful"."data": {
    "name": "Administrator"}}// GET /api/fail
{
  "code": - 1."message": "Request failed: XXX error!."data": null
}
Copy the code

Experimental environment

First, encapsulate axios in a basic way and write the API that calls the above two test interfaces. Any js transition guy can easily write the following code:

// @/utils/request.ts
import axios from 'axios'

const request = axios.create({
  baseURL: '/api'
})

export default request

// @/api/test.ts
export const successApi = () = > {
  return request({
    url: '/success'.method: 'get'})}export const failApi = () = > {
  return request({
    url: '/fail'.method: 'get'})}Copy the code

Finally, simply write down the page that calls the above interface, I used Vue3, here any other framework will do:

Note: To simplify the code here, do not write a message prompt component, directly useconsole.logwithconsole.errorTo replace the

// App.tsx
import { successApi, failApi } from '@/api/test'

export default {
  name: 'App',
  setup () {
    // Handle the click event
    const handleClick = async (isSuccess: boolean) = > {const api = isSuccess ? successApi : failApi
      const res = await api()
      if (res.data.code === 0) {
        console.log(res.data.message) // Success message
        console.log(res.data.data.name)
      } else {
        console.error(res.data.message) // A failure message is displayed}}/ / render function
    return () = > (
      <div>
        <button onClick={() = >HandleClick (true)} > success</button>
        <button onClick={() = >HandleClick (false)} > failure</button>
      </div>)}}Copy the code

When the experimental environment is set up and the next two interfaces are connected, you can continue.

Introduce the Response interceptor

The above code implements functional logic, but it has two obvious problems:

  1. Many interfaces have a message prompt, and writing a message prompt directly into a component repeats a lot of code logic.
  2. res.data.data.nameThe call chain is so long, in fact, that we tend to only use parts of res.data in components.

Both of these problems can be solved by introducing axios’s own Response interceptor. In JS, the following code solves the problem:

// @/utils/request.ts
request.interceptors.response.use((response) = > {
  const { data } = response
  data.code === 0
    ? console.log(data.message) // Success message
    : console.error(data.message) // A failure message is displayed
  return data
})

// App.tsx
import { successApi, failApi } from '@/api/test'

export default {
  name: 'App',
  setup () {
    // Handle the click event
    const handleClick = async (isSuccess: boolean) = > {const api = isSuccess ? successApi : failApi
      const data = await api()
      if (data.code === 0) {
        console.log(data.data.name)
      }
    }
    / / render function
    return () = > (
      <div>
        <button onClick={() = >HandleClick (true)} > success</button>
        <button onClick={() = >HandleClick (false)} > failure</button>
      </div>)}}Copy the code

Data in the const data = await API () is parsed by the compiler as AxiosResponse

, which does not have the code attribute, and it will report an error when executed on if (data.code === 0). The AxiosResponse

type corresponds to const {data} = response and does return this response when there is no interceptor.

To summarize the reason: Axios does not change its return type based on the passed Response interceptor function type, so a similar compiler problem occurs when the Response interceptor’s return type is not AxiosResponse

.

Simple and crude solution

A single line of code can solve this problem:

// App.tsx
const data: any = await api()
Copy the code

The compiler no longer reports errors, and all logic works.

Custom Response operation

However, as the saying goes, once you enter any, you are like the sea. Although the current data type is any, the above method is too simple and rough, which is not good for the subsequent deep encapsulation. Here, we can use custom Response operation to solve the problem:

// @/utils/request.ts
import axios, { AxiosRequestConfig } from 'axios'

const instance = axios.create({
  baseURL: '/api'
})

const request = async (config: AxiosRequestConfig) => {
  const { data } = await instance(config)
  data.code === 0
    ? console.log(data.message) // Success message
    : console.error(data.message) // A failure message is displayed
  return data
}

export default request
Copy the code

We can remove any from app. TSX:

// App.tsx
const data = await api()
Copy the code

OK, the compiler will not report any more errors.

Use generic – return type declarations

The data type is still any, and we don’t have the type check and prompt that ts does when we operate on it. When we communicate with the back end, we usually have a fixed format, so we can declare the return type again:

// @/types/index.ts
interface MyResponseType {
  code: number;
  message: string;
  data: any;
}
Copy the code

Axios also provides us with a very friendly generic method: Axiosinstance.request, which specifies the return type of Response.data:

// @/utils/request
import { MyResponseType } from '@/types'

const request = async (config: AxiosRequestConfig): Promise<MyResponseType> => {
  const { data } = await instance.request<MyResponseType>(config)
  data.code === 0
    ? console.log(data.message) // Success message
    : console.error(data.message) // A failure message is displayed
  return data
}
Copy the code

You can now enjoy the intelligent hints of TS when calling properties such as code and message on data. If you need to communicate with multiple backends, you simply declare multiple return types and write corresponding request functions.

Two-tier generics – Further encapsulation

After the above operation, the package has been basically perfect, but still a little inadequate. In the real world, we already know from the interface document provided by the backend that the interface/API /success must return a type with a name attribute, which is also the type of the data attribute in MyResponseType, but when we use data.data in app.tsx, It is still an any type and does not effectively generate an intelligent hint for our name attribute. You can solve this problem by declaring the type of the inner data in the returned result at the time the request is made

First define the inner data type (here defined as User) and change MyResponseType to the generic type:

// @/types/index.ts
export interface MyResponseType<T = any> {
  code: number;
  message: string;
  data: T;
}

export interface User {
  name: string;
}
Copy the code

Then make the request function generic as well:

// @/utils/request
const request = async <T = any>(config: AxiosRequestConfig): Promise<MyResponseType<T>> => {
  const { data } = await instance.request<MyResponseType<T>>(config)
  data.code === 0
    ? console.log(data.message) // Success message
    : console.error(data.message) // A failure message is displayed
  return data
}
Copy the code

Specify the generic type when the request is initiated:

// @/api/test.ts
export const successApi = () = > {
  return request<User>({
    url: '/success'.method: 'get'})}export const failApi = () = > {
  return request<User>({
    url: '/fail'.method: 'get'})}Copy the code

In app.tsx, the outer data is resolved to be of type MyResponseType

, and the inner data is resolved to be of type User. When using data.data.name, you can get an intelligent prompt.

// App.tsx
const data = await api()
if (data.code === 0) {
    console.log(data.data.name)
}
Copy the code

Error handling – final refinement

The async await syntax that ES6 gives us can get rid of the multi-layer callback hell of then catch, but the block of code that performs the asynchronous operation still needs to be wrapped in a try and catch block, to simplify the code and reduce nesting and avoid wrapping a try and catch every time an asynchronous request is called. The author does not use the try catch block here, but directly judge whether the request is successful through the code attribute in Response (0 means success, -1 means failure). In order to further reduce the code nesting, it can be modified in this way (after the success of the request, there will always be subsequent operations, and the failure of the request has been prompted in the message in the custom request. Generally there will be no further operations) :

// App.tsx
const data = await api()
if(data.code ! = =0) {
    // If there is a logic that the request failed, execute it here
    return
}
// Execute the request success logic
console.log(data.data.name)
Copy the code

However, if you do not use the try catch block at all, you will not be able to catch the error code 4xx, 5xx, and the user will not receive the message. This is obviously not a good experience, so we will catch and handle these errors in the interceptor:

// @/utils/request
const request = async <T = any>(config: AxiosRequestConfig): Promise<MyResponseType<T>> => {
  try {
    const response = await instance(config)
    const data: MyResponseType<T> = response.data
    data.code === 0
      ? console.log(data.message) // Success message
      : console.error(data.message) // A failure message is displayed
    return data
  } catch (err) {
    const message = err.message || 'Request failed'
    console.error(message) // Network error message
    return {
      code: -1,
      message,
      data: null as any}}}Copy the code

Note: The data attribute is forced to any in the catch block only to avoid type checking of the data in MyResponseType and should not be used if the request has already reported an error

The complete code

The complete code is attached for reference:

Axios encapsulates the code

// @/types/index.ts
export interface MyResponseType<T = any> {
  code: number;
  message: string;
  data: T;
}

// @/utils/request.ts
import axios, { AxiosRequestConfig } from 'axios'
import { MyResponseType } from '@/types'

const instance = axios.create({
  baseURL: '/api'
})

const request = async <T = any>(config: AxiosRequestConfig): Promise<MyResponseType<T>> => {
  try {
    const { data } = await instance.request<MyResponseType<T>>(config)
    data.code === 0
      ? console.log(data.message) // Success message
      : console.error(data.message) // A failure message is displayed
    return data
  } catch (err) {
    const message = err.message || 'Request failed'
    console.error(message) // A failure message is displayed
    return {
      code: -1,
      message,
      data: null as any}}}export default request
Copy the code

Use the sample

// @/types/index.ts
export interface User {
  name: string;
}

// @/api/test.ts
import { User } from '@/types'
import request from '@/utils/request'

export const successApi = () = > {
  return request<User>({
    url: '/success'.method: 'get'})}export const failApi = () = > {
  return request<User>({
    url: '/fail'.method: 'get'})}Copy the code

Learn more

In this paper, the encapsulation of AXIOS can meet the most commonly used application scenarios. If the business scenario needs deeper encapsulation, the author recommends the reference of Vue-Vben-Admin. The warehouse is a background management system template based on Vue3+TS, which carries out a deep encapsulation of AXIOS.