晋江市本地咨询网

Vue.js 中 LocalStorage 与 SessionStorage操作示例最佳实践

2026-03-29 16:26:02 浏览次数:2
详细信息

Vue.js 中 LocalStorage 与 SessionStorage 最佳实践

核心区别

特性 LocalStorage SessionStorage
存储周期 永久存储,除非手动清除 会话级别,标签页关闭时清除
作用域 同源标签页间共享 仅当前标签页可用
存储大小 通常 5-10MB 通常 5-10MB
适用场景 用户偏好设置、登录状态等 临时表单数据、页面状态等

封装 Storage 工具类

// utils/storage.js
class Storage {
  constructor(type) {
    if (type === 'local') {
      this.storage = localStorage
    } else if (type === 'session') {
      this.storage = sessionStorage
    } else {
      throw new Error('Storage type must be "local" or "session"')
    }
  }

  /**
   * 设置存储项
   * @param {string} key - 存储键名
   * @param {any} value - 存储值(会自动序列化)
   * @param {number} expires - 过期时间(毫秒)
   */
  set(key, value, expires = null) {
    const item = {
      data: value,
      timestamp: Date.now(),
      expires: expires ? Date.now() + expires : null
    }

    try {
      this.storage.setItem(key, JSON.stringify(item))
    } catch (e) {
      if (e.name === 'QuotaExceededError') {
        console.error('存储空间不足,正在清理过期数据...')
        this.clearExpired()
        // 重试一次
        this.storage.setItem(key, JSON.stringify(item))
      } else {
        throw e
      }
    }
  }

  /**
   * 获取存储项
   * @param {string} key - 存储键名
   * @param {any} defaultValue - 默认值
   * @returns {any} 存储值或默认值
   */
  get(key, defaultValue = null) {
    const itemStr = this.storage.getItem(key)

    if (!itemStr) return defaultValue

    try {
      const item = JSON.parse(itemStr)

      // 检查是否过期
      if (item.expires && Date.now() > item.expires) {
        this.remove(key)
        return defaultValue
      }

      return item.data
    } catch (e) {
      console.error(`解析存储项 ${key} 失败:`, e)
      return defaultValue
    }
  }

  /**
   * 删除存储项
   * @param {string} key - 存储键名
   */
  remove(key) {
    this.storage.removeItem(key)
  }

  /**
   * 清空所有存储项
   */
  clear() {
    this.storage.clear()
  }

  /**
   * 获取所有键名
   * @returns {string[]}
   */
  keys() {
    return Object.keys(this.storage)
  }

  /**
   * 检查键是否存在
   * @param {string} key - 存储键名
   * @returns {boolean}
   */
  has(key) {
    return this.storage.getItem(key) !== null
  }

  /**
   * 清理过期数据
   */
  clearExpired() {
    const keys = this.keys()
    keys.forEach(key => {
      this.get(key) // 触发过期检查
    })
  }
}

// 创建实例
export const localStore = new Storage('local')
export const sessionStore = new Storage('session')

Vue 3 Composition API 示例

<!-- composables/useStorage.js -->
import { ref, watchEffect, onUnmounted } from 'vue'
import { localStore, sessionStore } from '@/utils/storage'

/**
 * 响应式 Storage Hook
 */
export function useLocalStorage(key, initialValue, options = {}) {
  const { 
    expires = null,
    onExpired = null,
    deepWatch = false 
  } = options

  // 从存储中读取初始值
  const storedValue = localStore.get(key)
  const data = ref(storedValue !== null ? storedValue : initialValue)

  // 保存到存储
  const saveToStorage = () => {
    if (data.value === undefined || data.value === null) {
      localStore.remove(key)
    } else {
      localStore.set(key, data.value, expires)
    }
  }

  // 监听数据变化自动保存
  watchEffect(() => {
    saveToStorage()
  }, { flush: 'post' })

  // 监听 storage 事件(跨标签页同步)
  const handleStorageChange = (e) => {
    if (e.key === key && e.storageArea === localStorage) {
      const newValue = localStore.get(key, initialValue)
      if (JSON.stringify(data.value) !== JSON.stringify(newValue)) {
        data.value = newValue
      }
    }
  }

  window.addEventListener('storage', handleStorageChange)

  // 清理监听器
  onUnmounted(() => {
    window.removeEventListener('storage', handleStorageChange)
  })

  return {
    data,
    save: saveToStorage,
    clear: () => {
      localStore.remove(key)
      data.value = initialValue
    }
  }
}

