在第 14 课时中,我们重点介绍了前后端通信的重要协议 HTTP,但在实际通信场景中,光有协议是不够的。假设有下面的 GET 请求,返回结果是用户列表数据。

GET https://lagou.com/a

对于浏览器而言,可以通过头部字段 Content-Type 轻松判断出来,然后进行对应的逻辑处理。但对于工程师而言是不可读的,不知道 /a 代表什么。

解决这个问题的方法就是制定一种规范,让请求具有语义化,这种规范就是我们常说的 API 设计规范。下面就来介绍前后端通信中出现过的 3 种 API 规范。

RPC—远程过程调用

RPC(Remote Procedure Call,远程过程调用)常用于后端服务进程之间的通信。“远程”指的是不同服务器上的进程,“过程调用”里的“过程”可以理解为“函数”,这种接口设计和函数命名很相似,名称为动宾结构短语,类似下面的样子。

GET /getUsers
POST /deleteUser
POST /createUser

可能有的前端工程师对 RPC 比较陌生,但在 Web 开发早期,编写页面逻辑的工作由后端(或全栈)工程师完成,自然而然的,RPC 风格就被移植到了前后端通信中。

从接口命名上不难看出,RPC 风格和我们平常编写模块的思路很像,提供了一个函数作为接口,供其他模块调用。这明显是站在后端工程师的视角而设置的:为了像在本地调用一个函数那样调用远程的代码。

RPC 这种设计规范对前端工程师而言是不够友好的,具体表现在以下 2 个方面。

  • 紧耦合:当前端工程师需要获取或修改某个数据时,他有可能需要先调用接口 A ,再调用接口 B,这种调用需要对系统非常熟悉,让前端工程师熟悉后端逻辑和代码显然是难以办到的。

  • 冗余:把执行动作写在 URL 上实际是冗余的,因为 HTTP 的 Method 头部可以表示不同的动作行为。

REST—表现层状态转换

REST(Representational State Transfer),即表现层状态转换 。

什么是“表现层”?

在理解“表现层”之前,我们先理解另一个概念“资源”。资源指的是一个实体信息,一个文本文件、一段 JSON 数据都可以称为资源。

而一个资源可以有不同的呈现形式,比如一份数据可以是 XML 格式,也可以是 JSON 格式,这种呈现形式叫作“表现层(Representation)”。

什么又是“状态转移”?

当用户通过浏览器访问网站时,通常会涉及状态的变化,比如登录。

HTTP 本身是无状态的,因此,如果客户端想要操作服务器,则必须通过某种手段让服务器发生“状态转移(State Transfer)”。而这种转移是建立在表现层之上的,即“表现层状态转移”。

REST 的核心要点有两个,那就是资源方法。

REST 的 URL 指向某个或某类资源,所以不再是类似 RPC 的动宾结构,而是名词。比如像下面这些都是 REST 的设计风格,通常,当 URL 的路径以 ID 结尾则表示指代某个资源,无 ID 则指向一类资源。路径分隔符表示资源之间的嵌套关系。

/orgs
/orgs/123asdf12d
/orgs/ss1212sdf/users
/orgs/ss1212sdf/users/111asdl234l

所以像下面这些 URL 是不符合 REST 规范的。

/createUser
/samples/export

而要进行状态转移的时候,使用的是 HTTP 默认的语义化头部 Method 字段。

GET(SELECT):获取资源
POST(CREATE):新建一个资源
PUT(UPDATE):更新资源
DELETE(DELETE):从服务器删除资源

虽然 REST 的低耦合、高度语义化的设计风格比较适合前后端通信,但也存在 3 个不足,具体如下。

  • 弱约束。REST 定义请求路径和方法,但对非常重要的请求体和响应体并没有给出规范和约束。这就意味着需要借助工具来重新定义和校验这些内容,而不同工具之间的定义格式和校验方式都不相同,给工程师带来了一定的学习负担。

  • 接口松散。 REST 风格的数据粒度一般都非常小,前端要进行复杂查询的时候可能会涉及多个 API 查询,那么会产生多个网络请求,很容易造成性能问题。通常的解决方案是通过类似 API 网关的中转服务器来实现对接口的聚合和缓存。

  • 数据冗余。前端对网络请求性能是比较敏感的,所以传输的数据量尽可能小,但 REST API 在设计好之后,返回的字段值是固定的。所以很容易出现这样一个场景,对于后端工程师而言,为了减少代码修改,会尽可能地在返回结果中添加更多的字段;对于前端工程师而言,使用数据的场景往往是多变的,即使是调用同一个 API,在不同场景下也只会用到某些特定的字段。所以不可避免地产生数据冗余,从而造成带宽浪费,影响用户体验。

