時(shí)間:2023-07-03 14:24:01 | 來(lái)源:網(wǎng)站運(yùn)營(yíng)
時(shí)間:2023-07-03 14:24:01 來(lái)源:網(wǎng)站運(yùn)營(yíng)
使用Service Worker做一個(gè)PWA離線網(wǎng)頁(yè)應(yīng)用:在上一篇《我是怎樣讓網(wǎng)站用上HTML5 Manifest》介紹了怎么用Manifest做一個(gè)離線網(wǎng)頁(yè)應(yīng)用,結(jié)果被廣大網(wǎng)友吐槽說(shuō)這個(gè)東西已經(jīng)被deprecated,移出web標(biāo)準(zhǔn)了,現(xiàn)在被Service Worker替代了,不管怎么樣,Manifest的一些思想還是可以借用的。筆者又將網(wǎng)站升級(jí)到了Service Worker,如果是用Chrome等瀏覽器就用Service Worker做離線緩存,如果是Safari瀏覽器就還是用Manifest,讀者可以打開這個(gè)網(wǎng)站https://www.rrfed.com感受一下,斷網(wǎng)也是能正常打開。window.addEventListener("load", function() { console.log("Will the service worker register?"); navigator.serviceWorker.register('/sw-3.js') .then(function(reg){ console.log("Yes, it did."); }).catch(function(err) { console.log("No it didn't. This happened: ", err) }); });
在頁(yè)面load完之后注冊(cè),注冊(cè)的時(shí)候傳一個(gè)js文件給它,這個(gè)js文件就是Service Worker的運(yùn)行環(huán)境,如果不能成功注冊(cè)的話就會(huì)拋異常,如Safari TP雖然有這個(gè)對(duì)象,但是會(huì)拋異常無(wú)法使用,就可以在catch里面處理。這里有個(gè)問題是為什么需要在load事件啟動(dòng)呢?因?yàn)槟阋~外啟動(dòng)一個(gè)線程,啟動(dòng)之后你可能還會(huì)讓它去加載資源,這些都是需要占用CPU和帶寬的,我們應(yīng)該保證頁(yè)面能正常加載完,然后再啟動(dòng)我們的后臺(tái)線程,不能與正常的頁(yè)面加載產(chǎn)生競(jìng)爭(zhēng),這個(gè)在低端移動(dòng)設(shè)備意義比較大。const CACHE_NAME = "fed-cache";this.addEventListener("install", function(event) { this.skipWaiting(); console.log("install service worker"); // 創(chuàng)建和打開一個(gè)緩存庫(kù) caches.open(CACHE_NAME); // 首頁(yè) let cacheResources = ["https://www.rrfed.com/?launcher=true"]; event.waitUntil( // 請(qǐng)求資源并添加到緩存里面去 caches.open(CACHE_NAME).then(cache => { cache.addAll(cacheResources); }) );});
通過(guò)上面的操作,創(chuàng)建和添加了一個(gè)緩存庫(kù)叫fed-cache,如下Chrome控制臺(tái)所示:this.addEventListener("active", function(event) { console.log("service worker is active");});
Service Worker激活之后就能夠監(jiān)聽fetch事件了,我們希望每獲取一個(gè)資源就把它緩存起來(lái),就不用像上一篇提到的Manifest需要先生成一個(gè)列表。this.skipWaiting();
this.addEventListener("fetch", function(event) { event.respondWith( caches.match(event.request).then(response => { // cache hit if (response) { return response; } return util.fetchPut(event.request.clone()); }) );});
先調(diào)caches.match看一下緩存里面是否有了,如果有直接返回緩存里的response,否則的話正常請(qǐng)求資源并把它放到cache里面。放在緩存里資源的key值是Request對(duì)象,在match的時(shí)候,需要請(qǐng)求的url和header都一致才是相同的資源,可以設(shè)定第二個(gè)參數(shù)ignoreVary:caches.match(event.request, {ignoreVary: true})
表示只要請(qǐng)求url相同就認(rèn)為是同一個(gè)資源。let util = { fetchPut: function (request, callback) { return fetch(request).then(response => { // 跨域的資源直接return if (!response || response.status !== 200 || response.type !== "basic") { return response; } util.putCache(request, response.clone()); typeof callback === "function" && callback(); return response; }); }, putCache: function (request, resource) { // 后臺(tái)不要緩存,preview鏈接也不要緩存 if (request.method === "GET" && request.url.indexOf("wp-admin") < 0 && request.url.indexOf("preview_id") < 0) { caches.open(CACHE_NAME).then(cache => { cache.put(request, resource); }); } }};
需要注意的是跨域的資源不能緩存,response.status會(huì)返回0,如果跨域的資源支持CORS,那么可以把request的mod改成cors。如果請(qǐng)求失敗了,如404或者是超時(shí)之類的,那么也直接返回response讓主頁(yè)面處理,否則的話說(shuō)明加載成功,把這個(gè)response克隆一個(gè)放到cache里面,然后再返回response給主頁(yè)面線程。注意能放緩存里的資源一般只能是GET,通過(guò)POST獲取的是不能緩存的,所以要做個(gè)判斷(當(dāng)然你也可以手動(dòng)把request對(duì)象的method改成get),還有把一些個(gè)人不希望緩存的資源也做個(gè)判斷。this.addEventListener("fetch", function(event) { event.respondWith( caches.match(event.request).then(response => { // cache hit if (response) { //如果取的是html,則看發(fā)個(gè)請(qǐng)求看html是否更新了 if (response.headers.get("Content-Type").indexOf("text/html") >= 0) { console.log("update html"); let url = new URL(event.request.url); util.updateHtmlPage(url, event.request.clone(), event.clientId); } return response; } return util.fetchPut(event.request.clone()); }) );});
通過(guò)響應(yīng)頭header的content-type是否為text/html,如果是的話就去發(fā)個(gè)請(qǐng)求獲取一個(gè)文件,根據(jù)這個(gè)文件的內(nèi)容決定是否需要?jiǎng)h除緩存,這個(gè)更新的函數(shù)util.updateHtmlPage是這么實(shí)現(xiàn)的:let pageUpdateTime = {};let util = { updateHtmlPage: function (url, htmlRequest) { let pageName = util.getPageName(url); let jsonRequest = new Request("/html/service-worker/cache-json/" + pageName + ".sw.json"); fetch(jsonRequest).then(response => { response.json().then(content => { if (pageUpdateTime[pageName] !== content.updateTime) { console.log("update page html"); // 如果有更新則重新獲取html util.fetchPut(htmlRequest); pageUpdateTime[pageName] = content.updateTime; } }); }); }, delCache: function (url) { caches.open(CACHE_NAME).then(cache => { console.log("delete cache " + url); cache.delete(url, {ignoreVary: true}); }); }};
代碼先去獲取一個(gè)json文件,一個(gè)頁(yè)面會(huì)對(duì)應(yīng)一個(gè)json文件,這個(gè)json的內(nèi)容是這樣的:{"updateTime":"10/2/2017, 3:23:57 PM","resources": {img: [], css: []}}
里面主要有一個(gè)updateTime的字段,如果本地內(nèi)存沒有這個(gè)頁(yè)面的updateTime的數(shù)據(jù)或者是和最新updateTime不一樣,則重新去獲取 html,然后放到緩存里。接著需要通知頁(yè)面線程數(shù)據(jù)發(fā)生變化了,你刷新下頁(yè)面吧。這樣就不用等用戶刷新頁(yè)面才能生效了。所以當(dāng)刷新完頁(yè)面后用postMessage通知頁(yè)面:let util = { postMessage: async function (msg) { const allClients = await clients.matchAll(); allClients.forEach(client => client.postMessage(msg)); }};util.fetchPut(htmlRequest, false, function() { util.postMessage({type: 1, desc: "html found updated", url: url.href});});
并規(guī)定type: 1就表示這是一個(gè)更新html的消息,然后在頁(yè)面監(jiān)聽message事件:if ("serviceWorker" in navigator) { navigator.serviceWorker.addEventListener("message", function(event) { let msg = event.data; if (msg.type === 1 && window.location.href === msg.url) { console.log("recv from service worker", event.data); window.location.reload(); } }); }
然后當(dāng)我們需要更新html的時(shí)候就更新json文件,這樣用戶就能看到最新的頁(yè)面了?;蛘呤钱?dāng)用戶重新啟動(dòng)瀏覽器的時(shí)候會(huì)導(dǎo)致Service Worker的運(yùn)行內(nèi)存都被清空了,即存儲(chǔ)頁(yè)面更新時(shí)間的變量被清空了,這個(gè)時(shí)候也會(huì)重新請(qǐng)求頁(yè)面。location ~* .sw.json$ { expires 0;}
因?yàn)檫@個(gè)文件是需要實(shí)時(shí)獲取的,不能被緩存,firefox默認(rèn)會(huì)緩存,Chrome不會(huì),加上http緩存時(shí)間為0,firefox也不會(huì)緩存了。if ("serviceWorker" in navigator) { document.querySelector(".comment-form").addEventListener("submit", function() { navigator.serviceWorker.controller.postMessage({ type: 1, desc: "remove html cache", url: window.location.href} ); } });}
Service Worker也監(jiān)聽message事件:const messageProcess = { // 刪除html index 1: function (url) { util.delCache(url); }};let util = { delCache: function (url) { caches.open(CACHE_NAME).then(cache => { console.log("delete cache " + url); cache.delete(url, {ignoreVary: true}); }); }};this.addEventListener("message", function(event) { let msg = event.data; console.log(msg); if (typeof messageProcess[msg.type] === "function") { messageProcess[msg.type](msg.url); }});
根據(jù)不同的消息類型調(diào)不同的回調(diào)函數(shù),如果是1的話就是刪除cache。用戶發(fā)表完評(píng)論后會(huì)觸發(fā)刷新頁(yè)面,刷新的時(shí)候緩存已經(jīng)被刪了就會(huì)重新去請(qǐng)求了。{ "short_name": "人人FED", "name": "人人網(wǎng)FED,專注于前端技術(shù)", "icons": [ { "src": "/html/app-manifest/logo_48.png", "type": "image/png", "sizes": "48x48" }, { "src": "/html/app-manifest/logo_96.png", "type": "image/png", "sizes": "96x96" }, { "src": "/html/app-manifest/logo_192.png", "type": "image/png", "sizes": "192x192" }, { "src": "/html/app-manifest/logo_512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": "/?launcher=true", "display": "standalone", "background_color": "#287fc5", "theme_color": "#fff"}
icon需要準(zhǔn)備多種規(guī)格,最大需要512px * 512px的,這樣Chrome會(huì)自動(dòng)去選取合適的圖片。如果把display改成standalone,從生成的圖標(biāo)打開就會(huì)像打開一個(gè)App一樣,沒有瀏覽器地址欄那些東西了。start_url指定打開之后的入口鏈接。<link rel="manifest" href="/html/app-manifest/manifest.json">
這樣結(jié)合Service Worker緩存:關(guān)鍵詞:使用
客戶&案例
營(yíng)銷資訊
關(guān)于我們
客戶&案例
營(yíng)銷資訊
關(guān)于我們
微信公眾號(hào)
版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。