uni-app 中的接口请求封装

4,203 阅读9分钟

简介

项目工程中,有一个很重要的封装就是接口请求的封装,作为整个项目接口请求的出口和入口在这里可以做很多事情,如在请求头header中增加token、signature,如对不同响应状态的拦截等,在多数的项目中都是使用axios来封装的,但是在uni-app项目中需要更轻量,兼容性更好,我们可以选择lunch-request。

梳理需求

  1. 在header 中增加 token 字段用于 用户权限验证
  2. 在header 中增加 signature 字段用于 数据验签
  3. 在接口请求时增加loading控制
  4. 对响应接口进行拦截 如 401、200、500 等
  5. 对响应数据进行类型声明
  6. 对重复请求进行拦截
  7. 对失败请求进行重新发起

功能实现

需求1、需求2、需求3 都可以通过请求拦截的方式实现部分代码如下:

request.interceptors.request.use((config: HttpRequestConfig) => {
  /**
   *请求头增加token
   */
  config.header.token = uni.getStorageSync("token");
  /**
   *请求头增加接口验签
   */
  if (config.custom?.isSignatrue) {
    config.header["signature"] = "";
    if (config.data) {
      config.header["signature"] = md5(md5(JSON.stringify(config.data)));
    } else if (config.hasOwnProperty("params") && config.params) {
      config.header["signature"] = md5(md5(objToPathStr(config.params)));
    }
  }
  /**
   * 请求接口增加loading
   */
  if (config.custom?.loading) {
    uni.showLoading({
      title:
        typeof config.custom?.loading === "string"
          ? config.custom?.loading
          : "加载中",
      mask: true,
    });
  }
  return config;
});

备注:
验签需要用到md5加密,所以需要手动引入md5模块 - npm i md5,这种方式可在小程序端不兼容,需手动引入md5.js文件
lunch-request 提供了自定义参数custom 可以实现很多自定义功能,这里loading 就可以通过它来配置

需求4、需求5、需求7都可以在响应拦截中实现,具体代码如下:

request.interceptors.response.use(
  (response) => {
    return new Promise((resolve, reject) => {
      // 清除loading
      if (response.config.custom?.loading) {
        uni.hideLoading();
      }
      // 重复请求拦截 请求成功 删除缓存
      let reqKey = generateReqKey(response.config);
      delete cachcRequest[reqKey];

      const resData = response.data;
      if (resData.code === 200) {
        resolve(resData);
      } else if (resData.code === 401) {
        uni.showToast({
          title: "登录过期,请重新登录",
          icon: "none",
          mask: true,
        });
        // 使用防抖 返回首页,防止多次跳转
        backDebounce();
        reject(resData.message);
      } else if (resData.code === 500) {
        response.config.custom.withoutErrorToast ||
          uni.showToast({
            title: resData.message,
            icon: "none",
            duration: 4000,
          });
        reject(response);
      } else {
        resolve(response);
      }
    });
  },
  // 对响应错误做点什么 (statusCode !== 200)
  async (response) => {
    if (response.config.custom?.loading) {
      uni.hideLoading();
    }
    var config = response.config;

    console.log("response", config, config.custom.retry);

    // 重复请求拦截 - 如果是重复请求,不会继续发送请求,直接返回上一次请求的结果
    let reqKey = generateReqKey(response.config);
    if (cacheRequestToast[reqKey]) {
      delete cacheRequestToast[reqKey];
      return Promise.reject(response);
    }
    if (!config || !config.custom.retry) return Promise.reject(response);
    // 请求超时判断
    if (response?.errMsg === "request:fail timeout") {
      uni.showToast({
        title: "请求超时",
        icon: "error",
        duration: 3000,
        mask: true,
      });
      return Promise.reject(response);
    }
    if (response.statusCode) {
      return Promise.reject({ type: "none", msg: "系统维护中,请稍后再试" });
    }
    // 一般是网络断开 或者 后端卡死情况
    if (!response.statusCode) {
      // 重复请求拦截 请求失败 删除缓存
      delete cachcRequest[reqKey];
      // custom.retryCount用来记录当前是第几次发送请求
      config.custom.retryCount = config.custom.retryCount || 0;
      // 如果当前发送的请求大于等于设置好的请求次数时,不再发送请求,返回最终的错误信息
      if (config.custom.retryCount >= config.custom.retry) {
        uni.showToast({
          title: "当前网络不稳定,请检查您的网络设置",
          icon: "none",
          duration: 3000,
          mask: true,
        });
        return Promise.reject(response);
      }
      // 记录请求次数+1
      config.custom.retryCount += 1;
      // 设置请求间隔 在发送下一次请求之前停留一段时间,时间为上方设置好的请求间隔时间
      let backOff = new Promise<null>((resolve) => {
        setTimeout(() => {
          resolve(null);
        }, 500);
      });
      // 再次发送请求
      await backOff;
      return await request.request(config);
    }
    return Promise.reject(response);
  }
);

