GraphQL-Basedon-RESTful:基於 RESTful API 的 GraphQL 服務構建

*項目地址:graphql-basedon-restful前端

*假如以爲兩者是互相對立,推薦先看這篇文章:callstack.com/blog/the-po…ios

前不久看了工業聚的文章《GraphQL-BFF:微服務背景下的先後端數據交互方案》,很是很是精彩,又從新拾起了對 GraphQL 的興趣。自己在工做的項目中,已經在使用 GraphQL 了,多是由於使用方式的緣由,以爲用的有點多餘,只是有一個 GraphQL 的殼子而已,沒有足夠發揮出 GraphQL 的優秀特性,使用的方式有所改進但大體和文章中 [5.1] RESTful-Like 模式 描述的方式相似,type 與 type 之間的定義缺少聯繫,與 RESTful API 設計對應只是簡單的一對一關係,沒法發揮出 GraphQL 關聯查詢的能力。因此那時候對 GraphQL 這一塊的知識就沒太關注了。git

可是 GraphQL 的社區一直都在發展,關於 GraphQL 的 Repo、文章和討論也愈來愈多。在去年杭州開了第一屆 GraphQLParty,宋小菜前端團隊花了很長很長的篇幅介紹了他們在 GraphQL 方面的實踐,也很是很是精彩。因此對這一塊的知識也是會有一些關注,指望能找到一個比較理想的使用 GraphQL 的方式。github

正好前不久看到了工業聚寫的關於 GraphQL 的文章,發現原來是以前的使用方式有問題,才致使認爲 GraphQL 帶來的成效並不大。看完這篇文章以後,明白了項目在使用方式上的問題,同時也加深了對它的認識。因此決定對 GraphQL 作進一步的瞭解。因此就有了今天這個項目。這個項目也是從那個時候開始寫的,8 月剛開始。數據庫

關於 GraphQL 的介紹以及它與 RESTful API 設計的對比已經有不少了,這裏就不贅述了。在開始閱讀以前,若是對 GraphQL 還不瞭解的同窗,能夠先去查閱一些 GraphQL 的基礎,好比 GraphQL 的語法介紹中文版),或者查看下面的圖片,它把大概要掌握的基礎知識都在一張圖裏面列了出來(來自 graphql-shorthand-notation-cheat-sheet)。npm

graphql-shorthand-notation-cheat-sheet.png

如標題所示,這裏主要是介紹基於 RESTful API 設計的 GraphQL 服務構建。由於目前項目的接口都是基於 RESTful 規範設計的,這個時候若是想使用 GraphQL,不可能說根據 RESTful 提供的接口用 GraphQL 再實現一遍,這個代價太大了,時間和人力成本各方面都不容許。這個時候若是能直接基於 RESTful API 設計的接口來實現一套 GraphQL 接口是很是好的。RESTful API 接口能夠保留且不影響它的後端開發,同時又能夠對外提供 GraphQL 的服務,方便前端的使用。json

要實現這個功能,其實也不難,使用 GraphQL 的自定義 Directive (指令)就能夠完成。自定義指令特性很是實用和便捷,經過編寫自定義指令來轉換 Schema 的 types、fields 和 arguments,擴展 Schema 字符串所能描述的邏輯。關於自定義指令的介紹,具體能夠去看 GraphQL Tools 上關於它的介紹,其中就介紹了怎麼樣去獲取 REST API 的數據( Fetching data from a REST API)。axios

這個項目也就是圍繞這個自定義指令展開的,同時也藉着這個機會了解 GraphQL 的其餘特性。目前這個項目會涉及到的 GraphQL 的特性包括下面的部分,以後隨着瞭解的深刻,會繼續添加。後端

  • 定義 type 來描述各個模塊服務
  • 定義 custom scalar type 和 enum type 來擴展 field type
  • 實現自定義指令
  • query 查詢
  • mutation 操做
  • query 關聯查詢
  • 對 query 作 batching 處理?
  • mock 數據
  • 生成 api 文檔

