通过对 Service Worker 的系统学习,我们知道它为 Web 的离线处理提供了支持,但它仍需要配合离线存储、后台同步等技术来充分挖掘、发挥其威力。在本章节中,我们将对离线存储进行讨论,希望通过本章节的学习,大家可以掌握以下内容:

离线存储方案对比。

  • IndexedDB 的基本使用。
  • Cache API 的基本使用。

# 离线存储方案对比

如图所示,在 Chrome Devtools -> Application -> Storage / Cache 标签下,我们可以看到不同的离线存储方案,究竟这些方案有什么差异,我们又该使用哪些方案来为 Service Worker 提供底层服务,本节将一一为大家说明。

Cookie 是服务器发送并保存到客户端的一小块数据,目的是为了解决无状态 HTTP 协议下无法验证两个请求来自同一会话的问题,主要特点为:

  • 无法跨域访问。
  • 存储数据格式单一。
  • 接口同步访问。
  • 存储空间过小(一般为 4 KB)。
  • 生命周期一般由服务器设定,如果由浏览器生成,一般在浏览器关闭后失效。
  • 每次请求都会自动携带 Cookie 信息,如果存放的数据过多,将会带来额外的性能开销。
  • 无法在 Web WorkerService Worker 环境下访问。

# LocalStorage 和 SessionStorage

LocalStorage、SessionStorage 是 HTML5 引入的离线存储技术,主要特点为:

  • 无法跨域访问。
  • 存储数据格式单一。
  • 接口同步访问。
  • 存储空间相对于 Cookie 有所增加(一般为 5 MB)。
  • 生命周期 LocalStorage 为永久,SessionStorage 则随着页面关闭而失效。
  • 仅存在客户端中,不参与服务端通信。
  • 无法在 Web Worker 及 Service Worker 环境下访问。

至此,我们对 CookieLocalStorage 以及 SessionStorage 进行了简单说明,由于其均无法在 Service Worker 环境下访问且 Service Worker 的离线存储要求能够存储大量、具有不同格式的数据,故上述存储方案均无法使用。它们的主要使用场景为:

  • 由于每次请求都会自动携带 Cookie 信息,优先使用 Cookie 来保存用户登录状态信息。
  • 也正是由于每次请求都会自动携带 Cookie 信息,除用户登录状态之外任何会话(比如购物车、游戏分数等)信息优先使用 LocalStorageSessionStorage

# Web SQL 和 IndexedDB

Web SQLIndexedDB 同样是 HTML5 引入的离线存储技术,主要特点为:

  • 无法跨域访问。
  • 可存储丰富的数据格式。
  • 接口异步访问(基于事件)。
  • 存储空间较大(一般不少于 250 MB)。
  • 生命周期为永久。
  • 仅存在客户端中,不参与服务端通信。
  • 能够在 Web WorkerService Worker 环境下访问。

总的来说,Web SQLIndexedDB 是关系型数据库、非关系型数据库在客户端的各自实现,其所支持的丰富数据格式、较大的存储空间以及可在 Service Worker 环境下访问的特性均符合 Service Worker 对底层技术的要求,但由于 Web SQL 已被 W3C 废弃且浏览器支持情况不甚理想,所以我们可选用 IndexedDB 作为 Service Worker 的底层技术支持。

# Application Cache

Application Cache 是 HTML5 引入的旨在提供页面离线访问能力的缓存机制,其使用步骤如下:

  • 在文档的 html 标签中设置 manifest 属性以引用 manifest 文件。
  • 配置 manifest文件,在其中设置需要缓存的资源。
  • 服务端正确配置 MIME-type

上述可知,它仅仅提供了一套离线缓存机制,并非离线存储方案。这里之所以提及,因为这是 Web 离线处理的一次尝试,但由于存在各种各样的问题,导致它最终被废弃的命运,这也才会有后来的 Service Worker。其主要问题为:

  • 缓存内容存在大小限制(一般为 5 MB)。
  • 无法通过编程方式清除缓存,必须用户手动清除。
  • manifest 配置文件格式要求比较严格。
  • 只要 manifest 配置文件中的资源有一个缓存(更新)失败,将导致全部资源缓存(更新)失败。
  • 由于会自动缓存引用了 manifest 的 HTML,这就导致如果改了 HTML 内容,也需要更新版本才能更新。
  • 难以实现动态缓存,且一旦出现问题,将难以进行调试。

# CacheStorage

CacheStorage 是 Service Worker 规范的一部分,因此从它出生的那一刻起便决定了它为 Service Worker 提供底层服务的使命。虽然它是 Service Worker 规范的一部分,但依旧可以脱离 Service Worker 单独使用。主要特点为:

  • 无法跨域访问。
  • 可存储丰富的数据格式。
  • 接口异步访问(基于 Promise)。
  • 存储空间较大(一般不少于 250 MB)。
  • 生命周期为永久。
  • 仅存在客户端中,不参与服务端通信。
  • 能够在 Web Worker 及 Service Worker 环境下访问。

# 小结

