Encapsulating Taro.request with typescript

Table of Contents

summary

Extended functionality

Implementation details

how to use

Summarize


Summary

Taylor’s request API function is mainly used to request network services. It can initiate asynchronous request services. It is very convenient to use and has been used by many development engineers. In actual projects, it will be encapsulated or extended due to some requirements. The most common requirement is to add a token header to the request. There are several articles on the Internet that introduce how to encapsulate this function, and I learned a lot from these articles. The first few are encapsulations of the javascript version and lack strong type support. This article mainly introduces the use of typescript to encapsulate the request function.

Extended functions

The following code implementation mainly adds the following functional points:

  • If the request does not carry the content-type header, add the “application/json;charset=UTF-8” type
  • According to the program running environment, select the corresponding back-end service address
  • Add the user’s token in the request header
  • The return value is unified to the ResultDto type
  • Verify the business data returned by the background to detect data mismatch errors in advance
  • Unify background errors, HTTP errors and unknown system errors into ResultDto errors.
  • When encountering an invalid token error returned by the background, jump to the login page. The error code for invalid token here is 10002.

Implementation details

import Taro from "@tarojs/taro";
import TokenService from "../token/TokenService";

const jsonHader = "application/json;charset=UTF-8";

function getBaseUrl(): string {
  if (process.env.NODE_ENV === "development") {
    return "http://127.0.0.1:9080";
  } else return "";
}

function exist<T>(a: any, ...attrs: T[]): boolean {
  if (process.env.NODE_ENV === "development") {
    for (let i = 0; i < attrs.length; + + i) {
      let item = attrs[i];
      if (a[item] != "" & amp; & amp; !a[item]) {
        return false;
      }
    }
  }
  return true;
}

export const HTTP_STATUS = {
  SUCCESS: 200,
  CREATED: 201,
  ACCEPTED: 202,
  CLIENT_ERROR: 400,
  AUTHENTICATE: 401,
  FORBIDDEN: 403,
  NOT_FOUND: 404,
  SERVER_ERROR: 500,
  NOT_IMPLEMENTED: 501,
  BAD_GATEWAY: 502,
  SERVICE_UNAVAILABLE: 503,
  GATEWAY_TIMEOUT: 504,
};

export const REFRESH_STATUS = {
  NORMAL: 0,
  REFRESHING: 1,
  NO_MORE_DATA: 2,
};

export const getCurrentPageUrl = (): string => {
  let pages = Taro.getCurrentPages();
  let currentPage = pages[pages.length - 1];
  let url = currentPage.route;
  return url || "";
};

export const pageToLogin = () => {
  TokenService.clear();
  let path = getCurrentPageUrl();
  if (!path.includes("login")) {
    Taro.reLaunch({
      url: "/pages/login/index",
    });
  }
};

export type ResultDto<T> = {
  success: boolean;
  // error number
  errorCode?: string;
  // wrong description
  errorMessage?: string;
  // specific object
  data?: T;
  // error display type: 0 silent; 1 message.warn; 2 message.error; 4 notification; 9 page
  showType?: number;
  // Convenient for back-end Troubleshooting: unique request ID
  traceId?: string;
  // onvenient for backend Troubleshooting: host of current access server
  host?: string;
};

const tokenInterceptor = (chain: Taro.Chain) => {
  const requestParams = chain.requestParams;
  const { header } = requestParams;
  let token = TokenService.load();
  const tokenHeader = {
    Authorization: `Bearer ${token}`,
    // "content-type": jsonHader,
  };
  requestParams.header = { ...tokenHeader, ...header };
  return chain.proceed(requestParams);
};

// Taro provides two built-in interceptors
// logInterceptor - used to print request-related information
// timeoutInterceptor - throws an error if the request times out.
// const interceptors = [customInterceptor, Taro.interceptors.logInterceptor]
const interceptors = [tokenInterceptor];
interceptors.forEach((interceptorItem) => Taro.addInterceptor(interceptorItem));

type FilterOptional<T extends object> = Pick<T, Exclude<{ [K in keyof T]: T extends Record<K, T[K]> ? K : never }[keyof T], undefined>>
type kType<T extends object> = keyof FilterOptional<T>;

