GraphQL學(xué)習(xí)之原理篇

前言

在上一篇文章基礎(chǔ)篇中碑韵,我們介紹了GraphQL的語法以及類型系統(tǒng)叁怪,算是對GraphQL有個基本的認識族展。在這一篇中森缠,我們將會介紹GraphQL的實現(xiàn)原理。說到原理仪缸,我們就不得不依托于GraphQL的規(guī)范:GraphQL

概述

GraphQL規(guī)范主體部分有6大部分贵涵,除去我們在上一節(jié)講到的類型系統(tǒng)(Type System)和語言(Language)稿械,剩下的便是整個GraphQL的主流程观谦。也就是如下圖所示的:

image

根據(jù)規(guī)范的章節(jié)蛤克,也就是GraphQL的實現(xiàn)流程抗蠢,我們原理篇一一來看看規(guī)范到底定義了些什么,以及在實際的使用中援奢,是如何貼近到規(guī)范的實現(xiàn)的蓖谢。

Js語言的實現(xiàn)版本是: graphql-js

流程總覽

首先我們肯定會在客戶端上書寫查詢語句根吁,查詢語句在發(fā)送到服務(wù)端之前會轉(zhuǎn)換為標準的請求體片林。以之前的demo為例子坟奥,當我們發(fā)起如下的請求的時候:

image

客戶端發(fā)起的請求體應(yīng)該具備以下三個字段(POST請求):

{
  "query": "...",
  "operationName": "...",
  "variables": { "myVariable": "someValue", ... }
}

截圖如下:

image

參考Serving over HTTP

這些參數(shù)表達了客戶端的訴求:調(diào)用哪個方法,傳遞什么樣的參數(shù)拇厢,返回哪些字段。

服務(wù)端拿到這段Schema之后晒喷,通過事先定義好的服務(wù)端Schema接收請求參數(shù)孝偎,校驗參數(shù),然后執(zhí)行對應(yīng)的resolver函數(shù)凉敲,執(zhí)行完成返回數(shù)據(jù)衣盾。

express-graphql這個包我們可以看到服務(wù)端的整體處理流程寺旺,縮略如下:

...
function graphqlHTTP(options: Options): Middleware {
  ...
  // 返回express的中間件形式的函數(shù)
  return function graphqlMiddleware(request: $Request, response: $Response) {
    ...
    // 處理request的參數(shù),解析出來
    return getGraphQLParams(request)
    .then(
      graphQLParams => {}, // 解析成功
      error => {} // 解析失敗
    )
    .then(
      optionsData => {
        ...

        // GraphQL只支持GET/POST方法
        if (request.method !== 'GET' && request.method !== 'POST') {
          response.setHeader('Allow', 'GET, POST');
          throw httpError(405, 'GraphQL only supports GET and POST requests.');
        }

        ...

        // 校驗服務(wù)端這邊定義的Schema
        const schemaValidationErrors = validateSchema(schema);

        ...

        // 根據(jù)query生成GraphQL的Source
        const source = new Source(query, 'GraphQL request');

        // 根據(jù)Source生成AST
        try {
          documentAST = parseFn(source);
        } catch (syntaxError) {
          // Return 400: Bad Request if any syntax errors errors exist.
          response.statusCode = 400;
          return { errors: [syntaxError] };
        }

        // 校驗AST
        const validationErrors = validateFn(schema, documentAST, [
          ...specifiedRules,
          ...validationRules,
        ]);

        ...

        // 檢查GET請求方法是否在Query操作符上
        if (request.method === 'GET') {...}

        // 執(zhí)行resolver函數(shù)

      }
    )
    .then(result => {
      ... // 處理GraphQL返回的響應(yīng)體,做些額外的工作势决。
    })
  }
}

更多細節(jié)請查看源碼阻塑。

自省(Introspection)