通过上述对比,我们可以使用 IndexedDB 及 CacheStorage 来为 Service Worker 的离线存储提供底层服务,根据社区的经验,它们各自的适用场景为:

  • 对于网址可寻址的(比如脚本、样式、图片、HTML 等)资源使用 CacheStorage
  • 其他资源则使用 IndexedDB
  • 完成了技术选型,接下来我们将对 IndexedDBCacheStorage 相关 API 的使用进行简单说明

# IndexedDB 的基本使用

本节我们将通过数据库及数据操作两个方面对 IndexedDB 的使用进行简单说明。

# 数据库操作

const openRequest = window.indexedDB.open('TodoList', 1);

作为使用 IndexedDB 的第一步,我们通过 window.indexedDB.open 来打开名为 TodoList 的数据库,如果该数据库不存在或指定的版本大于当前版本,都将会触发接口返回值(此处为 openRequest)的 onupgradeneeded 事件,我们一般在该事件的回调函数中进行存储空间的创建。比如:

openRequest.onupgradeneeded = function(event) {
  const db = event.target.result;
  const todosStore = db.createObjectStore('todos', { keyPath: 'id', autoIncrement : true });
  todosStore.createIndex('status', 'status');
};

上例中,我们先通过 event.target.result 来获取 IDBDatabase 实例,然后通过该实例的 createObjectStore 方法创建了一个名为 todos 的存储空间,该方法接收 2 个参数:

  • 第一个参数为存储空间的名称。
  • 第二个参数用来将存储对象中的某个属性设置为存储空间的 key 值,其中 autoIncrement 指定了 key 值是否为自增。

通过 createObjectStore 创建了存储空间后,我们通过存储空间的 createIndex 方法创建了一个名为 status 的索引,该方法接收 3 个参数:

  • 第一个参数为索引的名称。
  • 第二个参数指定了根据存储数据的哪一个属性来构建索引,其值可以为字符串或字符串数组。
  • 第三个参数指定了该索引的一些约束,常用属性为:
{
  unique: boolean, // 索引值是否唯一
  /**
   * 常用于包含操作,比如以下结构:
   * const People = {
   *   name: 'Tom',
   *   skills: ['C++', 'JavaScript', 'Java'],
   * };
   * 如果想通过 `objectStore.index(’skill‘).get('Java')` 获取数据,
   * 那么在创建 `skill` 索引的时候则需将 `multiEntry` 设置为 `true`
   */
  multiEntry: boolean
}

至此便完成了数据库、存储空间的创建,接下来我们将对数据操作进行简要说明。

# 数据操作

openRequest.onsuccess = function(event) {
  const db = event.target.result;
  const transaction = db.transaction(['todos'], 'readonly');
  const todosStore = transaction.objectStore('todos');
  // ……
};

上例中,我们通过订阅 openRequestonsuccess 事件以便在回调函数中对数据进行操作,由于 IndexedDB 的数据操作都是基于事务的,因此第一步我们通过 IDBDatabase 的实例(此处为 db)方法 transaction 来获得了一个只读事务,该实例方法接收 2 个参数:

  • 第一个参数指定要操作的存储空间名称,其值可以为字符串或字符串数组。
  • 第二个参数指定事务的模式,常用值为 readonlyreadwrite,默认值为 readonly

得到事务实例后,我们便可通过事务实例的 objectStore 方法获得需要操作的存储空间,之后便可通过存储空间实例(此处为 todosStore)进行数据的增、删、改、查等操作,具体细节参看 IndexedDB使用文档 或参照本章 IndexedDB 示例,此处不再阐述

# Cache API 的基本使用

掌握了 IndexedDB,接下来我们来简单了解下 Cache API

# CacheStorage 接口

我们可通过 caches 来访问 CacheStorage,主要接口为:

  • open:获取指定名称的 Cache 对象。
  • keys:获取 CacheStorage 所有 Cache 对象中的 Response 条目键值列表。
  • has:判断是否存在指定名称的 Cache 对象。
  • delete:删除指定名称的 Cache 对象。
  • match:获取指定请求所对应的 Response 条目(如匹配到多个,则返回第一个)。

# Cache 接口

通过上述 CacheStorage 的一系列接口获取到 Cache 对象后,我们便可对缓存进行操作,主要接口为:

  • match:获取指定请求所对应的 Response 条目(如匹配到多个,则返回第一个)。
  • matchAll:与 match 的唯一差别是该接口返回所有匹配项。
  • add:获取指定 URL 的资源,将返回的 Response 添加到 Cache 对象中。
  • addAll: 与 add 的唯一差别是该接口可以获取多个 URL 的资源,并将其依次添加到 Cache 对象中。
  • put:将指定 RequestResponse添加到 Cache 对象中。
  • delete: 删除指定 RequestResponse 条目。
  • keys:获取指定 RequestResponse 条目键值列表

这里我们仅对 CacheStorage 和 Cache 的接口进行了一个简短说明,具体细节请参看 CacheStorage APICache API 或参照本章 Cache 示例,此处不再阐述。

# 总结

本章节我们首先通过对比得出了使用 IndexedDBCacheStorageService Worker 提供离线存储底层服务的结论,而后对它们的基本使用进行了简单说明。下一章我们将对离线处理中的后台同步进行讲解,相信通过 Service Worker 与这些底层服务的有效结合,我们完全可以构建出应对复杂网络状况下高可用的 Web 应用。

阅读全文