1. 程式人生 > >詳細解剖大型H5單頁面應用的核心技術點

詳細解剖大型H5單頁面應用的核心技術點

事件機制 設置 橫豎 模板 phone inline lib 優點 溢出

闡述下項目 Xut.js 開發中一個比較核心的優化技術點,這是一套平臺代碼,並非某一個插件功能或者框架可以直接拿來使用,核心代碼大概是6萬行左右(不包含任何插件) 。這也並非一個開源項目,不能商業使用,只是為了作者開發方便同步修改代碼而上傳的源碼

描述下,項目提出的概念“無需程序員編程”可批量制作app應用。分2大塊,1塊是客戶端(PPT),默認擴展插件提供用戶編輯的界面,平臺會把設計邏輯與界面數據編譯成前端數據資源包(前端能處理的js、css、圖片等資源了),另一個大塊就是純前端部分(Xut.js),簡單來說:前端通過讀取數據包資源後,翻譯出用戶設計出的交互行為與可視效果。可以這樣想象,蘋果iTunes是一個平臺,裏面有超多的交互應用類型的app,每個app都是程序員開發的,現在每個app都可以通過我們這套平臺生成了,但是實際上精細度與性能當然無法跟原生相比了,只能是盡可能的靠攏。在這種平臺結構下前端的最大難點在於未知性,編輯數據的復雜度與數量是不可控的,可以想想下設計一個簡單兒童類型的闖關遊戲需要多少細節?項目介紹可以看我以前寫過的一篇文章 Hybrid App技術批量制作APP應用與跨平臺解決方案。

數據的未知性,會導致應用性能呈現反比例關系,當應用數據結構越復雜運行的實際性能越差。在這種設計下,一定會印證“墨菲定律”如果你擔心某種情況發生,那麽它就更有可能發生,在真機上開始大批量崩潰了。這篇文章我著重描述下項目前端方面“地基”的優化,好比建房,100層與200層的地基結構肯定是不一樣的,只有地基建好了,房子才能建的更高。這裏所涉及的問題以及角度只是個人觀點與方案,篇幅有點長,有耐心可以看看。

上傳了簡單的單頁面測試應用,簡單看看 “猜謎語”

開發環境

1. 基於ES6規範編寫,加入了flow靜態檢測環境

2. 開發調試基於webpack2,上線發布基於rollup+gulp
3. 編寫了全套基於node的build,開發、調試、壓縮、發布

核心特性

1. 單頁面結構設計,采用ES6編寫,加入了eslint檢測與flow靜態規則
2. 采用面向對象設計,繼承、封裝、多態
3. 設計模式,包含單列、策略、代理、叠代器、觀察者、命令、中介、適配、裝飾等等
3. 管理上引入了場景的概念,按場景與容器分層,層層細分管理職責,盡量做到了高內聚低耦合
4. 針對不同的設備不同的平臺,提供了多種功能自動適配的方案,e.g. 顯示區、圖片尺寸、視頻、音頻、事件、動畫等等

5. 項目幾乎大部分運用了目前前端所能接觸的到一些H5、CSS3、JS所有發布的新的特性(webgl動畫也有實現,性能問題暫未直接使用)

前端的核心問題:體驗與性能

用戶體驗與高性能是一個矛盾體,就好像軟件的可維護性與高性能一樣,為了追求易維護、易擴展或多或少會犧牲更多的性能為代價,當然這個只是相對而言 。回到主題,因為是跨平臺,需要更多的考慮移動端的性能支持,這裏不僅僅只某個功能,或某個效果或者動畫,最大的問題就是“當量變堆積積累到一定程度總會產生質變”,這就是所謂的“量變產生質變”,移動設備分配給瀏覽器的資源總是有限的。舉個例子:我們有制作一個2500頁面的app應用,大概有幾百兆的體積,這個不算誇張,如果是做epub甚至會出現1G以上的數據包,我們可以分解下會產生的問題

在移動端app應用上,用戶交互的行為一般就3種:滑動頁面、點擊跳轉與組合形式

