<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          GraphQL 基礎(chǔ)實(shí)踐

          共 14087字,需瀏覽 29分鐘

           ·

          2020-11-09 03:09

          編者按:本文作者奇舞團(tuán)前端開(kāi)發(fā)工程師何文力,同時(shí)也是 W3C CSS 工作組成員。

          本次內(nèi)容是基于之前分享的文字版,若想看重點(diǎn)的話,可以看之前的 PPT (https://ppt.baomitu.com/d/4248c64a)。

          什么是 GraphQL

          GraphQL 是一款由 Facebook 主導(dǎo)開(kāi)發(fā)的數(shù)據(jù)查詢和操作語(yǔ)言, 寫(xiě)過(guò) SQL 查詢的同學(xué)可以把它想象成是 SQL 查詢語(yǔ)言,但 GraphQL 是給客戶端查詢數(shù)據(jù)用的。雖然這讓你聽(tīng)起來(lái)覺(jué)得像是一款數(shù)據(jù)庫(kù)軟件,但實(shí)際上 GraphQL 并不是數(shù)據(jù)庫(kù)軟件。你可以將 GraphQL 理解成一個(gè)中間件,是連接客戶端和數(shù)據(jù)庫(kù)之間的一座橋梁,客戶端給它一個(gè)描述,然后從數(shù)據(jù)庫(kù)中組合出符合這段描述的數(shù)據(jù)返回。這也意味著 GraphQL 并不關(guān)心數(shù)據(jù)存在什么數(shù)據(jù)庫(kù)上。

          同時(shí) GraphQL 也是一套標(biāo)準(zhǔn),在這個(gè)標(biāo)準(zhǔn)下不同平臺(tái)不同語(yǔ)言有相應(yīng)的實(shí)現(xiàn)。GraphQL 中還設(shè)計(jì)了一套類型系統(tǒng),在這個(gè)類型系統(tǒng)的約束下,可以獲得與 TypeScript 相近的相對(duì)安全的開(kāi)發(fā)體驗(yàn)。

          GraphQL 解決了什么問(wèn)題

          我們先來(lái)回顧一下我們已經(jīng)非常熟悉的 RESTful API 設(shè)計(jì)。簡(jiǎn)單的說(shuō) RESTful API 主要是使用 URL 的方式表達(dá)和定位資源,用 HTTP 動(dòng)詞來(lái)描述對(duì)這個(gè)資源的操作。

          我們以 IMDB 電影信息詳情頁(yè)為例子,看看我們得需要什么樣的 API 才能滿足 RESTful API 設(shè)計(jì)的要求。先來(lái)看看主頁(yè)面上都需要什么信息。

          可以看到頁(yè)面上由電影基本信息,演員和評(píng)分/評(píng)論信息組成,按照設(shè)計(jì)要求,我們需要將這三種資源放在不同 API 之下。首先是電影基本信息,我們有 API /movie/:id,給定一個(gè)電影ID返回基本信息數(shù)據(jù)。

          假裝 GET 一下獲得一個(gè) JSON 格式的數(shù)據(jù):

          {

          ??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”]

          }

          這里面包含了我們所需的電影名、分級(jí)等信息,以及一種叫做 HyperMedia 的數(shù)據(jù),通常是一個(gè) URL,指明了能夠獲取這個(gè)資源的 API 端點(diǎn)地址。如果我們跟著 HyperMedia 指向的連接請(qǐng)求下去,我們就能得到我們頁(yè)面上所需的所有信息。

          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

          [

          ??{

          ?????content:?“Its’s?as?good?as…”,

          ?????score:?9

          ??}

          ]

          最后根據(jù)需要,我們要將所有包含需要信息的 API 端點(diǎn)都請(qǐng)求一遍,對(duì)于移動(dòng)端來(lái)說(shuō),發(fā)起一個(gè) HTTP 請(qǐng)求還是比較消耗資源的,特別是在一些網(wǎng)絡(luò)連接質(zhì)量不佳的情況下,一下發(fā)出多個(gè)請(qǐng)求反而會(huì)導(dǎo)致不好的體驗(yàn)。

          而且在這樣的 API 設(shè)計(jì)之中,特定資源分布在特定的 API 端點(diǎn)之中,對(duì)于后端來(lái)說(shuō)寫(xiě)起來(lái)是挺方便的,但對(duì)于Web端或者客戶端來(lái)說(shuō)并不一定。例如在 Android 或 iOS 客戶端上,發(fā)版升級(jí)了一個(gè)很爆炸的功能,同一個(gè)API上可能為了支持這個(gè)功能而多吐一些數(shù)據(jù)。但是對(duì)于未升級(jí)的客戶端來(lái)說(shuō),這些新數(shù)據(jù)是沒(méi)有意義的,也造成了一定的資源浪費(fèi)。如果單單將所有資源整合到一個(gè) API 之中,還有可能會(huì)因?yàn)檎狭藷o(wú)關(guān)的數(shù)據(jù)而導(dǎo)致數(shù)據(jù)量的增加。

          而 GraphQL 就是為了解決這些問(wèn)題而來(lái)的,向服務(wù)端發(fā)送一次描述信息,告知客戶端所需的所有數(shù)據(jù),數(shù)據(jù)的控制甚至可以精細(xì)到字段,達(dá)到一次請(qǐng)求獲取所有所需數(shù)據(jù)的目的。

          GraphQL Hello World

          GraphQL 請(qǐng)求體

          我們先來(lái)看一下一個(gè) GraphQL 請(qǐng)求長(zhǎng)什么樣:

          query myQry?($name:?String!)?{

          ??movie(name:?“Manchester”)?{

          ????name

          ????desc

          ????ratings

          ??}

          }

          這個(gè)請(qǐng)求結(jié)構(gòu)是不是和 JSON 有那么點(diǎn)相似?這是 Facebook 故意設(shè)計(jì)成這樣的,希望你讀完之后就能體會(huì)到 Facebook 的用心良苦了。

          那么,上面的這個(gè)請(qǐng)求描述稱為一個(gè) GraphQL 請(qǐng)求體,請(qǐng)求體即用來(lái)描述你要從服務(wù)器上取什么數(shù)據(jù)用的。一般請(qǐng)求體由幾個(gè)部分組成,從里到外了解一下。

          首先是字段,字段請(qǐng)求的是一個(gè)數(shù)據(jù)單元。同時(shí)在 GraphQL 中,標(biāo)量字段是粒度最細(xì)的一個(gè)數(shù)據(jù)單元了,同時(shí)作為返回 JSON 響應(yīng)數(shù)據(jù)中的最后一個(gè)字段。也就是說(shuō),如果是一個(gè) Object,還必須選擇至少其中的一個(gè)字段。

          把我們所需要的字段合在一起,我們把它稱之為某某的選擇集。上面的 namedescratings 合在一起則稱之為 movie 的選擇集,同理,moviemyQry 的選擇集。需要注意的是,在標(biāo)量上使用不能使用選擇集這種操作,因?yàn)樗呀?jīng)是最后一層了。

          movie 的旁邊,name:?"Manchester",這個(gè)代表著傳入 movie 的參數(shù),參數(shù)名為 name 值為Manchester,利用這些參數(shù)向服務(wù)器表達(dá)你所需的數(shù)據(jù)需要符合什么條件。

          最后我們來(lái)到請(qǐng)求體的最外層:

          • 操作類型:指定本請(qǐng)求體要對(duì)數(shù)據(jù)做什么操作,類似與 REST 中的 GET POST。GraphQL 中基本操作類型有 query 表示查詢,mutation 表示對(duì)數(shù)據(jù)進(jìn)行操作,例如增刪改操作,subscription 訂閱操作。

          • 操作名稱:操作名稱是個(gè)可選的參數(shù),操作名稱對(duì)整個(gè)請(qǐng)求并不產(chǎn)生影響,只是賦予請(qǐng)求體一個(gè)名字,可以作為調(diào)試的依據(jù)。

          • 變量定義:在 GraphQL 中,聲明一個(gè)變量使用$符號(hào)開(kāi)頭,冒號(hào)后面緊跟著變量的傳入類型。如果要使用變量,直接引用即可,例如上面的 movie 就可以改寫(xiě)成 movie(name:?$name)

          如果上述三者都沒(méi)有提供,那么這個(gè)請(qǐng)求體默認(rèn)會(huì)被視為一個(gè) query 操作。

          請(qǐng)求的結(jié)果

          如果我們執(zhí)行上面的請(qǐng)求體,我們將會(huì)得到如下的數(shù)據(jù):

          {

          ??"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"

          ????}

          ??}

          }

          仔細(xì)對(duì)比結(jié)果和請(qǐng)求體的結(jié)構(gòu),你會(huì)發(fā)現(xiàn),與請(qǐng)求體的結(jié)構(gòu)是完全一致的。也就是說(shuō),請(qǐng)求體的結(jié)構(gòu)也確定了最終返回?cái)?shù)據(jù)的結(jié)構(gòu)

          GraphQL Server

          在前面的 REST 舉例中,我們請(qǐng)求多個(gè)資源有多個(gè) API 端點(diǎn)。在 GraphQL 中,只有一個(gè) API 端點(diǎn),同樣也接受 GET 和 POST 動(dòng)詞,如要操作 mutation 則使用 POST 請(qǐng)求。

          前面還提到 GraphQL 是一套標(biāo)準(zhǔn),怎么用呢,我們可以借助一些庫(kù)去解析。例如 Facebook 官方的 GraphQL.js。以及 Meteor 團(tuán)隊(duì)開(kāi)發(fā)的 Apollo,同時(shí)開(kāi)發(fā)了客戶端和服務(wù)端,同時(shí)也支持流行的 Vue 和 React 框架。調(diào)試方面,可以使用 Graphiql 進(jìn)行調(diào)試,得益于 GraphQL 的類型系統(tǒng)和 Schema,我們還可以在 Graphiql 調(diào)試中使用自動(dòng)完成功能。

          Schema

          前面我們提到,GraphQL 擁有一個(gè)類型系統(tǒng),那么每個(gè)字段的類型是怎么約定的呢?答案就在本小節(jié)中。在 GraphQL 中,類型的定義以及查詢本身都是通過(guò) Schema 去定義的。GraphQL 的 Schema 語(yǔ)言全稱叫 Schema Definition Language。Schema 本身并不代表你數(shù)據(jù)庫(kù)中真實(shí)的數(shù)據(jù)結(jié)構(gòu),它的定義決定了這整個(gè)端點(diǎn)能干些什么事情,確定了我們能向端點(diǎn)要什么,操作什么。再次回顧一下前面的請(qǐng)求體,請(qǐng)求體決定了返回?cái)?shù)據(jù)的結(jié)構(gòu),而 Schema 的定義決定了端點(diǎn)的能力。

          接下來(lái)我們就通過(guò)一個(gè)一個(gè)的例子了解一下 Schema。

          類型系統(tǒng)、標(biāo)量類型、非空類型、參數(shù)

          先看右邊的 Schema:type 是 GraphQL Schema 中最基本的一個(gè)概念,表示一個(gè) GraphQL 對(duì)象類型,可以簡(jiǎn)單地將其理解為 JavaScript 中的一個(gè)對(duì)象,在 JavaScript 中一個(gè)對(duì)象可以包含各種 key,在 GraphQL 中,type 里面同樣可以包含各種字段(field),而且字段類型不僅僅可以是標(biāo)量類型,還可以是 Schema 中定義的其他 type。例如上面的 Schema 中, Query 下的 movie 字段的類型就可以是 Movie

          在 GraphQL 中,有如下幾種標(biāo)量類型:Int, Float, String, Boolean, ID ,分別表示整型、浮點(diǎn)型、字符串、布爾型以及一個(gè)ID類型。ID類型代表著一個(gè)獨(dú)一無(wú)二的標(biāo)識(shí),ID 類型最終會(huì)被轉(zhuǎn)化成String類型,但它必須是獨(dú)一無(wú)二的,例如 mongodb 中的 _id 字段就可以設(shè)置為ID類型。同時(shí)這些標(biāo)量類型可以理解為 JavaScript 中的原始類型,上面的標(biāo)量類型同樣可以對(duì)應(yīng) JavaScript 中的 Number, Number, String, Boolean, Symbol

          在這里還要注意一點(diǎn),type QueryQuery 類型是 Schema 中所有 query 查詢的入口,類似的還有 MutationSubscription,都作為對(duì)應(yīng)操作的入口點(diǎn)。

          type Query下的 movie 字段中,我們使用括號(hào)定義我們可以接受的參數(shù)名和參數(shù)的類型。在上面的 Schema 中,后面緊跟著的感嘆號(hào)聲明了此類型是個(gè)不可空類型(Non-Nullable),在參數(shù)中聲明表示該參數(shù)不能傳入為空。如果感嘆號(hào)跟在 field 的后面,則表示返回該 type 的數(shù)據(jù)時(shí),此字段一定不為空。

          通過(guò)上面的類型定義,可以看到 GraphQL 中的類型系統(tǒng)起到了很重要的角色。在本例中,Schema 定義了 nameString類型,那么你就不能傳 Int類型進(jìn)去,此時(shí)會(huì)拋出類型不符的錯(cuò)誤。同樣的,如果傳出的 ratings 數(shù)據(jù)類型不為 String,也同樣會(huì)拋出類型不符的錯(cuò)誤。

          列表(List)、枚舉類型(Enum)

          如果我們的某個(gè)字段返回不止一個(gè)標(biāo)量類型的數(shù)據(jù),而是一組,則需要使用List類型聲明,在該標(biāo)量類型兩邊使用中括號(hào)[]包圍即可,與 JavaScript 中數(shù)組的寫(xiě)法相同,而且返回的數(shù)據(jù)也將會(huì)是數(shù)組類型。

          需要注意的是[Movie]![Movie!]兩種寫(xiě)法的含義是不同的:前者表示 movies字段始終返回不可為空但Movie元素可以為空。后者表示movies中返回的 Movie 元素不能為空,但 movies字段的返回是可以為空的。

          你可能在請(qǐng)求體中注意到,genre 參數(shù)的值沒(méi)有被雙引號(hào)括起來(lái),也不是任何內(nèi)置類型。看到 Schema 定義,COMEDY是枚舉類型MovieTypes中的枚舉成員。枚舉類型用于聲明一組取值常量列表,如果聲明了某個(gè)參數(shù)為某個(gè)枚舉類型,那么該參數(shù)只能傳入該枚舉類型內(nèi)限定的常量名。

          傳入復(fù)雜結(jié)構(gòu)的參數(shù)(Input)

          前面的例子中,傳入的參數(shù)均為標(biāo)量類型,那么如果我們想傳入一個(gè)擁有復(fù)雜結(jié)構(gòu)的數(shù)據(jù)該怎么定義呢。答案是使用關(guān)鍵字input。其使用方法和type完全一致。

          根據(jù)本例中的 Schema 定義,我們?cè)诓樵?search時(shí)data的參數(shù)必須為

          {?term:?"Deepwater Horizon"?}

          別名(Alias)

          想象這么一個(gè)頁(yè)面,我要列出兩個(gè)電影的信息做對(duì)比,為了發(fā)揮 GraphQL 的優(yōu)勢(shì),我要同時(shí)查詢這兩部電影的信息,在請(qǐng)求體中請(qǐng)求 movie 數(shù)據(jù)。前面我們說(shuō)到,請(qǐng)求體決定了返回?cái)?shù)據(jù)的結(jié)構(gòu)。在數(shù)據(jù)返回前查出兩個(gè) key 為 movie 的數(shù)據(jù),合并之后由于 key 重復(fù)而只能拿到一條數(shù)據(jù)。那么在這種情況下我們需要使用別名功能。

          別名即為返回字段使用另一個(gè)名字,使用方法也很簡(jiǎn)單,只需要在請(qǐng)求體的字段前面使用別名:的形式即可,返回的數(shù)據(jù)將會(huì)自動(dòng)替換為該名稱。

          片段(Fragment)、片段解構(gòu)(Fragment Spread)

          在上面的例子中,我們需要對(duì)比兩部電影的數(shù)據(jù)。如果換作是硬件對(duì)比網(wǎng)站,需要查詢的硬件數(shù)量往往不止兩個(gè)。此時(shí)編寫(xiě)冗余的選擇集顯得非常的費(fèi)勁、臃腫以及難維護(hù)。為了解決這個(gè)問(wèn)題,我們可以使用片段功能。GraphQL 允許定義一段公用的選擇集,叫片段。定義片段使用 fragment name on Type 的語(yǔ)法,其中 name為自定義的片段名稱,Type為片段來(lái)自的類型。

          本例中的請(qǐng)求體的選擇集公共部分提取成片段之后為

          fragment movieInfo on Movie?{

          ???name

          ???desc

          }

          在正式使用片段之前,還需要向各位介紹片段解構(gòu)功能。類似于 JavaScript 的結(jié)構(gòu)。GraphQL 的片段結(jié)構(gòu)符號(hào)將片段內(nèi)的字段“結(jié)構(gòu)”到選擇集中。

          接口(Interface)

          與其他大多數(shù)語(yǔ)言一樣,GraphQL 也提供了定義接口的功能。接口指的是 GraphQL 實(shí)體類型本身提供字段的集合,定義一組與外部溝通的方式。使用了 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]!

          }

          在本例中,定義了一個(gè)Basic接口,Song以及Video類型都要實(shí)現(xiàn)該接口的字段。然后在search查詢中返回該接口。

          searchMedia查詢返回一組Basic接口。由于該接口中的字段是所有實(shí)現(xiàn)了該接口的類型所共有的,在請(qǐng)求體上可以直接使用。而對(duì)于特定類型上的其他非共有字段,例如Video中的performers,直接選取是會(huì)有問(wèn)題的,因?yàn)?span style="margin-right: 2px;margin-left: 2px;border-width: 1px;border-style: solid;border-color: rgb(225, 225, 232);padding: 2px 4px;font-size: 12px;font-family: monospace;color: rgb(221, 17, 68);background-color: rgb(247, 247, 249);border-radius: 2px;word-break: break-all;overflow-wrap: break-word;">searchMedia在返回的數(shù)據(jù)中類型可能是所有實(shí)現(xiàn)了該接口的類型,而在 Song類型中就沒(méi)有performers字段。此時(shí)我們可以借助內(nèi)聯(lián)片段的幫助(下面介紹)。

          聯(lián)合類型(Union)

          聯(lián)合類型與接口概念差不多相同,不同之處在于聯(lián)合類型下的類型之間沒(méi)有定義公共的字段。在 Union 類型中必須使用內(nèi)聯(lián)片段的方式查詢,原因與上面的接口類型一致。

          union SearchResult?=?Song?|?Video

          Query?{

          ????search(term:?String!):?[SearchResult]!

          }

          內(nèi)聯(lián)片段(Inline Fragment)

          對(duì)接口或聯(lián)合類型進(jìn)行查詢時(shí),由于返回類型的不同導(dǎo)致選取的字段可能不同,此時(shí)需要通過(guò)內(nèi)聯(lián)片段的方式?jīng)Q定在特定類型下使用特定的選擇集。內(nèi)聯(lián)選擇集的概念和用法與普通片段基本相同,不同的是內(nèi)聯(lián)片段直接聲明在選擇集內(nèi),并且不需要fragment聲明。

          查詢接口的例子:

          query?{

          ????searchMedia(term:?"AJR")?{

          ????????name

          ????????year


          ????????...on Song?{

          ????????????artist

          ????????}


          ????????...on Video?{

          ????????????performers

          ????????}

          ????}

          }

          首選我們需要該接口上的兩個(gè)公共字段,并且結(jié)果為Song類型時(shí)選取artist字段,結(jié)果為Video類型時(shí)選取performers字段。下面查詢聯(lián)合類型的例子也是一樣的道理。

          查詢聯(lián)合類型的例子:

          query?{

          ????searchStats(player:?"Aaron")?{

          ????????...on NFLScore?{

          ????????????YDS

          ????????????TD

          ????????}


          ????????...on MLBScore?{

          ????????????ERA

          ????????????IP

          ????????}

          ????}

          }

          GraphQL 內(nèi)置指令

          GraphQL 中內(nèi)置了兩款邏輯指令,指令跟在字段名后使用。

          @include

          當(dāng)條件成立時(shí),查詢此字段

          query?{

          ????search?{

          ????????actors @include(if:?$queryActor)?{

          ????????????name

          ????????}

          ????}

          }

          @skip

          當(dāng)條件成立時(shí),不查詢此字段

          query?{

          ????search?{

          ????????comments @skip(if:?$noComments)?{

          ????????????from

          ????????}

          ????}

          }

          Resolvers

          前面我們已經(jīng)了解了請(qǐng)求體以及 Schema,那么我們的數(shù)據(jù)到底怎么來(lái)呢?答案是來(lái)自 Resolver 函數(shù)。

          Resolver 的概念非常簡(jiǎn)單。Resolver 對(duì)應(yīng)著 Schema 上的字段,當(dāng)請(qǐng)求體查詢某個(gè)字段時(shí),對(duì)應(yīng)的 Resolver 函數(shù)會(huì)被執(zhí)行,由 Resolver 函數(shù)負(fù)責(zé)到數(shù)據(jù)庫(kù)中取得數(shù)據(jù)并返回,最終將請(qǐng)求體中指定的字段返回。

          type Movie?{

          ????name

          ????genre

          }


          type Query?{

          ????movie:?Movie!

          }

          當(dāng)請(qǐng)求體查詢movie時(shí),同名的 Resolver 必須返回Movie類型的數(shù)據(jù)。當(dāng)然你還可以單獨(dú)為name字段使用獨(dú)立的 Resolver 進(jìn)行解析。后面的代碼例子中將會(huì)清楚地了解 Resolver。

          使用 ThinkJS 搭建 GraphQL API

          ThinkJS 是一款面向未來(lái)開(kāi)發(fā)的 Node.js 框架,整合了大量的項(xiàng)目最佳實(shí)踐,讓企業(yè)級(jí)開(kāi)發(fā)變得如此簡(jiǎn)單、高效。框架底層基于 Koa 2.x 實(shí)現(xiàn),兼容 Koa 的所有功能。

          本例中我們將使用 ThinkJS 配合 MongoDB 進(jìn)行搭建 GraphQL API,ThinksJS 的簡(jiǎn)單易用性會(huì)讓你愛(ài)不釋手!

          快速安裝

          首先安裝 ThinkJS 腳手架 npm install -g think-cli

          使用 CLI 快速創(chuàng)建項(xiàng)目 thinkjs new gqldemo

          切換到工程目錄中 npm install && npm start

          不到兩分鐘,ThinkJS?服務(wù)端就搭建完了,so?easy!

          配置 MongoDB 數(shù)據(jù)庫(kù)

          由于本人比較喜歡 mongoose,剛好 ThinkJS 官方提供了 think-mongoose 庫(kù)快速使用,安裝好之后我們需要在 src/config/extend.js中引入并加載該插件。

          const?mongoose?=?require('think-mongoose');

          module.exports?=?[mongoose(think.app)];

          接下來(lái),在 adapter.js 中配置數(shù)據(jù)庫(kù)連接

          export.model?=?{

          ????type:?'mongoose',

          ????mongoose:?{

          ????????connectionString:?'mongodb://你的數(shù)據(jù)庫(kù)/gql',

          ????????options:?{}

          ????}

          };

          現(xiàn)在,我們?cè)谡麄€(gè) ThinkJS 應(yīng)用中都擁有了 mongoose 實(shí)例,看看還差啥?數(shù)據(jù)模型!

          借助 ThinkJS 強(qiáng)大的數(shù)據(jù) 模型功能,我們只需要以數(shù)據(jù)集合的名稱作為文件名建立文件并定義模型即可使用,相比 mongoose 原生的操作更為簡(jiǎn)單。

          本例中我們實(shí)現(xiàn) actor 和 movie 兩組數(shù)據(jù),在 model 目錄下分別建立 actor.jsmovie.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 請(qǐng)求的中間件

          要處理 GraphQL 請(qǐng)求,我們就必須攔截特定請(qǐng)求進(jìn)行解析處理,在 ThinkJS 中,我們完全可以借助中間件的能力完成解析和數(shù)據(jù)返回。中間件的配置在 middleware.js中進(jìn)行。

          ThinkJS 中配置中間件有三個(gè)關(guān)鍵參數(shù):

          • match:?用于匹配?URL,我們想讓我們的請(qǐng)求發(fā)送到?/graphql 中進(jìn)行處理,那么我們對(duì)這個(gè)路徑進(jìn)行 match 后進(jìn)行處理;

          • handle:中間件的處理函數(shù),當(dāng) match 到時(shí),此處理函數(shù)會(huì)被調(diào)用執(zhí)行,我們的解析任務(wù)也在這里進(jìn)行,并將解析結(jié)果返回;

          • options:options 時(shí)傳給中間件的參數(shù),我們可以在此將我們的 Schema 等內(nèi)容傳給解析器使用。

          我們的中間件配置大概長(zhǎng)這樣:

          {

          ????match:?'/graphql',

          ????handle:?()?=>?{},

          ????options:?{}

          }

          解析 GraphQL 的核心

          Apollo Server

          Apollo Server 是一款構(gòu)建在 Node.js 基礎(chǔ)上的 GraphQL 服務(wù)中間件,其強(qiáng)大的兼容性以及卓越的穩(wěn)定性是本文選取此中間件的首要因素。

          盡管 Apollo Server 沒(méi)有 ThinkJS 版的中間件,但是萬(wàn)變不離其宗,我們可以通過(guò) Apollo Server Core 中的核心方法 runHttpQuery 進(jìn)行解析。

          將它安裝到我們的項(xiàng)目中:npm install apollo-server-core graphql --save

          編寫(xiě)中間件

          runHttpQuery主要接受兩個(gè)參數(shù),第一個(gè)是 GraphQLServerOptions,這個(gè)我們可以不需要配置,留空數(shù)組即可;第二個(gè)是HttpQueryRequest對(duì)象,我們至少需要包含 methods,options以及query

          他們分別表示當(dāng)前請(qǐng)求的方法,GraphQL服務(wù)配置以及請(qǐng)求體。

          而GraphQL服務(wù)配置中我們至少要給出 schemaschema 應(yīng)該是一個(gè) GraphQLSchema實(shí)例,對(duì)于我們前面例子中直接寫(xiě)的 Schema Language,是不能被識(shí)別的,此時(shí)我們需要借助 graphql-tools 中的 makeExecutableSchema 工具將我們的 Schema 和 Resolvers 進(jìn)行關(guān)聯(lián)成 GraphQLSchema實(shí)例。

          將它安裝到我們的項(xiàng)目中:npm install graphql-tools --save

          編寫(xiě) Schema 和 Resolver

          在轉(zhuǎn)換成 GraphQLSchema 之前,首先要將我們的 Schema 和 Resolver 準(zhǔn)備好。

          運(yùn)用前面所學(xué)的知識(shí),我們可以很快的編寫(xiě)出一個(gè)簡(jiǎn)單的 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]

          }

          接下來(lái),分別編寫(xiě)解析 Querymovieactor字段的 Resolver 函數(shù)。

          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 正確關(guān)聯(lián),Resolver 函數(shù)的結(jié)構(gòu)需要與 Schema 的結(jié)構(gòu)保持一致。

          到達(dá)這一步,有沒(méi)有發(fā)現(xiàn)什么不對(duì)呢?

          回憶前面的數(shù)據(jù)模型定義,里面的 moviesactors 字段是一組另一個(gè)集合中數(shù)據(jù)的引用,目的是方便建立電影和演員信息之間的關(guān)系以及維護(hù),在 Resolver 運(yùn)行之后,moviesactors 字段得到的是一組 id,不符合 Schema 的定義,此時(shí) GraphQL 會(huì)拋出錯(cuò)誤。

          那么這個(gè)問(wèn)題怎么解決呢?前面講到 Resolver 的時(shí)候說(shuō)到,每個(gè)字段都可以對(duì)應(yīng)一個(gè) Resolver 函數(shù),我們分別對(duì) moviesactors 字段設(shè)置 Resolver 函數(shù),將上一個(gè) Resolver 解析出來(lái)的 id 查詢一遍得出結(jié)果,最終返回的數(shù)據(jù)就能符合 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 參數(shù)就是上一個(gè) Resolver 解析出的數(shù)據(jù)。

          組合成 GraphQLSchema 實(shí)例

          有了 Schema 和 Resolver 之后,我們終于可以把它們變成一個(gè) GraphQLSchema 實(shí)例了。

          調(diào)用 graphql-tools 中的 makeEcecutableSchema 進(jìn)行組合好,放在 options 里面稍后使用。

          此時(shí)我們的中間長(zhǎng)這樣:

          const?{?makeExecutableSchema?}?=?require('graphql-tools');

          const?Resolvers?=?require('./resolvers');?// 我們剛寫(xiě)的 Resolver

          const?Schema?=?require('./schema');?// 我們剛寫(xiě)的 Schema

          module.exports?=?{

          ????match:?'/graphql',

          ????handle:?()?=>?{},

          ????options:?{

          ????????schema:?makeExecutableSchema({

          ????????????typeDefs:?Schema,

          ????????????resolvers:?Resolvers

          ????????})

          ????}

          }

          編寫(xiě) handler

          有請(qǐng)apollo-server-core 里面的runHttpQuery出場(chǎng)!

          const?{?runHttpQuery?}?=?require('apollo-server-core');

          參照 apollo-server-koa,快速構(gòu)建出 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;

          ??????}

          ????);

          ??};

          };

          接下來(lái)引用到我們中間件的handle配置中,完美,大功告成,用 ThinkJS 搭建的 GraphQL 服務(wù)器就此告一段落,npm start 運(yùn)行起來(lái)之后,用 GraphiQL “播放”一下你的請(qǐng)求體(記得自己先往數(shù)據(jù)庫(kù)灌數(shù)據(jù))。

          GraphQL 的優(yōu)缺點(diǎn)

          優(yōu)點(diǎn)

          • 所見(jiàn)即所得:所寫(xiě)請(qǐng)求體即為最終數(shù)據(jù)結(jié)構(gòu)

          • 減少網(wǎng)絡(luò)請(qǐng)求:復(fù)雜數(shù)據(jù)的獲取也可以一次請(qǐng)求完成

          • Schema 即文檔:定義的 Schema 也規(guī)定了請(qǐng)求的規(guī)則

          • 類型檢查:嚴(yán)格的類型檢查能夠消除一定的認(rèn)為失誤

          缺點(diǎn)

          • 增加了服務(wù)端實(shí)現(xiàn)的復(fù)雜度:一些業(yè)務(wù)可能無(wú)法遷移使用 GraphQL,雖然可以使用中間件的方式將原業(yè)務(wù)的請(qǐng)求進(jìn)行代理,這無(wú)疑也將增加復(fù)雜度和資源的消耗

          完整源代碼可以在這里?(https://github.com/NimitzDEV/graphpql-in-thinkjs)找到,中間件可以在這里(https://github.com/NimitzDEV/think-graphql-middleware)找到

          瀏覽 62
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  奇米久久久久 | 老司机视频在线视频18 | 亚洲第一狼区 | 精品无码一区二区 | 香蕉伊|