GraphQL服務(wù)器支持根據(jù)自己的schema進行自省。這對于我們想要查詢一些關(guān)心的信息很有用果复。比如我們可以查詢demo的一些關(guān)心的類型:

image

根據(jù)規(guī)范陈莽,有兩類自省系統(tǒng):類型名稱自省(__typename)和schema自省(__schema和__type)。

__typename

GraphQL支持在一個查詢中任何一個節(jié)點添加對類型名稱的自省虽抄,在識別Interface/Union類型實際使用的類型的時候比較常用走搁,在上圖演示,我們可以看到每個節(jié)點都可以添加__typename迈窟,返回的類型也有很多:__Type私植、__Field__InputValue车酣、__Schema

帶有__的都是GraphQL規(guī)范內(nèi)部定義的類型曲稼,屬于保留名稱。開發(fā)者自定義的類型不允許出現(xiàn)__字符湖员,否則在語法校驗的時候會失敗贫悄。

舉個例子:

將demo中的type Message改為type __Message,然后會報此類錯誤:

Name \"__Message\" must not begin with \"__\", which is reserved by GraphQL introspection.

__schema&__type

__schema可以用來查詢系統(tǒng)當前支持的所有語法破衔,包括query語法清女、mutation語法,看它的結(jié)構(gòu)就知道了:

type __Schema {
  types: [__Type!]! => 查詢系統(tǒng)當前定義的所有類型晰筛,包括自定義的和內(nèi)部定義的所有類型
  queryType: __Type!  => 查詢 type Query {} 里面所有的查詢方法
  mutationType: __Type => 查詢 type Mutation {} 里面所有的mutation方法
  subscriptionType: __Type => 查詢 type Subscription {} 里面所有subscription方法
  directives: [__Directive!]! => 查詢系統(tǒng)支持的指令
}

__type則是用來查詢指定的類型屬性嫡丙。關(guān)于這些類型的內(nèi)部定義請參考:Schema Introspection

上圖基于的Message類型是這樣的:

"""消息列表"""
type Message {
  """文章ID"""
  id: ID!
  """文章內(nèi)容"""
  content: String
  """作者"""
  author: String
  """廢棄的字段"""
  oldField: String @deprecated(reason: "Use \`newField\`.")
}

Tips: 因為有了自省系統(tǒng),GraphiQL才有可能在你輸入查詢信息地時候進行文字提示读第,因為在頁面加載的時候GraphiQL會去請求這些內(nèi)容曙博,請求的內(nèi)容可以看這個文件:introspectionQueries.js

校驗

在上面的流程總覽中提到,客戶端發(fā)起的請求query字段帶有查詢的語法怜瞒,這段語法要先經(jīng)過校驗父泳,我們以下面最簡單的一次查詢?yōu)槔?/p>

{
  getMessage {
    content
    author
  }
}

解析出來的請求參數(shù)數(shù)據(jù)是:

{ query: '{\n  getMessage {\n    content\n    author\n  }\n}',
  variables: null,
  operationName: null,
  raw: false
}

之后先是校驗服務(wù)端定義的schema:validateSchema(schema),上一節(jié)的那個錯誤就是在這邊拋出的吴汪。

接著將客戶端的query轉(zhuǎn)為Source類型的結(jié)構(gòu):

{
  body: '{\n  getMessage {\n    content\n    author\n  }\n}',
  name: 'GraphQL request',
  locationOffset: { line: 1, column: 1 }
}

接著轉(zhuǎn)為AST:graphql.parse惠窄,graphql-js根據(jù)特征字符串:

export const TokenKind = Object.freeze({
  SOF: '<SOF>',
  EOF: '<EOF>',
  BANG: '!',
  DOLLAR: '$',
  AMP: '&',
  PAREN_L: '(',
  PAREN_R: ')',
  SPREAD: '...',
  COLON: ':',
  EQUALS: '=',
  AT: '@',
  BRACKET_L: '[',
  BRACKET_R: ']',
  BRACE_L: '{',
  PIPE: '|',
  BRACE_R: '}',
  NAME: 'Name',
  INT: 'Int',
  FLOAT: 'Float',
  STRING: 'String',
  BLOCK_STRING: 'BlockString',
  COMMENT: 'Comment',
});

