前端er了解GraphQL,看这篇就够了
GraphQL
在近几年被提到的次数越来越多,最近参加过的几次技术大会前端分会场均提到过。对于这种光看名字并不容易想到它是什么的东西,还是存在些神秘感的。于是,打算去了解一下GraphQL
到底是什么。
什么是GraphQL?
首先,GraphQL
来自Facebook,如果你也跟我一样完全没了解过它,不知道它到底是干什么的,那么你一定听说过另一个叫做 Structured QL
的东西。WHAT? 其实就是SQL了。
嗯,和
SQL
一样,GraphQL
是一门查询语言(Query Language)同样和
SQL
一样的是,GraphQL
也是一套规范,就像MySQL
是SQL
的一套实现一样,Apollo
,Relay
...也是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: 代表你要执行的动作是增删改
query
和mutation
统称为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/:shareId
FormData:
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 function
import { 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 {
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的方式在线编辑代码,本篇中的示例就是在这个工具的基础上实现的
觉得有帮助,请点右下角正在看哦 ↓