JavaScript和浏览器API的强大功能世界之间的联系日益紧密-可以访问互联网的人数已增加到
45亿 。
但是,此数据不能反映Internet连接速度慢或断开的人数。 即使在美国,也有
490万家庭无法以每秒3兆比特以上的速度访问有线Internet。
世界其他地区-拥有可靠Internet访问的国家-仍然容易失去连接。 可能影响网络连接质量的
一些因素包括:
- 提供者的覆盖范围很差。
- 极端天气条件。
- 停电。
- 掉入死区的用户,例如阻塞其网络连接的建筑物。
- 火车旅行和隧道旅行。
- 由第三方控制并受时间限制的连接。
- 在特定时间或日期需要有限或没有Internet访问的文化习俗。
鉴于此,很明显,我们在开发和创建应用程序时必须考虑自主经验。

本文是在EDISON Software的支持下翻译的,EDISON Software是一家从华南地区获得出色订单的公司,并且还开发了Web应用程序和网站 。
我最近有机会使用服务工作者,缓存存储和IndexedDB向现有应用程序添加自治功能。 该应用程序脱机工作所需的技术工作被简化为四个单独的任务,我将在本文中进行讨论。
服务人员
为脱机使用而创建的应用程序不应高度依赖网络。 从概念上讲,只有在出现故障的情况下存在备份选项时,才有可能。
如果Web应用程序加载失败,我们必须将浏览器的资源放在某个地方(HTML / CSS / JavaScript)。 这些资源来自哪里(如果不是来自网络请求)? 缓存如何。 大多数人会同意,提供一个可能过时的用户界面比空白页更好。
浏览器不断查询数据。 作为后备数据缓存服务,仍然需要我们以某种方式拦截浏览器请求并编写缓存规则。 这就是服务人员发挥作用的地方-将他们视为中介。

服务工作者只是一个JavaScript文件,我们可以在其中订阅事件并编写我们自己的用于缓存和处理网络故障的规则。
让我们开始吧。
请注意:我们的演示应用程序在整个本文中,我们将向演示应用程序添加独立功能。 演示应用程序是一个用于在图书馆中借书/租书的简单页面。 进度将以一系列GIF以及离线Chrome DevTools模拟的使用形式呈现。
这是初始状态:

任务1-缓存静态资源
静态资源是不经常更改的资源。 HTML,CSS,JavaScript和图像可能属于此类。 浏览器尝试使用服务工作者可以拦截的请求来加载静态资源。
让我们从注册我们的服务工作者开始。
if ('serviceWorker' in navigator) { window.addEventListener('load', function() { navigator.serviceWorker.register('/sw.js'); }); }
服务工作者是
网络工作者 ,因此必须从单独的JavaScript文件导入。 加载网站后,使用
register
方法进行
register
。
现在我们已经加载了服务工作者,让我们缓存我们的静态资源。
var CACHE_NAME = 'my-offline-cache'; var urlsToCache = [ '/', '/static/css/main.c9699bb9.css', '/static/js/main.99348925.js' ]; self.addEventListener('install', function(event) { event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { return cache.addAll(urlsToCache); }) ); });
由于我们控制静态资源的网址,因此我们可以在服务工作者初始化之后使用“
Cache Storage
立即缓存它们。

