前言
隨著Service Worker(以下簡稱SW)的普及和規范,我們可以使用SW提供的緩存接口替代HTTP緩存。當然SW的功能是強大的,除了緩存功能,還能夠使用它來實現離線、數據同步、后臺編譯等等。
應用
一個標配版的sw緩存工代代碼應該有以下的片段:
const version = '2'; self.addEventListener('install', event => { event.waitUntil( caches.open(`static-${version}`) .then(cache => cache.addAll([ '/styles.css', '/script.js' ])) ); }); self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => response || fetch(event.request)) ); });
首先你要明白的前提是,網絡請求首先到達的是SW腳本中,如果未命中再轉發給HTTP緩存。
這段代碼的意思是,在SW的install階段我們將script.js和styles.css放入緩存中;而在請求發起的fetch階段,通過資源的URL去緩存內查找匹配,成功后立刻返回,否則走正常的網絡請求流程。
但你有沒有考慮過,在install階段的資源內容是哪里來的?仍然是從HTTP緩存中。這樣SW緩存機制又有可能隨著HTTP緩存陷入了之前所說的版本不一致的困境中。
既然我們借助SW重寫了緩存機制,所以也不想再受牽制于舊的HTTP緩存。解決辦法是讓SW中的請求必須向服務端驗證:
self.addEventListener('install', event => { event.waitUntil( caches.open(`static-${version}`) .then(cache => cache.addAll([ new Request('/styles.css', { cache: 'no-cache' }), new Request('/script.js', { cache: 'no-cache' }) ])) ); });
目前并非所有的瀏覽器都支持cache選項的配置。但這個不是太大問題,我們可以通過添加隨機數來保證每次請求的URL都不相同,間接的使得緩存失效:
self.addEventListener('install', event => { event.waitUntil( caches.open(`static-${version}`) .then(cache => Promise.all( [ '/styles.css', '/script.js' ].map(url => { // cache-bust using a random query string return fetch(`${url}?${Math.random()}`).then(response => { // fail on 404, 500 etc if (!response.ok) throw Error('Not ok'); return cache.put(url, response); }) }) )) ); });
上面的代碼使用的是隨機數作為文件版本,你當然可以使用更精確的方式,例如根據文件內容生成md5值來作為版本信息,而這個思維模式就是模塊sw-precache模塊的背后哲學。
sw-precache
想象一下現在我們需要實施上述繞過http緩存的解決方案。首先我們需要知道究竟站點中有多少靜態資源,然后設定版本號的生成規則,接著根據靜態資源再具體的編寫我們的SW腳本。
不難看出,上面描述的過程可以是機械化自動化的,包括識別靜態資源,生成SW腳本等。而類庫sw-precache則可以幫我們完成這些工作。尤其是在構建階段配合Gulp或者Grunt使用,具體用法我們可以摘錄它官網的一段DEMO:
gulp.task('generate-service-worker', function(callback) { var swPrecache = require('sw-precache'); var rootDir = 'App'; swPrecache.write(`${rootDir}/service-worker.js`, { staticFileGlobs: [rootDir + '/**/*.{js,html,css,png,jpg,gif,svg,eot,ttf,woff}'], stripPrefix: rootDir }, callback); });
這段腳本注冊了一個名為generate-service-worker的任務,用于在根目錄生成一個名為service-worker.js的sw腳本,而這個腳本緩存的資源呢,則是目錄下的所有腳本、樣式、圖片、字體等幾乎所有的靜態文件。
瀏覽器的整體緩存機制
除了HTTP標準緩存以外,瀏覽器還有可能存在標準以外的緩存機制。對于Chrome瀏覽器而言還存在Memory Cache、Push “Cache”。一個請求在查找資源的過程中經過的緩存順序是Memory Cache、Service Worker、HTTP Cache、Push “Cache”。HTTP Cache和Service Worker已經介紹過了,接下來簡單介紹Memory Cache和Push Cache
Memory Cache
“內存緩存”中主要包含的是當前文檔中頁面中已經抓取到的資源。例如頁面上已經下載的樣式、腳本、圖片等。我們不排除頁面可能會對這些資源再次發出請求,所以這些資源都暫存在內存中,當用戶結束瀏覽網頁并且關閉網頁時,內存緩存的資源會被釋放掉。
這其中最重要的緩存資源其實是preloader相關指令(例如<link rel="prefetch">)下載的資源??偹苤猵reloader的相關指令已經是頁面優化的常見手段之一,而通過這些指令下載的資源也都會暫存到內存中。根據一些材料,如果資源已經存在于緩存中,則可能不會再進行preload。
需要注意的事情是,內存緩存在緩存資源時并不關心返回資源的HTTP緩存頭Cache-Control是什么值,同時資源的匹配也并非僅僅是對URL做匹配,還可能會對Content-Type,CORS等其他特征做校驗
Push “Cache”
“推送緩存”是針對HTTP/2標準下的推送資源設定的。推送緩存是session級別的,如果用戶的session結束則資源被釋放;即使URL相同但處于不同的session中也不會發生匹配。推送緩存的存儲時間較短,在Chromium瀏覽器中只有5分鐘左右,同時它也并非嚴格執行HTTP頭中的緩存指令
Push “Cache”的優缺點
關于推送緩存,主要有以下幾大特點:
- 幾乎所有的資源都能被推送,并且能夠被緩存。測試過程是作者在推送資源之后嘗試用fetch()、XMLHttpRequest、<link rel="stylesheet" href="…">、<script src="…">、<iframe src="…">獲取推送的資源。Edge和Safari瀏覽器支持相對比較差
- no-cache和no-store資源也能被推送
- Push Cache是最后一道緩存機制(之前會經過Memory Cache、HTTP Cache、Service Worker)
- 如果連接被關閉則Push Cache被釋放
- 多個頁面可以使用同一個HTTP/2的連接,也就可以使用同一個Push Cache。這主要還是依賴瀏覽器的實現而定,出于對性能的考慮有的瀏覽器會對相同域名但不同的tab標簽使用同一個HTTP連接。
- 一旦Push Cache中的資源被使用即被移除如果Push Cache或者HTTP Cache已經存在被推送的資源,則有可能瀏覽器拒絕推送
- 你可以為其他域名推送資源