在項目實現過程當中爲了方便考慮,項目中並無真實實現一個提供 RESTful API 接口的服務,而是用的 json-server 這個 npm 包,模擬了一個 RESTful API 服務。不過畢竟它不是真正的 RESTful API 服務,使用上仍是會受到一些限制,好比:api

  • 關聯查詢的部分,只能查詢 /users/:userId/orders 獲得一個 orders 數組,不能查 /users/:useId/order,這樣只會獲得一個空對象。指望是若是 orders 數組只有一個元素,則返回這個元素對象,也就是 user 與 order 是一對一的關係。這時候須要想辦法,因此在 @rest 指令裏面加入了 responseAccessor 參數,用於對請求到的數據作處理。
  • 不方便作數據之間的聯動,好比我新增了一個定義訂單記錄,記錄了對應的 userId 來表示用戶信息,這時候須要聯動在對應用戶信息記錄 orderId 信息表示用戶的訂單號,來記錄它們之間的關聯關係。可是目前沒有找到方便支持的方法,目前的解決方法是事先設想好關聯的數據,手動在建立用戶信息那裏提早記錄設想好的 orderId 數據。

因此會致使 mutation 操做和 field resolver 的時候須要作特殊處理。這個具體會在下面的介紹中講到,這個先給你們說明一下。

服務構建過程

構建 GraphQL 服務的過程大體分下面幾步:

  1. 基於 Apollo Server 來建立這個服務
  2. 按照工業聚文章中提到的模塊服務設計方式來定義 schema
    • 定義各類 type
    • 定義 scalar type
    • 定義 enum type
    • 爲 schema 添加上 @rest 指令
  3. 實現 RestDirective 自定義指令。
  4. 模擬一個 REST 服務。
  5. 作 mutation 操做,往服務裏存儲數據。
  6. 作 query 操做,查詢以前存儲的數據。
  7. 體驗級聯查詢的效果。
  8. Batching 和 Caching ?
  9. Mock 數據。
  10. API 文檔。

1. 基於 Apollo Server 建立 GraphQL 服務

Apollo Server 是一個徹底由社區驅動開發的開源工具,使用它能快速便捷地建立出一個 GraphQL 服務。它既能獨立提供服務,也能以中間件的形式嵌入到 Nodejs 服務中對外提供服務。它對不少現有的 Nodejs 框架都作了支持,好比咱們經常使用的 Express 和 Koa。這個項目採用的是 Koa 的 apollo-server-koa 中間件來建立服務,並使用 TypeScript 來編寫。

下面是主要的實現代碼:

import { ApolloServer } from 'apollo-server-koa';
import { GraphQLSchema } from 'graphql';
import { fileLoader, mergeTypes } from 'merge-graphql-schemas';
import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools';
import { RestDirective } from './directives';
import { dateScalarType } from './scalarTypes';

export function createApolloServer(config: {
  // 因爲涉及到多個文件下的 Type 定義,這裏須要傳 schemaDir
  // 以後再用 graphql-tools 下的 fileLoader / mergeTypes / makeExecutableSchema 方法
  // 獲得最終的 schema 定義
  schemaDir: string;
  // 因爲可能會涉及到多個後端服務,這裏須要傳 endpointMap
  // 這在後面的 rest 指令中會用到
  endpointMap: { [key: string]: string };
  // 服務的 mock 數據配置
  mocks: boolean | { [ key: string ]: any };
}) {
  const { schemaDir, endpointMap, mocks } = config;
  // 準備 schema
  const typesArray = fileLoader(schemaDir, { recursive: true });
  const typeDefs = mergeTypes(typesArray, { all: true });
  const schema: GraphQLSchema = makeExecutableSchema({
    typeDefs,
    schemaDirectives: {
      // 添加 RestDirective 自定義指令
      rest: RestDirective,
    },
  });

  // 處理 mock 數據
  if (mocks) {
    addMockFunctionsToSchema({
      schema,
      mocks: typeof mocks === 'boolean' ? {} : mocks,
      preserveResolvers: true,
    });
  }

  return new ApolloServer({
    schema,
    context: () => ({
      endpointMap
    }),
    resolvers: {
      // 添加 custom scalar type
      Date: dateScalarType,
    },
  });
};
複製代碼

2. 定義 Schema

建立好服務,咱們須要定義 Schema 來描述各個服務模塊。這裏主要採用工業聚在上面文章中提到的方式來設計和定義。

Schema 定義包含如下幾部分:

  • 定義 type 來描述各個模塊服務,以及 query 和 mutation 要處理的操做
  • 定義 custom scalar type 和 enum type 來擴展 field type
  • 爲 schema 添加上 @rest 指令

下面是定義用戶服務模塊的 userService.gql 文件代碼。詳情的 Schema 定義,能夠去查看 ./examples/koaServer/schema 文件夾下的代碼。

directive @rest(
  endpoint: String
  path: String
  method: String
  parentAccessorMap: String
  responseAccessor: String
) on FIELD_DEFINITION

type User {
  id: Int
  name: String
}

