本文首先介绍了 GraphQL,再通过 MongoDB + graphql + graph-pack 的组合实战应用 GraphQL,详细阐述如何使用 GraphQL 来进行增删改查和数据订阅推送,并附有使用示例,边用边学印象深刻~
如果希望将 GraphQL 应用到前后端分离的生产环境,请期待后续文章。
本文实例代码:Github
0. 什么是 GraphQL
GraphQL 是一种面向数据的 API 查询风格。
传统的 API 拿到的是前后端约定好的数据格式,GraphQL 对 API 中的数据提供了一套易于理解的完整描述,客户端能够准确地获得它需要的数据,没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
1. 概述
前端的开发随着 SPA 框架全面普及,组件化开发也随之成为大势所趋,各个组件分别管理着各自的状态,组件化给前端仔带来便利的同时也带来了一些烦恼。比如,组件需要负责把异步请求的状态分发给子组件或通知给父组件,这个过程中,由组件间通信带来的结构复杂度、来源不明的数据源、不知从何订阅的数据响应会使得数据流变得杂乱无章,也使得代码可读性变差,以及可维护性的降低,为以后项目的迭代带来极大困难。
试想一下你都开发完了,产品告诉你要大改一番,从接口到组件结构都得改,后端也骂骂咧咧不愿配合让你从好几个 API 里取数据自己组合,这酸爽
在一些产品链复杂的场景,后端需要提供对应 WebApp、WebPC、APP、小程序、快应用等各端 API,此时 API 的粒度大小就显得格外重要,粗粒度会导致移动端不必要的流量损耗,细粒度则会造成函数爆炸 (Function Explosion);在此情景下 Facebook 的工程师于 2015 年开源了GraphQL 规范,让前端自己描述自己希望的数据形式,服务端则返回前端所描述的数据结构。
简单使用可以参照下面这个图:
![ec358806c8e42b2e9d4b768e4d0e88e8.gif](https://i-blog.csdnimg.cn/blog_migrate/ff3877a6c3f48b900cc7262646e31905.gif)
比如前端希望返回一个 ID 为 233 的用户的名称和性别,并查找这个用户的前十个雇员的名字和 Email,再找到这个人父亲的电话,和这个父亲的狗的名字(别问我为什么有这么奇怪的查找 ),那么我们可以通过 GraphQL 的一次 query 拿到全部信息,无需从好几个异步 API 里面来回找:
query { user (id : "233") { name gender employee (first: 10) { name email } father { telephone dog { name } } }}
返回的数据格式则刚好是前端提供的数据格式,不多不少,是不是心动了
2. 几个重要概念
这里先介绍几个对理解 GraphQL 比较重要的概念,其他类似于指令、联合类型、内联片段等更复杂的用法,参考 GraphQL 官网文档 ~
2.1 操作类型 Operation Type
GraphQL 的操作类型可以是 query、 mutation 或 subscription,描述客户端希望进行什么样的操作
- query 查询:获取数据,比如查找,CRUD 中的 R
- mutation 变更:对数据进行变更,比如增加、删除、修改,CRUD 中的 CUD
- substription 订阅:当数据发生更改,进行消息推送
这些操作类型都将在后文实际用到,比如这里进行一个查询操作
query { user { id }}
2.2 对象类型和标量类型 Object Type & Scalar Type
如果一个 GraphQL 服务接受到了一个 query,那么这个 query 将从 RootQuery 开始查找,找到对象类型(Object Type)时则使用它的解析函数 Resolver 来获取内容,如果返回的是对象类型则继续使用解析函数获取内容,如果返回的是标量类型(Scalar Type)则结束获取,直到找到最后一个标量类型。
- 对象类型:用户在 schema 中定义的 type
- 标量类型:GraphQL 中内置有一些标量类型 String、 Int、 Float、 Boolean、 ID,用户也可以定义自己的标量类型
比如在 Schema 中声明
type User { name: String! age: Int}
这个 User 对象类型有两个字段,name 字段是一个为 String 的非空标量,age 字段为一个 Int 的可空标量。
2.3 模式 Schema
如果你用过 MongoOSE,那你应该对 Schema 这个概念很熟悉,翻译过来是『模式』。
它定义了字段的类型、数据的结构,描述了接口数据请求的规则,当我们进行一些错误的查询的时候 GraphQL 引擎会负责告诉我们哪里有问题,和详细的错误信息,对开发调试十分友好。
Schema 使用一个简单的强类型模式语法,称为模式描述语言(Schema Definition Language, SDL),我们可以用一个真实的例子来展示一下一个真实的 Schema 文件是怎么用 SDL 编写的:
# src/schema.graphql# Query 入口type Query { hello: String users: [User]! user(id: String): [User]!}# Mutation 入口type Mutation { createUser(id: ID!, name: String!, email: String!, age: Int,gender: Gender): User! updateUser(id: ID!, name: String, email: String, age: Int, gender: Gender): User! deleteUser(id: ID!): User}# Subscription 入口type Subscription { subsUser(id: ID!): User}type User implements UserInterface { id: ID! name: String! age: Int gender: Gender email: String!}# 枚举类型enum Gender { MAN WOMAN}# 接口类型interface UserInterface { id: ID! name: String! age: Int gender: Gender}
这个简单的 Schema 文件从 Query、Mutation、Subscription 入口开始定义了各个对象类型或标量类型,这些字段的类型也可能是其他的对象类型或标量类型,组成一个树形的结构,而用户在向服务端发送请求的时候,沿着这个树选择一个或多个分支就可以获取多组信息。
注意:在 Query 查询字段时,是并行执行的,而在 Mutation 变更的时候,是线性执行,一个接着一个,防止同时变更带来的竞态问题,比如说我们在一个请求中发送了两个 Mutation,那么前一个将始终在后一个之前执行。
2.4 解析函数 Resolver
前端请求信息到达后端之后,需要由解析函数 Resolver 来提供数据,比如这样一个 Query:
query { hello}
那么同名的解析函数应该是这样的
Query: { hello (parent, args, context, info) { return ... }}
解析函数接受四个参数,分别为
- parent:当前上一个解析函数的返回值
- args:查询中传入的参数
- context:提供给所有解析器的上下文信息
- info:一个保存与当前查询相关的字段特定信息以及 schema 详细信息的值
解析函数的返回值可以是一个具体的值,也可以是 Promise 或 Promise 数组。
一些常用的解决方案如 Apollo 可以帮省略一些简单的解析函数,比如一个字段没有提供对应的解析函数时,会从上层返回对象中读取和返回与这个字段同名的属性。
2.5 请求格式
GraphQL 最常见的是通过 HTTP 来发送请求,那么如何通过 HTTP 来进行 GraphQL 通信呢
举个栗子,如何通过 Get/Post 方式来执行下面的 GraphQL 查询呢
query { me { name }}
Get 是将请求内容放在 URL 中,Post 是在 content-type:application/json 情况下,将 JSON 格式的内容放在请求体里
# Get 方式http://myapi/graphql?query={me{name}}# Post 方式的请求体{ "query": "...