在Egg中使用GraphQL
給 Egg 專欄的投稿
GraphQL使用 Schema 來描述數據,並通過制定和實現 GraphQL 規範定義了支持 Schema 查詢的 DSQL (Domain Specific Query Language,領域特定查詢語言,由 FACEBOOK 提出。
傳統 web 應用通過開發服務給客戶端提供介面是很常見的場景。而當需求或數據發生變化時,應用需要修改或者重新創建新的介面。長此以後,會造成伺服器代碼的不斷增長,介面內部邏輯複雜難以維護。而 GraphQL 則通過以下特性解決這個問題:
- 聲明式。查詢的結果格式由請求方(即客戶端)決定而非響應方(即伺服器端)決定。你不需要編寫很多額外的介面來適配客戶端請求
- 可組合。GraphQL 的查詢結構可以自由組合來滿足需求。
- 強類型。每個 GraphQL 查詢必須遵循其設定的類型才會被執行。
也就是說,通過以上的三個特性,當需求發生變化,客戶端只需要編寫能滿足新需求的查詢結構,如果服務端能提供的數據滿足需求,服務端代碼幾乎不需要做任何的修改。
本篇教程不會過多介紹 GraphQL 的概念,而會著重於講解如何通過 eggjs 來搭建 GraphQL 查詢服務。如果對 GraphQL 感興趣可以參考文末的參考鏈接。
技術選型
我們會使用 GraphQL Tools配合 eggjs 完成 GraphQL 服務的搭建。 GraphQL Tools 建立了一種 GraphQL-first 的開發哲學,主要體現在以下三個方面:
- 使用官方的 GraphQL schema 進行編程。 GraphQL Tools 提供工具,讓你可以書寫標準的 GraphQL schema,並完全支持裡面的特性。
- schema 與業務邏輯分離。 GraphQL Tools 建議我們把 GraphQL 邏輯分為四個部分: Schema, Resolvers, Models, 和 Connectors。
- 為很多特殊場景提供標準解決方案。最大限度標準化 GraphQL 應用。
egg-graphql
egg-graphql 插件是阿里濱江這邊一些 graphql 實踐的集合。其中使用了 GraphQL Server 完成了 GraphQL 查詢語言 DSQL 的解析。
同時會使用 dataloader 來優化數據緩存。
為了便於使用, egg-graphql在使用 apollo-graphql 推薦的開發最佳實踐之外,還會自動將schema載入到server中,把connector掛載到上下文中,便於使用。
egg-graphql 在服務端搭建了一個符合 GraphQL 規範的介面伺服器,而取數邏輯依然需要數據訪問層的配合,所以可以配合一些 ORM 框架如 egg-sequelize 來進行開發。
安裝與配置
安裝對應的依賴 [egg-graphql] :
$ npm i --save egg-graphql
開啟插件:
// config/plugin.jsexports.graphql = { enable: true, package: "egg-graphql",};
在 config/config.${env}.js
配置提供 graphql 的路由。
// config/config.${env}.jsexports.graphql = { router: "/graphql", // 是否載入到 app 上,默認開啟 app: true, // 是否載入到 agent 上,默認關閉 agent: false,};
在中間件中開啟 graphql
exports.middleware =
[
"graphql"
];
配置完成之後,每個落到 /graphql
的請求都會觸發 GraphQL Schema 的查詢。
使用方式
這裡附上一個實例代碼
https://github.com/freebyron/egg-graphql-boilerplate
請將 graphql 相關邏輯放到 app/graphql 下
目錄結構如下
.├── app│ ├── graphql│ │ ├── project│ │ │ └── schema.graphql│ │ └── user // 一個graphql模型│ │ ├── connector.js │ │ ├── resolver.js│ │ └── schema.graphql │ ├── model│ │ └── user.js│ ├── public│ └── router.js
編寫schema.graphql
GraphQL 使用 Schema 來描述數據。 這個 schema 表明了一個數據模型中,有哪些欄位,GraphQL 類庫的其他部分會來消費這個 Schema 對象。例子:
type User { id: ID! name: String! items: [Item!]}
編寫connector
編寫完 schema 之後,graphql 知道有哪些數據了,但他還需要知道「如何去取」, connector 的角色就在於此。 connector 的職責就是「取數」, 他既可以調用 rpc 介面取數,又可以調用內置的 orm 插件去取數,還可以直接調用 egg 的 service。
rpc方式
import rp from "request-promise";const { GITHUB_API_KEY, GITHUB_API_SECRET } = process.env;const qs = { GITHUB_API_KEY, GITHUB_API_SECRET };export function getRepositoryByName(name) { return rp({ uri: `https://api.github.com/repos/${name}`, qs, });}
直接調用 service
"use strict";class ArticleConnector { constructor(ctx) { this.ctx = ctx; } async getArticleInfoByService(iArticleID) { return await this.ctx.service.article.getArticleByID(iArticleID); } }module.exports = ArticleConnector;
在connector
中使用dataloader
上文講到,可以使用connector
直接調用資料庫進行取數操作。但使用 graphql 的查詢,直接訪問資料庫會出現問題。
舉個例子,以下 graphql schema 和 query 代碼:
# schematype User { name: String, friends: [User]}# query{ users { name friends { name friends { name } } }}
graphql 支持嵌套查詢,有時候會出現嚴重的N+1查詢性能問題,比如上一張圖,查詢了三次 User 表,而實際上只需要查詢一次即可。
Dataloader 是 facebook 搞的一個 js 庫,可以大幅降低資料庫的訪問頻次,從而降低系統負載,經常在 Graphql 場景中使用。通過使用 dataloader,資料庫的訪問頻次可以指數級別下降。
dataloader 是如何工作的呢,可以看下圖:
對於 User 表的多次訪問,通過 dataloader 去取,會自動合併為一個請求。dataloader 之所以可以實現這樣的能力,是因為他把每一次數據請求,都推遲到 node 的 Next Tick 後集中批處理運行,這樣就可以對請求進行加工合併。
下面貼一個使用 dataloader 寫的 connector:
"use strict";const DataLoader = require("dataloader");class UserConnector { constructor(ctx) { this.ctx = ctx; this.loader = new DataLoader(this.fetch.bind(this)); } fetch(ids) { const users = this.ctx.app.model.User.findAll({ where: { id: { $in: ids, }, }, }).then(us => us.map(u => u.toJSON())); return users; } fetchByIds(ids) { return this.loader.loadMany(ids); } fetchById(id) { return this.loader.load(id); }}module.exports = UserConnector;
我們通常的根據id取用戶的邏輯通過fetch
方法實現,之後封裝為兩個方法,將其用 dataloader 包裹。當用戶需要批量獲取時,直接調用 dataloader 包裹後的方法,即可自動進行優化。這樣就避免了多次調用 fetch
方法,跟資料庫建立多次查詢了。
編寫resolver
我們編寫好取數邏輯後,就要對用戶的查詢進行處理了,這個處理代碼稱之為 resolver.
其實 resolver 非常簡單,就是針對你暴露的查詢介面,調用相應的connector去取數即可,如下:
"use strict";module.exports = { Query: { user(root, { id }, ctx) { return ctx.connector.user.fetchById(id); }, },};
之後用戶所有對 User schema 的 graphql query,都會通過 connector 去獲取。由於 connector 已經掛載到上下文上,你可以直接使用 ctx.connector 引用。
完成一次查詢
我們可以手動構造一個查詢請求檢驗下
const query = JSON.stringify({ query: `{ user(id: ${user.id}) { id name } }`,}); // graphql 的 query ,可以通過工具或者自己構造出來const data = yield ctx.service.graphql.query(query);//主查詢方法
實際請求的時候不需要手動處理,只要請求落在了我們配置的路由上,就會自動調用ctx.service.graphql.query(query)
方法。
整體流程
其他
那如果我們要對數據進行增刪改,就需要使用 graphql 的 mutation方法了,可以參考 https://github.com/freebyron/egg-graphql-boilerplate/tree/master/app/graphql/mutation
目前 graphql 已經在阿里多個 node 應用中進行試點,並且已經開源到 https://github.com/eggjs/egg-graphql,感謝先行者同事 @鄧若奇 的探索。
參考文章
- graphql官網:http://facebook.github.io/graphql
- apollo-graphql: http://dev.apollodata.com/
推薦閱讀: