估算可用存储空间

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 和一些图片。服务工作线程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 的工作中,我们在 StorageManager 接口中添加了一些实用 API,该接口会以 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(表示当前来源可以存储的字节数上限)。(与与存储空间相关的所有其他内容一样,配额会应用于整个来源。)

如果网页应用尝试存储的数据量足以使给定来源超出其可用配额(例如,使用 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 反映的是给定来源实际用于同源数据的字节数,而这反过来又会受到内部压缩技术、可能包含未使用的空间的固定大小分配块以及删除后可能暂时创建的“墓碑”记录的影响。为防止泄露确切大小信息,保存在本地的跨源不透明资源可能会为总体 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});
}