前端進階篇之如何編寫可維護可升級的程式碼

發表於2015-10-16

前言

我還在攜程的做業務的時候,每個看似簡單的移動頁面背後往往會隱藏5個以上的資料請求,其中最過複雜的當屬機票與酒店的訂單填寫業務程式碼

這裡先看看比較“簡單”的機票程式碼:

然後看看稍微複雜的酒店業務邏輯:

機票一個頁面的程式碼量達到了5000行程式碼,而酒店的程式碼竟然超過了8000行,這裡還不包括模板(html)檔案!!!

然後初略看了機票的程式碼,就該頁面可能發生的介面請求有19個之多!!!而酒店的的互動DOM事件基本多到了令人髮指的地步:

當然,機票團隊的互動DOM事件已經多到了我筆記本不能截圖了:

就這種體量的頁面,如果需要迭代需求、打BUG補丁的話,我敢肯定的說,一個BUG的修復很容易引起其它BUG,而上面還僅僅是其中一個業務頁面,後面還有強大而複雜的前端框架呢!如此複雜的前端程式碼維護工作可不是開玩笑的!

PS:說道此處,不得不為攜程的前端水平點個贊,業內少有的單頁應用,一套程式碼H5&Hybrid同時執行不說,還解決了SEO問題,嗯,很贊。

如何維護這種頁面,如何設計這種頁面是我們今天討論的重點,而上述是攜程合併後的程式碼,他們兩個團隊的設計思路不便在此處展開。

今天,我這裡提供一個思路,認真閱讀此文可能在以下方面對你有所幫助:

文中是我個人的一些框架&業務開發經驗,希望對各位有用,也希望各位多多支援討論,指出文中不足以及提出您的一些建議

由於該專案涉及到了專案拆分與合併,基本屬於一個完整的前端工程化案例了,所以將之放到了github上:https://github.com/yexiaochai/mvc

其中工程化一塊的程式碼,後續會由另一位小夥伴持續更新,如果該文對各位有所幫助的話請各位給專案點個贊、加顆星:)

我相信如果是中級水平的前端,認真閱讀此文一定會對你有一點幫助滴。

一個實際的場景

演示地址

http://yexiaochai.github.io/mvc/webapp/bus/list.html

程式碼倉促,可能會有BUG哦:)

程式碼地址:https://github.com/yexiaochai/mvc/

頁面基本構成

因為訂單填寫頁一般有密度,我這裡挑選相對複雜而又沒有密度的產品列表頁來做說明,其中框架以及業務程式碼已經做過抽離,不會包含敏感資訊,一些優化後續會同步到開源blade框架中去。

我們這裡列表頁的首屏頁面如下:

簡單來說組成如下:

① 框架級別UI元件UIHeader,頭部元件

② 點選日期會出框架級別UI,日曆元件UICalendar

③ 點選出發時段、出發汽車站、到達汽車站,皆會出框架級別UI

④ header下面的日期工具欄需要作為獨立的業務模組

⑤ 列表區域可以作為獨立的業務模組,但是與主業務靠太近,不太適合

⑥ 出發時段、出發汽車站、到達汽車站皆是獨立的業務模組

一個頁面被我們拆分成了若干個小模組,我們只需要關注模組內部的互動實現,而包括業務模組的通訊,業務模組的樣式,業務模組的重用,暫時有以下約定:

這裡有些朋友可能認為單個模組的CSS以及image也應該參與獨立,我這裡不太同意,業務頁面樣式粒度太細的話會給設計帶來不小的麻煩,這裡再以通俗的話來說:尼瑪,我CSS功底一般,拆分的太細,對我來說難度太高……

不好的做法

不好的這個事情其實是相對的,因為不好的做法一般是比較簡單的做法,對於一次性專案或者業務比較簡單的頁面來說反而是好的做法,比如這裡的業務邏輯可以這樣寫:

根據之前的經驗,如果僅僅包含這些業務邏輯,這樣寫程式碼問題不是非常大,程式碼量預計在800行左右,但是為了完成完整的業務邏輯,我們這裡馬上產生了新的需求。

需求迭代

因為我這裡的班次列表,最初是沒有URL引數,所以根本無法產出班次列表,頁面上所有元件模組都是擺設,於是這裡新增一個需求:

於是,我們這裡會新增一個簡單的彈出層:

這個看似簡單的彈出層,背後卻隱藏了一個巨大的陷阱,因為點選出發或者到達時會出城市列表,而城市列表本身就是一個比較複雜的業務:

於是頁面的組成發生了改變:

① 本身業務邏輯約800行程式碼

② 新增出發到達篩選彈出層

③ 出發城市頁面,預計300行程式碼

而彈出層的新增對業務本身造成了深遠的影響,本來url是不帶有業務引數的,但是點選了彈出層的確定按鈕,需要改變URL引數,並且重新整理本身頁面的資料,於是簡單的一個彈出層新增直接將頁面的複雜程度提升了一倍。

