前端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起了一个别名,叫shareIdshareId: idtitledescwherestartTime}}
到此,一个基础的查询操作就完成了。
分页
通常,如果列表类数据量比较大的话,我们会采用分页的方式获取数据,而非一次性获取全部数据,依然以刚才的分享列表获取为例,如果用传统的接口调用的方式,通常是要这样去调接口:
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) {totalCountshares {shareId: idtitledescwherestartTime}}}
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: idtitledescwherelogoUrlattchments}# 评论信息commentInfo(shareId: $shareId, start: $start, limit: $limit) {totalCountcomments {iduserIdcontentcommentTime}}# TA的分享hisShares (creatorId: $creatorId) {shares {titledescwherestartTime}}}
一个查询即可搞定。
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: idtitledescwherelogoUrlattchments}}
这样,便可以直接将分享内容修改并返回修改后的分享详情。
其他的功能
为了我们写查询语句部分代码能有更好的可复用性,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: Stringattchments: StringlogoUrl: StringcreatorId: ID!lastUpdateTime: Intis_delete: Intscore: IntcreateTime: 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 {: 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的方式在线编辑代码,本篇中的示例就是在这个工具的基础上实现的
觉得有帮助,请点右下角正在看哦 ↓