點擊跳轉:這個相對容易處理,而且方案也很多,頁面為單位,可以單頁面實現,通過路由支持,這樣的框架很多了
滑動翻頁:與點擊跳轉最大的不同點就是頁面的“連續性”與“頁面的無縫連接
組合形式:點擊跳轉與滑動翻頁的行為的組合方式

點擊跳轉看起來更像一個原生態的APP應用設計,但是我們是平臺就需要對各種不同環境各種使用進行考量,重點分析下2500頁面滑動翻頁。

首先滑動翻頁的“連續性”與“無縫連接”就讓我只能選擇單頁面的設計實現(這裏我們必須拋開所有原生的支持情況,因為我是前端)。由於博客園上傳受限,簡單錄了一張gif效果圖 動態+多任務超快翻頁

頁面的拼接問題

1. 頁面的復雜度

大多數前端都做過這種拼接頁面,用一個swipe.js 分分鐘就OK了,沒啥技術難度,我只能說因為量小,而且內容簡單。不服嗎?來辯。我們的應用一個頁面結構是這樣的:其實我也不知道一個頁面有多少內容,因為是平臺,內容的制作是交給編輯的,理論上是無限塞進去,當然我見過最復雜的一個頁面有幾百個DOM節點的。簡單來說,如果把這些DOM節點看做一個個對象,那麽在頁面上可以直觀顯示為,人物,動物,物品,風景各種可視的圖片,每個對象上可以交叉組合綁定包含各種視頻,音頻,動畫,事件交互等等這些不可見的行為,同時對象之間也可以相對調用,形成多對多的關系,因為實際上的交互應用就是這樣設計的。DOM的組成還不止是圖片,數據也可能SVG的矢量圖,也有可能是文本文字或者蒙版組合,還有很多就不多說了,那麽假如這樣的頁面有2500個呢?實際上正是因為出現過,才有了現在這篇文章。

2. 場景頁

我習慣把整個結構展現用“場景”劃分,在我看來,整個應用就像一個電影,每個頁面就是戲劇中的場面,每個頁面場景可以上演一臺自己的場景戲,每個場景頁中的每個對象扮演了自己的角色。當有2500個這樣的場景頁時,不管是用戶體驗還是性能如果單純靠swipe.js是無法滿足。最直接的影響“在加載中直接崩潰”“加載的時間會無限延長”,不管哪種情況都是不應該出現的。這裏不做機器的性能數據對比的了,因為都是真實的教訓與經驗

3. 動態加載

多頁面或者是大數據優化,業內的方案“懶加載”,類似瀑布流一樣方案,可以用到再加載。甚至可以繼續優化下,動態加載,加下一個頁面,刪除上一個頁面。讓整個結構上永遠只保留3個頁面結構:前頁面,可視區頁,後一頁

隨手畫了下:動態加載的邏輯圖

技術分享

如上圖所示:可以這樣理解

1. 開始是2、3、4頁,第3頁面才是用戶的可視區域

2. 向右翻頁後,第4頁變成可視區域,第3頁為前一頁,第2頁是前前頁

3. 創建新的第5頁,刪除第2頁

圖簡單的描述了下動態處理頁面的邏輯,如果細化下去需要考慮的細節點就更多了,根據用戶的行為頁面的反饋可以分為,"滑動"、"反彈"、"翻頁",3種運動方式,動態加載方案需要控制2-3個頁面的動態生成,在運動的時候至少要控制2個頁面的坐標變化。

我們把場景頁可以想象一個正個拍攝電影的劇場,當劇組人員準備到位,導演說: action 開始,那麽拍攝開始,大家各司其職,大家都處理各自的動作,演員、打燈師,攝像師等各自上演自己的戲碼。如果戲中出問題了,導演會暫停,重拍。如果拍完了,就會接著拍下一場,一天結束,導演會cut。同樣在一個場景頁的切換中,是需要包含最少5個場景頁面處理

