在上一篇文章基礎篇中,咱們介紹了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", ... }
}
複製代碼
截圖以下:函數
這些參數表達了客戶端的訴求:調用哪一個方法,傳遞什麼樣的參數,返回哪些字段。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返回的響應體,作些額外的工做。
})
}
}
複製代碼
更多細節請查看源碼。
GraphQL服務器支持根據本身的schema進行自省。這對於咱們想要查詢一些關心的信息頗有用。好比咱們能夠查詢demo的一些關心的類型:
根據規範,有兩類自省系統:類型名稱自省(__typename)和schema自省(__schema和__type)。
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
能夠用來查詢系統當前支持的全部語法,包括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.parse
,graphql-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
章節的內容。根據規範,咱們將執行階段分爲:
每一個階段解釋一下:
assertValidExecutionArguments
getVariableValues
executeOperation
collectFields
executeFields
接下去咱們大概講解下每一個過程的一些要點
源碼中校驗了入參的三個:schema/document/variables
若是該操做定義了入參,那麼這些變量的值須要強制與方法聲明的入參類型進行比對。比對不經過,直接報錯,好比咱們將getMessage
改成這樣:
query getMessage($id: ID){
getMessage(id: $id) {
content
author
}
}
複製代碼
而後變量是:
{
"id": []
}
複製代碼
那麼通過這個函數將會報錯返回:"Variable \"$id\" got invalid value []; Expected type ID; ID cannot represent value: []"
在該流程上區分operation是query仍是mutation,兩者執行的方式前者是並行後者是串行。
總體流程以下所示:
在圖中標註的輸出的第一次執行的數據以下,僅供參考,流程圖以demo中的getMessage
爲例子所畫,其中粉紅色是第一次波執行的流程,也就是解析getMessage這個字段所走的流程,以completeValueCatchingError爲界限是開始遍歷[Message]中的Message,這個流程以紫色線標註,如此往復遞歸,遍歷完客戶端要求的全部數據爲止
{ getMessage:
[{
kind: 'Field',
alias: undefined,
name: [Object],
arguments: [],
directives: [],
selectionSet: [Object],
loc: [Object]
}]
}
複製代碼
{
getMessage: [Function: getMessage],
createMessage: [Function: createMessage],
updateMessage: [Function: updateMessage]
}
複製代碼
第一次執行返回的結果:
[
{ id: 0, content: 'test content', author: 'pp' },
{ id: 1, content: 'test content 1', author: 'pp1' }
]
複製代碼
[
{ content: 'test content', author: 'pp' },
{ content: 'test content 1', author: 'pp1' }
]
複製代碼
整個流程以getMessage這個字段名稱爲起點,執行resolver函數,獲得結果,由於返回類型是[Message]
,因此會對該返回類型先進行數組解析,再解析數組裏面的字段,以此不斷重複遞歸,直到獲取完客戶端指定的全部字段。圖形化的流程我在圖中用標號和顏色標註,應該很容易看懂整個流程的。
在這裏回答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步驟就很簡單了,定義了4個規則:
一、響應體必須是一個對象
二、若是執行operation錯誤的時候,那麼errors
必須存在,不然不該該有這個字段
2.1. `error`字段是一個數組對象,對象裏面必須包含一個`message`的字段來描述錯誤的緣由以及一些提示
2.2. 另外可能包含的字段有`location`、`path`、`extensions`來提示開發者錯誤的具體信息。
複製代碼
三、若是執行operation沒有錯誤,那麼data
字段必須有值
四、其餘自定義的信息能夠定義在extensions
這個字段內。
至此,整個GraphQL實現的流程到這裏就結束了。更多細節請查看源碼和規範,咱們將在下一篇文章中聊聊GraphQL的實際項目應用GraphQL學習之實踐篇
一、 GraphQL