摘要:在中應(yīng)用的思考原文發(fā)表在簡介熟悉的同學(xué)可直接跳過這一章,從實踐一章看起。這也是官方建議的最佳實踐。也就是說,只有在客戶端提交了包含相應(yīng)字段的時,才會真正去發(fā)送相應(yīng)的請求。在客戶端與服務(wù)端均不考慮緩存的情況,客戶端反而會少一個請求。。。
Apollo GraphQL 在 webapp 中應(yīng)用的思考
原文發(fā)表在: https://github.com/kuitos/kui...
簡介熟悉 Apollo GraphQL 的同學(xué)可直接跳過這一章,從 實踐 一章看起。
GraphQL 作為 FaceBook 2015年推出的 API 定義/查詢 語言,在歷經(jīng)了兩年的發(fā)展之后,社區(qū)已相對發(fā)達(dá)和完善。對于 GraphQL 的一些基礎(chǔ)概念,本文不再一一贅述,目前社區(qū)相關(guān)的文章已經(jīng)很多,有興趣的同學(xué)可以去 google,或者直接看GraphQL 官方教程 Apollo GraphQL Server 官方文檔。
而 Apollo GraphQL 作為目前社區(qū)最流行的 GraphQL 解決方案提供商,提供了從 client 到 server 的一整套完整的工具鏈。在這里我也準(zhǔn)備以 Apollo 為例,通過一步步搭建 Apollo GraphQL Server 的方式,來給大家展示 GraphQL 的特點,以及我的一些思考(主要是我的思考?)。
setup創(chuàng)建基于 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}`));
執(zhí)行 node server.js,這樣我們就能啟動一個 GraphQL server 了。
注意我們這里使用了 apollo-server-express 提供的 graphiqlExpress 插件,graphiql 是一個用于瀏覽器端調(diào)試 graphql 接口的 GUI 工具。服務(wù)啟動后,我們在瀏覽器打開 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 }));
這里的關(guān)鍵是用了 graphql-tools 這個庫提供的 makeExecutableSchema 組合了 schema 定義和對應(yīng)的 resolver。resolver 是 Apollo GraphQL 工具鏈中提出的一個概念,什么用呢?就是在我們客戶端請求過來的 schema 中的 field 如果在 GraphQL Server 中有對應(yīng)的 resolver,那么在返回數(shù)據(jù)時候,這些 field 就由對應(yīng)的 resolver 的執(zhí)行結(jié)果填充(支持返回 promise)。
客戶端請求這里借助 graphiql 面板的功能來發(fā)送請求:
看一下 http request payload 信息:
響應(yīng)體:
也就是說,無論你是用你熟悉的 http lib 還是社區(qū)的 apollo client,只要按照 GraphQL Server 要求的既定格式發(fā)請求就 ok 了。
這里我們使用了 GraphQL 中的 variable 語法,事實上在這種需要傳參的動態(tài)查詢場景下,我們應(yīng)該總是使用這種方式發(fā)送請求:即一個 static query + variable 的方式,而不是在運(yùn)行時動態(tài)的生成 query string。這也是官方建議的最佳實踐。
更復(fù)雜的嵌套查詢場景假設(shè)我們有這樣一個場景,即我們需要取到 User Entity 下的 nick 字段,而 nick 數(shù)據(jù)并不來自于 user 接口,而是需要根據(jù) userId 調(diào)用另一個接口取得。這時候我們服務(wù)端的代碼需要這樣寫。
// schema type User { id: ID! name: String age: Int nick: String }
// resolver User: { nick({ id }) { return getUserNick(id); } }
resolver 的參數(shù)列表中包含了當(dāng)前所在 Entity 已有的數(shù)據(jù),所以這里可以直接在函數(shù)的入?yún)⒗锶〉揭巡樵兂鰜淼?userId。
看下效果:
服務(wù)端的請求:
可以看到,這里多出了查詢 nick 的請求。也就是說,GraphQL Server 只有在客戶端提交了包含相應(yīng)字段的 query 時,才會真正去發(fā)送相應(yīng)的請求。更多 resolver 說明可以看這里。
其他在真實的生產(chǎn)環(huán)境中,我們通常會有更多更復(fù)雜的場景,比如接口的權(quán)限認(rèn)證、分頁、緩存、批量提交、schema 模塊化等需求,好在社區(qū)都有相對應(yīng)的一些解決方案,這不是本文的重點所以不在這里一一介紹了,有興趣的可以去看下我之前寫的 graphql-server-startkit,或者官方的 demo。
實踐如果你真實的使用過 Apollo GraphQL,你會經(jīng)歷如下過程:
定義一個 schema 用于描述查詢?nèi)肟?/p>
// schema.graphql type User { id: ID! name: String nick: String age: Int gender: String } type Query { user(id: ID!): User } schema { query: Query }
編寫 resolver 解析對應(yīng)類型
const resolvers = { Query: { user(root, { id }) { return getUser(id); } }, User: { nick({ id }) { return getUserNick(id); } } };
編寫客戶端請求代碼調(diào)用 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}}); }
如果你的項目中加入了靜態(tài)類型系統(tǒng),那么你的代碼可能就會變成這樣:
// 以 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}}); }
寫到這里你可能已經(jīng)發(fā)現(xiàn),不僅是 entity 類型定義,就連接口的封裝,我們在服務(wù)端和客戶端都重復(fù)了一遍(雖然一個用的 GraphQL Type Language 一個用的 TS)… 這還是最簡單的場景,如果業(yè)務(wù)模型復(fù)雜起來,你在兩端需要重復(fù)的代碼會更多(比如類型的嵌套定義和 resolve)。這時候你可能會想起 DRY 原則,然后開始思考有沒有什么方式可以使得類型及接口定義能兩端復(fù)用,或者根據(jù)一端的定義自動生成另一端的代碼?甚至你開始懷疑,到底有沒有引入 GraphQL 的必要?
思考GraphQL 作為一個標(biāo)準(zhǔn)化并自帶類型系統(tǒng)的 API Layer,其工程價值我也不再過多廣告了。只是在實踐過程中,既然我們無法完全避免服務(wù)端與客戶端的實體與接口定義重復(fù)(使用 apollo-codegen 可以避免一部分),而且對于大部分小團(tuán)隊而言,運(yùn)維一個 productive nodejs system 實際上都是力有未逮。那么我們是不是可以考慮在純客戶端構(gòu)建一個類 GraphQL 的 API Layer 呢?這樣既可以有效的避免編碼重復(fù),也能大大的降低對團(tuán)隊的要求,可操作的空間也比增加一個 nodejs 中間層大得多。
我們可以回憶一下,通常對于一個前端而言,促使我們需要一個 API Layer 的原因是什么:
后端接口設(shè)計不夠 restful,命名垃圾,用的時候看見那個*一樣的 url 就難受。
后端同學(xué)只愿意寫 microservice,提供聚合服務(wù)的 web api 被認(rèn)為沒有技術(shù)含量,不愿意寫。你需要一個數(shù)據(jù),他告訴你需要調(diào) a、b、c 三個接口,然后根據(jù) id 組裝合并。
接口返回的數(shù)據(jù)格式各種嵌套及不合理,不是前端想要的結(jié)構(gòu)。
接口返回的數(shù)據(jù)字段命名隨意或者風(fēng)格不統(tǒng)一,我有強(qiáng)迫癥用這種接口會發(fā)瘋。
后端返回的 數(shù)據(jù)格式/字段名 一旦變了,前端視圖綁定部分的代碼需要修改。
通常情況下,碰到這些問題,你可能去跟后端同學(xué)據(jù)理力爭,要求他們提供調(diào)用體驗更良好設(shè)計更優(yōu)雅的接口。沒錯這很好,畢竟為了追求完美去跟各種人撕(跟后端撕、跟產(chǎn)品撕、跟UI撕)是一個前端工程師基本的職業(yè)素養(yǎng)。但是如果你每天都被撕逼弄得心力交瘁,甚至是你根本找不到撕的對象(比如數(shù)據(jù)來源接口來著幾個不同部門,甚至是一些祖?zhèn)鞯臎]人敢動的接口),這些時候大概就是你迫切希望有一個 API Layer 的時候了。
如何在客戶端實現(xiàn)一個 API Layer其實很簡單,你只需要在客戶端把 Apollo Server 中要寫的 resolvers 寫一遍,然后配上一些性能提升手段(如緩存等),你的 API Layer 就完成了。
比如我們在src下新建一個 loaders/apis 目錄,所有的數(shù)據(jù)拉取接口都放在這里。比如這樣:
// 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}`); } }
然后在你業(yè)務(wù)需要的地方注入相應(yīng) loader 調(diào)用接口即可,如:
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 三種問題,你可能還需要在這一層做一下數(shù)據(jù)格式化。比如這樣:
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 // 剔除原始數(shù)據(jù)中無意義的層次結(jié)構(gòu) }; }
經(jīng)過這一層的數(shù)據(jù)處理,我們就能確保我們的應(yīng)用運(yùn)行在前端自己定義的數(shù)據(jù)模型之下。這樣之后后端接口不論是數(shù)據(jù)結(jié)構(gòu)還是字段名的變更,我們只需要在這一層做簡單調(diào)整即可,而不會影響到我們上層的業(yè)務(wù)及視圖。相應(yīng)的,我們的業(yè)務(wù)層邏輯不再會直接對接接口 url,而是將其隱藏在 API Layer 下,這樣不僅能提升業(yè)務(wù)代碼的可讀性,也能做到眼不見為凈。。。
總結(jié)熟悉 GraphQL 的同學(xué)可能會很快意識到,我這不過是在客戶端做了一個簡單的 API 封裝嘛,并不能解決在 GraphQL 出現(xiàn)之前的 lots of roundtrips 及 overfetching 問題。但事實上是 roundtrip 的問題我們可以通過客戶端緩存來緩解(如果你用的是 axios 你可能需要 axios-extensions ),而且 roundtrip 的問題其實本質(zhì)上我們不過是將客戶端的 http 開銷轉(zhuǎn)移到服務(wù)端了而已。在客戶端與服務(wù)端均不考慮緩存的情況,客戶端反而會少一個請求。。。overfetching 問題則取決于 backend service 的粒度,如果 endpoint 不夠 micro,即便是 GraphQL,也會出現(xiàn)接口數(shù)據(jù)冗余問題,畢竟 GraphQL 不生產(chǎn)數(shù)據(jù),它只是數(shù)據(jù)的搬運(yùn)工。。。而如果 endpoint 粒度足夠小,那么我在客戶端 API 層多開幾個接口(換成 Apollo 也要多寫幾個 resolver),一樣可以按需取數(shù)據(jù)。服務(wù)端 API Layer 只有一個不可替代的優(yōu)勢就是,如果我們的數(shù)據(jù)源接口是不支持跨域或者僅內(nèi)網(wǎng)可見的,那么就只能在服務(wù)端開個口子做代理了。另外一個優(yōu)勢就是,GraphQL Server 的 http 開銷是可控的,畢竟機(jī)器是我們自己控制,而客戶端的環(huán)境則不可控(http 開銷受終端設(shè)備及網(wǎng)絡(luò)環(huán)境影響,比如低版本瀏覽器或者低速網(wǎng)絡(luò),均會導(dǎo)致 http 開銷的性能權(quán)重增大)。
可能有同學(xué)會說,服務(wù)端 API Layer 部署一次任何系統(tǒng)都可以共享其服務(wù),而客戶端 API Layer 的作用域只在某一項目。其實,如果我們把某一項目需要共享的 API Layer 打成一個 npm 包發(fā)布出去,不也能達(dá)到同樣的效果嗎,很多平臺的 js sdk 不都是這個思路么(這里只討論 web 開發(fā)范疇)。
在我看來,不論你是否會搭建一個服務(wù)端的 API Layer,我們其實都需要有一個客戶端 API Layer 從數(shù)據(jù)源頭來保證客戶端數(shù)據(jù)的模型統(tǒng)一及一致性,從而有足夠的能力應(yīng)對接口的變遷。如果你考慮的再遠(yuǎn)一點,在 API Layer 服務(wù)的業(yè)務(wù)模型層,我們同樣需要有一套獨(dú)立的 Service/Model Layer 來應(yīng)對視圖框架的變遷。這個暫且按下不表,后面會再寫篇文字來詳細(xì)說一下我的思路。
事實上,對于大部分團(tuán)隊而言,客戶端 API Layer 已經(jīng)夠用了,增加一層 GraphQL 并不是那么必要。而且如果沒有很好的支持將客戶端接口轉(zhuǎn)換成 GraphQL Schema 和 resolver 的工具時,我們并不能很愉快的 coding,畢竟兩端重復(fù)的工作還是有點多。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/92152.html
摘要:前言兩篇文章學(xué)完了基礎(chǔ)篇原理篇,接下去便是實踐的過程,這個實踐我們使用了如下技術(shù)棧去實現(xiàn)一套任務(wù)管理系統(tǒng),源碼就不公開了等穩(wěn)定后再發(fā)布。后續(xù)我所在的公司網(wǎng)關(guān)團(tuán)隊會持續(xù)實踐,爭取貢獻(xiàn)出更多的解決方案。前言 兩篇文章學(xué)完了GraphQL(基礎(chǔ)篇, 原理篇),接下去便是實踐的過程,這個實踐我們使用了如下技術(shù)棧去實現(xiàn)一套任務(wù)管理系統(tǒng),源碼就不公開了, 等穩(wěn)定后再發(fā)布。效果如下: showImg(ht...
摘要:開發(fā)者體驗可以幫助團(tuán)隊更快地實現(xiàn)功能上線,因為它對開發(fā)者的體驗非常好??梢燥@示每個的埋點指標(biāo),可以幫忙你定位錯誤,可以分析中請求的每個字段的分布頻率。產(chǎn)品案例雖然規(guī)范是由在年公布的,但是自年以來,就是移動應(yīng)用開發(fā)的重要組成部分。 在大前端應(yīng)用的開發(fā)過程中,如何管理好數(shù)據(jù)是一件很有挑戰(zhàn)的事情。后端工程師需要聚合來自多個數(shù)據(jù)源的數(shù)據(jù),再分發(fā)到大前端的各個端中,而大前端工程師需要在實現(xiàn)用戶體...
摘要:然而,盡管使用有諸多好處,但邁出第一步可能并不容易。為了簡化初始教程,我們今天只構(gòu)建一個簡單的列表視圖。是我們將在本教程系列中使用的客戶端的名稱。我們將列表組件命名為。在本教程的其余部分中,你將了解到我們構(gòu)建一個真正的通信應(yīng)用的基礎(chǔ)。 首發(fā)于眾成翻譯 Part 1——前端:使用 Apollo 聲明式地請求和 mock 數(shù)據(jù) showImg(http://p0.qhimg.com/t0...
摘要:初始化項目使用初始化項目安裝項目結(jié)構(gòu)如下接口所有接口對封裝接下來對進(jìn)行封裝,加上中間件實現(xiàn)類似于攔截器的效果。 Graphql嘗鮮 在只學(xué)習(xí)graphql client端知識的過程中,我們常常需要一個graphql ide來提示graphql語法,以及實現(xiàn)graphql的server端來進(jìn)行練手。graphql社區(qū)提供了graphiql讓我們使用 graphiql (npm):一個交互...
摘要:關(guān)注業(yè)務(wù),而不是技術(shù)將數(shù)據(jù)需求放在它們所屬的客戶端。技術(shù)棧中的每一部分都起著作用技術(shù)棧中所有部分之間的協(xié)作可以借助緩存來完成。現(xiàn)在,我們來看看另一個貫穿整個技術(shù)棧的功能的例子。你可以認(rèn)為是首個內(nèi)置細(xì)粒度查看的技術(shù)。 本文整理自2017年 GraphQL 峰會上的演講,詳述緩存、追蹤、模式拼接和 GraphQL 未來發(fā)展等有關(guān)話題。 Facebook 開源 GraphQL 至今已兩年有余...
閱讀 1683·2023-04-26 00:30
閱讀 3155·2021-11-25 09:43
閱讀 2884·2021-11-22 14:56
閱讀 3194·2021-11-04 16:15
閱讀 1155·2021-09-07 09:58
閱讀 2028·2019-08-29 13:14
閱讀 3113·2019-08-29 12:55
閱讀 993·2019-08-29 10:57