原文發表在: https://github.com/kuitos/kui...html
熟悉 Apollo GraphQL 的同窗可直接跳過這一章,從 實踐 一章看起。前端
GraphQL 做爲 FaceBook 2015年推出的 API 定義/查詢 語言,在歷經了兩年的發展以後,社區已相對發達和完善。對於 GraphQL 的一些基礎概念,本文再也不一一贅述,目前社區相關的文章已經不少,有興趣的同窗能夠去 google,或者直接看GraphQL 官方教程 Apollo GraphQL Server 官方文檔。node
而 Apollo GraphQL 做爲目前社區最流行的 GraphQL 解決方案提供商,提供了從 client 到 server 的一整套完整的工具鏈。在這裏我也準備以 Apollo 爲例,經過一步步搭建 Apollo GraphQL Server 的方式,來給你們展現 GraphQL 的特色,以及個人一些思考(主要是個人思考?)。ios
建立基於 express 的 GraphQL servergit
// server.js import express from 'express'; import { graphiqlExpress, graphqlExpress } from 'apollo-server-express'; import schema from './models'; const PORT = 8080; const app = express(); ... app.use('/graphql', graphqlExpress({ schema })); app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' })); if (process.env.NODE_ENV === 'development') { glob(path.resolve(__dirname, './mock/**/*.js'), {}, (er, modules) => modules.forEach(module => require(module).default(app))); } app.listen(PORT, () => console.log(`> Listening at port ${PORT}`));
執行 node server.js
,這樣咱們就能啓動一個 GraphQL server 了。github
注意咱們這裏使用了 apollo-server-express
提供的 graphiqlExpress
插件,graphiql 是一個用於瀏覽器端調試 graphql 接口的 GUI 工具。服務啓動後,咱們在瀏覽器打開 http://localhost:8080/graphiql
就能夠看到這樣一個頁面web
咱們在 server.js 中定義了這樣一個 endpoint : app.use('/graphql', graphqlExpress({ schema }));
typescript
這裏傳入的 schema 是什麼呢?它大概長這樣:express
import { makeExecutableSchema } from 'graphql-tools'; // The GraphQL schema in string form const typeDefs = ` type User { id: ID! name: String age: Int } type Query { user(id: ID!): User } schema { query: Query } `; // The resolvers const resolvers = { Query: { user({id}) { return http.get(`/users/${id}`)}} }; // Put together a schema const schema = makeExecutableSchema({ typeDefs, resolvers }); app.use('/graphql', graphqlExpress({ schema }));
這裏的關鍵是用了 graphql-tools
這個庫提供的 makeExecutableSchema
組合了 schema 定義和對應的 resolver。resolver 是 Apollo GraphQL 工具鏈中提出的一個概念,什麼用呢?就是在咱們客戶端請求過來的 schema 中的 field 若是在 GraphQL Server 中有對應的 resolver,那麼在返回數據時候,這些 field 就由對應的 resolver 的執行結果填充(支持返回 promise)。npm
這裏藉助 graphiql 面板的功能來發送請求:
看一下 http request payload 信息:
響應體:
也就是說,不管你是用你熟悉的 http lib 仍是社區的 apollo client,只要按照 GraphQL Server 要求的既定格式發請求就 ok 了。
這裏咱們使用了 GraphQL 中的 variable 語法,事實上在這種須要傳參的動態查詢場景下,咱們應該老是使用這種方式發送請求:即一個 static query + variable 的方式,而不是在運行時動態的生成 query string。這也是官方建議的最佳實踐。
假設咱們有這樣一個場景,即咱們須要取到 User Entity 下的 nick 字段,而 nick 數據並不來自於 user 接口,而是須要根據 userId 調用另外一個接口取得。這時候咱們服務端的代碼須要這樣寫。
// schema type User { id: ID! name: String age: Int nick: String }
// resolver User: { nick({ id }) { return getUserNick(id); } }
resolver 的參數列表中包含了當前所在 Entity 已有的數據,因此這裏能夠直接在函數的入參裏取到已查詢出來的 userId。
看下效果:
服務端的請求:
能夠看到,這裏多出了查詢 nick 的請求。也就是說,GraphQL Server 只有在客戶端提交了包含相應字段的 query 時,纔會真正去發送相應的請求。更多 resolver 說明能夠看這裏。
在真實的生產環境中,咱們一般會有更多更復雜的場景,好比接口的權限認證、分頁、緩存、批量提交、schema 模塊化等需求,好在社區都有相對應的一些解決方案,這不是本文的重點因此不在這裏一一介紹了,有興趣的能夠去看下我以前寫的 graphql-server-startkit,或者官方的 demo。
若是你真實的使用過 Apollo GraphQL,你會經歷以下過程:
定義一個 schema 用於描述查詢入口
// schema.graphql type User { id: ID! name: String nick: String age: Int gender: String } type Query { user(id: ID!): User } schema { query: Query }
編寫 resolver 解析對應類型
const resolvers = { Query: { user(root, { id }) { return getUser(id); } }, User: { nick({ id }) { return getUserNick(id); } } };
編寫客戶端請求代碼調用 GraphQL 接口,一般咱們會封裝一個 get 方法
function getUser(id) { // 以 axios 爲例 return axios.post('/graphql', { query: 'query userQuery($id: ID!) {↵ user(id: $id) {↵ id↵ name↵ nick↵ }↵}', operationName: "userQuery", variables: {id}}); }
若是你的項目中加入了靜態類型系統,那麼你的代碼可能就會變成這樣:
// 以 ts 爲例 interface User { id: number name: string nick: string age: number gender: string } function getUser(id: number): User { return axios.post('/graphql', { query: 'query userQuery($id: ID!) {↵ user(id: $id) {↵ id↵ name↵ nick↵ }↵}', operationName: "userQuery", variables: {id}}); }
寫到這裏你可能已經發現,不只是 entity 類型定義,就鏈接口的封裝,咱們在服務端和客戶端都重複了一遍(雖然一個用的 GraphQL Type Language 一個用的 TS)… 這仍是最簡單的場景,若是業務模型複雜起來,你在兩端須要重複的代碼會更多(好比類型的嵌套定義和 resolve)。這時候你可能會想起 DRY 原則,而後開始思考有沒有什麼方式可使得類型及接口定義能兩端複用,或者根據一端的定義自動生成另外一端的代碼?甚至你開始懷疑,到底有沒有引入 GraphQL 的必要?
GraphQL 做爲一個標準化並自帶類型系統的 API Layer,其工程價值我也再也不過多廣告了。只是在實踐過程當中,既然咱們沒法徹底避免服務端與客戶端的實體與接口定義重複(使用 apollo-codegen 能夠避免一部分),並且對於大部分小團隊而言,運維一個 productive nodejs system 實際上都是力有未逮。那麼咱們是否是能夠考慮在純客戶端構建一個類 GraphQL 的 API Layer 呢?這樣既能夠有效的避免編碼重複,也能大大的下降對團隊的要求,可操做的空間也比增長一個 nodejs 中間層大得多。
咱們能夠回憶一下,一般對於一個前端而言,促使咱們須要一個 API Layer 的緣由是什麼:
一般狀況下,碰到這些問題,你可能去跟後端同窗力排衆議,要求他們提供調用體驗更良好設計更優雅的接口。沒錯這很好,畢竟爲了追求完美去跟各類人撕(跟後端撕、跟產品撕、跟UI撕)是一個前端工程師基本的職業素養。可是若是你天天都被撕逼弄得心力交瘁,甚至是你根本找不到撕的對象(好比數據來源接口來着幾個不一樣部門,甚至是一些祖傳的沒人敢動的接口),這些時候大概就是你迫切但願有一個 API Layer 的時候了。
其實很簡單,你只須要在客戶端把 Apollo Server 中要寫的 resolvers 寫一遍,而後配上一些性能提高手段(如緩存等),你的 API Layer 就完成了。
好比咱們在src
下新建一個 loaders/apis
目錄,全部的數據拉取接口都放在這裏。好比這樣:
// UserLoader.ts export interface User { id: number name: string nick: string } export default class UserLoader { async getUser(id: number): User { const base = await Promise.all([http.get('//xxx.com/users/${id}'), this.getUserNick(id)]); const user = base.reduce((acc, info) => ({...acc, ...info}), {}); return user; } getUserNick(id: number): string { return http.get(`//xxx.com/nicks/${id}`); } }
而後在你業務須要的地方注入相應 loader 調用接口便可,如:
import { inject } from 'mmlpx'; import UserLoader from './UserLoader'; // Controller.ts export default class Controller { @inject(UserLoader) userLoader = null; async doSomething() { // ... const user = await this.userLoader.getUser(this.id); // ... } }
若是你不喜歡依賴注入的方式,loaders/apis 層直接 export function getUser
也能夠。
若是你碰到了上面描述的第 三、4 、5 三種問題,你可能還須要在這一層作一下數據格式化。好比這樣:
async getUser(id: number): User { const base = await Promise.all([http.get('//xxx.com/users/${id}'), this.getUserNick(id)]); const user = base.reduce((acc, info) => ({...acc, ...info}), {}); return { id: user.id, name: user.user_name, // 重命名字段 nick: user.nick.userNick // 剔除原始數據中無心義的層次結構 }; }
通過這一層的數據處理,咱們就能確保咱們的應用運行在前端本身定義的數據模型之下。這樣以後後端接口不管是數據結構仍是字段名的變動,咱們只須要在這一層作簡單調整便可,而不會影響到咱們上層的業務及視圖。相應的,咱們的業務層邏輯再也不會直接對接接口 url,而是將其隱藏在 API Layer 下,這樣不只能提高業務代碼的可讀性,也能作到眼不見爲淨。。。
熟悉 GraphQL 的同窗可能會很快意識到,我這不過是在客戶端作了一個簡單的 API 封裝嘛,並不能解決在 GraphQL 出現以前的 lots of roundtrips 及 overfetching 問題。但事實上是 roundtrip 的問題咱們能夠經過客戶端緩存來緩解(若是你用的是 axios 你可能須要 axios-extensions ),並且 roundtrip 的問題其實本質上咱們不過是將客戶端的 http 開銷轉移到服務端了而已。在客戶端與服務端均不考慮緩存的狀況,客戶端反而會少一個請求。。。overfetching 問題則取決於 backend service 的粒度,若是 endpoint 不夠 micro,即使是 GraphQL,也會出現接口數據冗餘問題,畢竟 GraphQL 不生產數據,它只是數據的搬運工。。。而若是 endpoint 粒度足夠小,那麼我在客戶端 API 層多開幾個接口(換成 Apollo 也要多寫幾個 resolver),同樣能夠按需取數據。服務端 API Layer 只有一個不可替代的優點就是,若是咱們的數據源接口是不支持跨域或者僅內網可見的,那麼就只能在服務端開個口子作代理了。另一個優點就是,GraphQL Server 的 http 開銷是可控的,畢竟機器是咱們本身控制,而客戶端的環境則不可控(http 開銷受終端設備及網絡環境影響,好比低版本瀏覽器或者低速網絡,均會致使 http 開銷的性能權重增大)。
可能有同窗會說,服務端 API Layer 部署一次任何系統均可以共享其服務,而客戶端 API Layer 的做用域只在某一項目。其實,若是咱們把某一項目須要共享的 API Layer 打成一個 npm 包發佈出去,不也能達到一樣的效果嗎,不少平臺的 js sdk 不都是這個思路麼(這裏只討論 web 開發範疇)。
在我看來,不論你是否會搭建一個服務端的 API Layer,咱們其實都須要有一個客戶端 API Layer 從數據源頭來保證客戶端數據的模型統一及一致性,從而有足夠的能力應對接口的變遷。若是你考慮的再遠一點,在 API Layer 服務的業務模型層,咱們一樣須要有一套獨立的 Service/Model Layer 來應對視圖框架的變遷。這個暫且按下不表,後面會再寫篇文字來詳細說一下個人思路。
事實上,對於大部分團隊而言,客戶端 API Layer 已經夠用了,增長一層 GraphQL 並非那麼必要。並且若是沒有很好的支持將客戶端接口轉換成 GraphQL Schema 和 resolver 的工具時,咱們並不能很愉快的 coding,畢竟兩端重複的工做仍是有點多。