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 适合存储:
- 表单草稿
- 页面间临时数据传递
- 购物车临时数据
- 多步骤流程的中间状态
不要存储:
- 敏感信息(密码、支付信息)
- 大量结构化数据(考虑使用 IndexedDB)
- 频繁变化的状态(考虑使用 Vuex/Pinia)
内存管理:
- 定期清理过期数据
- 控制存储数据大小
- 使用压缩策略存储大型数据
这个完整示例提供了从基础封装到高级应用的最佳实践,可以根据项目需求进行调整和扩展。