阅读 3241

百万PV商城实践系列 -团队接口请求封装实践

⚠️ 本文为掘金社区首发签约文章,未获授权禁止转载

简介

本篇文章是商城实践系列的第三篇文章,主要为大家带来在项目开发时对接口请求的一些配置,方便团队开发时做一些业务上的处理。

很多朋友在日常开发中,不论是用原生ajax``、``fetch也好,还是比较流行的axiosumi-request等封装库,都会对其做一层业务上的封装来做一些请求上的交互处理,如:绑定Token、登录过期、请求错误提示,接口重试等常见的操作。

那么,如何在项目中封装一个接口请求方式,在满足业务需求的同时又可以减少一些重复工作呢?

今天,我会先带大家初始化一个项目,在此基础上,再对接口处理的一些常用请求方式的封装和插件做梳理。它们可以在前后端联调时,减少部分通用逻辑的处理,在提升业务开发效率的同时,也能够帮助团队实现统一化接口自动生成与管理。

这些常见的封装与配置主要有五大类,我把它们都列在了下图中。

image.png

本文使用umi-request为主要的请求库选择,它是基于fetch请求方式的扩展封装库。具体了解可以看:umi-request

初始化项目

在开始之前,为了更好地搭建我们对于接口请求封装,需要进行一些前置工作,来安装相对应的工具库。那么,下面我会一步步带大家来初始化我们的请求文件,同时进行简单的配置初始化。

首先,我们需要安装umi-request,顺便初始化项目。

// #npm
npm install --save umi-request

// #yarn
yarn add umi-request
复制代码

然后,创建一个封装文件,比如我在utils下创建了request.ts做为封装文件。

image.png

最后,在文件中,初始化当前的请求实例,通过umi-request中的extend方法初始化请求配置,且会返回一个RequestMethod实例。

在这里配置了超时时间(timeout)错误处理函数(errorHandler), 请求前缀(prefix)等基本参数。

import { extend } from 'umi-request';



/** 配置request请求时的默认参数 */

const request = extend({

  prefix: INTERFACE_URL,

  timeout: 3000,

  errorHandler

});
复制代码

当我们初始化相对应的配置后,我们生成了用于接口请求函数的request实例。下面,我们需要做的事情就是配置好相对应的拦截器钩子。

拦截器

在初始化章节中,我们提到了拦截器,那么拦截器是什么、能干什么?可能很多刚接触前端开发的小伙伴会“满头雾水”,下面,我就来讲下拦截器的机制是什么,以及如何配置我们项目中的拦截器。

首先,拦截器的机制,如下图所示。我们可以通过拦截器的机制分别对请求前、响应后做一些中间件处理的过程。

在这个过程可以通过当前请求实例request中的interceptorsAPI,来做当前的拦截器。

  • request.interceptors.request.use: 请求拦截器
  • request.interceptors.response.use: 响应拦截器

在请求拦截器中,可以对当前请求的请求地址(url)请求配置(options)等进行修改,随后请求将会使用新的请求配置做接口请求。

// request拦截器, 改变url 或 options.
request.interceptors.request.use((
  url: string, 
  options: RequestOptionsInit
) => {
  return {
    url: `${url}&interceptors=yes`,
    options,
  }
})
复制代码

在响应拦截器中,可以提前对response结果做一些通用的处理,值得注意的是我们需要使用response.clone().json()才能解析response的结果。

// 克隆响应对象做解析处理

request.interceptors.response.use(async (response: Response) => {

  const data = await response.clone().json();

  if (data.code === 500) {

      throw new Error('throw error ......')

  }

  return response;

});
复制代码

在这里只对拦截器做简单的介绍,之后的章节里会对拦截器功能做出扩展,来更深入使用封装。

Code 码与业务

在业务背景下,我们(确认)订单体系就使用了Code码来描述对应的订单异常错误标识,如下图所示,每一个code码都相对应一个确认订单出现的异常,根据不同的级别和序号,我们就可以在处理页面中,根据后端返回的code状态做策略以及状态机的处理。

在实际开发中,我们大量使用code代替当前的请求状态返回,也就是说在业务开发中,HTTP请求的状态都可以被描述继承。那么,下面我们就来探究下code体系的一些小技巧吧。

首先,来看一个基础的RESTFul请求的响应结构体:

{
    code: 200,
    message: ""
}
{
    code: 201,
    message: ""
}
{
    code: 202,
    message: ""
}
{
    code: 203,
    message: ""
}
复制代码

我们将请求状态挪移到了response当中去做处理。当接口正常访问时,前端只需要根据对应的code去执行操作。针对状态去处理。

因此,我们将code分为以下几个常见的错误状态:

其一就是我们接口请求的状态,这类状态判断如果是成功的状态的话直接返回response中的data。如果失败则直接进行错误处理。

其二就是一些服务异常状态了,这其中包括系统报错,也就是代码报错。其次是第三方服务(如微信,钉钉)等错误。

其三就是我们开发当中出现的一些反向逻辑都可以用自定义错误码来约定。

