{T}

本地存储技术

随着前端应用复杂度的提升,本地存储技术从 Cookie 演进到 Web Storage 再到 IndexedDB,各有所长。本章系统梳理浏览器本地存储的技术原理、API 使用与选型策略。

存储技术演进

code
Cookie(1994)→ Web Storage(2008)→ IndexedDB(2011)→ Cache API(2015)
  4KB              5-10MB               250MB+             无限制
技术容量生命周期与服务端通信数据类型
Cookie≤ 4KB可设过期时间✅ 自动携带字符串
localStorage5-10MB永久(手动删除)字符串
sessionStorage5-10MB会话级(Tab 关闭即清除)字符串
IndexedDB250MB+永久(手动删除)结构化数据 + 二进制
Cache API无限制永久(手动删除)HTTP 请求/响应

设计初衷

Cookie 的本职工作是维持状态(Session 管理),而非存储数据。HTTP 协议是无状态的,Cookie 通过在请求中携带键值对来标识客户端身份。网站登录后"不用重复登录"的现象,正是 Cookie 存储和传输 token 的典型应用。

Token 存储方式

客户端存储 token 有两种方式:

服务端自动植入

服务端通过 Set-Cookie 响应头将 token 植入浏览器 Cookie:

http
Set-Cookie: token=abc123; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Lax

之后同域下的所有请求,浏览器会自动携带 Cookie,前端无需关心 token 的存取。

前端手动存储

服务端将 token 通过响应体返回,前端手动存储到 localStorage 等:

javascript
import axios from 'axios'

const http = (params) => {
  const instance = axios.create({ baseURL: 'https://example.com' })
  const token = localStorage.getItem('token')

  return instance({
    url: '/api/data',
    method: 'post',
    data: params,
    headers: { 'x-token': token }  // 手动设置请求头
  })
}

两种方式对比

方式优点缺点
服务端植入自动携带、HttpOnly 防 XSS仅浏览器环境可用
前端手动存储跨环境可用(APP/小程序)需手动管理存取

性能劣势

问题说明
容量限制单个 Cookie 约 4KB,总量有限
性能开销每次 HTTP 请求都会自动携带 Cookie,增大请求体积
安全风险若被 XSS 窃取,会导致身份泄露(需配合 HttpOnly)
传输浪费无关请求也携带 Cookie,造成带宽浪费

SameSite 属性

SameSite 用于控制 Cookie 跨站携带策略,防止 CSRF:

  • Strict:严格同站,跨站不携带
  • Lax:默认值,部分跨站请求(如导航)携带
  • None:允许跨站携带,必须配合 Secure

原生 API 操作不够便捷:

code
// 设置
document.cookie = 'name=juejin; path=/; max-age=3600; SameSite=Lax; Secure'

// 读取(返回所有 Cookie 字符串,需手动解析)
console.log(document.cookie)

// 删除(设置过期时间)
document.cookie = 'name=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'

推荐使用 js-cookie 封装:

javascript
import Cookies from 'js-cookie'

Cookies.set('name', 'juejin', { domain: 'example.com' })
Cookies.get('name')       // 'juejin'
Cookies.remove('name')

Web Storage

Web Storage 是 HTML5 提供的浏览器端数据存储机制,分为 localStoragesessionStorage,API 完全一致。

localStorage vs sessionStorage

维度localStoragesessionStorage
生命周期永久(除非手动删除)会话级别(标签页关闭即清除)
作用域同源策略(同协议+域名+端口共享)同源 + 仅限同一窗口/标签页
容量5-10 MB5-10 MB
与服务端通信不通信不通信
跨 Tab 共享

核心 API

javascript
// 存储数据
localStorage.setItem('user_name', 'xiuyan')
localStorage.setItem('user_info', JSON.stringify({ id: 1, role: 'admin' }))

// 读取数据
const name = localStorage.getItem('user_name')
const info = JSON.parse(localStorage.getItem('user_info'))

// 删除某条数据
localStorage.removeItem('user_name')

// 清空所有数据
localStorage.clear()

// 获取存储数量
console.log(localStorage.length)

// 按 index 获取 key
console.log(localStorage.key(0))

数据类型陷阱

