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