国产成人精品无码青草_亚洲国产美女精品久久久久∴_欧美人与鲁交大毛片免费_国产果冻豆传媒麻婆精东

15158846557 在線咨詢 在線咨詢
15158846557 在線咨詢
所在位置: 首頁(yè) > 營(yíng)銷資訊 > 網(wǎng)站運(yùn)營(yíng) > 使用Service Worker做一個(gè)PWA離線網(wǎng)頁(yè)應(yīng)用

使用Service Worker做一個(gè)PWA離線網(wǎng)頁(yè)應(yīng)用

時(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)也是能正常打開。

1. 什么是Service Worker

Service Worker是谷歌發(fā)起的實(shí)現(xiàn)PWA(Progressive Web App)的一個(gè)關(guān)鍵角色,PWA是為了解決傳統(tǒng)Web APP的缺點(diǎn):

(1)沒有桌面入口

(2)無(wú)法離線使用

(3)沒有Push推送

那Service Worker的具體表現(xiàn)是怎么樣的呢?如下圖所示:







Service Worker是在后臺(tái)啟動(dòng)的一條服務(wù)Worker線程,上圖我開了兩個(gè)標(biāo)簽頁(yè),所以顯示了兩個(gè)Client,但是不管開多少個(gè)頁(yè)面都只有一個(gè)Worker在負(fù)責(zé)管理。這個(gè)Worker的工作是把一些資源緩存起來(lái),然后攔截頁(yè)面的請(qǐng)求,先看下緩存庫(kù)里有沒有,如果有的話就從緩存里取,響應(yīng)200,反之沒有的話就走正常的請(qǐng)求。具體來(lái)說(shuō),Service Worker結(jié)合Web App Manifest能完成以下工作(這也是PWA的檢測(cè)標(biāo)準(zhǔn)):







包括能夠離線使用、斷網(wǎng)時(shí)返回200、能提示用戶把網(wǎng)站添加一個(gè)圖標(biāo)到桌面上等。

2. Service Worker的支持情況

Service Worker目前只有Chrome/Firfox/Opera支持:







Safari和Edge也在準(zhǔn)備支持Service Worker,由于Service Worker是谷歌主導(dǎo)的一項(xiàng)標(biāo)準(zhǔn),對(duì)于生態(tài)比較封閉的Safari來(lái)說(shuō)也是迫于形勢(shì)開始準(zhǔn)備支持了,在Safari TP版本,可以看到:







在實(shí)驗(yàn)功能(Experimental Features)里已經(jīng)有Service Worker的菜單項(xiàng)了,只是即使打開也是不能用,會(huì)提示你還沒有實(shí)現(xiàn):







但不管如何,至少說(shuō)明Safari已經(jīng)準(zhǔn)備支持Service Worker了。另外還可以看到在今年2017年9月發(fā)布的Safari 11.0.1版本已經(jīng)支持WebRTC了,所以Safari還是一個(gè)上進(jìn)的孩子。

Edge也準(zhǔn)備支持,所以Service Worker的前景十分光明。

3. 使用Service Worker

Service Worker的使用套路是先注冊(cè)一個(gè)Worker,然后后臺(tái)就會(huì)啟動(dòng)一條線程,可以在這條線程啟動(dòng)的時(shí)候去加載一些資源緩存起來(lái),然后監(jiān)聽fetch事件,在這個(gè)事件里攔截頁(yè)面的請(qǐng)求,先看下緩存里有沒有,如果有直接返回,否則正常加載?;蛘呤且婚_始不緩存,每個(gè)資源請(qǐng)求后再拷貝一份緩存起來(lái),然后下一次請(qǐng)求的時(shí)候緩存里就有了。

(1)注冊(cè)一個(gè)Service Worker

Service Worker對(duì)象是在window.navigator里面,如下代碼:

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è)備意義比較大。

還有一點(diǎn)需要注意的是Service Worker和Cookie一樣是有Path路徑的概念的,如果你設(shè)定一個(gè)cookie假設(shè)叫time的path=/page/A,在/page/B這個(gè)頁(yè)面是不能夠獲取到這個(gè)cookie的,如果設(shè)置cookie的path為根目錄/,則所有頁(yè)面都能獲取到。類似地,如果注冊(cè)的時(shí)候使用的js路徑為/page/sw.js,那么這個(gè)Service Worker只能管理/page路徑下的頁(yè)面和資源,而不能夠處理/api路徑下的,所以一般把Service Worker注冊(cè)到頂級(jí)目錄,如上面代碼的"/sw-3.js",這樣這個(gè)Service Worker就能接管頁(yè)面的所有資源了。

(2)Service Worker安裝和激活

注冊(cè)完之后,Service Worker就會(huì)進(jìn)行安裝,這個(gè)時(shí)候會(huì)觸發(fā)install事件,在install事件里面可以緩存一些資源,如下sw-3.js:

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)所示:







