估算可用存储空间

tl;dr

Chrome 61 将推出更多浏览器,现在可以通过以下方式提供 Web 应用当前使用了多少存储空间以及可用存储空间的估算值:

if ('storage' in navigator && 'estimate' in navigator.storage) {
  navigator.storage.estimate().then(({usage, quota}) => {
    console.log(`Using ${usage} out of ${quota} bytes.`);
  });
}

现代 Web 应用和数据存储

如果考虑现代 Web 应用的存储需求,将存储内容分为两类:加载 Web 应用所需的核心数据,以及加载应用后有意义的用户互动所需的数据。

第一种类型的数据(加载 Web 应用所需的数据)包括 HTML、JavaScript、CSS 以及一些图片。Service Worker 以及 Cache Storage API 为保存这些核心资源提供了所需的基础架构,然后利用这些资源快速加载您的 Web 应用,理想情况下,这些资源最好完全绕过网络。(与 Web 应用的构建流程集成的工具(例如新的 Workbox 库或旧版 sw-precache)可以完全自动执行存储、更新和使用此类数据的过程。)

但其他类型的数据呢?这些资源并不是加载 Web 应用的必要资源,但可能会对您的整体用户体验发挥重要作用。例如,如果您正在编写图片编辑 Web 应用,则可能需要保存图片的一个或多个本地副本,以便用户在修订版本之间切换并撤消工作。或者,如果您正在开发离线媒体播放体验,则在本地保存音频或视频文件将是一项关键功能。每个可以进行个性化设置的 Web 应用最终都需要保存某种状态信息。如何知道有多少空间可用于此类运行时存储,以及如何在空间用尽后会出现什么情况?

过去的时间:window.webkitStorageInfonavigator.webkitTemporaryStorage

一直以来,浏览器都是通过带前缀的接口来支持此类自省,例如非常旧的(和已废弃的)window.webkitStorageInfo,以及不是很旧但仍非标准的 navigator.webkitTemporaryStorage。虽然这些接口提供了有用的信息,但它们没有成为未来的 Web 标准。

这就是 WHATWG 存储标准的用武之地。

未来:navigator.storage

随着 Storage Living Standard 的不断努力,目前有几个实用的 API 采用了 StorageManager 接口,该接口以 navigator.storage 的形式向浏览器公开。与许多其他较新的 Web API 一样,navigator.storage 仅适用于安全源(通过 HTTPS 或 localhost 提供)。

去年,我们推出了 navigator.storage.persist() 方法,可让您的 Web 应用请求对其存储空间免于自动清理。

它现在通过 navigator.storage.estimate() 方法联接,用于替代 navigator.webkitTemporaryStorage.queryUsageAndQuota()estimate() 会返回类似的信息,但公开了一个基于 promise 的接口,这与其他现代异步 API 保持一致。estimate() 返回的 promise 使用包含两个属性的对象进行解析:usage(表示当前使用的字节数)和 quota(表示当前源站可以存储的最大字节数)。(与其他所有与存储相关的任务一样,配额将应用于整个源站。)

如果 Web 应用尝试使用 IndexedDB 或 Cache Storage API 等存储数据,且该数据足以使指定源站超出其可用配额,则请求将失败并抛出 QuotaExceededError 异常。

实际存储空间估算值

到底如何使用 estimate() 取决于您的应用需要存储的数据类型。例如,您可以在界面中更新一个控件,以便用户了解每个存储操作完成后使用了多少空间。理想情况下,您可以提供一个界面,让用户能够手动清理不再需要的数据。您可以按照以下行编写代码:

// For a primer on async/await, see
// https://developers.google.com/web/fundamentals/getting-started/primers/async-functions
async function storeDataAndUpdateUI(dataUrl) {
  // Pro-tip: The Cache Storage API is available outside of service workers!
  // See https://googlechrome.github.io/samples/service-worker/window-caches/
  const cache = await caches.open('data-cache');
  await cache.add(dataUrl);

  if ('storage' in navigator && 'estimate' in navigator.storage) {
    const {usage, quota} = await navigator.storage.estimate();
    const percentUsed = Math.round(usage / quota * 100);
    const usageInMib = Math.round(usage / (1024 * 1024));
    const quotaInMib = Math.round(quota / (1024 * 1024));

    const details = `${usageInMib} out of ${quotaInMib} MiB used (${percentUsed}%)`;

    // This assumes there's a <span id="storageEstimate"> or similar on the page.
    document.querySelector('#storageEstimate').innerText = details;
  }
}

估算值的准确性如何?

您不容易错过这样一个事实:从函数返回的数据只是起点所占用空间的估算值。就在函数名称中!usagequota 值都不是预期值,因此建议您考虑以下事项:

  • usage 反映了给定源有效用于同源数据的字节数,而这些数据又会受到内部压缩技术、可能包含未使用空间的固定大小分配块的影响,以及删除后临时创建的“tombstone”记录的存在。为了防止确切大小信息泄露,在本地保存的跨源不透明资源可能会为总体 usage 值贡献额外的填充字节。
  • quota 表示当前为源站预留的空间量。该值取决于一些常量因素(例如总体存储空间大小),但也取决于许多潜在的易变因素,包括当前未使用的存储空间量。因此,当设备上的其他应用写入或删除数据时,浏览器愿意为 Web 应用的源占用的空间量可能会发生变化。

当前:特征检测和回退

从 Chrome 61 开始,estimate() 默认处于启用状态。Firefox 正尝试使用 navigator.storage,但自 2017 年 8 月起,此功能默认处于停用状态。您需要启用 dom.storageManager.enabled 偏好设置才能进行测试。

使用目前并非所有浏览器都支持的功能时,必须进行功能检测。您可以在旧版 navigator.webkitTemporaryStorage 方法的基础上,将功能检测与基于 promise 的封装容器相结合,以提供一致的接口,包括:

function storageEstimateWrapper() {
  if ('storage' in navigator && 'estimate' in navigator.storage) {
    // We've got the real thing! Return its response.
    return navigator.storage.estimate();
  }

  if ('webkitTemporaryStorage' in navigator &&
      'queryUsageAndQuota' in navigator.webkitTemporaryStorage) {
    // Return a promise-based wrapper that will follow the expected interface.
    return new Promise(function(resolve, reject) {
      navigator.webkitTemporaryStorage.queryUsageAndQuota(
        function(usage, quota) {resolve({usage: usage, quota: quota})},
        reject
      );
    });
  }

  // If we can't estimate the values, return a Promise that resolves with NaN.
  return Promise.resolve({usage: NaN, quota: NaN});
}