通過閱讀如何優(yōu)雅的為 PWA 注冊(cè) Service Worker一文,相信大家已經(jīng)對(duì)什么是 Service Worker 以及如何使用注冊(cè)有所了解。對(duì)于一些比較簡(jiǎn)單的小型站點(diǎn)這已足夠,但當(dāng)我們的業(yè)務(wù)壯大,站點(diǎn)復(fù)雜之后,會(huì)出現(xiàn)一些新的問題。
通常一個(gè)復(fù)雜的 WEB 站點(diǎn)的內(nèi)容會(huì)由多個(gè)模塊提供,這些模塊可以擁有自己的 URL pattern(如用戶模塊/user,信息模塊/about等等)。它們相互獨(dú)立,自己決定自己的展現(xiàn)內(nèi)容,邏輯,樣式等等。理所當(dāng)然地,如果我們引入 Service Worker 優(yōu)化站點(diǎn),那么每個(gè)模塊也應(yīng)當(dāng)能夠管理自身使用到的靜態(tài)資源。
一個(gè)大家比較熟知的例子是百度搜索結(jié)果頁(yè)。當(dāng)我們搜索關(guān)鍵詞“北京旅游”,能夠得到的頁(yè)面其實(shí)由不同的模塊生成,如下:
這個(gè)頁(yè)面上了多個(gè)模塊的渲染結(jié)果,仔細(xì)觀察網(wǎng)絡(luò)請(qǐng)求不難發(fā)現(xiàn),在請(qǐng)求每個(gè)模塊的靜態(tài)資源時(shí),header 信息中所攜帶的 referrer 是各不相同的,因此實(shí)際上和多個(gè)模塊擁有不同 URL 的情況是類似的。
Service Worker 組織方式面對(duì)由多個(gè)模塊組成的站點(diǎn),Service Worker有兩種組織方式。
全局僅注冊(cè)一個(gè) service-worker.js, scope 設(shè)置為 '/' ,統(tǒng)一管理所有模塊的靜態(tài)資源。每個(gè)模塊注冊(cè)一個(gè) service-worker.js,scope 設(shè)置為每個(gè)模塊的 URL pattern ,互相獨(dú)立地管理自身方案1的缺點(diǎn)顯而易見,任何模塊對(duì)緩存內(nèi)容或策略的修改都必須同步到全局 js 文件中,因此這個(gè) js 文件的維護(hù)是一個(gè)重點(diǎn)。方案2看起來(lái)更加符合獨(dú)立的思路,因此方案2勝出?
如果一個(gè)站點(diǎn)的所有模塊都是互斥的,那方案2的確更優(yōu)秀一些,但實(shí)際操作中并沒有如此理想。一個(gè)最簡(jiǎn)單的例子,主頁(yè)一般 URL 為 /,那么他所在的模塊的 scope 就會(huì)包含其他模塊。一旦有了這樣的父子層級(jí),會(huì)導(dǎo)致主頁(yè)模塊的 Service Worker 能緩存其他所有模塊的靜態(tài)資源。這樣靜態(tài)資源會(huì)存在多份,就會(huì)出現(xiàn)不同步和更新的問題。
因此我們其實(shí)更傾向于方案1,那么有沒有方法用程序生成這個(gè)全局的 Service Worker,減低維護(hù)成本呢?
解決方案我們可以通過請(qǐng)求靜態(tài)資源時(shí) header 的 referrer 信息(即模塊的 URL pattern)來(lái)把模塊相互區(qū)分開來(lái)。每個(gè)模塊提供自身的緩存配置信息,主要包含三點(diǎn)內(nèi)容:
如何確定是本模塊(提供自身的 referrer 規(guī)則)本模塊需要緩存哪些靜態(tài)資源本模塊需要緩存的靜態(tài)資源應(yīng)該以什么策略緩存通過每個(gè)模塊提供的配置信息,系統(tǒng)自動(dòng)生成一份 service-worker.js,經(jīng)由下方的注冊(cè)代碼,可以實(shí)現(xiàn)為每個(gè)模塊分別緩存數(shù)據(jù),互不影響,從而解決上述問題。
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js').then(function (registration) { // registration was successful console.log('service worker registration successful with scope ', registration.scope); }).catch(function (err) { // registration failed console.log(err); }); }那么問題歸結(jié)為如何通過這些配置信息生成 service-worker.js。
設(shè)計(jì)思路在實(shí)際設(shè)計(jì)解決方案之前,讓我們先考慮下除了滿足“互相隔離”,“自動(dòng)快速生成”等功能性需求之外,還有什么需要考慮的?這里有幾點(diǎn)參考:
緩存策略易于擴(kuò)展模塊較多時(shí)擁有較好的性能,否則起不到 PWA 本身的目的各類瀏覽器和環(huán)境下比較完備的基于這樣的需求,我們給出一種設(shè)計(jì)思路,供大家討論交流:
sw-base對(duì)外唯一接口,提供 add, precache 等方法對(duì)需要緩存的規(guī)則或者文件進(jìn)行注冊(cè)。此外還應(yīng)在這里調(diào)用 service worker 的 self.addEventListener 方法進(jìn)行注冊(cè)。
defaults記錄配置項(xiàng)和緩存策略的默認(rèn)值,同時(shí)也確保用戶配置文件的合法性,過濾非法項(xiàng)并和默認(rèn)值進(jìn)行合并
precache記錄預(yù)緩存文件列表
strategy保存所有緩存策略,由統(tǒng)一出口(如 index.js)對(duì)外暴露的接口,這樣成功將策略部分和上層部分解耦,保證緩存策略的可擴(kuò)展性
router提供 add 和 ** tch 方法。add 用于添加路由規(guī)則, ** tch 用于匹配路由規(guī)則并選擇緩存規(guī)則。其中匹配部分優(yōu)先匹配 referrer 再匹配路由規(guī)則,因此在模塊較多時(shí)是先選定一個(gè)模塊再匹配路由規(guī)則,避免匹配內(nèi)容過多影響性能。
listeners提供 service worker 的各個(gè)階段事件的響應(yīng)函數(shù),如 install, activate 和 fetch 事件。
此外,最外層還應(yīng)有一個(gè)主入口文件(如 ** in.js),將所有位于配置目錄下的配置文件加載并逐個(gè)調(diào)用 sw-base 的 add 和 precache 方法,完成注冊(cè)。
其他細(xì)節(jié)A. 構(gòu)建
讓我們先理清有哪些構(gòu)建的要求:
最基本的代碼合并和混淆,最終生成一個(gè) service-worker.js 文件插入 serviceworker-cache-polyfill 以解決 cache 接口在舊版瀏覽器的兼容問題源代碼采用 es6/7 編寫,因此需要引入 babel 編譯成 es5為了支持,需要一個(gè)額外的入口引入相關(guān)的緩存配置,而非所有正式緩存配置顯然 webpack 可以很好的完成這些構(gòu)建需求。
B.
我們可以參考 sw-toolbox 的方法,在構(gòu)建出供的 service-worker.js 之后,進(jìn)行如下流程:
使用 selenium-assistant 下載需要的瀏覽器內(nèi)核(如 chrome, firefox 等)使用 express 編寫一個(gè)簡(jiǎn)易服務(wù)器,供使用使用 mocha 啟動(dòng)上述服務(wù)器,并逐個(gè)運(yùn)行用例用例可以涵蓋所有策略,也包括其他的一些功能(如配置項(xiàng),HTTP方法和預(yù)緩存等),基本思路都是先驗(yàn)證 cache 的某個(gè) key 能否正常讀寫以及淘汰機(jī)制等等。
C. 驗(yàn)證
通過,我們可以確認(rèn)項(xiàng)目自身代碼的可靠性(包括緩存策略本身以及使用策略等等)。但每個(gè)模塊的配置文件是由用戶自身提供的,并不能覆蓋這一部分,而錯(cuò)誤的配置文件會(huì)導(dǎo)致生成的 service-worker.js 對(duì)站點(diǎn)產(chǎn)生不可預(yù)知的影響(如多個(gè)模塊的 referrer 存在包含關(guān)系導(dǎo)致同一個(gè)文件出現(xiàn)多份緩存等)。因此我們需要對(duì)用戶提供的緩存配置項(xiàng)進(jìn)行驗(yàn)證。
為了支持驗(yàn)證,用戶在提交配置文件時(shí),也要提交符合自身提供規(guī)則的驗(yàn)證 URL。舉例來(lái)說(shuō),用戶提交的 referrer 為 '/user/*',那么也應(yīng)該提交一條符合這條規(guī)則的 validateReferrer,例如 '/user/info'。
驗(yàn)證主要分為兩大類:
內(nèi)部檢查檢查每條 validate 是否能被 urlPattern 匹配,如不能則報(bào)錯(cuò)檢查每條 precache 是否能被 urlPattern 匹配,如不能則報(bào)錯(cuò)交叉檢查 (兩兩比對(duì))檢查兩個(gè)配置文件的 name 是否相同,如相同則報(bào)錯(cuò)檢查配置A的每條 validate url 是否能被配置B的 urlPattern 匹配,如能則報(bào)錯(cuò)具體實(shí)現(xiàn)事實(shí)上我們根據(jù)上述設(shè)計(jì)思路已經(jīng)實(shí)現(xiàn)了一個(gè)項(xiàng)目 crater (github),并將于近期在百度搜索結(jié)果頁(yè)進(jìn)行上線。上線之后,將對(duì)百度搜索結(jié)果頁(yè)中的兩類內(nèi)容(普通搜索結(jié)果和智能搜索聚合)的靜態(tài)資源進(jìn)行緩存,以提升訪問性能和用戶體驗(yàn)。
也歡迎各位同僚來(lái)到 github 項(xiàng)目社區(qū)提出寶貴建議和意見,共同打造以 PWA 為主的 WEB 新生態(tài),提升 WEB 站點(diǎn)的用戶體驗(yàn)。