Service Worker的API基本上都是返回Promise對(duì)象避免堵塞,所以要用Promise的寫法。上面在安裝Service Worker的時(shí)候就把首頁(yè)的請(qǐng)求給緩存起來(lái)了。在Service Worker的運(yùn)行環(huán)境里面它有一個(gè)caches的全局對(duì)象,這個(gè)是緩存的入口,還有一個(gè)常用的clients的全局對(duì)象,一個(gè)client對(duì)應(yīng)一個(gè)標(biāo)簽頁(yè)。

在Service Worker里面可以使用fetch等API,它和DOM是隔離的,沒有windows/document對(duì)象,無(wú)法直接操作DOM,無(wú)法直接和頁(yè)面交互,在Service Worker里面無(wú)法得知當(dāng)前頁(yè)面打開了、當(dāng)前頁(yè)面的url是什么,因?yàn)橐粋€(gè)Service Worker管理當(dāng)前打開的幾個(gè)標(biāo)簽頁(yè),可以通過(guò)clients知道所有頁(yè)面的url。還有可以通過(guò)postMessage的方式和主頁(yè)面互相傳遞消息和數(shù)據(jù),進(jìn)而做些控制。

install完之后,就會(huì)觸發(fā)Service Worker的active事件:

this.addEventListener("active", function(event) { console.log("service worker is active");});Service Worker激活之后就能夠監(jiān)聽fetch事件了,我們希望每獲取一個(gè)資源就把它緩存起來(lái),就不用像上一篇提到的Manifest需要先生成一個(gè)列表。

你可能會(huì)問,當(dāng)我刷新頁(yè)面的時(shí)候不是又重新注冊(cè)安裝和激活了一個(gè)Service Worker?雖然又調(diào)了一次注冊(cè),但并不會(huì)重新注冊(cè),它發(fā)現(xiàn)"sw-3.js"這個(gè)已經(jīng)注冊(cè)了,就不會(huì)再注冊(cè)了,進(jìn)而不會(huì)觸發(fā)install和active事件,因?yàn)楫?dāng)前Service Worker已經(jīng)是active狀態(tài)了。當(dāng)需要更新Service Worker時(shí),如變成"sw-4.js",或者改變sw-3.js的文本內(nèi)容,就會(huì)重新注冊(cè),新的Service Worker會(huì)先install然后進(jìn)入waiting狀態(tài),等到重啟瀏覽器時(shí),老的Service Worker就會(huì)被替換掉,新的Service Worker進(jìn)入active狀態(tài),如果不想等到重新啟動(dòng)瀏覽器可以像上面一樣在install里面調(diào)skipWaiting:

this.skipWaiting();

(3)fetch資源后cache起來(lái)

如下代碼,監(jiān)聽fetch事件做些處理:

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è)資源。

上面代碼的util.fetchPut是這樣實(shí)現(xiàn)的:

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è)判斷。

這樣一旦用戶打開過(guò)一次頁(yè)面,Service Worker就安裝好了,他刷新頁(yè)面或者打開第二個(gè)頁(yè)面的時(shí)候就能夠把請(qǐng)求的資源一一做緩存,包括圖片、CSS、JS等,只要緩存里有了不管用戶在線或者離線都能夠正常訪問。這樣我們自然會(huì)有一個(gè)問題,這個(gè)緩存空間到底有多大?上一篇我們提到Manifest也算是本地存儲(chǔ),PC端的Chrome是5Mb,其實(shí)這個(gè)說(shuō)法在新版本的Chrome已經(jīng)不準(zhǔn)確了,在Chrome 61版本可以看到本地存儲(chǔ)的空間和使用情況:







其中Cache Storage是指Service Worker和Manifest占用的空間大小和,上圖可以看到總的空間大小是20GB,幾乎是unlimited,所以基本上不用擔(dān)心緩存會(huì)不夠用。

(4)cache html

上面第(3)步把圖片、js、css緩存起來(lái)了,但是如果把頁(yè)面html也緩存了,例如把首頁(yè)緩存了,就會(huì)有一個(gè)尷尬的問題——Service Worker是在頁(yè)面注冊(cè)的,但是現(xiàn)在獲取頁(yè)面的時(shí)候是從緩存取的,每次都是一樣的,所以就導(dǎo)致無(wú)法更新Service Worker,如變成sw-5.js,但是PWA又要求我們能緩存頁(yè)面html。那怎么辦呢?谷歌的開發(fā)者文檔它只是提到會(huì)存在這個(gè)問題,但并沒有說(shuō)明怎么解決這個(gè)問題。這個(gè)的問題的解決就要求我們要有一個(gè)機(jī)制能知道html更新了,從而把緩存里的html給替換掉。

Manifest更新緩存的機(jī)制是去看Manifest的文本內(nèi)容有沒有發(fā)生變化,如果發(fā)生變化了,則會(huì)去更新緩存,Service Worker也是根據(jù)sw.js的文本內(nèi)容有沒有發(fā)生變化,我們可以借鑒這個(gè)思想,如果請(qǐng)求的是html并從緩存里取出來(lái)后,再發(fā)個(gè)請(qǐng)求獲取一個(gè)文件看html更新時(shí)間是否發(fā)生變化,如果發(fā)生變化了則說(shuō)明發(fā)生更改了,進(jìn)而把緩存給刪了。所以可以在服務(wù)端通過(guò)控制這個(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è)面。

