GraphQL:一種不同於REST的接口風格


從去年開始,JS算是完全踏入ES6時代。在React相關項目中接觸到了一些ES6的語法。這次接着GraphQL這種新型的接口風格,從后端的角度接觸ES6。

這篇文章從ES6的特征講起,打好語法基礎;然后引用GraphQL的規范說明;最后實驗性質地在node環境下實踐GraphQL這種接口風格,作為接下來重構接口工作的起點。

  1. ES6
  2. GraphQL
  3. Node ES6語法環境
  4. 搭建GraphQL Server

ES6

babel learning page

ES6也就是ECMAScript2015於2015年6月正式發布,這是最新的Javascript核心語言標准。新的語法規范涵蓋各種語法糖和新概念。ES6既兼容過去編寫的JS代碼,又以一種新的方式徹底改JS代碼。ES6始終堅持這樣的宗旨:

凡是新加入的特性,勢必已在其它語言中得到強有力的實用性證明。

下面依據Babeljs的文檔介紹ES6的新特性。

Arrows:箭頭函數

能夠編寫lambda函數的新語法,它的語法非常簡單:標志符=>表達式。表達式可以是返回值,也可以是塊語句(塊語句需要使用return手動返回)。當然要注意下列代碼出現的情況。由於空對象與塊語句的符號都是使用{}標志,箭頭函數看到{}會判定為空語法塊,需要強制使用括號包裹空對象。

let items = Objs.map(stuff => {}); //空語法塊 let items = Objs.map(stuff => ({})); //空對象 

並且,箭頭函數的this值繼承外圍作用域,共享父函數的“arguments”參數變量。

Class:類

我們知道在ES5中我們用多種方式實現函數的構造,這些分發看起來都比較復雜。ES6提供了一種原型OO的語法糖。比如使用static添加方法時,函數的.prototype屬性也能添加相應的方法。

Subclassing:子類

ES5中原有的繼承方式是這樣的:

為了使新創建的類繼承所有的靜態屬性,我們需要讓這個新的函數對象繼承超類的函數對象;同樣,為了使新創建的類繼承所有實例方法,我們需要讓新函數的prototype對象繼承超類的prototype對象。

ES6添加使用關鍵詞‘extends’聲明子類繼承父類,使用關鍵詞‘super’訪問父類的屬性。而父類可以使用new.target來確定子類的類型。

Template String:模板字符串

`Hello, This is template of ${language}?` 這種使用反引號的字符串就是模板字符串,它為JS提供了簡單的字符串插值。

Destructuring:解構

解構賦值允許你使用類似數組或者對象字面量的語法將數組和對象的屬性賦給各種變量。

let [foo, [[bar], baz]] = [1, [[2], 3]]; //嵌套數據解構 let { name: nameA } = { name: 'Ips' } //對象解構 

解構還可以應用到交換變量、函數返回多值、函數參數默認值(like python),使編寫的代碼更加簡潔。

Symbols:符號

JS的第七種類型的原始量,能夠避免沖突的風險地創建作為屬性鍵的值險。

Iterators:迭代器

ES6增加了新的一種循環語法 for-of。該方法可以正確響應break、continue、return。

向對象添加Symbol.iterator,就可以遍歷對象。迭代器對象是具有.next()方法的對象。for-of首次調用集合的Symbol.iterator()方法,緊接着返回一個新的迭代器對象。for-of循環每次調用.next()方法。比如下面這個迭代器實現了每次返回0。

let objIterator = { [Symbol.iterator]: function(){ return this; }; next: function(){ return { done: false, value: 0 }; } } 

Generators:生成器

生成器就是包含 yield 表達式的函數。yield類似return,不過在生成器的執行過程腫,遇到yield時立即暫停,后續可以恢復執行狀態。普通函數使用function聲明,而生成器函數使用function*聲明。

所有的生成器都有內建.next()和Symbol.interator方法的實現,所以生成器就是迭代器。

Modules:模塊

模塊標志就是一段腳本,Node采用CommonJS的方式模塊化。在ES6中的模塊默認在嚴格模式下運行模塊,並且可以使用關鍵詞‘import’和‘export’。‘export’可以導出最外層的函數、類以及var、let或者const聲明的變量。‘import’可以直接導入或者導入模塊內部多個模塊、重命名模塊。除了node使用‘require’關鍵字外,ES6的模塊和node的是一樣的。

