Service Worker的應用
Service worker本質上充當Web應用程序、瀏覽器與網絡(可用時)之間的代理服務器,這個API旨在創建有效的離線體驗,它會攔截網絡請求並根據網絡是否可用來采取適當的動作、更新來自服務器的的資源,它還提供入口以推送通知和訪問后台同步API。
描述
Service Worker本質上也是瀏覽器緩存資源用的,只不過他不僅僅是Cache,也是通過worker的方式來進一步優化,其基於h5的web worker,所以不會阻礙當前js線程的執行,其最主要的工作原理,1是后台線程,是獨立於當前網頁線程,2是網絡代理,在網頁發起請求時代理攔截,來返回緩存的文件。簡單來說Service Worker就是一個運行在后台的Worker線程,然后它會長期運行,充當一個服務,很適合那些不需要獨立的資源數據或用戶互動的功能,最常見用途就是攔截和處理網絡請求,以下是一些細碎的描述:
- 基於
web worker(一個獨立於JavaScript主線程的獨立線程,在里面執行需要消耗大量資源的操作不會堵塞主線程)。 - 在
web worker的基礎上增加了離線緩存的能力。 - 本質上充當
Web應用程序(服務器)與瀏覽器之間的代理服務器(可以攔截全站的請求,並作出相應的動作->由開發者指定的動作)。 - 創建有效的離線體驗(將一些不常更新的內容緩存在瀏覽器,提高訪問體驗)。
- 由事件驅動的,具有生命周期。
- 可以訪問
cache和indexDB。 - 支持推送。
- 可以讓開發者自己控制管理緩存的內容以及版本。
Service worker還有一些其他的使用場景,以及service worker的標准能夠用來做更多使web平台接近原生應用的事情:
- 后台數據同步。
- 響應來自其它源的資源請求。
- 集中接收計算成本高的數據更新,比如地理位置和陀螺儀信息,這樣多個頁面就可以利用同一組數據。
- 在客戶端進行
CoffeeScript、LESS、CJS/AMD等模塊編譯和依賴管理(用於開發目的)。 - 后台服務鈎子。
- 自定義模板用於特定
URL模式。性能增強,比如預取用戶可能需要的資源,比如相冊中的后面數張圖片。 - 可以配合
App Manifest和Service Worker來實現PWA的安裝和離線等功能。 - 后台同步,啟動一個
service worker即使沒有用戶訪問特定站點,也可以更新緩存。 - 響應推送,啟動一個
service worker向用戶發送一條信息通知新的內容可用。 - 對時間或日期作出響應。
- 進入地理圍欄(
LBS的一種應用)。
示例
實現一個簡單的Service worker應用示例,這個示例可以在斷網的時候同樣可以使用,相關的代碼在https://github.com/WindrunnerMax/webpack-simple-environment/tree/simple--service-worker,在這里就是用原生的Service Worker寫一個簡單示例,直接寫原生的Service Worker比較繁瑣和復雜,所以可以借助一些庫例如Workbox等,在使用Service Worker之前有一些注意事項:
Service worker運行在worker上,也就表明其不能訪問DOM。- 其設計為完全異步,同步
API(如XHR和localStorage)不能在service worker中使用。 - 出於安全考量,
Service workers只能由HTTPS承載,localhost本地調試可以使用http。 - 在
Firefox瀏覽器的用戶隱私模式,Service Worker不可用。 - 其生命周期與頁面無關(關聯頁面未關閉時,它也可以退出,沒有關聯頁面時,它也可以啟動)。
首先使用Node啟動一個基礎的web服務器,可以使用anywhere這個包,當然使用其他服務器都是可以的,執行完命令后訪問http://localhost:7890/即可。另外寫完相關代碼后建議重啟一下服務,之前我就遇到了無法緩存的問題,包括disk cache和memory cache,要重啟服務才解決。還有要打開的鏈接為localhost,自動打開瀏覽器可能並不是localhost所以需要注意一下。如果要清理緩存的話,可以在瀏覽器控制台的Application項目中Storage點擊Clear site data就能清理在網站中的所有緩存了。如果使用express或者koa等服務器環境,還可以嘗試使用Service Worker來緩存數據請求,同樣提供數據請求的path即可。
$ npm install -g anywhere
$ anywhere 7890 # http://localhost:7890/
編寫一個index.html文件和sw.js文件,以及引入相關的資源文件,目錄結構如下,可以參考https://github.com/WindrunnerMax/webpack-simple-environment/tree/simple--service-worker,當然直接clone下來運行一個靜態文件服務器就可以直接使用了。
simple--service-worker
├── static
│ ├── avatar.png
│ └── cache.js
├── index.html
└── sw.js
在html中引入相關文件即可,主要是為了借助瀏覽器環境,而關注的位置是js。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Service Worker</title>
<style type="text/css">
.avatar{
width: 50px;
height: 50px;
border-radius: 50px;
}
</style>
</head>
<body>
<img class="avatar" src="./static/avatar.png">
<script type="text/javascript">
navigator.serviceWorker
.register("sw.js")
.then(() => {
console.info("注冊成功");
})
.catch(() => {
console.error("注冊失敗");
});
</script>
<script src="./static/cache.js"></script>
</body>
</html>
使用Service worker的第一步,就是告訴瀏覽器,需要注冊一個Service worker腳本,在這里我們直接將其寫到了index.html文件中了。默認情況下,Service worker只對根目錄/生效,如果要改變生效范圍可以在register時加入第二個參數{ scope: "/xxx"},也可以直接在注冊的時候就指定路徑/xxx/sw.js。
navigator.serviceWorker
.register("sw.js")
.then(() => {
console.info("注冊成功")
}).catch(err => {
console.error("注冊失敗")
})
一旦登記成功,接下來都是Service worker腳本的工作,下面的代碼都是寫在service worker腳本里面的,登記后,就會觸發install事件,service worker腳本需要監聽這個事件。首先定義這個cache的名字,相當於是標識這一個緩存對象的鍵值,之后的urlsToCache數組是即將要緩存的數據,只要給定了相關的path,連數據請求也是同樣能夠緩存的,而不僅僅是資源文件,當然這邊必須是Get的請求下使用,這是Cache這個API決定的。之后便是進行install,關於event.waitUntil可以理解為new Promise的作用,是要等待serviceWorker運行起來才繼續后邊的代碼,其接受的實際參數只能是一個Promise。在MDN的解釋是因為oninstall和onactivate完成前需要一些時間,service worker標准提供一個waitUntil方法,當oninstall或者onactivate觸發時被調用,接受一個promise,在這個promise被成功resolve以前,功能性事件不會分發到service worker。之后便是從caches取出這個CACHE_NAME的key標識的cache,之后使用cache.addAll將數組中的path告訴cache,在第一次打開的時候,Service worker會自動去請求相關的數據並且緩存起來,使用Service worker去請求的數據,在Chrome控制台的Network中會顯示一個小小的齒輪圖標,很好辨認。
const CACHE_NAME = "service-worker-demo";
const urlsToCache = ["/", "/static/avatar.png", "/static/cache.js"];
this.addEventListener("install", event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log("[Service Worker]", urlsToCache);
return cache.addAll(urlsToCache);
})
);
});
之后是activated階段,如果是第一次加載sw,在安裝后,會直接進入activated階段,而如果sw進行更新,情況就會顯得復雜一些,流程如下:首先老的sw為A,新的sw版本為B, B進入install階段,而A還處於工作狀態,所以B進入waiting階段,只有等到A被terminated后,B才能正常替換A的工作。這個terminated的時機有如下幾種方式,1、關閉瀏覽器一段時間。2、手動清除Service Worker。3、在sw安裝時直接跳過waiting階段。然后就進入了activated階段,激活sw工作,activated階段可以做很多有意義的事情,比如更新存儲在Cache中的key和value。在下邊的代碼中,實現了不在白名單的CACHE_NAME就清理,可以在這里實現一個version也就是版本的控制,之前的版本就要清理等,另外還查看了一下目前的相關緩存。
this.addEventListener("activate", event => {
// 不在白名單的`CACHE_NAME`就清理
const cacheWhitelist = ["service-worker-demo"];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
// 查看一下緩存
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.keys().then(res => console.log(res)))
);
});
之后便是攔截請求的階段了,該階段是sw關鍵的一個階段,用於攔截代理所有指定的請求,並進行對應的操作,所有的緩存部分,都是在該階段。首先我們直接攔截掉所有的請求,在最前邊的判斷操作是為了防止所有的請求都被攔截從而都在worker里邊發起請求,當然不進行判斷也是可以使用的。然后對於請求如果匹配到了緩存,那么就直接從緩存中取得數據,否則就使用fetch去請求新的。另外如果有需要的話我們不需要在事件響應時進行匹配 可以直接將所有發起過的請求緩存。
this.addEventListener("fetch", event => {
const url = new URL(event.request.url);
if (url.origin === location.origin && urlsToCache.indexOf(url.pathname) > -1) {
event.respondWith(
caches.match(event.request).then(resp => {
if (resp) {
console.log("fetch ", event.request.url, "有緩存,從緩存中取");
return resp;
} else {
console.log("fetch ", event.request.url, "沒有緩存,網絡獲取");
return fetch(event.request);
// // 如果有需要的話我們不需要在事件響應時進行匹配 可以直接將所有發起過的請求緩存
// return fetch(event.request).then(response => {
// return caches.open(CACHE_NAME).then(cache => {
// cache.put(event.request, response.clone());
// return response;
// });
// });
}
})
);
}
});
第一次打開時控制台的輸出:
cache.js loaded
[Service Worker] (3) ['/', '/static/avatar.png', '/static/cache.js']
注冊成功
(3) [Request, Request, Request]
第二次及之后打開的控制台輸出:
fetch http://localhost:7811/static/avatar.png 有緩存,從緩存中取
fetch http://localhost:7811/static/cache.js 有緩存,從緩存中取
注冊成功
cache.js loaded
至此我們就完成了一個簡單的示例,在第二次打開頁面的時候,我們可以將瀏覽器的網絡連接斷開,例如關閉文件服務器或者在控制台的Network中選擇Offline,而我們也可以看到頁面依舊正常加載,不需要網絡服務,另外也可以在Network的相關的數據的Size列會出現(ServiceWorker)這個信息,說明資源是從ServiceWorker加載的緩存數據。可以在https://github.com/WindrunnerMax/webpack-simple-environment/tree/simple--service-worker中clone下來后運行這個示例。
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Service Worker</title>
<style type="text/css">
.avatar{
width: 50px;
height: 50px;
border-radius: 50px;
}
</style>
</head>
<body>
<img class="avatar" src="./static/avatar.png">
<script type="text/javascript">
navigator.serviceWorker
.register("sw.js")
.then(() => {
console.info("注冊成功");
})
.catch(() => {
console.error("注冊失敗");
});
</script>
<script src="./static/cache.js"></script>
</body>
</html>
// sw.js
const CACHE_NAME = "service-worker-demo";
const urlsToCache = ["/", "/static/avatar.png", "/static/cache.js"];
this.addEventListener("install", event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log("[Service Worker]", urlsToCache);
return cache.addAll(urlsToCache);
})
);
});
this.addEventListener("activate", event => {
// 不在白名單的`CACHE_NAME`就清理
const cacheWhitelist = ["service-worker-demo"];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
// 查看一下緩存
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.keys().then(res => console.log(res)))
);
});
this.addEventListener("fetch", event => {
const url = new URL(event.request.url);
if (url.origin === location.origin && urlsToCache.indexOf(url.pathname) > -1) {
event.respondWith(
caches.match(event.request).then(resp => {
if (resp) {
console.log("fetch ", event.request.url, "有緩存,從緩存中取");
return resp;
} else {
console.log("fetch ", event.request.url, "沒有緩存,網絡獲取");
return fetch(event.request);
// // 如果有需要的話我們不需要在事件響應時進行匹配 可以直接將所有發起過的請求緩存
// return fetch(event.request).then(response => {
// return caches.open(CACHE_NAME).then(cache => {
// cache.put(event.request, response.clone());
// return response;
// });
// });
}
})
);
}
});
// cache.js
console.log("cache.js loaded");
// avatar.png
// [byte]png
每日一題
https://github.com/WindrunnerMax/EveryDay
參考
https://github.com/mdn/sw-test/
https://zhuanlan.zhihu.com/p/25459319
https://zhuanlan.zhihu.com/p/115243059
https://zhuanlan.zhihu.com/p/161204142
https://github.com/youngwind/service-worker-demo
https://mp.weixin.qq.com/s/3Ep5pJULvP7WHJvVJNDV-g
https://developer.mozilla.org/zh-CN/docs/Web/API/Cache
https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
https://www.bookstack.cn/read/webapi-tutorial/docs-service-worker.md
