Skip to content

本地存储与会话存储:浏览器中的数据保险箱

客户端存储的演进

在 Web 开发的早期,如果你想在用户的浏览器中存储一点数据,Cookie 几乎是唯一的选择。但 Cookie 有诸多限制:容量小(约 4KB)、每次请求都会发送到服务器、操作 API 繁琐。

HTML5 带来了 Web Storage API,它像是为浏览器定制的小型数据库——容量更大(通常 5-10MB)、操作简单、不会自动发送到服务器。这让客户端数据存储变得优雅而高效。

Web Storage 分为两种:localStorage 提供持久化存储,数据会一直保留直到被明确删除;sessionStorage 则与会话绑定,关闭标签页或浏览器后数据就会消失。

localStorage:持久化的数据仓库

localStorage 是你在浏览器中的永久储物柜。只要用户不主动清理浏览器数据,存储的内容就会一直保留。

基础操作

javascript
// 存储数据
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);

你也可以使用点符号或方括号语法直接访问,但这不是推荐的做法:

javascript
// 可行但不推荐
localStorage.username = "Michael";
console.log(localStorage.username);

// 这种方式有风险
localStorage.setItem = "oops"; // 这会覆盖 setItem 方法!

// 始终使用 API 方法更安全
localStorage.setItem("setItem", "safe"); // 正确的方式

存储复杂数据

localStorage 只能存储字符串。如果你试图存储其他类型的数据,它们会被自动转换为字符串:

javascript
// 数字会被转换为字符串
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:

javascript
// 存储对象
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 可能会因为数据损坏而抛出错误,建议封装一个安全的读取函数:

javascript
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

遍历所有数据

javascript
// 方法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 完全相同,区别在于数据的生命周期——它只在当前会话(标签页)中有效。

基本特性

javascript
// 存储数据
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 中"会话"的含义很重要:

javascript
// 每个标签页都有独立的 sessionStorage
// 同一网站在不同标签页打开,它们的 sessionStorage 是隔离的

// 标签页 A
sessionStorage.setItem("tabId", "A");

// 标签页 B(即使是同一网站)
sessionStorage.setItem("tabId", "B");

// 各自读取自己的数据
// 标签页 A 读取到 'A'
// 标签页 B 读取到 'B'

// 刷新页面不会清除 sessionStorage
// 关闭标签页会清除 sessionStorage
// 使用"恢复标签页"功能可能会恢复 sessionStorage(浏览器实现不同)

实际应用:多步骤表单

javascript
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

特性localStoragesessionStorage
生命周期永久存储,直到手动清除标签页/会话结束后清除
作用域同源所有标签页共享仅当前标签页
容量通常 5-10MB通常 5-10MB
服务器发送不会自动发送不会自动发送

选择指南

javascript
// 使用 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 事件监听这些变化:

javascript
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 事件,可以实现简单的跨标签页通信:

javascript
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" });

实时同步用户状态

javascript
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 有容量限制,当存储空间不足时会抛出错误:

javascript
// 检测存储容量
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 不支持过期时间,但可以自己实现:

javascript
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); // 每分钟检查一次

实际应用场景

用户偏好设置

javascript
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;
    }
  }
});

购物车功能

javascript
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);

最近浏览记录

javascript
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 使用方便,但存在一些安全风险需要注意:

javascript
// ⚠️ 不要存储敏感信息
// 以下是错误示例:
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 应用更加智能和个性化。