摘要:在中應用的思考原文發表在簡介熟悉的同學可直接跳過這一章,從實踐一章看起。這也是官方建議的最佳實踐。也就是說,只有在客戶端提交了包含相應字段的時,才會真正去發送相應的請求。在客戶端與服務端均不考慮緩存的情況,客戶端反而會少一個請求。。。
Apollo GraphQL 在 webapp 中應用的思考
原文發表在: https://github.com/kuitos/kui...
簡介熟悉 Apollo GraphQL 的同學可直接跳過這一章,從 實踐 一章看起。
GraphQL 作為 FaceBook 2015年推出的 API 定義/查詢 語言,在歷經了兩年的發展之后,社區已相對發達和完善。對于 GraphQL 的一些基礎概念,本文不再一一贅述,目前社區相關的文章已經很多,有興趣的同學可以去 google,或者直接看GraphQL 官方教程 Apollo GraphQL Server 官方文檔。
而 Apollo GraphQL 作為目前社區最流行的 GraphQL 解決方案提供商,提供了從 client 到 server 的一整套完整的工具鏈。在這里我也準備以 Apollo 為例,通過一步步搭建 Apollo GraphQL Server 的方式,來給大家展示 GraphQL 的特點,以及我的一些思考(主要是我的思考?)。
setup創建基于 express 的 GraphQL server
// 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 了。
注意我們這里使用了 apollo-server-express 提供的 graphiqlExpress 插件,graphiql 是一個用于瀏覽器端調試 graphql 接口的 GUI 工具。服務啟動后,我們在瀏覽器打開 http://localhost:8080/graphiql就可以看到這樣一個頁面
定義 API schema我們在 server.js 中定義了這樣一個 endpoint : app.use("/graphql", graphqlExpress({ schema }));
這里傳入的 schema 是什么呢?它大概長這樣:
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)。
客戶端請求這里借助 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 的原因是什么:
后端接口設計不夠 restful,命名垃圾,用的時候看見那個*一樣的 url 就難受。
后端同學只愿意寫 microservice,提供聚合服務的 web api 被認為沒有技術含量,不愿意寫。你需要一個數據,他告訴你需要調 a、b、c 三個接口,然后根據 id 組裝合并。
接口返回的數據格式各種嵌套及不合理,不是前端想要的結構。
接口返回的數據字段命名隨意或者風格不統一,我有強迫癥用這種接口會發瘋。
后端返回的 數據格式/字段名 一旦變了,前端視圖綁定部分的代碼需要修改。
通常情況下,碰到這些問題,你可能去跟后端同學據理力爭,要求他們提供調用體驗更良好設計更優雅的接口。沒錯這很好,畢竟為了追求完美去跟各種人撕(跟后端撕、跟產品撕、跟UI撕)是一個前端工程師基本的職業素養。但是如果你每天都被撕逼弄得心力交瘁,甚至是你根本找不到撕的對象(比如數據來源接口來著幾個不同部門,甚至是一些祖傳的沒人敢動的接口),這些時候大概就是你迫切希望有一個 API Layer 的時候了。
如何在客戶端實現一個 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("http://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 也可以。
如果你碰到了上面描述的第 3、4 、5 三種問題,你可能還需要在這一層做一下數據格式化。比如這樣:
async getUser(id: number): User { const base = await Promise.all([http.get("http://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,畢竟兩端重復的工作還是有點多。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92152.html
摘要:前言兩篇文章學完了基礎篇原理篇,接下去便是實踐的過程,這個實踐我們使用了如下技術棧去實現一套任務管理系統,源碼就不公開了等穩定后再發布。后續我所在的公司網關團隊會持續實踐,爭取貢獻出更多的解決方案。前言 兩篇文章學完了GraphQL(基礎篇, 原理篇),接下去便是實踐的過程,這個實踐我們使用了如下技術棧去實現一套任務管理系統,源碼就不公開了, 等穩定后再發布。效果如下: showImg(ht...
摘要:開發者體驗可以幫助團隊更快地實現功能上線,因為它對開發者的體驗非常好。可以顯示每個的埋點指標,可以幫忙你定位錯誤,可以分析中請求的每個字段的分布頻率。產品案例雖然規范是由在年公布的,但是自年以來,就是移動應用開發的重要組成部分。 在大前端應用的開發過程中,如何管理好數據是一件很有挑戰的事情。后端工程師需要聚合來自多個數據源的數據,再分發到大前端的各個端中,而大前端工程師需要在實現用戶體...
摘要:然而,盡管使用有諸多好處,但邁出第一步可能并不容易。為了簡化初始教程,我們今天只構建一個簡單的列表視圖。是我們將在本教程系列中使用的客戶端的名稱。我們將列表組件命名為。在本教程的其余部分中,你將了解到我們構建一個真正的通信應用的基礎。 首發于眾成翻譯 Part 1——前端:使用 Apollo 聲明式地請求和 mock 數據 showImg(http://p0.qhimg.com/t0...
摘要:初始化項目使用初始化項目安裝項目結構如下接口所有接口對封裝接下來對進行封裝,加上中間件實現類似于攔截器的效果。 Graphql嘗鮮 在只學習graphql client端知識的過程中,我們常常需要一個graphql ide來提示graphql語法,以及實現graphql的server端來進行練手。graphql社區提供了graphiql讓我們使用 graphiql (npm):一個交互...
摘要:關注業務,而不是技術將數據需求放在它們所屬的客戶端。技術棧中的每一部分都起著作用技術棧中所有部分之間的協作可以借助緩存來完成。現在,我們來看看另一個貫穿整個技術棧的功能的例子。你可以認為是首個內置細粒度查看的技術。 本文整理自2017年 GraphQL 峰會上的演講,詳述緩存、追蹤、模式拼接和 GraphQL 未來發展等有關話題。 Facebook 開源 GraphQL 至今已兩年有余...
閱讀 1668·2023-04-26 00:30
閱讀 3145·2021-11-25 09:43
閱讀 2868·2021-11-22 14:56
閱讀 3183·2021-11-04 16:15
閱讀 1137·2021-09-07 09:58
閱讀 2014·2019-08-29 13:14
閱讀 3101·2019-08-29 12:55
閱讀 982·2019-08-29 10:57