原文地址: 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 中的数据。