觀察圖所示:

  • 創建一個新頁(開始布置劇場)
  • 運行當前頁面的自動行為(開拍)
  • 暫停一個頁面(沒拍好,先停止)
  • 還原上一個頁面動作(重來)
  • 銷毀一個頁面(拍完了)

如果是跳轉頁面的情況就更加復雜,需要最多控制8種情況(後文頁面的管理與優化會提及下)

4. 體驗至上

感覺問題好像是解決了? 其實遠遠不夠,因為還要滿足一個關鍵需求“快速翻頁”,雖然是動態創建頁面,但是用戶在翻頁過程中是需要等待的,動態加載一個新的頁面會有性能消耗,就需要等待,如果就這種方式處理,每次翻頁在手機上,至少需要等待1-2秒以上,甚至更多,這個跟手機性能、內容數據量,網絡都相關,但實際上細化下內容數據處理,這裏取數據、拼接結構、渲染DOM這些都是消耗的根源

看到一些互聯網評論家也努力尋找一個成功的方程式,用戶體驗為王、渠道為王、內容為王...。其中對用戶體驗的量化方式或者標準有很多,但是作為一個用戶基本的去衡量一個東西的好壞,簡單點就是,用起來舒服,內容吸引人,或許還要加上一個“不收費”。在動態加載加載的情況下雖然能“簡單”滿足性能上的需求,但是顯然無法滿足“快速翻頁”的需求,這理我引入了一個解決的方案 “多線程任務”

5. 多線程問題

JS中沒有多線程的概念,JS運行在瀏覽器中,是單線程的,每個window一個JS線程(這裏拋開Web Worker與Node),而且JS執行引擎與瀏覽器的繪制是共享一個線程的,換句話會說:JS引擎線程和UI線程是互斥,線程處理其中一個,另一個就會等待,當然這也能夠理解,因為JS可以控制DOM。那麽要實現快速翻頁最關鍵的一步就是用戶在翻頁時候“線程沒有被占用”,言下之意就是用戶翻頁的時候,新的頁面任務必須完畢了,這樣用戶才繼續翻下一頁。然而實際情況並不是這麽樂觀,動態創建一個頁面是有消耗的,這些消耗會集中幾個方面:數據的處理、事件的綁定、DOM的繪制,還有中間的各種過程處理的消耗。實際上,如果只做了動態加載的方案時,每次翻頁需要等待1-2秒左右等下一個創建完畢後,才能繼續翻頁(本地數據,這裏不考慮網絡的情況)

6. 定時器

JS運行時,除了一個運行線程,引擎還提供一個消息隊列,裏面是各種需要當前程序處理的消息。新的消息進入隊列的時候,會自動排在隊列的尾端。單線程意味著js任務需要排隊,如果前一個任務出現大量的耗時操作,後面的任務得不到執行,任務的積累會導致頁面的“假死”。這也是js編程一直在強調需要回避的“坑”。寫JS的時候,遇到一些未知的問題,往往加一個setTimeout(func, 0)非常有用,不知道有多少是摸透了這個特性,模擬多線程任務就需要通過setTimeout

7. 多線程任務方案

假設用戶在翻頁的時候就會發出一條指令,“我要在翻頁了”,我們將會看到這樣的場景:

技術分享

如圖所示這是一個很尷尬的場景,導演在action了,但是場景還沒布置好。實際我們要的效果:此時新的頁面如果還在創建就需要被強制打斷,這樣讓用戶的行為永遠保持第一響應。但是問題來了,JS是單線程,怎麽強制去打斷任務創建了? 其實可以反過來思考,讓任務自己打斷自己擁有決斷權,只要每個任務知道自己的狀態,是創建還是打斷。簡單來說:我們把一個任務會細分很多塊出來,如創建一個視頻,那麽這個任務可以劃分幾個部分, "處理數據"、"拼接正確結構"、"繪制到頁面",這麽3個小任務出來,每次流程運行到某一個任務段時,會通過定時器把"任務掛起”,並去主動探查下當前這個任務是否能繼續運行者被強制打斷,如果用戶沒有指示那麽就繼續創建,否則就被打斷。

