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

發表於2017-05-05

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

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

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

上傳了平臺生成的一個簡單的SPA單頁面測試應用,簡單看看 “猜謎語”

開發環境

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個頁面結構:前頁面,可視區頁,後一頁

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

image

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

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. 多執行緒任務方案

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

image

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

image

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

當然任務劃分不能這麼傻蛋,如果一個頁面要建立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,每個層都可以包含多組”場景頁”,多個層之間可以做整體視覺差效果,可以做共享多頁面等等….,多個管理層之間也會涉及互動的問題,如果物件與物件、頁面與頁面直接產生一對一或多對多的關係那麼就直接產生了強耦合,久而久之會形成一種網狀的交叉引用,當修改其中一個任意物件時,會難以避免引起其他的問題,所以在物件與物件之間互動通訊應該要增加一箇中介物件,相關物件都通過中介物件來通訊,而不是互相引用

image

如圖,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事件,我們可以在這個事件中呼叫全域性翻頁提供的方法讓全域性可以滑動,這裡由於功能單一,我就提供下原始碼的截圖,去掉了一些註釋,保留了處理的方法

`9FHS@UKHW6VUC)RH5G[S2J

通過擴充套件了iscroll提供的幾個介面,不改變iscroll自身的情況下,動態的給iscroll物件新增了新的行為,實現了滑動、反彈、翻頁的使用者響應,這就是簡單的裝飾模式的體現。在沒有改變iscroll內部原始碼的前提下,通過擴充套件的一些額外的程式碼就實現了新的功能,這樣其實是遵循了”開放-封閉”的原則

簡單工廠模式

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

外部引入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.支援自適應圖片解析度,配置不同的圖片模式

……

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

相關文章