離線可用:
- 在無網(wǎng)的情況下可以訪問叹谁, 甚至使用部分功能饲梭, 而不是展示"無網(wǎng)絡(luò)連接"的錯誤頁面。
- 在弱網(wǎng)的情況下焰檩, 能使用緩存快速訪問我們的頁面憔涉,提升體驗。
- 在正常網(wǎng)絡(luò)的情況下析苫, 可以通過自發(fā)各種控制的緩存來節(jié)省請求寬帶兜叨。
- ......
而這一切,其實都要歸功于PWA背后的英雄 —— Service Worker衩侥。
Service Worker可以簡單的理解為一個獨立于前端頁面国旷,在后臺運行的進(jìn)程(Worker)。因此茫死, 它不會阻塞瀏覽器腳本運行跪但, 同事也無法直接訪問瀏覽器相關(guān)的API(例如DOM、localStorage等)璧榄。此外,即便是離開頁面吧雹, 甚至是關(guān)閉瀏覽器后骨杂, 它仍然可以運行。 它就是web應(yīng)用后默默工作的小蜜蜂雄卷, 處理著緩存搓蚪、推送、通知與同步等工作丁鹉,如果學(xué)習(xí)PWA妒潭,也繞不開Service Worker蒜胖。
Service Worker是如何實現(xiàn)離線可用的风题?
當(dāng)訪問一個web網(wǎng)站時, 我們實際上做了些什么? 總體上來說纠亚, 我們通過與服務(wù)器建立連接, 獲取資源胃夏, 然后獲取到的部分資源還會去請求新的資源(例如html中使用的css,js等)蹬铺。粗粒度來說, 我們訪問一個網(wǎng)站匈庭,就是在獲取/訪問這些資源夫凸。
可想而知, 當(dāng)處于離線或弱網(wǎng)環(huán)境時阱持, 我們無法有效訪問這些資源夭拌, 這就是制約我們的關(guān)鍵因素。因此一個直觀的思路就是: 如果我們把這些資源緩存起來衷咽,在某些情況下鸽扁,將網(wǎng)絡(luò)請求變?yōu)楸镜卦L問,是否能解決這一問題兵罢?這時就需要有個本地cache, 可以靈活的將各類資源進(jìn)行本地存取献烦。
有了本地緩存的cache還不夠, 還需要有效的使用緩存卖词, 更新緩存與清除緩存巩那, 進(jìn)一步應(yīng)用各種個性化的緩存策略。 而這就需要有個能夠控制緩存的woker,也就是 Service Worker的部分工作之一此蜈。
Service Worker有一個非常重要的特性: 你可以在Service Worker中監(jiān)聽所有的客戶端(Web)發(fā)出的請求即横, 然后通過Service Worker來代理, 向服務(wù)器發(fā)起請求裆赵。 通過監(jiān)聽用戶請求信息东囚, Service Worker可以決定是都使用緩存來作為Web請求的返回。
普通 網(wǎng)頁與 添加了Service Worker的網(wǎng)頁對比圖如下:
-
普通網(wǎng)頁
-
添加Service Worker
注意: 圖中雖然將瀏覽器战授、SW(Service Worker)與后端服務(wù)三者并列放置了页藻, 但實際上瀏覽器和SW都是運行在本機上的, 所以這個場景下的SW類似一個"客戶端代理"植兰。
如何使用Service Worker實現(xiàn)離線可用
1. 注冊Service Worker
在index.js來注冊Service Worker(sw.js)
//注冊Service Worker份帐,腳本為sw.js
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').then(function () {
console.log("service Worker 注冊成功");
});
}
Service Worker的各類操作都被設(shè)計為異步, 用以避免一些長時間的阻塞操作楣导。 這些API都是以Promise 的形式來調(diào)用的废境。 所以在接下來的各段代碼中不斷會看到Promise的使用。
2. Service Worker的聲明周期
當(dāng)我們注冊Service Worker后, 它會經(jīng)歷生命周期的各個階段噩凹, 同時會觸發(fā)響應(yīng)的事件巴元。 整個生命周期包括了: installing -> installed -> activated -> redundant。 當(dāng)Service Worker安裝(installed)完畢后驮宴, 會觸發(fā)install事件逮刨;而激活(activated)后, 則會觸發(fā)activate事件幻赚。
寫個例子監(jiān)聽install事件:
//監(jiān)聽install
self.addEventListener('install', function () {
console.log("Service Worker當(dāng)前狀態(tài):install");
});
self
是Service Worker中一個特的全局變量禀忆,類似于我們常見的Window對象。 self引用了當(dāng)前這個Service Worker落恼。
3. 緩存靜態(tài)資源
要使網(wǎng)頁離線可用箩退, 就需要將所需資源緩存下來。我們需要一個資源列表佳谦, 當(dāng)Service Worker被激活時戴涝, 會將該列表內(nèi)的資源存進(jìn)cache。在sw.js中:
//創(chuàng)建一個cacheName
const cacheName = 'cache-0-1-2';
//需要緩存的資源列表
const cacheFiles = [
'/',
'./index.html',
'./index.js',
'./style.css',
'./img/wang.jpeg',
'./img/loading.svg'
];
//監(jiān)聽install事件, 完成安裝時钻蔑, 進(jìn)行文件緩存
self.addEventListener('install', function (e) {
console.log("Service Worker當(dāng)前狀態(tài):install");
const cacheOpenPromise = caches.open(cacheName).then(function (cache) {
return cache.addAll(cacheFiles);
});
e.waitUntil(cacheOpenPromise);
});
可以看到啥刻, 首先cacheFiles
中我們列出了所有的靜態(tài)資源依賴。注意 '/'
,由于根路徑也可以訪問頁面咪笑, 因此不要忘記將其也緩存下來可帽。 當(dāng)Service Worker install時, 我們就會通過caches.open()
與caches.addAll()
方法將資源緩存起來窗怒。這里給緩存起了個名字cacheName
映跟,這個值成為這些緩存的key。
caches
是一個全局變量扬虚, 通過它我們可以操作Cache相關(guān)接口努隙。
Cache接口提供緩存的Request/Response對象的存儲機制。Cache接口向wokers一樣辜昵, 是保留在window作用域下的荸镊。盡管被定義在Service Worker中, 但是它不必一定要配合Service Worker使用堪置。 ——MDN躬存。
4. 使用緩存的靜態(tài)資源
到目前為止, 我們僅僅注冊了一個Service Worker, 并在其install時緩存了一些靜態(tài)資源舀锨。 然而岭洲, 如果這是運行網(wǎng)頁時會發(fā)現(xiàn)無法使用我們的緩存。因為我們僅僅緩存了這些資源雁竞, 然而瀏覽器并不知道需要如何使用它們钦椭,換句話說,瀏覽器仍然會通過向服務(wù)器發(fā)送請求來等待并使用這些資源碑诉。
在文章的前半部分提到了Service Worker 可以做 ”客戶端代理“ ——用Service Worker 來幫助如何使用緩存彪腔。
下圖是一個簡單的策略:
-
瀏覽器有cache時:
-
瀏覽器無cache時:
- 瀏覽器發(fā)起請求, 請求各類靜態(tài)資源(html,css,img
- Service Worker攔截瀏覽器請求进栽, 并查詢當(dāng)前cache
- 若存在cache則直接返回德挣,結(jié)束
- 若不存在cache,則通過
fetch
方法向服務(wù)器端發(fā)起請求快毛, 并返回請求結(jié)果給瀏覽器
//cache存在則使用cache格嗅,無cache則fetch服務(wù)器端請求資源
self.addEventListener('fetch', function (e) {
e.respondWith(
caches.match(e.request).then(function (cache) {
return cache || fetch(e.request);
}).catch(function (err) {
console.log(err);
return fetch(e.request);
})
);
});
fetch
事件會監(jiān)聽所有瀏覽器請求。 e.responedWith()
方法接受Promise作為參數(shù)唠帝,通過它讓Service Worker向瀏覽器返回數(shù)據(jù)屯掖。caches.match(e.request)
則可以查看當(dāng)前的請求是否有一份本地緩存, 如果有緩存襟衰,則直接向瀏覽器返回cache
贴铜。如果沒有,則Service Worker 會向后端服務(wù)發(fā)起一個fetch(e.request)
的請求瀑晒, 并將請求返回給瀏覽器绍坝。
到目前為止, 我們的網(wǎng)頁靜態(tài)資源將會被緩存到本地苔悦;以后再訪問時轩褐, 就會使用這些緩存而不發(fā)起網(wǎng)絡(luò)請求,因此無網(wǎng)的情況下玖详,依舊能訪問該網(wǎng)頁把介。
5. 更新靜態(tài)緩存資源
當(dāng)緩存可以使用時,細(xì)心的話會發(fā)現(xiàn)一個小問題竹宋, 當(dāng)我們將資源緩存后劳澄,除非注銷(unregister)sw.js、手動清除緩存蜈七、否則新的靜態(tài)資源將無法緩存秒拔。
解決這個問題的一個簡單方法就是修改cacheName
。由于瀏覽器判斷sw.js是否更新是通過字節(jié)方式飒硅, 因此修改cacheName
會重新出發(fā)install并緩存資源砂缩。 此外,在activate事件中三娩, 我們需要檢查cacheName
是否變化庵芭,如果變化則表示有了新的緩存,原有緩存需要刪除雀监。
//監(jiān)聽activite事件双吆, 激活后通過cache的key來判斷是否需要更新cache中的靜態(tài)資源
self.addEventListener('activate',function (e) {
console.log('Service Worker 當(dāng)前狀態(tài): activate');
const cachePromise = caches.keys().then(function (keys) {
return Promise.all(keys.map(function (key) {
if(key !== cacheName) {
return caches.delete(key);
}
}))
});
e.waitUntil(cachePromise);
return self.clients.claim(); //激活觸發(fā)Service Worker
});
6.緩存離線接口##
離線時眨唬,我們發(fā)現(xiàn)只展示了靜態(tài)資源,而接口數(shù)據(jù)渲染的地方還是空白一片好乐。
所以我們把XHR也緩存一份匾竿, 然后再請求時, 會優(yōu)先使用本地緩存蔚万, 然后再向服務(wù)器端請求數(shù)據(jù)岭妖。大致過程如下:
首先需要改造一下sw.js的fetch事件進(jìn)行API數(shù)據(jù)的緩存:
//定義api緩存name
const apiCacheName = 'api-0-1-1';
// cache存在則使用cache,無cache則fetch服務(wù)器端請求資源
self.addEventListener('fetch', function (e) {
// 需要緩存的XHR請求
const cacheRequestUrls = [
'/book?'
];
console.log('當(dāng)前請求接口:' + e.request.url);
//判斷當(dāng)前請求是否需要緩存
const needCacheXhr = cacheRequestUrls.some(function (url) {
return e.request.url.indexOf(url) > -1;
});
if(needCacheXhr) {
// 使用fetch請求數(shù)據(jù)反璃, 并將請求結(jié)果clone一份緩存到cache
// 緩存后在browser中使用全局變量caches獲取
caches.open(apiCacheName).then(function (cache) {
return fetch(e.request).then(function (res) {
cache.put(e.request.url, res.clone());
return res;
})
})
}else {
// 非api請求昵慌, 直接查詢cache
// 如果有cache直接返回, 否則通過fetch請求
e.respondWith(
caches.match(e.request).then(function (cache) {
return cache || fetch(e.request);
}).catch(function (err) {
console.log(err);
return fetch(e.request);
})
);
}
});
這里也為API緩存數(shù)據(jù)創(chuàng)建了一個專門的緩存位置淮蜈, key值為變量apiCacheName斋攀。在fetch
事件中, 我們首先通過對比當(dāng)前請求與cacheRequestUrls
來判斷是否需要緩存XHR請求的數(shù)據(jù)梧田, 如果是的話就使用fetch方法向后端發(fā)起請求蜻韭。
在fetch.then
中我們以請求的url 為key, 向cache中更新了一份當(dāng)前請求所返回數(shù)據(jù)的緩存,cache.put(e.request.url, res.clone())
柿扣。這里使用.clone()
方法拷貝一份響應(yīng)數(shù)據(jù)肖方, 這樣就可以對響應(yīng)緩存進(jìn)行各類操作而不用擔(dān)心響應(yīng)原數(shù)據(jù)被修改了。
7.使用離線XHR數(shù)據(jù)未状, 完成離線渲染俯画,提升響應(yīng)速度
目前為止, 我們已經(jīng)對Service Worker(sw.js) XHR進(jìn)行了緩存改造司草, 最后只剩如何在XHR請求時有策略的使用緩存了艰垂, 這一部分的改造幾種在index.js, 也就是我們的前端腳本埋虹。
先看看XHR緩存的這張圖:
和普通情況不同猜憎, 前端瀏覽器會首先嘗試獲取緩存數(shù)據(jù)并使用其渲染頁面。同時搔课, 瀏覽器也會發(fā)起一個XHR請求胰柑,Service Worker 通過將請求返回的數(shù)據(jù)更新到緩存中的同事向前端頁面返回數(shù)據(jù)(這一部分主要就是緩存策略);最終爬泥,如果判斷返回的數(shù)據(jù)與最開始取到的cache不一致柬讨,則重新渲染界面, 否則忽略袍啡。
為了使代碼更加清晰踩官,我們將原來XHR請求部分單獨剝離出來,作為一個方法 getApiDataRemote()
使用境输, 同事將其改造為Promise蔗牡。
我們知道颖系,在Service Worker 中是可以通過caches
變量來訪問到緩存對象的。 而在前端應(yīng)用中辩越,也仍然可以通過caches
來訪問緩存集晚。 為了統(tǒng)一代碼, 將獲取該請求的緩存數(shù)據(jù)也封裝為Promise方法getApiDataFromCache()
区匣。
// 前端頁面中
function getApiDataRemote(url) {
if('caches' in window) {
return caches.match(url).then(function (cache) {
if(!cache) {
return;
}
return cache.json();
})
}else {
return Promise.resolve();
}
}
而原本在請求接口queryData()
方法中, 我們會請求后端數(shù)據(jù)蒋院, 然后再渲染頁面亏钩;而現(xiàn)在則加上緩存的渲染:
···
function queryBook(value) {
var input = document.querySelector('#js-search-input');
var query = value || input.value;
var url = '/book?q=' + query;
//請求緩存
var remotePromise = getApiDataRemote(url);
var cacheData;
//先使用緩存數(shù)據(jù)渲染
getApiDataFromCache(url).then(data => {
if(data) {
loading(false);
input.blur();
fillList(data.data.songs);
document.querySelector('#js-thanks').style = 'display: block';
}
cacheData = data || {};
return remotePromise;
}).then(function (data) {
if(JSON.stringify(data) !== JSON.stringify(cacheData)) {
loading(false);
input.blur();
fillList(data.data.songs);
document.querySelector('#js-thanks').style = 'display: block';
}
})
}
如果getApiDataFromCache(url).then
返回緩存數(shù)據(jù), 則使用它進(jìn)行先渲染欺旧。 當(dāng)getApiDataRemote()
返回的數(shù)據(jù)時姑丑, 與cacheData
進(jìn)行比對, 只有數(shù)據(jù)不一致的時候重新渲染頁面即可辞友。 這里使用 JSON.stringify()
進(jìn)行粗略的比較栅哀,這么做有兩個優(yōu)勢:
- 離線可用。 如果我們之前訪問過某些URL,那么及時在離線的情況下称龙, 重復(fù)響應(yīng)的操作依然可以正常顯示頁面留拾。
- 優(yōu)化體驗,提高訪問速度讀取本地cache耗時相對對網(wǎng)絡(luò)請求時間很低鲫尊,因此就會有一種 "秒開"痴柔, "秒響應(yīng)"的感覺。