技術分享

值得註意的是,用戶的行為操作與任務的打斷是有間隔的,比如正好任務在創建了,用戶翻頁此時是不會立馬響應的,但是它會再下一次任務馬上響應,所以這個響應的速度取決於任務顆粒度的劃分。

當然任務劃分不能這麽傻蛋,如果一個頁面要創建10個視頻,那麽不是要做3*10次任務中斷請求了?如果是這樣那麽總的消耗時間是沒有變化的,只是把時間打散而已,沒有達到根本的效率與速度要求。而且被打斷後的任務之後要怎麽處理?合理的邏輯就是跟迅雷下載資源一下,斷點續傳, 下一次運行,應該從上一次結尾開始,而不是重新加載所有的任務

8. 動態加載+多線程任務方案

動態加載+多線程任務方案解決了目前所遇到的翻頁性能跟體驗的技術壁壘,動態加載解決創建數據量過大內存與時間問題,多線程方案解快速翻頁體驗問題。

實際的代碼實現又是非常精細的,當快速翻頁時候,如果新創建的頁面正在創建階段,那麽就需要暫停創建任務處理,讓用戶翻頁優先,當然這裏需要特別註意,任務的一個斷點續傳的功能,在用戶沒有操作的情況下,後臺要慢慢的補全沒有創建完畢的頁面,所以任務要支持斷點續傳從上一個未完成的進度開始,而不是又從新創建。超快速翻頁完全可能導致3個都沒有創建完畢,所以這時候的斷點續傳就需要先從當前可視區頁面先繼續,然後再空閑時段執行繼續補充前後頁面

在場景頁的切換過程中,除了外部的場景頁與場景頁的切換,還要涉及到場景內部的狀態管理,之前動態加載中就提出了5個狀態段

9. 頁面的擴展:自動分欄排版

的需求是不斷的變化,因為這是平臺所以就需要各種支持,讓我們繼續支持"自動分欄排版設計",通俗點講,就是在任意一個場景頁中給一個新的任務:“給一段數據,有圖片有文字,在不同設備上顯示的時候要自動分出橫向頁面,可以支持滑動,還要和以前的場景頁面無縫銜接”,由於都是動態的數據,頁面必須動態計算出來後與正常的場景頁形成無縫鏈接。這裏要引入一個好屬性,CSS3 column分欄,column這個東東我很久前做JS版的小說閱讀器就用過,網絡抓數據,通過column可以自動分出排版頁面。表面上來看,分頁操作並不復雜,但實際上分頁是非常復雜的功能,這個想靠js去計算文字占用的空間,難,非常難。column的細節就不多說了,這裏的主要痛點就是column的頁面如何跟正常的場景頁面“無縫銜接”? column數據是綁定到每個場景頁中的,一個子功能,所以column的分布完全可能是間斷式的,一頁有,一頁沒有,但是我們是動態頁面,而且column完全是屬於動態插入的,性能考慮,也只能用到才處理。這裏的方案就是把場景頁通過column填充,並且支持場景頁內部column本身可以翻頁,然後在column前後頁面邊界的時候,通過一個方式調用委托全局翻頁。這裏可以理解內部層(column)通過可以通過接口控制外部翻頁(全局)。但是不管是外部翻頁還是內部翻頁,都必須保持同一個翻頁算法,從用戶角度講,體驗是一致的。同樣的問題,在網絡不好的情況下,column有不全或者丟失的情況,那麽就需要有一個機制進行監聽觀察與更新

10. 頁面的擴展:不規則頁面

不規則頁面:讓每個場景頁可以展示不同的可視區域。由於移動設備的尺寸限制,在某些應用上,為了顯示最佳的視覺效果,我們都會盡量保持圖片元素的橫縱比值,內部元素的橫縱比變化就會帶來場景頁的動態調整,所以就會帶來了這些問題:

  • 每個頁面的可視區域不一樣
  • 每個頁面的縮放比不一樣
  • 每個頁面翻頁的速率不一樣
  • 頁與頁之間的翻頁距離不一樣了

