首先請原諒本文標題取的有點大,但並非為了嘩眾取寵。本文取這個標題主要有3個原因,這也是寫作本文的初衷: (1)目前國內幾乎搜索不到全面講解如何搭建前後端分離框架的文章,講前後端分離框架思想的就更少了,而筆者希望在本文中能夠全面、詳細地闡述我們團隊在前後端分離的摸索中所得到的搭建思路、最佳實踐以及架構 ...
首先請原諒本文標題取的有點大,但並非為了嘩眾取寵。本文取這個標題主要有3個原因,這也是寫作本文的初衷:
(1)目前國內幾乎搜索不到全面講解如何搭建前後端分離框架的文章,講前後端分離框架思想的就更少了,而筆者希望在本文中能夠全面、詳細地闡述我們團隊在前後端分離的摸索中所得到的搭建思路、最佳實踐以及架構思想;
(2)我們團隊所搭建的前後端分離框架,並非只是將網上傳播的知識碎片簡單拼裝,而是一開始就從全局出發,按照整個系統對前後端分離框架的最高期望進行設計,到目前為止,可以說我們的框架完全實現了對我們前後端分離的全部期望;
(3)我們在搭建過程中產生了一些創新(比如最大的創新就是API文檔伺服器的搭建),希望這些創新可以為您的團隊在前後端分離的探索中提供一些有用的思路。
本文適合的讀者對象:對軟體系統架構有一定經驗+對WEB前端/客戶端軟體開發有一定經驗+對伺服器端開發有一定經驗。
註:本文中所提的“前端”主要指WEB前端,當然在很多情況下也適用於客戶端軟體,如桌面程式、APP等。
第一章 為什麼要前後端分離
1、引用“為什麼我不喜歡「前後端分離」(個人觀點,歡迎來噴)”
首先大家可以閱讀一下中文版原文:https://www.v2ex.com/t/298014?p=4,很有意思的一篇文章,作者文筆幽默,閱讀起來很輕鬆。
文中有幾個觀點是筆者特別贊同的,比如:
(1)前後端不分離的團隊,前端工程師都是頁面仔話語權很弱,技術大牛都在後端,前端相當於給後端工程師打雜的;前端工程師晉升機會很少,薪資不高,發展前景渺茫;
(2)前後端分離後,更好招聘,團隊耦合度更低,職責更分明。
但是文中也有一些觀點是筆者不敢認同的。比如作者最終推薦【全棧工程師】,雖然筆者多年前就是一名全棧工程師,但我深知前後端分離的好處遠大於全棧工程師帶來的好處。原因有4,詳見下一小節。
2、為什麼我們團隊要採用前後端分離
我們團隊最終決定進行前後端分離改造的4個主要原因:
(1)全棧工程師很難招聘,很難培養,很多後端開發人員不願意學前端技術,而很多前端開發人員怕學後端技術;
(2)如果前後端不分離,前端工程師的工作就必須依賴後端工程師,前端工程師變成打雜的,職業生涯前景慘淡;
(3)前後端分離後,前端和後端工程師獨立開發,大大提高開發效率;
(4)綜合來講,前後端分離的用人成本遠低於全棧工程師用人成本;同時,前後端分離的工作效率遠大於耦合工作的工作效率;
然而我們團隊也是從最近才開始全面實施前後端分離的,因為筆者深知,上面4個原因所描繪的美好願景,其實際效果將會極大的取決於一個關鍵環節:API文檔伺服器。一個將就的API文檔伺服器會使前後端開發工作痛不欲生,團隊矛盾日益尖銳,最終將會使程式質量下降,然後沒有人願意維護。
筆者在2018年3月終於完成一個近乎完美的API文檔伺服器的搭建方案,該方案完全實現了我對API文檔伺服器所期望的全部特性,然後花了一個周末開發完成。
在下一章中,筆者將會詳細闡述API文檔伺服器的重要性以及分享我們團隊自創的搭建方案。
第二章 前後端分離最困難、最關鍵的環節——API文檔伺服器
1、API文檔伺服器是什麼?
請先看下圖。
正如上圖所示,前後端開發人員可以獨立開發、獨立運行、獨立調試,他們之間的介面就是通過API文檔伺服器定義的。通常,一個頁面的載入或者表單的提交,都有數據在客戶端和伺服器端之間傳輸,而API文檔伺服器就是專門用來生成API文檔的。有了API文檔,前端開發人員就可以基於API文檔產生模擬數據(mock),然後使用mock的數據完成頁面樣式和頁面交互;有了API文檔,後端開發人員就可以基於API文檔完成請求的處理以及返迴響應數據。
而API文檔伺服器(通常為WEB伺服器)是一個可以動態生成最新版本API文檔的伺服器,並支持本地維護+遠程訪問。
(如果你們的項目還在使用靜態的API文檔,比如word文檔,那你們必定經歷著巨大的痛苦)
2、為什麼API文檔伺服器是最困難、最關鍵的環節?
為什麼本文會專門寫這樣一個章節來表述API文檔伺服器的重要性呢?因為筆者認為,很多團隊在前後端分離的探索中舉步維艱,可能最重要的原因就是對API文檔的搭建方案重視不夠(因為現成的方案俯拾皆是,如word文檔做API載體)。因此,筆者希望大家看清楚前後端分離過程中最重要的一個環節,不是原型,不是設計,不是開發,也不是測試,而是API文檔的編寫/維護/發佈/閱讀。
然後讓我們來回想一下軟體開發流程中的幾個關鍵環節:
(1)產品經理提需求,畫原型;
(2)UI設計師根據原型出設計圖;
(3)測試團隊根據產品原型編寫測試用例,制定測試計劃;
(4)架構師根據原型編寫API文檔;
(5)前後端工程師基於API文檔完成業務開發;
(6)測試、改BUG、發佈。
在上面這個流程中,API文檔環節直接關係到了前端和後端兩個開發團隊,也是整個軟體開發流程中耗時最大的環節。一旦API文檔編寫不合理,或者維護太麻煩,或者閱讀不方便,將極大的影響開發效率,影響團隊士氣。而其他環節通常不會跨部門耦合,常用解決方案非常成熟,並且所消耗的資源也遠不及開發團隊,因此,API文檔環節就變成了前後端分離團隊中最關鍵的環節。
如果你曾經參與過前後端分離團隊的開發工作,那你很可能對上文描述的場景深有體會。
接下來筆者說說為什麼API文檔伺服器是最困難的環節。這也是為了讓大家做足心理準備:搭建API文檔伺服器並沒有想象那麼容易。
首先,最大的難點在於這個世界上已經有大量現成的API文檔服務方案,比如用Word/Excel來做API的載體,或者使用類似swagger這樣的框架。我們大多數架構師,包括筆者在內,的第一反應就是找現成的,然後去對比各個現成的方案,對比每個方案的實施難度以及適用度。我們(指架構師們)最終找來找去,其實也沒有找到一個完美的方案,但不得不從各個現成的方案中選擇一個,我們很少去想有沒有可能自己來搭建一個,即使有時候想著自己去搭建一個,但是往往一想到其中的困難(甚至無從下手)以及項目進度壓力,可能就淺嘗輒止了。
筆者的團隊在2018年以前的項目中也使用過word文檔和swagger,但我是一個追求完美的人,我一直沒有停止思考去搭建一個完美的API文檔伺服器方案,直到2018年3月,終於靈光一現,解決了搭建過程中的一個關鍵問題(API版本管理),於是才有了我們團隊的前後端分離之路。
其實,本文所闡述的方案對於使用者來講也可以說是現成的了,因為每個人註冊個賬號就可以使用。但是本文的目的並不只是簡單的告訴大家我們做了一個新的API文檔伺服器,而是想跟大家分享我們為什麼要做這個文檔伺服器,以及為什麼這樣做。
3、為什麼我們不用word文檔作為API文檔的載體?
用word文檔(或者Excel)算是最落後的方式了吧,其缺點很明顯:
(1)API一多,維護和閱讀就變得極其困難;
(2)每次API文檔修改之後,需要修改者自己去維護版本修改記錄,這操作是違背人性的,並且如果想要基於單個API維護版本歷史,那可以算得上違背天理了;
(3)在word裡面寫JSON格式的數據結構是很難的,如果用截圖那就極大的增加了維護成本;
說完word文檔的缺點,按照套路應該說說word文檔的優點了吧。好吧,word文檔的優點就是方便傳播、離線閱讀,但是除非你們項目的API文檔的維護頻率是按年計的,那還是早早放棄word吧。
4、為什麼我們不用swagger(及類似方案)?
考慮到本文的讀者有可能從未接觸過swagger,筆者首先得說說swagger是做什麼的,以及它的先進性(沒有一定的先進性怎麼會得到這麼多人的追捧是吧)。
swagger是一個API文檔生成框架,說白了它是一個類庫,集成到系統之後,能夠通過反射讀取後端代碼定義的API文檔,java和.NET體系下都有對應的版本。
swagger最大的先進性:不需要專門有人來編寫API文檔,自動根據代碼生成API文檔,API文檔維護成本極低。swagger還有其他一些小優點本文就不細說了,因為那些都不是大家選擇swagger的主要原因。
那swagger到底有什麼不夠完美的地方讓我們團隊最終完全放棄了swagger?
swagger之不夠完美的地方:
(1)通過swagger生成的API文檔看不見版本修改記錄,你不知道什麼時候後端開發人員悄悄改了下API文檔而忘記/有意不通知前端開發人員,這樣的鍋前端背了太多;
(2)介面文檔由後端開發人員編寫,前端開發人員的地位實際上比後端的低,並且前端開發人員仍然會經常找後端開發人員溝通修改API文檔(強依賴仍然存在);
(3)本來swagger的API定義應該由架構師或項目經理編寫,但由於後端開發人員可以直接改文檔,這導致的實際效果就是:基本上API文檔都直接由開發人員編寫(或修改),其質量水平很難達到期望;
(4)swagger生成API文檔的類的定義可能是多層引用關聯的,但是這個類又太容易被開發人員修改到,或者不小心改到,如果開發人員忘記通知前端或者只通知了部分改動,那就會造成嚴重的問題;
(5)使用swagger的語法來編寫API文檔,其實還是很麻煩的,我們希望這個語法能夠超級簡單;
(6)swagger會污染你的代碼。
關於上方的第(1)點,筆者想跟大家分享一些工作中的趣事。以前我們團隊使用swagger的時候,有時我們的java開發人員發現某個欄位的命名寫錯了,但是他以為客戶端開發人員還沒有做這個功能,就悄悄的將命名修正了。後來測試提交了BUG。。。。
本文列出的swagger的這些缺點,其實每一個都不算大,這也是這麼多團隊可以一直忍受它的緣故。swagger的這些缺點其實綜合來講,其主要的問題就在於它讓API編寫/修改太過容易,讓軟體開發過程和管理過程太容易出錯。如果你們團隊建立了嚴格的管理機制,那還是可以將swagger用的很和諧的,但這不是筆者,作為一個架構師,可以撇開責任的理由。
作為一個架構師,筆者以為一個好的框架應該讓開發人員不那麼容易犯錯,甚至杜絕了開發人員寫出錯誤的代碼。關於這一觀點,筆者會在另外的文章中以我們對hibernate的改造為例,進行更詳細的闡述:如何讓開發人員更不容易寫出錯誤的代碼是評價一個架構師能力的重要標準。
行文至此,筆者已經殘忍的批判了很多團隊的API文檔方案,如果讓您感覺不適,我只能深表歉意了。
在說其他方案的缺點時,其實筆者已經逐漸透露了我們自創的API文檔服務框架將要解決的問題了。那麼,接下來請看我們的解決方案,以及我們為什麼這麼設計。
第三章 我們自創的API文檔服務框架詳解
1、我心目中的API文檔伺服器應該是什麼樣子?
筆者在寫代碼或做架構的時候有一個思維習慣,就是不管某個問題多麼複雜具體解決方案是什麼,我會先去想想這個問題的最佳的處理方式應該是什麼樣子,然後再去想這些最佳的處理方式哪些可以實現,哪些實現不了而只能用次一點的方案,然後次一點的方案是否可以接受。
對於API文檔服務框架,在我的心中早已有了期望:
(1)編寫API文檔的語法一定要非常簡單,同時又要非常靈活,對於大多數常見API必須能夠快速編寫,對於某些特殊API,又能夠支持自定義編寫;
(2)每一個API文檔能夠非常容易的定義請求和響應數據結構,最好能夠自動生成請求和響應示例,更重要的是,生成的示例必須看起來是符合業務需求的真實數據;
(3)能夠非常方便的編寫JSON格式的數據;
(4)API文檔的發佈要非常簡單,最好能在幾秒鐘內完成;
(5)API文檔的源文件最好獨立於項目源碼,不能污染項目源碼;
(6)每次修改API文檔,最好能夠自動創建版本歷史記錄,同時要能夠非常方便的查看歷史版本;
(7)能夠通過WEB瀏覽器訪問;
(8)API數量達到成千上萬的時候,能夠呈現一個樹形目錄結構,方便閱讀和搜索;
好了,大概就這8個特性吧,下麵請看我們是如何完成的。
2、我們自創的API文檔服務框架核心工作原理
首先請看簡易框架示意圖:
這個框架搭建起來並不複雜,甚至可以說是很簡單的。所用到的技術和工具如下:
(1)使用JavaScript作為API文檔源文件的編寫語言;
(2)使用任何支持JavaScript的IDE作為API文檔編寫工具,我們團隊使用intellij idea;
(3)使用SVN伺服器作為版本管理工具,用來管理API版本;
(4)WEB伺服器可以隨便使用哪個框架搭建。
核心工作原理(4步):
1、獲取JS目錄結構。當用戶在瀏覽器中輸入API文檔服務地址(如:http://api.some-domain.com)時,WEB伺服器根據事先配置好的SVN地址和賬戶信息,從SVN伺服器獲取JS文件(即API定義源文件)目錄,然後WEB伺服器將這些JS文件的樹形目錄響應到瀏覽器(非JS文件內容,僅僅是JS目錄結構),後面當用戶點擊某個JS文件名稱時,才會載入相應JS文件內容,這樣即使當API文檔增加至上萬個,也不會太大影響載入速度。
2、載入apiHelper.js文件。在上一步響應完成後,頁面會通過script標簽載入一個特別重要的apiHelper.js 文件。這個JS文件是做什麼的呢?它會極大的簡化我們編寫API文檔的語法!首先,這個文件在window下麵定義了一個apiHelper對象,這個對象用來封裝大量的靜態方法,這些靜態方法主要是用於定義API文檔數據結構的,比如apiHelper.response.page(object)方法將會直接根據object對象生成分頁響應數據結構。
3、載入單個API。當用戶點擊某個API時,頁面會動態將該JS文件載入到瀏覽器並且執行。那麼這個JS的API文件到底執行了什麼代碼呢?其實很簡單,我們的每個JS API文件都在window下麵定義了一個api對象(當然,是按照一定數據結構定義的對象),當這個JS文件執行完成後,當前頁面的下的window.api對象已經被更新了,這時我們只需要調一個render()方法,將此window.api對象用HTML呈現出來即可。在呈現window.api對象的render方法中,我們充分利用JS這門語言的動態性及其反射機制,從而極大的簡化了API文檔的定義。
4、查看單個API的歷史版本。在載入完單個API的JS文件後,我們可以通過SVN查找該JS文件的修改記錄,然後呈現一個revision記錄列表,點擊每一個revision即可載入曾經某個版本的JS,這時window.api對象已經被替換成了歷史版本,這時我們只需要再次調用之前的render方法,將這個window.api對象重新呈現出來即可,這樣就可完成快速版本切換和智能對比。
以上就是我們這個API框架的核心工作原理了,是不是非常簡單呢!如果你擁有豐富的軟體架構經驗,相信讀到這裡你已經完全明白我們的思路並可以搭建一套類似的API文檔服務框架了。但是可能對於大多數讀者來說,讀到這裡還是有些雲里霧裡。別擔心,接下來筆者將會以我們項目中的一個API文檔為例,結合界面和代碼對核心原理進行闡述。
首先請看我們某個API的閱讀界面(包含4個tab的一個網頁):
以上4張圖片分別是我們某個API文檔的請求、響應、請求示例和響應示例的定義文檔。這個文檔就是通過對window.api對象進行解析得到的。別看這個文檔定義的內容這麼多,但這個API的源文件卻是非常簡單的,請看下圖:
數一數,大概只有二十多行代碼就完成了一個這麼複雜的API的定義!這全都得歸功於JavaScript這門動態語言的強大!
如果你仔細閱讀上方的API源文件,你可能會發現我們定義了很多特殊語法,比如下劃線“_”屬性代表當前對象的整體說明,而api對象下麵的POST屬性則同時定義了http method和請求URL。還有apiHelper.p()方法可以將一個請求/響應欄位的定義放在一行代碼裡面,其4個參數分別是:數據類型、是否可空(字元串哦)、示例數據(不僅僅支持string哦)和欄位說明。這個JS文件執行後的最終效果就是定義window.api對象,然後瀏覽器載入完該JS後,我們的render()方法就可以將其呈現出來了。
下麵,我們到瀏覽器的控制臺中來看看實際的window.api對象是長什麼樣子吧:
可以看出,這個window.api對象實際上是非常複雜的,之所以我們的API源文件這麼簡單,那是因為apiHelper.js做了大量的事情,從而簡化了API的編寫。這就是一個架構師該做的事情:將儘可能多的事情交給框架來完成,在保證可擴展性的前提下,讓開發人員寫儘量少的代碼就可以完成工作。
最後,似乎就只差網頁中的render()方法了,但是筆者並不打算將其呈現在本文中,因為它真的很簡單了並且已經遠離了本文的主旨。如果基於上文的信息你還無法完成render()方法的編寫,那麼你可以使用我們現成的解決方案(加jframe官方QQ群瞭解吧:651499479)。
最終,我們的API文檔服務框架完全實現了我們最初的期望:
(1)API文檔編寫語法超級簡單,可擴展性強;
(2)非常容易編寫真實的請求和響應示例;
(3)非常容易編寫JSON格式的數據;
(4)發佈API文檔超級簡單:提交SVN即可;
(5)API文檔的源文件完全獨立於項目源碼;
(6)修改API文檔將自動創建版本歷史記錄,在網頁中可以非常容易的查看/對比歷史版本;
(7)通過WEB瀏覽器訪問;
(8)API數量達到成千上萬的時候,能夠呈現一個樹形目錄結構,方便閱讀和搜索;
到此為止,我們的API文檔服務框架已經介紹完畢。篇幅有點長,那是因為筆者認為API文檔框架確實是前後端分離過程中最重要的一個環節,因為相比後端框架或者前端框架,API文檔框架不確定性因素更大,基於現有的開源項目很難完成高質量的API文檔框架,而不像前端或後端框架搭建過程中成熟的方案很多,完成高質量前端和後端框架相對容易很多。
第四章 我們的前後端分離框架詳解
我們的前後端分離框架主要採用瞭如下技術:
(1)使用VueJs作為前端模板引擎;
(2)使用jQuery以及我們多年積累的JS控制項作為DOM操作函數庫;
(3)後端採用java體系的spring MVC返回HTML。
接下來筆者將會解釋我們為什麼會選擇這些技術。
1、筆者對VueJs的理解
首先,筆者相信很多人對VueJs或者react到底有什麼用都不是很清楚的,因為他們的官方網站講述了很多的特性,以致讓我們分不清楚VueJs的本職工作是什麼。筆者在權衡VueJs和react的過程中,也感到非常的困惑。因為按照VueJs官網的介紹,似乎我應該建立以.vue文件為主的項目工程,然後通過編譯器將其編譯成html、css和js。並且VueJs官網還大量介紹了基於NodeJs的Vue服務端渲染、Vue Ruoter、Vue Loader、規模化,以及打包工具webpack等。看上去這是一套全新的、完整的、包含伺服器端的前端開發框架。
我相信Vue官網介紹的確實是一套全新的、完整的前端開發框架。但是,在筆者看來這套框架並不是最好的前端開發框架。對於一個不懂伺服器後端架構的前端開發人員來講,使用NodeJs搭建WEB伺服器確實是一個最優的選擇,因為NodeJs比java、.NET簡單太多了,我相信Vue官網也是基於這個原因才對伺服器端解決方案做了大量的介入。
然而我們團隊有非常成熟的java伺服器後端框架,只需要增加幾行代碼即可完成Vue官方所介紹的那一堆堆特性。筆者也是把Vue官網上面介紹的這些服務端方案讀了很久,才明白Vue官網的真正意圖,並且最終明白,Vue官網所描述的架構還沒有基於我們的java服務端增加幾行代碼所得到的架構好。
這是筆者在閱讀Vue官網時遇到的最大的一個困惑。後面最終決定:我們只把Vue當做一個HTML模板引擎,這才是Vue的本職工作。
這個決定下的並不容易,因為這會讓我們的框架看上去不那麼新潮。因為Vue官網上介紹的知識,除了把Vue當做模板引擎的知識外,其他知識我們一點都沒有用得上。
Vue官網上還有一個隱形的基礎認識沒有介紹清楚,因為筆者發現,Vue官網上幾乎全部的知識都是基於SPA(單頁應用)這個框架下進行描述的,包括webpack、Vue Ruoter等。但是我們的系統是非常龐大又複雜的,根本不可能用SPA架構。關於這一點,筆者也是讀了很久才發現,Vue官網的預設設定場景就是SPA架構。
2、為什麼我們會選擇VueJs?
前後端分離之後,前端工程師需要將通過API獲取的數據呈現到頁面上,雖然也可以通過jQuery對頁面一個一個賦值,但是這種效率太低了,或者也可通過在JavaScript中拼接HTML,但是這種方式太難維護HTML代碼了,也很難閱讀。因此最好的方式就是使用模板引擎。
前端的模板引擎跟後端模板引擎很相似,比如JSP或cshtml(razor),他們的語法都非常相似,他們所實現的功能也幾乎一樣:將數據綁定到HTML模板。VueJs和react都可以充當這樣的模板引擎。我們最終沒有選用react而是選用了VueJs的原因只有一個,那就是VueJs是真正的響應式,而react改變model之後需要手工調用setState才會更新UI,這是完全無法忍受的。
因為這個原因,我們只能選擇VueJS作為模板引擎。
3、我們的前端框架的工作原理
雖然本文寫的有點長,但我們的前端框架卻是非常簡單的,這也是我們為什麼不選擇採用.vue文件構建工程的原因,因為那太複雜了。
核心工作原理:
(1)定義頁面URL格式。我們每個頁面的地址格式大概長這樣:http://www.your-domain.com/admin#home/index。admin代表模塊名稱,#後面的home代表子模塊對應controller,而index對應controller裡面的方法。所以當在同一個模塊中切換頁面的時候,只會改變地址的hash值,瀏覽器不會進行跳轉。但是當切換頁面之後,如果用戶點擊瀏覽器的刷新按鈕,框架能夠根據hash值載入當前頁面。
(2)根據URL載入layout.html。當伺服器收到“/admin#home/index”地址的請求時,不管後面的hash值為多少,直接返回一個layout.html。該layout.html文件包含頁面的基本框架,比如公共js、css頁面、導航、footer等公共元素。
(3)初始化layoutVue。Layout.html載入完成之後,其中的JS會根據layout.html的HTML結構生成一個Vue實例,並將layout.html下麵的全部動態HTML交給該Vue實例托管,比如根據用戶角色顯示相應的導航菜單,在頁面header顯示用戶個人信息等。
(4)根據location.hash通過jQuery ajax載入相應的內容HTML文件。這時我們會在伺服器端定義另一個介面,根據內容頁的路徑返回對於的html代碼。
比如:GET /admin/getPage?path=home/index。
內容頁的html除了返回html代碼之外,還會包含該頁面所需的JS和CSS。這樣,當內容頁的html呈現到layout中的某個容器div中後,內容頁的JS就會被載入並執行。那麼內容頁的JS都有些什麼邏輯呢?當然是初始化內容頁的Vue實例並接管內容頁的動態html生成工作。
以上4個步驟可以用下圖簡單表示:
讀到這裡,如果你對軟體架構很有經驗,那麼相信你已經完全明白了我們的前後端分離框架的工作原理了,你也應該可以按照本文的思路完成你自己的前後端分離框架了。但是對於大多數讀者來說,可能讀到這裡只是大概明白怎麼回事,如果說要自己動手開始搭建,可能就會面臨無從下手的尷尬了。不用擔心,接下來筆者就以我們的框架為例,一步一步通過代碼來展示我們框架的搭建過程。
4、一步一步搭建我們的前端框架
(1)頁面地址生成。伺服器根據同步請求URL(/pe#home/index…)響應一個layout.html. 請看java源碼:
上圖中的java代碼就是我們前端框架中全部的後端處理代碼了(請仔細讀這句話,有點繞哈),是不是非常簡單呢?雖然簡單,但是功能卻是很強大的。從上面的代碼中可以看出:
a. 只要是以“/pe#”開頭的URL,伺服器將直接返回某個目錄下的layout.html頁面;
b. 前端開發人員可以在“/modules/pe/views/”下麵隨便建立目錄、子目錄以及HTML文件,然後即可通過ajax請求類似“/pe/page?path=home/index”這樣的URL直接載入下麵的HTML文件,這樣前端開發人員不需要動一行後端代碼,只需要按照約定建立目錄和HTML文件就可以在瀏覽器中載入出來。這樣,前端開發人員就完全不需要依賴後端開發人員來獲得頁面地址了,前端開發人員自己就可以創建頁面地址!
上面的java代碼中,還可以將同步請求跟非同步請求合併成一個(@GetMapping(“/pe/*”)),然後在方法內部判斷是否是同步請求,如果是同步請求就返回layout.html,否則返回內容頁。這樣做就不是監聽hash變化了,而是每個URL地址都看上去像一個同步請求地址,如“/pe/home/index”的URL進入伺服器後,伺服器首先返回layout.html,然後layout.html再發起ajax請求“/pe/home/index”這時伺服器返回內容頁HTML。在我們項目中,我們更願意使用監聽hash變化的方式。當然筆者認為這兩種方式都是OK的,如果後期想切換也是比較容易的。
(2)layout.html。 下麵讓我們來看看伺服器首先返回的layout.html大概長什麼樣子,請看下圖:
這個HTML文件就是伺服器的同步響應內容,也是我們每一個頁面的入口。這時我們需要把一個網頁理解成一個應用(APP),而這個layout.html只定義APP啟動入口和框架。可以看到該文件引用了基本的css和js,其最後的core.js內部完成了這個APP的初始化。下麵我們來看看這個core.js內部的主要部分代碼。
(3)core.js。完成APP初始化的代碼如下:
上面的示例代碼是筆者為了本文精簡過的,實際上這個JS文件在完成APP初始化的過程中還做了很多的操作,比如獲取用戶登錄信息、獲取頁面動態導航菜單數據等,然後將獲取的數據通過一個layoutVue實例呈現到頁面上,從而layoutVue實例完全接管layout.html中全部的HTML呈現工作。
在APP初始化的最後一步,就是根據URL #後面的路徑載入內容頁HTML到一個id為body的DIV中了。伺服器如何非同步響應URL(/pe/page?path=home/index),請參考本章第一小節中的非同步請求java源碼(pePage方法)。
(4)內容頁HTML。請看ajax載入的內容頁的HTML:
從上方代碼可以看出,每個內容頁對應一個js和一個css文件,然後html代碼以一個id為page的DIV開始。當然,這些都不是必須的,只是我們的項目規範,當然,筆者也建議大家可以參考我們的規範。
當內容頁HTML載入完成後,就會執行其引用的js文件了。接下來就讓我們來看看內容頁的JS代碼。
(5)內容頁JS代碼。請看下圖:
這個JS文件首先是由一個自執行函數包裹,好處是避免不經意將對象定義到window下麵(編碼開發人員寫出錯誤的代碼),這也是我們的規範之一,實際上我們項目的所有JS文件都由自執行函數包裹。
上方代碼的主入口是Vue.nextTick方法的回調function。Vue.nextTick是一個非常重要的方法,但是官網上並沒有給他一個特別明顯的位置,因此筆者要在此多說兩句,這個方法有什麼用。這得要回到Vue的render機制了。當Vue實例發現綁定的數據改變之後,Vue採用了非同步更新UI元素的方式,因此,當我們修改了數據的時候,這時DOM元素還沒有生成出來,如果這時去操作DOM(比如通過jQuery),那麼就會報找不到該DOM元素,所以一定要在改變Vue數據後使用Vue.nextTick去操作其影響的DOM元素。
再回到上方代碼。在Vue.nextTick回調中,首先是初始化內容頁的Vue實例,從而接管id為page的div及其下的所有DOM元素的呈現工作。
至此,一個完整的頁面就算載入完成了,用戶在瀏覽器中就能看到這個完整的頁面了。
這就是我們前後端分離框架的整個工作流程,希望筆者已經把這個流程解釋的足夠清晰,然後你可以開始動手搭建自己的前後端分離框架了。但是在你真正開始之前,筆者還想跟大家分享一個我們前後端分離的最佳實踐:mock,請看下一章。
第五章 前後端分離框架中的API mock思路
想要實現真正的前後端分離,那就必須得用好API mock(模擬數據)。使用mock數據的好處有兩個:
(1)前端開發人員可以基於API文檔生成mock數據,在後端開發人員將API發佈出來之前就可以完成整個業務流程的開發;
(2)使用mock數據能夠更低成本、更快速地,通過直接修改mock數據的方式,調試頁面樣式、調試頁面功能。
在本文中,筆者不會給大家推薦任何mock框架,因為我們根本用不著:我們要用純手工造數據的方式造出更真實的mock數據。
我們前後端分離框架中需要用到mock數據的地方,主要就是API,因此其他使用場景(如硬體mock、第三方系統API)本文不做示例介紹,因為其mock思路其實是一樣的。
1、全局mock開關
API的mock數據主要分為兩種,一種是零散的、手工發起的ajax API請求;另一種是被封裝到控制項內部的ajax API請求。不管是哪一種mock,首先我們在每個頁面都會載入的core.js裡面定義了一個全局的mock開關:mvcApp.mock = true/false,然後在頁面載入完成後,判斷如果設置mock==true,則提示用戶/開發者當前使用的是mock數據!
為什麼要設置這樣一個全局的mock開關呢?主要基於以下兩點考慮:
(1)設置全局的mock開關之後就不再需要針對每一個頁面設置mock開關,更容易維護,避免項目中有多個mock開關而難以統一開關狀態;
(2)如果發佈時忘記將mock開關給關掉,那麼發佈之後一運行發佈者就會發現mock開關忘了關,然後可以快速修複之後再重新發佈,從而避免不小心將正式服更新為mock數據源。
正是由於以上兩點考慮,我們的全局mock開關可以幫助程式開發者和發佈者更不容易犯錯。
下麵筆者將會給大家展現全局的mock開關如何跟頁面API配合,從而完成整個站點的mock狀態控制。
2、普通API的mock
在我們的前端框架中,我們使用了grunt來將整個頁面的全部JS文件打包成一個JS文件,因此,在我們的前端框架中,每個頁面對應一個JS源文件的文件夾,在打包的時候,grunt會將該文件夾中的全部JS文件合併打包(發佈到生產環境時將執行壓縮混淆)。下圖所展示的是我們admin端的一個列表頁面所對應的的JS源文件目錄(index文件夾):
可以看到該文件夾下麵的第一個JS文件叫01.page.js,這個JS是整個頁面的入口,包括定義了頁面全部的配置(比如用到的ajax URL)。第2個文件是02.api.js文件,該文件包含了所有的ajax請求。我們把全部的ajax請求封裝到這個文件中,也是為了更好的mock。
下麵就讓我們來看這個02.api.js大概長什麼樣子吧:
從上面的代碼中可以看到,我們定義了page.api這個對象兩次,而中間有一個if判斷,那就是判斷我們全局的mock開關是否處於開啟中,如果mock開啟,則不會執行return而會繼續第二段page.api對象賦值的代碼,這樣第一段代碼定義的page.api對象就被覆蓋了,於是這個頁面中的其他JS文件就將使用mock的數據。如果全局的mock開關處於關閉狀態,那麼第一段page.api對象賦值代碼執行完成之後,就會調用if下麵的return語句了,這樣就不會執行第二段page.api對象賦值,於是這個頁面的其他JS文件就將使用真實數據。
這就是全局mock開關在頁面中的應用,使用方法簡單而靈活。這樣,前端開發人員就可以在API開發出來之前通過mock的API完成樣式和交互。
3、以Grid控制項為例的控制項級mock
在WEB前端開發過程中,一定會用到大量的控制項(UI組件)。如果這個控制項(比如Grid)內部封裝了ajax請求,那麼其ajax的mock操作就很難通過上一小節中的mock方法實現。
下麵,筆者就將以我們項目的Grid控制項為例,給大家詳細闡述我們的改造過程。
由於我們項目中的Grid控制項是我們自己開發的,雖然只有300行代碼,但是功能很強大,可定製性很高。因此,要改造我們的Grid就變得很容易了。
首先,我們定義了一個VueGrid類繼承自Grid類,然後重寫了其loadData這個ajax方法,請看下圖:
改造之後的VueGrid類多了一個getMockDataFunction這個屬性,在loadData方法中,首先判斷該grid實例是否設置了getMockDataFunction屬性,如果設置了再判斷getMockDataFunction方法的返回值是否為空,如果返回值為空則也使用真實數據,因此使用mock數據的條件是很苛刻的:必須設置getMockDataFunction屬性並且其返回值不能為null。
然後我們在VueGrid類中還公開了一個設置getMockDataFunction屬性的方法,如下圖:
在初始化Grid和Vue的頁面JS中(01.page.js),我們像下方代碼這樣使用:
在上方代碼中初始化VueGrid實例時,設置了mock數據源為page.api.mockSearchList這個方法。我們可以通過全局的mock開關控制page.api.mockSearchList這個方法是否為null從而控制該grid是否使用mock數據。
請看02.api.js中的代碼:
上方代碼中,如果mvcApp.mock被設置為了false,那麼page.api.mockSearchList就不會被定義,也就是undefined(null);如果mvcApp.mock被設置為了true,那麼page.api.mockSearchList才會被賦值,這時grid將使用mock數據。
最後,為了方便大家理解整個Grid控制項的使用過程,筆者再給大家看看我們自己寫的VueGrid的html端的代碼,很簡單,很靈活,支持排序、分頁,支持JSON和HTML兩種數據格式:
至此,我們的Grid控制項的mock改造就已經完全完成了。改造後實現的效果:不需要修改01.page.js,不需要VueGrid.js,也不需要修改02.api.js,只需要修改mvcApp.mock的值就可以切換是否使用mock數據。
這樣,我們mock開關的狀態控制就非常的簡單,並且,最關鍵的是,不容易出錯!
第六章 結語
最後,筆者再帶大家回顧一下本文中的提到的一些關鍵技術、觀點和看法:
(1)前後端分離最關鍵的環節是API文檔服務框架,沒有一個好的API文檔服務做支撐,前後端分離之路舉步維艱。筆者建議大家按照本文提供的思路自行搭建,或者考慮使用我們現成的框架;
(2)VueJs官網介紹的規模化架構方案並不一定是最好的,如果你們團隊擁有後端架構師,筆者建議僅僅把VueJs當做HTML模板引擎即可,至於VueJs官網描述的其他特性可以忽略;
(3)VueJs的非同步渲染是一個非常重要的知識點,但是VueJs官網並沒有將之置於顯眼的地方,因此一定要熟練掌握Vue.nextTick方法及其工作原理;
(4)VueJs是完全可以和其他第三方框架/庫相容的,關鍵是要掌握其工作原理,比如我們項目中VueJs就很好地與jQuery、jQueryUI以及我們自己的JS控制項庫交互,雖然VueJs官網建議DOM操作全部交由VueJs接管,但筆者仍然建議很多時候用jQuery操作DOM更有優勢,因為jQuery的封裝性更好(比如我們的Grid控制項,之所以使用起來很簡單,那是因為內部通過jQuery封裝了很多操作);
(5)真正的前後端分離一定離不開mock,不要覺得mock是一個多麼複雜多麼高深的東西,在筆者看來,mock僅僅是一種思想,你只要明白其核心思想,自己寫出的mock框架才是最好用的;
(6)前後端分離,如果做好了,利遠大於弊。從我們團隊實施分離之後的這段時間來看,其對團隊的正面影響非常明顯;從長遠來看,前後端分離促進社會分工,讓公司的人才培養之路更加清晰,更加高效,更加具有競爭力。
(完)