摘要:提及緩存時(shí),不僅僅是指存儲(chǔ),還包括瀏覽器內(nèi)用來(lái)保存數(shù)據(jù)以供離線使用的策略。當(dāng)請(qǐng)求成功返回時(shí),利用返回的數(shù)據(jù)更新頁(yè)面并緩存返回的數(shù)據(jù)。這種方案主要應(yīng)用用戶頻繁手動(dòng)更新內(nèi)容的場(chǎng)景,比如用戶的收件箱或者文章內(nèi)容。我們打算應(yīng)用的策略。
上一篇文章中,我們成功嘗試使用 service workers。我們也可以在應(yīng)用中緩存一些資源。這篇文章我們準(zhǔn)備了解這些:service workers 以及緩存是如何一起配合給用戶一個(gè)完美的離線體驗(yàn)。
在前一個(gè)章節(jié)當(dāng)我們學(xué)習(xí)如何 debugger 的時(shí)候,我們了解到瀏覽器的緩存存儲(chǔ)。提及緩存時(shí),不僅僅是指存儲(chǔ),還包括瀏覽器內(nèi)用來(lái)保存數(shù)據(jù)以供離線使用的策略。
在這篇文章中,我們將要:
了解社區(qū)中常見的緩存策略
嘗試可用的緩存 api
做一個(gè)用來(lái)展示 Github trending project 的 demo
在 demo 中演示離線狀態(tài)下利用緩存所帶來(lái)的體驗(yàn)
緩存策略軟件工程中的每一個(gè)理論都是對(duì)同一類問題解決方案的總結(jié),每一個(gè)都需要時(shí)間整理并被大眾接受,成為推薦的解決方案。對(duì)于 PWA 的緩存策略來(lái)說同樣如此。Jake Archibald 匯總了很多常用的方案,但我們只打算介紹其中一些常用的:
Install 期間緩存這個(gè)方案我們?cè)谏弦黄恼轮薪榻B過,緩存 app shell 展示時(shí)需要的所有資源:
self.addEventListener("install", function(e) { console.log("[ServiceWorker] Install"); e.waitUntil( caches.open(cacheName).then(function(cache) { console.log("[ServiceWorker] Caching app shell"); return cache.addAll(filesToCache); }) ); });
緩存的資源包括 HTML 模板,CSS 文件,JavaScript,fonts,少量的圖片。
緩存請(qǐng)求返回的數(shù)據(jù)這個(gè)方案是指如果之前的網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)被緩存了,那么就用緩存的數(shù)據(jù)更新頁(yè)面。如果緩存不可用,那直接去網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)。當(dāng)請(qǐng)求成功返回時(shí),利用返回的數(shù)據(jù)更新頁(yè)面并緩存返回的數(shù)據(jù)。
self.addEventListener("fetch", function(event) { event.respondWith( caches.open(cacheName).then(function(cache) { return cache.match(event.request).then(function (response) { return response || fetch(event.request).then(function(response) { cache.put(event.request, response.clone()); return response; }); }); }) ); });
這種方案主要應(yīng)用用戶頻繁手動(dòng)更新內(nèi)容的場(chǎng)景,比如用戶的收件箱或者文章內(nèi)容。
先展示緩存,再根據(jù)請(qǐng)求的數(shù)據(jù)更新頁(yè)面這種方案將同時(shí)請(qǐng)求緩存以及服務(wù)端的數(shù)據(jù)。如果某一項(xiàng)在緩存中有對(duì)應(yīng)的數(shù)據(jù),好,直接在頁(yè)面中展示。當(dāng)網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)返回時(shí),利用返回的數(shù)據(jù)更新頁(yè)面:
let networkReturned = false; if ("caches" in window) { caches.match(app.apiURL).then(function(response) { if (response) { response.json().then(function(trends) { console.log("From cache...") if(!networkReturned) { app.updateTrends(trends); } }); } }); } fetch(app.apiURL) .then(response => response.json()) .then(function(trends) { console.log("From server...") networkReturned = true; app.updateTrends(trends.items) }).catch(function(err) { // Error });
在大多數(shù)情況下,網(wǎng)絡(luò)請(qǐng)求返回的數(shù)據(jù)會(huì)將從緩存中取出的數(shù)據(jù)覆蓋。但在網(wǎng)頁(yè)中,什么情況都有可能發(fā)生,有時(shí)候網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)比從緩存中取數(shù)據(jù)要快。因此,我們需要設(shè)置一個(gè) flag 來(lái)判斷網(wǎng)絡(luò)請(qǐng)求有沒有返回,這就是上例中的 networkReturned。
緩存部分技術(shù)選型目前有兩種可持續(xù)性數(shù)據(jù)存儲(chǔ)方案 -- Cache Storage 以及 Index DB(IDB)。
Cache Storage:在過去的一段時(shí)間里,我們依賴 AppCache 來(lái)進(jìn)行緩存處理,但我們需要一個(gè)可操作性更強(qiáng)的 API。幸運(yùn)的是,瀏覽器提供了 Cache 這樣的一個(gè) API,給 Service Worker 的緩存操作帶來(lái)了更多的可能。并且,這個(gè) API 同時(shí)支持 service workers 以及 web 頁(yè)面。在前一篇文章中,我們已經(jīng)使用過了這個(gè) API。
Index DB:Index DB 是一個(gè)異步數(shù)據(jù)存儲(chǔ)方案。對(duì)于這個(gè) API 是又愛又恨,還好,像localForage這樣的類庫(kù)使用類似localStorage的操作方式簡(jiǎn)化了API。
Service Worker 對(duì)于這兩種存儲(chǔ)方案都提供支持。那么問題來(lái)了,什么場(chǎng)景下選擇哪一種技術(shù)方案呢? Addy Osmani 的博客已經(jīng)總結(jié)好了。
對(duì)于利用 URL 可直接查看的資源,使用支持 Service Worker 的 Cache Storage。其它類型的資源,使用利用 Promise 包裹之后的 IndexedDB。SW Precache
上文已經(jīng)介紹了緩存策略以及數(shù)據(jù)緩存數(shù)據(jù)。在實(shí)戰(zhàn)之前,還想給大家介紹一下谷歌的 SW Precache。
這個(gè)工具還有一個(gè)額外的功能:將我們之前討論的緩存文件設(shè)置利用正則簡(jiǎn)化成一個(gè)配置對(duì)象。所有你需要做的就是在一個(gè)數(shù)組中定義緩存的項(xiàng)目。
讓我們來(lái)嘗試使用一下 precache,讓其自動(dòng)生成 service-worker.js。首先,我們需要在項(xiàng)目的根目錄下新增一個(gè) package.json 文件:
npm init -y
安裝 sw-precache:
npm install --save-dev sw-precache
創(chuàng)建一個(gè)配置文件:
// ./tools/precache.js const name = "scotchPWA-v1" module.exports = { staticFileGlobs: [ "./index.html", "./images/*.{png,svg,gif,jpg}", "./fonts/**/*.{woff,woff2}", "./js/*.js", "./css/*.css", "https://fonts.googleapis.com/icon?family=Material+Icons" ], stripPrefix: "." };
staticFileGlobs 里面利用正則匹配我們想要緩存的文件。只需要利用正則,比之前枚舉所有的文件簡(jiǎn)單很多。
在 package.json 中新增一個(gè) script 用來(lái)生成 service worker 文件:
"scripts": { "sw": "sw-precache --config=tools/precache.js --verbose" },
運(yùn)行下面的命令即可生成 service worker 文件:
npm run sw
查看生成的文件,是不是很熟悉?
完成 demo在做 web 應(yīng)用離線功能之前,讓我們先來(lái)完成應(yīng)用的基本功能。
回到 app.js 文件,我們要在頁(yè)面加載完成時(shí)去獲取當(dāng)前 Github 流行的項(xiàng)目(項(xiàng)目以 star 數(shù)的多少來(lái)排序):
(function() { const app = { apiURL: `https://api.github.com/search/repositories?q=created:%22${dates.startDate()}+..+${dates.endDate()}%22%20language:javascript&sort=stars&order=desc` } app.getTrends = function() { fetch(app.apiURL) .then(response => response.json()) .then(function(trends) { console.log("From server...") app.updateTrends(trends.items) }).catch(function(err) { // Error }); } document.addEventListener("DOMContentLoaded", function() { app.getTrends() }) if ("serviceWorker" in navigator) { navigator.serviceWorker .register("/service-worker.js") .then(function() { console.log("Service Worker Registered"); }); } })()
注意 API URL 字符串中的日期。我們是這樣構(gòu)造的:
Date.prototype.yyyymmdd = function() { // getMonth is zero based, // so we increment by 1 let mm = this.getMonth() + 1; let dd = this.getDate(); return [this.getFullYear(), (mm>9 ? "" : "0") + mm, (dd>9 ? "" : "0") + dd ].join("-"); }; const dates = { startDate: function() { const startDate = new Date(); startDate.setDate(startDate.getDate() - 7); return startDate.yyyymmdd(); }, endDate: function() { const endDate = new Date(); return endDate.yyyymmdd(); } }
yyyymmdd 幫我們將日期構(gòu)造成 Github API 所規(guī)定的格式(yyyy-mm-dd)。
當(dāng) getTrends 獲取數(shù)據(jù)之后,調(diào)用了 updateTrends 方法,傳入獲取到的數(shù)據(jù)。讓我們看看這個(gè)方法做了些什么:
app.updateTrends = function(trends) { const trendsRow = document.querySelector(".trends"); for(let i = 0; i < trends.length; i++) { const trend = trends[i]; trendsRow.appendChild(app.createCard(trend)); } }
遍歷請(qǐng)求返回的數(shù)據(jù),利用 createCard 來(lái)創(chuàng)建 DOM 模板,然后,將這段 DOM 插入 .trends 元素:
createCard 利用下面的代碼來(lái)創(chuàng)建模板:
const app = { apiURL: `...`, cardTemplate: document.querySelector(".card-template") } app.createCard = function(trend) { const card = app.cardTemplate.cloneNode(true); card.classList.remove("card-template") card.querySelector(".card-title").textContent = trend.full_name card.querySelector(".card-lang").textContent = trend.language card.querySelector(".card-stars").textContent = trend.stargazers_count card.querySelector(".card-forks").textContent = trend.forks card.querySelector(".card-link").setAttribute("href", trend.html_url) card.querySelector(".card-link").setAttribute("target", "_blank") return card; }
下面就是所創(chuàng)建的 DOM 結(jié)構(gòu):
Card Titleinfo JavaScript star 299 assessment 100A set of best practices for JavaScript projects
在應(yīng)用程序運(yùn)行時(shí),需要緩存從服務(wù)端獲取的動(dòng)態(tài)內(nèi)容。不再是 app shell 了,而是用戶真正瀏覽的內(nèi)容。
我們需要提前配置告訴 service worker ,在運(yùn)行時(shí)需要緩存的文件:
// ./tools/precache.js const name = "scotchPWA-v1" module.exports = { staticFileGlobs: [ // ... ], stripPrefix: ".", // Run time cache runtimeCaching: [{ urlPattern: /https://api.github.com/search/repositories/, handler: "networkFirst", options: { cache: { name: name } } }] };
我們定義了一個(gè) url 正則匹配符,匹配成功時(shí),讀取緩存。這個(gè)正則匹配所有的 Github 搜索 API。我們打算應(yīng)用“Cache, Then network.”的策略。
這樣,我們先展示緩存的內(nèi)容,當(dāng)有網(wǎng)絡(luò)連接時(shí)候,更新內(nèi)容:
app.getTrends = function() { const networkReturned = false; if ("caches" in window) { caches.match(app.apiURL).then(function(response) { if (response) { response.json().then(function(trends) { console.log("From cache...") if(!networkReturned) { app.updateTrends(trends); } }); } }); } fetch(app.apiURL) .then(response => response.json()) .then(function(trends) { console.log("From server...") app.updateTrends(trends.items) networkReturned = true; }).catch(function(err) { // Error }); }
在 precache.js 中更新緩存的版本,重新生成 service worker:
const name = "scotchPWA-v2"
npm run sw
當(dāng)你運(yùn)行應(yīng)用的時(shí)候,嘗試刷新,打開控制臺(tái),勾選 offline 選項(xiàng)。之后,刷新,以及見證奇跡的時(shí)刻:
刷新用戶可能需要在網(wǎng)絡(luò)情況更佳的時(shí)候刷新頁(yè)面,我們需要給予用戶這樣的權(quán)利。我們可以給刷新按鈕添加一個(gè)事件,當(dāng)時(shí)間觸發(fā)時(shí),調(diào)用 getTrends 方法:
document.addEventListener("DOMContentLoaded", function() { app.getTrends() // Event listener for refresh button const refreshButton = document.querySelector(".refresh"); refreshButton.addEventListener("click", app.getTrends) })下一步?
感覺不是很滿足?現(xiàn)在你已經(jīng)知道了如何創(chuàng)建離線應(yīng)用,在接下來(lái)的文章中,我們將繼續(xù)討論這項(xiàng)技術(shù)的有趣之處,包括推送通知,主屏幕圖標(biāo)創(chuàng)建等等···
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/93937.html
摘要:上面提到在安卓完全不需要像這樣大費(fèi)周章的繞彎路,所以安卓可能就不需要這個(gè)自定義的,這樣又會(huì)導(dǎo)致面臨著與安卓差異化嚴(yán)重問題。前言 最早接觸離線包的概念要追溯到16年初,項(xiàng)目迎來(lái)大改版,其中重點(diǎn)項(xiàng)目之一就是離線包方案的制定與實(shí)施。離線包顧名思義就是將H5/CSS/JS和資源文件打包提前下發(fā)到App中,這樣App在加載網(wǎng)頁(yè)的時(shí)候?qū)嶋H上加載的是本地的文件,減少網(wǎng)絡(luò)請(qǐng)求來(lái)提高網(wǎng)頁(yè)的渲染速度,并實(shí)現(xiàn)動(dòng)態(tài)...
摘要:使用離線應(yīng)用構(gòu)建應(yīng)用服務(wù)端服務(wù)器配置創(chuàng)建文件客戶端構(gòu)建,并在標(biāo)簽上添加屬性,屬性值是服務(wù)器上配置的緩存資源列表的文件名配置相關(guān)事件,創(chuàng)建離線文件內(nèi)容將狀態(tài)代碼轉(zhuǎn)化成狀態(tài)離線應(yīng)用創(chuàng)建即使沒有互聯(lián)網(wǎng)連接也可以使用的應(yīng)用程序。 HTML5新增了localstroage和application cache做離線緩存,兩種緩存各有應(yīng)用的場(chǎng)景,今天我們說說application cache這種方案...
摘要:使用離線應(yīng)用構(gòu)建應(yīng)用服務(wù)端服務(wù)器配置創(chuàng)建文件客戶端構(gòu)建,并在標(biāo)簽上添加屬性,屬性值是服務(wù)器上配置的緩存資源列表的文件名配置相關(guān)事件,創(chuàng)建離線文件內(nèi)容將狀態(tài)代碼轉(zhuǎn)化成狀態(tài)離線應(yīng)用創(chuàng)建即使沒有互聯(lián)網(wǎng)連接也可以使用的應(yīng)用程序。 HTML5新增了localstroage和application cache做離線緩存,兩種緩存各有應(yīng)用的場(chǎng)景,今天我們說說application cache這種方案...
摘要:我喜歡移動(dòng),而且也是那些堅(jiān)持使用技術(shù)構(gòu)建移動(dòng)應(yīng)用程序的人之一。我們準(zhǔn)備做這樣的一個(gè)漸進(jìn)式應(yīng)用是典型的旨在提高用戶離線體驗(yàn)的應(yīng)用。當(dāng)我們開始構(gòu)建應(yīng)用時(shí),你就能理解上面的場(chǎng)景了。的作用范圍是針對(duì)相對(duì)路徑的。最佳的做法是在應(yīng)用的入口。 我喜歡移動(dòng)app,而且也是那些堅(jiān)持使用Web技術(shù)構(gòu)建移動(dòng)應(yīng)用程序的人之一。 經(jīng)過技術(shù)的不斷迭代(可能還有一些其它的東西),移動(dòng)體驗(yàn)設(shè)計(jì)愈來(lái)愈平易近人,給予用戶...
閱讀 726·2021-10-14 09:42
閱讀 1981·2021-09-22 15:04
閱讀 1588·2019-08-30 12:44
閱讀 2147·2019-08-29 13:29
閱讀 2742·2019-08-29 12:51
閱讀 558·2019-08-26 18:18
閱讀 709·2019-08-26 13:43
閱讀 2822·2019-08-26 13:38