1. 程式人生 > 實用技巧 >瀏覽器快取和Service Worker

瀏覽器快取和Service Worker

原文地址:https://www.cnblogs.com/bill-shooting/archive/2018/07/21/9347441.html

1. 傳統的HTTP瀏覽器快取策略

在一個網頁的生命週期中,開發者為了縮短使用者開啟頁面的時間,通常會設定很多快取。其中包括了:

  • 瀏覽器快取
  • 代理伺服器快取(CDN快取)
  • 伺服器快取
  • 資料庫快取

等各種快取。這些快取大多數和前端沒什麼關係,也不由前端開發者控制,其中和前端較為密切的是瀏覽器快取,但它本質上也是由伺服器控制的。

在Service Worker還未問世之前,瀏覽器快取主要是由HTTP快取策略和瀏覽器內建的儲存功能(cookie,Local Storage,Session Storage等)來提供。其中HTTP快取由於是欽定的,根正苗紅,瀏覽器支援的也很好,是最常用的瀏覽器快取技術。而通過瀏覽器內建儲存功能來實現快取,相比之下就沒那麼高階大氣上檔次了。因為這種方式沒個標準正規化,雖說可以通過JS進行控制顯得比HTTP快取靈活,但效果嘛就只能依賴程式設計師的水平了,也沒有個統一的輪子能用,所以這種方式也就是小打小鬧,不成氣候。

強制快取(Expires和Cache-Control)優先於協商快取進行(Last-Modified / If-Modified-Since和Etag / If-None-Match),若強制快取生效則直接使用快取,若不生效則進行協商快取。

協商快取由伺服器決定是否使用快取,若協商快取失效,那麼代表該請求的快取失效,重新獲取請求結果,再存入瀏覽器快取中;生效則返回304,繼續使用快取。

2. Service Worker的原理

HTTP快取已經足夠強大了,那開發者還有什麼不滿意呢?後端的開發者自然沒什麼不滿意,前端的開發者就要嘀咕了:“瀏覽器的事情,為什麼要依賴於後端呢?後端就好好提供資料就行了,快取這種事情我想自己控制”。確實有人這麼嘗試過,就是之前說的用Local Storage或者Session Storage來儲存一些資料,但這種方法缺少很多關鍵的瀏覽器基礎設施,比如非同步儲存、靜態資源儲存、URL匹配、請求攔截等功能。而Service Worker的出現填補了這些基礎設施缺少的問題。

概述:一個伺服器與瀏覽器之間的中間人角色,如果網站中註冊了service worker那麼它可以攔截當前網站所有的請求,進行判斷(需要編寫相應的判斷程式),如果需要向伺服器發起請求的就轉給伺服器,如果可以直接使用快取的就直接返回快取不再轉給伺服器。從而大大提高瀏覽體驗。

需要指出的是,Service Worker並非專門為快取而設計,它還可以解決Web應用推送、後臺長計算等問題。能解決精細化快取控制,實在是由於它的功能強大,因為它本質上就是一個全新的JavaScript執行緒,執行在與主Javascript執行緒不同的上下文。service worker執行緒被設計成完成非同步,一些原本在主執行緒中的同步API,如XMLHTTPRequest

localStorage是不能在service worker中使用的。

主Javascript執行緒是負責DOM的執行緒,而service worker執行緒被設計成無法訪問DOM。這是很自然的,一般從事過客戶端開發的開發者都知道,只能有一個UI執行緒,否則整個UI的控制會出現不可預估的問題。而保證UI順滑不卡頓的原則就是儘量不在UI執行緒做大量計算和同步IO處理

sw執行緒能夠用來和伺服器溝通資料(service worker的上下文內建了fetch和Push API)
能夠用來進行大量複雜的運算而不影響UI響應。
它能攔截所有的請求(通過監聽fetch事件,任何對網路資源的請求都會觸發該事件),並內建了一個完全非同步的儲存系統(Caches屬性,完全非同步並能儲存全部種類的網路資源),這是它能精細化控制快取的關鍵。

可以看出service worker功能非常強大,特別是攔擊所有請求、充當代理伺服器這個功能,是強大而危險的。所以為了這個功能不被別有用心的人利用,service worker必須執行在HTTPS的Origin中,同時localhost也被認為是安全的,可以用於除錯開發使用。

3. Service Worker的快取

如前所述,service worker如果用於快取則關鍵在於監聽Fetch事件管理Cache資源,不過在使用它們之前,得先把service worker啟用才行。而service worker的啟用則要經過以下步驟:

  1. 瀏覽器發現當前頁面註冊了service worker(通過navigator.service.Worker.register('/sw.js'));
  2. 瀏覽器下載sw.js並執行,完成安裝階段;
  3. service worker等待Origin中其他worker失效,然後完成啟用階段;
  4. service worker生效,注意它的生效範圍不是當前頁面,而是整個Origin。但是隻有是在register()成功之後開啟的頁面才受SW控制。所以執行註冊指令碼的頁面通常需要過載一下,以便讓SW獲得完全的控制。

