此文是做者考慮 GraphQL 在 Node.js 架構中的落地方案後所得。從最初考慮能夠(之內置中間件)加入基礎服務並提供完整的構建、發佈、監控支持,到最終選擇不改動基礎服務以提供獨立包適配,不限制實現技術選型,交由業務團隊自由選擇的輕量方式落地。中間經歷瞭解除誤解,對收益疑惑,對最初定位疑惑,最終完成利弊權衡的過程。前端
文章會從解除誤解,技術選型,利弊權衡的角度,結合智聯招聘的開發現狀進行交流分享。git
文章會以 JavaScript 生態和 JavaScript 客戶端調用與服務端開發體驗爲例。github
對入門知識不作詳細闡述,可自行查閱學習指南中文(https://graphql.cn/learn/)/英文(https://graphql.org/learn/),規範中文(https://spec.graphql.cn/)/英文(https://github.com/graphql/graphql-spec/tree/master/spec),中文文檔有些滯後,但不影響了解 GraphQL。ajax
GraphQL 是一種 API 規範。不是拿來即用的庫或框架。不一樣對 GraphQL 的實如今客戶端的用法幾乎沒有區別,但在服務端的開發方式則天差地別。npm
一套運行中的 GraphQL 分爲三層:編程
僅僅有客戶端是沒法工做的。後端
GraphQL 的實現能讓客戶端獲取以結構化的方式,從服務端結構化定義的數據中只獲取想要的部分的能力。緩存
符合 GraphQL 規範的實現我稱之爲 GraphQL 引擎。bash
這裏的服務端不只指網絡服務,用 GraphQL 做爲中間層數據引擎提供本地數據的獲取也是可行的,GraphQL 規範並無對數據源和獲取方式加以限制。網絡
咱們把客戶端調用時發送的數據稱爲 Query Document
(查詢文檔),是段結構化的字符串,形如:
# 客戶端發送 query { contractedAuthor: { name articles { time title } } updateTime } # 或 mutation { # xxxxxx }
須要注意的是 Query Document
名稱中的 Query 和操做模型中的 query 是沒有關係的,像上述示例所示,Query Document
也能夠包含 mutation 操做。因此爲了不誤解,後文將把 Query Document
(查詢文檔)稱爲 Document 或文檔。一個 Document 中可包含單個或多個操做,每一個操做均可以查詢補丁數量的跟字段。
其中 query 下的 updateTime、contractedAuthor 這種操做下的第一層字段又稱之爲 root field
(根字段)。其餘具體規範請自行查閱文檔。
服務端使用名爲 GraphQL Schema Language
(或 Schema Definition Language
、SDL
)的語言定義 Schema 來描述服務端數據。
# 服務端 schema type Query { contractedAuthor: Author unContractedAuthor: Author updateTime: String } type Mutation{ # xxx } type Subscription { # xxx } type Author { name: String articles: [Article] } type Article { time: String title: String content: String } schema { query: Query mutation: Mutation subscription: Subscription }
能夠看到,因爲 GraphQL 是語言無關的,因此 SDL 帶有本身簡單的類型系統。具體與 JavaScript、Go 其餘語言的類型如何結合,要看各語言的實現。
從上面的 Schema 中咱們能夠獲得以下的一個數據結構,這就是服務可提供的完整的數據的 Graph
(圖):
{ query: { contractedAuthor: { name: String articles: [{ time: String title: String content: String }] } unContractedAuthor: { name: String articles: [{ time: String title: String content: String }] } updateTime: String } mutation: { # xxx } subscription: { # xxx } }
在 Schema 定義中存在三種特殊的類型 Query、Mutation、Subscription,也稱之爲 root types
(根類型),與 Document 中的操做模型一一對應的。
結合 Document 和 Schema,能夠直觀的感覺到 Document 和 Schema 結構的一致,且 Document 是 Schema 結構的一部分,那麼數據就會按照 Document 的這部分返回,會獲得以下的數據:
{ errors: [], data: { contractedAuthor: { name: 'zpfe', articles: [ { time: '2020-04-10', title: '深刻理解GraphQL' }, { time: '2020-04-11', title: 'GraphQL深刻理解' } ] }, updateTime: '2020-04-11' } }
預期數據會返回在 data 中,當有錯誤時,會出現 errors 字段並按照規範規定的格式展現錯誤。
如今 Document 和 Schema 結構對應上了,那麼數據如何來呢?
Selection Sets
選擇集:
query { contractedAuthor: { name articles { time title } honour { time name } } updateTime }
如上的查詢中存在如下選擇集:
# 頂層 { contractedAuthor updateTime } # 二層 { name articles honour } # articles:三層 1 { time title } # honour:三層 2 { time name }
Field
字段:類型中的每一個屬性都是一個字段。省略一些如校驗、合併的細節,數據獲取的過程以下:
root field
(根字段)。Selection Sets
(選擇集)。query 操做下,引擎通常會以廣度優先、同層選擇集並行執行獲取選擇集數據,規範沒有明確規定。mutation 下,由於涉及到數據修改,規範規定要按照由上到下按順序、深度優先的方式獲取選擇集數據。執行字段:
肯定了選擇集的執行順序後開始真正的字段值的獲取,很是簡化的講,Schema 中的類型應該對其每一個字段提供一個叫作 Resolver 的解析函數用於獲取字段的值。那麼可執行的 Schema 就形如:
type Query { contractedAuthor () => Author } type Author { name () => String articles () => [Article] } type Article { time () => String title () => String content () => String }
其中每一個類型方法都是一個 Resolver。
contractedAuthor()
後獲得值類型爲 Author,會繼續執行 name ()
和 articles()
以獲取 name 和 articles 的值,直到獲得類型爲標量(String、Int等)的值。至此由 Schema 和 Resolver 組合而成的可執行 Schema
就誕生了,Schema 跑了起來,GraphQl 引擎也就跑了起來。
GrahpQL 服務端開發的核心就是定義 Schema (結構)和實現相應的 Resolver(行爲)。
固然,在使用 GraphQL 的過程當中,還能夠:
Variables
(變量)複用同一段 Document 來動態傳參。Fragments
(片斷)下降 Document 的複雜度。Field Alias
(字段別名)進行簡單的返回結果字段重命名。這些都沒有什麼問題。
可是在 Directives
(指令)的支持和使用上,規範和實現是有衝突的。
而在研究 GraphQL 時發生的的誤解在於:
Query Document
整個操做仍是,Document 中的 query 操做,亦或是服務端側定義在 Schema 中的 Query 類型。GraphQL 的典型實現主要有如下幾種:
graphql-js 能夠說是其餘實現的基礎。
可執行 Schema 的建立方式是這幾種實現最大的不一樣,下面將就這部分進行展現。
npm install --save graphql
建立可執行 Schema
import { graphql, GraphQLList, GraphQLSchema, GraphQLObjectType, GraphQLString, } from 'graphql' const article = new GraphQLObjectType({ fields: { time: { type: GraphQLString, description: '寫做時間', resolve (parentValue) { return parent.date } }, title: { type: GraphQLString, description: '文章標題', } } }) const author = new GraphQLObjectType({ fields: { name: { type: GraphQLString, description: '做者姓名', }, articles: { type: GraphQLList(article), description: '文章列表', resolve(parentValue, args, ctx, info) { // return ajax.get('xxxx', { query: args }) }, } }, }) const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQuery', fields: { contractedAuthor: { type: author, description: '簽約做者', resolve(parentValue, args, ctx, info) { // return ajax.get('xxxx', { query: args }) }, }, }, }), })
能明確的看到,graphql-js 實現經過 GraphQLSchema 建立出的 schema 中,field 和 resolver 和他們一一對應的關係,同時此 schema 就是可執行 Schema。
執行
import { parse, execute, graphql } from 'graphql' import { schema } from '上面的schema' // 實際請求中,document 由 request.body 獲取 const document = ` query { contractedAuthor { name articles { title } } }` // 或使用導入的 graphql 方法執行 const response = await execute({ schema, document: parse(document), // 其餘變量參數等 })
傳入可執行 schema 和解析後的 Document 便可獲得預期數據。
Apollo 提供了完整的 GraphQL Node.js 服務框架,可是爲了更直觀的感覺可執行 Schema 的建立過程,使用 Apollo 提供的 graphql-tools
進行可執行 Schema 建立。
npm install graphql-tools graphql
上面是 Apollo 給出的依賴安裝命令,能夠看到 graphql-tools 須要 graphql-js(graphql)做爲依賴 。
建立可執行 Schema
import { makeExecutableSchema } from 'graphql-tools' const typeDefs = ` type Article { time: String title: String } type Author { name: String articles: [Article] } type Query { contractedAuthor: Author } schema { query: Query } ` const resolvers = { Query: { contractedAuthor (parentValue, args, ctx, info) { // return ajax.get('xxxx', { query: args }) } }, Author: { articles (parentValue, args, ctx, info) { // return ajax.get('xxxx', { query: args }) } }, Article: { time (article) { return article.date } } } const executableSchema = makeExecutableSchema({ typeDefs, resolvers, })
resolvers 部分以類型爲維度,以對象方法的形式提供了 Resolver。在生成可執行 Schema 時,會將 Schema 和 Resolver 經過類型映射起來,有必定的理解成本。
這部分涉及 TypeScript,只作不完整的簡要展現,詳情自行查閱文檔。
npm i graphql @types/graphql type-graphql reflect-metadata
能夠看到 type-graphql 一樣須要 graphql-js(graphql)做爲依賴 。
建立可執行 Schema
import 'reflect-metadata' import { buildSchemaSync } from 'type-graphql' @ObjectType({ description: "Object representing cooking recipe" }) class Recipe { @Field() title: string } @Resolver(of => Recipe) class RecipeResolver { @Query(returns => Recipe, { nullable: true }) async recipe(@Arg("title") title: string): Promise<Recipe> { // return await this.items.find(recipe => recipe.title === title); } @Query(returns => [Recipe], { description: "Get all the recipes from around the world " }) async recipes(): Promise<Recipe[]> { // return await this.items; } @FieldResolver() title(): string { return '標題' } } const schema = buildSchemaSync({ resolvers: [RecipeResolver] })
type-graphql 的核心是類,使用裝飾器註解的方式複用類生成 Schema 結構,並由 reflect-metadata
將註解信息提取出來。如由 @ObjectType()
和 @Field
將類 Recipe 映射爲含有 title 字段的 schema Recipe 類型。由 @Query 註解將 recipe
、recipes
方法映射爲 schema query 下的根字段。由 @Resolver(of => Recipe)
和 @FieldResolver()
將 title()
方法映射爲類型 Recipe
的 title 字段的 Resolver。
同:在介紹 Apollo 和 type-graphql 時,跳過了執行部分的展現,是由於這兩種實現生成的可執行 Schema 和 graphql-js 的是通用的,查看這二者最終生成的可執行 Schema 能夠發現其類型定義都是使用的由 graphql-js 提供的 GraphQLObjectType
等, 能夠選擇使用 graphql-js 提供的執行函數(graphql、execute 函數),或 apollo-server 提供的服務執行。
異:
功能:
對 GraphQL 的直觀印象就是按需、無冗餘,這是顯而易見的好處,那麼在實際應用中真的這麼直觀美好麼?
xxx/graphql/im
)沒法像 RESTful 接口那樣(如:xxx/graphql/im/message
、xxx/graphql/im/user
)從 URL 直接分辨出業務類型,會給故障排查帶來一些不便。上面提到的點幾乎都是出於調用方的視角,能夠看到,做爲 GraphQL 服務的調用方是比較舒服的。
因爲智聯招聘前端架構Ada中包含基於 Node.js 的 BFF(Backends For Frontends 面向前端的後端)層,前端開發者有能力針對具體功能點開發一對一的接口,有且已經進行了數據聚合、處理、緩存工做,也在 BFF 層進行過數據模型定義的嘗試,同時已經有團隊在現有 BFF 中接入了 GraphQL 能力並穩定運行了一段時間。因此也會從 GraphQL 的開發者和二者間的角度談談成本和收益。
root field
(根字段)爲粒度。這也是須要額外考慮的。綜合來看,可用的 GraphQL 服務(不考慮拿 GraphQL 作本地數據管理的狀況)的重心在服務提供方。做爲 GraphQL 的調用方是很爽的,且幾乎沒有弊端。那麼要不要上馬 GraphQL 就要重點衡量服務端的成本收益了。就個人體會而言,有如下幾種狀況: