閱讀須知:這裡的大型單頁面應用(SPA Web App)是指頁面和功能元件在一個某個量級以上,舉個例子,比如 30+個頁面100+個元件,同時伴隨著大量的資料互動操作和多個頁面的資料同步操作。並且這裡提到的頁面,均屬於 hash 頁面,而多頁面概念的頁面,是一個獨立的 html 文件。基於這個前提,我們再來討論,否則我怕我們 Get 不到同一個 G 點上去。
挑戰一:前端元件化
基於我們所說的前提,第一個面對的挑戰是元件化。這裡還是要強調的是元件化根本目的不是為了複用,很多人根本沒想明白這點,總是覺得造的輪子別的業務可以用,說不定以後也可以用。
其實前端發展迭代這麼快,互動變化也快,各種適配更新層出不窮。今天造的輪子,過陣子別人造了個高階輪子,大家都會選更高檔的輪子,所以現在前端界有一個現象就是為了讓別人用自己的輪子,自己使勁不停地造。
在前端工業化生產趨勢下,如果要提高生產效率,就必須讓元件規範化標準化,達到怎樣的程度呢?一輛車除了底盤和車身框架需要自己設計打造之外,其他標準化零件都可以採購組裝(專業學得差,有啥謬誤請指正)。也就是說,除了 UI 和前端架構需要自己解決之外,其他的元件都是可以奉行拿來主義的,如果打算讓車子跑得更穩更安全,可以對元件進行打磨優化完善。
說了這麼說,倒不如看看徐飛的文章《2015前端元件化框架之路》 裡面寫的內容都是經過一定實踐得出的想法,所以大部分內容我是贊成而且深有體會的。
挑戰二:路由去中心化
基於我們所說的前提,中心化的路由維護起來很坑爹(如果做一兩個頁面 DEMO 的就沒必要出來現眼了)。MV* 架構就是存在這麼個坑爹的問題,需要宣告中心化 route(angular 和 react 等都需要先宣告頁面路由結構),針對不同的路由載入哪些元件模組。一旦頁面多起來,甚至假如有人偷懶直接在某個路由寫了一些業務耦合的邏輯,這個 route 的可維護性就變得有些糟糕了。而且使用者訪問的第一個頁面,都需要載入 route,即使其他路由的程式碼跟當前頁面無關。
我們再回過頭來思考靜態頁面簡單的載入方式。我們只要把 nginx 搭起來,把 html 頁面放在對應的靜態資源目錄下,啟動 nginx 服務後,在瀏覽器位址列輸入 127.0.0.1:8888/index.html 就可以訪問到這個頁面。再複雜一點,我們把目錄整成下面的形式:
1 2 3 4 5 6 |
/post/201509151800.html /post/201509151905.html /post/201509152001.html /category/js_base_knowledge.html /category/css_junior_use.html /category/life_is_beautiful.html |
這種目錄結構很熟吧,對 SEO 很友好吧,當然這是後話了,跟我們今天說的不是一回事。這種目錄結果,不用我們去給 Web Server 定義一堆路由規則,頁面存在即返回,否則返回 404,完全不需要多餘的宣告邏輯。
基於這種目錄結構,我們可以抽象成這樣子:
1 |
/{page_type}/{page_name}.html |
其實還可以更簡單:
1 |
/p/{name}.html |
從元件化的角度出發,還可以這樣子:
1 2 3 |
/p/{name}/name.js /p/{name}/name.tpl /p/{name}/name.css |
所以,按照我們簡化後的邏輯,我們只需要一個 page.js 這樣一個路由載入器,按照我們約定的資源目錄結構去載入相應的頁面,我們就不需要去幹宣告路由並且中心化路由這種蠢事了。具體來看程式碼。我們也懶得去解析了,裡面有註釋。
挑戰三:領域資料中心化
對於單向資料流迴圈和資料雙向繫結誰優誰劣是永遠也討論沒結果的問題,要看是什麼業務場景什麼業務邏輯,如果這個前提沒統一好說啥都是白搭。當然,這個挑戰的前提是非後臺的單頁面應用,後臺的前端根本就不需要考慮前端記憶體快取資料的處理,直接跟介面資料庫互動就行了。明確了這個前提,我們接著討論什麼叫領域資料中心化。
前面討論到兩種資料繫結的方式,但是如果頻繁跟介面互動:
- 記憶體資料銷燬了,重新請求資料耗時浪費流量
- 如果兩個介面欄位部分不一樣但是使用場景一樣
- 多個頁面直接有部分的資料相同,但是先來後到導致某些計數字段不一致
- 多個頁面的資料相同,其中某些資料發生使用者操作行為導致資料發生變動
因此,我們需要在業務檢視邏輯層和資料介面層中間增加一個 store(領域模型),而這個 store 需要有一個統一的 記憶體快取 cache,這個 cache 就是中心化的資料快取。那這個 store 究竟是用來弄啥勒?
Store 具有多形態,每個 store 好比某一類物品的倉儲(領域,換個詞容易理解),如蔬果店 fruit-store, 服裝店 clothes-store,蔬果店可以放蘋果香蕉黑木耳,服裝店可以放背心底褲人字拖。如果品種過於繁多,我們可以把蔬果店精細化運營變成香蕉專賣店,蘋果專賣店(!== appstore),甚至是黑木耳專賣店…( _ _)ノ|,蔬果種類不一樣,但是也都是稱重按斤賣嘛。
var bannerStore = new fruitStore();
var appleStore = new fruitStore();
有了這些倉儲之後,我們可以放心的把資料丟給檢視邏輯層大膽去用。想修改資料?直接讓 store 去改就行了,其他頁面的 DOM 文字內容也得修改吧?那是其他頁面的業務邏輯做的事,我們把事件丟擲去就好了,他們處不處理那是他們的事,我們別瞎操心(業務隔離)。
那麼 store 具體弄啥勒?
- 32 個贊位置可點贊或者取消,三個頁面的贊數需要同步,按鈕點贊與取消的狀態也要同步。
- 條目是否已收藏,取消收藏後 Page B 需要刪除資料,Page A+C 需要同步狀態,如果在 Page C 又有收藏操作,Page B 需要相應增減資料,Page A 狀態需要同步。
- 發評論,Page C 需要更新評論列表和評論數,Page A+B 需要更新評論數。如果 Page B 沒有被載入過,這時候 Page B 拿到的資料應該是最新的,需要同步給 A+C 頁面對應的資料進行更新。
所以,store 乾的活就是資料狀態讀寫和同步,如果把資料狀態的操作放到各個頁面自己去處理,頁面一旦多了或者複雜起來,就會產生各個頁面資料和狀態可能不一致,頁面之前雙向引用(業務耦合嚴重)。store 還有另一個作用就是資料的輸入輸出格式化,簡單舉個例子:
- 任何介面 API 返回的資料,都需要經過 input format 進行統一格式化,然後再寫入 cache,因為讀取的資料已按照我們約定的規範進行的處理,所以我們使用的時候也不需要理會介面是返回怎樣的資料型別。
- 某些元件需要的資料欄位格式可能不同,如果把資料處理放在模板進行處理,會導致模板無法更加簡潔通用(業務耦合),所以需要 output format 進行處理。
所以,store 就是扮演著這樣的角色——是資料狀態讀寫和同步,以及資料輸入輸出的格式化處理。
挑戰四:Hybrid App 化
現在 Hybrid App 架構應用很火啊 _ (:3」∠)_,不搞一下都不好意思說自己是做 H5的。這裡所說的 Hybrid App 可不是那種內建打包的 html 原始碼那種,而是直接去服務端請求 html 文件那種,可能會使用離線快取。有的人以為如果要使用 Hybrid 架構,就不能使用 SPA 的方式,其實 Hybrid 架構更應該使用 SPA。
遇到的幾個問題,我簡單列舉一下:
- 客戶端通過 url 傳參
如果通過 http get 請求的 query 引數進行傳參,會導致命中不到 html 文件快取,所以通過 SPA 的 hash query 傳參,可以規避這個問題。
- 與其他 html 頁面進行跳轉
這種場景下,進入新頁面和返回舊頁面導致 webview 會重新載入本地的 html 文件快取,視覺體驗很不爽,即使頁面使用了離線快取,而 SPA 可以規避這個問題。
- 使用了離線快取的頁面需要支援程式碼多版本化
由於採用了非覆蓋性資源釋出方式,所以需要仍然保留舊的程式碼一段時間,以防止使用者使用舊的 html 文件訪問某些按需載入功能或清除了本地快取資料而拿不到舊版本程式碼。
- js 和 css 資源 離線化
由於離線快取的資源需要先在 manifest 檔案宣告,你也不可能總是手動去維護需要引用的 js 和 css 資源,並且那些按需載入的功能也會因此失去按需載入的意義。所以需要將 js 和 css 快取到 localstorage,直接省去這一步維護操作。至於使用者清除 localstorage,參考第三點解決方案。
- 圖示資源離線化
將圖示檔案進行 base64 編碼後存入 css 檔案,方便離線使用。
挑戰五:效能優化
@前端農民工 在 別處 已經說得很清楚了,直接傳送門過去看吧,這裡不羅嗦了。