客户端存储

现代 Web 应用需要存储数据在客户端,本地存储提供了多种解决方案。

基本用法

// 设置 Cookie
document.cookie = "name=John Doe; expires=Fri, 31 Dec 2023 23:59:59 GMT; path=/"

// 设置多个 Cookie
document.cookie = "userId=12345; max-age=86400; path=/; secure; samesite=strict"
document.cookie = "theme=dark; max-age=604800; path=/"

// 读取所有 Cookie
console.log(document.cookie)
// "name=John Doe; userId=12345; theme=dark"

// 解析 Cookie
function getCookie(name) {
  const cookies = document.cookie.split(';')
  for (let cookie of cookies) {
    const [cookieName, cookieValue] = cookie.trim().split('=')
    if (cookieName === name) {
      return decodeURIComponent(cookieValue)
    }
  }
  return null
}

console.log(getCookie('name')) // "John Doe"

// 设置 Cookie 工具函数
function setCookie(name, value, options = {}) {
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`

  if (options.expires) {
    cookie += `; expires=${options.expires.toUTCString()}`
  }

  if (options.maxAge) {
    cookie += `; max-age=${options.maxAge}`
  }

  if (options.path) {
    cookie += `; path=${options.path}`
  }

  if (options.domain) {
    cookie += `; domain=${options.domain}`
  }

  if (options.secure) {
    cookie += '; secure'
  }

  if (options.sameSite) {
    cookie += `; samesite=${options.sameSite}`
  }

  document.cookie = cookie
}

// 删除 Cookie
function deleteCookie(name, path = '/', domain) {
  let cookie = `${encodeURIComponent(name)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT`

  if (path) cookie += `; path=${path}`
  if (domain) cookie += `; domain=${domain}`

  document.cookie = cookie
}

// 使用示例
setCookie('sessionId', 'abc123', {
  maxAge: 3600,  // 1小时
  path: '/',
  secure: true,
  sameSite: 'strict'
})

console.log(getCookie('sessionId')) // "abc123"

deleteCookie('sessionId')
// 检查 Cookie 是否启用
function areCookiesEnabled() {
  // 尝试设置测试 Cookie
  document.cookie = "testcookie=test; max-age=1"

  // 检查是否设置成功
  const cookiesEnabled = document.cookie.indexOf("testcookie") !== -1

  // 清理测试 Cookie
  document.cookie = "testcookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT"

  return cookiesEnabled
}

console.log('Cookies enabled:', areCookiesEnabled())

// Cookie 大小限制(通常 4KB)
function getCookieSize() {
  const cookieString = document.cookie
  return new Blob([cookieString]).size
}

console.log('Cookie size:', getCookieSize(), 'bytes')

// Cookie 数量限制(通常 50-100 个)
function getCookieCount() {
  return document.cookie ? document.cookie.split(';').length : 0
}

console.log('Cookie count:', getCookieCount())

Web Storage

localStorage

// 存储数据
localStorage.setItem('name', 'John Doe')
localStorage.setItem('age', '30')
localStorage.setItem('user', JSON.stringify({ id: 1, email: 'john@example.com' }))

// 读取数据
const name = localStorage.getItem('name')
const age = localStorage.getItem('age')
const user = JSON.parse(localStorage.getItem('user'))

console.log(name, age, user)

// 检查键是否存在
console.log(localStorage.hasOwnProperty('name'))  // false (localStorage 不是普通对象)
console.log('name' in localStorage)                // false
console.log(localStorage.getItem('name') !== null) // true

// 获取所有键
for (let i = 0; i < localStorage.length; i++) {
  const key = localStorage.key(i)
  console.log(key, localStorage.getItem(key))
}

// 使用对象语法
localStorage.theme = 'dark'
console.log(localStorage.theme)

// 删除数据
localStorage.removeItem('age')
delete localStorage.theme  // 也可以使用 delete

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

// 存储复杂对象
const settings = {
  theme: 'dark',
  language: 'zh-CN',
  notifications: {
    email: true,
    push: false
  }
}

localStorage.setItem('settings', JSON.stringify(settings))

// 读取时需要解析
const savedSettings = JSON.parse(localStorage.getItem('settings'))
console.log(savedSettings.theme) // 'dark'

sessionStorage

// sessionStorage 的 API 与 localStorage 相同
sessionStorage.setItem('sessionId', 'abc123')
sessionStorage.setItem('lastActivity', Date.now())

console.log(sessionStorage.getItem('sessionId'))

// 页面刷新后数据仍然存在
// 关闭标签页后数据丢失

// 使用 sessionStorage 存储表单数据
function saveFormData(formId) {
  const form = document.getElementById(formId)
  const formData = new FormData(form)
  const data = {}

  for (let [key, value] of formData) {
    data[key] = value
  }

  sessionStorage.setItem(`form_${formId}`, JSON.stringify(data))
}

function loadFormData(formId) {
  const savedData = sessionStorage.getItem(`form_${formId}`)
  if (savedData) {
    const data = JSON.parse(savedData)
    const form = document.getElementById(formId)

    for (let key in data) {
      const input = form.elements[key]
      if (input) {
        input.value = data[key]
      }
    }
  }
}

// 自动保存表单数据
document.getElementById('myForm').addEventListener('input', () => {
  saveFormData('myForm')
})

// 页面加载时恢复数据
window.addEventListener('load', () => {
  loadFormData('myForm')
})

存储事件

// 监听存储变化
window.addEventListener('storage', (event) => {
  console.log('Storage changed:')
  console.log('Key:', event.key)
  console.log('Old value:', event.oldValue)
  console.log('New value:', event.newValue)
  console.log('Storage area:', event.storageArea === localStorage ? 'localStorage' : 'sessionStorage')
  console.log('URL:', event.url)
})

// 注意:storage 事件不会在设置数据的同一个页面触发
// 只会在其他标签页或窗口中触发

// 在其他窗口中修改存储
const newWindow = window.open('other-page.html')

// 在新窗口中设置数据会触发 storage 事件
setTimeout(() => {
  newWindow.localStorage.setItem('sharedData', 'updated value')
}, 1000)

IndexedDB

基本概念

// IndexedDB 是一个事务型数据库系统
// 特点:
// - 存储大量结构化数据
// - 异步操作
// - 支持索引
// - 支持事务
// - 同源限制

// 打开数据库
const request = indexedDB.open('MyDatabase', 1)

// 数据库版本升级
request.onupgradeneeded = function(event) {
  const db = event.target.result

  // 创建对象存储
  if (!db.objectStoreNames.contains('users')) {
    const userStore = db.createObjectStore('users', { keyPath: 'id' })
    userStore.createIndex('email', 'email', { unique: true })
    userStore.createIndex('name', 'name', { unique: false })
  }

  if (!db.objectStoreNames.contains('posts')) {
    const postStore = db.createObjectStore('posts', { keyPath: 'id', autoIncrement: true })
    postStore.createIndex('authorId', 'authorId', { unique: false })
    postStore.createIndex('createdAt', 'createdAt', { unique: false })
  }
}

request.onsuccess = function(event) {
  const db = event.target.result
  console.log('Database opened successfully')

  // 开始使用数据库
  performDatabaseOperations(db)
}

request.onerror = function(event) {
  console.error('Database error:', event.target.error)
}

CRUD 操作

function performDatabaseOperations(db) {
  // 添加数据
  function addUser(user) {
    const transaction = db.transaction(['users'], 'readwrite')
    const store = transaction.objectStore('users')

    const request = store.add(user)

    request.onsuccess = () => console.log('User added:', user.id)
    request.onerror = () => console.error('Failed to add user')
  }

  // 查询数据
  function getUser(userId) {
    const transaction = db.transaction(['users'], 'readonly')
    const store = transaction.objectStore('users')

    const request = store.get(userId)

    request.onsuccess = () => {
      if (request.result) {
        console.log('User found:', request.result)
      } else {
        console.log('User not found')
      }
    }
  }

  // 更新数据
  function updateUser(user) {
    const transaction = db.transaction(['users'], 'readwrite')
    const store = transaction.objectStore('users')

    const request = store.put(user)

    request.onsuccess = () => console.log('User updated:', user.id)
    request.onerror = () => console.error('Failed to update user')
  }

  // 删除数据
  function deleteUser(userId) {
    const transaction = db.transaction(['users'], 'readwrite')
    const store = transaction.objectStore('users')

    const request = store.delete(userId)

    request.onsuccess = () => console.log('User deleted:', userId)
    request.onerror = () => console.error('Failed to delete user')
  }

  // 查询所有数据
  function getAllUsers() {
    const transaction = db.transaction(['users'], 'readonly')
    const store = transaction.objectStore('users')

    const request = store.getAll()

    request.onsuccess = () => {
      console.log('All users:', request.result)
    }
  }

  // 使用索引查询
  function getUserByEmail(email) {
    const transaction = db.transaction(['users'], 'readonly')
    const store = transaction.objectStore('users')
    const index = store.index('email')

    const request = index.get(email)

    request.onsuccess = () => {
      console.log('User by email:', request.result)
    }
  }

  // 使用游标遍历
  function iterateUsers() {
    const transaction = db.transaction(['users'], 'readonly')
    const store = transaction.objectStore('users')
    const request = store.openCursor()

    request.onsuccess = (event) => {
      const cursor = event.target.result

      if (cursor) {
        console.log('User:', cursor.value)
        cursor.continue()  // 移动到下一条记录
      } else {
        console.log('No more users')
      }
    }
  }

  // 使用示例
  const user = { id: 1, name: 'John Doe', email: 'john@example.com' }
  addUser(user)

  setTimeout(() => {
    getUser(1)
    getAllUsers()
    iterateUsers()
  }, 100)
}

异步操作封装

// 将 IndexedDB 操作 Promise 化
class IndexedDBWrapper {
  constructor(dbName, version) {
    this.dbName = dbName
    this.version = version
    this.db = null
  }

  async open() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version)

      request.onupgradeneeded = (event) => {
        const db = event.target.result
        this.setupSchema(db)
      }

      request.onsuccess = (event) => {
        this.db = event.target.result
        resolve(this.db)
      }

      request.onerror = (event) => {
        reject(event.target.error)
      }
    })
  }

  setupSchema(db) {
    // 子类重写此方法
    throw new Error('setupSchema must be implemented by subclass')
  }

  async transaction(storeNames, mode = 'readonly') {
    if (!this.db) {
      throw new Error('Database not opened')
    }

    const transaction = this.db.transaction(storeNames, mode)
    return transaction
  }

  async get(storeName, key) {
    const transaction = await this.transaction([storeName])
    const store = transaction.objectStore(storeName)

    return new Promise((resolve, reject) => {
      const request = store.get(key)

      request.onsuccess = () => resolve(request.result)
      request.onerror = () => reject(request.error)
    })
  }

  async put(storeName, value) {
    const transaction = await this.transaction([storeName], 'readwrite')
    const store = transaction.objectStore(storeName)

    return new Promise((resolve, reject) => {
      const request = store.put(value)

      request.onsuccess = () => resolve(request.result)
      request.onerror = () => reject(request.error)
    })
  }

  async delete(storeName, key) {
    const transaction = await this.transaction([storeName], 'readwrite')
    const store = transaction.objectStore(storeName)

    return new Promise((resolve, reject) => {
      const request = store.delete(key)

      request.onsuccess = () => resolve()
      request.onerror = () => reject(request.error)
    })
  }
}

// 使用示例
class UserDatabase extends IndexedDBWrapper {
  constructor() {
    super('UserDatabase', 1)
  }

  setupSchema(db) {
    if (!db.objectStoreNames.contains('users')) {
      const store = db.createObjectStore('users', { keyPath: 'id' })
      store.createIndex('email', 'email', { unique: true })
    }
  }
}

async function demo() {
  const db = new UserDatabase()
  await db.open()

  // 添加用户
  await db.put('users', { id: 1, name: 'John', email: 'john@example.com' })

  // 获取用户
  const user = await db.get('users', 1)
  console.log(user)

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

demo()

总结

客户端存储为 Web 应用提供了丰富的数据持久化选项:

  1. Cookie:传统的小容量存储,支持服务器读取
  2. Web Storage:localStorage 和 sessionStorage,简单的键值存储
  3. IndexedDB:强大的客户端数据库,支持复杂查询和大量数据

选择合适的存储方案取决于数据量、复杂度和持久化需求。