前后端集成没你想的这么难

by Teobler on 01/11/2020

undefined views

网上时不时就能看到一些求助帖,大意是前后端集成所产生的一些争执,其实集成的时候如果能有一些”规范“,这件事情可以很简单。

技术栈

本文跟技术栈强相关,但是理论上从里面抽离出来的方法论可以实践在任意的技术栈上,只需要投入一点点时间和精力写一个小工具就好了,下面是本文所用到的一些技术栈:

swr-request-generator

其中最后一项就是上面提到的需要自己去实现的小工具,这里我是把组里之前给redux用的工具改了一版,先是从 swagger2.0 升级到 openAPI 标准,然后改为生成 SWR 的代码而不是redux的,具体工具怎么使用可以看我的 github,里面有详细的 readme 和 example。这个工具虽然简单,却是前后端集成不可或缺的重要组成部分,稍后我们会讲到。

SWR

然后 SWR 是一个很酷的用于获取远端数据的一个基于 React hooks 的请求库。它实现了 HTTP 规范中的 stale-while-revalidate,即其会先使用 catch 中已存在的 data 先渲染页面,然后发送新的请求去验证 catch 中的 data 是否为最新,如果已经是最新则什么都不做,如果不是最新则更新本地的 catch 然后使用最新的 catch 重新渲染页面。

这样做的好处是可以极大地提升用户体验,用户在重复浏览同一页面时,如果页面数据更新不频繁则没有任何等待时间。在新项目上使用几个月后,我欣喜地发现 SWR 登上了 ThoughtWorks 最新一期的技术雷达,和 Recoil、Svelte 一起暂时位于评估象限。

SWR in tech radar

不过技术雷达也一针见血地指出了 SWR 的缺陷:

我们的开发者在使用 SWR 时获得了很好的开发体验,并且因为数据总是显示在屏幕上,从而显著提升用户体验。然而,我们提醒团队,只有当应用程序返回过时数据是合适的时候,才能使用 SWR 缓存策略。要注意,HTTP 通常要求缓存要用最新的响应返回给请求,只有在需要非常慎重的场景下,才会允许返回过时的响应数据。

也就是说,如果你的坏境对数据更新的要求极高,需要实时拿到最新的数据的话,不适合使用 SWR。

TypeScript

TypeScript 现在几乎成为了一门前端必上的技术栈,相比 JavaScript,其提供的类型系统能够保证程序员们常犯的”低级错误“在写代码时就暴露出来。

但也是由于其类型系统,我们在集成后端 API 时需要写一堆麻烦的接口类型,比如一个 request 的参数,这个 response 的数据类型等等。

而这些接口类型其实是后端定义的,我们其实是在依赖后端写的接口来写类型。这就很尴尬了,后端改了接口,前端也得跟着改类型,这多麻烦,后面我们会一起解决这个问题。

为什么前后端集成问题频出?

要解决一个问题,那么我们首先应该做的是想清楚为什么会出现这样的问题。

首先我们来回忆一下常规的开发过程:拿到需求,前后端讨论出接口的各种参数,开始写代码,这中间可能后端(前端)发现有问题,然后改接口,然后差不多写好的接口得重新改,改类型,改参数。

这是常规的,不常规的呢?后端自己定好接口的参数,然后告诉你他要啥给你啥,然后你按照他给的写,写好发现不能用,去找他,他说接口改了,你重新改下。那还能咋说,只能网上对线了。

说白了就是,前后端讨论好的东西可能会变(甚至都没有经过讨论,由单方面直接决定好了),变了之后由于各种各样的原因没能及时同步信息,即使及时同步了,改接口代码也是一件烦人的事情。

咋办?

从契约测试展开

首先,我们需要搞清楚什么是契约测试?

契约测试,又叫”消费者驱动的契约测试“(Consumer-Driven Contracts,简称CDC)。其中有两个角色,一个消费者,一个生产者。由消费者提供一份自己的”需求清单“(契约,约定好request和response),然后生产者根据“清单”进行相应实现。之后双方依赖于这份契约进行测试和实现。

其实契约测试将依赖双方做了一个类似于解耦的操作,从消费者依赖于生产者变成双方依赖于消费者提供的契约。我们是不是可以运用这个思想呢?

其实大多数项目里已经有一份契约了,没错,就是 swagger。只不过这份契约是生产者提供的,而且由生产者决定上面有啥,消费者没法决定。

但是其实我们可以将这份契约做一个转换。swagger 上通常会给出 API 的详细信息,包括 request 的参数和 response 长什么样子。而且其实 swagger 只是一份 json 文件,我们所看到的 swagger-ui 是后端的 lib 自动生成的。那么我们是不是可以拿到这份 json 文件。解析后直接生成前端需要的 request 和类型呢?

这样带来了几个好处:

  • 前端写代码可以不用一个个写 API 了,所有的url, request params, request method, response type等等信息我们都可以通过 swagger 自动生成
  • 后端接口改了?没问题,2秒钟更新好最新的接口代码,然后 TypeScript 的好处就体现出来了,根据新的接口类型改参数就好了
  • 生成的代码有问题?那就是后端的问题,你的实现和你自己提供的契约不一致,要么你实现不对,要么你契约不对

上代码

说了这么多都是理论,谁知道效果呢?没关系,我们直接上代码,下面所有的代码都可以在链接里面找到。

首先看一下这个example的目录:

. |____swagger | |____openAPI.json |____types.ts |____request | |____api.ts | |____useRequest.ts | |____client.ts |____page.tsx

其中 swagger 文件夹下是我从网上生成的 openAPI 规范的 swagger 文档。

request 文件夹下有三个文件:

  • api.ts 这个文件就是通过 @openapi-integration/swr-request-generator 根据上面的 swagger 文档生成的,里面包含 API 接口方法和相应的 request params 和 response的类型定义
  • client.js 里面只有一个单纯的 axios client instance,用于发送请求
  • useRequest 里面我将 SWR 和 axios 封装起来以供 api.ts 这个文件调用

这东西怎么用呢?

首先当然是安装这个包 npm i @openapi-integration/swr-request-generator -D

然后在项目的根目录添加一个配置文件,里面可以配置生成文件的输出目录,文件名,需要提前引入的方法,从哪里拿到 swagger 文件等(具体可以看readme)

然后跑一下 npm run ts-codegen,就会在你配置的相应生成对应的 API 文件。

比如下面这个方法就是生成的:

import { ISWRConfig, useRequest } from "./useRequest";
import { IResponseError } from "../types";
import { client } from "./client";

export const useDownloadUsingGetRequest = (
  {
    id,
  }: {
    id: string;
  },
  SWRConfig?: ISWRConfig<IResource, IResponseError>,
) =>
  useRequest<IResource, IResponseError>(
    {
      url: `/${id}`,
      method: "get",
    },
    SWRConfig,
  );

export interface IResource {
  description?: string;
  file?: IFile;
  filename?: string;
  inputStream?: IInputStream;
  open?: boolean;
  readable?: boolean;
  uri?: IUri;
  url?: IUrl;
}

而你只需要在调用的时候直接:

const { data, error } = useDownloadUsingGetRequest({ id: "id"})

假如后端改了代码,这个接口不要 id 了,要 name,你只需要重新跑一次npm run ts-codegen,然后这个方法就会更新,TypeScript 就会报错,告诉你这里要的是 name 不是 id,如果 response 的结构也改了,那么所生成的文件里面的 response 的类型也会跟着变, TypeScript 依然会提醒你 response 结构的改变。

这样一来接口无论怎么变其实前端都不在意,顶多就是跑一遍命令,重新穿个参数就好了,大大提升开发效率,降低内耗。