這裏因為涉及比較廣,就不說原理了,估計也沒興趣,貼下幾個代碼地址吧 v-distance v-style

11. 頁面的擴展:雙頁模式

模版是單頁面設計的,設計上是區分了橫豎版的,如果豎版設計在瀏覽器上打開後,顯示就是一長條兩邊是空白區域會相當怪異,那麽在不改變設計排版的情況下,只能通過程序把原來的頁面合並成雙頁顯示,之前動態生成3頁,那麽現在是6頁了,與之帶來了一系列的細節的處理

12. 翻頁擴展:豎版滑動

頁面的數據查詢問題

在翻頁一塊強調了數據問題,那麽數據會有什麽問題?首先數據存儲是用的sqlite存在本地的,不像傳統的web頁面,通過ajax獲取服務器數據。當然如果是純PC的情況下又不一樣,這裏討論是移動端APP版本。html5引入Web SQL Database概念,所以前端也可以直接操作數據庫了,是不是很6?完全跳出了服務端的挾持,前端開發者直接操作數據的CURD。

通過讀取數據去是創建一個場景內容,但是這個數據速度的快慢是直接影響到用戶體驗的要素之一。一個頁面涉及N多數據的的查詢,可能關聯很多表,幾十上百條記錄,如何優化?

數據查詢方式


1:sql數據
拼sql語句是不行的,你可以試試一條SQL語句耗費的時間是多少?基本上1條語句就是100毫秒了,安卓下面實測
現在一個頁面就可能存在幾百條數據的關聯,那麽直接通過語句查詢是行不通的

2:緩存哈希
把所有數據一次性讀取出來,存在內存中,通過空間換時間,每次找內存中的緩存即可了。但是忽略一個問題,瀏覽器分配給每一個應用的內存是有限的,所以這樣緩存的表數據一多,內存會溢出,應用直接崩

3: 緩存數據集
建立數據庫的引用,緩存數據集SQLResultSetRowList了,可以直接result.rows.item(0) 通過索引下表找到對應的數據,這樣只需要算出數據庫中每一個id對應的下標索引就可以大大加快查詢數據了。簡單來說就是把查詢的ID轉化成數據庫每條數據對應的索引數映射(從0開始),可以直接拿到這條數據,雖然前期的轉化過程復雜,但是結果是美好的,數據問題也完美解決了。

頁面任務的優化

頁面的拼接問題中第7點拋出一個問題:"如果一個頁面要創建10個視頻,那麽不是要做3*10次任務中斷請求了?"

假設一個對象的創建需要做3次中斷請求操作,那麽10個對象意味著需要10次數據讀取、10次HTML拼接、10次繪制 ,這樣明顯是不合理的,任務細分足夠,但是任務請求太頻繁,一樣會拖慢時間,任務的總時間沒有變化,只是被打散了而已,而且因為中斷增加的異步的請求,導致場景頁面對象生成的總時間會更長。

在處理上,原則應該要合並相同的類型的任務,讓其保持一次處理。例如:每個不同類型的任務都會經歷幾個過程,‘getData‘、‘getHTML‘,‘draw‘,等等,我們把每個任務相同部分的代碼收集起來合並到到一起,統一處理。這樣我們在做任務中斷的時候就只要處理3次了,1次讀取數據,1次HTML憑借,1次繪制。性能是不是10倍增加?

頁面的管理與優化

面向對象設計一直是鼓勵將行為分布到各個對象中去,把對象再劃分更細的粒度,整個代碼設計都會默認遵循這一點。場景頁的切換,會將每個頁面的滑動行為與坐標處理等,分解到每一個獨立的頁面中去,賦予每個場景頁有獨立的處理能力,如果讓傳統的父容器控制翻頁的邏輯變化,那麽父容器就需要控制3個頁面的變化邏輯了,這裏還包括了翻頁、滑動、反彈等行為,這樣代碼耦合度與復雜度就高了(考慮下如果每個場景頁的尺寸是不規則的?)。不管是場景頁切換,還是場景內部管理,原則都是將行為分布在每個對象內部,讓這些對象自己負責個自己的行為,這也正是面向對象設計的優點之處

