估算可用存储空间

杰夫·波斯尼克
Jeff Posnick

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 WorkerCache Storage API 提供了保存这些核心资源,供日后利用它们快速加载 Web 应用(理想情况下是完全绕过网络)所需的基础架构。(与 Web 应用的构建流程集成的工具,例如新的 Workbox 库或旧版 sw-precache,可以完全自动执行存储、更新和使用此类数据的过程。)

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

过去:window.webkitStorageInfonavigator.webkitTemporaryStorage

一直以来,浏览器都通过带前缀的接口支持此类内省,例如非常老旧(且已废弃的)window.webkitStorageInfo 以及不那么老旧但并非标准的 navigator.webkitTemporaryStorage。虽然这些界面可提供实用信息,但它们未来没有网络标准。

在这种情况下,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});
}