现在,我们的缓存已充满最近请求的静态资源,让我们在请求失败的情况下从缓存中加载这些资源。
self.addEventListener('fetch', function(event) { event.respondWith( fetch(event.request).catch(function() { caches.match(event.request).then(function(response) { return response; } ); ); });
每次浏览器发出请求时,都会触发
fetch
事件。 我们新的
fetch
事件处理程序现在具有其他逻辑,可在网络中断的情况下返回缓存的响应。
演示编号1

我们的演示应用程序现在可以离线提供静态资源! 但是我们的数据在哪里?
任务2-缓存动态资源
单页应用程序(SPA)通常在初始加载页面后逐渐请求数据,我们的演示应用程序也不例外-图书列表不会立即加载。 此数据通常来自XHR请求,这些请求返回的响应频繁更改以为应用程序提供新状态-因此它们是动态的。
缓存动态资源实际上与缓存静态资源非常相似-主要区别在于我们需要更频繁地更新缓存。 生成所有可能的动态XHR请求的完整列表也非常困难,因此我们将在它们到达时对其进行缓存,并且没有像静态资源那样具有预定义的列表。
看一下我们的
fetch
处理程序:
self.addEventListener('fetch', function(event) { event.respondWith( fetch(event.request).catch(function() { caches.match(event.request).then(function(response) { return response; } ); ); });
我们可以通过添加一些可以缓存成功的请求和响应的代码来定制此实现。 这确保了我们不断向缓存中添加新请求并不断更新缓存数据。
self.addEventListener('fetch', function(event) { event.respondWith( fetch(event.request) .then(function(response) { caches.open(CACHE_NAME).then(function(cache) { cache.put(event.request, response); }); }) .catch(function() { caches.match(event.request).then(function(response) { return response; } ); ); });
我们的
Cache Storage
目前有几个条目。

演示编号2

现在,无论我们的网络状态如何,我们的演示在启动时看起来都一样!
太好了 现在,让我们尝试使用我们的应用程序。

不幸的是,错误消息无处不在。 似乎我们与界面的所有交互都不起作用。 我无法选择或移交这本书! 需要解决什么?
任务3-建立乐观的用户界面
目前,我们的应用程序存在的问题是我们的数据收集逻辑仍然高度依赖于网络响应。 检入或检出操作将请求发送到服务器,并期望成功的响应。 这有利于数据一致性,但不利于我们的独立体验。
为了使这些交互脱机工作,我们需要使我们的应用程序更加
乐观 。 乐观的交互不需要服务器的响应,并愿意显示数据的更新视图。 在大多数Web应用程序中,通常的乐观操作是
delete
-如果我们已经拥有所有必要的信息,为什么不给用户即时反馈呢?
使用乐观方法将应用程序与网络断开连接相对容易实现。
case CHECK_OUT_SUCCESS: case CHECK_OUT_FAILURE: list = [...state.list]; list.push(action.payload); return { ...state, list, }; case CHECK_IN_SUCCESS: case CHECK_IN_FAILURE; list = [...state.list]; for (let i = 0; i < list.length; i++) { if (list[i].id === action.payload.id) { list.splice(i, 1, action.payload); } } return { ...state, list, };
关键是要以相同的方式处理用户操作-无论网络请求是否成功。 上面的代码段摘自我们应用程序的redux reducer,根据网络的可用性启动了
SUCCESS
和
FAILURE
。 无论网络请求如何完成,我们都将更新图书清单。
演示编号3

用户交互现在在线发生(不是字面上的)。 尽管控制台的红色消息表明没有执行网络请求,但“签入”和“签出”按钮会相应地更新界面。
好啊 乐观的离线渲染只有一个小问题...
我们不会失去我们的零钱吗?

任务4-将用户操作排队以进行同步
我们需要跟踪用户离线时执行的操作,以便在用户返回网络时将其与服务器同步。 浏览器中有几种存储机制可以充当操作队列,我们将使用IndexedDB。 IndexedDB提供了一些您无法从LocalStorage获得的功能:
看一下我们以前的reducer代码:
case CHECK_OUT_SUCCESS: case CHECK_OUT_FAILURE: list = [...state.list]; list.push(action.payload); return { ...state, list, }; case CHECK_IN_SUCCESS: case CHECK_IN_FAILURE; list = [...state.list]; for (let i = 0; i < list.length; i++) { if (list[i].id === action.payload.id) { list.splice(i, 1, action.payload); } } return { ...state, list, };
让我们对其进行修改,以在
FAILURE
事件期间将签入和签出事件存储在IndexedDB中。
case CHECK_OUT_FAILURE: list = [...state.list]; list.push(action.payload); addToDB(action);
这是与addToDB
addToDB
一起创建IndexedDB的实现。
let db = indexedDB.open('actions', 1); db.onupgradeneeded = function(event) { let db = event.target.result; db.createObjectStore('requests', { autoIncrement: true }); }; const addToDB = action => { var db = indexedDB.open('actions', 1); db.onsuccess = function(event) { var db = event.target.result; var objStore = db .transaction(['requests'], 'readwrite') .objectStore('requests'); objStore.add(action); }; };
现在,我们所有的离线用户操作都存储在浏览器的内存中,我们可以使用
online
浏览器事件监听器在恢复连接后同步数据。
window.addEventListener('online', () => { const db = indexedDB.open('actions', 1); db.onsuccess = function(event) { let db = event.target.result; let objStore = db .transaction(['requests'], 'readwrite') .objectStore('requests'); objStore.getAll().onsuccess = function(event) { let requests = event.target.result; for (let request of requests) { send(request);
在此阶段,我们可以清除已成功发送到服务器的所有请求的队列。
演示编号4
最终的演示看起来有些复杂。 在右侧的深色终端窗口中,记录了所有API活动。 该演示涉及脱机,选择多本书籍并在线返回。

显然,当用户返回联机状态时,脱机发出的请求已排队并立即发送。
这种“玩耍”的方法有点天真-例如,如果我们拿起并退回同一本书,则可能不需要发出两个请求。 如果几个人使用同一个应用程序,它也将不起作用。
仅此而已
出去使您的Web应用程序脱机! 这篇文章展示了您可以为应用程序添加独立功能的许多操作,但并不是绝对的。
要了解更多信息,请查看
Google Web基础知识 。 要查看另一个离线实现,请查看
此演讲 。

另请阅读博客
EDISON公司:
20个图书馆
壮观的iOS应用程序