本地存储与会话存储:浏览器中的数据保险箱
客户端存储的演进
在 Web 开发的早期,如果你想在用户的浏览器中存储一点数据,Cookie 几乎是唯一的选择。但 Cookie 有诸多限制:容量小(约 4KB)、每次请求都会发送到服务器、操作 API 繁琐。
HTML5 带来了 Web Storage API,它像是为浏览器定制的小型数据库——容量更大(通常 5-10MB)、操作简单、不会自动发送到服务器。这让客户端数据存储变得优雅而高效。
Web Storage 分为两种:localStorage 提供持久化存储,数据会一直保留直到被明确删除;sessionStorage 则与会话绑定,关闭标签页或浏览器后数据就会消失。
localStorage:持久化的数据仓库
localStorage 是你在浏览器中的永久储物柜。只要用户不主动清理浏览器数据,存储的内容就会一直保留。
基础操作
// 存储数据
localStorage.setItem("username", "Sarah");
localStorage.setItem("theme", "dark");
// 读取数据
const username = localStorage.getItem("username");
console.log(username); // 'Sarah'
// 读取不存在的键返回 null
const notExist = localStorage.getItem("notExist");
console.log(notExist); // null
// 删除单个数据
localStorage.removeItem("theme");
// 清除所有数据
localStorage.clear();
// 获取存储的键数量
console.log(localStorage.length);
// 通过索引获取键名
const firstKey = localStorage.key(0);
console.log(firstKey);你也可以使用点符号或方括号语法直接访问,但这不是推荐的做法:
// 可行但不推荐
localStorage.username = "Michael";
console.log(localStorage.username);
// 这种方式有风险
localStorage.setItem = "oops"; // 这会覆盖 setItem 方法!
// 始终使用 API 方法更安全
localStorage.setItem("setItem", "safe"); // 正确的方式存储复杂数据
localStorage 只能存储字符串。如果你试图存储其他类型的数据,它们会被自动转换为字符串:
// 数字会被转换为字符串
localStorage.setItem("count", 42);
console.log(localStorage.getItem("count")); // '42' (字符串)
console.log(typeof localStorage.getItem("count")); // 'string'
// 布尔值也一样
localStorage.setItem("isActive", true);
console.log(localStorage.getItem("isActive")); // 'true' (字符串)
// 对象会变成 [object Object]
localStorage.setItem("user", { name: "John" });
console.log(localStorage.getItem("user")); // '[object Object]'要正确存储对象和数组,需要使用 JSON:
// 存储对象
const user = {
id: 1,
name: "Sarah Chen",
email: "[email protected]",
preferences: {
theme: "dark",
language: "zh-CN",
},
};
localStorage.setItem("user", JSON.stringify(user));
// 读取对象
const storedUser = JSON.parse(localStorage.getItem("user"));
console.log(storedUser.name); // 'Sarah Chen'
console.log(storedUser.preferences.theme); // 'dark'
// 存储数组
const recentSearches = ["JavaScript", "React", "Vue", "Node.js"];
localStorage.setItem("searches", JSON.stringify(recentSearches));
// 读取数组
const searches = JSON.parse(localStorage.getItem("searches"));
console.log(searches[0]); // 'JavaScript'安全的读取包装
直接使用 JSON.parse 可能会因为数据损坏而抛出错误,建议封装一个安全的读取函数:
function getStoredItem(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
if (item === null) {
return defaultValue;
}
return JSON.parse(item);
} catch (error) {
console.warn(`读取 localStorage "${key}" 失败:`, error);
return defaultValue;
}
}
function setStoredItem(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error(`写入 localStorage "${key}" 失败:`, error);
return false;
}
}
// 使用示例
setStoredItem("settings", { volume: 80, muted: false });
const settings = getStoredItem("settings", { volume: 50, muted: false });
console.log(settings.volume); // 80遍历所有数据
// 方法1:使用 length 和 key()
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(`${key}: ${value}`);
}
// 方法2:使用 Object.keys()
Object.keys(localStorage).forEach((key) => {
console.log(`${key}: ${localStorage.getItem(key)}`);
});
// 方法3:获取所有数据为对象
function getAllLocalStorage() {
const data = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
try {
data[key] = JSON.parse(localStorage.getItem(key));
} catch {
data[key] = localStorage.getItem(key);
}
}
return data;
}
console.log(getAllLocalStorage());sessionStorage:会话级别的临时存储
sessionStorage 的 API 与 localStorage 完全相同,区别在于数据的生命周期——它只在当前会话(标签页)中有效。
基本特性
// 存储数据
sessionStorage.setItem("currentStep", "3");
sessionStorage.setItem(
"formData",
JSON.stringify({
name: "John",
email: "[email protected]",
})
);
// 读取数据
const step = sessionStorage.getItem("currentStep");
console.log(step); // '3'
// 其他操作与 localStorage 完全一致
sessionStorage.removeItem("currentStep");
sessionStorage.clear();会话的定义
理解 sessionStorage 中"会话"的含义很重要:
// 每个标签页都有独立的 sessionStorage
// 同一网站在不同标签页打开,它们的 sessionStorage 是隔离的
// 标签页 A
sessionStorage.setItem("tabId", "A");
// 标签页 B(即使是同一网站)
sessionStorage.setItem("tabId", "B");
// 各自读取自己的数据
// 标签页 A 读取到 'A'
// 标签页 B 读取到 'B'
// 刷新页面不会清除 sessionStorage
// 关闭标签页会清除 sessionStorage
// 使用"恢复标签页"功能可能会恢复 sessionStorage(浏览器实现不同)实际应用:多步骤表单
class MultiStepForm {
constructor(formId) {
this.formId = formId;
this.storageKey = `form_${formId}`;
}
saveProgress(step, data) {
const formData = this.getProgress() || { steps: {} };
formData.currentStep = step;
formData.steps[step] = data;
formData.lastUpdated = Date.now();
sessionStorage.setItem(this.storageKey, JSON.stringify(formData));
}
getProgress() {
const data = sessionStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : null;
}
getCurrentStep() {
const progress = this.getProgress();
return progress ? progress.currentStep : 1;
}
getStepData(step) {
const progress = this.getProgress();
return progress?.steps?.[step] || {};
}
clearProgress() {
sessionStorage.removeItem(this.storageKey);
}
submitForm() {
const progress = this.getProgress();
if (!progress) return null;
// 合并所有步骤的数据
const allData = Object.values(progress.steps).reduce(
(acc, step) => ({ ...acc, ...step }),
{}
);
// 提交后清除会话数据
this.clearProgress();
return allData;
}
}
// 使用示例
const registrationForm = new MultiStepForm("registration");
// 步骤1:基本信息
registrationForm.saveProgress(1, {
firstName: "Sarah",
lastName: "Johnson",
});
// 步骤2:联系方式
registrationForm.saveProgress(2, {
email: "[email protected]",
phone: "+1-234-567-8900",
});
// 用户刷新页面后,恢复进度
const currentStep = registrationForm.getCurrentStep();
console.log(`当前步骤: ${currentStep}`);
// 获取特定步骤的数据
const step1Data = registrationForm.getStepData(1);
console.log(step1Data); // { firstName: 'Sarah', lastName: 'Johnson' }localStorage vs sessionStorage
| 特性 | localStorage | sessionStorage |
|---|---|---|
| 生命周期 | 永久存储,直到手动清除 | 标签页/会话结束后清除 |
| 作用域 | 同源所有标签页共享 | 仅当前标签页 |
| 容量 | 通常 5-10MB | 通常 5-10MB |
| 服务器发送 | 不会自动发送 | 不会自动发送 |
选择指南
// 使用 localStorage 的场景
// ✅ 用户偏好设置
localStorage.setItem("theme", "dark");
localStorage.setItem("language", "zh-CN");
// ✅ 用户登录状态(配合安全策略)
localStorage.setItem("authToken", "xxx");
// ✅ 缓存不敏感数据
localStorage.setItem("cachedProducts", JSON.stringify(products));
// ✅ 记住用户选择
localStorage.setItem("rememberMe", "true");
// ---
// 使用 sessionStorage 的场景
// ✅ 单次会话的临时数据
sessionStorage.setItem("currentOrder", JSON.stringify(order));
// ✅ 表单草稿
sessionStorage.setItem("draftPost", JSON.stringify(postContent));
// ✅ 页面间传递一次性数据
sessionStorage.setItem("redirectReason", "login_required");
// ✅ 防止表单重复提交
sessionStorage.setItem("formSubmitted", "true");存储事件监听
当 localStorage 在其他标签页或窗口被修改时,可以通过 storage 事件监听这些变化:
window.addEventListener("storage", (event) => {
console.log("存储变化:", {
key: event.key, // 变更的键
oldValue: event.oldValue, // 旧值
newValue: event.newValue, // 新值
url: event.url, // 触发变更的页面 URL
storageArea: event.storageArea, // localStorage 或 sessionStorage
});
});
// 注意:storage 事件只在其他标签页/窗口修改时触发
// 当前页面修改 localStorage 不会触发自己的 storage 事件跨标签页通信
利用 storage 事件,可以实现简单的跨标签页通信:
class CrossTabMessenger {
constructor(channelName) {
this.channelName = channelName;
this.callbacks = new Set();
window.addEventListener("storage", (event) => {
if (event.key === this.channelName && event.newValue) {
const message = JSON.parse(event.newValue);
this.callbacks.forEach((callback) => callback(message));
}
});
}
send(data) {
const message = {
data,
timestamp: Date.now(),
tabId: this.tabId,
};
// 设置消息
localStorage.setItem(this.channelName, JSON.stringify(message));
// 立即删除,避免占用空间
localStorage.removeItem(this.channelName);
}
onMessage(callback) {
this.callbacks.add(callback);
return () => this.callbacks.delete(callback);
}
}
// 使用示例
const messenger = new CrossTabMessenger("app_messages");
// 监听消息
messenger.onMessage((message) => {
console.log("收到消息:", message.data);
});
// 发送消息(会被其他标签页收到)
messenger.send({ type: "logout", reason: "user_initiated" });实时同步用户状态
class UserStateSync {
constructor() {
this.stateKey = "user_state";
this.listeners = [];
// 监听其他标签页的状态变化
window.addEventListener("storage", (event) => {
if (event.key === this.stateKey) {
const newState = event.newValue ? JSON.parse(event.newValue) : null;
this.notifyListeners(newState, "external");
}
});
}
setState(state) {
localStorage.setItem(this.stateKey, JSON.stringify(state));
this.notifyListeners(state, "internal");
}
getState() {
const data = localStorage.getItem(this.stateKey);
return data ? JSON.parse(data) : null;
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter((l) => l !== listener);
};
}
notifyListeners(state, source) {
this.listeners.forEach((listener) => listener(state, source));
}
}
// 使用示例
const userSync = new UserStateSync();
// 订阅状态变化
userSync.subscribe((state, source) => {
if (source === "external") {
console.log("其他标签页更新了状态:", state);
// 更新当前页面的 UI
if (state?.isLoggedIn === false) {
// 用户在其他标签页登出
window.location.href = "/login";
}
}
});
// 登录后设置状态
userSync.setState({
isLoggedIn: true,
username: "Sarah",
lastActive: Date.now(),
});
// 登出时
function logout() {
userSync.setState({ isLoggedIn: false });
// 所有标签页都会收到通知并跳转到登录页
}存储限制与错误处理
Web Storage 有容量限制,当存储空间不足时会抛出错误:
// 检测存储容量
function getStorageSize(storage) {
let total = 0;
for (let key in storage) {
if (storage.hasOwnProperty(key)) {
total += key.length + storage.getItem(key).length;
}
}
return total;
}
console.log(`localStorage 已使用: ${getStorageSize(localStorage)} 字符`);
// 安全的存储写入
function safeSetItem(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return { success: true };
} catch (error) {
if (error.name === "QuotaExceededError") {
return { success: false, error: "QUOTA_EXCEEDED" };
}
return { success: false, error: error.message };
}
}
// 带自动清理的存储
function setItemWithCleanup(key, value, cleanupKeys = []) {
const result = safeSetItem(key, value);
if (!result.success && result.error === "QUOTA_EXCEEDED") {
// 尝试清理过期或不重要的数据
cleanupKeys.forEach((k) => localStorage.removeItem(k));
// 重试
return safeSetItem(key, value);
}
return result;
}带过期时间的存储
原生的 Web Storage 不支持过期时间,但可以自己实现:
class ExpiringStorage {
static set(key, value, ttlMs) {
const item = {
value,
expiry: ttlMs ? Date.now() + ttlMs : null,
};
localStorage.setItem(key, JSON.stringify(item));
}
static get(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) return null;
try {
const item = JSON.parse(itemStr);
// 检查是否过期
if (item.expiry && Date.now() > item.expiry) {
localStorage.removeItem(key);
return null;
}
return item.value;
} catch {
return null;
}
}
static remove(key) {
localStorage.removeItem(key);
}
// 清理所有过期项
static cleanup() {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
try {
const item = JSON.parse(localStorage.getItem(key));
if (item.expiry && Date.now() > item.expiry) {
keysToRemove.push(key);
}
} catch {
// 不是我们格式的数据,跳过
}
}
keysToRemove.forEach((key) => localStorage.removeItem(key));
return keysToRemove.length;
}
}
// 使用示例
// 缓存数据1小时
ExpiringStorage.set("apiResponse", { data: [1, 2, 3] }, 60 * 60 * 1000);
// 获取数据(如果过期返回 null)
const cached = ExpiringStorage.get("apiResponse");
// 定期清理过期数据
setInterval(() => {
const cleaned = ExpiringStorage.cleanup();
if (cleaned > 0) {
console.log(`清理了 ${cleaned} 个过期项`);
}
}, 60 * 1000); // 每分钟检查一次实际应用场景
用户偏好设置
class UserPreferences {
constructor() {
this.storageKey = "userPreferences";
this.defaults = {
theme: "light",
fontSize: "medium",
language: "en",
notifications: true,
sidebar: "expanded",
};
}
get(key) {
const prefs = this.getAll();
return prefs[key];
}
set(key, value) {
const prefs = this.getAll();
prefs[key] = value;
localStorage.setItem(this.storageKey, JSON.stringify(prefs));
// 触发变更事件
window.dispatchEvent(
new CustomEvent("preferencesChanged", {
detail: { key, value },
})
);
}
getAll() {
try {
const stored = localStorage.getItem(this.storageKey);
return stored
? { ...this.defaults, ...JSON.parse(stored) }
: this.defaults;
} catch {
return this.defaults;
}
}
reset() {
localStorage.removeItem(this.storageKey);
window.dispatchEvent(
new CustomEvent("preferencesChanged", {
detail: { reset: true },
})
);
}
}
// 使用示例
const prefs = new UserPreferences();
// 获取设置
console.log(prefs.get("theme")); // 'light'
// 更新设置
prefs.set("theme", "dark");
prefs.set("fontSize", "large");
// 监听设置变化
window.addEventListener("preferencesChanged", (event) => {
const { key, value, reset } = event.detail;
if (reset) {
console.log("设置已重置");
// 重新应用默认设置
} else {
console.log(`设置已更新: ${key} = ${value}`);
// 应用新设置
if (key === "theme") {
document.body.className = value;
}
}
});购物车功能
class ShoppingCart {
constructor() {
this.storageKey = "shopping_cart";
}
getItems() {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : [];
}
addItem(product, quantity = 1) {
const items = this.getItems();
const existingIndex = items.findIndex((item) => item.id === product.id);
if (existingIndex >= 0) {
items[existingIndex].quantity += quantity;
} else {
items.push({
id: product.id,
name: product.name,
price: product.price,
image: product.image,
quantity,
});
}
this.save(items);
return items;
}
updateQuantity(productId, quantity) {
const items = this.getItems();
const index = items.findIndex((item) => item.id === productId);
if (index >= 0) {
if (quantity <= 0) {
items.splice(index, 1);
} else {
items[index].quantity = quantity;
}
this.save(items);
}
return items;
}
removeItem(productId) {
const items = this.getItems().filter((item) => item.id !== productId);
this.save(items);
return items;
}
clear() {
localStorage.removeItem(this.storageKey);
}
save(items) {
localStorage.setItem(this.storageKey, JSON.stringify(items));
}
getTotal() {
const items = this.getItems();
return items.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
}
getItemCount() {
const items = this.getItems();
return items.reduce((count, item) => count + item.quantity, 0);
}
}
// 使用示例
const cart = new ShoppingCart();
// 添加商品
cart.addItem({ id: 1, name: "Wireless Mouse", price: 29.99 }, 2);
cart.addItem({ id: 2, name: "USB Keyboard", price: 49.99 }, 1);
// 获取购物车信息
console.log("商品数量:", cart.getItemCount()); // 3
console.log("总价:", cart.getTotal().toFixed(2)); // 109.97
// 更新数量
cart.updateQuantity(1, 3);
// 删除商品
cart.removeItem(2);最近浏览记录
class RecentlyViewed {
constructor(options = {}) {
this.storageKey = options.key || "recently_viewed";
this.maxItems = options.maxItems || 10;
}
add(item) {
const items = this.getAll();
// 移除已存在的相同项
const filtered = items.filter((i) => i.id !== item.id);
// 添加到开头
filtered.unshift({
...item,
viewedAt: Date.now(),
});
// 保持最大数量限制
const limited = filtered.slice(0, this.maxItems);
localStorage.setItem(this.storageKey, JSON.stringify(limited));
return limited;
}
getAll() {
try {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : [];
} catch {
return [];
}
}
clear() {
localStorage.removeItem(this.storageKey);
}
remove(id) {
const items = this.getAll().filter((item) => item.id !== id);
localStorage.setItem(this.storageKey, JSON.stringify(items));
return items;
}
}
// 使用示例
const recentlyViewed = new RecentlyViewed({ maxItems: 5 });
// 用户浏览产品
recentlyViewed.add({ id: 101, name: "Product A", price: 99.99 });
recentlyViewed.add({ id: 102, name: "Product B", price: 149.99 });
recentlyViewed.add({ id: 103, name: "Product C", price: 79.99 });
// 显示最近浏览
const recent = recentlyViewed.getAll();
console.log("最近浏览:", recent);安全注意事项
虽然 Web Storage 使用方便,但存在一些安全风险需要注意:
// ⚠️ 不要存储敏感信息
// 以下是错误示例:
localStorage.setItem("password", "mySecret123"); // ❌ 绝不要这样做
localStorage.setItem("creditCard", "4111111111111111"); // ❌ 绝不要这样做
// ⚠️ XSS 攻击可以访问 localStorage
// 如果网站存在 XSS 漏洞,攻击者可以执行:
// console.log(localStorage.getItem('authToken'));
// ✅ 安全建议:
// 1. 对存储的数据进行验证
function getValidatedItem(key, validator) {
try {
const value = JSON.parse(localStorage.getItem(key));
if (validator(value)) {
return value;
}
localStorage.removeItem(key); // 无效数据,清除
return null;
} catch {
return null;
}
}
// 2. 敏感操作使用 httpOnly Cookie,而不是 localStorage
// 3. 实现内容安全策略 (CSP) 防止 XSS
// 4. 对用户输入进行严格验证和清理总结
Web Storage API 为客户端数据存储提供了简洁而强大的解决方案。localStorage 适合持久化用户偏好、缓存数据等长期存储需求;sessionStorage 则适合临时数据、表单草稿等会话级别的存储场景。
关键要点:
- 只能存储字符串,复杂数据需要 JSON 序列化
localStorage跨标签页共享,sessionStorage各标签页隔离- 使用
storage事件可实现跨标签页通信 - 注意存储容量限制,做好错误处理
- 不要存储敏感信息,注意 XSS 防护
合理使用 Web Storage,可以显著提升用户体验,让你的 Web 应用更加智能和个性化。