本次內容是基於以前分享的文字版,諾想看重點的話能夠看以前的 PPT。javascript
GraphQL 是一款由 Facebook 主導開發的數據查詢和操做語言, 寫過 SQL 查詢的同窗能夠把它想象成是 SQL 查詢語言,但 GraphQL 是給客戶端查詢數據用的。雖然這讓你聽起來以爲像是一款數據庫軟件,但實際上 GraphQL 並不是數據庫軟件。 你能夠將 GraphQL 理解成一箇中間件,是鏈接客戶端和數據庫之間的一座橋樑,客戶端給它一個描述,而後從數據庫中組合出符合這段描述的數據返回。這也意味着 GraphQL 並不關心數據存在什麼數據庫上。php
同時 GraphQL 也是一套標準,在這個標準下不一樣平臺不一樣語言有相應的實現。 GraphQL 中還設計了一套類型系統,在這個類型系統的約束下,能夠得到與 TypeScript 相近的相對安全的開發體驗。html
咱們先來回顧一下咱們已經很是熟悉的 RESTful API 設計。簡單的說 RESTful API 主要是使用 URL 的方式表達和定位資源,用 HTTP 動詞來描述對這個資源的操做。vue
咱們以 IMDB 電影信息詳情頁爲例子,看看咱們得須要什麼樣的 API 才能知足 RESTful API 設計的要求。先來看看主頁面上都須要什麼信息。java
能夠看到頁面上由電影基本信息,演員和評分/評論信息組成,按照設計要求,咱們須要將這三種資源放在不一樣 API 之下。首先是電影基本信息,咱們有 API /movie/:id
,給定一個電影ID返回基本信息數據。git
僞裝 GET 一下得到一個 JSON 格式的數據:github
{
name: 「Manchester by the Sea」,
ratings: 「PG-13」,
score: 8.2,
release: 「2016」,
actors:[「https://api/movie/1/actor/1/」],
reviews:[「https://api/movie/1/reviews」]
}
複製代碼
這裏麪包含了咱們所需的電影名、分級等信息,以及一種叫作 HyperMedia
的數據,一般是一個 URL,指明瞭可以獲取這個資源的 API 端點地址。若是咱們跟着 HyperMedia
指向的鏈接請求下去,咱們就能獲得咱們頁面上所需的全部信息。mongodb
GET /api/movue/1/actor/1
數據庫
{
name: 「Ben Affleck」,
dob: 「1971-01-26」,
desc: 「blablabla」,
movies:[「https://api/movie/1」]
}
複製代碼
GET /api/movie/1/reviews
npm
[
{
content: 「Its’s as good as…」,
score: 9
}
]
複製代碼
最後根據須要,咱們要將全部包含須要信息的 API 端點都請求一遍,對於移動端來講,發起一個 HTTP 請求仍是比較消耗資源的,特別是在一些網絡鏈接質量不佳的狀況下,一下發出多個請求反而會致使很差的體驗。
並且在這樣的 API 設計之中,特定資源分佈在特定的 API 端點之中,對於後端來講寫起來是挺方便的,但對於Web端或者客戶端來講並不必定。例如在 Android 或 iOS 客戶端上,發版升級了一個很爆炸的功能,同一個API上可能爲了支持這個功能而多吐一些數據。可是對於未升級的客戶端來講,這些新數據是沒有意義的,也形成了必定的資源浪費。若是單單將全部資源整合到一個 API 之中,還有可能會由於整合了無關的數據而致使數據量的增長。
而 GraphQL 就是爲了解決這些問題而來的,向服務端發送一次描述信息,告知客戶端所需的全部數據,數據的控制甚至能夠精細到字段,達到一次請求獲取全部所需數據的目的。
咱們先來看一下一個 GraphQL 請求長什麼樣:
query myQry ($name: String!) {
movie(name: 「Manchester」) {
name
desc
ratings
}
}
複製代碼
這個請求結構是否是和 JSON 有那麼點類似?這是 Facebook 故意設計成這樣的,但願你讀完以後就能體會到 Facebook 的用心良苦了。
那麼,上面的這個請求描述稱爲一個 GraphQL 請求體,請求體即用來描述你要從服務器上取什麼數據用的。通常請求體由幾個部分組成,從裏到外瞭解一下。
首先是字段,字段請求的是一個數據單元。同時在 GraphQL 中,標量字段是粒度最細的一個數據單元了,同時做爲返回 JSON 響應數據中的最後一個字段。也就是說,若是是一個 Object,還必須選擇至少其中的一個字段。
把咱們所須要的字段合在一塊兒,咱們把它稱之爲某某的選擇集。上面的 name
、desc
、ratings
合在一塊兒則稱之爲 movie
的選擇集,同理,movie
是 myQry
的選擇集。須要注意的是,在標量上使用不能使用選擇集這種操做,由於它已是最後一層了。
在 movie
的旁邊,name: "Manchester"
,這個表明着傳入 movie
的參數,參數名爲 name
值爲Manchester
,利用這些參數向服務器表達你所需的數據須要符合什麼條件。
最後咱們來到請求體的最外層:
query
表示查詢,mutation
表示對數據進行操做,例如增刪改操做,subscription
訂閱操做。$
符號開頭,冒號後面緊跟着變量的傳入類型。若是要使用變量,直接引用便可,例如上面的 movie 就能夠改寫成 movie(name: $name)
。若是上述三者都沒有提供,那麼這個請求體默認會被視爲一個 query
操做。
若是咱們執行上面的請求體,咱們將會獲得以下的數據:
{
"data": {
"movie": {
"name": "Manchester By the Sea",
"desc": "A depressed uncle is asked to take care of his teenage nephew after the boy's father dies. ",
"ratings": "R"
}
}
}
複製代碼
仔細對比結果和請求體的結構,你會發現,與請求體的結構是徹底一致的。也就是說,請求體的結構也肯定了最終返回數據的結構。
在前面的 REST 舉例中,咱們請求多個資源有多個 API 端點。在 GraphQL 中,只有一個 API 端點,一樣也接受 GET 和 POST 動詞,如要操做 mutation
則使用 POST 請求。
前面還提到 GraphQL 是一套標準,怎麼用呢,咱們能夠藉助一些庫去解析。例如 Facebook 官方的 GraphQL.js。以及 Meteor 團隊開發的 Apollo,同時開發了客戶端和服務端,同時也支持流行的 Vue 和 React 框架。調試方面,可使用 Graphiql 進行調試,得益於 GraphQL 的類型系統和 Schema,咱們還能夠在 Graphiql 調試中使用自動完成功能。
前面咱們提到,GraphQL 擁有一個類型系統,那麼每一個字段的類型是怎麼約定的呢?答案就在本小節中。在 GraphQL 中,類型的定義以及查詢自己都是經過 Schema 去定義的。GraphQL 的 Schema 語言全稱叫 Schema Definition Language。Schema 自己並不表明你數據庫中真實的數據結構,它的定義決定了這整個端點能幹些什麼事情,肯定了咱們能向端點要什麼,操做什麼。再次回顧一下前面的請求體,請求體決定了返回數據的結構,而 Schema 的定義決定了端點的能力。
接下來咱們就經過一個一個的例子瞭解一下 Schema。
先看右邊的 Schema:type 是 GraphQL Schema 中最基本的一個概念,表示一個 GraphQL 對象類型,能夠簡單地將其理解爲 JavaScript 中的一個對象,在 JavaScript 中一個對象能夠包含各類 key,在 GraphQL 中,type 裏面一樣能夠包含各類字段(field),並且字段類型不只僅能夠是標量類型,還能夠是 Schema 中定義的其餘 type。例如上面的 Schema 中, Query
下的 movie
字段的類型就能夠是 Movie
。
在 GraphQL 中,有以下幾種標量類型:Int
, Float
, String
, Boolean
, ID
,分別表示整型、浮點型、字符串、布爾型以及一個ID類型。ID類型表明着一個獨一無二的標識,ID
類型最終會被轉化成String
類型,但它必須是獨一無二的,例如 mongodb 中的 _id 字段就能夠設置爲ID
類型。同時這些標量類型能夠理解爲 JavaScript 中的原始類型,上面的標量類型一樣能夠對應 JavaScript 中的 Number
, Number
, String
, Boolean
, Symbol
。
在這裏還要注意一點,type Query
, Query
類型是 Schema 中全部 query
查詢的入口,相似的還有 Mutation
和 Subscription
,都做爲對應操做的入口點。
在type Query
下的 movie
字段中,咱們使用括號定義咱們能夠接受的參數名和參數的類型。在上面的 Schema 中,後面緊跟着的感嘆號聲明瞭此類型是個不可空類型(Non-Nullable),在參數中聲明表示該參數不能傳入爲空。若是感嘆號跟在 field 的後面,則表示返回該 type 的數據時,此字段必定不爲空。
經過上面的類型定義,能夠看到 GraphQL 中的類型系統起到了很重要的角色。在本例中,Schema 定義了 name
爲 String
類型,那麼你就不能傳 Int
類型進去,此時會拋出類型不符的錯誤。一樣的,若是傳出的 ratings
數據類型不爲 String
,也一樣會拋出類型不符的錯誤。
若是咱們的某個字段返回不止一個標量類型的數據,而是一組,則須要使用List
類型聲明,在該標量類型兩邊使用中括號[]
包圍便可,與 JavaScript 中數組的寫法相同,並且返回的數據也將會是數組類型。
須要注意的是[Movie]!
與 [Movie!]
兩種寫法的含義是不一樣的:前者表示 movies
字段始終返回不可爲空但Movie
元素能夠爲空。後者表示movies
中返回的 Movie
元素不能爲空,但 movies
字段的返回是能夠爲空的。
你可能在請求體中注意到,genre
參數的值沒有被雙引號括起來,也不是任何內置類型。看到 Schema 定義,COMEDY
是枚舉類型MovieTypes
中的枚舉成員。枚舉類型用於聲明一組取值常量列表,若是聲明瞭某個參數爲某個枚舉類型,那麼該參數只能傳入該枚舉類型內限定的常量名。
前面的例子中,傳入的參數均爲標量類型,那麼若是咱們想傳入一個擁有複雜結構的數據該怎麼定義呢。答案是使用關鍵字input
。其使用方法和type
徹底一致。
根據本例中的 Schema 定義,咱們在查詢 search
時data
的參數必須爲
{ term: "Deepwater Horizon" }
複製代碼
想象這麼一個頁面,我要列出兩個電影的信息作對比,爲了發揮 GraphQL 的優點,我要同時查詢這兩部電影的信息,在請求體中請求 movie
數據。前面咱們說到,請求體決定了返回數據的結構。在數據返回前查出兩個 key 爲 movie
的數據,合併以後因爲 key 重複而只能拿到一條數據。那麼在這種狀況下咱們須要使用別名功能。
別名即爲返回字段使用另外一個名字,使用方法也很簡單,只須要在請求體的字段前面使用別名:
的形式便可,返回的數據將會自動替換爲該名稱。
在上面的例子中,咱們須要對比兩部電影的數據。若是換做是硬件對比網站,須要查詢的硬件數量每每不止兩個。此時編寫冗餘的選擇集顯得很是的費勁、臃腫以及難維護。爲了解決這個問題,咱們可使用片斷功能。GraphQL 容許定義一段公用的選擇集,叫片斷。定義片斷使用 fragment name on Type
的語法,其中 name
爲自定義的片斷名稱,Type
爲片斷來自的類型。
本例中的請求體的選擇集公共部分提取成片斷以後爲
fragment movieInfo on Movie {
name
desc
}
複製代碼
在正式使用片斷以前,還須要向各位介紹片斷解構功能。相似於 JavaScript 的結構。GraphQL 的片斷結構符號將片斷內的字段「結構」到選擇集中。
與其餘大多數語言同樣,GraphQL 也提供了定義接口的功能。接口指的是 GraphQL 實體類型自己提供字段的集合,定義一組與外部溝通的方式。使用了 implements
的類型必須包含接口中定義的字段。
interface Basic {
name: String!
year: Number!
}
type Song implements Basic {
name: String!
year: Number!
artist: [String]!
}
type Video implements Basic {
name: String!
year: Number!
performers: [String]!
}
Query {
search(term: String!): [Basic]!
}
複製代碼
在本例中,定義了一個Basic
接口,Song
以及Video
類型都要實現該接口的字段。而後在search
查詢中返回該接口。
searchMedia
查詢返回一組Basic
接口。因爲該接口中的字段是全部實現了該接口的類型所共有的,在請求體上能夠直接使用。而對於特定類型上的其餘非共有字段,例如Video
中的performers
,直接選取是會有問題的,由於searchMedia
在返回的數據中類型多是全部實現了該接口的類型,而在 Song
類型中就沒有performers
字段。此時咱們能夠藉助內聯片斷的幫助(下面介紹)。
聯合類型與接口概念差很少相同,不一樣之處在於聯合類型下的類型之間沒有定義公共的字段。在 Union 類型中必須使用內聯片斷的方式查詢,緣由與上面的接口類型一致。
union SearchResult = Song | Video
Query {
search(term: String!): [SearchResult]!
}
複製代碼
對接口或聯合類型進行查詢時,因爲返回類型的不一樣致使選取的字段可能不一樣,此時須要經過內聯片斷的方式決定在特定類型下使用特定的選擇集。內聯選擇集的概念和用法與普通片斷基本相同,不一樣的是內聯片斷直接聲明在選擇集內,而且不須要fragment
聲明。
查詢接口的例子:
query {
searchMedia(term: "AJR") {
name
year
...on Song {
artist
}
...on Video {
performers
}
}
}
複製代碼
首選咱們須要該接口上的兩個公共字段,而且結果爲Song
類型時選取artist
字段,結果爲Video
類型時選取performers
字段。下面查詢聯合類型的例子也是同樣的道理。
查詢聯合類型的例子:
query {
searchStats(player: "Aaron") {
...on NFLScore {
YDS
TD
}
...on MLBScore {
ERA
IP
}
}
}
複製代碼
GraphQL 中內置了兩款邏輯指令,指令跟在字段名後使用。
當條件成立時,查詢此字段
query {
search {
actors @include(if: $queryActor) {
name
}
}
}
複製代碼
當條件成立時,不查詢此字段
query {
search {
comments @skip(if: $noComments) {
from
}
}
}
複製代碼
前面咱們已經瞭解了請求體以及 Schema,那麼咱們的數據到底怎麼來呢?答案是來自 Resolver 函數。
Resolver 的概念很是簡單。Resolver 對應着 Schema 上的字段,當請求體查詢某個字段時,對應的 Resolver 函數會被執行,由 Resolver 函數負責到數據庫中取得數據並返回,最終將請求體中指定的字段返回。
type Movie {
name
genre
}
type Query {
movie: Movie!
}
複製代碼
當請求體查詢movie
時,同名的 Resolver 必須返回Movie
類型的數據。固然你還能夠單獨爲name
字段使用獨立的 Resolver 進行解析。後面的代碼例子中將會清楚地瞭解 Resolver。
ThinkJS 是一款面向將來開發的 Node.js 框架,整合了大量的項目最佳實踐,讓企業級開發變得如此簡單、高效。 框架底層基於 Koa 2.x 實現,兼容 Koa 的全部功能。
本例中咱們將使用 ThinkJS 配合 MongoDB 進行搭建 GraphQL API,ThinksJS 的簡單易用性會讓你愛不釋手!
首先安裝 ThinkJS 腳手架 npm install -g think-cli
使用 CLI 快速建立項目 thinkjs new gqldemo
切換到工程目錄中 npm install && npm start
不到兩分鐘,ThinkJS 服務端就搭建完了,so easy!
因爲本人比較喜歡 mongoose,恰好 ThinkJS 官方提供了 think-mongoose 庫快速使用,安裝好以後咱們須要在 src/config/extend.js
中引入並加載該插件。
const mongoose = require('think-mongoose');
module.exports = [mongoose(think.app)];
複製代碼
接下來,在 adapter.js
中配置數據庫鏈接
export.model = {
type: 'mongoose',
mongoose: {
connectionString: 'mongodb://你的數據庫/gql',
options: {}
}
};
複製代碼
如今,咱們在整個 ThinkJS 應用中都擁有了 mongoose 實例,看看還差啥?數據模型!
藉助 ThinkJS 強大的數據 模型功能,咱們只須要以數據集合的名稱做爲文件名創建文件並定義模型便可使用,相比 mongoose 原生的操做更爲簡單。
本例中咱們實現 actor 和 movie 兩組數據,在 model
目錄下分別創建 actor.js
和 movie.js
,並在裏面定義模型。
actor.js
module.exports = class extends think.Mongoose {
get schema() {
return {
name: String,
desc: String,
dob: String,
photo: String,
addr: String,
movies: [
{
type: think.Mongoose.Schema.Types.ObjectId,
ref: 'movie'
}
]
};
}
};
複製代碼
movie.js
module.exports = class extends think.Mongoose {
get schema() {
return {
name: String,
desc: String,
ratings: String,
score: Number,
release: String,
cover: String,
actors: [
{
type: think.Mongoose.Schema.Types.ObjectId,
ref: 'actor'
}
]
};
}
};
複製代碼
要處理 GraphQL 請求,咱們就必須攔截特定請求進行解析處理,在 ThinkJS 中,咱們徹底能夠藉助中間件的能力完成解析和數據返回。中間件的配置在 middleware.js
中進行。
ThinkJS 中配置中間件有三個關鍵參數:
/graphql
中進行處理,那麼咱們對這個路徑進行 match 後進行處理;咱們的中間件配置大概長這樣:
{
match: '/graphql',
handle: () => {},
options: {}
}
複製代碼
Apollo Server 是一款構建在 Node.js 基礎上的 GraphQL 服務中間件,其強大的兼容性以及卓越的穩定性是本文選取此中間件的首要因素。
儘管 Apollo Server 沒有 ThinkJS 版的中間件,可是萬變不離其宗,咱們能夠經過 Apollo Server Core 中的核心方法 runHttpQuery
進行解析。
將它安裝到咱們的項目中: npm install apollo-server-core graphql --save
runHttpQuery
主要接受兩個參數,第一個是 GraphQLServerOptions
,這個咱們能夠不須要配置,留空數組便可;第二個是HttpQueryRequest
對象,咱們至少須要包含 methods
,options
以及query
,
他們分別表示當前請求的方法,GraphQL服務配置以及請求體。
而GraphQL服務配置中咱們至少要給出 schema
, schema
應該是一個 GraphQLSchema
實例,對於咱們前面例子中直接寫的 Schema Language,是不能被識別的,此時咱們須要藉助 graphql-tools
中的 makeExecutableSchema
工具將咱們的 Schema 和 Resolvers 進行關聯成 GraphQLSchema
實例。
將它安裝到咱們的項目中:npm install graphql-tools --save
在轉換成 GraphQLSchema 以前,首先要將咱們的 Schema 和 Resolver 準備好。
運用前面所學的知識,咱們能夠很快的編寫出一個簡單的 Schema 提供查詢演員信息和電影信息的接口。
type Movie {
name: String!
desc: String!
ratings: String!
score: Float!
cover: String!
actors: [Actor]
}
type Actor {
name: String!
desc: String!
dob: String!
photo: String!
movies: [Movie]
}
type Query {
movie(name: String!): [Movie]
actor(name: String!): [Actor]
}
複製代碼
接下來,分別編寫解析 Query
下 movie
和actor
字段的 Resolver 函數。
const MovieModel = think.mongoose('movie');
const ActorModel = think.mongoose('actor');
module.exports = {
Query: {
movie(prev, args, context) {
return MovieModel.find({ name: args.name })
.sort({ _id: -1 })
.exec();
},
actor(prev, args, context) {
return ActorModel.find({ name: args.name })
.sort({ _id: -1})
.exec();
}
}
}
複製代碼
爲了可以和 Schema 正確關聯,Resolver 函數的結構須要與 Schema 的結構保持一致。
到達這一步,有沒有發現什麼不對呢?
回憶前面的數據模型定義,裏面的 movies
和 actors
字段是一組另外一個集合中數據的引用,目的是方便創建電影和演員信息之間的關係以及維護,在 Resolver 運行以後,movies
和 actors
字段獲得的是一組 id,不符合 Schema 的定義,此時 GraphQL 會拋出錯誤。
那麼這個問題怎麼解決呢?前面講到 Resolver 的時候說到,每一個字段均可以對應一個 Resolver 函數,咱們分別對 movies
和 actors
字段設置 Resolver 函數,將上一個 Resolver 解析出來的 id 查詢一遍得出結果,最終返回的數據就能符合 Schema 的定義了。
const MovieModel = think.mongoose('movie');
const ActorModel = think.mongoose('actor');
module.exports = {
Query: {
movie(prev, args, context) {
return MovieModel.find({ name: args.name })
.sort({ _id: -1 })
.exec();
},
actor(prev, args, context) {
return ActorModel.find({ name: args.name })
.sort({ _id: -1})
.exec();
}
},
Actor: {
movies(prev, args, context) {
return Promise.all(
prev.movies.map(_id => MovieModel.findOne({ _id }).exec())
);
}
},
Movie: {
actors(prev, args, context) {
return Promise.all(
prev.actors.map(_id => ActorModel.findOne({ _id }).exec())
);
}
}
}
複製代碼
其中用到的 prev
參數就是上一個 Resolver 解析出的數據。
有了 Schema 和 Resolver 以後,咱們終於能夠把它們變成一個 GraphQLSchema
實例了。
調用 graphql-tools
中的 makeEcecutableSchema
進行組合好,放在 options
裏面稍後使用。
此時咱們的中間長這樣:
const { makeExecutableSchema } = require('graphql-tools');
const Resolvers = require('./resolvers'); // 咱們剛寫的 Resolver
const Schema = require('./schema'); // 咱們剛寫的 Schema
module.exports = {
match: '/graphql',
handle: () => {},
options: {
schema: makeExecutableSchema({
typeDefs: Schema,
resolvers: Resolvers
})
}
}
複製代碼
有請apollo-server-core
裏面的runHttpQuery
出場!
const { runHttpQuery } = require('apollo-server-core');
複製代碼
參照 apollo-server-koa
,快速構建出 ThinkJS 版的 apollo-server 中間件。
const { runHttpQuery } = require('apollo-server-core');
module.exports = (options = {}) => {
return ctx => {
return runHttpQuery([ctx], {
method: ctx.request.method,
options,
query:
ctx.request.method === 'POST'
? ctx.post()
: ctx.param()
}).then(
rsp => {
ctx.set('Content-Type', 'application/json');
ctx.body = rsp;
},
err => {
if (err.name !== 'HttpQueryError') throw err;
err.headers &&
Object.keys(err.headers).forEach(header => {
ctx.set(header, err.headers[header]);
});
ctx.status = err.statusCode;
ctx.body = err.message;
}
);
};
};
複製代碼
接下來引用到咱們中間件的handle
配置中,完美,大功告成,用 ThinkJS 搭建的 GraphQL 服務器就此告一段落,npm start
運行起來以後,用 GraphiQL 「播放」一下你的請求體(記得本身先往數據庫灌數據)。