[轉] Node.js 服務端實踐之 GraphQL 初探


https://medium.com/the-graphqlhub/your-first-graphql-server-3c766ab4f0a2#.n88wyan4e

0.問題來了

DT 時代,各種業務依賴強大的基礎數據平台快速生長,如何高效地為各種業務提供數據支持,是所有人關心的問題。

現有的業務場景一般是這樣的,業務方提出需求,然后尋找開發資源,由后端提供數據,讓前端實現各種不同的業務視圖。這樣的做法存在很多的重復勞動,如果能夠將其中通用的內容抽取出來提供給各個業務方反復使用,必然能夠節省寶貴的開發時間和開發人力。

前端的解決方案是將視圖組件化,各個業務線既可以是組件的使用者,也可以是組件的生產者。那么問題來了,前端通過組件實現了跨業務的復用,后端接口如何相應地提高開發效率呢?

我們假設某個業務需要以下數據內容 a:

 
{
user(id: 3500401) {
id,
name,
isViewerFriend
}
}

對,這不是 JSON,但是我們仍然可以看懂它表示的是查詢 id 為 3500401 用戶的 id,name 和 isViewerFriend 信息。用戶信息對於各個業務都是通用的,假設另外一個業務需要這樣的用戶信息 b:

 
{
user(id: 3500401) {
name,
profilePicture(size: 50) {
uri,
width,
height
}
}
}

對比一下,我們發現只是少了兩個字段,多了一個字段而已。如果要實現我們的目標,即復用同一個接口來支持這兩種業務的話,會有以下幾種做法:

  1. 用同一個接口,這個接口提供了所有數據。這樣做的好處是實現起來簡單,但缺點是對業務做判斷的邏輯會增多,而且對於業務來說,響應內容中有些數據根本用不到;
  2. 使用參數來區分不同的業務方並返回相應的數據。好處仍然是實現簡單,雖然不會有用不到的數據返回,但是仍然需要增加業務邏輯判斷,會造成以后維護的困難。

此外,這樣還會造成不同業務之間的強依賴,每次發布都需要各個業務線一起測試和回歸。不重用接口則沒法提高開發效率,重用接口則會有這些問題,那么到底有沒有“好一點”的解決方案呢?

這是我們在處理復雜的前后端分離中經常要面臨的一個思考。

1.GraphQL,一種新的思路

我們知道,用戶信息對應的數據模型是固定的,每次請求其實是對這些數據做了過濾和篩選。對應到數據庫操作,就是數據的查詢操作。如果客戶端也能夠像“查詢”一樣發送請求,那不就可以從后端接口這個大的“大數據庫”去過濾篩選業務需要的數據了嗎?

GraphQL 就是基於這樣的思想來設計的。上面提到的(a)和(b)類型的數據結構就是 GraphQL 的查詢內容。使用上面的查詢,GraphQL 服務器會分別返回如下響應內容。

a 查詢對應的響應:

 
{
"user" : {
"id": 3500401,
"name": "Jing Chen",
"isViewerFriend": true
}
}

b 查詢對應的響應:

 
{
"user" : {
"name": "Jing Chen",
"profilePicture": {
"uri": "http: //someurl.cdn/pic.jpg",
"width": 50,
"height": 50
}
}
}

只需要改變查詢內容,前端就能定制服務器返回的響應內容,這就是 GraphQL 的客戶端指定查詢(Client Specified Queries)。假如我們能夠將基礎數據平台做成一個 GraphQL 服務器,不就能為這個平台上的所有業務提供統一可復用的數據接口了嗎?

了解了 GraphQL 的這些信息,我們一起來動手實踐吧。

2.使用 Node.js 實現 GraphQL 服務器

我們先按照官方文檔搭建一個 GraphQL 服務器:

 
$ mkdir graphql-intro && cd ./graphql-intro
$ npm install express --save
$ npm install babel --save
$ touch ./server.js
$ touch ./index.js

index.js 的內容如下:

 
//index.js
//require `babel/register` to handle JavaScript code
require('babel/register');
require('./server.js');

