vlambda博客
学习文章列表

前端er了解GraphQL,看这篇就够了





GraphQL在近几年被提到的次数越来越多,最近参加过的几次技术大会前端分会场均提到过。对于这种光看名字并不容易想到它是什么的东西,还是存在些神秘感的。于是,打算去了解一下GraphQL到底是什么。

什么是GraphQL?

首先,GraphQL来自Facebook,如果你也跟我一样完全没了解过它,不知道它到底是干什么的,那么你一定听说过另一个叫做 Structured QL的东西。WHAT? 其实就是SQL了。

  • 嗯,和SQL一样,GraphQL是一门查询语言(Query Language

  • 同样和SQL一样的是,GraphQL也是一套规范,就像MySQLSQL的一套实现一样,ApolloRelay...也是GraphQL规范的实现

  • SQL不同的是,SQL的数据源是数据库,而GraphQL的数据源可以是各种各样的REST API,可以是各种服务/微服务,甚至可以是数据库





这里借Apollo官网的一张图来说明GraphQL在互联网应用架构中所处的位置

几个时间点

  • GraphQL规范于2015年开源

  • Subscriptions操作于2017年被加入到规范中

那么为什么叫 Graph呢?

Graph是图的意思,在GraphQL的世界里,万物皆为图,也就是说你可以把你的业务模型建模为图。

图是将很多真实世界现象变成模型的强大工具,因为它们和我们自然的心智模型和基本过程的口头描述很相似。

这里就涉及到了图论,Graph Database之类的知识,感兴趣可以某歌学习一下。

这样我们可以对GraphQL大体有了一个概念了。那么我们来大概了解一下GraphQL。

GraphQL为我们解决了什么问题呢?

简单的说,站在前端角度,很多文章都会提到过为了取代Restful API,稍微具体点:

  • API字段的定制化,按需取字段

  • API的聚合,一次请求拿到所有的数据

  • 后端不再需要维护接口的版本号了

  • 完备的类型校验机制,提供了更健壮的接口

在知道以上这些优点是如何做到的之前,我们先来对GraphQL做一个简单的学习

前端学习GraphQL

做为一名前端开发人员,只站在前端的角度上来说,更多的时候,我们需要关心的只有两个操作:

  • query: 在GraphQL中这个关键字属于schema(可以理解为协议)中的一种,代表你要执行的是查询动作,即增删改查中的查

  • mutation: 代表你要执行的动作是增删改

querymutation 统称为schema其实还有一个subscriptions在2017年被加入到规范(spec)中,让我们可以更轻松的实现推送功能

这里我们以一个公司内部的分享平台的两个场景为例,来介绍一下这两个操作如何使用。

query操作

首先,最基础的一个场景,分享平台首页需要调一个接口,获取全部的分享列表,目前这个接口的调用方式是:

 
   
   
 
GET /api/share/allShares

返回:

 
   
   
 
{ "shares": [ { "shareId": 1238272, "title": "分享一下Vue3.0", "desc": "Vue3.0就要发布了,带来了哪些新功能呢?", "where": "6F-19会议室", "startTime": 1548842400 }, { "shareId": 1238272, "title": "用flutter写app页面是一种什么样的体验", "desc": "用跨平台框架flutter来写app页面的初体验",  "where": "6F-17会议室", "startTime": 1548842400 }, { "shareId": 1238272, "title": "Cordova原理", "desc": "一起来了解一下Cordova", "where": "6F-19会议室", "startTime": 1548842400 } ]}

那么换成GraphQL的方式,我们可以这么写:

query {
shares {
title
desc
where
startTime
}
}
复制代码

咦?发现漏掉了一个字段,如果我们要跳至详情页,需要知道分享的id,改造一下:

 
   
   
 
# 给一个查询起一个名字是一个好习惯query findAllShares { shares {# 为id起了一个别名,叫shareId shareId: id title desc where startTime }}

到此,一个基础的查询操作就完成了。

分页

通常,如果列表类数据量比较大的话,我们会采用分页的方式获取数据,而非一次性获取全部数据,依然以刚才的分享列表获取为例,如果用传统的接口调用的方式,通常是要这样去调接口:

 
   
   
 
GET /api/share/allShares?star=0&limit=10

返回:

 
   
   
 
{ "allShares": { "totalCount": 3, "shares": [ { "shareId": 1238272, "title": "分享一下Vue3.0", "desc": "Vue3.0就要发布了,带来了哪些新功能呢?", "where": "6F-19会议室", "startTime": 1548842400 }, { "shareId": 1238273, "title": "用flutter写app页面是一种什么样的体验", "desc": "用跨平台框架flutter来写app页面的初体验", "where": "6F-17会议室", "startTime": 1548842400 }, { "shareId": 1238274, "title": "Cordova原理", "desc": "一起来了解一下Cordova", "where": "6F-19会议室", "startTime": 1548842400 } ] }}

我们来继续改造GraphQL的方式,分页的方式:

# 分页方式query findAllShares($start: Int!, $limit: Int = 10) { allShares (start: $start, limit: $limit) { totalCount shares { shareId: id title desc where startTime } }}

GraphQL提供了完备的分页解决方案,可参考 Pagination

下一场景,得到了所有的分享列表,可以进入详情页了。目前详情页有三个主要的查询接口:获取分享详情,获取分享的评论列表和获取分享者所有的分享列表。如果是传统的方式,我们需要调三个接口:

 
   
   
 
// 获取分享的详情GET /api/share/:shareId
 
   
   
 
// 分享详情返回{ "shareDetail": { "shareId": 1238274, "title": "Cordova原理", "desc": "一起来了解一下Cordova", "where": "6F-19会议室", "startTime": 1548842400, "attchments": "", "creatorId": 321, "lastUpdateTime": 1548842400, "logoUrl": "", ... }}
 
   
   
 
// 获取分享的评论列表GET /api/share/comments/:shareId
 
   
   
 
// 分享评论列表返回{ "commentInfo": { "totalCount": 5, "comments": [ { "id": 1, "content": "非常不错", "userId": 213, "commentTime": 1548842400, }, { "id": 2, "content": "很好", "userId": 214, "commentTime": 1548842400, }, { "id": 3, "content": "不错", "userId": 216, "commentTime": 1548842400, }, { "id": 4, "content": "Very GOOD!", "userId": 2313, "commentTime": 1548842400, } ] }}
 
   
   
 
// 分享的创建者的创建的全部分享列表GET /api/share/shares/:creatorId
 
   
   
 
// 分享创建者的全部分享返回{ "hisShares": [ { "shareId": 1238272, "title": "分享一下Vue3.0", "desc": "Vue3.0就要发布了,带来了哪些新功能呢?", "where": "6F-19会议室", "startTime": 1548842400 }, { "shareId": 1238273, "title": "用flutter写app页面是一种什么样的体验", "desc": "用跨平台框架flutter来写app页面的初体验",  "where": "6F-17会议室", "startTime": 1548842400 }, { "shareId": 1238274, "title": "Cordova原理", "desc": "一起来了解一下Cordova", "where": "6F-19会议室", "startTime": 1548842400 } ]}

那么如果用GraphQL的方式呢?

 
   
   
 
query shareDetailPage($shareId: Int!, $creatorId:ID!, $start: Int!, $limit: Int = 10) { # 分享详情 shareDetail: share (shareId: $shareId) { shareId: id title desc where logoUrl attchments }  # 评论信息 commentInfo(shareId: $shareId, start: $start, limit: $limit) { totalCount comments { id userId content commentTime } }  # TA的分享 hisShares (creatorId: $creatorId) { shares { title desc where startTime } }}

一个查询即可搞定。

mutation操作

变更操作,这里只介绍一种场景。到了分享详情页,我们可能会需要编辑这个分享,在传统的方式中,需要调一个更新操作的接口:

POST /api/share/update/:shareIdFormData:title=xxx&desc=xxx&where=xxx

调完此接口后为了确认确实已经更新成功了,我们可能还会调一次获取分享详情接口:

GET /api/share/:shareId

接下来我们换成GraphQL的方式:

mutation editShareInfo($shareObj: ShareInput!) { editShareInfo(shareInfo: $shareObj) { shareId: id title desc where logoUrl attchments }}

这样,便可以直接将分享内容修改并返回修改后的分享详情。

其他的功能

为了我们写查询语句部分代码能有更好的可复用性,GraphQL还提供了Fragments(片段), Inline Fragments(内联片段)和Directives(指令)功能。前两者可以类比为JavaScript中的function(函数)和anonymous function(匿名函数),Directives(指令)可以根据我们传的参数来决定某些字段是否需要返回。这里就不做过多介绍了。

以上的功能如何实现?

schema

通过上面的例子,肯定会产生些疑问,我们要如何知道可以查询哪些字段?使用哪些参数?这就需要引入schema了。

通俗点说,schema就是协议,规范,或者可以当他是接口文档。

GraphQL规定,每一个schema有一个根(root)query和根(root)mutation。

我们先来看Root Query怎么写,依然是上面的查询的例子

 
   
   
 
# 定义一个根查询type Query { # 可以查询的字段和参数 shares(start: Int = 0, limit: Int = 10, creatorId: ID): [Share!]! share(shareId: ID!): Share! commentInfo(shareId: ID!, start: Int = 0, limit: Int = 10): CommentInfo!}

数据类型

如果你熟悉TypeScript或Flow的话可能会发现上面的写法似曾相识,是的,里面的含义就是你想的那样。每一个可以查询的字段的参数后面会跟标明这个参数的类型,!用来表示这个参数不可以是空的。[]表示查询这个字段返回的是数组,[]里面是数组的类型。

上面我们还看到了一些在TypeScript中不存在的类型,比如ID,ID我们暂且把他当成字符串String类型就可以了。类似我们熟悉的JavaScrpit或TypeScript,GraphQL也有几个基础类型,在GraphQL中他们统称叫标量类型(Scalar Type),主要包括:Int(整型), Float(浮点型), String(字符串), Boolean(布尔型)和ID(唯一标识符类型)。同时,GraphQL也允许你去自定义标量类型,例如:Date类型,只需实现相关的序列化,反序列化和验证的功能即可。

对象类型

上面的根查询定义中,我们还看到了一些与业务相关的类型,比如Share, Comment,这些统称为对象类型对象类型也是GraphQL中的schema的基本组件,他可以告诉我们在服务上可以获得到哪些对象,以及这个对象有哪些字段。接下来我们要做的就是定义这些对象类型,直到全部为基础类型。

 
   
   
 
# 定义Share的对象类型type Share { id: ID! title: String! desc: String! startTime: Int! where: String attchments: String logoUrl: String creatorId: ID! lastUpdateTime: Int is_delete: Int score: Int createTime: Int!}
# 定义评论信息对象类型type CommentInfo { totalCount: Int! comments: [Comment!]!}
# 定义评论对象类型type Comment { id: ID! content: String! commentTime: Int! userId: ID! shareId: ID!}

这样,我们就完成了schema的定义。

其他类型和功能

GraphQL其实还有Enumeration types(枚举类型),Union types(联合类型)。同时,为了代码能更好的复用,GraphQL还提供了 Interface(接口)功能。这里就不做过多介绍了。

实现执行

GraphQL约定,我们需要为Root Query(根查询)和Root Mutation(根变更)里面的每一个字段提供一个resolver的函数。并包装成一个对象暴露出去,就像这样:

 
   
   
 
const resolvers = { // 这里面写查询操作字段的resolver函数 Query: {}, // 这里面写变更操作字段的resolver函数 Mutation: {},}
export default resolvers

让我们继续写完整:

 
   
   
 
// 一些加载数据的async functionimport { loadSharesFromDB, loadShareById, loadCommentsByShareId } from './datasource'
const resolvers = { // 这里面写查询操作字段的resolver函数 Query: { shares: (parent, { start, limit, creatorId }, context, info) => { return loadSharesFromDB(start, limit, creatorId) .then(...) },
share: (parent, { shareId }, context, info) => { return loadShareById(shareId) .then(...) },
commentInfo: (parent, { shareId, start, limit }, context, info) => { return loadCommentsByShareId(shareId, start, limit) .then(...) }, }, // 这里面写变更操作字段的resolver函数 Mutation: { // ... },}

同样的,对于mutation(变更)操作,我们也是先把schema完成:

 
   
   
 
# 定义Mutation根入口type Mutation { editShareInfo(shareInfo: ShareInput!): Share!}
input ShareInput { id: ID! title: String! desc: String! where: String}

然后,补全resolver函数:

 
   
   
 
import { updateShareInfo, loadShareById } from './datasource'
const resolvers = { Query: { // ... }, Mutation: { editShareInfo: (parent, { shareInfo }, context, info) => { // 更新分享详情,then获取更新后的分享详情 return updateShareInfo(shareInfo.id, shareInfo) .then(loadShareById(shareInfo.id)) }, },}
export default resolvers

到此,我们就实现了这个简单的GraphQL的Server了。

结语

GraphQL还有很多内容可以探索,使用。比如,如果用Schema构建函数来生成对象类型,可以标记某一个字段为废弃,并给出废弃原因。这样,在版本迭代时,就可以友好的提示到旧版本的使用者,促使其升级到最新的接口,通过某些检测手段,我们也能很轻松的知道旧版本的使用频率,从而方便的让我们在某一个时间彻底删掉这个字段。

如果说GraphQL有什么缺点,那可能就是上手确实没那么容易,而且对于后端同学来说,还是有很多坑要踩的,比如缓存,性能问题等。好在目前的GraphQL的资料已经不像几年前那样的匮乏,不管是官方还是社区,GraphQL可以参考的资源和解决方案都越来越多了。

不管怎样,单纯的对于前端er来说,如果说上一次前端的技术变革是SPA的普及的话,相信当下一次变革到来时,一定有GraphQL的影子。

一些链接
  • GraphQL Tutorials

  • GraphQL 中文教程

  • 知乎上很火的GraphQL 为何没有火起来?

  • graphpack 一个0配置的GraphQL server工具,非常适合初学者去了解GraphQL,并提供了Codepen的方式在线编辑代码,本篇中的示例就是在这个工具的基础上实现的






觉得有帮助,请点右下角正在看哦