# Storage Buckets API

> navigator.storageBuckets.open() 创建各自带 IndexedDB、caches、配额与过期时间的命名分区。选项、命名规则、每种拒绝与 Chrome 122 行为。

`navigator.storageBuckets.open(name, options)` 创建或打开一个命名的存储桶：它是源存储空间的一个分区，拥有自己的 `indexedDB` 工厂、`caches`、桶级文件系统、持久化模式、配额上限和过期时间，因此浏览器可以在存储紧张时逐出低价值的桶，同时保留已持久化的桶。没有桶时，一个源只有一个默认桶，逐出是全有或全无的。

Chrome 122 与 Edge 122 一并提供了 `StorageBucketManager` 和 `StorageBucket` 的全部成员（`indexedDB`、`caches`、`getDirectory()`、`persist()`、`estimate()`、`setExpires()`）；Android 版 Chrome、Samsung Internet 与 Android WebView 镜像该版本。Firefox 尚未实现（[Bugzilla 1594740](https://bugzil.la/1594740)），Safari 没有实现（BCD `api.StorageBucketManager`）。把桶当作默认桶之上的增强：在 Chrome 上把数据存进 `bucket.indexedDB`、在其他浏览器存进 `window.indexedDB` 的页面，可以共用同一套 schema 代码。

## 语法

```js
navigator.storageBuckets.open(name)
navigator.storageBuckets.open(name, options)
navigator.storageBuckets.keys()
navigator.storageBuckets.delete(name)

bucket.persist()
bucket.persisted()
bucket.estimate()
bucket.setExpires(timestamp)
bucket.expires()
bucket.getDirectory()
```

`open()` 返回 `Promise<StorageBucket>`，`keys()` 返回存活桶名的 `Promise<sequence<DOMString>>`，`delete()` 返回 `Promise<undefined>`，即使不存在同名桶也会兑现。`navigator.storageBuckets` 带 `[SecureContext]`，暴露在 `Window` 与 worker 上；`persist()` 仅限 `Window`，因为它可能要查询 `persistent-storage` 权限。`indexedDB` 与 `caches` 属性是作用域限定在桶内的普通 `IDBFactory` 与 `CacheStorage` 对象，`getDirectory()` 返回桶级源私有文件系统的 `FileSystemDirectoryHandle`。

## 参数

`open()` 接受一个桶名和一个可选的 `StorageBucketOptions` 字典。桶名必须是 1 到 64 个码点的 ASCII 小写字母、数字、`_` 或 `-`，且不能以 `_` 或 `-` 开头；打开已存在的名称会返回已有的桶并忽略 `options`。

| 成员 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `persisted` | `boolean` | 否，默认 `false` | 请求 `"persistent"` 桶模式。只有 `persistent-storage` 权限为 `"granted"` 时才会生效；否则桶以尽力而为模式打开，`bucket.persisted()` 兑现为 `false`。 |
| `quota` | `unsigned long long`（字节） | 否 | 桶用量的上限；浏览器可以施加更低的限制。设置后 `bucket.estimate()` 把它作为 `quota` 报告。 |
| `expires` | `DOMHighResTimeStamp`（自 Unix 纪元起的毫秒数） | 否 | 桶被视为已消失的时间点。过期的桶在下一次 `open()`、`keys()` 或访问时被惰性移除。 |

Chromium 的 IDL 还带有一个 `durability` 成员（`"strict"` 或 `"relaxed"`），位于 `StorageBucketsDurability` 运行时标志之后；WICG explainer 已把它从 API 的第一版中移除，因为 IndexedDB 事务本身就暴露同样的 `durability` 选项，所以不要依赖它。

`open()` 返回的 `StorageBucket` 有以下成员。

| 成员 | 类型 | 说明 |
|---|---|---|
| `name` | `DOMString`（只读） | 打开桶时使用的键。 |
| `persist()` | `Promise<boolean>` | `persistent-storage` 权限已授予时升级为 `"persistent"` 模式；以结果模式兑现。 |
| `persisted()` | `Promise<boolean>` | 桶处于 `"persistent"` 模式时为 `true`。 |
| `estimate()` | `Promise<StorageEstimate>` | 仅此桶的 `usage` 与 `quota`，不同于覆盖整个源的 `navigator.storage.estimate()`。 |
| `setExpires(ms)` / `expires()` | `Promise<undefined>` / `Promise<DOMHighResTimeStamp?>` | 设置或读取过期时间；未设置时 `expires()` 兑现为 `null`。 |
| `indexedDB`、`caches`、`getDirectory()` | `IDBFactory`、`CacheStorage`、`Promise<FileSystemDirectoryHandle>` | 桶级的存储端点。 |

## 异常

`open()`、`keys()` 与 `delete()` 以拒绝而非抛出的方式报错，唯一的例外是整个 API 对当前上下文不可用时 Chromium 同步抛出的 `SecurityError`。

| 异常 | 方法 | 条件 |
|---|---|---|
| `TypeError` | `open()`、`keys()`、`delete()`、`estimate()` | 无法获得 storage shelf（opaque origin、存储被禁用）；`open()` 还会因桶名无效、`expires` 早于当前时间、`quota` 小于等于零而拒绝。 |
| `InvalidCharacterError` | `delete()` | 规范规定的桶名无效情形。Chromium 在这里改以 `TypeError` 拒绝（见实测）。 |
| `InvalidStateError` | `persist()`、`persisted()`、`estimate()`、`setExpires()`、`expires()` | 桶的 `removed` 标志已置位：你拿到句柄之后它被删除或已过期。 |
| `QuotaExceededError` | `open()` | Chromium 特有：该源创建的桶数量超过实现允许的上限（`Too many buckets created.`）。 |
| `UnknownError` | `open()`、`keys()`、`delete()` | Chromium 特有：存储后端失败（`Unknown error occured while creating a bucket.`，拼写错误是 Chromium 原文）。 |
| `SecurityError` | 任一方法（同步抛出） | Chromium 在此上下文中拒绝该 API，例如 opaque origin：`Access to Storage Buckets API is denied in this context.` |

:::observed
在 Chrome 中，`navigator.storageBuckets.open('Inbox')` 因大写字母以 `TypeError: The bucket name 'Inbox' is not a valid name.` 拒绝；`open('inbox', { quota: 0 })` 以 `TypeError: The bucket's quota cannot equal zero.` 拒绝；`open('inbox', { expires: Date.now() - 1000 })` 以 `TypeError: The bucket expiration is invalid.` 拒绝。`navigator.storageBuckets.delete('Inbox')` 以 `TypeError: The bucket name Inbox is not a valid name.` 拒绝，而规范要求的是 `InvalidCharacterError`。四条字符串见 Chromium 的 [`storage_bucket_manager.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/buckets/storage_bucket_manager.cc)（chromium.googlesource.com）。
:::

## 示例

两个示例都先解析出一个存储端点，再让同一套应用代码在它之上运行，这样 Firefox 与 Safari 的回退就是源的默认存储，而不是第二套代码路径。

### 把草稿放进持久化桶，缺少支持时回退到默认 IndexedDB

草稿是用户最不愿丢失的数据，所以放进一个申请 `persisted: true` 的桶。`navigator.storageBuckets` 缺失时，同一个数据库在 `window.indexedDB` 上打开；持久化请求被拒时，代码如实告知，而不是假定桶是安全的。

```js
function openDatabase(factory, name, version) {
  return new Promise((resolve, reject) => {
    const request = factory.open(name, version);
    request.onupgradeneeded = () => request.result.createObjectStore('drafts', { keyPath: 'id' });
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

async function openDraftsDatabase() {
  if (!('storageBuckets' in navigator)) {
    return { db: await openDatabase(indexedDB, 'drafts', 1), persisted: false };
  }
  const bucket = await navigator.storageBuckets.open('drafts', { persisted: true });
  return {
    db: await openDatabase(bucket.indexedDB, 'drafts', 1),
    persisted: await bucket.persisted(), // persistent-storage 授予前为 false
  };
}

const { db, persisted } = await openDraftsDatabase();
if (!persisted) {
  document.querySelector('#storage-note').textContent =
    '草稿以尽力而为方式保存，存储紧张时可能被清除。';
}
```

在全新的 Chrome 配置文件中 `persisted` 会返回 `false`，因为 `persistent-storage` 尚未授予；先在用户手势中调用 `navigator.storage.persist()` 才会改变这一结果。

### 自行过期的资讯缓存

供离线阅读而缓存的资讯流值得保留一周，之后就没有价值。桶带上 `expires` 时间戳，浏览器会在没有清理任务的情况下移除它；回退分支把同样的响应存进默认 `caches`，并按记录的日期手动清理。

```js
const WEEK = 7 * 24 * 60 * 60 * 1000;

async function feedCache() {
  if ('storageBuckets' in navigator) {
    const bucket = await navigator.storageBuckets.open('feed', { expires: Date.now() + WEEK });
    return bucket.caches.open('articles');
  }
  const cache = await caches.open('articles');
  const stamp = await cache.match('/__cached-at');
  if (stamp && Date.now() - Number(await stamp.text()) > WEEK) {
    await caches.delete('articles');
    return caches.open('articles');
  }
  if (!stamp) await cache.put('/__cached-at', new Response(String(Date.now())));
  return cache;
}

const cache = await feedCache();
await cache.add('/api/feed?page=1');
```

打开已存在的桶不会刷新它的过期时间，所以长期使用的资讯桶需要在每次成功同步后调用 `bucket.setExpires(Date.now() + WEEK)` 把期限持续后推。

## 另请参阅

- [IndexedDB：面向 PWA 的结构化客户端存储](/zh/reference/storage/indexeddb/)
- [存储持久化、配额与逐出](/zh/reference/storage/persistence/)，桶所细化的源级模型
- [配额与 StorageManager.estimate()](/zh/reference/storage/quota-estimate/)
- [Cache Storage：离线 PWA 背后的请求/响应存储](/zh/reference/storage/cache-storage/)
- [Storage Buckets 支持情况](/zh/compatibility/storage-buckets/)
- [Storage Buckets API: open() method](https://wicg.github.io/storage-buckets/#dom-storagebucketmanager-open)（wicg.github.io）
- [Storage Buckets API: validate a bucket name](https://wicg.github.io/storage-buckets/#validate-a-bucket-name)（wicg.github.io）
- [Storage Buckets explainer](https://github.com/WICG/storage-buckets/blob/main/explainer.md)（github.com）
- [Bugzilla 1594740: Implement support for storage buckets](https://bugzil.la/1594740)（bugzilla.mozilla.org）