Web Storage 存储的数据都会被转为字符串

javascript
localStorage.setItem('age', 18)
localStorage.getItem('age')  // '18'(字符串,非数字)

// 存储对象不序列化 → [object Object]
localStorage.setItem('obj', { a: 1 })
localStorage.getItem('obj')  // '[object Object]'

// 正确做法:序列化存储
localStorage.setItem('obj', JSON.stringify({ a: 1 }))
JSON.parse(localStorage.getItem('obj'))  // { a: 1 }

封装方案

封装 localStorage 可以增加过期时间管理和自动序列化/反序列化:

javascript
const storage = {
  set(key, value, duration) {
    const data = {
      value,
      expiryTime: !duration || isNaN(duration)
        ? 0
        : Date.now() + parseInt(duration)
    }
    localStorage.setItem(key, JSON.stringify(data))
  },

  get(key) {
    const data = JSON.parse(localStorage.getItem(key))
    if (data && data.expire) {
      // 过期则删除并返回 null
      if (Date.now() > data.expire) {
        localStorage.removeItem(key)
        return null
      }
      return data.value
    }
    return data
  }
}

// 使用
storage.set('userinfo', { name: 'juejin', age: 18 }, 3600000)  // 1小时过期
storage.get('userinfo')  // { name: 'juejin', age: 18 }

也可使用 npm 包 web-storage-cache

监听变化

同源下的其他标签页/窗口可以监听到 Storage 变化(当前页面自身修改不会触发):

javascript
window.addEventListener('storage', (event) => {
  console.log(`Key: ${event.key}`)
  console.log(`Old Value: ${event.oldValue}`)
  console.log(`New Value: ${event.newValue}`)
  console.log(`URL: ${event.url}`)
  console.log(`Storage: ${event.storageArea}`)
})

应用场景

存储类型适用场景
localStorage用户偏好设置、主题配置、不常更新的静态数据、Base64 图片缓存
sessionStorage表单临时数据、页面状态恢复、浏览足迹、分页信息、单次会话的认证态

IndexedDB

IndexedDB 是浏览器内置的非关系型数据库(NoSQL),支持存储结构化数据和二进制大文件,是浏览器端最强大的存储方案。

核心特性

特性说明
存储容量理论无上限(通常 > 250MB)
数据类型字符串、对象、ArrayBuffer、Blob 等
支持操作增删改查、索引、事务、游标
异步操作基于 DOM 事件或 Promise
同源限制遵循同源策略

数据库结构

code
IndexedDB
├── Database(数据库)
│   └── Object Store(对象仓库,类似"表")
│       ├── Key(主键,唯一标识记录)
│       ├── Value(存储的数据,可以是任意结构化数据)
│       └── Index(索引,加速查询)
└── Transaction(事务,保证操作的原子性)

基础使用

打开/创建数据库

javascript
const request = indexedDB.open('myDatabase', 1)

request.onerror = () => console.error('IndexedDB 打开失败')

// 版本变化时触发(新建或版本号升级)
request.onupgradeneeded = (event) => {
  const db = event.target.result
  if (!db.objectStoreNames.contains('users')) {
    const store = db.createObjectStore('users', {
      keyPath: 'id',
      autoIncrement: true
    })
    // 创建索引(索引名、索引属性、配置)
    store.createIndex('nameIndex', 'name', { unique: false })
    store.createIndex('emailIndex', 'email', { unique: true })
  }
}

request.onsuccess = (event) => {
  const db = event.target.result
  console.log('数据库打开成功')
}

写入数据

javascript
function addData(db, data) {
  const transaction = db.transaction(['users'], 'readwrite')
  const store = transaction.objectStore('users')
  const request = store.add(data)

  request.onsuccess = () => console.log('写入成功')
  request.onerror = () => console.error('写入失败')

  transaction.oncomplete = () => console.log('事务完成')
}

addData(db, { name: '张三', email: 'zhangsan@example.com', age: 25 })

读取数据

javascript
// 按主键查询
function getData(db, id) {
  const transaction = db.transaction(['users'], 'readonly')
  const store = transaction.objectStore('users')
  const request = store.get(id)

  request.onsuccess = () => {
    console.log('查询结果:', request.result)
  }
}