於是該頁面程式碼輕輕鬆鬆破千了,後續需求迭代js程式碼量破2000僅僅是時間問題,到時候維護便複雜了,頁面複雜無規律的DOM操作將會令你焦頭爛額,這個時候元件化開發的優勢便得以體現了,於是下面進入元件化開發的設計。

準備工作

總體架構

這次的程式碼依賴於blade骨架,包括:

① MVC模組,完成通過url獲取正確的page控制器,從而通過view.js完成渲染頁面的功能

② 資料請求模組,完成介面請求

全站依賴於javascript的繼承功能,詳情見:【一次面試】再談javascript中的繼承,如果不太瞭解物件導向程式設計,文中程式碼可能會有點吃力,也請各位多多瞭解。

總體業務架構如圖:

框架架構圖:

.

下面分別介紹下各個模組,幫助各位在下文中能更好的瞭解程式碼,首先是基本MVC的介紹,這裡請參考我這篇文章:簡單的MVC介紹

全域性控制器

其實控制器可謂是變化萬千的一個物件,對於伺服器端來說,控制器完成的功能是將本次請求分發到具體的程式碼模組,由程式碼模組處理後返回字串給前端;

對於請求已經來到瀏覽器的前端來說,根據這次請求URL(或者其它判斷條件),判斷該次請求應該由哪個前端js控制器執行,這是前端控制器乾的事情;

當真的這次處理邏輯進入一個具體的page後,這個page事實上也可以作為一個控制器存在……

我們這裡的控制器,主要完成根據當前請求例項化View的功能,並且會提供一些view級別希望單例使用的介面:

這裡屬於框架控制器層面的程式碼,與今天的主題不是非常相關,有興趣的朋友可以詳細讀讀。

頁面基類

這裡的核心是頁面級別的處理,這裡會做比較多的介紹,首先我們為所有的業務級View提供了一個繼承的View:

一個Page級別的View會有以下幾個關鍵屬性&方法:

① template,html字串,不包含請求的基礎模組,會構成頁面的html骨架層

② events,所有的DOM事件定義處,以事件代理的方式定義,所以不必擔心執行順序

③ addEvent,用於頁面級別各個階段的監控事件註冊點,一般來說使用者只需要關注很少幾個事件,比如:

一個頁面的基本寫法:

只要按照這種規則寫,便能展示頁面,並且具備DOM互動事件。

頁面模組類

所謂頁面模組類,便是用於拆分一個頁面為單個元件模組所用類,這裡有這些約定:

這裡程式碼可以再優化,但不是我們這裡關注的重點:

資料實體類

這裡的資料實體對應著,MVC中的Model,因為之前已經使用model用作了資料請求相關的命名,這裡便使用Entity做該工作:

這裡的資料實體會以例項的方式注入給模組類例項,他的工作是起一箇中樞左右,完成模組之間的通訊,反正非常重要就是了

其它

資料請求統一使用abstract.model,資料前端快取使用abstract.store,這裡因為目標是做頁面拆分,請求模組不是關鍵,各位可以把這段程式碼看層一個簡單的ajax即可:

業務入口

最後簡單說下業務入口檔案:

很簡單的程式碼,指定了下require的path配置,最後我們看看入口頁面的呼叫:

接下來,讓我們真實的開始拆分頁面吧。

元件式程式設計

骨架設計

首先,我們進行最簡單的骨架設計,這裡依次是其js程式碼與模板程式碼:

頁面展示如圖:

日曆工具欄的實現

這裡要做的第一步是將日曆工具欄模組實現,以資料為先的思考,我們先實現了一個與日曆業務有關的資料實體:

裡面完成日期工具欄所有相關資料操作,並且不包含實際的業務邏輯。

然後這裡開始設計日期工具欄的模組View:

這個元件模組幹了幾個事情:

① 首先,dateEntity實體需要由list.js這個主view注入

② 這裡為dateEntity註冊了兩個資料響應事件:

render方法繼承至基類,使用template與資料生成html,其中資料產生必須重寫父類一個方法:

因為這裡的日曆資料,預設取當前時間,但是url引數可能傳遞日期引數,所以定義了一個資料初始化方法:

該方法在主頁面渲染結束後會第一時間呼叫,這個時候日曆工具欄便渲染出來,其中日曆元件的使用便不予理睬了,主控制器的程式碼改變如下:

於是,整個介面變成了這個樣子:

這裡是對應的日曆工具模板檔案tpl.calendar.html:

搜尋工具欄的實現

我們現在的頁面,就算不傳任何URL引數,已經能渲染出部分頁面了,但是下面出發站汽車等業務資料必須等待班次列表資料請求結束才能替換資料,但是這些資料如果沒有出發城市和到達城市是不能發起請求的,所以這裡先實現搜尋工具欄功能:

在出發城市或者到達城市不存在的話便彈出搜尋工具欄,引導使用者選擇城市,這裡新增彈出層需要在主頁面控制器(檢測主控制器)中使用一個UI元件:

