HTML5 离线存储原理与使用

使用 Service Worker 和 Cache API 实现网页离线访问

问题

HTML5 的离线储存怎么使用,工作原理能不能解释一下?

解答

HTML5 离线存储有两种方案:

  1. Application Cache(已废弃)- 通过 manifest 文件声明缓存资源
  2. Service Worker + Cache API(推荐)- 现代离线存储方案

Application Cache(了解即可)

<!-- index.html -->
<!DOCTYPE html>
<html manifest="app.appcache">
<head>
  <title>离线应用</title>
</head>
<body>
  <h1>Hello Offline</h1>
</body>
</html>
# app.appcache
CACHE MANIFEST
# v1.0.0

# 需要缓存的文件
CACHE:
index.html
styles.css
app.js

# 需要网络访问的文件
NETWORK:
api/

# 离线时的替代页面
FALLBACK:
/ offline.html

⚠️ Application Cache 已被废弃,新项目请使用 Service Worker。

Service Worker + Cache API(推荐)

1. 注册 Service Worker

// main.js - 在页面中注册 Service Worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('SW 注册成功,作用域:', registration.scope);
    })
    .catch(error => {
      console.log('SW 注册失败:', error);
    });
}

2. 编写 Service Worker

// sw.js - Service Worker 文件
const CACHE_NAME = 'my-app-v1';
const CACHE_URLS = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/images/logo.png'
];

// 安装阶段:缓存静态资源
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('缓存资源');
        return cache.addAll(CACHE_URLS);
      })
      .then(() => self.skipWaiting()) // 立即激活
  );
});

// 激活阶段:清理旧缓存
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames
          .filter(name => name !== CACHE_NAME)
          .map(name => caches.delete(name))
      );
    }).then(() => self.clients.claim()) // 接管所有页面
  );
});

// 拦截请求:优先使用缓存
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // 缓存命中,返回缓存
        if (response) {
          return response;
        }
        // 缓存未命中,发起网络请求
        return fetch(event.request).then(response => {
          // 检查是否是有效响应
          if (!response || response.status !== 200) {
            return response;
          }
          // 克隆响应并缓存
          const responseClone = response.clone();
          caches.open(CACHE_NAME).then(cache => {
            cache.put(event.request, responseClone);
          });
          return response;
        });
      })
      .catch(() => {
        // 网络失败,返回离线页面
        return caches.match('/offline.html');
      })
  );
});

3. 常用缓存策略

// 策略1:缓存优先(Cache First)
async function cacheFirst(request) {
  const cached = await caches.match(request);
  return cached || fetch(request);
}

// 策略2:网络优先(Network First)
async function networkFirst(request) {
  try {
    const response = await fetch(request);
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, response.clone());
    return response;
  } catch {
    return caches.match(request);
  }
}

// 策略3:仅缓存(Cache Only)
async function cacheOnly(request) {
  return caches.match(request);
}

// 策略4:仅网络(Network Only)
async function networkOnly(request) {
  return fetch(request);
}

// 策略5:Stale While Revalidate(返回缓存同时更新)
async function staleWhileRevalidate(request) {
  const cache = await caches.open(CACHE_NAME);
  const cached = await cache.match(request);
  
  const fetchPromise = fetch(request).then(response => {
    cache.put(request, response.clone());
    return response;
  });
  
  return cached || fetchPromise;
}

工作原理

┌─────────────┐     ┌─────────────────┐     ┌─────────────┐
│   浏览器     │ ──► │  Service Worker │ ──► │    网络     │
│   (页面)    │ ◄── │   (代理层)       │ ◄── │   (服务器)  │
└─────────────┘     └────────┬────────┘     └─────────────┘


                    ┌─────────────────┐
                    │   Cache Storage │
                    │    (本地缓存)    │
                    └─────────────────┘
  1. Service Worker 作为浏览器和网络之间的代理
  2. 拦截所有网络请求(fetch 事件)
  3. 根据缓存策略决定返回缓存还是发起网络请求
  4. 离线时直接返回缓存内容

关键点

  • Application Cache 已废弃,现代项目使用 Service Worker
  • Service Worker 运行在独立线程,不能直接操作 DOM
  • 必须在 HTTPS 环境下使用(localhost 除外)
  • Service Worker 有生命周期:install → waiting → activate → fetch
  • 根据资源类型选择合适的缓存策略:静态资源用缓存优先,API 用网络优先