GraphQL學習之原理篇

前言

在上一篇文章基礎篇中,咱們介紹了GraphQL的語法以及類型系統,算是對GraphQL有個基本的認識。在這一篇中,咱們將會介紹GraphQL的實現原理。說到原理,咱們就不得不依託於GraphQL的規範:GraphQLgit

概述

GraphQL規範主體部分有6大部分,除去咱們在上一節講到的類型系統(Type System)和語言(Language),剩下的即是整個GraphQL的主流程。也就是以下圖所示的:github

根據規範的章節,也就是GraphQL的實現流程,咱們原理篇一一來看看規範到底定義了些什麼,以及在實際的使用中,是如何貼近到規範的實現的。express

Js語言的實現版本是: graphql-js數組

流程總覽

首先咱們確定會在客戶端上書寫查詢語句,查詢語句在發送到服務端以前會轉換爲標準的請求體。以以前的demo爲例子,當咱們發起以下的請求的時候:bash

客戶端發起的請求體應該具有如下三個字段(POST請求):服務器

{
  "query": "...",
  "operationName": "...",
  "variables": { "myVariable": "someValue", ... }
}
複製代碼

截圖以下:函數

參考Serving over HTTP學習

這些參數表達了客戶端的訴求:調用哪一個方法,傳遞什麼樣的參數,返回哪些字段。ui

服務端拿到這段Schema以後,經過事先定義好的服務端Schema接收請求參數,校驗參數,而後執行對應的resolver函數,執行完成返回數據。spa

express-graphql這個包咱們能夠看到服務端的總體處理流程,縮略以下:

...
function graphqlHTTP(options: Options): Middleware {
  ...
  // 返回express的中間件形式的函數
  return function graphqlMiddleware(request: $Request, response: $Response) {
    ...
    // 處理request的參數,解析出來
    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.');
        }

        ...

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

        ...

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

        // 根據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') {...}

        // 執行resolver函數

      }
    )
    .then(result => {
      ... // 處理GraphQL返回的響應體,作些額外的工做。
    })
  }
}
複製代碼

更多細節請查看源碼。

自省(Introspection)

GraphQL服務器支持根據本身的schema進行自省。這對於咱們想要查詢一些關心的信息頗有用。好比咱們能夠查詢demo的一些關心的類型:

根據規範,有兩類自省系統:類型名稱自省(__typename)和schema自省(__schema和__type)。

__typename

GraphQL支持在一個查詢中任何一個節點添加對類型名稱的自省,在識別Interface/Union類型實際使用的類型的時候比較經常使用,在上圖演示,咱們能夠看到每一個節點均可以添加__typename,返回的類型也有不少:__Type__Field__InputValue__Schema

帶有__的都是GraphQL規範內部定義的類型,屬於保留名稱。開發者自定義的類型不容許出現__字符,不然在語法校驗的時候會失敗。

舉個例子:

將demo中的type Message改成type __Message,而後會報此類錯誤:

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

__schema&__type

__schema能夠用來查詢系統當前支持的全部語法,包括query語法、mutation語法,看它的結構就知道了:

type __Schema {
  types: [__Type!]! => 查詢系統當前定義的全部類型,包括自定義的和內部定義的全部類型
  queryType: __Type!  => 查詢 type Query {} 裏面全部的查詢方法
  mutationType: __Type => 查詢 type Mutation {} 裏面全部的mutation方法
  subscriptionType: __Type => 查詢 type Subscription {} 裏面全部subscription方法
  directives: [__Directive!]! => 查詢系統支持的指令
}
複製代碼

__type則是用來查詢指定的類型屬性。關於這些類型的內部定義請參考:Schema Introspection

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

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

Tips: 由於有了自省系統,GraphiQL纔有可能在你輸入查詢信息地時候進行文字提示,由於在頁面加載的時候GraphiQL會去請求這些內容,請求的內容能夠看這個文件:introspectionQueries.js

校驗

在上面的流程總覽中提到,客戶端發起的請求query字段帶有查詢的語法,這段語法要先通過校驗,咱們如下面最簡單的一次查詢爲例:

{
  getMessage {
    content
    author
  }
}
複製代碼

解析出來的請求參數數據是:

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

以後先是校驗服務端定義的schema:validateSchema(schema),上一節的那個錯誤就是在這邊拋出的。

接着將客戶端的query轉爲Source類型的結構:

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

接着轉爲AST:graphql.parsegraphql-js根據特徵字符串:

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,再執行parseDocument生成解析階段的產出物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!該字段也就是咱們在最開始說的請求的數據中的operationName,這些校驗都發聲在源碼的buildExecutionContext方法內

接着執行校驗的最後一個步驟:校驗客戶端語法並給出合理的解釋, graphql.validate(schema, documentAST, validationRules),好比我在將query語句變動爲:

{
  getMessage1 {
    content
    author
  }
}
複製代碼

