preface

Recently, I learned VUE3 and TS. After learning vuE3, I was ready to build a project to practice my skills. It suddenly occurred to me that the current wrapped web request seems to be vue2 combined with native JS and AXIos, so when we develop with VUe3 and TS we will definitely have to repackage a more structured Axios-based web request architecture based on the current technology. After encapsulation we can build projects can be directly used, learning this article can let us experience TS in the encapsulation network request to give more perfect security, let us look at the unique charm of TS.

Structure preview and description

  • SRC create the service folder as the main folder
  • Request is the folder that encapsulates the request
  • Config.ts is a fixed configuration for some requests and values in different development environments
  • Errcode. ts indicates the status of the error request code
  • Index. ts is the key configuration for the main construct request class
  • Type. ts indicates the types of custom interfaces used in the export
  • The index.ts in service.ts instantiates the constructor class that generates the actual network request configuration and returns it

Now that we have a clear understanding of the structure, let me start building!

Class constructing ZJRequest

Our main work is to configure in the construction class ZJRequest. After configuration, we only need to instantiate the ZJRequest class and pass in the corresponding configuration to get a complete network request configuration object. It is recommended that you use your own nickname plus Request for naming, which is also a development habit. The network request configuration set up by me includes complete configuration of interceptor and complete configuration of response mask. Let’s take a look at the most standard basic configuration.