如果要改进上述不足,该怎样定义 API 规范呢?

GraphQL—图表查询语言

我们再次将关注点从资源转移到 API 的调用者上,从调用者的角度来思考 API 设计。对于调用者而言,最关心的不是资源和方法,而是响应内容。在前后端的交互中,请求体和响应内容一般都采用 JSON 格式。下面是 GitHub REST API 的响应内容示例,由于响应内容字段太多,只截取了部分字段。

{"id": 1296269,"stargazers_count": 80,"name": "Hello-World","full_name": "octocat/Hello-World","owner": {"login": "octocat","id": 1,"avatar_url": "https://github.com/images/error/octocat_happy.gif"}
}

假设上面的响应内容是前端所需要的内容,现在来思考一个问题,该如何告诉后端所期望得到的数据结构呢?

如果只考虑对 JSON 数据的描述,其实已经有现成的规范来实现了,即用 JSON-Schema 来描述上面的 JSON 数据,代码如下:

{"type": "object","properties": {"id": {"name": "id","type": "number"},"stargazers_count": {"name": "stargazers_count","type": "number"},"name": {"name": "name","type": "string"},"full_name": {"name": "full_name","type": "string"},"owner": {"name": "owner","type": "object","properties": {"login": {"name": "login","type": "string"},"id": {"name": "id","type": "number"},"avatar_url": {"name": "avatar_url","type": "string"}},"required": ["login","id","avatar_url"]}},"required": ["id","stargazers_count","name","full_name","owner"]
}

可以看到,描述信息本身大小已经超过了数据内容,所以这种烦琐的描述方式显然不适用于前后端通信,因为会占据较多的带宽。

既然不能做加法,那么就尝试做减法。对于 JSON 数据而言,重要的是描述其结构,值是可变的,所以可以把值去除。上述示例数据会变成下面的结构。

{"id","stargazers_count","name","full_name","owner": {"login","id","avatar_url"}
}

在进行结构描述的时候,我们关注的是字段名称和层级关系,所以还有进一步的优化空间,那就是去掉一些不必要的符号,变成下面的形式。

{idstargazers_countnamefull_nameowner {loginidavatar_url}
}

然而这个结构已经是最基础的 GraphQL 查询语句了,当然 GraphQL 并不止如此,还有更多的高级功能,比如参数变量、片段。下面就来介绍一下 GraphQL。

GraphQL(Graph Query Language) 是图表查询语言,在 REST 规范中,请求路径表示资源之间的嵌套关系,那么很容易形成树型结构,如下图所示。


REST 风格的树结构 API

GraphQL 中不同类型之间的关联关系通过图来表示。下面是一张通过 GraphQL 工具生成的示例图,描述了不同类型之间的关系。


GraphQL Voyager 示例图

虽然 GraphQL 的设计理念和 REST 有较大差别,而且还上升到了“语言”层面,但核心概念其实就两个:查询语句和模式,分别对应 API 的调用者和提供者。

GraphQL 的查询语句提供了 3 种操作:查询(Query)、变更(Mutation)和订阅(Subscription)。查询是最常用的操作,变更操作次之,订阅操作则使用场景就比较少了。

下面重点介绍一下查询操作中 3 个常用的高级功能。

别名(Aliases)

别名看上去是一个锦上添花的功能,但在开发中也会起到非常重要的作用。考虑一个场景,前端通过请求 GET /user/:uid 获取一个关于用户信息的 JSON 对象,并使用了返回结果中的 name 字段。如果后端调整了接口数据,将 name 字段改成了 username,那么对于前端来说只能被动地修改代码;而如果使用 GraphQL,只需要修改查询的别名即可。

下面是一个使用别名将 GitHub GraphQL API 的 createdAt 改为 createdTime 的代码示例。

片段(Fragments)

如果我们在查询中有重复的数据结构,可以通过片段来对它们进行抽象。下面是一个使用 GitHub GraphQL API 来查询当前仓库第一位 star 用户和最后一位 star 用户的例子。将 StargazerEdge 类型的部分字段抽取成了 Fragment,然后在查询中通过扩展符“...”来使用。

内省(Introspection)