對應搜尋彈出層html模板:

這裡核心程式碼是:

於是當URL什麼引數都沒有的時候,就會彈出這個搜尋框

這裡也迎來了一個難點,因為城市列表事實上應該是一個獨立的可訪問的頁面,但是這裡是想用彈出層的方式呼叫他,所以我在APP層實現了一個方法可以用彈出層的方式調起一個獨立的頁面。

這裡有一個不同的地方是,因為我們點選查詢的時候才會做實體資料更新,這裡是單純的做DOM操作了,這裡不設定資料實體一個原因就是:

這個搜尋彈出層是一個頁面級DOM之外的部分,資料實體變化一般只應該影響Page級別的DOM,除非真的有兩個頁面級View會公用一個資料實體。

搜尋功能完成後,我們這裡便可以進入真正的資料請求功能渲染列表了。

其餘模組

在實現資料請求之前,我按照日期模組的方式將下面三個模組的功能也一併完成了,這裡唯一不同的是,這些模組的DOM已經存在,我們不需要渲染了,完成後的程式碼大概是這樣的:

這個時候整個邏輯結構大概出來了:

最後功能:

到此,demo結束了,最後形成的目錄:

一個js便可以拆分成這麼多的小元件模組,如果是更加複雜的頁面,這裡的檔案會很多,比如訂單填寫頁的元件模組是這裡的三倍。

元件化的優缺點

元件化帶來的幾個優點十分明顯:

缺點

事實上,元件化不會帶來什麼不足,對於不瞭解的朋友可能會認為程式碼複雜度有所增加,其實不這樣做程式碼才真正叫一個難呢!

真正的美中不足的要挑一個毛病的話,這種分拆可能會比單個檔案程式碼量稍大

從效能優化角度看元件化

無論什麼前端優化,最後的瓶頸一定是在請求量上做文章:壓縮、快取、僅僅做首屏渲染、將jQuery快取zepto……

說都會說,但是很多場景由不得你那樣做,專案足夠複雜,而UI又提供給了不同團隊使用的話,有一天前端做了一次UI優化,而如何將這次UI優化反應到線上才是考驗架構設計的時候,如果是不好的設計的話,想將這次優化推上線,會發生兩個事情:

① 業務團隊大改程式碼

② 框架資源(js&css)膨脹

這種頭疼的問題是一般人做優化考慮不到的,而業務團隊不會因為你的更新而去修改程式碼,所以一般會以程式碼膨脹為代價將這次優化強推上線,那往往會讓情況更加複雜:

新老程式碼融合,半年後你根本不知道哪些程式碼可以刪,哪些程式碼可以留,很大時候這個問題會體現在具有公共特性的CSS中 如果你的CSS同時服務於多個團隊,而各個團隊的框架版本不一致,那麼UI升級對你來說可能是一個噩夢! 如果你想做第三輪的UI升級,那還是算了吧……

事實上,我評價一個前端是否足夠厲害,往往就會從這裡考慮:

當一個專案足夠複雜後,你私下做好了優化,但是你的優化程式碼不能無縫的讓業務團隊使用,而需要業務團隊做很多改變,你如何解決這種問題

很多前端做一個優化,便是重新做了一個東西,剛開始肯定比線上的好,但半年後,那個程式碼質量還未必有以前的好呢,所以我們這裡應該解決的是:

如何設計一個機制,讓業務團隊以最小的修改,而可以用上新的UI(樣式、特性),而不會增加CSS(JS)體積 這個可能是元件化真正要解決的事情!

理想情況下,一個H5的資源組成情況是這樣的:

① 公共核心CSS檔案(200行左右)

② 框架核心檔案(包含框架核心和第三方庫)

③ UI元件(有很多獨立的UI元件組成,每個UI元件又包含完整的HTML&CSS)

④ 公共業務模組(提供業務級別公共服務,比如登入、城市列表等業務相關功能)

⑤ 業務頻道一個頁面,也就是我們這裡的list頁的程式碼

因為框架核心一般來說是不經常改變的,就算改變也是對錶現層透明的,UI採用增量與預載入機制,這樣做會對後續樣式升級,UI升級有莫大的好處,而業務元件化後本身要做什麼滾動載入也是輕而易舉

好的前端架構設計應該滿足不停的UI升級需求,而不增加業務團隊下載量

結語

本文就如何分解複雜的前端頁面提出了一些自己的想法,並且給予了實現,希望對各位有所幫助。

關於合併

前端程式碼有分拆就有合併,因為最終一個完整的頁面需要所有資源才能執行,但考慮到此文已經很長了,關於合併一塊的工作留待下文分析吧

關於程式碼

為了方便各位理解元件化開發的思想,我這裡寫了一個完整的demo幫助各位分析,由於精力有限,程式碼難免會有BUG,各位多多包涵:

https://github.com/yexiaochai/mvc

可能會瀏覽的程式碼:

https://github.com/yexiaochai/blade

相關文章