浏览器存储与离线策略—Cookie、IndexedDB与Service Worker缓存|新宇宙博客返回列表浏览器存储与离线策略
系统讲解Cookie完整属性与安全限制、Web Storage同源策略、IndexedDB事务模型及Service Worker离线缓存策略
浏览器存储与离线策略
浏览器提供多层存储方案:从同步的 localStorage/sessionStorage、异步的 IndexedDB,到 Service Worker 驱动的离线缓存策略。选择正确的存储方案并实现有效的离线策略,是构建 PWA 和提升用户体验的关键。
目录
- 存储方案对比
- IndexedDB 异步操作
- Service Worker 缓存策略
- Storage API 配额管理
- 数据同步与冲突解决
- 实战案例
- 深度追问
- 总结表格
1. 存储方案对比
localStorage.setItem('user', JSON.stringify({ name: 'Alice' }));
const user = JSON.parse(localStorage.getItem('user'));
.(, );
. = ;
sessionStorage
setItem
'temp'
'data'
document
cookie
'token=abc; max-age=3600; secure; samesite=strict'
2. IndexedDB 异步操作
class DB {
#dbPromise;
constructor(name, version, upgrade) {
this.#dbPromise = new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onupgradeneeded = (e) => upgrade(e.target.result, e.oldVersion);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async transaction(stores, mode, fn) {
const db = await this.#dbPromise;
return new Promise((resolve, reject) => {
const tx = db.transaction(stores, mode);
const result = fn(tx);
tx.oncomplete = () => resolve(result);
tx.onerror = () => reject(tx.error);
});
}
async get(store, key) {
const db = await this.#dbPromise;
return new Promise((resolve, reject) => {
const tx = db.transaction(store, 'readonly');
const request = tx.objectStore(store).get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async put(store, value, key) {
const db = await this.#dbPromise;
return new Promise((resolve, reject) => {
const tx = db.transaction(store, 'readwrite');
const request = tx.objectStore(store).put(value, key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAll(store, query, count) {
const db = await this.#dbPromise;
return new Promise((resolve, reject) => {
const tx = db.transaction(store, 'readonly');
const request = tx.objectStore(store).getAll(query, count);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
const db = new DB('myApp', 1, (db, oldVersion) => {
if (oldVersion < 1) {
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('email', 'email', { unique: true });
}
});
await db.put('users', { id: 1, name: 'Alice', email: 'alice@test.com' });
const user = await db.get('users', 1);
3. Service Worker 缓存策略
const CACHE_NAME = 'v1';
const PRECACHE_URLS = ['/index.html', '/styles.css', '/app.js'];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE_URLS))
);
});
async function cacheFirst(request) {
const cached = await caches.match(request);
return cached || fetch(request).then(response => {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
});
}
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);
}
}
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;
}
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request));
} else {
event.respondWith(cacheFirst(event.request));
}
});
4. Storage API 配额管理
async function checkStorage() {
if (navigator.storage) {
const { usage, quota } = await navigator.storage.estimate();
console.log(`Used: ${(usage / 1024 / 1024).toFixed(1)}MB`);
console.log(`Quota: ${(quota / 1024 / 1024).toFixed(1)}MB`);
console.log(`Remaining: ${((quota - usage) / 1024 / 1024).toFixed(1)}MB`);
const persistent = await navigator.storage.persist();
console.log(`Persistent: ${persistent}`);
}
}
async function cleanupOldData(db, maxAge = 7 * 24 * 60 * 60 * 1000) {
const cutoff = Date.now() - maxAge;
const tx = db.transaction('cache', 'readwrite');
const store = tx.objectStore('cache');
const index = store.index('timestamp');
const range = IDBKeyRange.upperBound(cutoff);
let cursor = await index.openCursor(range);
let deleted = 0;
while (cursor) {
cursor.delete();
deleted++;
cursor = await cursor.continue();
}
console.log(`Cleaned ${deleted} old entries`);
}
5. 数据同步与冲突解决
class OfflineQueue {
#db;
constructor(db) { this.#db = db; }
async enqueue(action) {
await this.#db.put('syncQueue', {
id: crypto.randomUUID(),
action,
timestamp: Date.now(),
retries: 0
});
}
async sync() {
const pending = await this.#db.getAll('syncQueue');
for (const item of pending.sort((a, b) => a.timestamp - b.timestamp)) {
try {
await this.#execute(item.action);
await this.#db.delete('syncQueue', item.id);
} catch (e) {
if (item.retries >= 3) {
await this.#db.delete('syncQueue', item.id);
console.error('Permanently failed:', item);
} else {
await this.#db.put('syncQueue', { ...item, retries: item.retries + 1 });
}
}
}
}
async #execute(action) {
return fetch(action.url, { method: action.method, body: JSON.stringify(action.body) });
}
}
6. 实战案例
实战案例 1:离线优先的笔记应用
class OfflineNotes {
#db; #syncQueue;
async init() {
this.#db = new DB('notes-app', 1, (db) => {
db.createObjectStore('notes', { keyPath: 'id' });
db.createObjectStore('syncQueue', { keyPath: 'id' });
});
this.#syncQueue = new OfflineQueue(this.#db);
window.addEventListener('online', () => this.#syncQueue.sync());
}
async saveNote(note) {
note.updatedAt = Date.now();
await this.#db.put('notes', note);
if (navigator.onLine) {
await fetch('/api/notes', { method: 'PUT', body: JSON.stringify(note) });
} else {
await this.#syncQueue.enqueue({ url: '/api/notes', method: 'PUT', body: note });
}
}
}
实战案例 2:IndexedDB 全文搜索
class ClientSearch {
#db;
async index(documents) {
for (const doc of documents) {
const tokens = this.#tokenize(doc.content);
await this.#db.put('documents', { ...doc, _tokens: tokens });
}
}
async search(query) {
const queryTokens = this.#tokenize(query);
const all = await this.#db.getAll('documents');
return all
.map(doc => ({
...doc,
_score: this.#score(queryTokens, doc._tokens)
}))
.filter(doc => doc._score > 0)
.sort((a, b) => b._score - a._score);
}
#tokenize(text) {
return text.toLowerCase().split(/\W+/).filter(t => t.length > 2);
}
#score(queryTokens, docTokens) {
const docSet = new Set(docTokens);
return queryTokens.filter(t => docSet.has(t)).length / queryTokens.length;
}
}
实战案例 3:跨标签页状态同步
class TabSync {
#channel;
#handlers = new Map();
constructor(channelName = 'app-sync') {
this.#channel = new BroadcastChannel(channelName);
this.#channel.onmessage = (e) => {
this.#handlers.get(e.data.type)?.forEach(h => h(e.data.payload));
};
}
broadcast(type, payload) {
this.#channel.postMessage({ type, payload });
}
on(type, handler) {
if (!this.#handlers.has(type)) this.#handlers.set(type, new Set());
this.#handlers.get(type).add(handler);
}
}
const sync = new TabSync();
sync.on('logout', () => window.location.href = '/login');
sync.on('theme-change', (theme) => document.body.className = theme);
7. 深度追问
Q1:localStorage 的同步操作会阻塞主线程吗?
是的。localStorage 读写涉及磁盘 I/O(虽然通常被 OS 缓存)和跨进程锁。在频繁读写或大量数据时会导致明显的主线程阻塞。IndexedDB 是异步的,不会阻塞。
Q2:Service Worker 的生命周期如何影响缓存策略?
SW 有 install → activate → fetch 三个关键阶段。新 SW 在 install 时预缓存资源,但直到旧 SW 控制的所有标签页关闭后才 activate。skipWaiting() + clients.claim() 可以立即接管。
Q3:IndexedDB 事务的 ACID 保证?
IndexedDB 事务保证原子性(全部成功或全部回滚)和隔离性(同一对象存储的读写事务串行化)。但不保证持久性——浏览器可能在崩溃时丢失最近提交的事务。
8. 总结表格
| 存储方案 | 容量 | 同步/异步 | 适用数据 |
|---|
| localStorage | ~5MB | 同步 | 用户偏好、小型配置 |
| sessionStorage | ~5MB | 同步 | 会话临时数据 |
| IndexedDB | 几乎无限 | 异步 | 结构化数据、离线数据 |
| Cache API | 几乎无限 | 异步 | HTTP 响应缓存 |
| Cookie | 4KB | 同步 | 身份凭证 |