瀏覽器緩存和Service Worker

轉載;https://www.cnblogs.com/bill-shooting/archive/2018/07/21/9347441.htmljavascript

1. 傳統的HTTP瀏覽器緩存策略

在一個網頁的生命週期中,開發者爲了縮短用戶打開頁面的時間,一般會設置不少緩存。其中包括了:css

  • 瀏覽器緩存
  • 代理服務器緩存(CDN緩存)
  • 服務器緩存
  • 數據庫緩存

等各類緩存。這些緩存大多數和前端沒什麼關係,也不禁前端開發者控制,其中和前端較爲密切的是瀏覽器緩存,但它本質上也是由服務器控制的。html

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

下面介紹一下HTTP緩存的一些用法:java

  • Expires頭部
    早在HTTP協議被設計的時候,協議的起草者們就想到了緩存的事情,天然也有相應的功能,那就是Expires這個頭部。每當瀏覽器請求時,服務器能夠在相應的報文中附加這個Expires,它的典型值看起來是這樣的:
Expires: Tue, 01 May 2018 11:37:06 GMT

也就是在該資源在世界協調時2018/05/01 11:37:06才過時,個人請求時間是2018/05/01 07:37:06,因此就是這個資源在4小時以後過時,4小時以內對該資源的請求都直接使用緩存,除非你用Ctrl+F5刷新。可是呢,這種控制明顯是不夠精細的,這是個HTTP1.0協議中規定的頭部。因爲咱們如今都用HTTP2.0都已經來了,HTTP1.1已經全面普及了,這玩意天然已經用的很少了。程序員

  • Cache-Control頭部
    Expires頭部只能控制過時時間,萬一請求的資源在過時時間以前就更新了,那就可能會出現顯示或者功能問題。爲此,HTTP協議再更新到1.1版本的時候,增長了一個新的頭部Cache-Control並規定:若是同時存在Cache-ControlExpires前者有效。它有如下經常使用的值可選:public private max-age s-maxage no-cache no-store等。一個典型的值看起來是如下這樣:
Cache-Control: s-maxage=300, public, max-age=60

爲了更好的說明各個字段的意義,先說下瀏覽器請求資源的步驟:數據庫

  1. 判斷請求是否命中緩存,如命中則執行步驟2;如沒有則執行步驟3;
  2. 判斷緩存是否過時,如沒有則直接返回;如過時則執行步驟3,並帶上緩存信息;
  3. 瀏覽器向服務器請求資源;
  4. 服務器判斷緩存信息,如資源還沒有更新,則返回304,如沒有緩存信息或則資源已更新則返回200,並把資源返回。
  5. 瀏覽器根據響應頭部決定要不要存儲緩存(只有no-store時不存儲緩存信息)。

s-maxage表示共享緩存的時間,單位是s,也就是5分鐘;
public表示這是個共享緩存,能夠被其餘session使用;
max-age意義與s-maxage差很少,只是它用於private的情形;
no-cache這種策略下,瀏覽器會跳過步驟2,並帶上緩存信息向服務器發起請求。
no-store這種策略下,瀏覽器會跳過步驟5,因爲沒有緩存信息,每次瀏覽器請求時都不會帶上緩存信息,就像第一次請求同樣(Ctrl+F5效果)。json

  • Last-Modified/If-Modified-Since
    上面說了,瀏覽器在有緩存信息的狀況下,會帶上緩存信息發起請求,那這個信息是怎麼來的?又是怎麼帶在Request的頭部當中呢?
    原來,服務器在響應請求時,除了返回Cache-Control頭部外,還會返回一個Last-Modified頭部,用於指定該資源的服務器更新時間。當該資源在瀏覽器端過時時(由max-age或者no-cache決定),瀏覽器會帶上緩存信息去發起請求,這個信息就由Request中的If-Modified-Since指定,一般也就是上次Response中Last-Modified的值。典型值以下:
//Response: Last-Modified: Sat, 01 Jan 2000 00:00:00 GMT //Request: If-Modified-Since: Sat, 01 Jan 2000 00:00:00 GMT
  • Etag/If-None-Match
    Last-Modified/If-Modified-Since提供的控制已經比較多了,但有些時候,開發者仍是不滿意,由於它們只能提供對資源時間的控制,並只有精確到秒級。若是有些資源變化很是快,或者有些資源定時生成,但內容倒是同樣的,這些狀況下Last-Modified/If-Modified-Since就不是很適用。
    爲此,HTTP1.1規定了Etag/If-None-Match這兩個頭部,它們的用法和Last-Modified/If-Modified-Since徹底相同,一個用於響應,一個用於請求。只不過Etag用的不是時間,而是服務器規定的一個標籤(一般是資源內容、大小、時間的hash值)。這樣服務器經過這個頭部能夠更加啊精確地控制資源的緩存策略。
    一樣的,因爲這個頭部控制更加精細, 因此它的優先級會高於Last-Modified/If-Modified-Since,就像Cache-Control高於Expires同樣。

2. Service Worker的原理

HTTP緩存已經足夠強大了,那開發者還有什麼不滿意呢?後端的開發者天然沒什麼不滿意,前端的開發者就要嘀咕了:「瀏覽器的事情,爲何要依賴於後端呢?後端就好好提供數據就好了,緩存這種事情我想本身控制」。確實有人這麼嘗試過,就是以前說的用Local Storage或者Session Storage來存儲一些數據,但這種方法缺乏不少關鍵的瀏覽器基礎設施,好比異步存儲、靜態資源存儲、URL匹配、請求攔截等功能。而Service Worker的出現填補了這些基礎設施缺乏的問題。後端

須要指出的是,Service Worker並不是專門爲緩存而設計,它還能夠解決Web應用推送、後臺長計算等問題。能解決精細化緩存控制,實在是因爲它的功能強大,由於它本質上就是一個全新的JavaScript線程,運行在與主Javascript線程不一樣的上下文。service worker線程被設計成完成異步,一些本來在主線程中的同步API,如XMLHTTPRequestlocalStorage是不能在service worker中使用的。promise

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

  1. sw線程可以用來和服務器溝通數據(service worker的上下文內置了fetch和Push API)
  2. 可以用來進行大量複雜的運算而不影響UI響應。
  3. 它能攔截全部的請求(經過監聽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的生命流程:
sw生命流程.png-38.4kB

下面用一個簡單的例子來介紹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.css image-1.png script-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這套始於瀏覽器的技術,已經成爲跨平臺應用開發的主流技術。

 
相關文章
相關標籤/搜索