原文: https://juejin.im/post/5ba0fe356fb9a05d2c43a25c#comment
Service Worker學習與實踐(一)——離線緩存
什麼是Service Worker
Service Worker
本質上充當Web應用程序與瀏覽器之間的代理服務器,也可以在網絡可用時作爲瀏覽器和網絡間的代理。它們旨在(除其他之外)使得能夠創建有效的離線體驗,攔截網絡請求並基於網絡是否可用以及更新的資源是否駐留在服務器上來採取適當的動作。他們還允許訪問推送通知和後臺同步API
。
Service Worker
的本質是一個Web Worker
,它獨立於JavaScript
主線程,因此它不能直接訪問DOM
,也不能直接訪問window
對象,但是,Service Worker
可以訪問navigator
對象,也可以通過消息傳遞的方式(postMessage)與JavaScript
主線程進行通信。Service Worker
是一個網絡代理,它可以控制Web
頁面的所有網絡請求。Service Worker
具有自身的生命週期,使用好Service Worker
的關鍵是靈活控制其生命週期。
Service Worker
的作用
- 用於瀏覽器緩存
- 實現離線
Web APP
- 消息推送
Service Worker
兼容性
Service Worker
是現代瀏覽器的一個高級特性,它依賴於fetch API
、Cache Storage
、Promise
等,其中,Cache
提供了Request / Response
對象對的存儲機制,Cache Storage
存儲多個Cache
。
示例
在瞭解Service Worker
的原理之前,先來看一段Service Worker
的示例:
self.importScripts('./serviceworker-cache-polyfill.js');
var urlsToCache = [
'/',
'/index.js',
'/style.css',
'/favicon.ico',
];
var CACHE_NAME = 'counterxing';
self.addEventListener('install', function(event) {
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
self.addEventListener('activate', function(event) {
var cacheWhitelist = ['counterxing'];
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
複製代碼
下面開始逐段逐段地分析,揭開Service Worker
的神祕面紗:
polyfill
首先看第一行:self.importScripts('./serviceworker-cache-polyfill.js');
,這裏引入了Cache API的一個polyfill,這個polyfill
支持使得在較低版本的瀏覽器下也可以使用Cache Storage API
。想要實現Service Worker
的功能,一般都需要搭配Cache API
代理網絡請求到緩存中。
在Service Worker
線程中,使用importScripts
引入polyfill
腳本,目的是對低版本瀏覽器的兼容。
Cache Resources List
And Cache Name
之後,使用一個urlsToCache
列表來聲明需要緩存的靜態資源,再使用一個變量CACHE_NAME
來確定當前緩存的Cache Storage Name
,這裏可以理解成Cache Storage
是一個DB
,而CACHE_NAME
則是DB
名:
var urlsToCache = [
'/',
'/index.js',
'/style.css',
'/favicon.ico',
];
var CACHE_NAME = 'counterxing';
複製代碼
Lifecycle
Service Worker
獨立於瀏覽器JavaScript
主線程,有它自己獨立的生命週期。
如果需要在網站上安裝Service Worker
,則需要在JavaScript
主線程中使用以下代碼引入Service Worker
。
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
console.log('成功安裝', registration.scope);
}).catch(function(err) {
console.log(err);
});
}
複製代碼
此處,一定要注意sw.js
文件的路徑,在我的示例中,處於當前域根目錄下,這意味着,Service Worker
和網站是同源的,可以爲當前網站的所有請求做代理,如果Service Worker
被註冊到/imaging/sw.js
下,那隻能代理/imaging
下的網絡請求。
可以使用Chrome
控制檯,查看當前頁面的Service Worker
情況:
安裝完成後,Service Worker
會經歷以下生命週期:
- 下載(
download
) - 安裝(
install
) - 激活(
activate
)
-
用戶首次訪問
Service Worker
控制的網站或頁面時,Service Worker
會立刻被下載。之後至少每24
小時它會被下載一次。它可能被更頻繁地下載,不過每24
小時一定會被下載一次,以避免不良腳本長時間生效。 -
在下載完成後,開始安裝
Service Worker
,在安裝階段,通常需要緩存一些我們預先聲明的靜態資源,在我們的示例中,通過urlsToCache
預先聲明。 -
在安裝完成後,會開始進行激活,瀏覽器會嘗試下載
Service Worker
腳本文件,下載成功後,會與前一次已緩存的Service Worker
腳本文件做對比,如果與前一次的Service Worker
腳本文件不同,證明Service Worker
已經更新,會觸發activate
事件。完成激活。
如圖所示,爲Service Worker
大致的生命週期:
install
在安裝完成後,嘗試緩存一些靜態資源:
self.addEventListener('install', function(event) {
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});
複製代碼
首先,self.skipWaiting()
執行,告知瀏覽器直接跳過等待階段,淘汰過期的sw.js
的Service Worker
腳本,直接開始嘗試激活新的Service Worker
。
然後使用caches.open
打開一個Cache
,打開後,通過cache.addAll
嘗試緩存我們預先聲明的靜態文件。
監聽fetch
,代理網絡請求
頁面的所有網絡請求,都會通過Service Worker
的fetch
事件觸發,Service Worker
通過caches.match
嘗試從Cache
中查找緩存,緩存如果命中,則直接返回緩存中的response
,否則,創建一個真實的網絡請求。
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
複製代碼
如果我們需要在請求過程中,再向Cache Storage
中添加新的緩存,可以通過cache.put
方法添加,看以下例子:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// 緩存命中
if (response) {
return response;
}
// 注意,這裏必須使用clone方法克隆這個請求
// 原因是response是一個Stream,爲了讓瀏覽器跟緩存都使用這個response
// 必須克隆這個response,一份到瀏覽器,一份到緩存中緩存。
// 只能被消費一次,想要再次消費,必須clone一次
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(
function(response) {
// 必須是有效請求,必須是同源響應,第三方的請求,因爲不可控,最好不要緩存
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 消費過一次,又需要再克隆一次
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
複製代碼
在項目中,一定要注意控制緩存,接口請求一般是不推薦緩存的。所以在我自己的項目中,並沒有在這裏做動態的緩存方案。
activate
Service Worker
總有需要更新的一天,隨着版本迭代,某一天,我們需要把新版本的功能發佈上線,此時需要淘汰掉舊的緩存,舊的Service Worker
和Cache Storage
如何淘汰呢?
self.addEventListener('activate', function(event) {
var cacheWhitelist = ['counterxing'];
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
複製代碼
- 首先有一個白名單,白名單中的
Cache
是不被淘汰的。 - 之後通過
caches.keys()
拿到所有的Cache Storage
,把不在白名單中的Cache
淘汰。 - 淘汰使用
caches.delete()
方法。它接收cacheName
作爲參數,刪除該cacheName
所有緩存。
sw-precache-webpack-plugin
sw-precache-webpack-plugin是一個webpack plugin
,可以通過配置的方式在webpack
打包時生成我們想要的sw.js
的Service Worker
腳本。
一個最簡單的配置如下:
var path = require('path');
var SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
const PUBLIC_PATH = 'https://www.my-project-name.com/'; // webpack needs the trailing slash for output.publicPath
module.exports = {
entry: {
main: path.resolve(__dirname, 'src/index'),
},
output: {
path: path.resolve(__dirname, 'src/bundles/'),
filename: '[name]-[hash].js',
publicPath: PUBLIC_PATH,
},
plugins: [
new SWPrecacheWebpackPlugin(
{
cacheId: 'my-project-name',
dontCacheBustUrlsMatching: /\.\w{8}\./,
filename: 'service-worker.js',
minify: true,
navigateFallback: PUBLIC_PATH + 'index.html',
staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
}
),
],
}
複製代碼
在執行webpack
打包後,會生成一個名爲service-worker.js
文件,用於緩存webpack
打包後的靜態文件。
一個最簡單的示例。
Service Worker Cache
VS Http Cache
對比起Http Header
緩存,Service Worker
配合Cache Storage
也有自己的優勢:
- 緩存與更新並存:每次更新版本,藉助
Service Worker
可以立馬使用緩存返回,但與此同時可以發起請求,校驗是否有新版本更新。 - 無侵入式:
hash
值實在是太難看了。 - 不易被沖掉:
Http
緩存容易被沖掉,也容易過期,而Cache Storage
則不容易被沖掉。也沒有過期時間的說法。 - 離線:藉助
Service Worker
可以實現離線訪問應用。
但是缺點是,由於Service Worker
依賴於fetch API
、依賴於Promise
、Cache Storage
等,兼容性不太好。
後話
本文只是簡單總結了Service Worker
的基本使用和使用Service Worker
做客戶端緩存的簡單方式,然而,Service Worker
的作用遠不止於此,例如:藉助Service Worker
做離線應用、用於做網絡應用的推送(可參考push-notifications)等。
甚至可以藉助Service Worker
,對接口進行緩存,在我所在的項目中,其實並不會做的這麼複雜。不過做接口緩存的好處是支持離線訪問,對離線狀態下也能正常訪問我們的Web
應用。
Cache Storage
和Service Worker
總是分不開的。Service Worker
的最佳用法其實就是配合Cache Storage
做離線緩存。藉助於Service Worker
,可以輕鬆實現對網絡請求的控制,對於不同的網絡請求,採取不同的策略。例如對於Cache
的策略,其實也是存在多種情況。例如可以優先使用網絡請求,在網絡請求失敗時再使用緩存、亦可以同時使用緩存和網絡請求,一方面檢查請求,一方面有檢查緩存,然後看兩個誰快,就用誰。