前言
在上一篇文章基礎(chǔ)篇中碑韵,我們介紹了GraphQL的語法以及類型系統(tǒng)叁怪,算是對GraphQL有個基本的認識族展。在這一篇中森缠,我們將會介紹GraphQL的實現(xiàn)原理。說到原理仪缸,我們就不得不依托于GraphQL的規(guī)范:GraphQL
概述
GraphQL規(guī)范主體部分有6大部分贵涵,除去我們在上一節(jié)講到的類型系統(tǒng)(Type System)和語言(Language)稿械,剩下的便是整個GraphQL的主流程观谦。也就是如下圖所示的:
根據(jù)規(guī)范的章節(jié)蛤克,也就是GraphQL的實現(xiàn)流程抗蠢,我們原理篇一一來看看規(guī)范到底定義了些什么,以及在實際的使用中援奢,是如何貼近到規(guī)范的實現(xiàn)的蓖谢。
Js語言的實現(xiàn)版本是: graphql-js
流程總覽
首先我們肯定會在客戶端上書寫查詢語句根吁,查詢語句在發(fā)送到服務(wù)端之前會轉(zhuǎn)換為標準的請求體片林。以之前的demo為例子坟奥,當我們發(fā)起如下的請求的時候:
客戶端發(fā)起的請求體應(yīng)該具備以下三個字段(POST請求):
{
"query": "...",
"operationName": "...",
"variables": { "myVariable": "someValue", ... }
}
截圖如下:
這些參數(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)心的類型:
根據(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í)行階段分為:
每個階段解釋一下:
- Validating Requests:到這個階段的校驗其實已經(jīng)很少了,在源碼實現(xiàn)上只需要校驗入?yún)⑹欠穹弦?guī)范即可黔帕,對應(yīng)源碼的方法是:
assertValidExecutionArguments
- Coercing Variable Values:檢查客戶端請求變量的合法性成黄,需要和schema進行比對奋岁,對應(yīng)源碼的方法是:
getVariableValues
- Executing Operations:執(zhí)行客戶端請求的方法與之對應(yīng)的resolver函數(shù)闻伶。對應(yīng)源碼的方法是:
executeOperation
- Executing Selection Sets:搜羅客戶端請求需要返回的字段集合蓝翰,對應(yīng)源碼的方法是:
collectFields
- 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í)行的方式前者是并行后者是串行鞭执。
整體流程如下所示:
在圖中標注的輸出的第一次執(zhí)行的數(shù)據(jù)如下,僅供參考估脆,流程圖以demo中的getMessage
為例子所畫疙赠,其中粉紅色是第一次波執(zhí)行的流程圃阳,也就是解析getMessage這個字段所走的流程,以completeValueCatchingError為界限是開始遍歷[Message]中的Message锣夹,這個流程以紫色線標注晕城,如此往復(fù)遞歸砖顷,遍歷完客戶端要求的所有數(shù)據(jù)為止
- collectFields
{ getMessage:
[{
kind: 'Field',
alias: undefined,
name: [Object],
arguments: [],
directives: [],
selectionSet: [Object],
loc: [Object]
}]
}
- 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' }
]
- 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