小洋的前端記事本(NO.5):GraphQL小實踐

catus發表於2016-12-19

這裡記錄著小洋童鞋在前端道路上的所思所想所感。

生命在於折騰,不在折騰中崩潰,就在折騰中涅槃。


GraphQL| A query language for your API,誕生於移動快速迭代時代,理念是給客戶端提供類似”資料庫操作“的能力,使其能夠從服務介面這個大的“大資料庫”中去篩選或修改想要的資料,用以滿足前端資料的個性化需求,既保證了多樣性,又控制了介面數量,期望更好的分類維護介面。GraphQL 伺服器有很多的實現,Node.js 當然也不例外,下面我們就一起折騰折騰 GraphQL 吧 : )

GraphQL Node.js Server

準備

首先給大家簡單的介紹一下 GraphQL 特性,GraphQL 其實是一種和後端交換資料的協議,像通過 SQL 語言可以運算元據庫一樣,GraphQL 通過 Schema 的方式來控制資料,並約定提供以下能力:

  • query 查詢資料 詳見
  • mutation 特殊查詢,可以看做為修改資料 詳見
  • introspection 檢查資料的支援情況(下文簡稱”自省”) 詳見

可以說很多資料交換的情景都可以使用 GraphQL 的概念,我們就先基於 Express 來快速打造一個 http 伺服器 ( GraphQL 官方並沒有說只限於 http) 用最簡單的方式體驗一下 GraphQL,我們的專案起始於 package.json:

{
  "name": "graphql-start",
  "description": "簡單體驗一下 GraphQL ",
  "private": true, 
  "dependencies": {
    "babel-register": "^6.18.0", // ES6 你懂的
    "babel-preset-es2015": "^6.18.0", // ES6 你懂的
    "express": "^4.14.0", // 快速搭一個伺服器
    "body-parser": "^1.15.2", // Express 中介軟體,獲取 GraphQL 請求用的
    "graphql": "^0.8.2" // 我們的主角,處理 GraphQL 請求用的
  }
}

$npm install 以後我們就可以愉快的玩耍了,首先搭好程式入口,本地服務以及處理 GraphQL 請求的 handler:

/* 入口:index.js */
require(`babel-register`)({
    presets: [ `es2015` ]
});
require(`./server.js`);

/* 本地服務:server.js */
// base
import express from `express`;
import {graphql} from `graphql`;
import bodyParser from `body-parser`;

// 組織服務端識別的 schema
import schema from `./schema`;

let app = express();
let PORT = 3000;

// 用 text 的方式解析 request doby,形象起見我們設定一個特殊的 Content-Type
app.use(bodyParser.text({type: `application/graphql`}));

// 建立 "/graphql" 的路由(可以看做一個API)接收 GraphQL 請求,使用 GET ? 特殊字元,url連結長度不考慮啦?
app.post(`/graphql`, (req, res) => {
    // 組織服務端識別的 schema, 並處理 GraphQL 請求
    graphql(schema, req.body).then((result) => {
        res.send(JSON.stringify(result, null, 2));
    })
});

let server = app.listen(PORT, function () {
    let host = server.address().address;
    let port = server.address().port;

    console.log(`GraphQL listening at http://127.0.0.1`, host, port);
});

/* schema 例項:schema.js */
import {GraphQLSchema, GraphQLInt, GraphQLString, GraphQLBoolean, GraphQLObjectType, GraphQLList} from `graphql`;

// 建立一個GraphQLSchema例項,它提供了一個配置,下面主要來寫我們的頂級鍵 query 和 mutation 
let schema = new GraphQLSchema({
  query: ... ,
  mutation: ...
});

export default schema;

上述程式碼建立了入口 index.js 、本地服務 server.js 以及 schema 例項 schema.js ,至此我們的 GraphQL 服務的骨架就出來了,下面我們只要來把 schema.js 填充完整就可以體驗 GraphQL 啦~

GraphQL Schema

以下所有程式碼均在 schema.js 中,開始前可以先了解一下 graphql-js

g1.png

我們可以按照上述描述資料篩選資料然後獲取資料的順序生成測試程式碼感受一下 GraphQL 的魅力吧~

目標資料

首先我們先建立一個模擬資料來源 todo list:

let TODOs = [
    {
        "id": 111,
        "title": "Read emails",
        "completed": false
    },
    {
        "id": 222,
        "title": "Buy orange",
        "completed": true
    }
];

todo list 是一個元素為 todo object 的陣列,由於我們要給客戶端提供篩選及自省的能力,所以我們要通過 GraphQL 的 type 來描述所有組織結構,首先我們來描述元素 todo object

// 申明 todo object 的結構及組成
let TodoType = new GraphQLObjectType({
    name: `todo`, 
    fields: function () {
        return {
            id: {
                type: GraphQLInt, // Int 型
                description: "todo object`s id"
            },
            title: {
                type: GraphQLString, // String 型
                description: "todo object`s title"
            },
            completed: {
                type: GraphQLBoolean, // Boolean 值
                description: "todo object`s status"
            }
        }
    }
});