對source逐一解析生成lexer,再執(zhí)行parseDocument生成解析階段的產(chǎn)出物document

{
  "kind": "Document",
  "definitions": [{
    "kind": "OperationDefinition",
    "operation": "query",
    "variableDefinitions": [],
    "directives": [],
    "selectionSet": {
      "kind": "SelectionSet",
      "selections": [{
        "kind": "Field",
        "name": {
          "kind": "Name",
          "value": "getMessage",
          "loc": {
            "start": 4,
            "end": 14
          }
        },
        "arguments": [],
        "directives": [],
        "selectionSet": {
          "kind": "SelectionSet",
          "selections": [{
              "kind": "Field",
              "name": {
                "kind": "Name",
                "value": "content",
                "loc": {
                  "start": 21,
                  "end": 28
                }
              },
              "arguments": [],
              "directives": [],
              "loc": {
                "start": 21,
                "end": 28
              }
            },
            {
              "kind": "Field",
              "name": {
                "kind": "Name",
                "value": "author",
                "loc": {
                  "start": 33,
                  "end": 39
                }
              },
              "arguments": [],
              "directives": [],
              "loc": {
                "start": 33,
                "end": 39
              }
            }
          ],
          "loc": {
            "start": 15,
            "end": 43
          }
        },
        "loc": {
          "start": 4,
          "end": 43
        }
      }],
      "loc": {
        "start": 0,
        "end": 45
      }
    },
    "loc": {
      "start": 0,
      "end": 45
    }
  }],
  "loc": {
    "start": 0,
    "end": 45
  }
}

其中AST支持的kind可以參考這里的定義: kinds.js

如果同時有多段語法漾橙,比如:

query gm($id: ID) {
  all: getMessage  {
    content
    author
  }
  single: getMessage(id: $id) {
    content
    author
  }
}
mutation cr($input: MessageInput) {
  createMessage(input: $input) {
    id
  }
}

那么生成的documentAST就是:

[ { kind: 'OperationDefinition',
    operation: 'query',
    name: { kind: 'Name', value: 'gm', loc: [Object] },
    variableDefinitions: [ [Object] ],
    directives: [],
    selectionSet: { kind: 'SelectionSet', selections: [Array], loc: [Object] },
    loc: { start: 0, end: 128 } },
  { kind: 'OperationDefinition',
    operation: 'mutation',
    name: { kind: 'Name', value: 'cr', loc: [Object] },
    variableDefinitions: [ [Object] ],
    directives: [],
    selectionSet: { kind: 'SelectionSet', selections: [Array], loc: [Object] },
    loc: { start: 129, end: 210 } } ]

這種情況下杆融,必須提供一個operationName來確定操作的是哪個document!該字段也就是我們在最開始說的請求的數(shù)據(jù)中的operationName,這些校驗都發(fā)聲在源碼的buildExecutionContext方法內(nèi)

接著執(zhí)行校驗的最后一個步驟:校驗客戶端語法并給出合理的解釋, graphql.validate(schema, documentAST, validationRules)霜运,比如我在將query語句變更為:

{
  getMessage1 {
    content
    author
  }
}

graphql-js就會校驗不通過脾歇,并且給出對應(yīng)的提示:

{
  "errors": [
    {
      "message": "Cannot query field \"getMessage1\" on type \"Query\". Did you mean \"getMessage\"?",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ]
    }
  ]
}

這種結(jié)構(gòu)化的報錯信息也是GraphQL的一大特點,定位問題非常方便藕各。只要語法沒問題校驗階段就能順利完成激况。

執(zhí)行階段

