深入理解 GraphQL
0.引子
通过上一篇文章我们对 GraphQL 有了基础的了解。我们知道 GraphQL 使用 Schema 来描述数据,并通过制定和实现 GraphQL 规范定义了支持 Schema 查询的 DSQL (Domain Specific Query Language,领域特定查询语言)。Schema 帮助将复杂的业务模型数据抽象拆分成细粒度的基础数据结构,而 DSQL 的实现则赋予了前端开发者自由组织和定制请求数据的能力。如果以一张图来表示的话,可以将 GraphQL 看做一条以通用基础业务数据模型为基础、将传统后端服务和前端页面紧密且自由地联系在一起的纽带。
为什么 GraphQL 的 Schema 能够表示出服务器所支持的复杂业务模型数据,GraphQL 的 Query 又是怎样赋予前端开发者对数据的定制能力,本文将通过分析和理解 GraphQL 的设计来和大家一起探讨解答这些问题。
1.GraphQL 的设计
GraphQL 由以下组件构成:
- 类型系统(Type System)
- 查询语言(Query Language)
- 执行语义(Execution Semantics)
- 静态验证(Static Validation)
- 类型检查(Type Introspection)
作为将数据模型和具体接口实现解耦的 DSL,GraphQL 的基础组件,也是它最重要的组件之一就是类型系统。
1.1 类型系统
可以将 GraphQL 的类型系统分为标量类型(Scalar Types,标量类型)和其他高级数据类型,标量类型即可以表示最细粒度数据结构的数据类型,可以和 JavaScript 的原始类型对应。GraphQL 规范目前规定支持的标量类型有:
Int
:整数,对应 JavaScript 的 NumberFloat
:浮点数,对应 JavaScript 的 NumberString
:字符串,对应 JavaScript 的 StringBoolean
:布尔值,对应 JavaScript 的 BooleanID
:ID 值,是一个序列化后值唯一的字符串,可以视作对应 ES 2015 新增的 Symbol
Scalar Types
的 JavaScript 参考实现代码可以查看这里。
其他高级数据类型包括:
Object
:对象用于描述层级或者树形数据结构。对于树形数据结构来说,叶子字段的类型都是标量数据类型。几乎所有 GraphQL 类型都是对象类型。Object 类型有一个 name 字段,以及一个很重要的 fields 字段。fields 字段可以描述出一个完整的数据结构。例如一个表示地址数据结构的 GraphQL 对象为:
const AddressType = new GraphQLObjectType({ name: 'Address', fields: { street: { type: GraphQLString }, number: { type: GraphQLInt }, formatted: { type: GraphQLString, resolve(obj) { return obj.number + ' ' + obj.street } } }});
Interface
:接口接口用于描述多个类型的通用字段,例如一个表示实体数据结构的 GraphQL 接口为:
const EntityType = new GraphQLInterfaceType({ name: 'Entity', fields: { name: { type: GraphQLString } }});
Union
:联合联合类型用于描述某个字段能够支持的所有返回类型以及具体请求真正的返回类型,例如一个表示宠物(可以是猫或者狗)的 GraphQL 联合类型为:
const PetType = new GraphQLUnionType({ name: 'Pet', types: [DogType, CatType], resolveType(value) { if (value instanceof Dog) { return DogType; } if (value instanceof Cat) { return CatType; } }});
Enum
:枚举用于表示可枚举数据结构的类型,例如表示 RGB 色值的 GraphQL 枚举类型为:
const RGBType = new GraphQLEnumType({ name: 'RGB', values: { RED: { value: 0 }, GREEN: { value: 1 }, BLUE: { value: 2 }, }});
Input Object
:输入对象是为了查询(query)而定义的数据类型,不直接重用 Object 类型是因为 Object 的字段可能存在循环引用,或者字段引用了不能作为查询输入对象的接口和联合类型。参考实现中
Input Object
的定义代码为:export type GraphQLInputType = GraphQLScalarType | GraphQLEnumType | GraphQLInputObjectType | GraphQLList<GraphQLInputType> | GraphQLNonNull< GraphQLScalarType | GraphQLEnumType | GraphQLInputObjectType | GraphQLList<GraphQLInputType> >; export function isInputType(type: ?GraphQLType): boolean { const namedType = getNamedType(type); return ( namedType instanceof GraphQLScalarType || namedType instanceof GraphQLEnumType || namedType instanceof GraphQLInputObjectType );}
可以看到,Object、Interface 和 Union 三种类型是不能作为输入对象类型的。
List
:列表列表是其他类型的封装,通常用于对象字段的描述。例如下面 PersonType 类型数据的 parents 和 children 字段:
const PersonType = new GraphQLObjectType({ name: 'Person', fields: () => ({ parents: { type: new GraphQLList(Person) }, children: { type: new GraphQLList(Person) }, })});
Non-Null
:不能为 NullNon-Null 强制类型的值不能为 null,并且在请求出错时一定会报错。可以用于必须保证值不能为 null 的字段。例如数据库的行的 id 字段不能为 null:
const RowType = new GraphQLObjectType({ name: 'Row', fields: () => ({ id: { type: new GraphQLNonNull(GraphQLString) } })});
还有一种重要的数据类型,即 schema 类型,它描述了后端服务器能够提供的数据支持。这里先暂时不介绍,因为它涉及 GraphQL 的其他组件,等全部介绍完我们再来看 GraphQL 中 schema 的具体实现。
1.2 查询语言
类型系统对应我们开头提到的 Schema,是对服务器端数据的描述,而查询语言则解耦了前端开发者与后端接口的依赖。前端开发者利用查询语言可以自由地组织和定制系统能够提供的业务数据。
GraphQL 的一个查询请求被称为一份 query 文档(query document),即 GraphQL 服务能够解析验证并执行的一串请求字符串。query 由操作(Operation)和片段(Fragments)组成。一个 query 可以包含多个操作和片段。只有包含操作的 query 才会被 GraphQL 服务执行。但是不包含操作,只有片段的 query 也会被 GraphQL 服务解析验证,这样一份片段就可以在多个 query 文档内使用。
只包含一个操作的 query 可以不带操作名称或者使用简写形式(即 query 关键字加操作名)。query 包含多个操作时,所有操作都必须带上名称。
操作(Operations)
GraphQL 规范支持两种操作:
- query:仅获取数据(fetch)的只读请求
- mutation:获取数据后还有写操作的请求
在官方提供的参考实现中我们会发现还支持一种操作 subscription,这是为了处理订阅更新这种比较复杂的实时数据更新场景而设计的操作,不过目前这种操作还处于试验阶段,不建议在生产环境中使用。
查询请求的模型可以用下面的图来表示:
注:图片来源
选择集合(Selection Sets)
选择集合表示当前选中的数据内容,格式为:
{ Field // 字段名 FragmentSpread // 片段展开 InlineFragment // 内联片段} |
关于选择集合的使用,可以参考 graphql-js 的代码。参考实现代码在这里。
字段(Field)
字段格式为:
alias:name(argument:value)
其中 alias
是字段的别名,即结果中显示的字段名称。
name
为字段名称,对应 schema 中定义的 fields 字段名。
argument
为参数名称,对应 schema 中定义的 fields 字段的参数名称。
value
为参数值,值的类型对应标量类型的值。
例如这样的请求:http://yunhe.taobao.com/?query={banner{backgroundURL:bg,biaoti:slogan}}
backgroundURL 就是 bg 字段的别名。
片段(Fragment)
片段是 GraphQL 的主要组合数据结构,通过片段可以重用重复的字段选择,减少 query 中的重复内容。片段又分为 FragmentSpread 和 InlineFragment。例如没有片段时需要这样编写 query:
query noFragments { user(id: 4) { friends(first: 10) { id name profilePic(size: 50) } mutualFriends(first: 10) { id name profilePic(size: 50) } }} |
query 中存在下列重复的选择集合:
{ id name profilePic(size: 50)} |
可以用片段简化为:
query withFragments { user(id: 4) { friends(first: 10) { ...friendFields } mutualFriends(first: 10) { ...friendFields } }} fragment friendFields on User { id name profilePic(size: 50)} |
使用片段时需要加上 ...
操作符表示展开片段内容。
内联片段示例如下:
query inlineFragmentTyping { profiles(handles: ["zuck", "cocacola"]) { handle ... on User { friends { count } } ... on Page { likers { count } } }} |
指令(Directives)
指令要解决的是 query 执行时字段参数无法覆盖的情况,例如引入或者忽略某个字段。指令为 GraphQL 执行添加了更多的信息。
指令实例如下:
query hasConditionalFragment($condition: Boolean) { ...maybeFragment @include(if: $condition)} fragment maybeFragment on Query { me { name }} |
include 指令表示只有在 if 参数为 true 时才引入片段表示的字段。
skip 指令表示在 if 参数为 true 时忽略片段中的字段。
熟悉了类型系统和查询语言我们就可以用 GraphQL 来实现应用层的数据请求了。其他三个 GraphQL 组件更偏向于 DSL 的实现和原理,因此本文不再做详细介绍,感兴趣的同学可以对照规范和参考实现自己研究。
2.总结
GraphQL 是在应用层对业务数据模型的抽象,是对数据请求定制的 DSQL,它解除了接口和数据之间的绑定,对业务数据结构做了抽象和整理,业务逻辑中的数据依赖于底层数据库结构,并且可以由具体业务场景来定制,不同的业务场景只要基于同样一套基础业务数据模型就可以得到复用,在我看来,这才是 GraphQL 带来的最大改变和收益。
参考资料
- State of Subscriptions in GraphQL
- Initial pass at adding
subscription
to executor - Implement subscriptions for real-time updates
- Event-stream based GraphQL subscriptions
深入理解 GraphQL相关推荐
- 干货 | 万字长文全面解析GraphQL,携程微服务背景下的前后端数据交互方案
作者简介 古映杰,携程研发高级经理,负责前端框架和基础设施的设计.研发与维护.开源项目react-lite和react-imvc作者. 前言 随着多终端.多平台.多业务形态.多技术选型等各方面的发展, ...
- GraphQL及元数据驱动架构在后端BFF中的实践
GraphQL是Facebook提出的一种数据查询语言,核心特性是数据聚合和按需索取,目前被广泛应用于前后端之间,解决客户端灵活使用数据问题.本文介绍的是GraphQL的另一种实践,我们将GraphQ ...
- GraphQL 的前世今生
GraphQL是什么 GraphQL是一种新的API标准,它提供了一种更高效.强大和灵活的数据提供方式.它是由Facebook开发和开源,目前由来自世界各地的大公司和个人维护.GraphQL本质上是一 ...
- 2018先知白帽大会 | 议题解读
为什么80%的码农都做不了架构师?>>> 摘要: 今年的先知白帽大会,与会者将能够亲身感受到非常多有趣的技术议题,如HITCON在国际赛事中屡夺佳绩的CTF团队,其队长Oran ...
- ApacheCN React 译文集 20211118 更新
React 入门手册 零.前言 一.React 和 UI 设计简介 二.创建组件 三.管理用户交互 React 全栈项目 零.前言 一.使用 MERN 释放 React 应用 二.准备开发环境 三.使 ...
- 上海莉莉丝、米哈游、B站、小红书、得物等互联网公司前端面试总结
前言 目前在杭州工作,很多朋友都转战上海了,金九银十在朋友的鼓动下,投了上海的几家互联网公司,面试的基本都是中高级前端岗位,面试运气不错基本都拿到offer.面试也很久了,空闲下来记录下面试记录. 在 ...
- bff Backends For Frontends 聚合层
GraphQL及元数据驱动架构在后端BFF中的实践 Sam Newman - Backends For Frontends 原文地址:https://samnewman.io/patterns/arc ...
- 老司机 iOS 周报 #26 | 2018-07-09
老司机 iOS 周报,只为你呈现有价值的信息. 你也可以为这个项目出一份力,如果发现有价值的信息.文章.工具等可以到 Issues 里提给我们,我们会尽快处理.记得写上推荐的理由哦.有建议和意见也欢迎 ...
- 2018先知白帽大会 | 议题解读 1
摘要: 今年的先知白帽大会,与会者将能够亲身感受到非常多有趣的技术议题,如HITCON在国际赛事中屡夺佳绩的CTF团队,其队长Orange将亲临现场,分享穿针引线般的漏洞利用艺术. 当然,还有代码审计 ...
- NewStarCTF 公开赛赛道
HTTP 跟着提示一步一步走. Head?Header! 同样是跟着提示. NotPHP <?php error_reporting(0); highlight_file(__FILE__); ...
最新文章
- 利用Event和MapFile进程共享信息
- 二元偏导数存在的条件_高等数学入门——高阶偏导数的概念和计算
- iOS应用数据持久化(一)
- 基于face++的人脸识别(上)
- [译]星际争霸人工智能比赛——规则
- ConcurrentHashMap的源码分析-为什么要做高低位的划分
- where is application controller bound to application main view
- vector和list容器有哪些区别
- 盖茨基金会重发明厕所进展
- C++ std::enable_shared_from_this
- 搞科研必须了解的五十个学术网站
- php的电阻率是多少,PTF65517KBT-10B14
- JAVA版数字转人民币大写-四位数分隔法
- WPF开发之dll文件创建与调用
- 我如何用Unity3D实现一个Galgame框架(存档读档与设置面板的思路)
- Vue PC端和移动端的切换
- 关于虚拟机中IPI中断的思考
- k8s中Ingress安装
- OSChina 周一乱弹 ——程序员已经习惯熬夜了吧
- 边角地“变废为宝” 重庆首批社区体育文化公园交付使用
热门文章
- [解决方案]SQLSTATE[HY000]: General error: 2006 MySQL server has gone away
- Linux 内存管理之 SLUB分配器(5):slub初始化过程
- Linux数据报文的来龙去脉
- Audio播放流程(六)---数据进入AudioTrack
- FreeSwitch Lua编程接口(1)dialplan里的配置
- 用glew,glfw实现opengl绘制3D学习笔记1-实现一个窗口
- Reverse Linked List 反转链表
- NYOJ954--N的阶乘的二进制表示最低位的1的位置
- python两个列表匹配_Python:检查两个列表之间的字符串是否部分匹配
- 不调用python函数实现直方图均衡化_数字图像处理之直方图均衡化