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

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

這篇文章從ES6的特徵講起,打好語法基礎;而後引用GraphQL的規範說明;最後實驗性質地在node環境下實踐GraphQL這種接口風格,做爲接下來重構接口工做的起點。java

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

ES6

babel learning page

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

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

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

Arrows:箭頭函數

可以編寫lambda函數的新語法,它的語法很是簡單:標誌符=>表達式。表達式能夠是返回值,也能夠是塊語句(塊語句須要使用return手動返回)。固然要注意下列代碼出現的狀況。因爲空對象與塊語句的符號都是使用{}標誌,箭頭函數看到{}會斷定爲空語法塊,須要強制使用括號包裹空對象。mongodb

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

而且,箭頭函數的this值繼承外圍做用域,共享父函數的「arguments」參數變量。數據庫

Class:類

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

Subclassing:子類

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

爲了使新建立的類繼承全部的靜態屬性,咱們須要讓這個新的函數對象繼承超類的函數對象;一樣,爲了使新建立的類繼承全部實例方法,咱們須要讓新函數的prototype對象繼承超類的prototype對象。json

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的這種靈活的接口可以下降對於複雜結構數據的請求數量,進而減小網絡通訊;而接口的自洽(自動生成接口文檔)能夠幫助先後端開發者的溝通,從而提升開發效率。

相關文章
相關標籤/搜索