浏览器存储方案完全指南

Created on

前端开发中,将数据持久化到浏览器是一个高频需求。但浏览器提供了多种存储方案,面对 Cookie、localStorage、sessionStorage、IndexedDB、Cache API,如何选择?本文做一次系统梳理。

快速对比

方案容量生命周期可以被服务端读取同步/异步适合存储
Cookie4KB可设置过期时间✅ 每次请求自动携带同步身份凭证、少量数据
localStorage~5MB永久(手动清除)同步用户偏好、持久化数据
sessionStorage~5MB标签页关闭即清除同步临时表单数据、页面状态
IndexedDB~250MB+永久(手动清除)异步大量结构化数据、文件
Cache API受磁盘限制永久(手动清除)异步HTTP 响应缓存(PWA)

Cookie 是最古老的浏览器存储机制,核心特点是每次 HTTP 请求都会自动携带,因此主要用于身份认证。

基础操作

// 写入
document.cookie = "username=Alice; max-age=86400; path=/"; // max-age 单位秒

// 读取(返回所有 cookie 拼成的字符串,需要自己解析)
document.cookie; // "username=Alice; theme=dark"

// 封装成工具函数更好用
const Cookie = {
  set(key, value, days = 7) {
    const expires = new Date(Date.now() + days * 864e5).toUTCString();
    document.cookie = `${key}=${encodeURIComponent(
      value
    )}; expires=${expires}; path=/`;
  },
  get(key) {
    const match = document.cookie.match(new RegExp(`(?:^|; )${key}=([^;]*)`));
    return match ? decodeURIComponent(match[1]) : null;
  },
  remove(key) {
    document.cookie = `${key}=; max-age=0; path=/`;
  },
};

Cookie.set("theme", "dark");
Cookie.get("theme"); // "dark"
Cookie.remove("theme");

安全属性

// HttpOnly:禁止 JS 读取(只能服务端设置),防止 XSS 盗取
// Set-Cookie: session=xxx; HttpOnly

// Secure:只在 HTTPS 下传输
// Set-Cookie: session=xxx; Secure

// SameSite:防止 CSRF 攻击
// SameSite=Strict:完全禁止跨站携带
// SameSite=Lax:导航跳转时携带,第三方请求不携带(推荐默认值)
// SameSite=None:始终携带(需配合 Secure)

使用建议:Cookie 应专注于存储服务端需要读取的数据(如 session token),前端偏好数据用 localStorage 更合适。

localStorage

localStorage 是前端最常用的持久化存储,关闭浏览器后数据依然存在。

基础操作

// 写入(值必须是字符串,对象需序列化)
localStorage.setItem("user", JSON.stringify({ name: "Alice", role: "admin" }));

// 读取
const user = JSON.parse(localStorage.getItem("user") ?? "null");

// 删除
localStorage.removeItem("user");

// 清空当前域名下所有数据
localStorage.clear();

// 封装:支持任意类型,附带过期时间
const storage = {
  set(key, value, ttl) {
    const data = { value, expires: ttl ? Date.now() + ttl : null };
    localStorage.setItem(key, JSON.stringify(data));
  },
  get(key) {
    const raw = localStorage.getItem(key);
    if (!raw) return null;
    const { value, expires } = JSON.parse(raw);
    if (expires && Date.now() > expires) {
      localStorage.removeItem(key);
      return null;
    }
    return value;
  },
  remove(key) {
    localStorage.removeItem(key);
  },
};

// 存储 token,1小时过期
storage.set("token", "eyJhbG...", 60 * 60 * 1000);
storage.get("token"); // 1小时内有效,过期自动返回 null

跨标签页通信

localStorage 有一个鲜为人知的功能:在同一域名的其他标签页修改数据时,当前标签页会触发 storage 事件

// 标签页 A 修改数据
localStorage.setItem("count", "42");

// 标签页 B 监听变化
window.addEventListener("storage", (event) => {
  if (event.key === "count") {
    console.log(`count 从 ${event.oldValue} 变为 ${event.newValue}`);
    // 可以用来实现跨标签同步登录状态、主题等
  }
});

注意:当前标签页修改 localStorage 不会触发自身的 storage 事件,只触发其他标签页的。

sessionStorage

sessionStorage 与 localStorage API 完全一致,区别在于生命周期:关闭标签页或浏览器后数据就消失