// 按索引查询
function getByIndex(db, name) {
  const transaction = db.transaction(['users'], 'readonly')
  const store = transaction.objectStore('users')
  const index = store.index('nameIndex')
  const request = index.get(name)

  request.onsuccess = () => {
    console.log('索引查询结果:', request.result)
  }
}

更新与删除

javascript
// 更新(put 方法:存在则更新,不存在则新增)
function updateData(db, data) {
  const transaction = db.transaction(['users'], 'readwrite')
  const store = transaction.objectStore('users')
  store.put(data)
}

// 删除
function deleteData(db, id) {
  const transaction = db.transaction(['users'], 'readwrite')
  const store = transaction.objectStore('users')
  store.delete(id)
}

使用游标遍历

javascript
function cursorGetAll(db) {
  const transaction = db.transaction(['users'], 'readonly')
  const store = transaction.objectStore('users')
  const request = store.openCursor()
  const results = []

  request.onsuccess = (event) => {
    const cursor = event.target.result
    if (cursor) {
      results.push(cursor.value)
      cursor.continue()  // 移动到下一条
    } else {
      console.log('所有数据:', results)
    }
  }
}

使用 idb 库简化操作

原生 IndexedDB API 基于事件回调,使用不便。推荐使用 idb 库,将 API 转为 Promise 风格:

javascript
import { openDB } from 'idb'

const db = await openDB('myDatabase', 1, {
  upgrade(db) {
    db.createObjectStore('users', { keyPath: 'id' })
  }
})

// 写入
await db.add('users', { id: 1, name: '张三', email: 'zhangsan@example.com' })

// 读取
const user = await db.get('users', 1)

// 更新
await db.put('users', { id: 1, name: '李四', email: 'lisi@example.com' })

// 删除
await db.delete('users', 1)

// 遍历
const allUsers = await db.getAll('users')

应用场景

场景说明
离线应用PWA 中存储离线所需的大量结构化数据
大文件管理存储 Blob 数据(图片、视频、文档等)
复杂状态需要索引和查询的复杂数据模型
缓存层作为 Service Worker 的持久化缓存后端
草稿保存富文本编辑器内容自动保存

浏览器兼容性

IndexedDB 在现代浏览器中支持良好,但部分旧版浏览器存在兼容问题。使用时应遵循渐进增强原则,先检测支持情况:

javascript
if (!('indexedDB' in window)) {
  console.log('浏览器不支持 IndexedDB')
  // 降级到 Web Storage
}

存储方案选型

决策流程

code
需要存储什么?
├── 少量文本(< 4KB)+ 服务端需读取 → Cookie
│   └── 需防 XSS → HttpOnly + Secure + SameSite
├── 简单键值对(< 10MB)+ 不需要服务端 → Web Storage
│   ├── 需要跨会话保持 → localStorage
│   └── 仅当前会话 → sessionStorage
├── 结构化数据 / 大文件 / 需要索引查询 → IndexedDB
└── HTTP 缓存替代方案 → Cache API(配合 Service Worker)

选型对比

维度CookielocalStoragesessionStorageIndexedDB
容量≤ 4KB5-10MB5-10MB250MB+
生命周期可配置永久会话级永久
数据类型字符串字符串字符串结构化 + 二进制
网络传输自动携带不传输不传输不传输
查询能力key 查询key 查询索引 + 游标
事务支持
异步 API
跨 Tab✅(同源)✅(同源)✅(同源)

最佳实践

  1. Cookie:仅用于身份认证和少量必须传递给服务端的数据,始终设置 HttpOnlySecureSameSite
  2. localStorage:适合持久化的用户偏好、配置信息;注意序列化/反序列化和容量控制
  3. sessionStorage:适合临时性数据,如表单暂存、页面状态恢复
  4. IndexedDB:适合大数据量、结构化存储场景;推荐使用 idb 库简化操作
  5. Cache API:配合 Service Worker 实现 HTTP 缓存,适合离线应用
  6. 敏感数据:任何本地存储都不应存放密码、密钥等敏感信息
  7. 容量管理:定期清理过期数据,避免占用过多存储空间