/**
 * 会话级存储 Hook
 */
export function useSessionStorage(key, initialValue) {
  const storedValue = sessionStore.get(key)
  const data = ref(storedValue !== null ? storedValue : initialValue)

  watchEffect(() => {
    if (data.value === undefined || data.value === null) {
      sessionStore.remove(key)
    } else {
      sessionStore.set(key, data.value)
    }
  }, { flush: 'post' })

  return {
    data,
    clear: () => {
      sessionStore.remove(key)
      data.value = initialValue
    }
  }
}

Vue 组件使用示例

<template>
  <div class="user-settings">
    <!-- 用户偏好设置示例 -->
    <h2>用户设置</h2>

    <div class="form-group">
      <label>
        <input 
          type="checkbox" 
          v-model="theme.darkMode"
          @change="saveTheme"
        >
        深色模式
      </label>
    </div>

    <div class="form-group">
      <label>语言设置:</label>
      <select v-model="theme.language" @change="saveTheme">
        <option value="zh-CN">中文</option>
        <option value="en-US">英文</option>
        <option value="ja-JP">日文</option>
      </select>
    </div>

    <!-- 临时表单数据示例 -->
    <h3>临时表单</h3>
    <div class="form-group">
      <label>草稿内容:</label>
      <textarea 
        v-model="draftContent" 
        @input="saveDraft"
        placeholder="输入内容,关闭页面后仍会保留"
      ></textarea>
    </div>

    <!-- 登录状态示例 -->
    <div class="auth-section">
      <button @click="login" v-if="!isLoggedIn">登录</button>
      <button @click="logout" v-else>退出登录</button>
    </div>

    <!-- 数据管理 -->
    <div class="actions">
      <button @click="clearDraft">清空草稿</button>
      <button @click="clearAll">清除所有本地数据</button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { 
  useLocalStorage, 
  useSessionStorage,
  localStore,
  sessionStore
} from '@/composables/useStorage'

// 1. 使用 LocalStorage 存储用户偏好设置
const theme = useLocalStorage('user-theme', {
  darkMode: false,
  language: 'zh-CN',
  fontSize: 14
}, { expires: 30 * 24 * 60 * 60 * 1000 }) // 30天过期

// 2. 使用 SessionStorage 存储临时表单数据
const draft = useSessionStorage('form-draft', {
  content: '',
  lastSaved: null
})

const draftContent = computed({
  get: () => draft.data.value.content,
  set: (value) => {
    draft.data.value.content = value
    draft.data.value.lastSaved = new Date().toISOString()
  }
})

const saveDraft = () => {
  // 自动保存,通过 watchEffect 实现
}

// 3. 手动操作示例 - 登录状态
const userToken = useLocalStorage('auth-token', null)
const isLoggedIn = computed(() => !!userToken.data.value)

const login = () => {
  // 模拟登录
  const fakeToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
  userToken.data.value = fakeToken

  // 记录登录时间
  localStore.set('login-time', Date.now())

  // 存储用户信息
  const userInfo = {
    id: 1,
    name: '张三',
    email: 'zhangsan@example.com',
    role: 'user'
  }
  localStore.set('user-info', userInfo)
}

const logout = () => {
  userToken.data.value = null
  localStore.remove('user-info')
  localStore.remove('login-time')
  sessionStore.clear() // 清除所有会话数据
}

// 4. 其他操作
const clearDraft = () => {
  draft.clear()
}

