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
在網上能找到的文章每每對第一點描述的比較詳細,並且這一點也確實比較吸引人。但對於關鍵的第二點,如何實現這套查詢機制的介紹卻很難找到。github
假設咱們的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方案,咱們一般會設計以下兩個接口:
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方式,咱們首先須要對數據進行類型定義:
用戶定義:
# 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
函數來進行查詢的狀況。
因爲存在如上痛點,筆者在進行了相關的探索後,封裝了一個使用上更簡便的npm庫(easy-graphql)。
easy-graphql
設計了一套約定,使得開發會更便捷和規範: SQR
S - Schemas,即數據的類型定義
Q - Query,即對外提供的查詢接口
R - Resolvers,即如何查詢數據的函數實現
按照上述約定來創建目錄結構,指定的目錄下存放對應的文件,好比上文blog的例子,咱們創建的目錄格式以下:
graphql
目錄做爲根目錄graphql
下創建schemas
和resolvers
兩個子目錄,分別用於存放數據類型定義文件和對應的查詢解決實現函數文件query.graphqls
文件,用於對外提供的查詢接口定義graphql # GraphQL相關定義、代碼的跟目錄 ├── query.graphqls # 對外提供的查詢接口定義文件 ├── resolvers # 如何查詢數據的函數實現文件所在目錄 │ ├── post_resolver.js │ └── user_resolver.js └── schemas # 數據的類型定義文件所在目錄 ├── post_schema.graphqls └── user_schema.graphqls
文件存放在graphql/schemas
目錄下,命名規則:xxx_schema.graphqls
帖子和用戶的數據類型定義上文已有,此處再也不贅述
文件放在graphql
目錄下,命名爲:query.graphqls
文件放在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;
新建一個easy-graphql
對象:
const path = require('path'); const easyGraphqlModule = require('easy-graphql'); const basePath = path.join(__dirname, 'graphql'); const easyGraphqlObj = new easyGraphqlModule(basePath);
對於採用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目錄下查看,歡迎你們給這個項目點贊!