當JS引擎運行模塊時,按照下列四個步驟執行:

  1. 語法解析:閱讀模塊源代碼,檢查語法錯誤。
  2. 加載:遞歸地加載所有被導入的模塊。這也正是沒被標准化的部分。
  3. 連接:每遇到一個新加載的模塊,為其創建作用域並將模塊內聲明的所有綁定填充到該作用域中,其中包括由其它模塊導入的內容。
  4. 運行時:最終,在每一個新加載的模塊體內執行所有語句。

Proxies:代理

代理(Proxy)對象作為定義對象基礎操作(get、set、has等總共14個方法名稱)的全局構造函數。它接受兩個參數:目標對象與句柄對象。

var p = new Proxy(target, handler);

代理的行為很簡單:將代理的所有內部方法轉發到目標對象。而句柄對象是用來覆寫任意代理的內部方法。

Reflect:反射

ES6的Reflect對象提供對任意對象進行某種特定的可攔截操作(interceptable operation)。Reflect對象提供14個與代理方法名字相同的方法,可以方便的管理對象。使用時直接通過Reflect.method()這樣來調用。

Promises:

Promise代表某個未來才會結束的事件的結果,這通常是異步的。ES 
6提供Promise后,就可以將異步操作以同步操作的流程表達出來。Promise接受一個executor參數。executor帶有resolve、reject參數,resolve失成功的回調函數,reject是失敗的回調函數。

Promise對象是一個返回值的代理,這個返回值在promise對象創建時是未知的。

如圖,Promise對象有:pending、fulfilled、rejected狀態。pending狀態可以轉換成帶成功值的fulfilled狀態,也可以轉換成帶失敗信息的rejected狀態。當狀態發生變化時,就會調用綁定在.then上的方法。

promise from mdn

創建一個Promise:

