為什么不行?
在設(shè)計(jì)通用 API 時(shí),你會(huì)遇到一系列鬧心的問(wèn)題:
如何預(yù)測(cè)和支持所有可能的工作流程?
如何避免某些蹩腳的工作流程中的 N+1 問(wèn)題?
如何測(cè)試每個(gè)可能出現(xiàn)的請(qǐng)求的功能、性能和安全性?
如何在不破壞現(xiàn)有工作流程的情況下修改某個(gè) API?
如何根據(jù)內(nèi)部和社區(qū)的需求,劃分修改 API 的優(yōu)先級(jí)?
如何完善文檔,以方便各方順利完成工作?
從前端的角度來(lái)看,還有更多問(wèn)題需要考慮:
如何收集渲染頁(yè)面所需的所有數(shù)據(jù)?
如何優(yōu)化發(fā)往多個(gè)端點(diǎn)的多個(gè)請(qǐng)求?
如何避免以預(yù)期之外的方式使用 API 數(shù)據(jù)字段?
如何權(quán)衡新功能與構(gòu)建新 API 的成本?
如果只是為了前端而構(gòu)建后端,你需要考慮這么多問(wèn)題嗎?你需要考慮每一種可能的工作流程,避免 N+1 請(qǐng)求問(wèn)題,測(cè)試每個(gè)請(qǐng)求配置?還是應(yīng)該拒絕某些功能,因?yàn)槟惴浅G宄總€(gè)頁(yè)面需要呈現(xiàn)什么?看到這里,你可能明白我想說(shuō)什么了。
建議
我建議不要將前端視為某個(gè)通用 API 的客戶(hù)端,應(yīng)將其視為應(yīng)用的一半。
假設(shè)你可以把整個(gè)頁(yè)面所需的 JSON 全部發(fā)送給前端。那么只需要?jiǎng)?chuàng)建一個(gè)端點(diǎn)/page/a,然后渲染/page/a的整個(gè)JSON就可以了。而且,每個(gè)頁(yè)面都應(yīng)該采用相同的做法。不要強(qiáng)迫前端開(kāi)發(fā)人員發(fā)送一堆單獨(dú)的請(qǐng)求來(lái)渲染復(fù)雜的頁(yè)面。不要人為地制造的限制。
這個(gè) JSON 需要負(fù)責(zé)渲染整個(gè)頁(yè)面。不要渲染抽象模型和集合,而是應(yīng)該渲染具體的方框、小節(jié)、段落、列表等。渲染可視化頁(yè)面結(jié)構(gòu)。
{
“section1”: {
“topBoxTitle”: “Foo”,
“l(fā)eftBoxTitle”: “Bar”,
“l(fā)inkToClose”: “https://…”
},
“section2”: {
…
}
}
這與服務(wù)器驅(qū)動(dòng) UI 類(lèi)似,但不完全相同。我們可以稱(chēng)之為服務(wù)器通知 UI。
哪種方法更好?
看到上面那些繁雜的考慮事項(xiàng)了嗎?設(shè)計(jì)前端專(zhuān)用的 API 就不會(huì)為這些問(wèn)題鬧心了。
你可以自由決定:我想要一個(gè)頁(yè)面A。然后只需在后端和前端實(shí)現(xiàn)頁(yè)面A。非常簡(jiǎn)單。
我們無(wú)需再考慮:必須引入哪些 API 工作流程,才能成功地渲染這個(gè)頁(yè)面?頁(yè)面 A 的實(shí)現(xiàn)非常簡(jiǎn)單,只需要實(shí)現(xiàn)頁(yè)面本身的功能。你可以完整地測(cè)試頁(yè)面A,檢查 Bug、安全和性能問(wèn)題。你甚至可以通過(guò)一個(gè)大型 SQL 查詢(xún)語(yǔ)句,獲取頁(yè)面 A 所需的所有數(shù)據(jù)。你可以將頁(yè)面 A 的整個(gè) JSON 放入緩存。
前端非常清楚頁(yè)面 A 中每個(gè)字段的用途。這些字段沒(méi)有歧義,它們準(zhǔn)確地代表了前端的需求。
當(dāng)需要修改頁(yè)面 A 時(shí),你只需徑直打開(kāi)頁(yè)面 A,完成修改就行了,無(wú)需花大把時(shí)間開(kāi)會(huì)討論如何修改后端 API 才能實(shí)現(xiàn)前端的變更。這個(gè) API 只服務(wù)于頁(yè)面 A,不需要精心設(shè)計(jì)服務(wù)多個(gè)請(qǐng)求。只有這樣,我們才能擺脫自我強(qiáng)加的限制。
此外,業(yè)務(wù)邏輯可以全部交由后端負(fù)責(zé),無(wú)需在前端和后端之間的分工上浪費(fèi)精力。前端可以專(zhuān)心呈現(xiàn)頁(yè)面,而后端則可以專(zhuān)心實(shí)現(xiàn)前端所需的內(nèi)容。目標(biāo)明確,不是嗎?
如何實(shí)踐?
我曾在多個(gè)生產(chǎn)項(xiàng)目中嘗試過(guò)這種做法。其中有一個(gè)是個(gè)人的項(xiàng)目,還有一個(gè)是在公司現(xiàn)有項(xiàng)目的基礎(chǔ)之上進(jìn)行的重構(gòu)。我們整個(gè)團(tuán)隊(duì)都參與了那個(gè)項(xiàng)目,而且效果很好。我們遇到的唯一問(wèn)題就是,前端的工作越來(lái)越無(wú)聊,因?yàn)閹缀跛械臉I(yè)務(wù)邏輯都由后端負(fù)責(zé)。同時(shí),后端團(tuán)隊(duì)也沒(méi)有感覺(jué)到太大壓力。而且大多數(shù)時(shí)候,我們談?wù)摰亩际菢I(yè)務(wù)相關(guān)的內(nèi)容,而不是代碼。
當(dāng)然,很多人不太贊同這種做法,常見(jiàn)的反對(duì)意見(jiàn)如下。
我希望前端自由(或者,我希望前端解耦)!
不要自欺欺人了,通用 API 并沒(méi)有賦予前端真正的自由。為了渲染一個(gè)頁(yè)面,需要發(fā)送 7 個(gè)請(qǐng)求,這不是自由。這是為了滿(mǎn)足基本的要求而作繭自縛。一旦需求發(fā)生變化,后端也必然需要變更。這樣的自由都是偶然的,而且大多是發(fā)生在錯(cuò)誤的地方。
如果真的想讓前端團(tuán)隊(duì)自由,直接在 Postgres 上安裝一個(gè) GraphQL 包裝器,就可以了。
我們本來(lái)就需要通用API,這樣不是一箭雙雕嗎?
不,其實(shí)你沒(méi)有必要公開(kāi)這些 API。真正到了發(fā)布的時(shí)候,你可能會(huì)想:“也許我不應(yīng)該公開(kāi)這些API”。通用 API 與前端專(zhuān)用API 的變更有非常大的不同。公開(kāi) API 需要支持客戶(hù)端的工作流程。而前端專(zhuān)用 API 可以隨意變更。沒(méi)有困難,就不要制造困難了。
如果給每個(gè)頁(yè)面構(gòu)建JSON,那么邏輯代碼該如何重用?現(xiàn)在的 CRUD 控制器中大量的邏輯代碼都是重用的!
如果編程語(yǔ)言允許,完全可以重用邏輯。你可以使用 mixins、組合、繼承,以及其他任何方式。如果能建立良好的抽象,就像樂(lè)高積木一樣,只需要將積木搭建起來(lái)就可以了。
通用 API 可以在移動(dòng)應(yīng)用中重用!
通常移動(dòng)應(yīng)用都有不同的頁(yè)面,其中包含的信息和結(jié)構(gòu)不同,變更的原因也不同。專(zhuān)門(mén)針對(duì)各個(gè)頁(yè)面構(gòu)建一個(gè)后端,可以節(jié)省很多時(shí)間。
如果頁(yè)面需要部分XHR 更新怎么辦?始終返回整個(gè)頁(yè)面嗎?
創(chuàng)建一個(gè)只返回特定數(shù)據(jù)的端點(diǎn)也是沒(méi)問(wèn)題的。你完全可以針對(duì)頁(yè)面特定部分的數(shù)據(jù)建立一個(gè)端點(diǎn)。在頁(yè)面最初加載的時(shí)候渲染 React 組件,然后通過(guò)調(diào)用這些端點(diǎn)的 XHR 更新各個(gè)組件。但是,只有某些頁(yè)面有這樣的需求時(shí),才有必要引入這些端點(diǎn)。這是特殊的處理,一般不需要實(shí)現(xiàn)。
我的前端是一個(gè)SPA,只需要一部分?jǐn)?shù)據(jù),不需要整個(gè)頁(yè)面。
即便是部分?jǐn)?shù)據(jù),也可以作為部分頁(yè)面結(jié)構(gòu)提供給前端,不需要作為通用資源。只要后端能夠滿(mǎn)足前端的需求即可。
我正在構(gòu)建一個(gè)站點(diǎn)構(gòu)建器,所以前端實(shí)際上只是站點(diǎn)構(gòu)建器 API 的測(cè)試。
這是一種正確的用法,加油!
你有數(shù)據(jù)支持這種做法嗎?
我希望能夠收集到這樣的數(shù)據(jù),然而,我們很難準(zhǔn)確地衡量。誰(shuí)會(huì)同時(shí)針對(duì)一個(gè)軟件長(zhǎng)期維護(hù)兩個(gè)架構(gòu),然后比較二者的效果?本文表達(dá)的只是我個(gè)人的經(jīng)歷。