extend type Order {
  user(orderId: Int): User @rest(
    endpoint: "endpoint2001"
    path: "/orders/:orderId/users"
    parentAccessorMap: "{ id: orderId }"
    responseAccessor: "[0]"
  )
}

extend type Product {
  users(productId: Int): [User] @rest(
    endpoint: "endpoint2001"
    path: "/products/:productId/users"
    parentAccessorMap: "{ id: productId }"
  )
}

input UserInput {
  name: String!
  # 提早記錄設想好的 orderId 信息
  orderId: Int
  # 提早記錄設想好的 productId 信息
  productId: Int
}

extend type Query {
  user(userId: Int!): User @rest(
    endpoint: "endpoint2001"
    path: "/users/:userId"
  )
  users: [User] @rest(
    endpoint: "endpoint2001"
    path: "/users"
  )
}

extend type Mutation {
  createUser(body: UserInput): User @rest(
    endpoint: "endpoint2001"
    path: "/users"
    method: "post"
  )
  updateUser( userId: Int! body: UserInput ): User @rest(
    endpoint: "endpoint2001"
    path: "/users/:userId"
    method: "patch"
  )
}
複製代碼

3. 實現 RestDirective 自定義指令

關於 RestDirective 自定義指令的實現,能夠查看 graphql-tools 的文檔示例:Fetching data from a REST API。這裏說明一下項目中 @rest 指令的主要實現思路。

  1. 它是定義在 field 上的,用來 resolve field 的值。
  2. 目前提供五個設置參數,分別是:
    • endpoint:接口服務地址的字符串標識,以後根據 context 中 endpointMap 對象的值來得到實際的接口服務地址。
    • path:接口請求路徑,好比 /api/users/:userId,這裏使用 :userId 的方式來表示須要傳遞給 path 的 userId 變量。這裏與常規的 url route path 定義變量的方式相同,而後用 route-parser 相似功能的 npm 包來解析。
    • method:接口請求方法,小寫字符,默認爲 'get'。
    • responseAccessor:對於接口返回的值作處理,它的值實際是 lodash get 方法的第二個參數。
    • parentAccessorMap: 在關聯查詢中,上一級 resolve 的值須要傳遞一些數據給當前查詢,可是參數名有可能不一樣,好比查用戶下的訂單數據,須要根據 userId 來查詢,可是上一級獲取到的用戶標識的參數名是 id,這樣參數名就對應不上。這個時候能夠經過設置 parentAccessorMap 的值來設置參數名之間的映射關係。parentAccessorMap 值的類型是 String,可是你能夠基本按照正常描述對象的方式來編寫,不過目前不支持換行,因此說如今的寫法稍微有些不舒服。
      • 以後會支持寫真正的對象,看到過有這種實現的自定義指令,好比 graphql-faker 項目中的 @fake 指令,支持寫對象和數組。到時候就不會有這個問題了,不過到時候 @rest 指令的參數格式應該會相應發生改變。
  3. 實際 field.resolve 函數中要處理的 args 參數對象,項目中會把這個對象裏面的參數分紅三塊:
    • 一塊是 params,附加到 url path 上。
    • 一塊是 body,附加到請求體裏面。
    • 還有一塊是 path args,用於 url route path 的解析。
    • 以後也能夠有 headers 參數來自定義一些要發送請求頭等等。
  4. 最後使用 axios 來發送接口請求。
  5. 目前尚未處理接口權限問題。

具體代碼可查看 ./src/directives/rest.ts 文件。

4. 模擬 RESTful API 服務

上面提到了,項目中並無真實實現一個 RESTful API 服務,而是用 json-server 這個 npm 包,模擬了一個。省略了鏈接數據庫,或者鏈接遠程服務的步驟,用本地 json 文件來保存數據對外提供 RESTful API 服務。因爲不是主要邏輯模塊,實現邏輯這裏不詳述。下面是實現代碼:

import jsonServer from 'json-server';
import path from 'path';

const server = jsonServer.create();
const dbPath = path.join(__dirname, './db.json');
const router = jsonServer.router(dbPath);
const middleware = jsonServer.defaults();
const port = 2001;

server.use(middleware);
server.use(router);
server.use(jsonServer.bodyParser);
server.listen(port, () => {
  console.log(`JSON Server is running on: http://127.0.0.1:${port}`);
});
複製代碼

5. 作 mutation 操做,往服務裏存儲數據

OK,上面算是作好建立服務的基礎工做以後,這個時候須要往裏面存儲數據了。好比說建立用戶信息。這裏推薦 Altair GraphQL Client 來調試,一個相似於 Postman 的接口調試客戶端,挺好用的。

