參考資料
MDN --- Service Worker API
Service Workers: an Introduction
服務工作線程生命周期
Service Worker Cookbook(收集了Service Worker的一些實踐例子)
理解 Service Workers
溫馨提示
- 使用限制
Service Worker由于權限很高,只支持https協(xié)議或者localhost。
個人認為Github Pages是一個很理想的練習場所特占。 - 儲備知識
Service Worker大量使用Promise吐句,不了解的請移步:Javascript:Promise對象基礎
兼容性
一蟀给、 生命周期
個人覺得先理解一下它的生命周期很重要瓮增!之前查資料的時候巩步,很多文章一上來就監(jiān)聽install
事件功咒、waiting
事件愉阎、activate
事件……反正我是一臉懵逼。
1. Parsed
SW是一個JS文件力奋,如果我們要使用一個SW(Service Worker)榜旦,那么我們需要在我們的js代碼中注冊它,類似于:
navigator.serviceWorker.register('/sw-1.js')
現(xiàn)在并不需要知道這個方法各個部分的詳細含義景殷,只要知道我們現(xiàn)在在為我們的網(wǎng)頁注冊一個SW就可以了溅呢。
可以看到我們傳入的參數(shù)是一個JS文件的路徑澡屡,當瀏覽器執(zhí)行到這里的時候,就會到相應的路徑下載該文件藕届,然后對該腳本進行解析挪蹭,如果下載或者解析失敗,那么這個SW就會被舍棄休偶。
如果解析成功了梁厉,那就到了parsed狀態(tài)√ざ担可以進行下面的工作了词顾。
2. Installing
在installing狀態(tài)中,SW 腳本中的 install 事件被執(zhí)行碱妆。在能夠控制客戶端之前肉盹,install 事件讓我們有機會緩存我們需要的所有內容。
比如疹尾,我們可以先緩存一張圖片上忍,那么當SW控制客戶端之后,客戶點擊該鏈接的圖片纳本,我們就可以用SW捕獲請求窍蓝,直接返回該圖片的緩存。
若事件中有 event.waitUntil() 方法繁成,則 installing 事件會一直等到該方法中的 Promise 完成之后才會成功吓笙;若 Promise 被拒,則安裝失敗巾腕,Service Worker 直接進入廢棄(redundant)狀態(tài)面睛。
3. Installed / Waiting
如果安裝成功,Service Worker 進入installed(waiting)狀態(tài)尊搬。在此狀態(tài)中叁鉴,它是一個有效的但尚未激活的 worker。它尚未納入 document 的控制毁嗦,確切來說是在等待著從當前 worker 接手亲茅。
處于 Waiting 狀態(tài)的 SW,在以下之一的情況下狗准,會被觸發(fā) Activating 狀態(tài)。
- 當前已無激活狀態(tài)的 worker
- SW 腳本中的 self.skipWaiting() 方法被調用
- 用戶已關閉 SW作用域下的所有頁面茵肃,從而釋放了此前處于激活態(tài)的 worker
- 超出指定時間腔长,從而釋放此前處于激活態(tài)的 worker
4. Activating
處于 activating 狀態(tài)期間,SW 腳本中的 activate 事件被執(zhí)行验残。我們通常在 activate 事件中捞附,清理 cache 中的文件(清除舊Worker的緩存文件)。
SW激活失敗,則直接進入廢棄(redundant)狀態(tài)鸟召。
5. Activated
如果激活成功胆绊,SW 進入激活狀態(tài)。在此狀態(tài)中欧募,SW開始接管控制客戶端压状,并可以處理fetch
(捕捉請求)、 push
(消息推送)跟继、 sync
(同步事件)等功能性事件:
// sw.js
self.addEventListener('fetch', function(event) {
// Do stuff with fetch events
});
self.addEventListener('message', function(event) {
// Do stuff with postMessages received from document
});
......
6. Redundant 廢棄
Service Worker 可能以下之一的原因而被廢棄(redundant)——
- installing 事件失敗
- activating 事件失敗
- 新的 Service Worker 替換其成為激活態(tài) worker
?
我們已經(jīng)理解了SW的生命周期了种冬,那么現(xiàn)在就開始來做一個離線應用。
我們只實現(xiàn)最簡單的功能:用戶每發(fā)送一個http請求舔糖,我們就用SW捕獲這個請求娱两,然后在緩存里找是否緩存了這個請求對應的響應內容,如果找到了金吗,就把緩存中的內容返回給主頁面十兢,否則再發(fā)送請求給服務器。
二摇庙、 register 注冊
首先要注冊一個SW旱物,在index.js文件中:
// index.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
// 注冊一個service worker,這個例子中worker的路徑是根目錄中的跟匆,所以這個worker可以緩存這個項目中任意文件异袄。如果目錄是‘/js/sw.js‘,那么只能緩存目錄'/js'下的文件
// 參數(shù)registration存儲了本次注冊的一些相關信息
navigator.serviceWorker.register('/sw.js').then(function(registration) {
// Registration was successful
// registration.scope 返回的是這個service worker的作用域
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
});
}
知識點:
1. window.navigator
返回一個Navigator對象玛臂,該對象簡單來說就是允許我們獲取我們用戶代理(瀏覽器)的一些信息烤蜕。比如,瀏覽器的官方名稱迹冤,瀏覽器的版本讽营,網(wǎng)絡連接狀況,設備位置信息等等泡徙。
2. navigator.serviceWorker
返回一個 ServiceWorkerContainer對象橱鹏,該對象允許我們對SW進行注冊、刪除堪藐、更新和通信莉兰。
上面的代碼中首先判斷navigator
是否有serviceWorker
屬性(存在的話表示瀏覽器支持SW),如果存在礁竞,那么通過navigator.serviceWorker.register()
(也就是ServiceWorkerContainer.register()
)來注冊一個新的SW糖荒,.register()
接受一個 路徑 作為第一個參數(shù)。
ServiceWorkerContainer.register()
返回一個Promise模捂,所以可以用.then()
和.catch()
來進行后續(xù)處理捶朵。
3. SW的作用域
如果沒有指定該SW的作用域蜘矢,那么它的默認作用域就是其所在的目錄。
比如综看,.register('/sw.js')
中品腹,sw.js在根目錄中,所以作用域是整個項目的文件红碑。
如果是這樣:.register('/controlled/sw.js')
舞吭,sw.js的作用域是/controlled。
我們可以手動為SW指定一個作用域:
.register('service-worker.js', { scope: './controlled' });
3. 為什么在load事件中進行注冊
為什么需要在load事件啟動呢句喷?因為你要額外啟動一個線程镣典,啟動之后你可能還會讓它去加載資源,這些都是需要占用CPU和帶寬的唾琼,我們應該保證頁面能正常加載完兄春,然后再啟動我們的后臺線程,不能與正常的頁面加載產生競爭锡溯,這個在低端移動設備意義比較大赶舆。
三、install 安裝
我們已經(jīng)注冊好了SW祭饭,如果 sw.js 下載并且解析成功芜茵,我們的SW就進入安裝階段了,這時候會觸發(fā)install事件倡蝙。我們一般在install事件中緩存我們想要緩存的靜態(tài)資源九串,供SW控制主頁面之后使用:
// sw.js
var CACHE_NAME = 'my-site-cache-v1'; // cache對象的名字
var urlsToCache = [ // 想要緩存的文件的數(shù)組
'/',
'/styles/main.css',
'/script/main.js'
];
// 如果所有文件都成功緩存,則將安裝成功
self.addEventListener('install', function(event) {
// 執(zhí)行安裝步驟
// ExtendableEvent.waitUntil()方法延長了安裝過程寺鸥,直到其傳回的Promise被resolve之后才會安裝成功
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
// console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
知識點:
1. cache
Cache是允許我們管理緩存的 Request / Response 對象對的接口猪钮,可以通過這個接口增刪查改 Request / Response 對。
上面代碼中cache.addAll(urlsToCache)
表示把數(shù)組中的文件都緩存在內存中胆建。
詳細了解請戳 : Cache
2. caches
caches是一個CacheStorage對象烤低,提供一個可被訪問的命名Cache對象的目錄,維護字符串名稱到相應Cache對象的映射笆载。
我們可以通過該對象打開某一個特定的Cache對象扑馁,或者查看該列表中是否有名字為“xxx”的Cache對象,也可以刪除某一個Cache對象凉驻。
四腻要、activate 激活
我們的SW已經(jīng)安裝成功了,它可以準備控制客戶端并處理 push 和 sync 等功能事件了涝登,這時闯第,我們獲得一個 activate 事件。
// sw.js
self.addEventListener("activate", function(event) {
console.log("service worker is active");
});
如果SW安裝成功并被激活缀拭,那么控制臺會打印出"service worker is active"咳短。
如果我們是在更新SW的情況下,此時應該還有一個舊的SW在工作蛛淋,這時我們的新SW就不會被激活咙好,而是進入了 "Waiting" 狀態(tài)。
我們需要關閉此網(wǎng)站的所有標簽頁來關閉舊SW褐荷,使新的SW激活勾效。或者手動激活叛甫。
那么activate事件可以用來干什么呢层宫?假設我們現(xiàn)在換了一個新的SW,新SW需要緩存的靜態(tài)資源和舊的不同其监,那么我們就需要清除舊緩存萌腿。
為什么呢?因為一個域能用的緩存空間是有限的抖苦,如果沒有正確管理緩存數(shù)據(jù)毁菱,導致數(shù)據(jù)過大,瀏覽器會幫我們刪除數(shù)據(jù)锌历,那么可能會誤刪我們想要留在緩存中的數(shù)據(jù)贮庞。
這個以后會詳細講,現(xiàn)在只需要知道activate事件能用來清除舊緩存舊可以了究西。
五窗慎、 fetch事件
現(xiàn)在我們的SW已經(jīng)激活了,那么可以開始捕獲網(wǎng)絡請求卤材,來提高網(wǎng)站的性能了遮斥。
當網(wǎng)頁發(fā)出請求的時候,會觸發(fā)fetch事件商膊。
Service Workers可以監(jiān)聽該事件伏伐,'攔截' 請求,并決定返回內容 ———— 是返回緩存的數(shù)據(jù)晕拆,還是發(fā)送請求藐翎,返回服務器響應的數(shù)據(jù)。
下面的代碼中实幕,SW會檢測緩存中是否有用戶想要的內容吝镣,如果有,就返回緩存中的內容昆庇。否則再發(fā)送網(wǎng)絡請求末贾。
// sw.js
self.addEventListener('fetch', event => {
const { request } = event; // 獲取request
const findResponsePromise = caches.open(CACHE_NAME)
// 在match的時候,需要請求的url和header都一致才是相同的資源
// caches.match(event.request, {ignoreVary: true}) 表示只要請求url相同就認為是同一個資源整吆。
.then(cache => cache.match(request)) // 查看cache對象中是否有匹配的項
.then(response => {
if (response) { // 如果response不為空拱撵,則返回response辉川,否則發(fā)送網(wǎng)絡請求
return response;
}
return fetch(request);
});
// event.respondWith 是一個 FetchEvent 對象中的特殊方法,用于將請求的響應發(fā)送回瀏覽器拴测。它接收一個對響應(或網(wǎng)絡錯誤)resolve 后的 Promise 對象作為參數(shù)乓旗。
event.respondWith(findResponsePromise);
});
箭頭函數(shù)真的很適合用于Promise對象,省略了一堆的function
集索、return
關鍵字屿愚,看著舒服多了……
關于緩存策略
不同的應用場景需要使用不同的緩存策略。
比如务荆,小紅希望她的網(wǎng)站在在線的時候總是返回緩存中的內容妆距,然后在后臺更新緩存;在離線的時候函匕,返回緩存的內容娱据。
比如,小明希望他的網(wǎng)站可以在在線的時候返回最新的響應內容浦箱,離線的時候再返回緩存中的內容吸耿。
……
如果想要研究一下各種緩存策略,可以參考下面的資料酷窥,這里就不詳述了咽安,不然文章就成裹腳布了……
The Service Worker Cookbook
離線指南
Service Worker最佳實踐
不過,既然標題是“做一個離線網(wǎng)頁應用”蓬推,那我們就做一個最簡單的緩存策略:如果緩存中保存著請求的內容妆棒,則返回緩存中的內容,否則沸伏,請求新內容糕珊,并緩存新內容。
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache hit - return response
if (response) {
return response;
}
// 克隆請求毅糟。因為請求是一個“stream”红选,只能用一次。但我們需要用兩次姆另,一次用來緩存喇肋,一次給瀏覽器抓取內容,所以需要克隆
var fetchRequest = event.request.clone();
// 返回請求的內容
return fetch(fetchRequest).then(
response => {
// 檢查是否為有效的響應迹辐。basic表示同源響應蝶防,也就是說,這意味著明吩,對第三方資產的請求不會添加到緩存间学。
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 同request,response是一個“stream”,只能用一次低葫,但我們需要用兩次详羡,一次用來緩存一個返回給瀏覽器,所以需要克隆氮采。
var responseToCache = response.clone();
// 緩存新請求
caches.open(CACHE_NAME)
.then(cache => cache.put(event.request, responseToCache));
return response;
}
);
})
);
});
?
完成啦殷绍!我們簡陋的離線應用!
打開頁面鹊漠,看一下緩存中有什么內容:
然后點擊“Vue”的鏈接:
可以看到緩存中多了一張后綴為.png的圖片。
SW緩存了我們的新請求茶行!
打開chrome的開發(fā)者工具躯概,點擊offline,使標簽頁處于離線狀態(tài):
然后畔师,刷新頁面娶靡。
依然可以訪問頁面。