善用設計模式

顆粒度的劃分,可粗可細,這個根據自己設計,在xut.js項目中,可以把場景頁看做一個對象,也可以把場景頁內部的每個任務看做一個對象,甚至每個單獨的DOM元素。在代碼設計上很忌諱對象與對象直接關聯,這樣會產生對象之間的強耦合,但是實際上,每個對象與對象之間都可能產生錯綜復雜的關系,解耦的方式也有很多,比如通過觀察,通過中介等等,之前強制加了下redux的思路,我只能說不是我的菜,這種單數據流的思路導致整個結構都改變了。OMG!

中介模式與觀察者模式

頁為獨立單位,多個場景頁之間的通訊會通過一個中介層,這裏我稱之為"page"管理層,其實上最復雜的組合情況下,會有4個管理層,一個page,一個master,一個浮動mater,一個浮動page,每個層都可以包含多組"場景頁",多個層之間可以做整體視覺差效果,可以做共享多頁面等等....,多個管理層之間也會涉及交互的問題,如果對象與對象、頁面與頁面直接產生一對一或多對多的關系那麽就直接產生了強耦合,久而久之會形成一種網狀的交叉引用,當修改其中一個任意對象時,會難以避免引起其他的問題,所以在對象與對象之間交互通訊應該要增加一個中介對象,相關對象都通過中介對象來通訊,而不是互相引用

技術分享

如圖,page層與master分別各自控制了3個場景頁面組,2個層繼續向上衍生中介層,層與中介之間可以通過發布訂閱的方式再一次解耦,可以將page層作為為發布者,中介層作為訂閱者,當page層發生改變,那麽就會通知中介對象,由中介對象通知master層,引入中介後網狀式的多對多的關系變成相對簡單的一對多關系,如果增加新的的層級,只需要增加中介層對應的通訊控制就行了。可能感覺會比較迷惑2個模式太相像,其實是有區別的,可以看一篇文章吧 中介與觀察者模式有何不同?

模板方法與鉤子方法
es6中直接支持OOP的繼承語法,也就是extends,我非常喜歡用這個特性,當然大多時候extends可以被mix-in的方式替換。在整個代碼設計中,同一個類型,都會有相同的行為與不同的行為,那麽就可以利用繼承實現"模板方法"。在多任務分配中,所有任務都會繼承一個抽象父類,定義流程接口,例如:處理數據、處理結構、繪制頁面等等,所有的子類都實現了父類的接口,父類可以對子類進行流行控制與探測算法的處理。這樣如果我們要往頁面增加新的任務的時候,就需要實現這些抽象接口就OK了,不需要管具體的探測與流程控制,如果不同的任務有流程上的變動差異也可以用“鉤子”的方式去實現不同的變化,把子類實現種相同的部分挪到父類中,不同的分布具體執行各自任務部分留在子類實現。這樣就很體現了“泛型”的是思想。鉤子方法也是非常常見的一種手段,這個我在JQuery源碼中已經有很多分解了,xut.js也是到處可見hook

命令模式
在動態加載中提出了5個狀態段處理的問題,例如:用戶翻頁發送請求給場景頁中的每個對象,比如想讓對象執行 "運行"、"停止"、"復位"、"銷毀"等動作。其實用戶翻頁跟具體的對象其實是完全沒有關聯的,我並不希望把翻頁的請求與每一個對象的狀態直接關聯,所以我希望有一種非常松耦合的方式處理程序,消除用戶翻頁與具體對象之間的直接耦合關系,那麽我們可以把用戶的請求處理的具體操作封裝成commond對象,也就是命令對象。那麽我們可以在任意時候去調用這個方法,那麽這個方法就會執行針對對象狀態的獨立控制。這樣的好處非常明顯了,如果要做外部接口控制,那麽接口只需要操控這個命令commond方法就可以了,直接解開了調用者與接收者之間的耦合關系