graphql-js就會校驗不經過,而且給出對應的提示:

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

這種結構化的報錯信息也是GraphQL的一大特色,定位問題很是方便。只要語法沒問題校驗階段就能順利完成。

執行階段

graphql.execute是實現GraphQL規範的Execute章節的內容。根據規範,咱們將執行階段分爲:

每一個階段解釋一下:

  1. Validating Requests:到這個階段的校驗其實已經不多了,在源碼實現上只須要校驗入參是否符合規範便可,對應源碼的方法是:assertValidExecutionArguments
  2. Coercing Variable Values:檢查客戶端請求變量的合法性,須要和schema進行比對,對應源碼的方法是:getVariableValues
  3. Executing Operations:執行客戶端請求的方法與之對應的resolver函數。對應源碼的方法是:executeOperation
  4. Executing Selection Sets:蒐羅客戶端請求須要返回的字段集合,對應源碼的方法是:collectFields
  5. Executing Fields:執行每一個字段,須要進行遞歸,對應源碼的方法是:executeFields

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

Validating Requests

源碼中校驗了入參的三個:schema/document/variables

Coercing Variable Values

若是該操做定義了入參,那麼這些變量的值須要強制與方法聲明的入參類型進行比對。比對不經過,直接報錯,好比咱們將getMessage改成這樣:

query getMessage($id: ID){
  getMessage(id: $id) {
    content
    author
  }
}
複製代碼

而後變量是:

{
  "id": []
}
複製代碼

那麼通過這個函數將會報錯返回:"Variable \"$id\" got invalid value []; Expected type ID; ID cannot represent value: []"

Executing Operations => Executing Selection Sets => Executing Fields

在該流程上區分operation是query仍是mutation,兩者執行的方式前者是並行後者是串行。

總體流程以下所示:

在圖中標註的輸出的第一次執行的數據以下,僅供參考,流程圖以demo中的getMessage爲例子所畫,其中粉紅色是第一次波執行的流程,也就是解析getMessage這個字段所走的流程,以completeValueCatchingError爲界限是開始遍歷[Message]中的Message,這個流程以紫色線標註,如此往復遞歸,遍歷完客戶端要求的全部數據爲止

  1. collectFields
{ getMessage:
  [{
    kind: 'Field',
    alias: undefined,
    name: [Object],
    arguments: [],
    directives: [],
    selectionSet: [Object],
    loc: [Object]
  }]
}
複製代碼
  1. resolveFieldValueOrError 其入參第一次傳進去的source是:
{
  getMessage: [Function: getMessage],
  createMessage: [Function: createMessage],
  updateMessage: [Function: updateMessage]
}
複製代碼

第一次執行返回的結果:

[
  { 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這個字段名稱爲起點,執行resolver函數,獲得結果,由於返回類型是[Message],因此會對該返回類型先進行數組解析,再解析數組裏面的字段,以此不斷重複遞歸,直到獲取完客戶端指定的全部字段。圖形化的流程我在圖中用標號和顏色標註,應該很容易看懂整個流程的。

執行resolver函數的選擇

在這裏回答demo中提到的問題,一種寫法是將schema和resolve分別傳入schema和rootValue兩個字段內,另一種寫法是使用graphql-tools將typedefs和resolvers轉換成帶有resolve字段的schema,兩者寫法都是可行的,緣由以下:

首先代碼會給系統默認的fieldResolver賦值一個defaultFieldResolver函數,若是fieldResolver沒有傳值的話,這裏明顯沒有傳值。

以後在選擇resolver函數執行的時候有這麼一段代碼來實現了上述兩種寫法的可行性(resolveField.js):

const resolveFn = fieldDef.resolve || exeContext.fieldResolver;
複製代碼

這樣就優先使用schema定義的resolve函數,沒有的話就使用了rootValue傳遞的resolver函數了。所以執行的不同的話致使resolver函數獲取參數的方式略微不一樣:

第一種入參是:(args, contextValue, info) 第二種入參是:(source, args, contextValue, info) => 也就是此時你想要獲取參數的話得從第二個字段開始

Response

Response步驟就很簡單了,定義了4個規則:

一、響應體必須是一個對象

二、若是執行operation錯誤的時候,那麼errors必須存在,不然不該該有這個字段

2.1. `error`字段是一個數組對象,對象裏面必須包含一個`message`的字段來描述錯誤的緣由以及一些提示

2.2. 另外可能包含的字段有`location`、`path`、`extensions`來提示開發者錯誤的具體信息。
複製代碼

三、若是執行operation沒有錯誤,那麼data字段必須有值

四、其餘自定義的信息能夠定義在extensions這個字段內。

最後

至此,整個GraphQL實現的流程到這裏就結束了。更多細節請查看源碼和規範,咱們將在下一篇文章中聊聊GraphQL的實際項目應用GraphQL學習之實踐篇

參考

一、 GraphQL

相關文章
相關標籤/搜索