调用 REST API 非常依赖文档,但 GraphQL 则不需要,因为它提供了一个内省系统来描述后端定义的类型。比如我要通过 GitHub GraphQL API 来查询某个仓库的 star 数量,可以先通过查询 __schema 字段来向 GraphQL 询问哪些类型是可用的。因为每个查询的根类型总是有 __schema 字段的。


__schema 查询根类型

通过搜索和查看描述信息 description 字段可以发现,其提供了一个 Repository 类型。


在返回的模式中找到 “Repository” 类型定义

然后再通过 __type 来查看 Repository 类型的字段,找到和 star 有关的 stargazers 字段描述,发现这个字段属于 StargazerConnection 类型,以此类推继续查找,后面的嵌套子类型查找过程就不一一截图了。


通过 __type 查找 Repository 类型字段

最终通过下面的查询语句获得了第一页的查询结果。


查询 Repository 的前100 个关注者

后端的模式与 Mongoose 及 JSON-Schema 的模式有些类似,都是通过声明数据类型来定义数据结构的。数据类型又可以分为默认的标量类型,如 Int、String 及自定义的对象类型。下面是一个类型声明的例子:

type User{id: ID!name: String!books: [Book!]!
}

这段代码定义了一个 User 类型,包含 3 个字段:ID 类型的 id 字段,String 类型的 name 字段以及 Book 类型列表 的 books 字段。其中 ID 和 String 为标量类型,Book 为对象类型,惊叹号表示字段值不能为 null。

GraphQL 的类型声明和 TypeScript 的类型定义除了在写法上有些类似,在一些高级功能上也有异曲同工之处,比如联合类型和接口定义。

union Owner = User | Organization
interface Member {id: ID!name: String
}
type User implements Member {...
}
type Organization implements Member {...
}

定义好模式之后,就要实现数据操作了。在 GraphQL 中这一部分逻辑称为解析器(Resolver),解析器与类型相对应,下面是类型定义以及对应的解析器。

const schemaStr = `
type Hero {id: Stringname: String
}
# 根类型
type Query {hero(id: String, name: String): [Hero]
}
`
const resolver = {hero({id='hello', name='world'}) {if(id && name) {return [...data.hero, {id, name}]}return data.hero}
}

总体而言,GraphQL 在弥补 REST 不足的同时也有所增强,表现在:

  • 高聚合。GraphQL 提倡将系统所有请求路径都聚合在一起形成一个统一的地址,并使用 POST 方法来提交查询语句,比如 GitHub 使用的请求地址就是:https://api.github.com/graphql。

  • 无冗余。后端会根据查询语句来返回值,不会出现冗余字段。

  • 类型校验。由于有模式的存在,可以轻松实现对响应结果及查询语句进行校验。

  • 代码即文档。内省功能可以直接查询模式,无须查询文档也可以通过命名及描述信息来进行查询。

对于前端而言,GraphQL 提供了一种基于特定语言的查询模式,让前端可以随心所欲地获得想要的数据类型,是相当友好的;而对于后端而言,把数据的查询结果编写成 REST API 还是 GraphQL 的解析器,工作量相差不大,最大的问题是带来的收益可能无法抵消学习和改造成本。这在很大程度上增加了 GraphQL 的推广难度。

所以 GraphQL 的大多数实际使用场景分为两类,一类是前端工程师主导的新项目,后端采用 Node.js 来实现,用 GraphQL 来替代 REST;另一类就是将 Node.js 服务器作为中转服务器,为前端提供一个 GraphQL 查询,但实际上仍然是调用后端的 REST API 来获取数据。

总结

从 RPC 到 REST 再到 GraphQL,可以看到 API 规范上的一些明显变化。

  • 关注点发生了明显的转移。从 API 的提供者,到 API 数据,再到 API 的使用者。

  • 语义化的特性更加明显。从最初通过路径命名的方式,到利用 HTTP 头部字段 Method,再到直接定义新的查询语言。

  • 带来的副作用,约束更多,实现起来更加复杂。

站在前端工程师的角度再来看这些 API 规范,对于 RPC 风格,了解即可;对于 REST API,需要重点理解它通过路径指向资源,以及利用 HTTP 方法来指代动作的特性;对于 GraphQL,应该从 API 调用者和 API 提供者两个角度来分别学习查询语句和模式。

最后布置一道思考题:在谈到 REST 规范时,提到一个反例“/samples/export”,你还能找到常见的不符合 REST 规范的例子吗?


精选评论

**杰:

login register就是典型的反例

*聪:

这一讲没搞懂,没理解是开发中的哪个环节用到的,我工作中怎么没接触到

前端高手进阶第17讲:前后端如何有效沟通?相关推荐

  1. 前端视角漫谈百度ueditor编辑器前后端分离配置

    此文旨在前后端分离的前提下,配置ueditor联通前后端接口,实现ueditor的文件上传(一般是图片上传)并在编辑器中反显 目录结构 百度的ueditor组件提供了各种主流的后端语言(Java,As ...

  2. 我的前端学习之路<初识前后端交互>

    前后端交互 前后端数据通讯 从数据库中获取或提交内容 使用到的技术栈:ajax ajax async JavaScript and xml(严格意义上的html,闭合标签) 发送ajax 是有严格步骤 ...

  3. 前端--阶段笔记(四)前后端交互

    第一章 ajax + http URL url 统一资源定位符 uniformResourceLocatior 由三部分组成:通信协议 服务器名 服务器上具体存储位置 http://<host& ...

  4. 前端工程化、模块化方案教程大全,现代前端高手进阶必经之路(欢迎收藏)...

    关注掘金和github很久了,但是一直没有在上面发表什么文章,这次静下心来写了个前端的目前工程化与模块化方案的总结,后期会继续更新,为开源社区做出更大贡献. 前端的模块化方案从早年的require.j ...

  5. 前端学习(1254):Vue前后端交互方式

  6. 前端学习(1253):Vue前后端交互

  7. 前后端分离后的前端时代 1

    本文从前端开发的视角,聊一聊前后端分离之后的前端开发的那些事儿.阅读全文,大约需要8分钟. 什么是前后端分离 除了前端之外都属于后端了. 你负责貌美如花,我负责赚钱养家 在传统的像ASP,JSP和PH ...

  8. 【前端】第一章 前端三要素、前后端分离的演变史

    第一章 前端三要素.前后端分离的演变史 文章目录 第一章 前端三要素.前后端分离的演变史 一.前端三要素 结构层(HTML) 表现层(CSS) 行为层(JavaScript) 二.前后端分离的演变史 ...

  9. 大前端–Vue前端体系、前后端分离

    大前端–Vue前端体系.前后端分离 前言 Soc:关注点分离原则 HTML+CSS+JS(视图):给用户看,刷新后台给的数据 网络通信:axios 页面跳转:vue-router 状态管理:vuex ...

最新文章

  1. Scrapy_splash组件的使用
  2. 网络请求可以返回数据的网站_实例解析|Python加解密VIP网站反爬请求头实现数据爬取...
  3. linux入门之实用指令(三)
  4. 转 -- 推荐几本云计算的经典书籍
  5. java员工请假系统_基于jsp的员工请假管理系统-JavaEE实现员工请假管理系统 - java项目源码...
  6. 3中查询数据库连接数
  7. datatables 添加时间按钮_Java 添加页面跳转按钮到PDF文档
  8. android selector 开始自定义样式
  9. 23种设计模式(6):模版方法模式
  10. python爬取大众点评数据_爬虫爬取大众点评评论数
  11. 万豪国际12家餐厅再登米其林指南;五大高端品牌酒店签约港珠澳口岸城 | 中国酒店业周刊...
  12. Hi3516开发笔记(四):Hi3516虚拟机编译uboot、kernel、roofts和userdata以及分区表
  13. [数论 反演]BZOJ4816 [Sdoi2017]数字表格
  14. cp: omitting directory ‘./.local/lib/python3.9/site-packages/.’
  15. 青春饭碗——程序员,年纪大了怎么办?
  16. pinned memory or page locked memory)
  17. bcdedit添加linux引导,用BCDEdit编辑启动菜单
  18. 【增长黑客读书笔记-范冰】
  19. 为人处世,请从学会闭嘴开始!
  20. NPDP产品经理小知识:端到端的流程建设与跨职能团队管理

热门文章

  1. 4 路由策略与策略路由
  2. 二维数组赋初值你会几种方法?
  3. 《防灾自救手册--地震》
  4. 漏洞挖掘 符号执行_基于符号执行的二进制代码漏洞发现
  5. 线程池QueueUserWorkItem
  6. 贝佐斯:管理亚马逊的20条经营理念
  7. linux中root权限找tmp路径,通过可写文件获取 Linux root 权限的 5 种方法-tmp文件
  8. 详细对比java中的 final,finally, finalized关键字
  9. Python3.7 如何安装dlib
  10. Linux之CentOS安装心得