跳到主要内容

HTML5 离线存储怎么用?工作原理如何?

面试速答(30 秒版 TL;DR)

  • “离线存储”常见两层含义
    • 离线缓存资源(让页面/静态资源离线可用):现代主流是 Service Worker + Cache API(PWA 方案)。
    • 离线存数据(把业务数据留在本地):常用 IndexedDB,其次是 localStorage/sessionStorage(更适合小体量、低频配置)。
  • 面试里说的“HTML5 离线存储”很多是历史题:早期方案 Application Cache(AppCache) 通过 manifest 文件声明要缓存的资源,但它已废弃,新项目不建议用。
  • 工作原理一句话
    • AppCache:浏览器根据 manifest 生成“应用缓存”,离线优先读缓存;更新依赖 manifest 变化,且通常要 刷新/重开页面 才生效。
    • Service Worker:浏览器在后台线程运行 SW,拦截 fetch,你用代码决定“走网络还是走缓存”,并通过版本化缓存实现可控更新。

心智模型:离线“资源” vs 离线“数据”

目标典型技术本质常见坑
让页面资源离线可用Service Worker + Cache API(推荐)缓存 Request/Response更新策略、缓存失效、离线兜底页
让页面资源离线可用(历史)AppCache(废弃)manifest 声明的资源集合更新不直观、缓存不受控、任一资源失败导致整次更新失败
让业务数据离线可用IndexedDB(推荐)浏览器端数据库需要封装、版本迁移、事务与索引设计
轻量持久化localStorage同步 KV(字符串)阻塞主线程、容量小、XSS 风险高(别存敏感信息)

下面重点讲“离线缓存资源”的两套方案:AppCache(历史题)Service Worker(现代题)


方案一(历史题):Application Cache(AppCache)

注意

AppCache 已被废弃,现代浏览器对它的支持逐步移除,新项目不要用。面试提到它,通常是考你是否理解它的 用法、缓存规则、更新机制与坑

1)怎么用(基本步骤)

  1. 写一个 manifest 文件(扩展名不重要,内容格式重要)
  2. 在 HTML 根元素上声明 manifest<html manifest="/app.appcache">
  3. 服务端正确返回 manifest
    • 让浏览器每次都能检查更新:通常会给 manifest 配 Cache-Control: no-cache(避免 manifest 自己被强缓存导致永远不更新)
    • 任何被缓存的资源如果 404/超时/下载失败,可能导致本次更新整体失败

一个最小示例:

<!doctype html>
<html manifest="/app.appcache">
<head>
<meta charset="utf-8" />
<title>AppCache Demo</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<script src="/app.js"></script>
</body>
</html>

/app.appcache(示例):

CACHE MANIFEST
# v1.0.0 (改这行就能触发更新检查的“内容变化”)

CACHE:
/styles.css
/app.js
/logo.png

NETWORK:
*

FALLBACK:
/ /offline.html

2)manifest 的三段(面试高频)

  • CACHE::显式缓存的资源列表(离线时主要靠它)
  • NETWORK::必须走网络的资源
    • * 表示“除 manifest 指定的缓存资源外,其它请求都走网络”
  • FALLBACK::网络不可用时的降级映射(常用于离线兜底页)

3)工作原理与更新机制(核心考点)

AppCache 有一个非常反直觉的点:“检查更新”与“使用新缓存”不是同一件事

你需要记住的结论:

  • 更新触发条件:浏览器认为 manifest 内容有变化(所以很多人会在 manifest 里放 # version 注释行做“版本戳”)。
  • 更新时机:下载新资源集一般在后台进行;更新完成会进入“ready”状态。
  • 生效时机:多数情况下要 刷新/重开页面(或显式 swapCache())才切到新缓存。
  • 一致性策略:AppCache 倾向于“整包更新”(某个资源失败可能导致整次更新失败),这也是它难用的原因之一。

4)典型追问与坑

  • 为什么 AppCache 容易“发布了用户还在旧版本”?
    • 因为资源默认离线优先,且“新缓存就绪”不等于“立刻生效”,用户不刷新就可能继续用旧缓存。
  • 为什么改了 JS/CSS 但没更新?
    • 很多时候是 manifest 被强缓存 了,浏览器根本没去重新拉取 manifest,自然也不会触发更新。
  • 为什么一加离线缓存反而更不稳定?
    • AppCache 对错误非常敏感:manifest 语法错误、资源下载失败等,都会导致缓存更新流程不可控。

方案二(推荐):Service Worker + Cache API(PWA)

1)怎么用(最小可用)

页面注册:

// main.ts
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}

/sw.js(极简 cache-first 示例):

const CACHE_NAME = 'app-cache-v1';
const PRECACHE_URLS = ['/', '/index.html', '/styles.css', '/app.js', '/offline.html'];

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(PRECACHE_URLS);
}),
);
});

self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys
.filter((key) => key !== CACHE_NAME)
.map((key) => caches.delete(key)),
);
}),
);
});

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached;
return fetch(event.request).catch(() => caches.match('/offline.html'));
}),
);
});

2)工作原理(抓住生命周期 + 拦截点)

面试可背的结论:

  • SW 是可编程的代理层:你可以实现 cache-first、network-first、stale-while-revalidate 等策略。
  • 更新可控:常用做法是缓存名版本化(如 app-cache-v2),activate 时清理旧缓存。
  • 离线兜底:网络失败时返回 /offline.html 或离线骨架页。

速记要点(可背诵)

  • “HTML5 离线存储”如果指资源离线:AppCache(历史) vs Service Worker + Cache API(现代)
  • AppCache 用法:<html manifest> + CACHE MANIFEST 文件(CACHE/NETWORK/FALLBACK)。
  • AppCache 更新:看 manifest 是否变化;新缓存就绪后通常要 刷新 才切换;manifest 自己被缓存会导致“怎么都不更新”。
  • Service Worker:install 预缓存,fetch 拦截,activate 清旧缓存;策略与更新完全可编程,适合 PWA。