1

更轻松的使用GraphQL

引言

GraphQL是Facebook开发的一套数据查询解决方案,让我们先来看一下官方的定义:

GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data. GraphQL isn't tied to any specific database or storage engine and is instead backed by your existing code and data.

翻译过来就是:

GraphQL是对你的API的一种查询语言,并且提供了对你采用类型系统所定义的数据进行查询的服务器端运行时方案。GraphQL并不与特定的数据库或存储引擎绑定,而是能对你现有的代码和数据进行支持。

其中有2个重点:

  1. 一种查询语言
  2. 服务器端运行时方案

在网上能找到的文章往往对第一点描述的比较详细,而且这一点也确实比较吸引人。但对于关键的第二点,如何实现这套查询机制的介绍却很难找到。

以一个简单的blog为例

假设我们的blog有以下两张表:

用户表中的数据:

uid name avatar
1 Tom https://pre00.deviantart.net/...
2 Jerry https://vignette.wikia.nocook...

帖子表中的数据(考虑到允许用户修改头像,所以帖子表中不冗余作者的信息,而只有作者的ID):

pid title content authorId
1 foo xxx 1
2 bar yyy 2

然后,界面大致是上下两栏模式的,上部是帖子标题、内容等;下部是作者的名字、头像等。让我们来看一下resuful和GraphQL方案的实现对比。

restful接口

如果采用restful方案,我们通常会设计如下两个接口:

  • 查询帖子内容:GET /posts/:id
  • 查询作者信息:GET /users/:id

然后,前端先调用拉取帖子内容的接口,拿到类似如下的返回结果:

GET /posts/1

{
    "code": 0,
    "reason": "success",
    "data": {
        "pid": 1,
        "title": "foo",
        "content": "xxx",
        "authorId": 1
    }
}

然后,再根据上述结果中的authorId去调用拉取用户信息的接口,来获取作者的相关信息:

GET /users/1

{
    "code": 0,
    "reason": "success",
    "data": {
        "uid": 1,
        "name": "Tom",
        "avatar": "https://pre00.deviantart.net/2930/th/pre/i/2014/182/a/2/tom_cat_by_1997ael-d7ougoa.png"
    }
}

在Web前端这样调用问题还不大,但遇到App时,由于绘制界面是一体化的,所以必须要两个restful接口都调用完毕,才能绘制界面。

而随着需求的变化,这个页面可能还会要展现评论、评论发表者的头像,等等等等;这就会导致这里需要调用的接口越来越多,从而使得App渲染这个界面的速度越来越慢。

GraphQL方式

采用GraphQL方式,我们首先需要对数据进行类型定义:

用户定义:

# user schema
type User {
    uid : ID!
    name : String!
    avatar : String!
}

帖子定义:

# post schema
type Post {
    pid : ID!
    title : String!
    content : String!
    author : User!
}

查询定义:

type Query {
    post(id: ID): Post
}

然后,我们根据界面要求编写查询语句,因为界面要求同时展现帖子内容和作者信息,所以会有如下的GraphQL查询语句:

query {  
  post(id:1) {
    pid
    title
    content
    author {
      uid
      name
      avatar
    }
  }
}

因为数据定义中,post下的author成员是User类型的,所以我们只需要通过一次查询就能够拿到绘制界面所需的数据:

{
  "data": {
    "post": {
      "pid": "1",
      "title": "foo",
      "content": "xxx",
      "author": {
        "uid": "1",
        "name": "Tom",
        "avatar": "https://pre00.deviantart.net/2930/th/pre/i/2014/182/a/2/tom_cat_by_1997ael-d7ougoa.png"
      }
    }
  }
}

看到这里大家一定能体会到GraphQL的查询语言的爽点所在了,但网上大多数的资料也往往是继续介绍这个查询语言的更多语法,但对于服务器端如何执行查询却介绍的不够深入,所给出的简单的例子甚至是上述数据结构中的每一个成员变量都要写一个对应的resolver函数来进行查询的情况。

GraphQL 的服务器端解决方案

由于存在如上痛点,笔者在进行了相关的探索后,封装了一个使用上更简便的npm库(easy-graphql)。

easy-graphql设计了一套约定,使得开发会更便捷和规范: SQR

S - Schemas,即数据的类型定义
Q - Query,即对外提供的查询接口
R - Resolvers,即如何查询数据的函数实现

使用步骤
1. 根据SQR约定创建目录