let p = new Promise(function(resolve, reject) { if (/* condition */) { resolve(/* value */); // fulfilled successfully } else { reject(/* reason */); // error, rejected } }); 

Promise的.then()方法接受兩個參數:第一個函數當Promise成功(fulfilled)時調用,第二個函數當Promise失敗(rejected)時掉用。

p.then((val) => console.log("fulfilled:", val), (err) => console.log("rejected: ", err)); 

上述代碼等價於

p.then((val) => console.log("fulfilled:", val)) .catch((err) => console.log("rejected:", err)); 

Others:新增數值字面量、數據結構、庫函數

ES6還有一些新增的特性,這些都是不對語言原有的內容進行沖突而加入的補充功能。

GraphQL

GraphQL是一種API查詢語言,也是開發者定義數據的類型系統在服務器端的運行時。

GraphQL分為定義數據和查詢交互過程。比如定義一個包含兩個字段的User類型的GraphQL service,其提供數據結構和處理該類型各字段的函數。

type User{  
  id: ID
  name: String
}
function User_name(user){  
  return user.getName();
}

而查詢的方式與json類型有點相似。

{
  user{
 name } } 

查詢返回的數據可以以一個json對象的形式表達。

{
  "data": { "user": { "name": "Leo" } } } 

@medium

上圖來自medium的文章。GraphQL的查詢與Rest風格是不一樣的。Rest的數據是以資源為導向的,交互圍繞着定位資源的路由(Route)進行;而GraphQL的模型與對象模型更加類似,模型是通過圖的形式組織數據。相比Rest在客戶端定義響應數據的結構,GraphQL靈活地將響應數據的結構交給了客戶端。這樣的好處是:客戶端只需要一次請求就能夠獲得結構復雜的數據。

GraphQL有着自己的規范。依據官網給出的主要概念,規范文檔主要分為查詢操作和封裝數據的類型系統兩方面的內容。

Query and Mutation:查詢和修改

查詢和修改都是針對GraphQL服務器的查詢操作。

Field:字段

GraphQL對數據對象的指定字段進行操作。

除了上一節的查詢,還可以對內嵌對象、數組進行查詢:

{
 user {  name  friends {  name  }  } } 

其json格式的結果如下:

{
    "data": { "user":{ "name": "Leo", "friends": { […] } } } } 

Arguments:查詢參數

查詢語法還支持傳遞參數,並且參數也是可以嵌套的。

{
 user(id: "1003"){  name  } } 

Aliases:別名

如同SQL的AS作別名功能一樣,我們可以對每一個查詢字段的:前面添上別名。

{
 Chinese: user(nation: "china"){  name  } } 

Fragments:片段

片段可以構造查詢需要的字段,用分割復雜應用所需的數據來提高查詢語句的復用程度。

{
 Chinese: user(nation: "china"){  ...comparisonFields  }  American: user(nation: "America"){  ...comparisonFields  } } fragment comparisonFields on User{  name  age  speaksLanguage } 

Variables:查詢中的變量

為了動態傳遞參數,GraphQL提供了查詢語言設置變量的功能,查詢以字典的形式傳遞變量。

query UserNameAndFriends($age: Age) {  //變量定義: 變量以$前綴,后接類型  
  user(age: $age) {
 name  friends {  name  } } } { “age”: 26 } 

Directives:指令

在查詢中標記字段的指令,可以改變查詢的結構。比如下述這兩種指令就能控制字段是否返回。

  • @include(if: Boolean) 條件為真時,只返回當前字段
  • @skip(if: Boolean) 條件為真時,過濾掉該字段

Mutations:修改數據

就像Rest以PUT/POST約定為修改服務器端數據一樣,Mutations操作在GraphQL的意義就是修改數據庫。就像官網中的例子:

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { //!表示必須填寫的查詢條件  
  createReview(episode: $ep, review: $review) {
 stars  commentary } } { "ep": "JEDI", "review": {  "stars": 5,  "commentary": "This is a great movie!" } } 

需要注意的是,為了保證mutation操作不沖突,mutation只能序列執行。而query可以並行。

Inline Fragments:內聯片段

使用內聯片段返回接口或者聯合類型(interface、union)的數據。如果查詢接口或者聯合類型的字段,會返回其具體的類型。比如下方的例子,這個查詢的fragment以 ... on Droid 標記,表示當Hero的Character是Droid類型時primaryFunction字段才會被執行。同樣的,height字段只有在Human類型下才顯示。

query HeroForEpisode($ep: Episode!) {  
  hero(episode: $ep) {
 name  ... on Droid {  primaryFunction  }  ... on Human {  height  } } } 

Meta fields:元字段

元字段用來描述查詢中的各個字段。比如當Query查詢__typename時,服務器端就會返回響應的數據類型。

Schema and Type:數據結構和類型

GraphQL有着自己的類型系統來描述被查詢的數據。

Type system:類型系統

當接收到客戶端發送的查詢時,服務器毀從指定的‘root’對象開始,一層層選擇查詢字段。GraphQL的結合與返回結果類似,客戶端通過schema可以預知服務器大概返回的結果。

Type language:類型語言

GraphQL不依賴特定的編程語言,自有一套GraphQL schema language,與大多數的查詢語言類似。

Object types and fields:對象類型和字段

對象類型是GraphQL用來表示該對象結構的對象,其包含查詢的目標字段。

Query and Mutation types:

這兩個是特殊的類型。每一個GraphQL必須有一個Query來指定查詢處理。

Scalar types:默認標量類型

GraphQL對象類型有Int、Float、String、Boolean、ID這幾種標量類型。

Enumeration types:枚舉類型

枚舉類型用來指定該類型的取值(可數的)。比如下列Nation類型只能取China、Japan、India這三個值。

enum Nation {  
  China
  Japan
  India
}

Lists:列表

GraphQL支持的數組類型。除了對象、標量、枚舉類型這些類型外,還可以將字段定義為數組類型的數據,該字段能夠內嵌包含標量的數組。

Interface types:接口類型

接口是一種抽象類型,可以指定實現接口時的類型字段。比如下列代碼中的Character接口,和實現它的Human類型。Human類型除了實現接口必備的字段外,還有其特殊擁有的字段。

interface Character {  
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}
type Human implements Character {  
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

正如我們上面所說,接口的查詢需要借助內聯片段來查詢。

Union types:聯合類型

聯合類型與接口非常相似,不過其不需要指定公共字段。而是會把滿足查詢條件的所有union指定的數據組合在一個結果里。比如下列的SearchResult聯合類型,就可以將不同類型(Hunam | Droid | Starship)的數據對象以一個結果數組返回給客戶端。

union SearchResult = Human | Droid | Starship  

Input types:輸入類型

除了傳遞標量數據,查詢還可以傳遞復雜的對象。

input ReviewInput {  
  stars: Int!
  commentary: String
}

這樣我們在mutation時就可以傳遞一個對象ReviewInput作為查詢條件。

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {  

Execution:執行

當被認可后,GraphQL查詢就會被服務器執行並返回給客戶端。GraphQL借助類型系統來執行查詢,將每個字段當作函數或者上個類型的方法。而這類方法就叫做resolver。當執行到一個字段,相應的函數resolver也會被執行。而我們大多數的開發任務都將在這里完成。

resolver(obj, args, context)的三個參數分別表示:

  • obj: 前一個對象,root字段時這個參數為空
  • args: 查詢條件參數
  • context: 上下文信息(比如用戶信息、數據庫鏈接)

如果resovler的執行是一種異步的方式(比如node中的數據庫操作),GraphQL會等待Promises。

Introspection

該特性支持查詢GraphQL Service提供查詢的Schema信息。比如schema可以獲得查詢的數據結構,type可以獲得字段的類型。

鋪墊了這么多,下面開始動手編寫GraphQL。首先,需要有一個支持ES6的node環境,然后搭建一個支持查詢MongoDB數據庫的Express with GraphQL。

Node with ES6

搭建Node環境版本為6.9.1,其可以通過--harmony參數運行帶ES6特性的代碼。但是Node不支持模塊的導入導出(import)等特性,我們還是需要借助Babel庫來將ES6的代碼轉換成兼容版本代碼。

首先我們將必要的包安裝好。

{
  "dependencies": { "bluebird": "^3.4.6", //提供異步Promise的 "body-parser": "^1.15.2", //解析http請求主體 "express": "^4.14.0", //后端框架 "express-graphql": "^0.6.1", //封裝上graphql的express "graphql": "^0.8.1", //GraphQL的node實現 "mongodb": "^2.2.11" //數據庫驅動 }, "devDependencies": { "babel-core": "^6.18.2", //babel編譯器 "babel-polyfill": "^6.16.0", //提供ES2015+的環境 "babel-preset-es2015": "^6.18.0", //提供所有2015包含的內容 "babel-preset-node6": "^11.0.0", //在node6.x的preset "babel-preset-stage-3": "^6.17.0", //提供stage-3 "babel-register": "^6.18.0" //babel require的鈎子 } } 

上述 babel-preset-* 表示設定轉碼規則,我們需要在.babelrc中添加這些規則。

{
  "presets": [ "es2015", "stage-3" ] } 

首先是入口文件,我們使用babel-register將后續的require改寫成使用Babel進行轉碼。

//index.js //require 'babel/register' to handle JavaScript code(successive 'require's will be babeled) require('babel-register') //rewrite require cmd with Babel transform require('./server.js') 

在寫后續的代碼(server.js)就可以使用ES6的語法,首先是編寫一個http服務器。

//server.js import express from 'express'; import schema from './schema.js'; import { graphql} from 'graphql'; import bodyParser from 'body-parser'; 

第一步是使用import引用依賴模塊。

//server.js let app = express(); let PORT = 2333; // parse post content as text app.use(bodyParser.text({ type: 'application/graphql'})) app.use('/graphql', (req, res) => { //GraphQL executor graphql(schema, req.body) .then((result) => { res.send(JSON.stringify(result, null, 2)); }) }); 

然后就是配置一個GraphQL的Endpoint。將所有給/graphql路徑的請求就交給GraphQL處理,並且請求的正文會被解析為'application/graphql'的文本。

let server = app.listen(PORT, function(){ let host = server.address().address; let port = server.address().port; console.log('GraphQL-api listening at http://%s:%s', host, port); }); 

最后就是啟動服務器。

而GraphQL處理請求的schema來自schema.js文件。schema.js中定義了一個簡單的schema,其包含一個query操作和一個mutation操作。

// schema.js import { GraphQLObjectType, GraphQLSchema, GraphQLInt, GraphQLString } from 'graphql'; // local variable to give client let count = 0; // return RootQueryType Object { field: count } let schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQueryType', fields: { count: { type: GraphQLInt, description: 'Get count value', resolve: function(){ return count; } } } }), // Note: Mutation is serialization of change data query mutation: new GraphQLObjectType({ name: 'RootMutationType', fields: { updateCount: { type: GraphQLInt, description: 'Update the count', resolve: function(){ count += 1; return count; } } } }) }); export default schema; 

我們打開命令行,敲入 curl -v -POST -H "Content-Type:application/graphql" -d 'query RootQueryType { count }' http://localhost:2333/graphql 就可以看到結果。

這樣,我們就完成基本GraphQL Service。

Express-GraphQL

上述內容雖然能夠完成GraphQL Server基本任務,但是對於調試不太友好。GraphiQL是官方推薦的調試工具,而express-graphql就集成了GraphiQL。所以我們用express-graphql重構下服務器代碼。首先我們將schema.js移到data目錄下方便管理代碼。然后用graphqlHTTP替換成處理/graphql路由的函數。

graphqlHTTP接受的參數:schema就是數據對象的schema,graphiql控制GraphiQL(debug一般開啟)的提供,pretty參數控制json響應的形式,rootValue用來傳遞在整個graphql共享的變量,formatError參數來指定處理錯誤的方式。

//server.js import express from 'express'; import query_schema from './data/schema.js'; import graphqlHTTP from 'express-graphql'; import bodyParser from 'body-parser'; import { MongoClient } from 'mongodb'; import Promise from 'bluebird'; let app = express(); let PORT = 2333; app.use(bodyParser.json({ type: 'application/json' })) app.use('/graphql', graphqlHTTP(req =>({ schema: query_schema, graphiql: true, // debug work pretty: true, rootValue: { db: req.app.locals.db }, // pass db(mongodb) to graphql formatError: error => ({ // return error message: error.message, locations: error.locations, stack: error.stack }) }))); 

在rootValue傳遞來一個express內置對象req的成員變量,在這個應用里是數據庫連接客戶端。這個客戶端的定義如下。使用MongoClient連接本地數據庫,第二個參數中的promiseLibrary用來指定異步處理的庫,這里選用的是Bluebird的Promise對象。當app.locals.db的引用變量被指定為成功連接數據庫的句柄后,就可以發布GraphQL service了。

MongoClient.connect('mongodb://localhost:27017/atm_analysis', { promiseLibrary: Promise }) .catch(err => console.error(err.stack)) .then(db => { app.locals.db = db; let server = app.listen(PORT, function () { let host = server.address().address; let port = server.address().port; console.log('GraphQL-api listening at http://%s:%s', host, port); // ipv6 is :: }); }); 

接下來看看,schema應該怎么寫。

首先是外層的GraphQL Schema對象,里頭包含里一個查詢。這個對象還內嵌了一個GraphQL Object類型的對象。對於這個內嵌對象,我們在resolve函數上進行數據庫查詢操作(node對於直接返回標量數據的resolver,會忽略resolver執行直接獲得數據,這樣可以加快響應速度)。

let NetnodeType = new GraphQLObjectType({ name: 'netnode', fields: { id: { type: GraphQLID }, net_node_name: { type: GraphQLString }, customer_name: { type: GraphQLString } } }); // create instance of 'GraphQLSchema' let schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'NetNodeInfo', //object description: 'get netnode geograph infomation about fault, alarm', fields: { test: { type: GraphQLString, description: 'test info string', resolve: function () { return 'test graphql'; } }, node: { type: new GraphQLList(NetnodeType), description: 'netnode info', async resolve({ db }, args) { let data = await db.collection('dbo.TBL_NETNODE_INFO').find().limit(1500).sort({ 'ID': 1 }).toArray(); return data.map(x => ({ id: x.ID, net_node_name: x.net_node_name, customer_name: x.customer_name })); } } } }) }); 

注意,這里一定要引入babel-polyfill庫,不然會由於node沒有完全支持async的相關特性,async函數的regenerator功能報錯。

對於客戶端的測試請求,我們可以先使用GraphiQL工具來操作。在瀏覽器敲入地址:http://localhost:2333/graphql

首先測試GraphQL的query,我們對NetNodeInfo的test字段進行查詢。

test query

我們看到返回的data中有對應的數據,證明GraphQL Service正常運行。

然后測試對於數據庫操作的字段,我們對NetNodeInfo的node字段進行查詢。通過GraphiQL上右側的自建文檔可以看到,這個字段內部的對象有3個字段。下圖的查詢結果是只對"id"和"netnodename"字段查詢的情況,返回的數據就不會包括沒有請求的字段(沒有"customer_name"字段)。

test node

GraphQL的這種靈活的接口能夠降低對於復雜結構數據的請求數量,進而減少網絡通信;而接口的自洽(自動生成接口文檔)可以幫助前后端開發者的溝通,從而提高開發效率。


免責聲明!

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



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