享元模式
這個比較麻煩點,使用過但是後來又改了,這裏可以提及下,同樣的用任務為例,一個場景頁,如果創建了100個音頻任務,那麽意味著就是構建了100個音頻對象,那麽100個音頻對象內部,其實會有相同的共享屬性存在,比如傳入音頻類的類型,用Flash、用插件Phonegap、或者用HTML5去播放這個音頻文件,那麽這個類型的的屬性其實任何對象都存在的,再回頭看看享元模式的條件,大量使用相似的對象,造成了內存消耗,對象大多數狀態可以改變為外部狀態,剝離後可以用較少的共享對象取代大量對象。可以把音頻的 文件的url,界面的配置等文件,等剝離成外部狀態,並創建一個管理器保存。此時在去創建音頻對象的時候,其實只有3個對象了,分別對應了3個類型的,Flash、用插件Phonegap、或者用HTML5對象。調用時通過傳遞外部管理器到每個音頻對象中去組合成一個完整的對象,這樣100個音頻對象,減少的最多也只會存在3個了。當然這個麻煩的就是要區分內部狀態與外部狀態,增加額外的外部狀態管理器,而且對象如果消耗不大,優化並不明顯。

裝飾模式與OCP

iscroll是前端使用頻率很高的一個插件,在項目融合iscroll的過程中,其實會有一個這樣的問題:iscroll作用之一是讓對象區域上下或者左右滾動,這個滾動是會跟頁面滑動形成沖突的,所以我們一般需要在iscroll停止用戶事件傳遞與默認的瀏覽器行為。那麽這樣會出現一個弊端,如果iscroll是作用上下區域滾動,用戶在iscroll的區域中如果想左右翻頁此事就無法響應(翻頁是全局控制,但是iscroll已經屏蔽了默認事件傳遞,全局無法得到通知)。如果iscroll的區域非常大,那麽用戶在整個頁面上的體驗感受就會是頁面進入卡死狀態,無法翻頁,非常糟糕。

要解決這個問題,可以在iscroll左右翻頁的時候讓其響應全局滑動即可,因為iscroll是一個第三方插件,我們不應該去修改其內部結構,而是通過增加代碼的的方式處理。iscroll插件被暴露幾個事件監聽接口scroll,scrollEnd,其實就是對內部處理用戶操作行為的一個反饋,我們可以在這幾個接口中擴展自己的代碼,比如在用戶滑動了,會觸發scroll事件,我們可以在這個事件中調用全局翻頁提供的方法讓全局可以滑動,這裏由於功能單一,我就提供下源碼的截圖,去掉了一些註釋,保留了處理的方法

技術分享

通過擴展了iscroll提供的幾個接口,不改變iscroll自身的情況下,動態的給iscroll對象添加了新的行為,實現了滑動、反彈、翻頁的用戶響應,這就是簡單的裝飾模式的體現。在沒有改變iscroll內部源碼的前提下,通過擴展的一些額外的代碼就實現了新的功能,這樣其實是遵循了"開放-封閉”的原則

簡單工廠模式

這個是實用性很強的模式之一,正好上圖的iscroll用到了就提及下。針對iscroll,遵循了"開放-封閉”的原則做了新功能的擴展,但是其實並不是任何時候都需要處理滑動、反彈、翻頁行為的,所以應該對這個創建的接口做再一次的封裝,實現這個接口的類來決定實例化哪個類

export default function IScroll(node, options, delegate) {
  if(delegate && config.hasTrackCode(‘swipe‘)) {
    options.stopPropagation = true
    options.probeType = 2
    const iscroll = new iScroll(node, options)
    iscroll.on(‘scroll‘, function(e) {
    })
    iscroll.on(‘scrollEnd‘, function(e) {
    })
    return iscroll
  } else {
    /*其余滑動*/
    return new iScroll(node, options)
  }
}

外部引入IScroll這個接口,只要傳遞不同的參數,內部來決定實例化哪個類。

其余優化