按照上述约定来建立目录结构,指定的目录下存放对应的文件,比如上文blog的例子,我们建立的目录格式如下:

  1. 建立graphql目录作为根目录
  2. graphql下建立schemasresolvers两个子目录,分别用于存放数据类型定义文件和对应的查询解决实现函数文件
  3. 建立query.graphqls文件,用于对外提供的查询接口定义
graphql             # GraphQL相关定义、代码的跟目录
├── query.graphqls  # 对外提供的查询接口定义文件
├── resolvers       # 如何查询数据的函数实现文件所在目录
│   ├── post_resolver.js
│   └── user_resolver.js
└── schemas         # 数据的类型定义文件所在目录
    ├── post_schema.graphqls
    └── user_schema.graphqls
2. 创建数据类型定义(schema)文件

文件存放在graphql/schemas目录下,命名规则:xxx_schema.graphqls

帖子和用户的数据类型定义上文已有,此处不再赘述

3. 创建查询接口定义(query)文件

文件放在graphql目录下,命名为:query.graphqls

4. 创建数据查询的函数实现(reslver)文件

文件放在graphql/reslvers目录下,命名规则:xxx_resolver.js

这里就只针对上文提及的帖子内容和作者信息的查询是如何实现的(resolvers/post_reslver.js):

'use strict'

const fakeDB = require('../../fakeDB');

function fetchPostById (root, {id}, ctx) {
    // post的查询,第二个参数是查询语句中传入的
    let pid = parseInt(id);
    return fakeDB.getPostById(pid);
}

// 对post下的author字段进行查询解决的函数
function fetchUserByAuthorId (root, args, ctx) {
    // 执行完post的数据查询后,遇到需要author字段的情况,会再来调用本函数,root参数就是前一步查询完的post数据
    let uid = root.authorId;
    return fakeDB.getUserById(uid);
}

const postReolvers = {
    Query : {
        post : fetchPostById,
    },

    Post : {
        // 针对嵌套的数据结构需要的解决函数
        author : fetchUserByAuthorId,
    },
};
module.exports = postReolvers;
5. 初始化

新建一个easy-graphql对象:

const path = require('path');

const easyGraphqlModule = require('easy-graphql');

const basePath = path.join(__dirname, 'graphql');
const easyGraphqlObj = new easyGraphqlModule(basePath);
  • 可视化IDE调试

对于采用node.js来进行开发的话,GraphQL提供了可视化的图形化的Web界面来编写、调试查询语句。

express插件:express-graphql
KOA插件:koa-graphql

easy-graphql配合express-graphql使用:

const express = require('express');
const graphqlHTTP = require('express-graphql');

const allSchema = easyGraphqlObj.getSchema();

// using with express-graphql middleware
app.use('/graphql', graphqlHTTP({
    schema : allSchema,
    graphiql : true,
}));

然后,就能在浏览器中,直接访问对应的网址,打开可视化IDE调试界面了,效果如下图所示:

  • 直接接口形式

使用上面的中间件方案,我们已经可以实现对外提供GraphQL查询的能力了,但往往我们的项目已经有约定好的返回数据结构了,比如:

{
    "code" : 0,
    "reason" : "success",
    "data" : {...}
}

而直接采用插件形式,并不能自定义返回的数据结构,所以easy-graphql又提供了一个直接执行GraphQL查询语句的API:

/**
 * do the GraphQL query execute
 * @param {*} requestObj -  GraphQL query object {query: "..."}
 * @param {*} context - [optional] query context
 * @returns {Promise} - GraphQL execute promise 
 */
queryGraphQLAsync(requestObj, {context})

以使用express框架为例,我们可以自己实现一个接口供前端调用:

const bodyParser = require('body-parser');
app.use(bodyParser.json()); // for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded

app.post('/restful', async (req, res) => {
    let queryObj = req.body;
    
    let result;
    try {
        // using with your restful service
        result = await easyGraphqlObj.queryGraphQL(queryObj, {context: req});
    } catch (err) {
        console.error(err);
        res.json({code : -1, reason : "GraphQL error"});
        return;
    }
    
    res.json({
        code : 0,
        reason : "success",
        data : result.data,
    });
});
完整示例

完整的代码示例,请前往gayhub上的test目录下查看,欢迎大家给这个项目点赞!

参考资料

Introduction to GraphQL
Apollo GraphQL


FigoZhu
58 声望1 粉丝