适用场景

// 保存多步骤表单进度
function saveStepData(step, data) {
  sessionStorage.setItem(`form_step_${step}`, JSON.stringify(data));
}

function getStepData(step) {
  return JSON.parse(sessionStorage.getItem(`form_step_${step}`) ?? "null");
}

function clearForm() {
  // 表单完成后清除所有步骤数据
  for (let i = 1; i <= 3; i++) {
    sessionStorage.removeItem(`form_step_${i}`);
  }
}

IndexedDB

IndexedDB 是一个完整的客户端数据库,支持索引、事务、游标,适合存储大量结构化数据。原生 API 比较繁琐,推荐用封装库。

原生 API 示意

// 1. 打开数据库(异步)
const request = indexedDB.open("MyDB", 1);

// 2. 数据库结构升级(首次创建或版本更新时触发)
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  // 创建 "products" 对象仓库,主键为 id
  const store = db.createObjectStore("products", {
    keyPath: "id",
    autoIncrement: true,
  });
  // 创建索引,加速按 name 查询
  store.createIndex("name", "name", { unique: false });
};

// 3. 读写数据
request.onsuccess = (event) => {
  const db = event.target.result;

  // 写入
  const tx = db.transaction("products", "readwrite");
  tx.objectStore("products").add({ name: "键盘", price: 399 });

  // 读取
  const getTx = db.transaction("products", "readonly");
  getTx.objectStore("products").get(1).onsuccess = (e) => {
    console.log(e.target.result);
  };
};

推荐使用 idb 库

idb 是 Jake Archibald 写的 IndexedDB Promise 封装,极大简化了 API:

import { openDB } from "idb";

const db = await openDB("MyDB", 1, {
  upgrade(db) {
    const store = db.createObjectStore("products", {
      keyPath: "id",
      autoIncrement: true,
    });
    store.createIndex("name", "name");
  },
});

// 写入
await db.add("products", { name: "键盘", price: 399 });
await db.put("products", { id: 1, name: "机械键盘", price: 599 }); // 更新

// 读取
const product = await db.get("products", 1);
const all = await db.getAll("products");

// 按索引查询
const byName = await db.getAllFromIndex("products", "name", "键盘");

// 删除
await db.delete("products", 1);

适用场景

Cache API

Cache API 专为 Service Worker 设计,用于缓存 HTTP 请求和响应,是 PWA(渐进式 Web 应用)的核心技术。

// Service Worker 中缓存静态资源
const CACHE_NAME = "my-app-v1";
const ASSETS = ["/", "/index.html", "/app.js", "/style.css"];

// 安装时预缓存
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))
  );
});

// 请求时:缓存优先策略
self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      // 有缓存直接返回,同时后台更新
      if (cached) {
        fetch(event.request).then((response) => {
          caches.open(CACHE_NAME).then((cache) => {
            cache.put(event.request, response);
          });
        });
        return cached;
      }
      // 没有缓存则请求网络
      return fetch(event.request);
    })
  );
});

如何选择?

根据实际需求选择存储方案:

需要服务端读取?
├── 是 → Cookie(HttpOnly + Secure + SameSite)
└── 否 → 前端自己管理

    ├── 大量结构化数据 / 需要查询 / 要存文件?
    │   └── IndexedDB(用 idb 库)

    ├── 缓存 HTTP 响应 / PWA 离线?
    │   └── Cache API

    ├── 关闭标签页就失效?
    │   └── sessionStorage

    └── 其他(用户偏好、token、持久化简单数据)
        └── localStorage(注意 5MB 上限)

常见注意事项

  1. localStorage/sessionStorage 是同步的:大量读写会阻塞主线程,避免在循环中频繁操作

  2. 存储配额不能假定:虽然通常有 5MB,但用户可以清除,代码要做好容错处理

  3. 不要存储敏感信息:localStorage 可被同域 JS 读取,XSS 攻击可以窃取数据,token 最好存 HttpOnly Cookie

  4. 私有浏览/无痕模式:存储可能有限制,Safari 下 localStorage 在私有模式下写入会抛出异常

// 安全的 localStorage 写入(兼容异常情况)
function safeSetItem(key, value) {
  try {
    localStorage.setItem(key, value);
  } catch (e) {
    // 满了或私有模式被禁用
    console.warn("localStorage 写入失败:", e);
  }
}