那麼 todo list 顯而易見就是 new GraphQLList(TodoType) 啦,即元素為 TodoType 的陣列。

Schema

有了資料來源我們就可以愉快的通過 GraphQL Schema 來取資料啦,我們的第一個 Schema 就先為獲取整個 todo list 吧,先在伺服器上註冊一個處理 GraphQL 請求的方法:

let schema = new GraphQLSchema({
    query: new GraphQLObjectType({
        name: `getTODOs`, // query name
        description: `The TODOs!`,
        fields: function () {
            return {
                TODOs: { // 返回結果 {TODOs: [...]}
                    type: new GraphQLList(TodoType),
                    description: `The TODOs List!`,
                    resolve: function () {
                        // 返回 todo list
                        return TODOs;
                    }
                }
            }
        }
    })
    //, mutation: ...
});

之前已經建立了一個埠為3000,路由為 /graphql 的 GraphQL API,至此終於可以 $node index.js 開始我們的 GraphQL 之旅啦~

query

比如我們可以通過下面的 query 來獲取 TODOs 下的 title ,由於 TODOs 是陣列所以結果對應也是陣列:

g3.png

注:通過控制檯,Postman等均可構造 POST 請求驗證結果,本文所有請求都通過 curl 來構造(包括新增 ContentType 請求頭及 data)。

introspection

在介紹自省之前,我們可以先了解一下域,每個 GraphQL 根域都有 __schema 域,這個域有一個子域叫 queryType。我們可以通過查詢這些域來了解 GraphQL 伺服器支援那些查詢,按照 Object 的方式理解其實就是通過物件的屬性可以瞭解其結構及內容

g4.png

看到上面的結果大家應該都有感覺了,其實 GraphQL 請求的成功與否與介面的 Schema 設計結構息息相關,所有的資料結構查詢都是按協商好的規則進行的,還記得我們宣告的 GraphQLSchema 是怎麼寫的嗎?

{
    name: `getTODOs`,
    description: `The TODOs!`,
    fields: function () {
        return {
            TODOs: {
                type: new GraphQLList(TodoType),
                description: `The TODOs List!`
                ...
            }
        }
    }
}

發現結果是一一對應的了吧,哈哈~ 可以記住這個“萬能查詢”,同時 GraphQL 也提供了__Type, __TypeKind, __Field, __InputValue等等的關鍵字來檢測介面所支援查詢的屬性,比如我們現在已經知道了“/garphql”這個 API 會返回一個物件陣列,那我想知道他包含的物件是什麼結構咋變咧?

g5.png

沒錯我們可以通過限定 __Type 去查已經註冊的 “todo” 物件來自省介面,還有很多關鍵字方法都可以達到上述的效果本文就不一一列舉了。細心的同學發現上面 __type(name: todo) 的用法了吧,同樣也可以用到我們的程式碼中來查詢具體某一條的 todo 物件:

let schema = new GraphQLSchema({
    query: new GraphQLObjectType({
        name: `getTODO`,
        fields: function () {
            return {
                todo: {
                    type: TodoType, // 這次我們只返回一個 todo 物件
                    description: `The TODO!`,
                    args: {
                        id: {
                            type: GraphQLInt,
                            required: true 
                        }
                    },
                    resolve: function (root, param, context) {
                        console.log(param.id);
                          // 這裡就可以放和資料庫的互動了,本例就簡單的返回第一個 todo 物件
                        return TODOs[0];
                    }
                }
            }
        }
    })
    //, mutation: ...
});

g6.png

mutation

有查詢資料當然也有修改資料,mutation 查詢和普通查詢請求(query)的重要區別在於 mutation 操作是按照順序執行的,即多次修改資料後再查詢,其返回一定是最後一次修改後的結果:

let schema = new GraphQLSchema({
    // query: ... ,
    mutation: new GraphQLObjectType({
        name: `updateTODO`,
        fields: {
            updateTODO: {
                type: TodoType,
                description: `Update the TODO status`,
                args: {
                    id: {
                        type: GraphQLInt,
                        required: true
                    }
                },
                resolve: function (root, param, context) {
                   // 這裡就可以放和資料庫的互動了,本例就簡單的更新 completed 屬性
                      console.log(param.id);
                    TODOs[0].completed = true;
                    return TODOs[0];
                }
            }
        }
    })
});

g7.png

除錯

GraphiQL 可以快速聯想搜尋介面,並提供了非常強大的自省能力,是很好的提高 GraphQL 開發效率的工具

g2.png

總結

正如官方所說 GraphQL 是一個查詢語言,而且目前還未完成,未來也可能會有更多更大的變動,但已經拓寬了我們的思路,讓我們看到了更多的可能性。目前 GraphQL 利好主要是在於前端的開發效率,落地時需要服務端的全力配合,也存在著一定的安全風險(暴力破解介面能力等),最大的效能瓶頸可能來自於資料庫查詢,但究竟好不好用,關鍵是看你怎麼用了,對吧? 我們仍在前行,陽光總在風雨後。


相關文章