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