標籤:

在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 的查詢。

使用方式

這裡附上一個實例代碼

github.com/freebyron/eg

請將 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方法了,可以參考 github.com/freebyron/eg

目前 graphql 已經在阿里多個 node 應用中進行試點,並且已經開源到 github.com/eggjs/egg-gr,感謝先行者同事 @鄧若奇 的探索。

參考文章

  • graphql官網:facebook.github.io/grap
  • apollo-graphql: dev.apollodata.com/

推薦閱讀:

Node.js 性能調優之內存篇(二)——heapdump
NodeJS 工程師必備的 8 個工具

TAG:Nodejs | GraphQL |