浏览器存储 API(Browser Storage APIs)
一、浏览器存储体系概述
1.1 存储金字塔
现代浏览器提供了多层次的存储解决方案,每种方案都有其特定的用途、性能特征和容量限制。理解这些差异是选择正确存储方案的基础。
容量 ↑
│ IndexedDB
│ (无限制*)
│
├─ localStorage
│ (约 5-10MB)
│
├─ Session Storage
│ (约 5-10MB)
│
├─ Cookie
│ (约 4KB)
│
└─ Cache API
(无限制*)
*IndexedDB 和 Cache API 的限制取决于可用的磁盘空间和浏览器策略
1.2 选择存储方案的因素
在选择存储方案时,需要考虑以下因素:
数据持久性:数据需要保存多久?
- Session Storage:标签页关闭后清除
- localStorage:持久保存,直到手动清除
- IndexedDB:持久保存,容量更大
数据类型:存储什么类型的数据?
- 简单键值对:localStorage/sessionStorage
- 结构化数据:IndexedDB
- 需要发送服务器:Cookie
同步还是异步:API 特性
- localStorage/sessionStorage:同步 API
- IndexedDB:异步 API(但也可以同步访问)
容量需求:需要存储多少数据?
- Cookie:4KB
- localStorage/sessionStorage:5-10MB
- IndexedDB:几百 MB 到无限制
二、Web Storage:localStorage 和 sessionStorage
2.1 基本 API
Web Storage 提供了简洁的键值对存储接口:
// 存储数据
localStorage.setItem('username', '张三');
localStorage.setItem('preferences', JSON.stringify({ theme: 'dark', lang: 'zh' }));
// 读取数据
const username = localStorage.getItem('username');
const preferences = JSON.parse(localStorage.getItem('preferences'));
// 删除数据
localStorage.removeItem('username');
// 清空所有数据
localStorage.clear();
// 获取所有键
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(`${key}: ${value}`);
}
2.2 sessionStorage 的生命周期
sessionStorage 与 localStorage 的 API 完全相同,但生命周期不同:
// sessionStorage 在新标签页中创建独立的存储
sessionStorage.setItem('tempData', '仅当前标签页有效');
// 关闭标签页后,数据自动清除
// 刷新页面不会清除 sessionStorage
// 但"恢复上次会话"功能可能会保留 sessionStorage
// 在同一标签页的 iframe 之间共享
// 但不同标签页的 sessionStorage 相互隔离
2.3 存储事件
当 localStorage 发生变化时,同源的其他标签页会收到通知:
// 监听存储变化
window.addEventListener('storage', (event) => {
console.log('Key:', event.key);
console.log('Old Value:', event.oldValue);
console.log('New Value:', event.newValue);
console.log('Storage Area:', event.storageArea);
});
// 注意:设置相同值不会触发事件
// 当前标签页的修改不会触发自身的事件
2.4 容量与限制
// 检测存储是否已满
function isQuotaExceeded(e) {
return e.name === 'QuotaExceededError' ||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED';
}
try {
localStorage.setItem('data', 'some large data');
} catch (e) {
if (isQuotaExceeded(e)) {
console.log('Storage quota exceeded');
}
}
// 估算已用空间
function estimateStorageUsage() {
let total = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
total += localStorage[key].length + key.length;
}
}
return total;
}
2.5 实际应用场景
// 主题偏好
function setTheme(theme) {
localStorage.setItem('theme', theme);
document.documentElement.setAttribute('data-theme', theme);
}
// 表单草稿自动保存
function saveFormDraft(formId, data) {
sessionStorage.setItem(`draft_${formId}`, JSON.stringify(data));
}
function loadFormDraft(formId) {
const draft = sessionStorage.getItem(`draft_${formId}`);
return draft ? JSON.parse(draft) : null;
}
function clearFormDraft(formId) {
sessionStorage.removeItem(`draft_${formId}`);
}
// 多语言支持
function setLanguage(lang) {
localStorage.setItem('preferredLanguage', lang);
location.reload();
}
三、IndexedDB:浏览器端数据库
3.1 IndexedDB 的核心概念
IndexedDB 是一个完整的浏览器内数据库系统,包含以下核心概念:
数据库(Database):一个 IndexedDB 数据库可以包含多个对象存储区
对象存储区(Object Store):类似于 SQL 的表,但以对象为单位存储
索引(Index):用于快速查询的索引字段
事务(Transaction):保证数据一致性的操作单位
游标(Cursor):用于遍历数据集
3.2 数据库初始化
// 打开数据库
function openDB(dbName, version = 1) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建对象存储区
if (!db.objectStoreNames.contains('users')) {
// 主键索引
const store = db.createObjectStore('users', { keyPath: 'id' });
// 创建普通索引
store.createIndex('name', 'name', { unique: false });
// 创建唯一索引
store.createIndex('email', 'email', { unique: true });
// 创建复合索引
store.createIndex('nameAge', ['name', 'age'], { unique: false });
}
};
});
}
// 使用
openDB('myDatabase', 1).then((db) => {
console.log('Database opened:', db.name, db.version);
}).catch((err) => {
console.error('Failed to open database:', err);
});
3.3 CRUD 操作
// 添加数据
function addUser(db, user) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.add(user);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 读取数据
function getUser(db, id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 更新数据
function updateUser(db, user) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.put(user); // put 会覆盖已存在的记录
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 删除数据
function deleteUser(db, id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
3.4 查询与索引
// 使用索引查询
function findUserByEmail(db, email) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('email');
const request = index.get(email);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 使用游标遍历所有数据
function getAllUsers(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.openCursor();
const users = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
users.push(cursor.value);
cursor.continue();
} else {
resolve(users);
}
};
request.onerror = () => reject(request.error);
});
}
// 使用索引和游标进行范围查询
function findUsersByAgeRange(db, minAge, maxAge) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('age');
const range = IDBKeyRange.bound(minAge, maxAge);
const request = index.openCursor(range);
const users = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
users.push(cursor.value);
cursor.continue();
} else {
resolve(users);
}
};
request.onerror = () => reject(request.error);
});
}
3.5 封装库的选择
直接使用 IndexedDB API 既繁琐又容易出错,实际开发中推荐使用封装库:
// Dexie.js - 简洁的 IndexedDB 封装
const db = new Dexie('myDatabase');
db.version(1).stores({
users: '++id, name, email, age',
posts: '++id, title, authorId'
});
async function main() {
// 添加
await db.users.add({ name: '张三', email: '[email protected]', age: 25 });
// 查询
const user = await db.users.where('email').equals('[email protected]').first();
// 复杂查询
const youngUsers = await db.users.where('age').below(30).toArray();
// 关系查询
const userPosts = await db.posts.where('authorId').equals(user.id).toArray();
}
四、Cookie 的现代用法
4.1 Cookie 的特点
Cookie 是最老的客户端存储方案有其历史原因,但它在现代 Web 中仍有不可替代的用途:
- 自动随请求发送:Cookie 会被浏览器自动添加到 HTTP 请求的 Cookie 头中
- 服务器可读:服务端可以读取和设置 Cookie
- 容量极小:每个 Cookie 约 4KB
- 可设置过期时间:可以控制 Cookie 的生命周期
4.2 JavaScript 操作 Cookie
// 读取所有 Cookie
function getCookies() {
return document.cookie.split(';').reduce((cookies, cookie) => {
const [name, value] = cookie.trim().split('=');
cookies[name] = decodeURIComponent(value);
return cookies;
}, {});
}
// 设置 Cookie(简单方式)
document.cookie = 'username=张三; path=/; max-age=86400'; // 1 天
document.cookie = 'preferences=dark; path=/; max-age=2592000'; // 30 天
// 设置 Cookie(完整选项)
function setCookie(name, value, options = {}) {
let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (options.expires) {
cookieString += `; expires=${options.expires.toUTCString()}`;
}
if (options.maxAge) {
cookieString += `; max-age=${options.maxAge}`;
}
if (options.path) {
cookieString += `; path=${options.path}`;
}
if (options.domain) {
cookieString += `; domain=${options.domain}`;
}
if (options.secure) {
cookieString += '; secure';
}
if (options.sameSite) {
cookieString += `; samesite=${options.sameSite}`;
}
document.cookie = cookieString;
}
// 使用
setCookie('sessionId', 'abc123', {
maxAge: 3600, // 1 小时
path: '/',
sameSite: 'Strict'
});
4.3 HttpOnly 和 Secure Cookie
从安全角度,敏感 Cookie 不应该被 JavaScript 访问:
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict
- HttpOnly:JavaScript 无法通过 document.cookie 读取
- Secure:只通过 HTTPS 发送
- SameSite:防止 CSRF 攻击
4.4 Cookie 的适用场景
// ✅ 适合:需要发送到服务器的少量数据
// 例如:会话标识、CSRF token、用户偏好(如果服务器需要)
// ❌ 不适合:大量数据(用 localStorage/IndexedDB)
// ❌ 不适合:客户端专用数据(用 localStorage)
五、Storage 进阶主题
5.1 存储事件监听
Web Storage 的 storage 事件只在变化时触发,且只在其他标签页触发:
window.addEventListener('storage', (event) => {
// event.key: 变化的键
// event.oldValue: 旧值
// event.newValue: 新值
// event.storageArea: localStorage 或 sessionStorage
// event.url: 触发变化的文档 URL
if (event.key === 'cart') {
updateCartUI(JSON.parse(event.newValue));
}
});
5.2 存储配额查询
// Navigator.storage API
async function getStorageEstimate() {
if ('storage' in navigator && 'estimate' in navigator.storage) {
const estimate = await navigator.storage.estimate();
return {
usage: estimate.usage,
quota: estimate.quota,
percentUsed: (estimate.usage / estimate.quota) * 100
};
}
return null;
}
getStorageEstimate().then(({ usage, quota, percentUsed }) => {
console.log(`Using ${(usage / 1024 / 1024).toFixed(2)} MB of ${(quota / 1024 / 1024).toFixed(2)} MB (${percentUsed.toFixed(2)}%)`);
});
5.3 持久化存储
默认情况下,存储是"尽力而为"的——浏览器可能在存储压力下清除数据。使用 Persistent Storage API 可以申请更长的保留期:
async function requestPersistentStorage() {
if ('storage' in navigator && 'persist' in navigator.storage) {
const persistent = await navigator.storage.persist();
console.log('Persistent storage granted:', persistent);
return persistent;
}
return false;
}
// 检查是否有持久化存储权限
async function isPersisted() {
if ('storage' in navigator && 'persisted' in navigator.storage) {
return await navigator.storage.persisted();
}
return false;
}
5.4 数据迁移策略
当应用从 Cookie 迁移到 Web Storage 时:
// 从 Cookie 迁移到 localStorage
function migrateFromCookie() {
const cookies = getCookies();
if (cookies.userId && !localStorage.getItem('userId')) {
localStorage.setItem('userId', cookies.userId);
// 清除 Cookie
document.cookie = 'userId=; max-age=0';
}
}
六、实际应用架构
6.1 分层存储策略
// 存储分层
const StorageManager = {
// 即时访问:少量配置
config: {
get: (key) => JSON.parse(localStorage.getItem(`config_${key}`)),
set: (key, value) => localStorage.setItem(`config_${key}`, JSON.stringify(value)),
},
// 会话数据:当前会话需要
session: {
get: (key) => JSON.parse(sessionStorage.getItem(key)),
set: (key, value) => sessionStorage.setItem(key, JSON.stringify(value)),
},
// 持久数据:IndexedDB
db: {
async get(store, key) {
const db = await openDB('appDB');
return getRecord(db, store, key);
},
async set(store, data) {
const db = await openDB('appDB');
return putRecord(db, store, data);
},
}
};
6.2 离线优先架构
// Service Worker 中的缓存策略
const CACHE_NAME = 'app-cache-v1';
const DATA_CACHE = 'data-cache-v1';
// 缓存应用外壳
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/app.js'
]);
})
);
});
// IndexedDB 作为数据层
async function fetchAndCache(url) {
const response = await fetch(url);
if (response.ok) {
const db = await openDB('offlineDB');
await putRecord(db, 'responses', { url, data: await response.clone().json() });
return response;
}
return getFromCache(url);
}