为了打造可靠的离线体验,您的 PWA 需要存储空间管理功能。在缓存这一章中,您已了解到,将数据保存到设备上的一种方案就是使用缓存存储。在本章中,我们将向您介绍如何管理离线数据,包括数据持久化、限制和可用工具。
存储
存储不仅仅是文件和资产,还可能包含其他类型的数据。在所有支持 PWA 的浏览器上,以下 API 可用于设备端存储:
- IndexedDB:适用于结构化数据和 blob(二进制数据)的 NoSQL 对象存储选项。
- WebStorage:一种使用本地存储或会话存储存储键值对字符串对的方法。它在 Service Worker 环境中不可用。此 API 是同步 API,因此不建议用于复杂的数据存储。
- 缓存存储:如缓存模块中所述。
您可以在受支持的平台上使用 Storage Manager API 管理所有设备存储空间。 Cache Storage API 和 IndexedDB 提供对 PWA 永久性存储空间的异步访问,并且可以通过主线程、Web 工作器和 Service Worker 访问。在确保 PWA 在网络不稳定或不存在的情况下可靠运行方面,两者都发挥着重要作用。但分别应该在什么情况下使用呢?
针对网络资源(即您通过网址请求的资源,例如 HTML、CSS、JavaScript、图片、视频和音频)使用 Cache Storage API。
使用 IndexedDB 存储结构化数据。这包括需要以类似 NoSQL 的方式搜索或组合的数据,或其他数据,例如不一定与网址请求匹配的特定于用户的数据。请注意,IndexedDB 不适用于全文搜索。
IndexedDB
如需使用 IndexedDB,请先打开一个数据库。这会创建一个新数据库(如果不存在)。
IndexedDB 是一个异步 API,但它接受回调,而不是返回 Promise。以下示例使用 Jake Archibald 的 idb 库,这是一个适用于 IndexedDB 的小型 Promise 封装容器。使用 IndexedDB 不需要帮助程序库,但如果您想使用 Promise 语法,可以选择使用 idb
库。
以下示例将创建一个数据库,用于保存烹饪食谱。
创建和打开数据库
如需打开数据库,请执行以下操作:
- 使用
openDB
函数新建一个名为cookbook
的 IndexedDB 数据库。由于 IndexedDB 数据库采用版本控制,因此每当您更改数据库结构时,您都需要增加版本号。第二个参数是数据库版本。在示例中, 设置为 1。 - 系统会将包含
upgrade()
回调的初始化对象传递给openDB()
。回调函数将在首次安装数据库或数据库升级到新版本时调用。此函数是唯一可以执行操作的位置。操作可能包括创建新的对象存储(IndexedDB 用于整理数据的结构)或索引(您想在其中进行搜索)。这也是应进行数据迁移的地方。通常,upgrade()
函数包含不带break
语句的switch
语句,以允许各个步骤根据旧版数据库按顺序执行。
import { openDB } from 'idb';
async function createDB() {
// Using https://github.com/jakearchibald/idb
const db = await openDB('cookbook', 1, {
upgrade(db, oldVersion, newVersion, transaction) {
// Switch over the oldVersion, *without breaks*, to allow the database to be incrementally upgraded.
switch(oldVersion) {
case 0:
// Placeholder to execute when database is created (oldVersion is 0)
case 1:
// Create a store of objects
const store = db.createObjectStore('recipes', {
// The `id` property of the object will be the key, and be incremented automatically
autoIncrement: true,
keyPath: 'id'
});
// Create an index called `name` based on the `type` property of objects in the store
store.createIndex('type', 'type');
}
}
});
}
该示例在 cookbook
数据库中创建了一个名为 recipes
的对象存储,将 id
属性设置为该存储的索引键,并根据 type
属性创建了另一个名为 type
的索引。
我们来看看刚刚创建的对象存储。将配方添加到对象存储,并在基于 Chromium 的浏览器上打开开发者工具或在 Safari 中打开 Web Inspector 后,您应该会看到以下内容:
添加数据
IndexedDB 使用事务。事务是将操作组合到一起的,因此这些操作会作为一个单元进行。它们有助于确保数据库始终处于一致的状态。如果您正在运行多个应用程序副本,那么它们也至关重要,可以防止同时写入相同的数据。 要添加数据,请执行以下操作:
- 启动一项事务,并将
mode
设置为readwrite
。 - 获取对象存储,您将在其中添加数据。
- 使用您要保存的数据调用
add()
。该方法以字典形式接收数据(以键值对的形式)并将其添加到对象存储中。必须可以使用结构化克隆来克隆字典。如果要更新现有对象,则应调用put()
方法。
事务有一个 done
promise,该 promise 在事务成功完成后进行解析,或拒绝并返回事务错误。
如 IDB 库文档中所述,如果您要向数据库写入数据,则 tx.done
表示所有内容已成功提交到数据库。但是,等待单个操作会有好处,因为这样您可以看到导致事务失败的任何错误。
// Using https://github.com/jakearchibald/idb
async function addData() {
const cookies = {
name: "Chocolate chips cookies",
type: "dessert",
cook_time_minutes: 25
};
const tx = await db.transaction('recipes', 'readwrite');
const store = tx.objectStore('recipes');
store.add(cookies);
await tx.done;
}
添加 Cookie 后,此配方将与其他配方一起存储在数据库中。此 ID 由 indexDB 自动设置并递增。如果运行此代码两次,将出现两个相同的 Cookie 条目。
正在检索数据
下面展示了如何从 IndexedDB 获取数据:
- 启动事务并指定一个或多个对象存储,还可选择性地指定事务类型。
- 从该事务调用
objectStore()
。请务必指定对象存储名称。 - 使用要获取的密钥调用
get()
。默认情况下,存储区使用其键作为索引。
// Using https://github.com/jakearchibald/idb
async function getData() {
const tx = await db.transaction('recipes', 'readonly')
const store = tx.objectStore('recipes');
// Because in our case the `id` is the key, we would
// have to know in advance the value of the id to
// retrieve the record
const value = await store.get([id]);
}
存储空间管理器
了解如何管理 PWA 的存储空间对于正确存储和流式传输网络响应尤为重要。
存储空间容量在所有存储方案之间共享,包括 Cache Storage、IndexedDB、Web Storage,甚至 Service Worker 文件及其依赖项。
不过,可用的存储空间大小因浏览器而异。您的存储空间不可能用完;网站在某些浏览器中可能会存储 MB 级甚至 GB 级的数据。例如,Chrome 最多允许浏览器使用总磁盘空间的 80%,而单个来源最多可使用整个磁盘空间的 60%。对于支持 Storage API 的浏览器,您可以了解您的应用还有多少存储空间可供使用、其配额和使用情况。
以下示例使用 Storage API 估算配额和用量,然后计算已用百分比和剩余字节数。请注意,navigator.storage
会返回 StorageManager
的实例。具有单独的 Storage
接口,很容易造成混淆。
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
// quota.usage -> Number of bytes used.
// quota.quota -> Maximum number of bytes available.
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(`You've used ${percentageUsed}% of the available storage.`);
const remaining = quota.quota - quota.usage;
console.log(`You can write up to ${remaining} more bytes.`);
}
在 Chromium 开发者工具中,打开 Application 标签页中的 Storage 部分,即可查看网站的配额和存储空间使用量(按使用量细分)。
Firefox 和 Safari 不提供用于查看当前源的所有存储空间配额和使用情况的摘要屏幕。
数据持久性
您可以要求浏览器在兼容平台上提供永久性存储空间,以避免在闲置或出现存储压力后自动逐出数据。如果授予此权限,浏览器将永远不会从存储空间中逐出数据。此保护包括 Service Worker 注册、IndexedDB 数据库和缓存存储空间中的文件。请注意,用户始终拥有存储空间,他们可以随时删除存储空间,即使浏览器授予了永久性存储空间也是如此。
如需请求永久性存储空间,请调用 StorageManager.persist()
。与之前一样,StorageManager
接口是通过 navigator.storage
属性进行访问的。
async function persistData() {
if (navigator.storage && navigator.storage.persist) {
const result = await navigator.storage.persist();
console.log(`Data persisted: ${result}`);
}
您还可以通过调用 StorageManager.persisted()
检查当前源是否已授予永久性存储空间。Firefox 向用户请求使用永久存储的权限。基于 Chromium 的浏览器会根据启发法确定内容对用户的重要性,从而提供或拒绝保留内容。Google Chrome 的一个条件就是必须安装 PWA。如果用户在操作系统中安装了 PWA 的图标,浏览器可能会授予永久性存储空间。
API 浏览器支持
网络存储
浏览器支持
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
文件系统访问
浏览器支持
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
存储空间管理器
浏览器支持
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">