server.js 的內容如下:

 
//server.js
import express from 'express';

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

app.post('/graphql', (req, res) => {
res.send('Hello!');
});

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

console.log('GraphQL listening at http://%s:%s', host, port);
});

然后執行代碼: nodemon index.js:

如果沒有安裝 nodemon,需要先 npm install -g nodemon,也推薦使用 node-dev 模塊

測試是否有效:

 
curl -XPOST http://localhost:3000/graphql

接着編寫 GraphQL Schema

接下來是添加 GraphQL Schema(Schema 是 GraphQL 請求的入口,用戶的 GraphQL 請求會對應到具體的 Schema),首先回憶一下 GraphQL 請求是這樣的:

 
query getHightScore { score }

上面的請求是獲取 getHightScore 的 score 值。也可以加上查詢條件,例如:

 
query getHightScore(limit: 10) { score }

這樣的請求格式就是 GraphQL 中的 schema。通過 schema 可以定義服務器的響應內容。

接下來我們在項目中使用 graphql:

 
npm install graphql --save

使用 body-parser 來處理請求內容:npm install body-parser --save。 而 graphql 這個 npm 包會負責組裝服務器 schema 並處理 GraphQL 請求。

創建 schema:touch ./schema.js

 
//schema.js
import {
GraphQLObjectType,
GraphQLSchema,
GraphQLInt
} from 'graphql';

let count = 0;

let schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
count: {
type: GraphQLInt,
resolve: function() {
return count;
}
}
}
})
});

export default schema;

這段代碼創建了一個 GraphQLSchema 實例。這個 schema 的頂級查詢對象會返回一個 RootQueryType 對象,這個 RootQueryType 對象有一個整數類型的 count 域。GraphQL 除了支持整數( Interger ),還支持字符串( String )、列表( List )等多種類型的數據。

連接 schema 

下面是將 GraphQL schema 和服務器連接起來,我們需要修改 server.js 為如下所示:

 
//server.js
import express from 'express';
import schema from './schema';

import { graphql } from 'graphql';
import bodyParser from 'body-parser';

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

//Parse post content as text
app.use(bodyParser.text({ type: 'application/graphql' }));

app.post('/graphql', (req, res) => {
//GraphQL executor
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://%s:%s', host, port);
});

驗證下效果:

 
curl -v -XPOST -H "Content-Type:application/graphql" -d 'query RootQueryType { count }' http://localhost:3000/graphql

結果如下圖所示:

GraphQL 查詢還可以省略掉 query RootQueryType 前綴,即:

檢查服務器

GraphQL 最讓人感興趣的是可以編寫 GraphQL 查詢來讓 GraphQL 服務器告訴我們它支持那些查詢,即官方文檔提到的自檢性(introspection)。

例如:

 
curl -XPOST -H 'Content-Type:application/graphql' -d '{__schema { queryType { name, fields { name, description} }}}' http://localhost:3000/graphql

而我們實際的 GraphQL 查詢請求內容為:

 
{
__schema {
queryType {
name,
fields {
name,
description
}
}
}
}

基本上每個 GraphQL 根域都會自動加上一個 __schema 域,這個域有一個子域叫 queryTyp。我們可以通過查詢這些域來了解 GraphQL 服務器支持那些查詢。我們可以修改 schema.js 來為 count 域加上 description:

 
let schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
count: {
type: GraphQLInt,
//Add description
description: 'The count!',
resolve: function() {
return count;
}
}
}
})
});

驗證一下:

 
curl -XPOST -H 'Content-Type:application/graphql' -d '{__schema { queryType { name, fields { name, description} }}}' http://localhost:3000/graphql

變異(mutation,即修改數據)

GraphQL中將對數據的修改操作稱為 mutation。在 GraphQL Schema 中按照如下形式來定義一個 mutation:

 
let schema = new GraphQLSchema({
query: ...
mutation: //TODO
});

