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

15,227 阅读11分钟

image

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

什么是GraphQL?

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

  • 嗯,和SQL一样,GraphQL是一门查询语言(Query Language
  • 同样和SQL一样的是,GraphQL也是一套规范,就像MySQLSQL的一套实现一样,Apollo, Relay...也是GraphQL规范的实现
  • SQL不同的是,SQL的数据源是数据库,而GraphQL的数据源可以是各种各样的REST API,可以是各种服务/微服务,甚至可以是数据库

这里借Apollo官网的一张图来说明GraphQL所处的位置

这里借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/: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 {
  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的影子。

一些链接