系列文章:
- Service Workers 和離線緩存 (本文)
- Notification with Service Workers push events
- PWA:添加應(yīng)用至桌面及分享
第一次聽到 Service Workers 這個(gè)詞還是在去年 Google 來安利 Angular 2 的時(shí)候把将,那時(shí)就覺得很驚艷王财,想搞一搞,但是因?yàn)闆]把網(wǎng)站升級成 https 一直拖到現(xiàn)在绒净。不久前,把網(wǎng)站升級成了 https改览,終于可以搞一發(fā)了缤言。
本篇主要包含以下內(nèi)容:
- What's Service Workers?
- 小試 Service Workers
- 調(diào)試 Service Workers
- 通過 postMessage 與主窗口通信
- 為應(yīng)用添加離線緩存
- Service workers 的生命周期與更新
當(dāng)然,還是先來看看 Service Workers 究竟是什么庆揩?
<a name="Whats-service-workers"></a>
What's Service Workers?
Service Workers 是谷歌 chrome 團(tuán)隊(duì)提出并大力推廣的一項(xiàng) web 技術(shù)跌穗。在 2015 年,它加入到 W3C 標(biāo)準(zhǔn)蚌吸,進(jìn)入草案階段。W3C 標(biāo)準(zhǔn)中對 Service Workers 的解釋太細(xì)致奕枢,相對而言肉迫,我更喜歡 MDN 上的解釋,更簡練跌造,更易于理解。
Service workers essentially act as proxy servers that sit between web applications, and the browser and network (when available). They are intended to (amongst other things) enable the creation of effective offline experiences, intercepting network requests and taking appropriate action based on whether the network is available and updated assets reside on the server. They will also allow access to push notifications and background sync APIs. - MDN
簡單翻譯一下:Service workers 基本上充當(dāng)應(yīng)用同服務(wù)器之間的代理服務(wù)器陵珍,可以用于攔截請求,也就意味著可以在離線環(huán)境下響應(yīng)請求互纯,從而提供更好的離線體驗(yàn)磕蒲。同時(shí),它還可以接收服務(wù)器推送和后臺同步 API辣往。
那么,這項(xiàng)技術(shù)的瀏覽器支持情況是什么樣坊萝,還是來看一眼 Can I use?
可以從看到许起,Chrome 和 Firefox, Opera 都已經(jīng)支持 Service Workers,底下的備注也寫到 Edge 在開發(fā)中园细,Safari 也考慮支持惦积。至于 IE荣刑,船長都跳船了伦乔×液停看了 PC 端,再來看看移動端招刹。移動端的支持率并不盡如人意窝趣,不過在安卓 4.4 之后,安卓原生瀏覽器妇拯,以及安卓版的 Chrome 都已經(jīng)開始支持 Service Workers。
說句題外話越锈,突然發(fā)現(xiàn)在 Can I use 中選擇導(dǎo)入我國數(shù)據(jù)時(shí),竟出現(xiàn)了 UC 和 QQ 瀏覽器的支持情況稀拐,口以口以??????...
言歸正傳丹弱,在真正開始使用 Service Workers 之前,還有幾點(diǎn)要注意:
- Service Workers 基于 Https蜓洪,這是硬性條件(如何升級 https 可以參考上一篇文章)
- 每個(gè) Service Worker 都有自己的作用域,它只會處理自己作用域下的請求蝠咆,而 Service Worker 的存放位置就是它的最大作用域
- Service Workder 是 Web Worker 的一種北滥,它不能夠直接操作 DOM
Github 上有一個(gè)非常棒的資源,它用圖片的方式展示了 Servic Workers 的一些核心要點(diǎn)菊霜。
搞定這些基礎(chǔ)就可以正式開搞了...
<a name="try-service-workers"></a>
小試 Service Workers
和其他 worker 一樣济赎,service worker 有一個(gè)獨(dú)自的文件。由于之前所提到的 service worker 只能作用在自己存放位置之下的文件司训,所以,一般在應(yīng)用根目錄下存放 service worker 文件勾徽。
首先统扳,先寫一個(gè)最簡單的來看看瀏覽器是不是支持,以及能否正確地安裝并運(yùn)行 service worker吹由。
// service-worker.js
const _self = this;
console.log('In service worker.');
_self.addEventListener('install', function () {
console.log('Install success');
});
_self.addEventListener('activate', function () {
console.log('Activated');
});
雖然朱嘴,service worker 是 web worker 其中的一種,但它有些不同,它有自己的注冊方式舌劳。
// ServiceWorkerService.js
const SERVICE_WORKER_API = 'serviceWorker';
const SERVICE_WORKER_FILE_PATH = 'service-worker.js';
const isSupportServiceWorker = () => SERVICE_WORKER_API in navigator;
if (isSupportServiceWorker()) {
navigator
.serviceWorker
.register(SERVICE_WORKER_FILE_PATH)
.then(() => console.log('Load service worker Success.'))
.catch(() => console.error('Load service worker fail'));
} else {
console.info('Browser not support Service Worker.');
}
重啟程序之后甚淡,你應(yīng)該就能在控制臺中看到 Load service worker Success.
。然而贯卦,卻沒有另兩句的輸出焙贷,難道加載失敗了?但是啡彬,控制臺不是顯示加載成功了么故硅?不要擔(dān)心,程序沒有出錯(cuò)往踢,只是 service worker 中的日志信息有它自己的輸出位置,而并非輸出在主日志之中峻呕。
接下去趣效,先來看看如何調(diào)試 service worker。
<a name="debug-service-workers"></a>
調(diào)試 Service Workers
在 Chrome 中佩憾,service worker 的信息顯示在 Application -> Service Workers
中干花,就像這樣
里面會顯示注冊的 service worker池凄,以及它當(dāng)前的狀態(tài)鬼廓。還能通過切換最上面的選項(xiàng)來模擬不同的網(wǎng)絡(luò)環(huán)境,測試在不同環(huán)境下 service worker 的響應(yīng),它們分別是:
- Offline: 離線
- Update on reload: 加載時(shí)更新
- Bypass for network: 使用網(wǎng)絡(luò)內(nèi)容
回到之前的問題馏锡,如何查看 service worker 之中的日志哪伟端?只需點(diǎn)擊圖中的 inspect
鏈接,它會彈出另一個(gè)開發(fā)者窗口责蝠,在里面可以查看 service worker 的日志。是不是覺得需要那么多步有點(diǎn)麻煩齿拂,別擔(dān)心肴敛,Chrome 已經(jīng)替我們解決了這個(gè)煩惱。重新刷新頁面后砸狞,Chrome 的開發(fā)者工具中已經(jīng)能夠查看 service workers 的信息了,比如:在 console 選項(xiàng)卡勾選 Show all messages
就能顯示 service workers 中控制臺的信息趾代;在 source 選項(xiàng)卡也能看到 service workers 的代碼丰辣,當(dāng)然也可以打斷點(diǎn)啦~
在 firefox 中笙什,默認(rèn)會將 service worker 中的日志輸出到主控制臺中,但要打開 service worker 的調(diào)試器就有點(diǎn)麻煩了琐凭。有兩種方法查看,一個(gè)是在地址欄中輸入 about:debugging#workers
胚吁,另一種就是通過菜單欄中選擇 Tools -> Web Developer -> Service Workers
愁憔。
更多關(guān)于在 firefox 中調(diào)試 service workers 的信息可以點(diǎn)此查看吨掌。
雖然脓恕,已經(jīng)將日志輸出到主控制臺了窿侈,可這里就有個(gè)疑問了,主頁能不能獲取 service workers 中的信息哪乃秀?答案是肯定的乘瓤,那就是通過 postMessage
。
<a name="postmessage"></a>
通過 postMessage 與主窗口通信
和 web worker 一樣抬吟,service worker 與主窗口通訊也需要通過 postMessage
,但它的語法又有些許不同火本。
首先聪建,是主頁面給 service worker 發(fā)消息。
// ServiceWorkerService.js
const sendMessageToSW = msg => navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage(msg);
if (isSupportServiceWorker()) {
const sw = navigator.serviceWorker;
sw.register(SERVICE_WORKER_FILE_PATH)
.then(() => console.log('Load service worker Success.'))
.catch(() => console.error('Load service worker fail'))
.then(() => sendMessageToSW('Hello, service worker.'))
.catch(() => console.error('Send message error.'));
} else {
console.info('Browser not support Service Worker.');
}
可以看到擎析,postMessage
方法并不在 worker 實(shí)例下挥下,而是在 serviceWorker 下的 controller 對象下。這里需要注意一下现斋,當(dāng) service worker 還沒有注冊成功時(shí),navigator.serviceWorker.controller
對象的值是 null
偎蘸,所以,在調(diào)用 postMessage
之前需要確保 controller
對象已經(jīng)存在限书。在 service worker 這邊就沒有什么區(qū)別了
// service-worker.js
_self.addEventListener('message', function(event) {
console.log(event.data);
});
是不是很簡單章咧?不過,反過來 service worker 給主頁面發(fā)消息就要復(fù)雜一點(diǎn)了慧邮。在 service worker 里發(fā)送信息需要通過 Client
對象的 postMessage
方法。獲取 Client
的方法有很多耻矮,比如裆装,剛從主頁面發(fā)來的消息倡缠,事件的來源就是一個(gè) Client
對象哨免,即 event.source
。不過昙沦,這只能向來源發(fā)消息琢唾,但如果你開了幾個(gè)網(wǎng)頁,或者不是通過主頁消息發(fā)來的該怎么辦哪盾饮?方法還是有的采桃,在 service workers 中可以通過 clients
來獲取所有的頁面對象或其他的 service workers。
// service-worker.js
_self.clients.matchAll().then(function(clients) {
clients.forEach(function(client) {
client.postMessage('Service worker attached.');
})
});
不過丘损,如果你發(fā)出一個(gè)消息需要等到另一方的返回的消息做處理普办,上述的辦法就做不到了徘钥。這時(shí)就需要建立一個(gè)通道來處理了,修改一下之前的 sendMessageToSW
方法舆驶。
// ServiceWorkerService.js
const sendMessageToSW = msg => new Promise((resolve, reject) => {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = event => {
if (event.data.error) {
reject(event.data.error);
} else {
resolve(event.data);
}
};
navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage(msg, [messageChannel.port2]);
});
這樣信息發(fā)送出去后會返回一個(gè) promise
贞远,然后就可以優(yōu)雅地鏈?zhǔn)秸{(diào)用了蓝仲。
// ServiceWorkerService.js
if (isSupportServiceWorker()) {
const sw = navigator.serviceWorker;
sw.register(SERVICE_WORKER_FILE_PATH)
.then(() => console.log('Load service worker Success.'))
.catch(() => console.error('Load service worker fail'))
.then(() => sendMessageToSW('Hello, service worker.'))
.then(console.log)
.catch(() => console.error('Send message error.'));
} else {
console.info('Browser not support Service Worker.');
}
了解了如何在瀏覽器中調(diào)試 service workers 和與主頁面通信這些基礎(chǔ)之后袱结,就可以搞一些正真功能性的東西垢夹,比如創(chuàng)造 service workers 最初的動機(jī)——提供更好的離線體驗(yàn)果元。
<a name="offline-cache"></a>
為應(yīng)用添加離線緩存
為應(yīng)用添加緩存的方式有很多,但能夠提供離線緩存的蝇狼,據(jù)我所知倡怎,那就只有 service workers 一家了监署。這就好比已經(jīng)安裝了的應(yīng)用,無論是否有網(wǎng)絡(luò)連接都可以隨時(shí)打開使用(google 所推的 PWA 最終目的就是這個(gè))栖秕。你可能會懷疑累魔,聽起來這么高大上實(shí)現(xiàn)起來會不會很復(fù)雜垦写?然而并沒有梯投,使用 service workers 為應(yīng)用添加離線緩存還是相當(dāng)簡單的况毅。
就如同文章開頭 MDN 中所提到的,service workers 可以充當(dāng)應(yīng)用與服務(wù)器之前的代理服務(wù)器么鹤,它通過監(jiān)聽 fetch
事件來捕捉自己作用域下發(fā)出的網(wǎng)絡(luò)請求蒸甜,并通過 event.respondWith
來返回請求結(jié)果柠新,過程中可以對返回結(jié)果做任何的修改(所以必須 https 昂拊鳌)憔恳。
// service-worker.js
const handleFetchRequest = function(request) {
return fetch(request);
};
const onFetch = function(event) {
event.respondWith(handleFetchRequest(event.request));
};
_self.addEventListener('fetch', onFetch);
上面這段代碼就是捕獲請求最基本的方式,然后直接將請求發(fā)送出去喇嘱,并將請求的結(jié)果返回者铜,沒有做其他額外的操作作烟。如果拿撩,你這時(shí)觀察控制臺的網(wǎng)絡(luò)請求压恒,會發(fā)現(xiàn)所有請求的 size
都不再是原先的文件大小或來自緩存错邦,而是 from ServiceWorker
撬呢。
接下去,就來給應(yīng)用添加離線緩存毛仪。既然箱靴,所有的請求都是手動發(fā)出的衡怀,而且能夠拿到返回的結(jié)果路翻,那么茂契,緩存這些結(jié)果就變得輕而易舉了掉冶。
不過脐雪,這里要先講另一個(gè)知識點(diǎn)——Cache Storage
战秋。它作為 service worker 的一部分寫在草案中脂信。通過它狰闪,我們可以方便地把請求埋泵,以及請求結(jié)果一同緩存起來丽声。了解了 Cache Storage
觉义,那就把上面的代碼改一下谁撼,讓它能夠緩存請求厉碟。
// service-worker.js
const handleFetchRequest = function(request) {
return caches.match(request)
.then(function(response) {
return response || fetch(request)
.then(function(response) {
const clonedResponse = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(request, clonedResponse);
});
return response;
});
});
};
這里主要修改了如何處理請求的方法箍鼓,先判斷這個(gè)請求是否已經(jīng)被緩存過了,緩存過了就直接返回結(jié)果何暮,沒有的話就去請求铐殃,并把結(jié)果添加到緩存中富腊,以便下次請求來時(shí)可以直接返回。
離線緩存就這樣添加好了肖揣,來看看效果怎么樣。這就要用到之前調(diào)試時(shí)所提到的模擬不同環(huán)境龙优,不記得的童鞋可以往上翻一翻彤断。(提示關(guān)鍵詞:控制臺, Application
, Service Workers
, Offline
)這里模擬離線環(huán)境瓦糟,設(shè)置好后再刷新頁面。
Awesome~????
雖然已實(shí)現(xiàn)了離線緩存句伶,但是陆淀,使用 Cache Storage
還需要注意以下幾點(diǎn):
- 它只能緩存
GET
請求考余; - 每個(gè)站點(diǎn)只能緩存屬于自己域下的請求轧苫,同時(shí)也能緩存跨域的請求含懊,比如 CDN岔乔,不過無法對跨域請求的請求頭和內(nèi)容進(jìn)行修改
- 緩存的更新需要自行實(shí)現(xiàn)雏门;
- 緩存不會過期茁影,除非將緩存刪除募闲,而瀏覽器對每個(gè)網(wǎng)站
Cache Storage
的大小有硬性的限制,所以需要清理不必要的緩存呼盆。
上面的代碼并沒有做緩存的清除和更新访圃,所以,還要更新一下饭宾。同時(shí)徽鼎,通過給跨域請求添加 {mode: 'cors'}
屬性來使請求支持跨域否淤,從而拿到響應(yīng)頭信息石抡。
const HOST_NAME = location.host;
const VERSION_NAME = 'CACHE-v1';
const CACHE_NAME = HOST_NAME + '-' + VERSION_NAME;
const CACHE_HOST = [HOST_NAME, 'cdn.bootcss.com'];
const isNeedCache = function(url) {
return CACHE_HOST.some(function(host) {
return url.search(host) !== -1;
});
};
const isCORSRequest = function(url, host) {
return url.search(host) === -1;
};
const isValidResponse = function(response) {
return response && response.status >= 200 && response.status < 400;
};
const handleFetchRequest = function(req) {
if (isNeedCache(req.url)) {
const request = isCORSRequest(req.url, HOST_NAME) ? new Request(req.url, {mode: 'cors'}) : req;
return caches.match(request)
.then(function(response) {
// Cache hit - return response directly
if (response) {
// Update Cache for next time enter
fetch(request)
.then(function(response) {
// Check a valid response
if(isValidResponse(response)) {
caches
.open(CACHE_NAME)
.then(function (cache) {
cache.put(request, response);
});
} else {
sentMessage('Update cache ' + request.url + ' fail: ' + response.message);
}
})
.catch(function(err) {
sentMessage('Update cache ' + request.url + ' fail: ' + err.message);
});
return response;
}
// Return fetch response
return fetch(request)
.then(function(response) {
// Check if we received an unvalid response
if(!isValidResponse(response)) {
return response;
}
const clonedResponse = response.clone();
caches
.open(CACHE_NAME)
.then(function(cache) {
cache.put(request, clonedResponse);
});
return response;
});
});
} else {
return fetch(req);
}
};
升級之后嗡贺,還是有緩存先拿緩存,這樣比較快厢漩,但依舊會在后臺發(fā)出請求溜嗜,如果返回合法的請求架谎,就更新 cache 中的值谷扣,那么,下次訪問時(shí)就是這次訪問返回的結(jié)果了瑞凑。
service worker 的 install
和 activite
事件對象都包含一個(gè) waitUntil
方法籽御,方法接受一個(gè) promise惰匙,當(dāng) promise 被 resolve
后才會繼續(xù)執(zhí)行到下一個(gè)狀態(tài)。如果哑梳,想要強(qiáng)制更新緩存鸠真,就可以通過這個(gè)方法在 service worker 激活時(shí)除舊版本緩存弧哎。
// service-worker.js
const onActive = function(event) {
event.waitUntil(
caches
.keys()
.then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
// Remove expired cache response
if (CACHE_NAME.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
};
_self.addEventListener('activate', onActive);
這樣請求的緩存就能隨時(shí)更新了,不過蠢终,你可能會和我有同樣的疑問——那 service workers 怎么更新呢寻拂?
<a name="lifecycle-and-update"></a>
Service workers 的生命周期與更新
事實(shí)上祭钉,service workers 的更新并不需要我們操心慌核,只要 service workers 文件有任何一點(diǎn)的修改垮卓,瀏覽器就會立即裝載它粟按。然而,它還是有需要注意的地方疼鸟,不然也就不值一提了空镜。
雖然姑裂,瀏覽器立即裝載它舶斧,但它并沒有立即生效茴厉,這和它的生命周期有關(guān)。下面這張圖來自 Service Workers 101嗜闻,非常形象地展示了 service workers 的生命周期琉雳。
先看圖的右邊翠肘,它展示了 service workers 的 3 種狀態(tài):Installing
, Waiting
和 Active
辫秧;左邊是 service workers 的生命周期绪妹,兩者結(jié)合在一起,直觀地展現(xiàn)了在 service workers 不同的生命周期時(shí)廊移,service workers 所處的狀態(tài)《可以看到,install
與 activate
2 個(gè)時(shí)間中間离唐,service workers 是處于 Waiting
的狀態(tài)。
回到剛才提到的 service workers 更新嵌戈,瀏覽器雖然會立即裝載最新的 service workers听皿,但只是讓它 install
尉姨,并進(jìn)入 Waiting
的狀態(tài)又厉,而并沒有立即 activate
。只有當(dāng)用戶將瀏覽器關(guān)閉后,重新打開頁面時(shí)婆排,舊的 service workers 才會被新的 service workers 替換腮猖。不過澈缺,圖中也有提到莱预,可以在 install
事件中 self.skipWaiting
方法來跳過等待,直接進(jìn)入 activate
狀態(tài)危喉。同樣的,可以在 activate
事件中調(diào)用 self.clients.claim
方法來更新所有客戶端上的 service works。
為 service workers 添加上述兩個(gè)方法就能較好地處理更新問題岂座。代碼改動很小杭措,這里就不再重復(fù)貼了鸳址,所有的代碼都已上傳 Github。
下次準(zhǔn)備搗鼓 service workers 相關(guān)的服務(wù)器推送,敬請關(guān)注...??