需要注意的是,要把這個(gè)json文件的http cache時(shí)間設(shè)置成0,這樣瀏覽器就不會(huì)緩存了,如下nginx的配置:

location ~* .sw.json$ { expires 0;}因?yàn)檫@個(gè)文件是需要實(shí)時(shí)獲取的,不能被緩存,firefox默認(rèn)會(huì)緩存,Chrome不會(huì),加上http緩存時(shí)間為0,firefox也不會(huì)緩存了。

還有一種更新是用戶更新的,例如用戶發(fā)表了評(píng)論,需要在頁(yè)面通知service worker把html緩存刪了重新獲取,這是一個(gè)反過(guò)來(lái)的消息通知:

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)求了。

這樣就解決了實(shí)時(shí)更新的問題。

4. Http/Manifest/Service Worker三種cache的關(guān)系

要緩存可以使用三種手段,使用Http Cache設(shè)置緩存時(shí)間,也可以用Manifest的Application Cache,還可以用Service Worker緩存,如果三者都用上了會(huì)怎么樣呢?

會(huì)以Service Worker為優(yōu)先,因?yàn)镾ervice Worker把請(qǐng)求攔截了,它最先做處理,如果它緩存庫(kù)里有的話直接返回,沒有的話正常請(qǐng)求,就相當(dāng)于沒有Service Worker了,這個(gè)時(shí)候就到了Manifest層,Manifest緩存里如果有的話就取這個(gè)緩存,如果沒有的話就相當(dāng)于沒有Manifest了,于是就會(huì)從Http緩存里取了,如果Http緩存里也沒有就會(huì)發(fā)請(qǐng)求去獲取,服務(wù)端根據(jù)Http的etag或者M(jìn)odified Time可能會(huì)返回304 Not Modified,否則正常返回200和數(shù)據(jù)內(nèi)容。這就是整一個(gè)獲取的過(guò)程。

所以如果既用了Manifest又用Service Worker的話應(yīng)該會(huì)導(dǎo)致同一個(gè)資源存了兩次。但是可以讓支持Service Worker的瀏覽器使用Service Worker,而不支持的使用Manifest.

5. 使用Web App Manifest添加桌面入口

注意這里說(shuō)的是另外一個(gè)Manifest,這個(gè)Manifest是一個(gè)json文件,用來(lái)放網(wǎng)站icon名稱等信息以便在桌面添加一個(gè)圖標(biāo),以及制造一種打開這個(gè)網(wǎng)頁(yè)就像打開App一樣的效果。上面一直說(shuō)的Manifest是被廢除的Application Cache的Manifest。

這個(gè)Maifest.json文件可以這么寫:

{ "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指定打開之后的入口鏈接。

然后添加一個(gè)link標(biāo)簽指向這個(gè)manifest文件:

<link rel="manifest" href="/html/app-manifest/manifest.json">這樣結(jié)合Service Worker緩存:


把start_url指向的頁(yè)面用Service Worker緩存起來(lái),這樣當(dāng)用戶用Chrome瀏覽器打開這個(gè)網(wǎng)頁(yè)的時(shí)候,Chrome就會(huì)在底部彈一個(gè)提示,詢問用戶是否把這個(gè)網(wǎng)頁(yè)添加到桌面,如果點(diǎn)“添加”就會(huì)生成一個(gè)桌面圖標(biāo),從這個(gè)圖標(biāo)點(diǎn)進(jìn)去就像打開一個(gè)App一樣。感受如下:







比較尷尬的是Manifest目前只有Chrome支持,并且只能在安卓系統(tǒng)上使用,IOS的瀏覽器無(wú)法添加一個(gè)桌面圖標(biāo),因?yàn)镮OS沒有開放這種API,但是自家的Safari卻又是可以的。




綜上,本文介紹了怎么用Service Worker結(jié)合Manifest做一個(gè)PWA離線Web APP,主要是用Service Worker控制緩存,由于是寫JS,比較靈活,還可以與頁(yè)面進(jìn)行通信,另外通過(guò)請(qǐng)求頁(yè)面的更新時(shí)間來(lái)判斷是否需要更新html緩存。Service Worker的兼容性不是特別好,但是前景比較光明,瀏覽器都在準(zhǔn)備支持?,F(xiàn)階段可以結(jié)合offline cache的Manifest做離線應(yīng)用。




相關(guān)閱讀:

  1. 為什么要把網(wǎng)站升級(jí)到HTTPS
  2. 怎樣把網(wǎng)站升級(jí)到http/2
  3. 我是怎樣讓網(wǎng)站用上HTML5 Manifest


關(guān)鍵詞:使用

74
73
25
news

版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。

為了最佳展示效果,本站不支持IE9及以下版本的瀏覽器,建議您使用谷歌Chrome瀏覽器。 點(diǎn)擊下載Chrome瀏覽器
關(guān)閉