原文地址: Build an offline-first, data-driven PWA
譯文出自: 個人我的博客
在本文中,您將學習如何使用 Workbox 和 IndexedDB 建立離線優先、數據驅動的漸進式Web應用程序(PWA)。在離線的狀況下也可使用後臺同步功能將應用程序與服務器同步。css
若是你沒有安裝 Nodejs 須要安裝一下html
以後經過下面的方式 clone 快速啓動倉庫node
git clone https://github.com/googlecodelabs/workbox-indexeddb.git
或者直接下載 壓縮包git
到下載好的 git 倉庫目錄中,轉到 project
文件夾github
cd workbox-indexeddb/project/
而後安裝依賴並啓動服務web
npm install npm start
這個步驟中會根據 package.json
定義的依賴並安裝,打開 package.json
文件查看,有不少依賴,大部分是開發環境須要的(你能夠忽略),主要的依賴是:chrome
npm start
會構建並輸出到 build
文件夾,啓動 dev server,而且會開啓一個 gulp watch
任務。gulp watch
會監聽文件的修改自動構建。concurrently
能夠同時跑 gulp
和 dev servershell
打開 Chrome 而且跳轉到 localhost:8081
你會看到一個事件列表的控制檯,在彈出的權限確認菜單中點擊容許數據庫
咱們使用通知系統來告知用戶 app 的後臺同步已經更新,試着測試一下頁面底部的添加功能npm
這個小項目的目標是離線保存用戶的事件日曆。你能夠查看一下 app/js/main.js
文件的 loadContentNetworkFirst
方法當前是怎麼工做的,首先會請求 server,成功則更新頁面,失敗會在控制檯打印一個信息,目前脫機是沒法使用的,接下來咱們添加一些方法使它脫機可用。
要想脫機工做,就須要 server worker,如今寫一個。
把下面的代碼添加到 app/src/sw.js
importScripts('workbox-sw.dev.v2.0.0.js'); importScripts('workbox-background-sync.dev.v2.0.0.js'); const workboxSW = new WorkboxSW(); workboxSW.precache([]);
在開頭咱們引入了 workbox-sw
和 workbox-background-sync
workbox-sw
包含了 precache
和向 service worker 添加路由的方法workbox-background-sync
是在 service worker 中後臺同步的庫,稍後會提到precache
方法接收一個文件列表的數組,先用一個空的,下一步咱們會用 workbox-build
去計算出這個數組的結果。
推薦使用 Workbox 的構建模塊,好比 workbox-build
把下面的代碼添加進 project/gulpfile.js
gulp.task('build-sw', () => { return wbBuild.injectManifest({ swSrc: 'app/src/sw.js', swDest: 'build/service-worker.js', globDirectory: 'build', staticFileGlobs: [ 'style/main.css', 'index.html', 'js/idb-promised.js', 'js/main.js', 'images/**/*.*', 'manifest.json' ], templatedUrls: { '/': ['index.html'] } }).catch((err) => { console.log('[ERROR] This happened: ' + err); }); });
如今取消一些註釋:
gulpfile.js:
// uncomment the line below: const wbBuild = require('workbox-build'); // ... gulp.task('default', ['clean'], cb => { runSequence( 'copy', // uncomment the line below: 'build-sw', cb ); });
保存修改,由於修改了 gulp,咱們得從新跑一下,Ctrl + C
退出當前的進程,從新運行 npm start
,會看到 service worker 的文件被生成在了 build/service-worker.js
取消 app/index.html
中 service worker 註冊代碼的註釋
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js') .then(function(registration) { console.log('Service Worker registration successful with scope: ', registration.scope); }) .catch(function(err) { console.log('Service Worker registration failed: ', err); }); }
保存修改,刷新瀏覽器 service worker 就會被安裝。Ctrl + C
關閉 dev server,再返回到瀏覽器中刷新頁面,已經能夠脫機運行了!
在這一步中,workbox-build
和 build-sw
任務被合併到咱們的 gulp 文件中,咱們的構建過程是使用 workbox-build
庫來從 swSrc(app/src/sw.js)
中生成 service work 到 swDest(build/service-worker.js)
,來自 globDirectory(build)
的 staticFileGlobs
文件被注入到 build/service-worker.js
以供 precache
調用,還有每一個文件的修訂哈希。templatedUrls 選項告訴 Workbox 咱們的站點以 index.html 的內容響應請求。
順便貼一個 injectManifest 的連接
安裝生成好的 service worker 緩存 app shell 的資源文件,Workbox 會自動去:
目前爲止還不能離線加載數據,咱們接下來建立一個 IndexDB 來保存程序的數據,數據庫命名爲 dashboardr
添加下面代碼到 app/js/main.js
function createIndexedDB() { if (!('indexedDB' in window)) {return null;} return idb.open('dashboardr', 1, function(upgradeDb) { if (!upgradeDb.objectStoreNames.contains('events')) { const eventsOS = upgradeDb.createObjectStore('events', {keyPath: 'id'}); } }) }
取消調用 createIndexedDB
的註釋:
const dbPromise = createIndexedDB();
保存文件,重啓 server:
npm start
回到瀏覽器刷新頁面,激活 skipWaiting 並再次刷新頁面,在 Chrome 中,你能夠在開發者工具中的 Application
面板中選擇 Service Workers
點擊 skipWaiting
,以後使用 開發者工具 檢查數據庫是否存在。在 Chrome 中你能夠在 Application
面板中點擊 IndexedDB
選擇 dashboardr
查看 events
對象是否存在。
注意:開發者工具的 IndexedDB UI 可能不會準確的反應你數據庫的狀況,在 Chrome 中你能夠刷新數據庫查看,或者從新打開開發者工具
在上面的代碼中,咱們建立了一個 dashboardr 數據庫,並把他的版本號設置爲 1
,而後檢查 events 對象是否存在,這個檢查是爲了不潛在的錯誤,咱們還給 event 提供了一個惟一的 key path id
。
因爲咱們修改了 app/main.js
文件,gulp 的 watch
任務會自動構建,Workbox 會自動更新修訂哈希,而後智能更新緩存中的 main.js
。
如今咱們保存數據到剛建立的數據庫 dashboardr
中的 event
對象中。
function saveEventDataLocally(events) { if (!('indexedDB' in window)) {return null;} return dbPromise.then(db => { const tx = db.transaction('events', 'readwrite'); const store = tx.objectStore('events'); return Promise.all(events.map(event => store.put(event))) .catch(() => { tx.abort(); throw Error('Events were not added to the store'); }); }); }
而後更新 loadContentNetworkFirst
方法,如今這是完整的方法:
function loadContentNetworkFirst() { getServerData() .then(dataFromNetwork => { updateUI(dataFromNetwork); saveEventDataLocally(dataFromNetwork) .then(() => { setLastUpdated(new Date()); messageDataSaved(); }).catch(err => { messageSaveError(); console.warn(err); }); }).catch(err => { // if we can't connect to the server... console.log('Network requests have failed, this is expected if offline'); }); }
取消註釋 addAndPostEvent
中的 saveEventDataLocally
調用
function addAndPostEvent() { // ... saveEventDataLocally([data]); // ... }
保存文件,刷新頁面從新激活 service worker。再次刷新頁面,檢查一下來自網絡的數據是否被保存到 events
中去(你可能須要刷新一下開發者工具中的 IndexedDB
)
saveEventDataLocally
接收一個數組並一條條的保存到 IndexedDB 數據庫中,咱們把 store.put
寫在了 Promise.all
中,這樣若是某一條更新出錯咱們就能夠終止事務。
loadContentNetworkFirst
方法中,一旦收到來自服務器的數據,就會更新 IndexedDB 和頁面。而後,數據成功保存時,將存儲時間戳,並通知用戶數據可供離線使用。
在addAndPostEvent
中調用 saveEventDataLocally
方法保證了添加新的 event
時本地會存有最新的數據。
離線的時候,咱們就要查詢本地緩存的數據。
添加下面的代碼到 app/js/main.js
中:
function getLocalEventData() { if (!('indexedDB' in window)) {return null;} return dbPromise.then(db => { const tx = db.transaction('events', 'readonly'); const store = tx.objectStore('events'); return store.getAll(); }); }
而後更新 loadContentNetworkFirst
方法,完整的方法以下:
function loadContentNetworkFirst() { getServerData() .then(dataFromNetwork => { updateUI(dataFromNetwork); saveEventDataLocally(dataFromNetwork) .then(() => { setLastUpdated(new Date()); messageDataSaved(); }).catch(err => { messageSaveError(); console.warn(err); }); }).catch(err => { console.log('Network requests have failed, this is expected if offline'); getLocalEventData() .then(offlineData => { if (!offlineData.length) { messageNoData(); } else { messageOffline(); updateUI(offlineData); } }); }); }
保存文件,刷新瀏覽器激活更新的 service worker,如今 Ctrl + C
關閉 dev server,返回到瀏覽器中刷新頁面,如今 app 和數據均可以離線加載了!
loadContentNetworkFirst
被調用的時候若是沒有網絡鏈接,getServerData
會被 reject,以後便會進入到 catch
中去,而後 getLocalEventData
會調用本地緩存的數據。有網絡鏈接的話會正常的請求 server 而且 updateUI
咱們的 app 已經能夠離線保存和瀏覽數據,如今咱們來用 workbox-background-sync
把離線狀態下保存的數據同步到服務端去。
把下面的的代碼添加到 app/src/sw.js
let bgQueue = new workbox.backgroundSync.QueuePlugin({ callbacks: { replayDidSucceed: async(hash, res) => { self.registration.showNotification('Background sync demo', { body: 'Events have been updated!' }); } } }); workboxSW.router.registerRoute('/api/add', workboxSW.strategies.networkOnly({plugins: [bgQueue]}), 'POST' );
保存,如今轉到命令行:
npm run start
刷新瀏覽器,激活更新的 service worker
Ctrl + C
把 app 變爲離線狀態,添加一個 event
確認請求 /api/add
已經被添加進 bgQueueSyncDB
的 QueueStore
對象。
當用戶試圖在離線狀況下添加 event
的時候,workbox-background-sync
會把失敗的請求保存爲一個離線隊列,當用戶從新聯網 backgroundSync
會從新發送這些請求,甚至都不須要用戶打開 app!可是,從聯網到從新發請求的這個過程大概須要 5 分鐘,下一節咱們將會介紹如何在 app 中當即發送這些請求。
由於重發請求會有延遲,因此用戶可能回到 app 以後尚未同步數據,因此咱們在用戶聯網的時候當即發送這些請求。
把下面的代碼添加到 app/src/sw.js
workboxSW.router.registerRoute('/api/getAll', () => { return bgQueue.replayRequests().then(() => { return fetch('/api/getAll'); }).catch(err => { return err; }); });
只要用戶請求服務端數據(加載或刷新頁面時),該路由就會 replay 排隊的請求,而後返回最新的服務端數據。這很好,可是用戶仍是得刷新頁面去從新獲取數據,咱們還有更好的作法。
把下面的代碼添加進 app/js/main.js
window.addEventListener('online', () => { container.innerHTML = ''; loadContentNetworkFirst(); });
重啓 server
npm start
刷新瀏覽器激活新的 service worker,並再次刷新頁面。
Ctrl + C
把 app 變爲離線狀態
添加一條 event
重啓 server
npm start
這時你應該能當即收到一條數據更新的通知,檢查 server-data/events.json
中的數據是否已經更新。
頁面加載的時候會請求 /api/getAll
,咱們攔截了這個請求,以後主要作了兩件事:
/api/getAll
也就是在從新獲取服務端的數據以前先同步
注意:本例中的網絡請求設計的很是簡單,實際狀況下你可能須要考慮更多因素去減小請求的數量。
下面的時間就交給你了,添加一個刪除的功能,記得刪除 IndexedDB 中的數據。