# 搭建前后端之桥
随着前后端分离,开发的门槛降低了,我们不再要求团队中的每个开发都是全栈工程师,这样更容易找到项目的合适人选。团队也划分成了前端和后端两个团队。前端负责消费 API 并展示页面,后端负责提供 API。这两个团队可以并行开发互不影响,大大提升了效率。虽然前后端分离解决了很多问题,但同时也带来了新的困扰。
# 前后端分离带来的困扰
# 沟通成本
前后端成为两个独立团队之后,协作的问题便随之而来。通过什么来协作呢?契约。简单来说,就是预先定义好精准的接口,比如接口的 URL,包含哪些参数、返回值,每个值的类型,是否为空等等。定义好之后,前后端就按照契约进行开发。但是在实际场景中,却经常出现问题。
举个真实的例子。有一次,后端在重构时修改了一个字段名,同时也修改了契约测试,但是却忘了告诉前端。由于这个页面的使用频率不高,前端也工作在别的地方,因此那个页面挂了许久都没有人发现。这些隐藏 Bug 会给我们的应用带来隐患,同时也会增加开发的负担。
在实际开发过程中,保证人人都遵守契约是一个很困难的事情,因为人都可能会犯错。
# 大量的模板代码
即便团队中所有人都能严格遵循契约,但集成 API 仍旧是个苦力活。我需要定义请求的 URL、Method、参数、参数类型以及返回值类型等等。于是项目中就充斥着下面这样的模板代码:
interface ICreateBookRequestData {
bookId: string;
category: string;
date: string;
createdBy: string;
}
interface IBook {
id: string;
author: string;
name: string;
price: string;
publishDate: string;
publishVendor?: string;
}
export const createBook = createRequestAction<ICreateBookRequestData, IBook>(
"@@books/createBook",
(data) => ({
url: "/books/book",
method: "PUT",
data,
}),
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在多人协作时,为了减少冲突,我们通常会按照业务场景,将请求相关的代码存储到不同文件。比如 login.api.ts
存放登录相关的请求代码,account.api.ts
中存放账户相关的请求代码。但是这会造成另一个问题,就是类型的重复定义和定义不一致的问题。
在上面的代码中,我们定义了请求响应数据的类型:IBook
。对于后端来说,这个数据类型是可复用的,其他接口也可以使用它。但是对于前端来说,很难确定这个数据类型在哪些地方被使用了。因此在不同的文件中,可能会重复定义相同的类型。由于不同的人对同一个数据的类型的理解可能不一致,还会造成定义不一致的问题。比如在 A 文件的 IBook
中我们定义 publishVendor
是一个可选的属性,而在 B 文件中我们可能再次定义 IBook
并把 publishVendor
定义为一个必需的属性。如下所示:
interface IBook {
id: string;
author: string;
name: string;
price: string;
publishDate: string;
publishVendor: string;
}
2
3
4
5
6
7
8
# 连接前后端
为了解决上面的问题,我们实现了一个自动化工具,将割裂的前端和后端重新连接起来。简单来说,就是通过 Swagger JSON 自动生成调用 API 所需的代码以及类型定义。
OpenAPI 规范(以前称为 Swagger 规范)为 RESTful API 定义了一个与语言无关的标准接口,允许人和计算机发现和理解服务的功能,而无需访问源代码、文档或开发者工具。Swagger 是一套围绕 OpenAPI 规范构建的开源工具,可以帮助我们生成、描述、调用和可视化 RESTful 风格的服务。
有了自动化工具之后,只需要在终端中执行一行命令,就能立刻生成项目中所有 API 相关的代码以及类型定义。这样就不用再写模板代码了,节省了很多时间。同时,由于所有代码都是通过 Swagger JSON 生成的,当接口发生变动时,我们不用再去查看文档或者询问后端修改了什么,只需要通过命令就能知道哪些接口发生了变化并自动更新对应的前端代码。因为所有代码都是自动生成的,重复定义的问题也就不存在了。对于简单业务场景来说,集成 API 就是一行代码的事:
// getBooksUsingGET 方法由工具自动生成, useTempData 会发起 HTTP 请求并返回响应数据
const [books] = useTempData(getBooksUsingGET, { bookType }, [bookType]);
// 拿到 books 数据,渲染 UI
2
3
4
5
# 落地和优化
当我实现完这个工具的第一个可用版本,准备在项目中推行时,却发现还有一些问题亟待解决:
Q: 如何保证生成代码的一致性?
A: 每次运行命令都会从远程服务器上去获取 Swagger JSON,以保证数据来源的一致性。生成新的代码之后覆盖之前的文件即可。这样就能保证每个人生成的代码与当前服务器上的 API 是一一对应的。不过需要注意的是,生成的文件不能手动修改,否则修改最终会被覆盖。
Q: 如何快速得知 API 的变化?
A: 跟 package-lock.json
一样,我们会对生成的代码进行排序,以减少生成文件的变化。重新运行命令之后,通过 git diff
就能准确得知 API 的变化。
Q: 如何进行多人协作?
A: 如果后端修改了一个字段名,可能会导致前端所有用到这个字段的地方都发生编译错误。这时如果大家都去修改编译问题,不仅可能产生冲突,还会造成时间的浪费。虽然这个问题在没有自动化工具之前一样存在,但仍然需要解决。好在这个问题发生的频率不高,我们可以和项目成员约定:如果有人正好在做这个功能,那么就由他来修改,否则就由指定的人去协调安排。通过自动化工具,可以更快地完成修改,从而减少阻塞别人工作的时间。
Q: 当后端进度落后于前端时,如何保证先有 Swagger 定义?
A: 由于前后端是两个独立的团队,所以进度也常常不同。后端可能无法先于前端实现好 API,甚至无法和前端同时开始去做一个功能。而这套方案依赖于 Swagger 定义,必须先有 Swagger 定义才能生成代码。如果前端要先于后端完成某个功能,必须先和后端商定好 API Schema 再进行开发。定义好 API Schema 之后,随之更新 Swagger 定义即可(后端无需实现具体功能)。
# 最后
自动化工具为前后端搭建了一座桥梁,当后端发生变动时,前端也能及时得知并做出相应修改。再也不用担心后端悄悄改接口了!到目前为止,自动化工具已经为我们项目生成了上万行代码。不仅提升了大家的效率,也减少了因为不遵循契约带来的隐藏 Bug。前端终于不用写大量模板代码了,集成 API 也变成了一件很容易的事情。
如果对代码实现感兴趣,可以移步这里:ts-codegen,或者看看我之前写的这篇文章:基于 React 和 Redux 的 API 集成解决方案。
不仅仅是前后端协作的问题,更重要的是前端立刻知道后端做了什么。
对比没有这个工具之前和有这个工具之后,前端的工作方式。
可视化地将效率提升、沟通成本展示出来。
后端升级 springfox swagger 发现原先前端的某个 API response 数据结构和后端不一致,且这种不一致已经存在很久了。。。后端实际提供的数据是 B 结构,前端以为是 A 结构。如果前端用错误的 interface,那么可能功能已经 break 了。
不同的接口可能返回字段相同的 interface,比如:
interface UserVO {
name: string;
age: number;
}
interface UserVO2 {
name: string;
age: number
}
2
3
4
5
6
7
8
9
对于前端来说,很难人工地去区分哪个 UserVO 是属于哪个接口的,很有可能把它们定义成同一个 interface,这样就和后端的设计不一致了,如果后续只修改 UserVO2,那么用到 UserVO 的地方不会挂掉。
- 人工定义还有一个问题,就是可能会重复定义某些 interface,造成混乱和不一致