下圖是整個service worker的生命流程:

下面用一個簡單的例子來介紹service worker如何控制快取,通常它在index.html中被註冊:
程式碼清單:index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <link href="style/style-1.css" rel-"stylesheet">
    </head>
    <body>
        <img src="image/image-1.png" />
        <script async src="js/script-1.js"></script>
        <script>
            if ('serivceWorker' in navigator) {
                navigator.serviceWorker.register('/sw.js')
                    .then(reg => console.log('Service worker registered successfully!'))
                    .catch(err => console.log('Service worker failed to register!'));
            }
        </script>
    </body>
</html>

可以看到這個頁面有4個資源style-1.cssimage-1.pngscript-1.js以及sw.js。當頁面中JS執行到register方法時,瀏覽器下載sw.js並根據sw.js內容準備安裝Service worker。
程式碼清單: sw.js

let cacheName = 'indexCache';
self.addEventListener('install', event => {
    //waitUntil接受一個Promise,直到這個promise被resolve,安裝階段才算結束
    event.waitUntil(
        caches.open(cacheName)
            .then(cache => cacheAll(['/style/style-1.css',
                                     '/image/image-1.png',
                                     '/script/script-1.js',
                                    ]))
                    );
});

//監聽activate事件,可以在這個事件裡情況上個sw快取的內容
self.addEventListener('activate', event => ...}

//監聽fetch事件,可以攔截所有請求並處理
self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
            .then(res => {
                //1. 如果請求的資源已被快取,則直接返回
                if (res) return res;
                //2. 沒有,則發起請求並快取結果
                let requestClone = event.request.clone();
                return fetch(requestClone).then(netRes => {
                    if(!netRes || netRes.status !== 200) return netRes;
                    let responseClone = netRes.clone();
                    caches.open(cacheName).then(cache => cache.put(requestClone, responseClone));
                    return netRes;
                });
            })
    );
});

可以看到,service worker在安裝時就快取了三個資原始檔,如果下次該Origin下有頁面對這三個資源發起請求,則會被Fetch事件攔截,然後直接用快取返回。如果對其他資源發起請求,則會使用網路資源作為響應,並把這些資源再次儲存起來。

可以看到僅用幾十行程式碼就完成了一個非常強大的快取控制功能,你還可以對特定的幾個資源做自己的處理,取決你想怎麼控制你的資源。目前還有一個問題尚待解決,那就是如果資源更新了,快取該怎麼辦?目前有兩種方法可以做到:

  1. 更新sw.js檔案,一旦瀏覽器發現安裝使用的sw.js是不同的(通過計算hash值),瀏覽器就會重新安裝service worker,你可以在安裝啟用的過程中清空之前的快取,這樣瀏覽器就會使用伺服器上最新的資源。
  2. 對資原始檔進行版本控制,就像我上面的例子一樣你可以用style-2.css來代替style-1.css,這樣service worker就會使用新的資源並快取它。當然版本號不應該這麼簡單,最好是使用檔案的內容+修改時間+大小的hash值來作為版本號。

以上兩種方法都是可靠的,第一種方法的可靠性由瀏覽器保證,第二種方法則是已經久經考驗,目前大多數網站的靜態資源更新策略都是用的類似於第二種方法的版本控制。這兩種方法通常會混在一起使用,因為你在調整資源的版本號的時候,必須要更新sw.js中資源列表,導致sw.js檔案本身就修改。

還有個問題需要注意,那就是sw.js本身也會被HTTP快取策略快取。通過對sw.js檔名進行版本控制,可以避免因為service worker安裝檔案被快取而導致資源更新不及時的問題。

4. Service Worker離線應用

前面說過,service worker的出現並不是單純的為解決精細化控制瀏覽器快取問題的。它能充當代理伺服器這一能力(通過攔截所有請求實現),能夠實現HTTP快取無法實現的功能:離線應用。因為在HTTP快取策略下,如果一個資源過了伺服器規定的到期時間,則必須要發起請求,一旦網路連線有問題,整個網站就會出現功能問題。而在service worker控制下的快取,能夠在程式碼中發現網路連線問題並直接返回快取的資源。這種方式返回的響應對於瀏覽器來說是透明的,它會認為該響應就是伺服器傳送回來的資源。

藉助於上述能力以及service worker帶來的推送能力,基於Web的應用已經能夠媲美原生應用了。谷歌將這種Web應用稱為PWA(Progressive Web Application)。

隨著Web應用的功能越來越強大,安卓和IOS上套殼應用越來越多,最近微軟也宣佈win 10 上UWP應用可以採用PWA模式開發。至此跨平臺應用開發的主流技術變得越來越清晰起來,業界在經歷了Java-SWT,QT,Xamarin的嘗試之後,HTML+CSS+Javascript這套始於瀏覽器的技術,已經成為跨平臺應用開發的主流技術。