下面,我们也相对应的枚举了部分常见的HTTP的code码来做为标识,绝大部份的场景下是完全够用的。在这里我就不详细的将所有的公共状态都列举出来了。

不论是后端还是前端都有一些比较完善的库来做这件事情

export enum HTTP_STATUS {
  SUCCESS = 200,
  CREATED = 201,
  UPDATED = 202,
  DELETED = 203,
  CLIENT_ERROR = 400,
  AUTHENTICATE = 401,
  FORBIDDEN = 403,
  NOT_FOUND = 404,
  SERVER_ERROR = 500,
  BAD_GATEWAY = 502,
  SERVICE_UNAVAILABLE = 503,
  GATEWAY_TIMEOUT = 504
}

export enum CUSTOM_HTTP_CODE {
  MESSAGE_TOAST = 601,
  SERVICE_TRY_CATCH = 602
  ......
}
复制代码

当我们有了对code的基本的定义后,下面我们就来带入一些实际的使用场景中去,然后看下它们的使用方式吧。

身份过期

说起身份过期,大部分同学可能都想到了401这个状态。绝大部分的业务逻辑都是清空缓存重新让用户登录。

而在复杂场景下,身份过期的定义有多种,包括Token过期、单点登录、身份异常等等。因此,我们会在拦截器中调用对应的处理方式,如下代码:

/**
 * [ u -> 401 ] 用户身份已经过期,清除后重新生成新的身份
 * [ u -> 402 ] 用户身份异常,清除后重新生成新的身份
 * [ u -> 403 ] 用户在其他地方登录,清除后重新生成新的身份
 * */
if (
  responseBody.code === 401 ||
  responseBody.code === 402 ||
  responseBody.code === 403
) {
  clearUserTraces();
  return undefined;
}
复制代码

通过对code的判断,我们会触发执行clearUserTraces来引导用户前往登录界面,完成新的授权登录。

/**
 * 清除当前用户身份信息,并且跳转到登录页面
 */
 function clearUserTraces () {
  Modal.confirm({
    title: '提示信息',
    icon: createElement(ExclamationCircleOutlined),
    content: '您当前设备信息已过期,可以取消继续留在该页面,或者重新登录。',
    okText: "登录",
    cancelText: "关闭",
    onOk() {
      history.replace('/login')
    }
  })
}

复制代码

image.png

通知自定义

在绝大部分中后台项目当中,都会涉及不同种类和状态的通知条。从ant design的通知条样式来看,我们分为messagenotification两种通知样式,以及不同的status显示。

根据这些信息,我们也与后端约定了一个字段showMessage用于管理部分错误信息。我们来看下现在的接口定义:

type CommonResult<T = any> = {
  data: T;
  code: string;
  showMessage:
    | undefined
    | false
    | {
        method: "message" | "notification";
        type: "success" | "error" | "info" | "warning";
        message: string;
        description?: string;
      };
};

复制代码

后端通过返回的主体模型就可以在接口中操作前端界面的通知提示了,主要用于业务上会对操作成功与失败做相应的提示。

那么在响应拦截器中,我对showMessage做了一个判断。如下,如果showMessage存在的话那么执行showMessageNotificationHandler方法处理弹窗信息。

if (responseBody.showMessage) {
  showMessageNotificationHandler(responseBody.showMessage)
}
复制代码

showMessageNotificationHandler具体做什么东西呢?我们来看看方法的实现,大致的实现思路就是在switch case中对后端传入的类型和参数展示不同的组件弹窗。

/**
 * 显示通知弹窗
 * @param showParams 显示通知参数
 * @param api 当前标识
 */
export function showMessageNotificationHandler(
  showParams: API.CommonResult["showMessage"],
  api: string = ""
): void {
  /** @name 判断是否开启了通知描述显示 */
  if (showParams) {
    const { method, type, message, description } = showParams;
    switch (method) {
      /** @name 全局提示弹窗 */
      case "message":
        showMessage[type](message);
        break;

      /** @name 通知提示弹窗 */
      case "notification":
        notification[type]({
          message,
          description,
        });
        break;
      default:
        throw new Error(
          `[ request ]: 接口约定了弹窗信息参数,但是未给出使用模型,请检测当前。${api}`
        );
    }
  }
}

复制代码

当我们封装好了之后,我们Mock一个接口来看看效果, 我们返回了一个message弹窗信息来看看效果吧:

'GET /api/service-admin/v1/data/table': {
  code: 40001,
  message: '操作成功',
  showMessage: {
    method: 'message',
    type: 'error',
    message: '我是一个自定义的message消息',
  },
  data: {}
}
复制代码

image.png

错误处理

在错误处理中,我们会对一些请求错误做出处理。除了对HTTP原有的状态做相应错误处理外,也需要对自定义code导出,返回一个Promise.Reject的错误出去。我们就可以在try catch中捕获当前的异常,做一些业务上的code处理。

