深入理解 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 的 Number
  • Float:浮点数,对应 JavaScript 的 Number
  • String:字符串,对应 JavaScript 的 String
  • Boolean:布尔值,对应 JavaScript 的 Boolean
  • ID: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:不能为 Null

    Non-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 带来的最大改变和收益。

参考资料

转载自:http://taobaofed.org/blog/2016/03/10/graphql-in-depth/

作者:云翮

时间: 2024-09-09 05:43:12

深入理解 GraphQL的相关文章

[译] 我经常听到的 GraphQL 到底是什么?

本文讲的是[译] 我经常听到的 GraphQL 到底是什么?, 原文地址:So what's this GraphQL thing I keep hearing about? 原文作者:Sacha Greif 译文出自:掘金翻译计划 译者:lsvih 校对者:xiaoyusilen,steinliber 我经常听到的 GraphQL 到底是什么? 当听说出了一门新技术的时候,你可能会和我一样有以下 3 种反应: 1. 嫌弃 又来一个 JavaScript 类库?反正我只用 JQuery 就行了.

[译] 理解 Service Workers

本文讲的是[译] 理解 Service Workers, 原文地址:Learning React.js is easier than you think 原文作者:Samer Buna 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:Cherry 校对者:LeviDing.undead25 学习 React.js 比你想象的要简单 通过 Medium 中的一篇文章来学习 React.js 的基本原理 你有没有注意到在 React 的 logo 中隐藏着

小洋的前端记事本(NO.5):GraphQL 小实践

这里记录着小洋童鞋在前端道路上的所思所想所感. 生命在于折腾,不在折腾中崩溃,就在折腾中涅槃. GraphQL| A query language for your API,诞生于移动快速迭代时代,理念是给客户端提供类似"数据库操作"的能力,使其能够从服务接口这个大的"大数据库"中去筛选或修改想要的数据,用以满足前端数据的个性化需求,既保证了多样性,又控制了接口数量,期望更好的分类维护接口.GraphQL 服务器有很多的实现,Node.js 当然也不例外,下面我们就

GraphQL 用例:使用 Golang 和 PostgreSQL 构建一个博客引擎 API

摘要 GraphQL 在生产环境中似乎难以使用:虽然对于建模功能来说图接口非常灵活,但是并不适用于关系型存储,不管是在实现还是性能方面. 在这篇博客中,我们会设计并实现一个简单的博客引擎 API,它支持以下功能: 三种类型的资源(用户.博文以及评论)支持多种功能(创建用户.创建博文.给博文添加评论.关注其它用户的博文和评论,等等.) 使用 PostgreSQL 作为后端数据存储(选择它因为它是一个流行的关系型数据库). 使用 Golang(开发 API 的一个流行语言)实现 API. 我们会比较

[译] REST API 已死,GraphQL 长存

本文讲的是[译] REST API 已死,GraphQL 长存, 原文地址:REST APIs are REST-in-Peace APIs. Long Live GraphQL 原文作者:Samer Buna 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:sigoden 校对者:jasonxia23.shawnchenxmu REST API 已死,GraphQL 长存 在使用多年的 REST API 后,当我第一次接触到 GraphQL 并了解到

C/C++:如何理解复杂的声明

http://blog.chinaunix.net/u/12783/showart_378340.html   C/C++:如何理解复杂的声明 这里说的声明,不光适用于C/C++,其他的一些语言也能适用. 与java和C#等不同,声明和定义在C/C++中有着比较明显的区别:声明仅仅是介绍名字(introduce names),而定义则会为该名字分配相应的空间.打个通俗的比喻:声明就是你在谈话中提到某个人的名字,而定义就是把你提到的这个人带到谈话的人群中来,让大家见识一下他/她是什么样子. 这里主

编程-我这个有不理解的地方帮我讲讲好吗?

问题描述 我这个有不理解的地方帮我讲讲好吗? #include ""mainwindow.h""#include ""ui_mainwindow.h""#include <QTextFrame>#include <QDebug>#include <QLineEdit>#include <QDialog>#include <QPushButton>#include &l

HTTP协议是无状态协议,怎么理解

HTTP协议是无状态协议,怎么理解?   Http是一个无状态协议,同一个会话的连续两个请求互相不了解,他们由最新实例化的环境进行解析,除了应用本身可能已经存储在全局对象中的所有信息外,该环境不保存与会话有关的任何信息.  自己的理解,在asp.net里:每次提交服务器的页面没有任何关系,每次记录在页面的信息下次提交是记不住的,(除了应用本身可能已经存储在全局对象中的所有信息外)在.net里实际就是ViewState,ViewState是asp.net中保存页面信息的基本单位,应用时就是保存在控

理解finalize()-析构函数替代者

函数 理解finalize()-析构函数替代者   在许多方面,Java 类似于 C++.Java 的语法非常类似于 C++,Java 有类.方法和数据成员:Java 的类有构造函数: Java 有异常处理.       但是,如果你使用过 C++ 会发现 Java 也丢掉一些可能是你熟悉的特性.这些特性之一就是析构函数.取代使用析构函数,Java 支持finalize() 方法.       在本文中,我们将描述 finalize() 与 C++ 析构函数的区别.另外,我们将创建一个简单的 A