备注:
一般接口响应会与后端接口指定统一规范如code的状态代表值以及后端返回数据结构,在这里就规定的是后端返回结构为{code:200,result:'内容',message:'成功'},所以利用code码的不同值做不同的处理。
对于401 用户身份过期返回登录页需要用到防抖,避免多次触发

请求失败重发功能逻辑

  1. 什么时候重新发起请求?
    当请求响应体不存在的时候,如直接断网情况,请求体都没有返回回来,可重新尝试一次
  2. 重复发起逻辑 利用custom 可以声明一个 retry 用于定义重新请求的次数,默认为1次,当发现请求没有响应体时,便进入重新发起的逻辑,先定义一个重新发起的次数retryCount,初次为0,如果retryCount < retry 就发起一次请求并retryCount++,如果最终还是请求失败则提示网络有问题

重复请求进行拦截逻辑

  1. 拦截判断
    当发起一个请求后,在没有收到响应值时,再发起一个相同的请求时,丢弃该请求
  2. 重复请求逻辑
/**
 *重复请求拦截 - 如果上一次请求还没有结束,相同的请求不会继续发送
 * cachcRequest:用与缓存每次请求的key
 * cacheRequestToast:用于缓存每次请求的key,用于判断是否需要弹出提示
 */
let cachcRequest: any = {} as any;
let cacheRequestToast: any = {} as any;

利用cachcRequest缓存当前请求的接口,保存的键是通过 method,url,param ,data生成的唯一key 生成代码如下:

// 生成请求key
function generateReqKey(config: HttpRequestConfig) {
  const { method, url, params, data } = config;
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join("&");
}

然后在请求时,会判断缓存里面是否请求过改接口,如果请求过,就放弃请求,放请请求会用到abort()函数具体实现如下:

const request = new Service({
  baseURL,
  timeout: 60 * 1000,
  custom: {
    loading: false, //是否增加loading,可以设置为true 或者 string
    retry: 1, // 设置自动发送请求次数,默认1次
    closerePeatIntercept: false, // 是否关闭该接口重复请求拦截
    isSignatrue: true, // 是否开启验签
    withoutErrorToast: false, // 是否关闭错误提示
  },
  // 重复请求拦截 - 如果上一次请求还没有结束,相同的请求不会继续发送
  getTask: (task, config) => {
    if (config.custom.closerePeatIntercept) return;
    let reqKey = generateReqKey(config);
    if (cachcRequest[reqKey]) {
      cacheRequestToast[reqKey] = 1;
      //放弃请求
      task.abort();
    } else {
      cachcRequest[reqKey] = 1;
    }
  },
});

请求成功后清除缓存中的key,以便下次请求能够正常请求。

  1. 放弃请求因为没有相应体 会 触发重复请求发起 所以需要 cacheRequestToast 来缓存重复请求方法,用于在请求失败重新发起逻辑里面过滤,过滤代码如下:
 // 重复请求拦截 - 如果是重复请求,不会继续发送请求,直接返回上一次请求的结果
let reqKey = generateReqKey(response.config);
if (cacheRequestToast[reqKey]) {
  delete cacheRequestToast[reqKey];
  return Promise.reject(response);
}

对响应数据进行类型声明 实现逻辑

其实lunch-request正常发起请求时可以增加类型定义 如 request.get(url) 写法,但是这种返回的类型说明太复杂,有body、data这些并不是我们完全想要的类型,我们其实定义最多的类型是接口返回的数据体即res.data.result 中result的类型,所以我想通过一次二次封装 实现Type 对应的 就是result的类型。 具体实现是将lunch-request 中的各个请求方法进行了重新赋值,具体代码如下:

/*
 *对请求方法进行封装,方便类型定义
 */

const requestMethods = [
  "GET",
  "POST",
  "PUT",
  "DELETE",
  "CONNECT",
  "HEAD",
  "OPTIONS",
  "TRACE",
  "UPLOAD",
  "DOWNLOAD",
];