通過上面的一些優化手段,目前已經能滿足現有的應用翻頁性能了,優化是體現在各個細節上的

1. 引入對象池,利用空間換時間,共享了場景頁的的重復的數據,盡量減少重復處理

2. 實現了多套事件機制,一套是全局用戶收集派發用戶行為(比如頁面控制),一套針對hammer.js適配後支持獨立對象事件綁定,實現多事件疊加嵌套的優先級處理

3. 實現全局事件機制中類似jquery的on的向上層層刷選委托處理,可以向全局註冊很多不同類型的處理。例如:默認用戶可以在頁面上任意一個對象上滑動,如果對象有獨立的事件,獨立事件>全局事件優先級

4. 簡單實現了類似sizzle的嵌套閉包,增加數據篩選的速度與重復利用效率

5. 引入了vue早期batcher刷新思路,沒有做虛擬dom,因為合並的文檔碎片一次繪制,性能不會差

項目是基於自己的理解與實際運用的結晶,其中簡單列舉一些模式在項目中的運行,至於其余什麽單列、適配器、叠代、策略等模式就很常用,這裏就不多提及了。模式理解因人而異,或許與其實的理論有一點偏差,有則改之無則加勉。有人會說,這是強加模式上去,這屬於推模式和過渡設計,我就只能呵呵,開始的代碼其實並不多復雜,而且隨著需求的不斷變化,代碼就會越來越朝著"模式"的方向進化了,因為你會覺得這樣改是很比較合理的。模式本來就是在面向對象設計中提煉出來的可復用的設計技巧。所以很多時候,我們寫出了帶了模式的代碼,只是自己不覺得而已。不是為了模式而模式,是為了更好的維護,更好的復用。當快速開發完全任務交付代碼之後,之後會用更多的時間去考慮程序的延展性、健壯性,通過提升代碼的可維護度從而提升工作效率,長期下來,這個是利大於弊的。

模式也並非一成不變的,實際開發中,為了使用上的便利就會犧牲維護度,比如我們最常用的jQuery,jQuery中的大多數方法同一個接口方法都會承載非常多的職責,例如:css方法,不僅可以以多種方式獲取元素的樣式,同時也支持多種方式設置樣式值,最直接的就是違反了SRP單一職責原則,但是對於使用者來說簡化了API的復雜度,也簡化了用戶的使用。利於弊得與失總是在不斷的衡量與取舍。

功能與插件支持

場景頁面支持4種縮放比值
1. 永遠100%屏幕尺寸自適應
2. 寬度100% 正比自適應高度
3. 高度100%,寬度溢出可視區隱藏
4. 高度100% 正比自適應寬度

多媒體類
修復音頻在移動端不能自動播放的問題
1. 音頻自適應適配設備(5種方式)
2. 視頻自適應適配設備(3種方式)


動畫類
1. 2D普通精靈動畫
2. 2.5D高級精靈動畫
3. PPT動畫(56種)
4. 頁面零件動畫與iframe零件動畫(81種)

事件類
事件分為2大塊
全局事件,又全局控制並且委派,主要控制翻頁,與用戶的組要行為
獨立事件,作用於每個獨立的對象上
1. 普通tap與click事件
2. 對象拖動與拖拽
3. 多種hammer.js支持的事件(14種)


支持2種縮放
1. page頁面級縮放
2. 圖片放大後並縮放

零碎功能
1.支持代碼監聽追蹤用戶行為
2.支持圖片模式webp模式
3.支持4種工具欄配置
4.支持忙碌光標配置
5.支持自適應圖片分辨率,配置不同的圖片模式

……

這只是一篇介紹性的文章,啰啰嗦嗦寫了一大堆,主要只是介紹了翻頁與之涉及的一些可利用的模式,當然一個項目上細節的處理還會有很多的。由於不是開源項目,沒有寫出具體的使用文檔了,見諒。如果有時間,我會把動態翻頁+多線程的處理出獨立的插件可以提供使用。

詳細解剖大型H5單頁面應用的核心技術點