上一篇文章中,咱們成功嘗試使用 service workers。咱們也能夠在應用中緩存一些資源。這篇文章咱們準備瞭解這些:service workers 以及緩存是如何一塊兒配合給用戶一個完美的離線體驗。javascript
在前一個章節當咱們學習如何 debugger 的時候,咱們瞭解到瀏覽器的緩存存儲。說起緩存時,不只僅是指存儲,還包括瀏覽器內用來保存數據以供離線使用的策略。css
在這篇文章中,咱們將要:html
軟件工程中的每個理論都是對同一類問題解決方案的總結,每個都須要時間整理並被大衆接受,成爲推薦的解決方案。對於 PWA 的緩存策略來講一樣如此。Jake Archibald 彙總了不少經常使用的方案,但咱們只打算介紹其中一些經常使用的:java
這個方案咱們在上一篇文章中介紹過,緩存 app shell 展現時須要的全部資源:git
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,少許的圖片。github
這個方案是指若是以前的網絡請求數據被緩存了,那麼就用緩存的數據更新頁面。若是緩存不可用,那直接去網絡請求數據。當請求成功返回時,利用返回的數據更新頁面並緩存返回的數據。web
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; }); }); }) ); });
這種方案主要應用用戶頻繁手動更新內容的場景,好比用戶的收件箱或者文章內容。shell
這種方案將同時請求緩存以及服務端的數據。若是某一項在緩存中有對應的數據,好,直接在頁面中展現。當網絡請求的數據返回時,利用返回的數據更新頁面:npm
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 });
在大多數狀況下,網絡請求返回的數據會將從緩存中取出的數據覆蓋。但在網頁中,什麼狀況都有可能發生,有時候網絡請求數據比從緩存中取數據要快。所以,咱們須要設置一個 flag 來判斷網絡請求有沒有返回,這就是上例中的 networkReturned。json
目前有兩種可持續性數據存儲方案 -- Cache Storage 以及 Index DB(IDB)。
Service Worker 對於這兩種存儲方案都提供支持。那麼問題來了,什麼場景下選擇哪種技術方案呢? Addy Osmani 的博客已經總結好了。
對於利用 URL 可直接查看的資源,使用支持 Service Worker 的 Cache Storage。其它類型的資源,使用利用 Promise 包裹以後的 IndexedDB。
上文已經介紹了緩存策略以及數據緩存數據。在實戰以前,還想給你們介紹一下谷歌的 SW Precache。
這個工具還有一個額外的功能:將咱們以前討論的緩存文件設置利用正則簡化成一個配置對象。全部你須要作的就是在一個數組中定義緩存的項目。
讓咱們來嘗試使用一下 precache,讓其自動生成 service-worker.js
。首先,咱們須要在項目的根目錄下新增一個 package.json
文件:
npm init -y
安裝 sw-precache:
npm install --save-dev sw-precache
建立一個配置文件:
// ./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
裏面利用正則匹配咱們想要緩存的文件。只須要利用正則,比以前枚舉全部的文件簡單不少。
在 package.json
中新增一個 script 用來生成 service worker 文件:
"scripts": { "sw": "sw-precache --config=tools/precache.js --verbose" },
運行下面的命令便可生成 service worker 文件:
npm run sw
查看生成的文件,是否是很熟悉?
在作 web 應用離線功能以前,讓咱們先來完成應用的基本功能。
回到 app.js
文件,咱們要在頁面加載完成時去獲取當前 Github 流行的項目(項目以 star 數的多少來排序):
(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 字符串中的日期。咱們是這樣構造的:
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
幫咱們將日期構形成 Github API 所規定的格式(yyyy-mm-dd
)。
當 getTrends
獲取數據以後,調用了 updateTrends
方法,傳入獲取到的數據。讓咱們看看這個方法作了些什麼:
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)); } }
遍歷請求返回的數據,利用 createCard
來建立 DOM 模板,而後,將這段 DOM 插入 .trends
元素:
<!-- ./index.html --> <div class="row trends"> <!-- append here --> </div>
createCard
利用下面的代碼來建立模板:
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; }
下面就是所建立的 DOM 結構:
<div class="row trends"> <divclass="col s12 m4 card-template"> <div class="card horizontal"> <div class="card-stacked"> <div class="card-content white-text"> <span class="card-title">Card Title</span> <div class="card-sub grey-text text-lighten-2"> <i class="material-icons">info</i><span class="card-lang"> JavaScript</span> <i class="material-icons">star</i><span class="card-stars"> 299</span> <i class="material-icons">assessment</i><span class="card-forks"> 100</span> </div> <p>A set of best practices for JavaScript projects</p> </div> <div class="card-action"> <a href="#" class="card-link">Visit Repo</a> </div> </div> </div> </div> </div>
在應用程序運行時,須要緩存從服務端獲取的動態內容。再也不是 app shell 了,而是用戶真正瀏覽的內容。
咱們須要提早配置告訴 service worker ,在運行時須要緩存的文件:
// ./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 } } }] };
咱們定義了一個 url 正則匹配符,匹配成功時,讀取緩存。這個正則匹配全部的 Github 搜索 API。咱們打算應用「Cache, Then network.」的策略。
這樣,咱們先展現緩存的內容,當有網絡鏈接時候,更新內容:
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
當你運行應用的時候,嘗試刷新,打開控制檯,勾選 offline 選項。以後,刷新,以及見證奇蹟的時刻:
用戶可能須要在網絡狀況更佳的時候刷新頁面,咱們須要給予用戶這樣的權利。咱們能夠給刷新按鈕添加一個事件,當時間觸發時,調用 getTrends
方法:
document.addEventListener('DOMContentLoaded', function() { app.getTrends() // Event listener for refresh button const refreshButton = document.querySelector('.refresh'); refreshButton.addEventListener('click', app.getTrends) })
感受不是很知足?如今你已經知道了如何建立離線應用,在接下來的文章中,咱們將繼續討論這項技術的有趣之處,包括推送通知,主屏幕圖標建立等等···