graphql.execute是實現(xiàn)GraphQL規(guī)范的Execute章節(jié)的內(nèi)容作彤。根據(jù)規(guī)范宦棺,我們將執(zhí)行階段分為:

image

每個階段解釋一下:

  1. Validating Requests:到這個階段的校驗其實已經(jīng)很少了,在源碼實現(xiàn)上只需要校驗入?yún)⑹欠穹弦?guī)范即可黔帕,對應(yīng)源碼的方法是:assertValidExecutionArguments
  2. Coercing Variable Values:檢查客戶端請求變量的合法性成黄,需要和schema進行比對奋岁,對應(yīng)源碼的方法是:getVariableValues
  3. Executing Operations:執(zhí)行客戶端請求的方法與之對應(yīng)的resolver函數(shù)闻伶。對應(yīng)源碼的方法是:executeOperation
  4. Executing Selection Sets:搜羅客戶端請求需要返回的字段集合蓝翰,對應(yīng)源碼的方法是:collectFields
  5. Executing Fields:執(zhí)行每個字段畜份,需要進行遞歸爆雹,對應(yīng)源碼的方法是:executeFields

接下去我們大概講解下每個過程的一些要點

Validating Requests

源碼中校驗了入?yún)⒌娜齻€:schema/document/variables

Coercing Variable Values

如果該操作定義了入?yún)⒏铺敲催@些變量的值需要強制與方法聲明的入?yún)㈩愋瓦M行比對册倒。比對不通過,直接報錯,比如我們將getMessage改為這樣:

query getMessage($id: ID){
  getMessage(id: $id) {
    content
    author
  }
}

然后變量是:

{
  "id": []
}

那么經(jīng)過這個函數(shù)將會報錯返回:"Variable \"$id\" got invalid value []; Expected type ID; ID cannot represent value: []"

Executing Operations => Executing Selection Sets => Executing Fields

在該流程上區(qū)分operation是query還是mutation,二者執(zhí)行的方式前者是并行后者是串行鞭执。

整體流程如下所示:

image

在圖中標注的輸出的第一次執(zhí)行的數(shù)據(jù)如下,僅供參考估脆,流程圖以demo中的getMessage為例子所畫疙赠,其中粉紅色是第一次波執(zhí)行的流程圃阳,也就是解析getMessage這個字段所走的流程,以completeValueCatchingError為界限是開始遍歷[Message]中的Message锣夹,這個流程以紫色線標注晕城,如此往復(fù)遞歸砖顷,遍歷完客戶端要求的所有數(shù)據(jù)為止

  1. collectFields
{ getMessage:
  [{
    kind: 'Field',
    alias: undefined,
    name: [Object],
    arguments: [],
    directives: [],
    selectionSet: [Object],
    loc: [Object]
  }]
}
  1. resolveFieldValueOrError
    其入?yún)⒌谝淮蝹鬟M去的source是:
{
  getMessage: [Function: getMessage],
  createMessage: [Function: createMessage],
  updateMessage: [Function: updateMessage]
}

第一次執(zhí)行返回的結(jié)果:

[
  { id: 0, content: 'test content', author: 'pp' },
  { id: 1, content: 'test content 1', author: 'pp1' }
]
  1. completeValueCatchingError
[
  { content: 'test content', author: 'pp' },
  { content: 'test content 1', author: 'pp1' }
]

整個流程以getMessage這個字段名稱為起點,執(zhí)行resolver函數(shù)物咳,得到結(jié)果芯肤,因為返回類型是[Message]崖咨,所以會對該返回類型先進行數(shù)組解析击蹲,再解析數(shù)組里面的字段歌豺,以此不斷重復(fù)遞歸,直到獲取完客戶端指定的所有字段轮听。圖形化的流程我在圖中用標號和顏色標注血巍,應(yīng)該很容易看懂整個流程的述寡。

執(zhí)行resolver函數(shù)的選擇