// 请求方法定义
interface RequestOptions {
  get<T = any>(
    url: string,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  upload<T = any>(
    url: string,
    config?: HttpRequestConfig<UniApp.UploadTask>
  ): Promise<T>;
  delete<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  head<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  post<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  put<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  connect<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  options<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  trace<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  download(
    url: string,
    config?: HttpRequestConfig<UniApp.DownloadTask>
  ): Promise<HttpDownloadResponse>;
}

let exportMethod = {};
requestMethods.forEach((item) => {
  request[item.toLowerCase()] = async <T = any>(
    url: string,
    ...args: any[]
  ) => {
    let temp = null;
    if (["get", "upload", "download"].includes(item.toLowerCase())) {
      temp = {
        url,
        method: item,
        ...(args[0] || {}),
      };
    } else {
      temp = {
        url,
        method: item,
        data: args[0],
        ...(args[1] || {}),
      };
    }
    const res = await request.request(temp);
    return (res.data.result === undefined ? res.data : res.data.result) as T;
  };
  exportMethod[item.toLowerCase()] = request[item.toLowerCase()];
});

export default exportMethod as RequestOptions;

这样在使用时就可以直接定义类型作用到result上

使用

最终request.ts 文件会导出 request 对象,我们在api文件中就可以引入request方法并进行接口定义,示例代码如下:

import request from "@/utils/request";
import { Iindex } from "./types/index";

export function reqAddTesting(data) {
  return request.post<Iindex>("/pet", data, {
    custom: {
      loading: true,
    },
  });
}                    

代码TS提示效果

image.png

image.png image.png

这样在每次请求接口时就可以先与后端定义类型,然后在获取结果时得到类型推断。

总结

接口类型声明是一个比较庞大而繁琐的事情,可以借助一些工具帮我们把JSON对象转为TS类型声明如:mrpandaliu.github.io/json-to-ts 网址,他的好处就是在于后期我们维护系统时更加方便,例如有一天接口突然少了一个字段,我们在类型声明哪里去掉,就可以通过报错知道我们业务代码那些地方有问题以及在日常编写时代码提示更加友好,总之仁者见仁智者见智。

request.ts文件的封装是一个项目搭建的基础,需要自身项目与后端制定的接口规范做一些小的调整,但是总体的思路一般是不会变的如 请求添加请求头,响应增加根据状态拦截等。

附上完整代码

import { debounce } from "lodash";
import Service, { HttpDownloadResponse, HttpRequestConfig } from "luch-request";
import { MD5 } from "crypto-js";

const backendUR = {
  dev: "http://127.0.0.1:4523/m1/1062323-0-default/",
  test: "http://127.0.0.1:4523/m1/1062323-0-default/",
  prod: "http://127.0.0.1:4523/m1/1062323-0-default/",
};

// 当前环境
export const ProcessEnv: "dev" | "test" | "prod" = "dev";
// 后端地址
export const baseURL = backendUR[ProcessEnv]; //本地/

/**
 *重复请求拦截 - 如果上一次请求还没有结束,相同的请求不会继续发送
 * cachcRequest:用与缓存每次请求的key
 * cacheRequestToast:用于缓存每次请求的key,用于判断是否需要弹出提示
 */
let cachcRequest: any = {} as any;
let cacheRequestToast: any = {} as any;

const request = new Service({
  baseURL,
  timeout: 60 * 1000,
  custom: {
    loading: false, //是否增加loading,可以设置为true 或者 string
    retry: 1, // 设置自动发送请求次数,默认1次
    closerePeatIntercept: false, // 是否关闭该接口重复请求拦截
    isSignatrue: true, // 是否开启验签
    withoutErrorToast: false, // 是否关闭错误提示
  },
  // 重复请求拦截 - 如果上一次请求还没有结束,相同的请求不会继续发送
  getTask: (task, config) => {
    if (config.custom.closerePeatIntercept) return;
    let reqKey = generateReqKey(config);
    if (cachcRequest[reqKey]) {
      cacheRequestToast[reqKey] = 1;
      task.abort();
    } else {
      cachcRequest[reqKey] = 1;
    }
  },
});

/**
 * @author: pxt
 * @return {*}
 * @description: 跳转到登陆也,使用防抖
 */
const backDebounce = debounce(
  () => {
    uni.reLaunch({
      url: "/common-pages/account",
    });
  },
  2000,
  {
    trailing: true,
  }
);

request.interceptors.request.use((config: HttpRequestConfig) => {
  /**
   *请求头增加token
   */
  config.header.token = uni.getStorageSync("token");
  /**
   *请求头增加接口验签
   */
  if (config.custom?.isSignatrue) {
    config.header["signature"] = "";
    if (config.data) {
      config.header["signature"] = MD5(MD5(JSON.stringify(config.data)));
    } else if (config.hasOwnProperty("params") && config.params) {
      config.header["signature"] = MD5(MD5(objToPathStr(config.params)));
    }
  }
  /**
   * 请求接口增加loading
   */
  if (config.custom?.loading) {
    uni.showLoading({
      title:
        typeof config.custom?.loading === "string"
          ? config.custom?.loading
          : "加载中",
      mask: true,
    });
  }
  return config;
});
request.interceptors.response.use(
  (response) => {
    return new Promise((resolve, reject) => {
      // 清除loading
      if (response.config.custom?.loading) {
        uni.hideLoading();
      }
      // 重复请求拦截 请求成功 删除缓存
      let reqKey = generateReqKey(response.config);
      delete cachcRequest[reqKey];

      const resData = response.data;
      if (resData.code === 200) {
        resolve(resData);
      } else if (resData.code === 401) {
        uni.showToast({
          title: "登录过期,请重新登录",
          icon: "none",
          mask: true,
        });
        // 使用防抖 返回首页,防止多次跳转
        backDebounce();
        reject(resData.message);
      } else if (resData.code === 500) {
        response.config.custom.withoutErrorToast ||
          uni.showToast({
            title: resData.message,
            icon: "none",
            duration: 4000,
          });
        reject(response);
      } else {
        resolve(response);
      }
    });
  },
  // 对响应错误做点什么 (statusCode !== 200)
  async (response) => {
    if (response.config.custom?.loading) {
      uni.hideLoading();
    }
    var config = response.config;

    console.log("response", config, config.custom.retry);

    // 重复请求拦截 - 如果是重复请求,不会继续发送请求,直接返回上一次请求的结果
    let reqKey = generateReqKey(response.config);
    if (cacheRequestToast[reqKey]) {
      delete cacheRequestToast[reqKey];
      return Promise.reject(response);
    }
    if (!config || !config.custom.retry) return Promise.reject(response);
    // 请求超时判断
    if (response?.errMsg === "request:fail timeout") {
      uni.showToast({
        title: "请求超时",
        icon: "error",
        duration: 3000,
        mask: true,
      });
      return Promise.reject(response);
    }
    if (response.statusCode) {
      return Promise.reject({ type: "none", msg: "系统维护中,请稍后再试" });
    }
    // 一般是网络断开 或者 后端卡死情况
    if (!response.statusCode) {
      // 重复请求拦截 请求失败 删除缓存
      delete cachcRequest[reqKey];
      // custom.retryCount用来记录当前是第几次发送请求
      config.custom.retryCount = config.custom.retryCount || 0;
      // 如果当前发送的请求大于等于设置好的请求次数时,不再发送请求,返回最终的错误信息
      if (config.custom.retryCount >= config.custom.retry) {
        uni.showToast({
          title: "当前网络不稳定,请检查您的网络设置",
          icon: "none",
          duration: 3000,
          mask: true,
        });
        return Promise.reject(response);
      }
      // 记录请求次数+1
      config.custom.retryCount += 1;
      // 设置请求间隔 在发送下一次请求之前停留一段时间,时间为上方设置好的请求间隔时间
      let backOff = new Promise<null>((resolve) => {
        setTimeout(() => {
          resolve(null);
        }, 500);
      });
      // 再次发送请求
      await backOff;
      return await request.request(config);
    }
    return Promise.reject(response);
  }
);

/*
 *对请求方法进行封装,方便类型定义
 */

const requestMethods = [
  "GET",
  "POST",
  "PUT",
  "DELETE",
  "CONNECT",
  "HEAD",
  "OPTIONS",
  "TRACE",
  "UPLOAD",
  "DOWNLOAD",
];

// 请求方法定义
interface RequestOptions {
  get<T = any>(
    url: string,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  upload<T = any>(
    url: string,
    config?: HttpRequestConfig<UniApp.UploadTask>
  ): Promise<T>;
  delete<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  head<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  post<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  put<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  connect<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  options<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  trace<T = any>(
    url: string,
    data?: AnyObject,
    config?: HttpRequestConfig<UniApp.RequestTask>
  ): Promise<T>;
  download(
    url: string,
    config?: HttpRequestConfig<UniApp.DownloadTask>
  ): Promise<HttpDownloadResponse>;
}

let exportMethod = {};
requestMethods.forEach((item) => {
  request[item.toLowerCase()] = async <T = any>(
    url: string,
    ...args: any[]
  ) => {
    let temp = null;
    if (["get", "upload", "download"].includes(item.toLowerCase())) {
      temp = {
        url,
        method: item,
        ...(args[0] || {}),
      };
    } else {
      temp = {
        url,
        method: item,
        data: args[0],
        ...(args[1] || {}),
      };
    }
    const res = await request.request(temp);
    return (res.data.result === undefined ? res.data : res.data.result) as T;
  };
  exportMethod[item.toLowerCase()] = request[item.toLowerCase()];
});

export default exportMethod as RequestOptions;

/**
 *工具函数
 */

// 生成请求key
function generateReqKey(config: HttpRequestConfig) {
  const { method, url, params, data } = config;
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join("&");
}
// 对象转字url格式
function objToPathStr(val: object) {
  let str = "";
  for (const key in val) {
    str += key + "=" + (val[key] + "") + "&";
  }
  str = str.substring(0, str.length - 1);
  return str;
}