mutation 查詢和普通查詢請求(query)的重要區別在於 mutation 操作是序列化執行的。例如 GraphQL 規范中給出的示例,服務器一定會序列化處理下面的 mutation 請求:

 
{
first: changeTheNumber(newNumber: 1) {
theNumber
},
second: changeTheNumber(newNumber: 3) {
theNumber
},
third: changeTheNumber(newNumber: 2) {
theNumber
}
}

請求結束時 theNumber 的值會是 2。下面為我們的服務器添加一個 mutation 查詢,修改 schema.js 為如下所示:

 
//schema.js
import {
GraphQLObjectType,
GraphQLSchema,
GraphQLInt
} from 'graphql';

let count = 0;

let schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
count: {
type: GraphQLInt,
//Add description
description: 'The count!',
resolve: function() {
return count;
}
}
}
}),
//Note:this is the newly added mutation query
mutation: new GraphQLObjectType({
name: 'RootMutationType',
fields: {
updateCount: {
type: GraphQLInt,
description: 'Update the count',
resolve: function() {
count += 1;
return count;
}
}
}
})
});

export default schema;

驗證:

 
curl -XPOST -H 'Content-Type:application/graphql' -d 'mutation RootMutationType { updateCount }' http://localhost:3000/graphql

搭建好 GraphQL 服務器后,我們來模擬下業務場景的實際需求,對於電商平台來說,最常用的就是商品信息,假設目前的商品數據模型可以用下面的 GraphQLObject 來表示:

 
var ItemType =  new GraphQLObjectType({
name: "item",
description: "item",
fields: {
id: {
type: GraphQLString,
description: "item id"
},
title: {
type: GraphQLString,
description: "item title"
},
price: {
type: GraphQLString,
description: "item price",
resolve: function(root, param, context) {
return (root.price/100).toFixed(2);
}
},
pic: {
type: GraphQLString,
description: "item pic url"
}
}
});

查詢商品的 schema 如下所示:

 
var ItemSchema = new GraphQLSchema({
query: {
name: "ItemQuery",
description: "query item",
fields: {
item: {
type: ItemType,
description: "item",
args: {
id: {
type: GraphQLInt,
required: true //itemId required for query
}
},
resolve: function(root, obj, ctx) {
return yield ItemService(obj['id']);
}
}
}
}
});

通過如下 query 可以查詢 id 為 12345 的商品信息:

 
query ItemQuery(id: 12345){
id
title
price
pic
}

商品詳情頁展示時需要加上優惠價格信息,我們可以修改 ItemType,為它加上一個 promotion 字段:

 
var ItemType =  new GraphQLObjectType({
name: "item",
description: "item",
fields: {
id: {
type: GraphQLString,
description: "item id"
},
title: {
type: GraphQLString,
description: "item title"
},
price: {
type: GraphQLString,
description: "item price",
resolve: function(root, param, context) {
return (root.price/100).toFixed(2);
}
},
pic: {
type: GraphQLString,
description: "item pic url"
},
promotion: {
type: GraphQLInt,
description: "promotion price"
}
}
});

商品詳情頁的查詢為:

 
query ItemQuery(id: 12345){
id
title
price
pic
promotion
}

ItemSchema 無需修改,只要在 ItemService 的返回結果中加上 promotion 就可以了。這樣接口的修改對於原有業務是透明的,而新的業務也能基於已有的代碼快速開發和迭代。

再假設有一個新的頁面,只需要用到寶貝的圖片信息,業務方可以使用下面的查詢:

 
query ItemQuery(id: 12345){
id
pic
}

服務器代碼不用做任何修改。

4.總結

至此我們已經實現了一個 GraphQL 基礎服務器。在實際業務中數據模型肯定會更加復雜,而 GraphQL 也提供了強大的類型系統(Type System)讓我們能夠輕松地描述各種數據模型,它提供的抽象層能夠為依賴同一套數據模型的不同業務方提供靈活的數據支持。關於 GraphQL 在淘寶更多的生產實踐,請持續關注我們博客未來的系列文章。

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM