前后端分離并不是什么新鮮事,到處都是前后端分離的實(shí)踐。然而一些歷史項(xiàng)目在從一體化 Web 設(shè)計(jì)轉(zhuǎn)向前后端分離的架構(gòu)時(shí),仍然不可避免的會(huì)遇到各種各樣的問題。由于層出不窮的問題,甚至?xí)袌F(tuán)隊(duì)質(zhì)疑,一體化好好的,為什么要前后端分離?
說到底,并不是前后分離不好,只是可能不適合,或者說……設(shè)計(jì)思維還沒有轉(zhuǎn)變過來……
一體式 Web 架構(gòu)示意
前后分離式 Web 架構(gòu)示意
比為什么要前后端分離更現(xiàn)實(shí)的問題是什么時(shí)候需要前后端分離,即前后端分離的應(yīng)用場景。
說起這個(gè)問題,我想到了 2011 年左右,公司在以 .NET 開發(fā)團(tuán)隊(duì)為主的基礎(chǔ)上擴(kuò)展了 Java 團(tuán)隊(duì),兩個(gè)團(tuán)隊(duì)雖然是在做不同的產(chǎn)品,但是仍然存在大量重復(fù)性的開發(fā),比如用 ASP.NET WebPage 寫了組織機(jī)構(gòu)相關(guān)的頁面,用 JSP 又要再寫一遍。在這種情況下,團(tuán)隊(duì)就開始思考這樣一個(gè)方案:如果前端實(shí)現(xiàn)與后端技術(shù)無關(guān),那頁面呈現(xiàn)的部分就可以共用,不同的后端技術(shù)只需要實(shí)現(xiàn)后端業(yè)務(wù)邏輯就好。
方案根本要解決的問題是把數(shù)據(jù)和頁面剝離開來。應(yīng)對(duì)這種需求的技術(shù)是現(xiàn)成的,前端采用靜態(tài)網(wǎng)頁相關(guān)的技術(shù),HTML + CSS + JavaScript,通過 AJAX 技術(shù)調(diào)用后端提供的業(yè)務(wù)接口。前后端協(xié)商好接口方式通過 HTTP 提供,統(tǒng)一使用 POST 謂詞。接口數(shù)據(jù)結(jié)構(gòu)使用 XML 實(shí)現(xiàn),前端 jQuery 解析 XML 很方便,后端對(duì) XML 的處理工具就更多了……后來由于后端 JSON庫(比如 Newtonsoft JSON.NET、jackson、Gson 等)崛起,前端處理 JSON 也更容易(JSON.parse()
和 JSON.stringify()
),就將數(shù)據(jù)結(jié)構(gòu)換成了 JSON 實(shí)現(xiàn)。
這種架構(gòu)從本質(zhì)上來說就是 SOA(面向服務(wù)的架構(gòu))。當(dāng)后端不提供頁面,只是純粹的通過 Web API 來提供數(shù)據(jù)和業(yè)務(wù)交互能力之后,Web 前端就成了純粹的客戶端角色,與 WinForm、移動(dòng)終端應(yīng)用屬于同樣的角色,可以把它們合在一起,統(tǒng)稱為前端。以前的一體化架構(gòu)需要定制頁面來實(shí)現(xiàn) Web 應(yīng)用,同時(shí)又定義一套 WebService/WSDL 來對(duì) WinForm 和移動(dòng)終端提供服務(wù)。轉(zhuǎn)換為新的架構(gòu)之后,可以統(tǒng)一使用 Web API 形式為所有類型的前端提供服務(wù)。至于某些類型的前端對(duì)這個(gè) Web API 進(jìn)行的 RPC 封裝,那又是另外一回事了。
通過這樣的架構(gòu)改造,前后端實(shí)際就已經(jīng)分離開了。拋開其它類型的前端不提,這里只討論 Web 前端和后端。由于分離,Web 前端在開發(fā)的時(shí)候壓根不需要了解后端是用的什么技術(shù),只需要后端提供了什么樣的接口可以用來做什么事情就好,什么 C#/ASP.NET、Java/JEE、數(shù)據(jù)庫……這些技術(shù)可以統(tǒng)統(tǒng)不去了解。而后端的 .NET 團(tuán)隊(duì)和 Java 團(tuán)隊(duì)也脫離了邏輯無關(guān)的美學(xué)思維,不需要面對(duì)美工精細(xì)的界面設(shè)計(jì)約束,也不需要在思考邏輯實(shí)現(xiàn)的同時(shí)還要去考慮頁面上怎么布局的問題,只需要處理自己擅長的邏輯和數(shù)據(jù)就好。
前后端分離之后,兩端的開發(fā)人員都輕松不少,由于技術(shù)和業(yè)務(wù)都更專注,開發(fā)效率也提高了。分離帶來的好處漸漸體現(xiàn)出來:
前端傾向于呈現(xiàn),著重處理用戶體驗(yàn)相關(guān)的問題;后端則傾處于業(yè)務(wù)邏輯、數(shù)據(jù)處理和持久化等。在設(shè)計(jì)清晰的情況下,后端只需要以數(shù)據(jù)為中心對(duì)業(yè)務(wù)處理算法負(fù)責(zé),并按約定為前端提供 API 接口;而前端使用這些接口對(duì)用戶體驗(yàn)負(fù)責(zé)。
前端可以不用了解后端技術(shù),也不關(guān)心后端具體用什么技術(shù)來實(shí)現(xiàn),只需要會(huì) HTML/CSS/JavaScript 就能入手;而后端只需要關(guān)心后端開發(fā)技術(shù),除了省去學(xué)習(xí)前端技術(shù)的麻煩,連 Web 框架的學(xué)習(xí)研究都只需要關(guān)注 Web API 就好,而不用去關(guān)注基于頁面視圖的 MVC 技術(shù)(并不是說不需要 MVC,Web API 的接口部分的數(shù)據(jù)結(jié)構(gòu)呈現(xiàn)也是 View),不用考慮特別復(fù)雜的數(shù)據(jù)組織和呈現(xiàn)。
前端可以根據(jù)用戶不同時(shí)期的體驗(yàn)需求迅速改版,后端對(duì)此毫無壓力。同理,后端進(jìn)行的業(yè)務(wù)邏輯升級(jí),數(shù)據(jù)持久方案變更,只要不影響到接口,前端可以毫不知情。當(dāng)然如果需求變更引起接口變化的時(shí)候,前后端又需要坐在一起同步信息了。
后端只提供 API 服務(wù),不考慮頁面呈現(xiàn)的問題。實(shí)現(xiàn) SOA 架構(gòu)的 API 可以服務(wù)于各種前端,而不僅僅是 Web 前端,可以做到一套服務(wù),各端使用;同時(shí)對(duì)于前端來說,不依賴后端技術(shù)的前端部分可以獨(dú)立部署,也可以應(yīng)于 Hybrid 架構(gòu),嵌入各種“殼”(比如 Electron、Codorva 等),迅速實(shí)現(xiàn)多終端。
任何技術(shù)方案都不是銀彈,前后分離不僅帶來好處,也帶來矛盾。我們?cè)趯?shí)踐初期,由于前端團(tuán)隊(duì)力量相對(duì)薄弱,同時(shí)按照慣例,所有業(yè)務(wù)處理幾乎都是由后端(原來的技術(shù)骨干)來設(shè)計(jì)和定義的,前端處理過程中常常發(fā)現(xiàn)接口定義不符合用戶操作流程,AJAX 異步請(qǐng)求過多等問題。畢竟后端思維和前端思維還是有所不同——前端思維傾向于用戶體驗(yàn),而后端思維則更傾向于業(yè)務(wù)的技術(shù)實(shí)現(xiàn)。
除此之外,前后分離在安全性上的要求也略有不同。由于前后分離本質(zhì)上是一種 SOA 架構(gòu),所以在授權(quán)上也需要按 SOA 架構(gòu)的方式來思考。Cookie/Session 的方式雖然可用,但并不是特別合適,相對(duì)來說,基于 Token 的認(rèn)證則更適合一些。采用基于 Token 的認(rèn)證就意味著后端的認(rèn)證部分需要重寫……后端當(dāng)然不想重寫,于是會(huì)將皮球踢給前端來讓前端想辦法實(shí)現(xiàn)基于 Cookie/Session 的認(rèn)證……于是前端開始報(bào)怨(悲劇)……
這些矛盾的出現(xiàn),歸根結(jié)底在于設(shè)計(jì)不夠清晰明確。毫無疑問,在開發(fā)過程中,主導(dǎo)者應(yīng)該是架構(gòu)師或者設(shè)計(jì)師。然而實(shí)際場景中,架構(gòu)師或者設(shè)計(jì)師往往也是開發(fā)人員,所以他們的主要技術(shù)棧會(huì)極大的影響前后端在整個(gè)項(xiàng)目中的主次作用。這位骨干處于哪端,開發(fā)的便捷性就會(huì)向哪端傾斜。這是一個(gè)不好的現(xiàn)象,但是我們不得不面對(duì)這樣的現(xiàn)狀,我相信很多不太大的團(tuán)隊(duì)也面臨著類似的問題。
如果沒有良好的流程規(guī)范,通常前端接觸的到角色會(huì)比后端更多(多數(shù)應(yīng)用型項(xiàng)目/產(chǎn)品,并非所有情況)。
換句話說,前端可以成為項(xiàng)目溝通的中心,所以比后端更合適承擔(dān)主導(dǎo)的角色。
接口分后端服務(wù)實(shí)現(xiàn)和前端調(diào)用兩個(gè)部分,技術(shù)都是成熟技術(shù),并不難,接口設(shè)計(jì)才是難點(diǎn)。前面提到前后端會(huì)產(chǎn)生一些矛盾。從前端的角度來看,重點(diǎn)關(guān)注的是用戶體驗(yàn),包括用戶在進(jìn)行業(yè)務(wù)操作時(shí)的流動(dòng)方向和相關(guān)處理;而從后端的角度來看,重點(diǎn)關(guān)注的是數(shù)據(jù)完整、有效、安全。矛盾在于雙方關(guān)注點(diǎn)不同,信息不對(duì)稱,還各有私心。解決這些矛盾的著眼點(diǎn)就是接口設(shè)計(jì)。
接口設(shè)計(jì)時(shí),其粒度的大小往往代表了前后端工作量的大?。ǚ墙^對(duì),這和整體架構(gòu)有關(guān))。接口粒度太小,前端要處理的事情就多,尤其是對(duì)各種異步處理就可能會(huì)感到應(yīng)接不暇;粒度太大,就會(huì)出現(xiàn)高耦合,降低靈活性和擴(kuò)展性,當(dāng)然這種情況下后端的工作就輕松不了。業(yè)務(wù)層面的東西涉及到具體的產(chǎn)品,這里不多做討論。這里主要討論一點(diǎn)點(diǎn)技術(shù)層面的東西。
就形式上來說,Web API 可以定義成 REST,也可以是 RPC,只要前后端商議確定下來就行。更重要的是在輸入?yún)?shù)和輸出結(jié)果上,最好一開始就有相對(duì)固定的定義,這往往取決于前端架構(gòu)或采用的 UI 框架。
常見請(qǐng)求參數(shù)的數(shù)據(jù)形式有如下一些:
而服務(wù)器響應(yīng)的數(shù)據(jù)形式就五花八門各式各樣了,通常一個(gè)完整的響應(yīng)至少需要包含狀態(tài)碼、消息、數(shù)據(jù)三個(gè)部分的內(nèi)容,其中
我們?cè)趯?shí)踐中使用 JSON 形式,最初定義了這樣一種形式
{ "code": "number", "message": "string", "data": "any"}
code
主要用于指導(dǎo)前端進(jìn)行一些特殊的操作,比如 0
表示 API 調(diào)用成功,非0
表示調(diào)用失敗,其中 1
表示需要登錄、2
表示未獲取授權(quán)……對(duì)于這個(gè)定義,前端拿到響應(yīng)之后,就可以在應(yīng)用框架層進(jìn)行一些常規(guī)處理,比如當(dāng) code
為 1
的時(shí)候,彈出登錄窗口請(qǐng)用戶在當(dāng)前頁面登錄,而當(dāng) code
為 2
的時(shí)候,則彈出消息提示并后附鏈接引導(dǎo)用戶獲取授權(quán)。
一開始這樣做并沒有什么問題,直到前端框架換用了 jQuery EasyUI。以 EasyUI 為例的好多 UI 庫都支持為組件配置數(shù)據(jù) URL,它會(huì)自動(dòng)通過 AJAX 來獲取數(shù)據(jù),但對(duì)數(shù)據(jù)結(jié)構(gòu)有要求。如果仍然采用之前設(shè)計(jì)的響應(yīng)結(jié)構(gòu),就需要為組件定義數(shù)據(jù)過濾器(filter)來處理響應(yīng)結(jié)果,這樣做寫 filter 以及為組件聲明 filter 的工作量也是不小的。為了減少這部分工作量我們決定改一改接口。
新的接口是一種可變結(jié)構(gòu),正常情況下返回 UI 需要的數(shù)據(jù)結(jié)構(gòu),出錯(cuò)的情況則響應(yīng)一個(gè)類型于原定結(jié)構(gòu)的數(shù)據(jù)結(jié)構(gòu):
{ "error": { "identity": "special identity string", "code": "number", "message": "string", "data": "any" }}
對(duì)于新響應(yīng)數(shù)據(jù)結(jié)構(gòu),前端框架只需要判斷一下是否存在 error
屬性,如果存在,檢查其 identity
屬性是否為指定的特殊值(比如某個(gè)特定的 GUID),然后再使用其 code
和 message
屬性處理錯(cuò)誤。這個(gè)錯(cuò)誤判斷過程略為復(fù)雜一些,但可以由前端應(yīng)用框架統(tǒng)一處理。
如果使用 RESTful 風(fēng)格的接口,部分狀態(tài)碼可以用 HTTP 狀態(tài)碼代替,比如 401 表示需要登錄,403 就可以表示沒有獲得授權(quán),500 表示程序處理過程中發(fā)生錯(cuò)誤。當(dāng)然,雖然 HTTP 狀態(tài)碼與 RESTful 風(fēng)格更配,但是非 RESTful 風(fēng)格也可以使用 HTTP 狀態(tài)碼來代替 error.code
。
認(rèn)證方案很多,比如 Cookie/Session 在某些環(huán)境下仍然可行、也可以使用基于 Token 和 OAuth 或者 JWT,甚至是自己實(shí)現(xiàn)基于 Token 的認(rèn)證方式。
采用傳統(tǒng)的 Cookie/Session 認(rèn)證方案并非不可行,只不過有一些限制。如果前端部分和后端部分同源,比如頁面發(fā)布在 http://domain.name/
,而 Web API 發(fā)布在 http://domain.name/api/
,這種情況下,原來的一體式 Web 方案所采用的 Cookie/Session 方案可以直接遷移過來,毫無壓力。但是如果前面發(fā)布和 API 發(fā)布不同源,這種方法處理起來就復(fù)雜了。
然后一般前后端分離的開發(fā)方式,不管是開發(fā)階段還是發(fā)布階段,不同源的可能性占絕大比例,所以認(rèn)證方案通常會(huì)使用與 Cookie 無關(guān)的方案。
目前各大網(wǎng)站的開放式接口都是 SOA 架構(gòu),如果把這些開放式接口看作提供服務(wù)方(服務(wù)端),而把使用這些開放式接口的應(yīng)用看作客戶端,那么就可以產(chǎn)生這樣一種和前后分離對(duì)應(yīng)的關(guān)系:
前端 ? 客戶端 ? (基于 OAuth 的認(rèn)證) ? 后端 ? 服務(wù)端
所以,開放式接口廣泛使用的 OAuth 方案用于前后分離是可行的,但在具體實(shí)施上卻并不是那么容易。尤其是在安全性上,由于前端是完全暴露在外的,與 OAuth 通常實(shí)施的環(huán)境(后端?服務(wù)端)相比,要注意的是首次認(rèn)證不是使用已注冊(cè)的 AppID 和 AppToken,而是使用用戶名和密碼。
雖然這個(gè)方案放在最后,但這個(gè)方案卻是目前前后端分離最適合的方案。基于 Token 的認(rèn)證方案,各種討論由來已久,而 JWT 是相對(duì)較為成熟,也得到多數(shù)人認(rèn)可的一種。從 jwt.io 上可以找到各種技術(shù)棧的 JWT 實(shí)現(xiàn),應(yīng)用起來也比較方便。
話雖如此,JWT 方案和以前使用的 Cookie/Session 在處理上還是有較大的差別,需要一定的學(xué)習(xí)成本。有人擔(dān)心 JWT 的數(shù)據(jù)量太大。這確實(shí)是一個(gè)問題,但是硬件并不貴,4G 也開始進(jìn)入不限流量階段,一般應(yīng)用中不用太在意這個(gè)問題。
前后分離之后,前端的測試將以用戶體驗(yàn)測試和集成測試為主,而后端則主要是進(jìn)行單元測試和 Web API 接口測試。與一體化的 Web 應(yīng)用相比,多了一層接口測試,這一層測試可以完全自動(dòng)化,一旦完成測試開發(fā),就能在很大程度上控制住業(yè)務(wù)處理和數(shù)據(jù)錯(cuò)誤。這樣一來,集成測試的工作量會(huì)相對(duì)單一也容易得多。
前端測試的工作相對(duì)來說減輕不了多少,前后分離之后的前端部分承擔(dān)了原來的集成測試工作。但是在假設(shè) Web API 正確的情況下進(jìn)行集成測試,工作量是可以減輕不少的,用例可以只關(guān)注前端體驗(yàn)性的問題,比如呈現(xiàn)是否正確,跳轉(zhuǎn)是否正確,用戶的操作步驟是否符合要求以及提示信息是否準(zhǔn)確等等。
對(duì)于用戶輸入有效性驗(yàn)證這部分工作在項(xiàng)目時(shí)間緊迫的情況下甚至都可以完全拋給 Web API 去處理。不管是否前后端分離,Web 開發(fā)中都有一個(gè)共識(shí):永遠(yuǎn)不要相信前端!既然后端必須保證數(shù)據(jù)的安全性和有效性,那么前端省略這一步驟并不會(huì)對(duì)后端造成什么實(shí)質(zhì)性的威脅,最多只是用戶體驗(yàn)差一點(diǎn)。但是,如果前后端都要做數(shù)據(jù)有效性驗(yàn)證,那一定要嚴(yán)格按照文檔來進(jìn)行,不然很容易出現(xiàn)前后端數(shù)據(jù)驗(yàn)證不一致的情況(這不是前后分離的問題,一體化架構(gòu)同樣存在這個(gè)問題)。
總的來說,前后分離所帶來的好處還是很明顯的。但是具體實(shí)施的時(shí)候需要一個(gè)全新的思考方式,而不是基于原有一體化 Web 開發(fā)方式來進(jìn)行思考。前后分離的開放方式將開發(fā)人員從復(fù)雜的技術(shù)組合中解放出來,大家都可以更專注于自己擅長的領(lǐng)域來進(jìn)行開發(fā),但同時(shí)也對(duì)前后端團(tuán)隊(duì)的溝通交流提出了更高的要求,前后端團(tuán)隊(duì)必須要一同設(shè)計(jì)出相對(duì)穩(wěn)定的 Web API 接口(這部分工作其實(shí)不管是否前后端分離都是少不了的,只是前后分離的架構(gòu)對(duì)此要求更高,更明確地要求接口不只存在于人的記憶中,更要文檔化、持久化)。
聯(lián)系客服