在這里回答demo中提到的問題,一種寫法是將schema和resolve分別傳入schema和rootValue兩個字段內(nèi)螟炫,另外一種寫法是使用graphql-tools將typedefs和resolvers轉(zhuǎn)換成帶有resolve字段的schema,二者寫法都是可行的昼钻,原因如下:

首先代碼會給系統(tǒng)默認的fieldResolver賦值一個defaultFieldResolver函數(shù)然评,如果fieldResolver沒有傳值的話,這里明顯沒有傳值亿眠。

之后在選擇resolver函數(shù)執(zhí)行的時候有這么一段代碼來實現(xiàn)了上述兩種寫法的可行性(resolveField.js):

const resolveFn = fieldDef.resolve || exeContext.fieldResolver;

這樣就優(yōu)先使用schema定義的resolve函數(shù)魂莫,沒有的話就使用了rootValue傳遞的resolver函數(shù)了。因此執(zhí)行的不一樣的話導(dǎo)致resolver函數(shù)獲取參數(shù)的方式略微不同:

第一種入?yún)⑹牵?args, contextValue, info)
第二種入?yún)⑹牵?source, args, contextValue, info) => 也就是此時你想要獲取參數(shù)的話得從第二個字段開始

Response

Response步驟就很簡單了潭兽,定義了4個規(guī)則:

1、響應(yīng)體必須是一個對象

2账蓉、如果執(zhí)行operation錯誤的時候铸本,那么errors必須存在箱玷,否則不應(yīng)該有這個字段

2.1. `error`字段是一個數(shù)組對象,對象里面必須包含一個`message`的字段來描述錯誤的原因以及一些提示

2.2. 另外可能包含的字段有`location`舶得、`path`沐批、`extensions`來提示開發(fā)者錯誤的具體信息。

3捻撑、如果執(zhí)行operation沒有錯誤顾患,那么data字段必須有值

4设预、其他自定義的信息可以定義在extensions這個字段內(nèi)鳖枕。

最后

至此宾符,整個GraphQL實現(xiàn)的流程到這里就結(jié)束了。更多細節(jié)請查看源碼和規(guī)范哄褒,我們將在下一篇文章中聊聊GraphQL的實際項目應(yīng)用GraphQL學(xué)習(xí)之實踐篇

參考

1、 GraphQL

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末罚舱,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子包个,更是在濱河造成了極大的恐慌,老刑警劉巖糯而,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件像寒,死亡現(xiàn)場離奇詭異诺祸,居然都是意外死亡,警方通過查閱死者的電腦和手機胃夏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事砂沛“郑” “怎么了静浴?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵浴麻,是天一觀的道長软免。 經(jīng)常有香客問我漓骚,道長认境,這世上最難降的妖魔是什么亩冬? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮营袜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘跪另。我一直安慰自己,他們只是感情好嘲驾,可當我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般彤枢。 火紅的嫁衣襯著肌膚如雪缴啡。 梳的紋絲不亂的頭發(fā)上秒咐,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天,我揣著相機與錄音雷滋,去河邊找鬼。 笑死澳泵,一個胖子當著我的面吹牛兔辅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了原探?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎闹蒜,沒想到半個月后姥闪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體筐喳,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡函似,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年顿天,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸟缕。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡蹲蒲,死狀恐怖届搁,靈堂內(nèi)的尸體忽然破棺而出卡睦,到底是詐尸還是另有隱情牺汤,我是刑警寧澤檐迟,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布溶其,位于F島的核電站,受9級特大地震影響廓块,放射性物質(zhì)發(fā)生泄漏厢绝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一带猴、第九天 我趴在偏房一處隱蔽的房頂上張望昔汉。 院中可真熱鬧,春花似錦拴清、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至沪停,卻和暖如春煤辨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背木张。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工掷酗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人窟哺。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像技肩,于是被迫代替她去往敵國和親且轨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內(nèi)容