Import type {AxiosInstance, AxiosRequestConfig} from 'axios' import axios from 'axios' class {instance: AxiosInstance constructor(config: AxiosRequestConfig) {this.instance = axios.create(config) // Manually create an instance from the incoming configuration}Copy the code

The ZJRequest instance or config parameter is specified in axios, and the type is specified in axios. The ZJRequest instance or config is specified in Axios.

It’s a simple code that just calls AXIos to create a new Instance based on the incoming configuration to get the basic network configuration. Obviously we don’t need such a simple encapsulation, so our encapsulation doesn’t make much sense, so we also know that interceptors and response masks are the main objects to encapsulate. Let’s see how this works.

Interceptors do the reading

Post a complete request code first, so that we can explain how to implement it.

import type { AxiosInstance } from 'axios' import type { KZJRequestInterceptors, KZJRequestConfig } from './type' import { getErrMessage } from './errCode' import axios from 'axios' import { ElLoading } from 'element-plus' import type { ILoadingInstance } from 'element-plus/lib/el-loading/src/loading.type' const DEFAULT_LOADING = true class ZJRequest { instance: AxiosInstance interceptors? : KZJRequestInterceptors showLoading: boolean loading? : ILoadingInstance constructor(config: KZJRequestConfig) {//KZJRequestConfig extends AxiosRequestConfig this. Instance = axios.create(config) // Manually create instance this.interceptors = config.interceptors // add interceptor this.showloading = config.showloading?? DEFAULT_LOADING // Control loading display // use interceptor // 1. From the config interceptor is the corresponding instance of the interceptor enclosing instance. Interceptors. Request. Use (this) interceptors?) requestInterceptor, / / request is successful intercept this. Interceptors?. RequestInterceptorCatch / / request failed to intercept) enclosing the instance. The interceptors. Response. Use ( this.interceptors?.responseInterceptor, / / response successful intercept this. Interceptors?. ResponseInterceptorCatch response failed to intercept) / / / / global request interceptor enclosing instance. The interceptors. Request. Use ((res) If (this.showloading) {this.loading = elloading. service({lock: true, text: 'Loading... ' }) } return res }, (err) = > {return err}) / / global response interceptor enclosing instance. The interceptors. Response. Use ((res) = > {the console. The log (' global successful intercept response) Loading setTimeout(() => {this.loading?.close()}, 2000) return res.data}, Close () // Error status code parsing information err = getErrMessage(err) return err})} Request <T>(config: KZJRequestConfig<T>): Promise<T> { return new Promise((resolve, Reject) => {if (config.interceptors?.requestInterceptor) {if (config.interceptors?.requestInterceptor) {if (config.interceptors? Is the purpose of the interceptor may change so you need to get the latest results are returned to config = config. The interceptors. RequestInterceptor (config) / / whether individual request need to show loading and modify the configuration is inconsistent if (config.showLoading === ! This.showloading) {this.showloading = config.showloading}} // The purpose of the request passed into generic T here and above is that the type of result returned can be defined when it is sent // By default, the res in the responseInterceptor(res) is AxiosResponse; // But we want to use the interface type we defined when we send it. We need to pass in the generic to handle this.instance.request<any, T>(config).then((res) => {if (config.interceptors?.responseinterceptor) {if (config.interceptors?.responseinterceptor) {if (config.interceptors Config. Interceptors. ResponseInterceptor (res) / / no matter in response to the success of failure need to initialize the showliadong values in order to response the next request configuration enclosing showLoading = DEFAULT_LOADING } resolve(res) }, (err) => { this.showLoading = DEFAULT_LOADING reject(err) return err } ) }) } get<T>(config: KZJRequestConfig<T>): Promise<T> { return this.request({ ... config, method: 'GET' }) } post<T>(config: KZJRequestConfig<T>): Promise<T> { return this.request({ ... config, method: 'POST' }) } delete<T>(config: KZJRequestConfig<T>): Promise<T> { return this.request({ ... config, method: 'DELETE' }) } patch<T>(config: KZJRequestConfig<T>): Promise<T> { return this.request({ ... config, method: 'PATCH' }) } } export default ZJRequestCopy the code

Let’s start at the beginning by talking about the interface type in the reference Type folder: type.ts

import type { AxiosResponse, AxiosRequestConfig} from 'axios' // define the responseInterceptor interface type KZJRequestInterceptors<T = AxiosResponse> { requestInterceptor? : (config: AxiosRequestConfig) => AxiosRequestConfig requestInterceptorCatch? : (error: any) => any responseInterceptor? : (res: T) => T responseInterceptorCatch? : (error: Any) => any} export interface KZJRequestConfig<T = AxiosResponse> extends AxiosRequestConfig { interceptors? : KZJRequestInterceptors<T> showLoading? : boolean }Copy the code

Here we can see that we have defined two interfaces, one for the architecture of the interceptor and one to extend the AxiosRequestConfig type. Also, in the figure below, the extension AxiosRequestConfig type is configured specifically for config. Why extend it? The main thing is that we want to add our own interceptor, so we need to extend it on the original AxiosRequestConfig type.

Once interceptors are added, we can use any of them and configure them into the request object. It is worth noting that config here refers to the configuration passed in when the class is instantiated, so the interceptor defined here will be used in the instantiated object. In other words, when we instantiate multiple request objects, each incoming configuration will generate a different interceptor. Some objects may contain all interceptors, while others may have only interceptors for which the request was successfully intercepted, etc. So we can identify and configure the interceptor we want when we instantiate the object, and that’s itThe first interceptor is the default interceptor.

There is also a case where no matter how many objects we instantiate, we want them all to share one or more interceptors. That’s what we’re going to talk aboutThe second type of interceptor is the global interceptor. One of the main uses of global interceptors is to coordinate our response masks, because we need to intercept each request before it is sent, and we need to intercept the response after it is successful. In this case it is best to configure it in the global interceptor.

There is a third type of interceptor, the single-request interceptor. Sometimes we only need a single interceptor for an interface, where neither of the above is obviously appropriate, so we use a single request interceptor instead. It is important to note that the config passed in for the request method is different from the config instantiated by default, so don’t get confused.

The first way to use interceptors is to configure them directly when the object is instantiated

The second type of interceptor is simply configured in the global interceptor

Third use of interceptors: Add the interceptors attribute to the incoming request and configure it

Let’s take a look at the order in which all three interceptors are executed. First, let’s enable all three interceptors and see what happens.



This leads us to the sequence of our interceptors

Graph TD The third separate interface request is successfully blocked --> the second global interface request is successfully blocked --> the first default configuration request is successfully blocked --> the second global interface response is successfully blocked --> the third separate interface response is successfully blocked

Response mask animation for reading

ShowLoading is used to control whether a response mask animation is required

Loading is an instance object created by the loading component, in which close can be called to close the maskThe response mask should be displayed based on the value of the showloading property passed in when instantiating the object, and then the value of showloading in the class should be modified. There is a priority, because we discussed the order in which interceptors are executed above, from which we derive.ShowLoading in the individual interface configuration is preferred, then showLoading in the instantiated object, and DEFAULT_LOADING is used by default if neither is configured.

Some of you may wonder why a single interface is showLoading in the first place. Shouldn’t it be covered by the back?

The first thing you need to understand is the logic, and this code is key. When was it executed? If it is instantiated, the showLoading value is determined according to the configuration at the time of instantiation. If it is not configured, DEFAULT_LOADING is used by default. ShowLoading is not enabled at this time, so they must be the first to decide the value of showLoading. However, if showLoading is configured in a separate interface after the interceptor is enabled, showLoading will overwrite the previous value of showLoading. So the individual interface takes precedence.

this.showLoading = config.showLoading ?? DEFAULT_LOADING // Controls loading display

Then in the global interceptor, we enable the response mask animation when the request succeeds and showLoading is true, and cancel the mask animation when the request response succeeds or fails.The timer here is convenient for testing, after all, the interface request speed is too fast to see the effect.

If the value of showLoading in a separate request configuration is inconsistent with the value of showLoading in the current class, showLoading configured on a separate interface is preferred. After our response completes, whether it succeeds or fails, we need to restore it to its original default, DEFAULT_LOADING.In case an error occurs when the next interface adopts the last value without a value, the override is still performed if there is a value.

Perfecting interface types

get<T>(config: KZJRequestConfig<T>): Promise<T> { return this.request({ ... config, method: 'GET' }) } post<T>(config: KZJRequestConfig<T>): Promise<T> { return this.request({ ... config, method: 'POST' }) } delete<T>(config: KZJRequestConfig<T>): Promise<T> { return this.request({ ... config, method: 'DELETE' }) } patch<T>(config: KZJRequestConfig<T>): Promise<T> { return this.request({ ... config, method: 'PATCH' }) }Copy the code

Without further explanation, this is equivalent to reusing the request method above, but with a different name and modified method request method

Other documents

config.ts

Let BASE_URL = "" const TIME_OUT = 10000 if (process.env.node_env === 'development') {// The development mode is configured across domains see vue.config.js BASE_URL = '/ API '} else if (process.env.node_env === 'production') {// Production mode BASE_URL = 'http://kzj.org/prod'} else { // Test environment BASE_URL = 'http://kzj.org/test'} export {BASE_URL, TIME_OUT}Copy the code

errCode.ts

export function getErrMessage(err: any): void { switch (err.response.status) { case 400: Err. message = 'request error' break case 401: err.message = 'Unauthorized, please login' break case 403: err.message = 'access denied' break case 404: Err. message = 'Error: ${err.response.config.url}' break case 408: err.message = 'Request timed out' break case 500: Err. message = 'Server internal error' break case 501: err.message = 'service not implemented' break case 502: err.message = 'gateway error' break case 503: Err. message = 'Service unavailable' break case 504: err.message = 'Gateway timed out' break case 505: Err. message = 'HTTP version not supported 'break default: } return err }Copy the code

Under the service of the index. Ts

import ZJRequest from './request' import { BASE_URL, TIME_OUT } from './request/config' import localCache from '@/utils/cache' const zjRequest = new ZJRequest({ baseURL: {BASE_URL, timeout: TIME_OUT, showLoading: false, // The default interface has no Loading animation effect. (config) => {// With token interception console.log(' request with default configuration intercepted successfully ') const token = localCache.getCache('token')?? '' if (token && config.headers) { config.headers.Authorization = `Bearer ${token}` } return config }, (err) => {return err}, return responseInterceptor: (res) = > {the console. The log (' default configurations of the corresponding successful intercept) return res}, / / response failed to intercept responseInterceptorCatch: (err) => { return err } } }) export default zjRequestCopy the code

Begin to use

Here we use the login interface to demonstrate use



type.ts

export interface TAccount {
  name: string
  password: string
}

export interface ILoginResult {
  id: number
  name: string
  token: string
}

export interface IDataType<T = any> {
  code: number
  data: T
}
Copy the code

login.ts

import KZJRequest from '.. /index' import { TAccount, ILoginResult, IDataType } from './type' enum LoginApi { AccountLogin = '/login', Menus = '/ menus /', // role/1/menu } export function accountLogin(account: TAccount) { return KZJRequest.post<IDataType<ILoginResult>>({ url: LoginApi.AccountLogin, data: account, showLoading: true }) } export function requestUserInfoById(id: number) { return KZJRequest.get<IDataType>({ url: LoginApi.LoginUserInfo + id }) } export function requestUserMenusByRoleId(id: number) { return KZJRequest.get<IDataType>({ url: LoginApi.UserMenus + id + '/menu' }) }Copy the code

After the definition is complete, import the corresponding interface directly

conclusion

I hope to be helpful to you. If you need a file, you can send it to me. Creation is not easy, hope you can point a thumbs-up, thank you! That’s it for today, and I’ll see you next time.