Extending the GraphQL Specification
作者:禅与计算机程序设计艺术
1.简介
GraphQL是一种用于API查询的高级编程语言。其设计灵感来源于RESTful API模式,并提供了一种声明性的查询方式来增强数据交互能力。该方法的优势在于能够有效地解决组件之间依赖关系的问题,并将复杂性转移至后端框架层面。其核心价值在于简化开发者的前端工作量,在不影响后端组织架构的前提下实现灵活的数据访问策略。作为一种广泛采用的技术,在现代软件开发中发挥着重要作用
该编程语言不仅具备强大的类型系统(type system),还可以为开发者提供精确的数据模型管理能力,并确保客户端与服务器之间的通信安全性和可靠性
2.基本概念及术语
2.1 Graphql
- 查询语言:Graphql查询语言(简称GraphQL QL)是一种用于定义数据查询需求的高级字符串型脚本语言。
- 类型系统:GraphQL支持基于类型系统的功能。该系统允许开发者通过明确的数据结构定义来管理信息。每个 GraphQL 类型都包含名称字段和可选属性等核心要素。
- 请求:客户端需向GraphQL服务端发送POST请求。此操作会携带预定义格式化的 GraphQL 查询语句作为请求参数。
- 执行器(Executor):GraphQL中的执行器负责接收并解析客户端提交的 GraphQL 查询语句。它会首先验证查询语句的语法完整性,并构建对应的抽象语法树结构。
- 响应:服务端接收到请求后会解析并执行相应的 GraphQL 查询。完成后会返回一个包含结果或错误信息的JSON格式数据包。
- 变量:为了提高灵活性和可扩展性(GraphQL), GraphQL允许客户端在提交至服务端前传入必要的变量参数。
2.2 Schema & Type
GraphQL服务的入口一般是一个URL地址。该架构通过一个Schema框架提供了所有可访问的数据类型及其关联属性。当客户端发起请求时,系统需要明确传输哪些数据项以及预期的数据格式. GraphQL中的类型体系由两类核心组件构成:基本类型与属性域.每一基本类型的结构由其属性域集合决定,而各属性域则明确了这些类型的可操作性与功能边界.值得注意的是,每一基本类型的属性域数量可能从零到多个不等,同时每一属性域的支持输入参数数量同样可能从零到多个不等.
2.3 Resolvers
在Graphed体系中, Resolver扮演着关键角色. Resolver负责在运行时解析并处理Graphed查询语句, 并整合结果以生成最终响应. 其主要职责是确定Graphed查询语句应返回何种类型的值. 每个Graphed数据类型都必须配置一个对应的Resolver. 其相应的执行机构会负责解析并处理这些Graphed查询.
2.4 Subscription
Subscription作为GraphQL的一个核心特性,在其生态系统中扮演着关键角色。该机制通过服务器主动向客户端推送数据流,并避免在常规查询模式下立即返回结果集。订阅采用异步轮询机制,在保障用户体验的同时实现高效的数据更新能力
2.5 Batching
Batching也被视为GraphQL的一种扩展功能。该技术使客户端能够在一个请求中同时提交多个查询,并降低了网络传输量和响应时间。
2.6 Interface Merging
除了作为GraphQL的一个扩展功能外, Interface Merging还支持不同服务提供商的GraphQL接口整合在一起, 形成一个更为统一和高效的接口.
3.原理和具体操作步骤
3.1 理解graphql语法规则
3.1.1 关键字定义
query{...} //查询语句,query关键字表示这是一个查询语句
mutation{...} //修改语句,mutation关键字表示这是一个修改语句
subscription{...} //订阅语句,subscription关键字表示这是一个订阅语句
type{...} //类型定义,type关键字表示这是一个类型的定义
field{...} //字段定义,field关键字表示这是一个字段的定义
interface{...} //接口定义,interface关键字表示这是一个接口的定义
union{...} //联合类型定义,union关键字表示这是一个联合类型的定义
enum{...} //枚举类型定义,enum关键字表示这是一个枚举类型的定义
input{...} //输入类型定义,input关键字表示这是一个输入类型的定义
implements{...} //实现接口,implements关键字表示这是一个实现接口的定义
extend{...} //扩展类型,extend关键字表示这是一个扩展类型定义
schema{...} //定义GraphQL schema,schema关键字表示这是一个定义GraphQL schema的语句
directive@name{...} //自定义指令,directive关键字表示这是一个自定义指令的定义
fragment name on type{...} //片段定义,fragment关键字表示这是一个片段的定义
$variable:type = defaultValue //变量定义,$符号表示这是一个变量的定义,冒号前面的名字表示变量名,冒号后面的类型表示变量的类型,等于号后面的是默认值。
代码解读
3.1.2 标量类型
Int # 整数
Float # 浮点数
String # 字符串
Boolean # 布尔值
ID # 唯一标识符,如UID
代码解读
3.1.3 复合类型
[Type] # 表示该字段可以返回一个数组,其元素类型为Type
[Int!]! # 表示该字段可以返回一个非空且元素类型为Int的嵌套数组
[[Int]] # 表示该字段可以返回一个二维数组,其元素类型为Int
InputObject # 输入对象类型
ObjectType # 对象类型
InterfaceType # 接口类型
UnionType # 联合类型
EnumType # 枚举类型
ScalarType # 标量类型
代码解读
3.1.4 操作符
queryName { } //查询命名空间
mutationName { } //修改命名空间
subscriptionName { } //订阅命名空间
:param_name //查询参数定义
"Description" #注释
FieldAlias: fieldName @Directive(arg:"value") //字段别名和指令
fragment frag_name on TYPE {...} //片段定义
{...frag_name } //片段引用
Query($var_name: Type!) {...} //查询变量定义
Mutation($var_name: Type!) {...} //修改变量定义
Subscription($var_name: Type!) {...} //订阅变量定义
on OperationType {... } //订阅操作类型
&anotherType //接口合并
|otherType //联合类型
...TypeName //扩展类型
__typename //内置变量,获取当前对象类型名称
field(arg: "value") //字段调用,括号中代表可选的参数
[key: value] #表示映射表
代码解读
3.1.5 查询
{
myField {
subFieldA
subFieldB
}
}
代码解读
myField是一个返回值为对象类型或数组类型的字段;其中包含两个子字段subFieldA和subFieldB;这些子字段共同决定了最终的输出结果。
3.1.6 参数
{
fieldWithArg(arg: "value"){
id
title
}
}
代码解读
fieldWithArg是一个具有特定参数的字段,其值设为"value";其中id和title均为其子字段。
3.1.7 变量
query ($userId: ID!, $postId: Int){
user(userId: $userId){
posts (postIds: [$postId]){
text
}
}
}
代码解读
userId和postId都是变量,可以在多个地方被引用。
3.1.8 别名
{
userList: users {
username
},
postList: posts {
text
}
}
代码解读
userList和postList是两个别名,分别对应users和posts的查询结果。
3.1.9 字段合并
{
user {
username
email
}
profilePic: userProfilePic(size: 50){
url
}
}
代码解读
用户被定义为一个根节点,在此架构中包含多个属性与关系:其中用户名和电子邮箱字段分别作为其子字段存在。同时对变量命名进行优化以提升代码可读性:将变量名称重新赋值为其别名为userProfilePic的对象,并将其返回值设定为一个独立的对象类型:该对象作为返回值供后续处理使用:而URL字段也作为其中一员出现以确保数据完整性
3.1.10 自定义指令
query {
user(userId: "1"){
firstName @capitalize
lastName @uppercase
}
}
directive @capitalize on FIELD_DEFINITION | FRAGMENT_SPREAD | INLINE_FRAGMENT {
selectionSet(selections: [FIELD]) {
if (!selectionSet ||!selectionSet.selections) return;
for (const selection of selectionSet.selections) {
switch (selection.kind) {
case 'FragmentSpread':
capitalizeSelectionSet(
fragment: getFragment(selection),
visitedFragments: new Set(),
);
break;
case 'InlineFragment':
capitalizeSelectionSet(
fragment: selection.selectionSet,
visitedFragments: visitedFragments,
);
break;
default:
// Capitalize only non aliased fields and fragments selections.
const aliasOrName = selection.alias?? selection.name;
if (/^[a-zA-Z][a-z]*$/.test(aliasOrName)) {
selection.alias = aliasOrName
.charAt(0).toUpperCase() + aliasOrName.slice(1);
}
}
}
}
}
function capitalizeSelectionSet({ fragment, visitedFragments }) {
let visited = false;
if (visitedFragments.has(fragment.name.value)) {
visited = true;
} else {
visitedFragments.add(fragment.name.value);
visited = false;
}
for (const selection of fragment.selectionSet.selections) {
switch (selection.kind) {
case 'FragmentSpread':
if (!visited) {
capitalizeSelectionSet(
fragment: getFragment(selection),
visitedFragments: visitedFragments,
);
}
break;
case 'InlineFragment':
if (!visited) {
capitalizeSelectionSet(
fragment: selection.selectionSet,
visitedFragments: visitedFragments,
);
}
break;
default:
// Capitalize only non aliased fields and fragments selections.
const aliasOrName = selection.alias?? selection.name;
if (/^[a-zA-Z][a-z]*$/.test(aliasOrName)) {
selection.alias = aliasOrName
.charAt(0).toUpperCase() + aliasOrName.slice(1);
}
}
}
}
function getFragment(spread) {
const fragmentDoc = parse(`fragment ${spread.name.value} on ${spread.typeName.name.value} {}`);
return fragmentDoc.definitions[0];
}
代码解读
自定义指令capitalize,可以修改查询语句的输出,使字段首字母大写。
query {
user(userId: "1"){
firstName @capitalize
lastName @uppercase
}
}
代码解读
firstName和lastName将输出为大写字母。
{
"data": {
"user": {
"firstName": "John",
"lastName": "DOE"
}
}
}
代码解读