mutation CreateUser($body: UserInput!) {
  createUser(body: $body) {
    name
  }
}
複製代碼
{
  "body": {
     "name": "gogogo",
     "orderId": 1,
     "productId": 1
  }
}
複製代碼

image.png

6. 作 query 操做,查詢以前存儲的數據

存儲了數據以後,就能夠對裏面的數據進行查詢了,好比獲取用戶信息。

query {
  user(userId: 1) {
    name
  }
}
複製代碼

query-user.png

7. 體驗 GraphQL 關聯查詢

按照上面的步驟分別存儲看 users、orders、products 的數據(實際上項目裏已經預先存儲好了須要調試的數據,能夠去 ./examples/jsonServer/db.json 文件下查看),這時候咱們一塊兒體驗一下 GraphQL 優秀特性之一關聯查詢。好比查詢用戶名下的訂單數和產品數,每一個訂單和產品又能夠往下查對應的用戶信息。一級一級按照各個服務模塊之間的關聯關係來查詢數據。

query userInfo($uid: Int!) {
  user(userId: $uid) {
    name
    orders {
      price
      user {
        name
      }
    }
    collections {
      id
      description
      users {
        name
      }
    }
  }
}
複製代碼
{
  "uid": 1
}
複製代碼

query-user-graph.png

8. Batching 和 Caching?

這個部分主要涉及到 GraphQL 比較經典的 N+1 問題,它會給服務器帶來性能問題。

關於這個問題,facebook 官方關於 graphql 的 github group 下維護了一個叫 dataloder 的包來處理這個問題。主要的操做是 batching 和 caching。詳細瞭解能夠去看它的 README 文檔。

項目中預計用不了 caching 操做,由於用的是 RESTful API 接口,沒有深刻到數據庫操做那一層,不知道接口數據何時會發生變化(若是有辦法知道數據何時會發生變化也能夠用;或者把不可修改數據和其餘數據分開,單獨對這一部分數據作 caching 操做,通常來講不可修改數據都是比較重要的數據),也就不知道何時要更新緩存,保證不了緩存是否新鮮,這樣使用過程當中確定會出現問題。batching 操做的話,能夠有效去重 event loop 下同一個 tick 內發送的請求,減輕必定的服務端壓力。可是面對不一樣的請求,請求該發還得發,基本避免不了,除非 RESTful 服務有對應的 API 支持。

目前這部分的代碼還麼有實現 😂,計劃本週五(08/23)以前完成。

9. Mock 數據

關於 mock 數據,GraphQL 有很天然的優點,由於它自己 field 在定義的時候就肯定了數據類型,而後咱們再根據它的數據類型來提供 mock 數據。

內置的 scalar type,在打開 mock 數據開關以後,會默認給對應的類型提供 mock 數據(固然也能夠修改),對於自定義的 scalar type 和 enum type,咱們也能夠很輕鬆地提供 mock 數據,配合上 casual 或者 faker.js 這些模擬數據的工具。

給特定的 field 提供模擬數據,好比說 user 或者 users。就至關於定義了 這個 field 的 resolver,能夠拿到傳進來的參數,根據傳進來的參數動態的返回模擬數據。

上面是須要侵入項目代碼的模擬數據的方式,也能夠選擇在外面另外起一個服務來模擬數據,也能夠嘗試 graphql-faker 經過擴展示有服務的方式來提供 mock 數據,不改動原有項目代碼,它會在項目根目錄下建立一個 .grahql 文件。目前在這個庫上我尚未太多嘗試,因此只是一個嘗試建議。

10. 生成 API 文檔

同理,GraphQL 也能夠很方便地生成 API 文檔,並且還能夠根據 type 與 type 之間關係,畫一張關係圖出來。

下面是使用 graphql-voyager 生成的 API 文檔,結果以下圖:

api-doc.png

結語

整個項目實現下來最大的兩點感覺是:Schema 定義的部分可能須要反覆調整,須要對業務模塊足夠了解,才能比較好的定義出 type 與 type 之間的關聯關係;自定義指令方面還須要對參數和參數類型作很好地支持,才能讓開發者輕鬆地使用它來完成他們所須要的功能。

對於如何使用 GraphQL 來講,這個項目只是一個開始,接下來還有不少內容須要去了解和實踐,包括對現有實現模塊的改進。最後,但願本文對你在 GraphQL 的使用方面有所幫助。

*項目地址:graphql-basedon-restful

The End!

參考

原文連接:zhuanlan.zhihu.com/p/78962152

相關文章
相關標籤/搜索