作爲前端開發,有沒有不需要後端配合的緩存方式呢?下面,我們就一起來了解一下,在客戶端代碼控制web離線緩存的sevice worker。
Service Worker 是 Chrome 團隊提出和力推的一個 WEB API,用於給 web 應用提供高級的可持續的後臺處理能力。該 WEB API 標準起草於 2013 年,於 2014 年納入 W3C WEB 標準草案,當前還在草案階段。
Service workers 本質上充當Web應用程序與瀏覽器之間的代理服務器,也可以在網絡可用時作爲瀏覽器和網絡間的代理。它們能夠創建有效的離線體驗,攔截網絡請求並基於網絡是否可用以及更新的資源是否駐留在服務器上來採取適當的動作。他們還允許訪問推送通知和後臺同步API。
當瀏覽器發送請求時,首先到達sw腳本中,如果沒有命中,再轉發給http請求。
sevice worker的特點
網站必須使用 HTTPS。除了使用本地開發環境調試時(如域名使用 localhost)
運行於瀏覽器後臺,可以控制打開的作用域範圍下所有的頁面請求
單獨的作用域範圍,單獨的運行環境和執行線程
不能操作頁面 DOM。但可以通過事件機制來處理
sevice worker瀏覽器支持情況
sevice worker的生命週期
註冊 -> 安裝 -> 激活 -> 廢棄
註冊register
service worker URL 通過 serviceWorkerContainer.register() 來獲取和註冊。
如果註冊成功,service worker 就在 ServiceWorkerGlobalScope 環境中運行;這是一個特殊類型的 woker 上下文運行環境,與主運行線程(執行腳本)相獨立,同時也沒有訪問 DOM 的能力。
service worker 現在可以處理事件了
chrome 瀏覽器下,註冊成功後,可以打開 chrome://serviceworker-internals/ 查看瀏覽器的 Service Worker 信息
也可以在開發者工具中查看瀏覽器中sevice worker的情況註冊sevice worker:
f ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('sw.js', {scope: '/'})
.then(registration => {
console.log('ServiceWorker 註冊成功!作用域爲: ', registration.scope)
})
.catch(err => {
console.log('ServiceWorker 註冊失敗: ', err)
});
}
代碼解析:
sw.js是sevice worker的腳本,sevice worker的所有行爲邏輯都在其中呈現
sw.js所在的位置決定了sevice worker的作用域,作用域內所有的頁面請求都會被 sevice worker所監控, scope參數可以修改其作用域。
安裝(install)和更新
初次打開受 service worker 控制的頁面後,會觸發install事件
當 oninstall 事件的處理程序執行完畢後,可以認爲 service worker 安裝完成了
sw.js文件有更新,也會觸發install事件
如果 sw.js 文件的內容有改動,當訪問網站頁面時瀏覽器獲取了新的文件,它會認爲有更新,於是會安裝新的文件並觸發 install 事件。但是此時已經處於激活狀態的舊的 Service Worker 還在運行,新的 Service Worker 完成安裝後會進入 waiting 狀態。直到所有已打開的頁面都關閉,舊的 Service Worker 自動停止,新的 Service Worker 纔會在接下來打開的頁面裏生效 ![image](http://zhang-yue.oss-cn-beijing.aliyuncs.com/bingshan/截圖1561690352625.png )
可以在 install 事件中執行 skipWaiting 方法跳過 waiting 狀態,然後會直接進入 activate 階段。接着在 activate 事件發生時,通過執行 clients.claim 方法,更新所有客戶端上的 Service Worker。示例代碼:
// 安裝階段跳過等待,直接進入 active
self.addEventListener('install', function(event) {
event.waitUntil(self.skipWaiting());
});
激活active
當 service worker 安裝完成後,會接收到一個激活事件(activate event)。onactivate 主要用途是清理先前版本的service worker 腳本中使用的資源
通過監聽 activate 事件你可以做一些預處理,如對於舊版本的更新、對於無用緩存的清理等
在下面的示例中,我們實現對舊版本的緩存資源清理
this.addEventListener('activate', event => {
const cacheWhitelist = ['lzwme_cache_v1.6.0'];
event.waitUntil(
// 遍歷當前的緩存,刪除 指定版本號 之外的所有緩存
caches.keys().then(keyList => {
return Promise.all(keyList.map(key => {
if (cacheWhitelist.indexOf(key) === -1) {
return caches.delete(key);
}
}));
})
);
});
傳給 waitUntil() 的 Promise 會阻塞其他的事件,直到它完成。這可以確保清理操作會在第一次 fetch 事件之前完成
fetch事件與緩存策略
當瀏覽器發起請求時,會觸發 fetch 事件, fetch事件會攔截所有作用域內的請求
攔截請求後,根據request url和緩存做比對,如果當請求資源已經在緩存裏了,直接返回緩存裏的內容;否則使用 fetch API 繼續請求,並且將請求成功後的response存入緩存中
參考下面的示例:
self.addEventListener('fetch', function(event) {
// console.log('Handling fetch event for', event.request.url);
event.respondWith(
// Opens Cache objects that start with 'font'.
caches.open(CURRENT_CACHES['carry']).then(function(cache) {
return cache.match(event.request).then(function(response) {
// 如果命中了緩存,直接返回緩存中的數據
if (response) {
console.log(' Found response in cache:', response);
return response;
}
// 請求是stream,只能使用一次
var fetchRequest = event.request.clone();
return fetch(fetchRequest)
.then(function(response) {
//請求不成功,則不存入緩存
if(!response || response.status !== 200) {
return response;
}
// 如果沒有命中緩存,將請求和響應緩存到cache中
// 響應也是stream,只能使用一次,一次用於緩存,一次用於瀏覽器響應
var responseToCache = response.clone();
caches.open(CURRENT_CACHES['carry'])
.then(function(cache) {
// 抓取請求及其響應,並將其添加到給定的cache
cache.put(event.request, responseToCache);
});
return response;
});
}).catch(function(error) {
// Handles exceptions that arise from match() or fetch().
console.error(' Error in fetch handler:', error);
throw error;
});
})
);
});
緩存策略優化
指定緩存的接口類型,如只緩存js和css請求,其他類型依然發送http請求
清除舊緩存
給緩存增加版本號,當修改版本號之後,在active階段,所有上一版本的緩存都會被清空
以下完整代碼
var CACHE_VERSION = 2;
// Shorthand identifier mapped to specific versioned cache.
var CURRENT_CACHES = {
carry: 'version' + CACHE_VERSION
};
const cacheList = [
'css',
'js'
]
// 安裝階段跳過等待,直接進入 active
self.addEventListener('install', function(event) {
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', function(event) {
var expectedCacheNames = Object.keys(CURRENT_CACHES).map(function(key) {
return CURRENT_CACHES[key];
});
// Active worker won't be treated as activated until promise resolves successfully.
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
// 更新客戶端
clients.claim(),
// 清理舊版本
cacheNames.map(function(cacheName) {
if (expectedCacheNames.indexOf(cacheName) == -1) {
console.log('Deleting out of date cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});
self.addEventListener('fetch', function(event) {
console.log('Handling fetch event for', event.request.url);
let cached = cacheList.find(c => {return event.request.url.indexOf(c) !== -1 });
event.respondWith(
if(cached){
// 打開指定版本的緩存列表
// 每個cache對象和請求的request url 匹配
caches.open(CURRENT_CACHES['carry']).then(function(cache) {
return cache.match(event.request).then(function(response) {
// 如果命中了緩存,直接返回緩存中的數據
if (response) {
console.log(' Found response in cache:', response);
return response;
}
// 請求是stream,只能使用一次
var fetchRequest = event.request.clone();
return fetch(fetchRequest)
.then(function(response) {
if(!response || response.status !== 200) {
return response;
}
// 如果沒有命中緩存,將請求和響應緩存到cache中
// 響應也是stream,只能使用一次,一次用於緩存,一次用於瀏覽器響應
var responseToCache = response.clone();
caches.open(CURRENT_CACHES['carry'])
.then(function(cache) {
// 抓取請求及其響應,並將其添加到給定的cache
cache.put(event.request, responseToCache);
});
return response;
});
}).catch(function(error) {
// Handles exceptions that arise from match() or fetch().
console.error(' Error in fetch handler:', error);
throw error;
});
})
}else{
return fetch(fetchRequest)
.then(response => {
return response;
})
}
);
});
廢棄Redundant
導致廢棄的原因
安裝(install)失敗
激活(activating)失敗
新版本的 Service Worker 替換了它併成爲激活狀態
手動在瀏覽器開發者工具中停止sevice worker
總結
sevice worker的作用,遠遠不止請求資源緩存這一條,基於 Service Worker API 的特性,結合 Fetch API、Cache API、Push API、postMessage API 和 Notification API,可以在基於瀏覽器的 web 應用中實現 如離線緩存、消息推送、靜默更新等 native 應用常見的功能,以給 web 應用提供更好更豐富的使用體驗。