更轻松的使用GraphQL

更轻松的使用GraphQL

引言

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

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.前端

翻译过来就是:java

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

其中有2个重点:git

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

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

以一个简单的blog为例

假设咱们的blog有如下两张表:数据库

用户表中的数据:express

uid name avatar
1 Tom https://pre00.deviantart.net/2930/th/pre/i/2014/182/a/2/tom_cat_by_1997ael-d7ougoa.png
2 Jerry https://vignette.wikia.nocookie.net/tomandjerry/images/2/29/Jerry_2.png

帖子表中的数据(考虑到容许用户修改头像,因此帖子表中不冗余做者的信息,而只有做者的ID):npm

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

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

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目录下查看,欢迎你们给这个项目点赞!

参考资料

相关文章
相关标签/搜索