*項目地址: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
如標題所示,這裏主要是介紹基於 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 的特性包括下面的部分,以後隨着瞭解的深刻,會繼續添加。後端
在項目實現過程當中爲了方便考慮,項目中並無真實實現一個提供 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 服務的過程大體分下面幾步:
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,
},
});
};
複製代碼
建立好服務,咱們須要定義 Schema 來描述各個服務模塊。這裏主要採用工業聚在上面文章中提到的方式來設計和定義。
Schema 定義包含如下幾部分:
下面是定義用戶服務模塊的 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"
)
}
複製代碼
關於 RestDirective 自定義指令的實現,能夠查看 graphql-tools 的文檔示例:Fetching data from a REST API。這裏說明一下項目中 @rest 指令的主要實現思路。
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
,可是你能夠基本按照正常描述對象的方式來編寫,不過目前不支持換行,因此說如今的寫法稍微有些不舒服。
具體代碼可查看 ./src/directives/rest.ts
文件。
上面提到了,項目中並無真實實現一個 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}`);
});
複製代碼
OK,上面算是作好建立服務的基礎工做以後,這個時候須要往裏面存儲數據了。好比說建立用戶信息。這裏推薦 Altair GraphQL Client 來調試,一個相似於 Postman 的接口調試客戶端,挺好用的。
mutation CreateUser($body: UserInput!) {
createUser(body: $body) {
name
}
}
複製代碼
{
"body": {
"name": "gogogo",
"orderId": 1,
"productId": 1
}
}
複製代碼
存儲了數據以後,就能夠對裏面的數據進行查詢了,好比獲取用戶信息。
query {
user(userId: 1) {
name
}
}
複製代碼
按照上面的步驟分別存儲看 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
}
複製代碼
這個部分主要涉及到 GraphQL 比較經典的 N+1 問題,它會給服務器帶來性能問題。
關於這個問題,facebook 官方關於 graphql 的 github group 下維護了一個叫 dataloder 的包來處理這個問題。主要的操做是 batching 和 caching。詳細瞭解能夠去看它的 README 文檔。
項目中預計用不了 caching 操做,由於用的是 RESTful API 接口,沒有深刻到數據庫操做那一層,不知道接口數據何時會發生變化(若是有辦法知道數據何時會發生變化也能夠用;或者把不可修改數據和其餘數據分開,單獨對這一部分數據作 caching 操做,通常來講不可修改數據都是比較重要的數據),也就不知道何時要更新緩存,保證不了緩存是否新鮮,這樣使用過程當中確定會出現問題。batching 操做的話,能夠有效去重 event loop 下同一個 tick 內發送的請求,減輕必定的服務端壓力。可是面對不一樣的請求,請求該發還得發,基本避免不了,除非 RESTful 服務有對應的 API 支持。
目前這部分的代碼還麼有實現 😂,計劃本週五(08/23)以前完成。
關於 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
文件。目前在這個庫上我尚未太多嘗試,因此只是一個嘗試建議。
同理,GraphQL 也能夠很方便地生成 API 文檔,並且還能夠根據 type 與 type 之間關係,畫一張關係圖出來。
下面是使用 graphql-voyager 生成的 API 文檔,結果以下圖:
整個項目實現下來最大的兩點感覺是:Schema 定義的部分可能須要反覆調整,須要對業務模塊足夠了解,才能比較好的定義出 type 與 type 之間的關聯關係;自定義指令方面還須要對參數和參數類型作很好地支持,才能讓開發者輕鬆地使用它來完成他們所須要的功能。
對於如何使用 GraphQL 來講,這個項目只是一個開始,接下來還有不少內容須要去了解和實踐,包括對現有實現模塊的改進。最後,但願本文對你在 GraphQL 的使用方面有所幫助。
*項目地址:graphql-basedon-restful
The End!