PWA—新生代手機APP
- 2019 年 10 月 6 日
- 筆記
最近公司里讓我研究PWA,經過幾天的研究發現PWA竟是如此萬能,功能接近手機原生APP,最大的優點是它的離線快取機制,這主要得益於ServiceWorker!
離線只是它的一種功能表現而已,具體說來,它可以:
- 讓我們的Web App在無網(offline)情況下可以訪問,甚至使用部分功能,而不是展示「無網路連接」的錯誤頁;
- 讓我們在弱網的情況下,能使用快取快速訪問我們的應用,提升體驗;
- 在正常的網路情況下,也可以通過各種自發控制的快取方式來節省部分請求頻寬;

那麼,Service Worker是什麼呢?你可以把Service Worker簡單理解為一個獨立於前端頁面,在後台運行的進程。因此,它不會阻塞瀏覽器腳本的運行,同時也無法直接訪問瀏覽器相關的API(例如:DOM、localStorage等)。此外,即使在離開你的Web App,甚至是關閉瀏覽器後,它仍然可以運行。它就像是一個在Web應用背後默默工作的勤勞小蜜蜂,處理著快取、推送、通知與同步等工作。所以,要學習PWA,繞不開的就是Service Worker。
Service Worker是如何實現離線可用的?
Service Worker是如何讓我們在離線的情況下也能訪問Web App的。當然,離線訪問只是其中一種表現。
首先,我們想一下,當訪問一個web網站時,我們實際上做了什麼呢?總體上來說,我們通過與與伺服器建立連接,獲取資源,然後獲取到的部分資源還會去請求新的資源(例如html中使用的css、js等)。所以,粗粒度來說,我們訪問一個網站,就是在獲取/訪問這些資源。
可想而知,當處於離線或弱網環境時,我們無法有效訪問這些資源,這就是制約我們的關鍵因素。因此,一個最直觀的思路就是:如果我們把這些資源快取起來,在某些情況下,將網路請求變為本地訪問,這樣是否能解決這一問題?是的。但這就需要我們有一個本地的cache,可以靈活地將各類資源進行本地存取。

有了本地的cache還不夠,我們還需要能夠有效地使用快取、更新快取與清除快取,進一步應用各種個性化的快取策略。而這就需要我們有個能夠控制快取的「worker」——這也就是Service Worker的部分工作之一。順便多說一句,可能有人還記得 ApplicationCache 這個API。當初它的設計同樣也是為了實現Web資源的快取,然而就是因為不夠靈活等各種缺陷,如今已被Service Worker與cache API所取代了。
Service Worker有一個非常重要的特性:你可以在Service Worker中監聽所有客戶端(Web)發出的請求,然後通過Service Worker來代理,向後端服務發起請求。通過監聽用戶請求資訊,Service Worker可以決定是否使用快取來作為Web請求的返回。
下圖展示普通Web App與添加了Service Worker的Web App在網路請求上的差異:

這裡需要強調一下,雖然圖中好像將瀏覽器、SW(Service Worker)與後端服務三者並列放置了,但實際上瀏覽器(你的Web應用)和SW都是運行在你的本機上的,所以這個場景下的SW類似一個「客戶端代理」。
了解了基本概念之後,就可以具體來看下,我們如何應用這個技術來實現一個離線可用的Web應用。
如何使用Service Worker實現離線可用的「秒開」應用
註冊Service Worker
注意,我們的應用始終應該是漸進可用的,在不支援Service Worker的環境下,也需要保證其可用性。要實現這點,可以通過特性檢測,在index.js中來註冊我們的Service Worker(sw.js):

這裡我們將sw.js文件註冊為一個Service Worker,注意文件的路徑不要寫錯了。
值得一提的是,Service Worker的各類操作都被設計為非同步,用以避免一些長時間的阻塞操作。這些API都是以Promise的形式來調用的。所以你會在接下來的各段程式碼中不斷看到Promise的使用。如果你完全不了解Promise,可以先在這裡了解基本的Promise概念:Promise(MDN)和JavaScript Promise:簡介。
Service Worker的生命周期
當我們註冊了Service Worker後,它會經歷生命周期的各個階段,同時會觸發相應的事件。整個生命周期包括了:installing –> installed –> activating –> activated –> redundant。當Service Worker安裝(installed)完畢後,會觸發install事件;而激活(activated)後,則會觸發activate事件。

下面的例子監聽了install事件:

self
是Service Worker中一個特殊的全局變數,類似於我們最常見的window
對象。self
引用了當前這個Service Worker。
快取靜態資源
通過上一節,我們已經學會了如何添加事件監聽,來在合適的時機觸發Service Worker的相應操作。現在,要使我們的Web App離線可用,就需要將所需資源快取下來。我們需要一個資源列表,當Service Worker被激活時,會將該列表內的資源快取進cache。

可以看到,首先在cacheFiles
中我們列出了所有的靜態資源依賴。注意其中的'/'
,由於根路徑也可以訪問我們的應用,因此不要忘了將其也快取下來。當Service Worker install時,我們就會通過caches.open()
與cache.addAll()
方法將資源快取起來。這裡我們給快取起了一個cacheName
,這個值會成為這些快取的key。
上面這段程式碼中,caches
是一個全局變數,通過它我們可以操作Cache相關介面。
使用快取的靜態資源
到目前為止,我們僅僅是註冊了一個Service Worker,並在其install時快取了一些靜態資源。然而,如果這時運行這個demo你會發現——「圖書搜索」這個Web App依然無法離線使用。
為什麼呢?因為我們僅僅快取了這些資源,然而瀏覽器並不知道需要如何使用它們;換言之,瀏覽器仍然會通過向伺服器發送請求來等待並使用這些資源。那怎麼辦?
聰明的你應該想起來了,我們在文章前半部分介紹Service Worker時提到了「客戶端代理」——用Service Worker來幫我們決定如何使用快取。
下圖是一個簡單的策略:

- 瀏覽器發起請求,請求各類靜態資源(html/js/css/img);
- Service Worker攔截瀏覽器請求,並查詢當前cache;
- 若存在cache則直接返回,結束;
- 若不存在cache,則通過
fetch
方法向服務端發起請求,並返回請求結果給瀏覽器

fetch
事件會監聽所有瀏覽器的請求。e.respondWith()
方法接受Promise作為參數,通過它讓Service Worker向瀏覽器返回數據。caches.match(e.request)
則可以查看當前的請求是否有一份本地快取:如果有快取,則直接向瀏覽器返回cache
;否則Service Worker會向後端服務發起一個fetch(e.request)
的請求,並將請求結果返回給瀏覽器。
到目前為止,運行我們的demo:當第一聯網打開「圖書搜索」Web App後,所依賴的靜態資源就會被快取在本地;以後再訪問時,就會使用這些快取而不發起網路請求。因此,即使在無網情況下,我們似乎依舊能「訪問」該應用。
更新靜態快取資源
然而,如果你細心的話,會發現一個小問題:當我們將資源快取後,除非註銷(unregister)sw.js、手動清除快取,否則新的靜態資源將無法快取。
解決這個問題的一個簡單方法就是修改cacheName
。由於瀏覽器判斷sw.js是否更新是通過位元組方式,因此修改cacheName
會重新觸發install並快取資源。此外,在activate事件中,我們需要檢查cacheName
是否變化,如果變化則表示有了新的快取資源,原有快取需要刪除。

待續………