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