web workers
脱机Web应用程序变得越来越流行。 脱机支持是如此重要,以至于现在谈论“脱机优先”方法成为了主要考虑因素。 随着渐进式Web应用程序的兴起,它也越来越受欢迎。
在本文中,我们将研究如何通过实现资产缓存,客户端数据存储以及与远程数据存储的同步来向基本联系人列表Web应用添加离线支持。
为什么离线支持?
我们为什么要关心离线支持?
我自己每天在火车上花费一个多小时。 我不想浪费时间,所以我带上笔记本电脑继续工作。 我使用手机网络在线。 连接不可靠,因此我会不时丢失它。 我的用户体验取决于我正在使用的Web应用程序。 只有少数具有良好脱机支持的应用程序表现正常,并且连接丢失是透明的。 有些人的行为很怪异,因此刷新页面时会丢失数据。 大多数都根本不支持脱机,我必须等待稳定的连接才能使用它们。
不可靠的连接不是唯一的用例。 我们还可以讨论您可能离线几个小时的情况,例如在飞机上。
离线支持的另一个重要优势是性能的提高。 确实,浏览器不需要等待服务器加载资产。 数据存储在客户端后也是如此。
因此,我们需要离线:
- 甚至在连接不稳定的情况下(火车中的蜂窝网络)也能够使用应用
- 无需网络连接即可工作(在飞机上)
- 提高性能
渐进式Web应用
Google的渐进式Web应用程序 (PWA)概念是一种旨在提供可提供本机移动应用程序用户体验的Web应用程序的方法。 PWA包含脱机支持,但它还涵盖了更多内容:
- 响应速度–支持各种外形:手机,平板电脑,台式机
- Web App Manifest –在主屏幕上安装应用程序
- App Shell –一种设计模式,其中基本的UI App Shell与之后加载的内容分开
- 推送通知–从服务器获取“即时”更新
阿迪·奥斯曼尼(Addy Osmani)写了一篇有关PWA的精彩介绍性文章 。
在本文中,我们将只关注一个方面:脱机支持。
定义离线支持
让我们澄清一下离线支持需要什么。 我们需要照顾两个方面:
- 应用程序资产–缓存HTML,JS脚本,CSS样式表,图像
- 应用程序数据–在客户端存储数据
应用资产
HTML5中第一个用于缓存脱机资产的解决方案是AppCache 。 这个想法是提供一个应用清单,描述应该在浏览器缓存中存储哪些资源。 因此,下次加载应用程序时,将从浏览器缓存中获取这些资产。
引入了服务工作者来替代AppCache。 他们为离线支持提供了灵活的解决方案。 服务人员可以控制传出的请求,允许脚本拦截它们并返回必要的响应。 缓存逻辑完全在开发人员的肩膀上。 应用程序代码本身可以检查资产是否保存在缓存中,并仅在需要时才向服务器请求。
请务必注意,仅通过HTTPS(本地主机允许使用HTTP)连接支持Service Worker。 我们将很快讨论如何使用服务工作者。
应用程序数据
应用程序数据可以存储在浏览器提供的离线存储中。
HTML5引入了几个选项:
- WebStorage –键值存储
- IndexedDB – NoSQL数据库
- WebSQL –内置SQLite数据库
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
WebStorage是键值存储。 这是最简单的跨浏览器存储,但是有几个陷阱需要注意。 您必须注意放入其中的数据的序列化和反序列化,因为这些值必须是纯字符串。 您可能会遇到较大数据集的大小限制 。 同样,有可能进入竞争状态,这意味着,如果在浏览器中同时打开两个选项卡,则可能会导致意外行为。
IndexedDB功能更强大,并且似乎是脱机存储的最佳方式。 它有足够的可用空间 。 它支持交易,并且可以同时在多个浏览器选项卡中安全使用。 所有现代浏览器也都支持它。
WebSQL实际上是浏览器中SQLite。 客户端上具有ACID的全功能关系数据库。 不幸的是,标准委员会已弃用WebSQL,并且在非Blink / Webkit浏览器中从未支持过。
有几个库可以提供脱机存储的抽象:
- localForage –简单的类似于localStorage的API
- IDBWrapper –跨浏览器的IndexedDB包装器
- PouchDB –受CouchDB启发的客户端存储解决方案。 如果正在使用CouchDB,它支持与后端自动同步。
ContactBook应用程序
现在,让我们看看如何向Web应用程序添加脱机支持。 我们的示例应用是基本的联系方式:
我们在左侧有联系人列表,在右侧有一个用于编辑联系人的详细信息表单。 联系人包含三个字段:名字,姓氏和电话。
您可以在GitHub上找到应用程序源代码 。 要运行该应用程序,您需要安装Node.js。 如果您不确定此步骤,可以按照npm的初学者指南进行操作 。
首先下载源代码,然后从项目文件夹中运行以下命令:
$ npm install
$ npm run serve
后端呢? 我们正在使用pouchdb-server在CouchDB存储上提供REST API,并在http-server上提供前端资产。
我们的package.json
scripts
部分如下所示:
"scripts": {
"serve": "npm-run-all -p serve-front serve-backend",
"serve-front": "http-server -o",
"serve-backend": "pouchdb-server -d db"
},
软件包npm-run-all
允许并行运行多个命令。 我们启动两个服务器: http-server
和pouchdb-server
。
现在,让我们看看对应用程序资产的脱机支持的实现。
离线资产
目录/ public包含应用程序的所有资产:
- /css/style.css –应用程序样式表
- / js / ext –包含外部库的目录(PouchDB和Babel使用ES2015语法)
- /js/app.js –主应用程序脚本
- /js/register-service-worker.js –注册服务工作者的脚本
- /js/store.js –用于PouchDB存储的适配器类
- /contactbook.appcache – AppCache宣言
- /index.html –应用程序标记
- /service-worker.js –服务工作者的来源
旅程始于服务人员的注册。 这是register-service-worker.js
的注册代码:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js', {
scope: '/'
}).then(function() {
// success
}).catch(function(e) {
// failed
});
}
首先,我们检查浏览器是否支持serviceWorker
。 如果是,我们将调用register
方法,提供服务工作者脚本的URL(在我们的示例中为/service-worker.js
),并提供其他参数来指定服务工作者的范围。 参数是可选的,并且根/
是scope
默认值。
重要提示 :为了能够将应用程序的根用作范围,服务工作者脚本应位于应用程序的根目录中。
register
方法返回Promise
。
服务人员的生命周期始于安装。 我们可以处理install
事件,并将所有必需的资源放入缓存中:
var CACHE_NAME = 'contact-book-v1';
var resourcesToCache = [
'/',
'/css/style.css',
'/js/ext/babel.min.js',
'/js/ext/pouchdb.min.js',
'/js/register-service-worker.js',
'/js/store.js',
'/js/app.js'
];
self.addEventListener('install', function(event) {
event.waitUntil(
// open the app browser cache
caches.open(CACHE_NAME)
.then(function(cache) {
// add all app assets to the cache
return cache.addAll(resourcesToCache);
})
);
});
最后一件事是处理每次从Service Worker范围中获取资源时触发的fetch
事件:
self.addEventListener('fetch', function(event) {
event.respondWith(
// try to find corresponding response in the cache
caches.match(event.request)
.then(function(response) {
if (response) {
// cache hit: return cached result
return response;
}
// not found: fetch resource from the server
return fetch(event.request);
})
);
});
而已。 让我们测试一下它是否正常工作:
- 使用
npm run serve
运行应用 - 在Chrome中打开URL http://127.0.0.1:8080/
- 在控制台中使用
Ctrl + C
停止网络服务器(或使用Chrome开发工具模拟脱机 ) - 刷新网页
该应用程序仍然可用。 太棒了!
应用缓存
上面的解决方案的问题在于Service Workers对浏览器的支持有限 。 我们可以使用广泛支持的 AppCache实施后备解决方案。 在此处阅读有关AppCache使用的更多信息。
基本用法很简单,包括两个步骤:
-
定义应用程序缓存清单清单
contactbook.appcache
:CACHE MANIFEST # v1 2017-30-01 CACHE: index.html css/style.css js/ext/babel.min.js js/ext/pouchdb.min.js js/store.js js/app.js
对于我们的简单应用程序,我们定义一个单独的
CACHE
部分,并将所有资产放在此处。 -
从HTML引用清单文件:
<html manifest="contactbook.appcache" lang="en">
而已。 让我们在不支持Service Workers的浏览器中打开页面,并以与以前相同的方式对其进行测试。
离线数据
能够缓存资产非常棒。 但这还不够。 使应用程序更强大的是独特的数据。 我们将使用PouchDB作为客户端数据存储。 它功能强大,易于使用,并且开箱即用地提供数据同步。
如果您不熟悉它,请查看PouchDB的简介 。
助手类Store
负责与PouchDB的交互:
class Store {
constructor(name) {
this.db = new PouchDB(name);
}
getAll() {
// get all items from storage including details
return this.db.allDocs({
include_docs: true
})
.then(db => {
// re-map rows to collection of items
return db.rows.map(row => {
return row.doc;
});
});
}
get(id) {
// find item by id
return this.db.get(id);
}
save(item) {
// add or update an item depending on _id
return item._id ?
this.update(item) :
this.add(item);
}
add(item) {
// add new item
return this.db.post(item);
}
update(item) {
// find item by id
return this.db.get(item._id)
.then(updatingItem => {
// update item
Object.assign(updatingItem, item);
return this.db.put(updatingItem);
});
}
remove(id) {
// find item by id
return this.db.get(id)
.then(item => {
// remove item
return this.db.remove(item);
});
}
}
Store
类的代码是典型的CRUD实现,提供基于Promise的API。
现在,我们的主要应用程序组件可以使用Store
:
class ContactBook {
constructor(storeClass) {
// create store instance
this.store = new storeClass('contacts');
// init component internals
this.init();
// refresh the component
this.refresh();
}
refresh() {
// get all contacts from the store
this.store.getAll().then(contacts => {
// render retrieved contacts
this.renderContactList(contacts);
});
}
...
}
将Store
类传递给构造函数,以使App类与具体商店脱钩。 创建存储后,将在refresh
方法中使用它来获取所有联系人。
应用程序初始化如下所示:
new ContactBook(Store);
其他应用程序方法与商店互动:
class ContactBook {
...
showContact(event) {
// get contact id from the clicked element attributes
var contactId = event.currentTarget.getAttribute(CONTACT_ID_ATTR_NAME);
// get contact by id
this.store.get(contactId).then(contact => {
// show contact details
this.setContactDetails(contact);
// turn off editing
this.toggleContactFormEditing(false);
})
}
editContact() {
// get id of selected contact
var contactId = this.getContactId();
// get contact by id
this.store.get(this.getContactId()).then(contact => {
// show contact details
this.setContactDetails(contact);
// turn on editing
this.toggleContactFormEditing(true);
});
}
saveContact() {
// get contact details from edit form
var contact = this.getContactDetails();
// save contact
this.store.save(contact).then(() => {
// clear contact details form
this.setContactDetails({});
// turn off editing
this.toggleContactFormEditing(false);
// refresh contact list
this.refresh();
});
}
removeContact() {
// ask user to confirm deletion
if (!window.confirm(CONTACT_REMOVE_CONFIRM))
return;
// get id of selected contact
var contactId = this.getContactId();
// remove contact by id
this.store.remove(contactId).then(() => {
// clear contact details form
this.setContactDetails({});
// turn off editing
this.toggleContactFormEditing(false);
// refresh contact list
this.refresh();
});
}
这些是使用存储CRUD方法的基本操作:
-
showContact
–从列表中选择联系人后,显示联系人详细信息 -
editContact
–允许编辑联系人的详细信息 -
saveContact
–保存新联系人或现有联系人的详细信息 -
removeContact
–删除选定的联系人
现在,如果您在离线时添加联系人并刷新页面,则数据不会丢失。
但是,有一个“但是”…
资料同步
一切正常,但是所有数据都存储在本地浏览器中。 如果我们在其他浏览器中打开该应用程序,则不会看到更改。
我们需要与服务器实现数据同步。 双向数据同步的实现不是一个小问题。 幸运的是,如果后端有CouchDB,则由PouchDB提供。
让我们稍微更改一下Store
类,使其与远程数据源同步:
class Store {
constructor(name, remote, onChange) {
this.db = new PouchDB(name);
// start sync in pull mode
PouchDB.sync(name, `${remote}/${name}`, {
live: true,
retry: true
}).on('change', info => {
onChange(info);
});
}
我们向构造函数添加了两个参数:
-
remote
–远程服务器的URL -
onChange
–一旦后端发生更改,就会触发回调
PouchDB.sync
方法可以解决问题,并与后端开始同步。 live
参数指示应定期检查更改,而retry
指示在发生错误时重试(因此,如果用户脱机,同步将不会停止)。
我们需要相应地更改应用程序类,并将必需的参数传递给Store
构造函数:
class ContactBook {
constructor(storeClass, remote) {
this.store = new storeClass('contacts', remote, () => {
// refresh contact list when data changed
this.refresh();
});
...
}
现在,主应用程序类的构造函数接受传递给商店的远程URL。 onChange
回调仅调用refresh
方法刷新联系人列表。
应用初始化必须更新:
new ContactBook(Store, 'http://localhost:5984');
做完了! 现在,我们的应用程序允许离线时编辑联系人列表。 应用建立网络连接后,数据将与后端存储同步。
让我们测试一下:
- 使用
$ npm run serve
运行Web服务器 - 在两个不同的浏览器中打开URL http://127.0.0.1:8080/
- 单击
Ctrl + C
停止Web服务器 - 在两个浏览器中编辑联系人列表
- 使用
$ npm run serve
再次运行Web服务器 - 在两个浏览器中签出联系人列表(根据两个浏览器中的更改,该列表应该是最新的)
太好了,我们做到了!
结论
今天,提供脱机体验越来越有价值。 对于频繁使用的应用程序而言,能够在运输过程中使用不稳定连接的应用程序或在飞机上脱机时使用它至关重要。 这也与提高应用程序性能有关。
为了支持离线,我们需要注意以下事项:
- 缓存应用程序资产–将Service Workers保留为AppCache,直到所有现代浏览器都支持前者
- 在客户端存储数据–使用浏览器脱机存储,例如IndexedDB,其中有一个可用的库
我们只是研究了如何实现所有这些。 希望您喜欢阅读。 请在评论中分享您对该主题的想法!
本文由James Kolce和Craig Buckler进行同行评审。 感谢所有SitePoint的同行评审员使SitePoint内容达到最佳状态!
翻译自: https://www.sitepoint.com/offline-web-apps-service-workers-pouchdb/
web workers