const clearAll = () => {
  if (confirm('确定要清除所有本地存储的数据吗?')) {
    localStorage.clear()
    location.reload()
  }
}

// 组件挂载时初始化
onMounted(() => {
  // 检查并清理过期数据
  localStore.clearExpired()

  // 恢复表单草稿
  const savedDraft = draft.data.value
  if (savedDraft?.lastSaved) {
    console.log('草稿恢复:', savedDraft.lastSaved)
  }
})
</script>

<style scoped>
.user-settings {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.form-group {
  margin: 15px 0;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

.form-group input[type="checkbox"] {
  margin-right: 8px;
}

.form-group select,
.form-group textarea {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.form-group textarea {
  min-height: 100px;
  resize: vertical;
}

.actions {
  margin-top: 30px;
  padding-top: 20px;
  border-top: 1px solid #eee;
}

.actions button {
  margin-right: 10px;
  padding: 8px 16px;
  background: #f5f5f5;
  border: 1px solid #ddd;
  border-radius: 4px;
  cursor: pointer;
}

.actions button:hover {
  background: #e5e5e5;
}

.auth-section button {
  padding: 10px 20px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.auth-section button:hover {
  background: #0056b3;
}
</style>

最佳实践总结

1. 安全性考虑

// 敏感数据应加密存储
import CryptoJS from 'crypto-js'

const SECRET_KEY = 'your-secret-key'

export const secureStorage = {
  set(key, value) {
    const encrypted = CryptoJS.AES.encrypt(
      JSON.stringify(value), 
      SECRET_KEY
    ).toString()
    localStorage.setItem(key, encrypted)
  },

  get(key) {
    const encrypted = localStorage.getItem(key)
    if (!encrypted) return null

    try {
      const decrypted = CryptoJS.AES.decrypt(encrypted, SECRET_KEY)
      return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
    } catch {
      return null
    }
  }
}

2. TypeScript 支持

// types/storage.d.ts
interface StorageItem<T = any> {
  data: T
  timestamp: number
  expires: number | null
}

interface StorageOptions {
  expires?: number
  encrypt?: boolean
}

// 使用泛型包装
export function useTypedStorage<T>(key: string, initialValue: T, options?: StorageOptions) {
  // 类型安全的实现
}

3. 性能优化

// 批量操作减少频繁写入
class BatchStorage {
  constructor() {
    this.batch = new Map()
    this.flushTimeout = null
  }

  set(key, value) {
    this.batch.set(key, value)
    this.scheduleFlush()
  }

  scheduleFlush() {
    if (this.flushTimeout) clearTimeout(this.flushTimeout)

    // 防抖:100ms后批量写入
    this.flushTimeout = setTimeout(() => {
      this.flush()
    }, 100)
  }

  flush() {
    this.batch.forEach((value, key) => {
      localStorage.setItem(key, JSON.stringify(value))
    })
    this.batch.clear()
  }
}

4. 错误处理

// 封装错误边界
function safeStorageOperation(operation) {
  try {
    return operation()
  } catch (error) {
    if (error.name === 'QuotaExceededError') {
      // 存储空间不足的处理逻辑
      handleStorageFull()
      throw new Error('存储空间不足,请清理数据')
    } else if (error.name === 'SecurityError') {
      // 隐私模式或安全限制
      console.warn('Storage 访问被拒绝,可能处于隐私模式')
      return null
    } else {
      console.error('Storage 操作失败:', error)
      throw error
    }
  }
}

5. 数据迁移策略

// 版本化管理存储数据
const STORAGE_VERSION = '1.0.0'

function migrateStorage() {
  const version = localStorage.getItem('storage-version')

  if (!version || version !== STORAGE_VERSION) {
    // 执行数据迁移
    migrateFromOldVersion(version)
    localStorage.setItem('storage-version', STORAGE_VERSION)
  }
}

使用建议

LocalStorage 适合存储

SessionStorage 适合存储

不要存储

内存管理

这个完整示例提供了从基础封装到高级应用的最佳实践,可以根据项目需求进行调整和扩展。

相关推荐