const request = <T extends object>(params: Taro.request.Option, ...attrs: kType<T>[]): Promise<ResultDto<T>> => {
  let { url, header } = params;
  const baseUrl = getBaseUrl();
  const url2 = baseUrl + url;
  let contentType = jsonHader;
  contentType = header?.contentType || jsonHader;
  const option = {
    ...params,
    header: { "content-type": contentType },
    timeout: 50000,
    url: url2,
  };

  Taro.showLoading({
    title: "Loading",
  });
  return Taro.request(option)
    .then((res: Taro.request.SuccessCallbackResult<any>) => {
      const pos = contentType.indexOf("application/json");
      const { statusCode, data } = res;
      if (pos == -1) {
        return { success: true, data };
      }
      // As long as the request is successful, no matter what status code is returned, this callback will be used
      if (statusCode == HTTP_STATUS.SUCCESS) {
        if (data?.success) {
          // Successful and data obtained
          if(!exist(data.data, ...attrs)){
            console.error("The return value does not contain required fields", data.data, attrs);
            return Promise.resolve({...data, success: false, errorCode: "BizError", errorMessage: "The return value cannot match"});
          }
          return Promise.resolve(data);
        }
        // Successful, but an error occurred during processing
        let dto: ResultDto<T> = data;
        console.warn(
          `url =${url2}, traceid=${dto.traceId}, error code=${dto.errorCode}, error msg=${dto.errorMessage}`
        );
        if(dto.errorCode === "10002"){
          pageToLogin();
        }
        Promise.resolve(dto);
      }

      let dto: ResultDto<T> = {
        success: false,
        errorCode: statusCode + "",
        errorMessage: `http status: ${statusCode}`,
      };
      if (statusCode === HTTP_STATUS.NOT_FOUND) {
        dto.errorMessage = "The requested resource does not exist";
      } else if (statusCode === HTTP_STATUS.FORBIDDEN) {
        dto.errorMessage = "No permission to access";
      } else if (statusCode === HTTP_STATUS.AUTHENTICATE) {
        dto.errorMessage = "Authentication required";
      } else if (statusCode === HTTP_STATUS.SERVER_ERROR) {
        dto.errorMessage = "Server error";
      } else if (statusCode === HTTP_STATUS.NOT_IMPLEMENTED) {
        dto.errorMessage = "Service not implemented";
      } else if (statusCode === HTTP_STATUS.BAD_GATEWAY) {
        dto.errorMessage = "There is a problem with the service gateway";
      } else if (statusCode === HTTP_STATUS.SERVICE_UNAVAILABLE) {
        dto.errorMessage = "The server cannot process the request";
      }
      Taro.showToast({ title: dto.errorMessage || "", icon: "error" });
      return Promise.resolve(dto);
    })
    .catch((error) => {
      console.error("http return error,", error);
      return Promise.resolve({
        success: false,
        errorCode: "system",
        errorMessage: error.toString(),
      });
    })
    .finally(() => Taro.hideLoading());
};

export default request;

The TokenService class in the above code provides local and reducer access functions for tokens. This class also references other files, which will not be expanded here.

import Taro from '@tarojs/taro'
import store from '../../store'
import {setValue} from '../../store/model/token'
import StringUtil from '@/utils/stringUtil';

export default class TokenService{

    public static save(data: string): void{
        Taro.setStorage({
            key: "token",
            data
        })
        store.dispatch(setValue({token: data}));
    }

    public static load(): string {
        const {TokenStateReducer} =store.getState();
        if(!StringUtil.isEmpty(TokenStateReducer.token)){
            return TokenStateReducer.token;
        }
        try {
            const token = Taro.getStorageSync<string>('token');
            store.dispatch(setValue({token}));
            return token
          } catch (e) {
            console.log("can not read token in storage");
            return "";
          }
    }

    public static clear(): void {
        TokenService.save("");
    }
}

How to use

Here is an example of obtaining a graphical verification code

import request, { ResultDto } from '../request'
export type CaptchaResponseDto = {
  key: string;
  code?: string;
  image: string;
}

// Get image verification code
export async function getCaptchaImage(): Promise<ResultDto<CaptchaResponseDto>>{
    return request<CaptchaResponseDto>({url: "/public/image-captcha", method: 'GET'}, "key", "image");
}

Summary

Through the above encapsulation, it is easier to use the request function for getCaptchaImage, and it also has complete type support, which improves the readability and maintainability of the system.