摘要:學習與實踐系列文章已整理至學習手冊,文字內容已同步至。本文是學習與實踐系列的第三篇文章。引言其中一個令人著迷的能力就是離線可用。但是,如果你注意到文章開頭的圖片就會發現,離線時我們不僅可以訪問,還可以使用搜索功能。
《PWA學習與實踐》系列文章已整理至gitbook - PWA學習手冊,文字內容已同步至learning-pwa-ebook。轉載請注明作者與出處。
本文是《PWA學習與實踐》系列的第三篇文章。文中的代碼都可以在learning-pwa的sw-cache分支上找到(git clone后注意切換到sw-cache分支)。
PWA作為時下最火熱的技術概念之一,對提升Web應用的安全、性能和體驗有著很大的意義,非常值得我們去了解與學習。對PWA感興趣的朋友歡迎關注《PWA學習與實踐》系列文章。
1. 引言PWA其中一個令人著迷的能力就是離線(offline)可用。
離線只是它的一種功能表現而已,具體說來,它可以:
讓我們的Web App在無網(offline)情況下可以訪問,甚至使用部分功能,而不是展示“無網絡連接”的錯誤頁;
讓我們在弱網的情況下,能使用緩存快速訪問我們的應用,提升體驗;
在正常的網絡情況下,也可以通過各種自發控制的緩存方式來節省部分請求帶寬;
……
而這一切,其實都要歸功于PWA背后的英雄 —— Service Worker。
那么,Service Worker是什么呢?你可以把Service Worker簡單理解為一個獨立于前端頁面,在后臺運行的進程。因此,它不會阻塞瀏覽器腳本的運行,同時也無法直接訪問瀏覽器相關的API(例如:DOM、localStorage等)。此外,即使在離開你的Web App,甚至是關閉瀏覽器后,它仍然可以運行。它就像是一個在Web應用背后默默工作的勤勞小蜜蜂,處理著緩存、推送、通知與同步等工作。所以,要學習PWA,繞不開的就是Service Worker。
在接下來的幾篇文章里,我會從如何使用Service Worker來實現資源的緩存、消息的推送、消息的通知以及后臺同步這幾個角度,來介紹相關原理與技術實現。這些部分會是PWA技術的重點。需要特別注意的是,由于Service Worker所具有的強大能力,因此規范規定,Service Worker只能運行在HTTPS域下。然而我們開發時候沒有HTTPS怎么辦?別著急,還有一個貼心的地方——為方便本地開發,Service Worker也可以運行在localhost(127.0.0.1)域下。
好了,簡單了解了Service Worker與它能實現的功能后,我們還是要回到這一篇的主題,也就是Service Worker的第一部分——如何利用Service Worker來實現前端資源的緩存,從而提升產品的訪問速度,做到離線可用。
2. Service Worker是如何實現離線可用的?這一小節會告訴大家,Service Worker是如何讓我們在離線的情況下也能訪問Web App的。當然,離線訪問只是其中一種表現。
首先,我們想一下,當訪問一個web網站時,我們實際上做了什么呢?總體上來說,我們通過與與服務器建立連接,獲取資源,然后獲取到的部分資源還會去請求新的資源(例如html中使用的css、js等)。所以,粗粒度來說,我們訪問一個網站,就是在獲取/訪問這些資源。
可想而知,當處于離線或弱網環境時,我們無法有效訪問這些資源,這就是制約我們的關鍵因素。因此,一個最直觀的思路就是:如果我們把這些資源緩存起來,在某些情況下,將網絡請求變為本地訪問,這樣是否能解決這一問題?是的。但這就需要我們有一個本地的cache,可以靈活地將各類資源進行本地存取。
有了本地的cache還不夠,我們還需要能夠有效地使用緩存、更新緩存與清除緩存,進一步應用各種個性化的緩存策略。而這就需要我們有個能夠控制緩存的“worker”——這也就是Service Worker的部分工作之一。順便多說一句,可能有人還記得 ApplicationCache 這個API。當初它的設計同樣也是為了實現Web資源的緩存,然而就是因為不夠靈活等各種缺陷,如今已被Service Worker與cache API所取代了。
Service Worker有一個非常重要的特性:你可以在Service Worker中監聽所有客戶端(Web)發出的請求,然后通過Service Worker來代理,向后端服務發起請求。通過監聽用戶請求信息,Service Worker可以決定是否使用緩存來作為Web請求的返回。
下圖展示普通Web App與添加了Service Worker的Web App在網絡請求上的差異:
這里需要強調一下,雖然圖中好像將瀏覽器、SW(Service Worker)與后端服務三者并列放置了,但實際上瀏覽器(你的Web應用)和SW都是運行在你的本機上的,所以這個場景下的SW類似一個“客戶端代理”。
了解了基本概念之后,就可以具體來看下,我們如何應用這個技術來實現一個離線可用的Web應用。
3. 如何使用Service Worker實現離線可用的“秒開”應用還記得我們之前的那個圖書搜索的demo Web App么?不了解的朋友可以看下本系列的第一篇文章,當然你可以忽略細節,繼續往下了解技術原理。
沒錯,這次我仍然會基于它進行改造。在上一篇添加了manifest后,它已經擁有了自己的桌面圖標,并有一個很像Native App的外殼;而今天,我會讓它變得更酷。
如果想要跟著文章內容一起實踐,可以在這里下載到所需的全部代碼。3.1. 注冊Service Worker
記得切換到manifest分支,因為本篇內容,是基于上一篇的最終代碼進行相應的開發與升級。畢竟我們的最終目標是將這個普通的“圖書搜索”demo升級為PWA。
注意,我們的應用始終應該是漸進可用的,在不支持Service Worker的環境下,也需要保證其可用性。要實現這點,可以通過特性檢測,在index.js中來注冊我們的Service Worker(sw.js):
// index.js // 注冊service worker,service worker腳本文件為sw.js if ("serviceWorker" in navigator) { navigator.serviceWorker.register("./sw.js").then(function () { console.log("Service Worker 注冊成功"); }); }
這里我們將sw.js文件注冊為一個Service Worker,注意文件的路徑不要寫錯了。
值得一提的是,Service Worker的各類操作都被設計為異步,用以避免一些長時間的阻塞操作。這些API都是以Promise的形式來調用的。所以你會在接下來的各段代碼中不斷看到Promise的使用。如果你完全不了解Promise,可以先在這里了解基本的Promise概念:Promise(MDN)和JavaScript Promise:簡介。
3.2. Service Worker的生命周期當我們注冊了Service Worker后,它會經歷生命周期的各個階段,同時會觸發相應的事件。整個生命周期包括了:installing --> installed --> activating --> activated --> redundant。當Service Worker安裝(installed)完畢后,會觸發install事件;而激活(activated)后,則會觸發activate事件。
下面的例子監聽了install事件:
// 監聽install事件 self.addEventListener("install", function (e) { console.log("Service Worker 狀態: install"); });
self是Service Worker中一個特殊的全局變量,類似于我們最常見的window對象。self引用了當前這個Service Worker。
3.3. 緩存靜態資源通過上一節,我們已經學會了如何添加事件監聽,來在合適的時機觸發Service Worker的相應操作?,F在,要使我們的Web App離線可用,就需要將所需資源緩存下來。我們需要一個資源列表,當Service Worker被激活時,會將該列表內的資源緩存進cache。
// sw.js var cacheName = "bs-0-2-0"; var cacheFiles = [ "/", "./index.html", "./index.js", "./style.css", "./img/book.png", "./img/loading.svg" ]; // 監聽install事件,安裝完成后,進行文件緩存 self.addEventListener("install", function (e) { console.log("Service Worker 狀態: install"); var cacheOpenPromise = caches.open(cacheName).then(function (cache) { return cache.addAll(cacheFiles); }); e.waitUntil(cacheOpenPromise); });
可以看到,首先在cacheFiles中我們列出了所有的靜態資源依賴。注意其中的"/",由于根路徑也可以訪問我們的應用,因此不要忘了將其也緩存下來。當Service Worker install時,我們就會通過caches.open()與cache.addAll()方法將資源緩存起來。這里我們給緩存起了一個cacheName,這個值會成為這些緩存的key。
上面這段代碼中,caches是一個全局變量,通過它我們可以操作Cache相關接口。
Cache 接口提供緩存的 Request / Response 對象對的存儲機制。Cache 接口像 workers 一樣, 是暴露在 window 作用域下的。盡管它被定義在 service worker 的標準中, 但是它不必一定要配合 service worker 使用。——MDN3.4 使用緩存的靜態資源
到目前為止,我們僅僅是注冊了一個Service Worker,并在其install時緩存了一些靜態資源。然而,如果這時運行這個demo你會發現——“圖書搜索”這個Web App依然無法離線使用。
為什么呢?因為我們僅僅緩存了這些資源,然而瀏覽器并不知道需要如何使用它們;換言之,瀏覽器仍然會通過向服務器發送請求來等待并使用這些資源。那怎么辦?
聰明的你應該想起來了,我們在文章前半部分介紹Service Worker時提到了“客戶端代理”——用Service Worker來幫我們決定如何使用緩存。
下圖是一個簡單的策略:
瀏覽器發起請求,請求各類靜態資源(html/js/css/img);
Service Worker攔截瀏覽器請求,并查詢當前cache;
若存在cache則直接返回,結束;
若不存在cache,則通過fetch方法向服務端發起請求,并返回請求結果給瀏覽器
// sw.js self.addEventListener("fetch", function (e) { // 如果有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); }) ); });
fetch事件會監聽所有瀏覽器的請求。e.respondWith()方法接受Promise作為參數,通過它讓Service Worker向瀏覽器返回數據。caches.match(e.request)則可以查看當前的請求是否有一份本地緩存:如果有緩存,則直接向瀏覽器返回cache;否則Service Worker會向后端服務發起一個fetch(e.request)的請求,并將請求結果返回給瀏覽器。
到目前為止,運行我們的demo:當第一聯網打開“圖書搜索”Web App后,所依賴的靜態資源就會被緩存在本地;以后再訪問時,就會使用這些緩存而不發起網絡請求。因此,即使在無網情況下,我們似乎依舊能“訪問”該應用。
3.5. 更新靜態緩存資源然而,如果你細心的話,會發現一個小問題:當我們將資源緩存后,除非注銷(unregister)sw.js、手動清除緩存,否則新的靜態資源將無法緩存。
解決這個問題的一個簡單方法就是修改cacheName。由于瀏覽器判斷sw.js是否更新是通過字節方式,因此修改cacheName會重新觸發install并緩存資源。此外,在activate事件中,我們需要檢查cacheName是否變化,如果變化則表示有了新的緩存資源,原有緩存需要刪除。
// sw.js // 監聽activate事件,激活后通過cache的key來判斷是否更新cache中的靜態資源 self.addEventListener("activate", function (e) { console.log("Service Worker 狀態: activate"); var 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(); });3.6. 緩存API數據的“離線搜索”
到這里,我們的應用基本已經完成了離線訪問的改造。但是,如果你注意到文章開頭的圖片就會發現,離線時我們不僅可以訪問,還可以使用搜索功能。
這是怎么回事呢?其實這背后的秘密就在于,這個Web App也會把XHR請求的數據緩存一份。而再次請求時,我們會優先使用本地緩存(如果有緩存的話);然后向服務端請求數據,服務端返回數據后,基于該數據替換展示。大致過程如下:
首先我們改造一下前一節的代碼在sw.js的fetch事件里進行API數據的緩存
// sw.js var apiCacheName = "api-0-1-1"; self.addEventListener("fetch", function (e) { // 需要緩存的xhr請求 var cacheRequestUrls = [ "/book?" ]; console.log("現在正在請求:" + e.request.url); // 判斷當前請求是否需要緩存 var needCache = cacheRequestUrls.some(function (url) { return e.request.url.indexOf(url) > -1; }); /**** 這里是對XHR數據緩存的相關操作 ****/ if (needCache) { // 需要緩存 // 使用fetch請求數據,并將請求結果clone一份緩存到cache // 此部分緩存后在browser中使用全局變量caches獲取 caches.open(apiCacheName).then(function (cache) { return fetch(e.request).then(function (response) { cache.put(e.request.url, response.clone()); return response; }); }); } /* ******************************* */ 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緩存的數據創建一個專門的緩存位置,key值為變量apiCacheName。在fetch事件中,我們首先通過對比當前請求與cacheRequestUrls來判斷是否是需要緩存的XHR請求數據,如果是的話,就會使用fetch方法向后端發起請求。
在fetch.then中我們以請求的URL為key,向cache中更新了一份當前請求所返回數據的緩存:cache.put(e.request.url, response.clone())。這里使用.clone()方法拷貝一份響應數據,這樣我們就可以對響應緩存進行各類操作而不用擔心原響應信息被修改了。
3.7. 應用離線XHR數據,完成“離線搜索”,提升響應速度如果你跟著做到了這一步,那么恭喜你,距離我們酷酷的離線應用還差最后一步了!
目前為止,我們對Service Worker(sw.js)的改造已經完畢了。最后只剩下如何在XHR請求時有策略的使用緩存了,這一部分的改造全部集中于index.js,也就是我們的前端腳本。
還是回到上一節的這張圖:
和普通情況不同,這里我們的前端瀏覽器會首先去嘗試獲取緩存數據并使用其來渲染界面;同時,瀏覽器也會發起一個XHR請求,Service Worker通過將請求返回的數據更新到存儲中的同時向前端Web應用返回數據(這一步分就是上一節提到的緩存策略);最終,如果判斷返回的數據與最開始取到的cache不一致,則重新渲染界面,否則忽略。
為了是代碼更清晰,我們將原本的XHR請求部分多帶帶剝離出來,作為一個方法getApiDataRemote()以供調用,同時將其改造為了Promise。為了節省篇幅,我部分的代碼比較簡單,就不多帶帶貼出了。
這一節最重要的部分其實是讀取緩存。我們知道,在Service Worker中是可以通過caches變量來訪問到緩存對象的。令人高興的是,在我們的前端應用中,也仍然可以通過caches來訪問緩存。當然,為了保證漸進可用,我們需要先進行判斷"caches" in window。為了代碼的統一,我將獲取該請求的緩存數據也封裝成了一個Promise方法:
function getApiDataFromCache(url) { if ("caches" in window) { return caches.match(url).then(function (cache) { if (!cache) { return; } return cache.json(); }); } else { return Promise.resolve(); } }
而原本我們在queryBook()方法中,我們會請求后端數據,然后渲染頁面;而現在,我們加上基于緩存的渲染:
function queryBook() { // …… // 遠程請求 var remotePromise = getApiDataRemote(url); var cacheData; // 首先使用緩存數據渲染 getApiDataFromCache(url).then(function (data) { if (data) { loading(false); input.blur(); fillList(data.books); 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.books); document.querySelector("#js-thanks").style = "display: block"; } }); // …… }
如果getApiDataFromCache(url).then返回緩存數據,則使用它先進行渲染。而當remotePromise的數據返回時,與cacheData進行比對,只有在數據不一致時需要重新渲染頁面(注意這里為了簡便,粗略地使用了JSON.stringify()方法進行對象間的比較)。這么做有兩個優勢:
離線可用。如果我們之前訪問過某些URL,那么即使在離線的情況下,重復相應的操作依然可以正常展示頁面;
優化體驗,提高訪問速度。讀取本地cache耗時相比于網絡請求是非常低的,因此就會給我們的用戶一種“秒開”、“秒響應”的感覺。
4. 使用Lighthouse測試我們的應用至此,我們完成了PWA的兩大基本功能:Web App Manifest和Service Worker的離線緩存。這兩大功能可以很好地提升用戶體驗與應用性能。我們用Chrome中的Lighthouse來檢測一下目前的應用:
可以看到,在PWA評分上,我們的這個Web App已經非常不錯了。其中唯一個扣分項是在HTTPS協議上:由于是本地調試,所以使用了http://127.0.0.1:8085,在生產肯定會替換為HTTPS。
5. 這太酷了,但是兼容性呢?隨著今年(2018年)年初,Apple在iOS 11.3中開始支持Service Worker,加上Apple一直以來較為良好的系統升級率,整個PWA在兼容性問題上有了重大的突破。
雖然Service Worker中的一些其他功能(例如推送、后臺同步)Apple并未表態,但是Web App Manifest和Service Worker的離線緩存是iOS 11.3所支持的。這兩大核心功能不僅效果拔群,而且目前看來具有還不錯的兼容性,非常適合投入生產。
更何況,作為漸進式網頁應用,其最重要的一個特點就是在兼容性支持時自動升級功能與體驗;而在不支持時,會靜默回退部分新功能。在保證我們的正常服務情況下,盡可能利用瀏覽器特性,提供更優質的服務。
6. 寫在最后本文中所有的代碼示例均可以在learn-pwa/sw-cache上找到。注意在git clone之后,切換到sw-cache分支,本文所有的代碼均存在于該分支上。切換其他分值可以看到不同的版本:
basic分支:基礎項目demo,一個普通的圖書搜索應用(網站);
manifest分支:基于basic分支,添加manifest等功能,具體可以看上一篇文章了解;
sw-cache分支:基于manifest分支,添加緩存與離線功能;
master分支:應用的最新代碼。
如果你喜歡或想要了解更多的PWA相關知識,歡迎關注我,關注《PWA學習與實踐》系列文章。我會總結整理自己學習PWA過程的遇到的疑問與技術點,并通過實際代碼和大家一起實踐。
最后聲明一下,文中的代碼作為demo,主要是用于了解與學習PWA技術原理,可能會存在一些不完善的地方,因此,不建議直接使用到生產環境。《PWA技術學習與實踐》系列
第一篇:開始你的PWA學習之旅
第二篇:10分鐘學會使用Manifest,讓你的WebApp更“Native”
第三篇:從今天起,讓你的WebApp離線可用(本文)
第四篇:TroubleShooting: 解決FireBase login驗證失敗問題
第五篇:與你的用戶保持聯系: Web Push功能
第六篇:How to Debug? 在chrome中調試你的PWA
第七篇:增強交互:使用Notification API來進行提醒
第八篇:使用Service Worker進行后臺數據同步
第九篇:PWA實踐中的問題與解決方案
第十篇:Resource Hint - 提升頁面加載性能與體驗
第十一篇:從PWA離線工具集workbox中學習各類離線策略(寫作中…)
參考資料Using Service Workers(MDN)
Cache(MDN)
Service Worker使用方式
JavaScript Promise:簡介
Promise(MDN)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/98996.html
摘要:本文是學習與實踐系列的第五篇文章。實際上,消息推送與提醒是兩個功能和。在這一篇里,我們先來學習如何使用進行消息推送。而當服務端要推送消息時,會使用私鑰對發送的數據進行數字簽名,并根據數字簽名生成一個叫請求頭。 《PWA學習與實踐》系列文章已整理至gitbook - PWA學習手冊,文字內容已同步至learning-pwa-ebook。轉載請注明作者與出處。 本文是《PWA學習與實踐》系...
摘要:,不過在上會導致狀態欄不顯示任何東西。下面是項目中的相關設置圖書搜索中的處理方式與類似,中也有自己的標簽來指示相應的資源。 《PWA學習與實踐》系列文章已整理至gitbook - PWA學習手冊,文字內容已同步至learning-pwa-ebook。轉載請注明作者與出處。 本文是《PWA學習與實踐》系列的第二篇文章。文中的代碼都可以在learning-pwa的manifest分支上找到...
摘要:學習與實踐系列文章已整理至學習手冊,文字內容已同步至。本系列文章學習與實踐會逐步拆解背后的各項技術,通過實例代碼來講解這些技術的應用方式。而隨著在中也開始支持其中的某些技術,的舞臺更大了。這個最開始是不具備任何的能力。 《PWA學習與實踐》系列文章已整理至gitbook - PWA學習手冊,文字內容已同步至learning-pwa-ebook。轉載請注明作者與出處。 PWA作為今年最火...
閱讀 3547·2021-08-31 09:39
閱讀 1853·2019-08-30 13:14
閱讀 2917·2019-08-30 13:02
閱讀 2768·2019-08-29 13:22
閱讀 2340·2019-08-26 13:54
閱讀 766·2019-08-26 13:45
閱讀 1585·2019-08-26 11:00
閱讀 981·2019-08-26 10:58