更輕鬆的使用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/...
2 Jerry https://vignette.wikia.nocook...

帖子表中的數據(考慮到容許用戶修改頭像,因此帖子表中不冗餘做者的信息,而只有做者的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目錄下查看,歡迎你們給這個項目點贊!

參考資料

Introduction to GraphQL
Apollo GraphQL

相關文章
相關標籤/搜索