/** 异常处理程序 */
function errorHandler(
  error: ResponseError<any> & {
    code?: string | number;
  }
) {
  const { response, request } = error;
  if (response && response.status) {
    const errorText = codeMessage[response.status] || response.statusText;
    const { status, url } = response;

    notification.error({
      message: `请求错误 ${status}: ${url}`,
      description: errorText,
    });
  }

  if (error.code && error.code === 5000) {
    // todo: 上报出错人以及对应信息
    throw new Error(`[fetch fail]: 接口请求失败,当前接口地址${request.url}`);
  }

  return Promise.reject(error);
}

复制代码

如下代码示例,我们在调用层的try catch中尝试捕获了当前接口的请求信息,如下显示,40001code在响应拦截器中未处理,从而流入到了errorHandle后仍然未处理,那么就交给调用方去处理:

const handleRequest = async (
  props: API.TableRequestParams
): Promise<{
  data: API.GithubIssueItem[];
  success: boolean;
  total: number;
}> => {
  try {
    const res = await fetchTableList(props);
    return {
      data: res.list,
      success: true,
      total: res.total,
    };
  } catch (throwError: unknown) {
    console.log(throwError, "throwError");
    return {
      data: [],
      success: false,
      total: 0,
    };
  }
};
复制代码

image.png

配置映射

当我们有了一个封装好的接口请求函数后,为了方便团队成员调用,我也封装了一个配置映射的umi插件来处理接口函数与接口规范的转换。

如下图,我们会声明一个配置映射,然后将其抽象生成为函数。方便开发时调用。

image.png

在这里给大家说下具体的使用方式。

首先,我在services目录下声明了一个配置文件,它的标准式是一个对象,同时你也可以是一个JSON文件。约定Key值是映射函数的名称。Value请求方式请求地址

Object写法

module.exports = {
  fetchTableList: 'GET /service-admin/v1/data/table'
}
复制代码

JSON写法

{
  "fetchLogin": "POST /service-admin/v1/user/login",
  "fetchUserInfo": "POST /service-admin/v1/user/info"
}
复制代码

当我们有了配置文件后,启动项目时会生成对应的请求函数,如下图所示:

image.png

插件会将对应的配置文件转换成接口请求函数。那么我是如何借助插件的功能去实现这个功能的呢?下面就来看看插件的核心

我们将不同目录下的接口配置转换成对应的配置,然后输出在template模板中,来完成映射。

Map(1) {
  '/Users/wangly19/Desktop/Project/plugins/example/./constant/a.ts' => [
    {
      functionName: 'a',
      url: '/app-services/${id}',
      method: 'post',
      linkParams: [Array],
      exportTemp: 'const { id, ...data } = payload',
      paramType: 'data'
    }
  ]
}
复制代码

以上是我们生成的配置,下面我们来看下模板中的内容吧。

我们可以将Map当中的Value渲染到下面的模板当中,最后在输出成为一个完整的接口函数文件。

import request from '{{{ requestPath }}}'
import { RequestOptionsInit } from 'umi-request'


{{ #requestASTModules }}

export function {{ functionName }} <T = any, R = any>(
  payload?: T = {}, 
  options?: RequestOptionsInit = {},
): Promise<R> {

  {{#exportTemp}}
    {{{ exportTemp }}}
  {{/exportTemp}}
  {{^exportTemp}}
    /* [info]: @no link params */
  {{/exportTemp}}
  
  return request( `{{{ url }}}`, {
    {{ paramType }}: {{#exportTemp}}data{{/exportTemp}}{{^exportTemp}}payload{{/exportTemp}},
    method: '{{ method }}',
    ...options
  })
}

{{ /requestASTModules }}
复制代码

我们来看下效果,如下图:

Kapture 2021-08-25 at 22.27.15.gif

资源

总结

本篇文章针对团队中请求做了一些约定式的封装,将日常开发中可能会碰到的业务场景做了一些基本的封装,区分接口错误与成功时的一系列交互。🤔阐述了在团队业务当中,灵活多变的状态如何管理与控制。同时,在最后也对团队开发中解耦情况下对接口请求管理配置映射将其封装成为了插件,团队成员不需要关心request请求方法的多样性,可以专注于业务开发和接口调用。

最后,我们也对一些其他场景做了尝试。后面,我们可以继续对其进行扩展优化,我把这几个点列在了下面:

  • 接口请求重试机制,可以对request失败后重试的业务逻辑
  • 页面离开时,接口请求自动停止,可以通过cancelTokencontroller.abort来做接口请求的取消
  • 对接接口管理平台,一键生成接口映射配置,减少手动工作
  • 对接swagger,生成typescript部分接口请求类型

以上是提升团队效率的几个常见的封装手段。对于团队来说,在双方合理的约定下,可以将更多通用逻辑的行为放在请求上,在中后台项目中往往能够有奇效。

对于一个商城项目来说,它的挑战性不是在于功能的实现逻辑上,而在于部分视觉感受与体验的优化上。如果觉得文章对你有帮助,可以点个👍,给我加个油。如果对前端电商项目想了解更多的Yoyo们可以关注本专栏。

近期好文

尾注

本文首发于:掘金技术社区
类型:签约文章
作者:wangly19
收藏于专栏:# 百万PV商城实践系列
公众号: ItCodes 程序人生

文章分类
前端