Nuxt.js状态持久化 #
一、持久化概述 #
状态持久化是将应用状态保存到客户端存储中,在页面刷新或重新访问时恢复状态。Nuxt.js 提供了多种持久化方案。
1.1 存储方式对比 #
| 存储方式 | 容量 | 过期时间 | SSR支持 | 适用场景 |
|---|---|---|---|---|
| Cookie | 4KB | 可设置 | ✅ | 认证令牌、小数据 |
| localStorage | 5MB | 永久 | ❌ | 用户偏好、缓存数据 |
| sessionStorage | 5MB | 会话 | ❌ | 临时数据 |
| IndexedDB | 无限制 | 永久 | ❌ | 大量数据 |
二、Cookie存储 #
2.1 useCookie #
vue
<script setup lang="ts">
const token = useCookie('token', {
maxAge: 60 * 60 * 24 * 7,
path: '/',
secure: true,
httpOnly: false,
sameSite: 'lax'
})
const setToken = (value: string) => {
token.value = value
}
const clearToken = () => {
token.value = null
}
</script>
2.2 Cookie选项 #
typescript
interface CookieOptions {
maxAge?: number
expires?: Date
path?: string
domain?: string
secure?: boolean
httpOnly?: boolean
sameSite?: 'strict' | 'lax' | 'none'
encode?: (value: string) => string
decode?: (value: string) => string
default?: () => any
watch?: boolean | 'shallow'
readonly?: boolean
}
2.3 类型化Cookie #
vue
<script setup lang="ts">
interface UserPreferences {
theme: 'light' | 'dark'
language: string
fontSize: number
}
const preferences = useCookie<UserPreferences>('preferences', {
default: () => ({
theme: 'light',
language: 'zh-CN',
fontSize: 14
})
})
const updatePreferences = (updates: Partial<UserPreferences>) => {
preferences.value = {
...preferences.value,
...updates
}
}
</script>
2.4 响应式Cookie #
vue
<script setup lang="ts">
const theme = useCookie('theme', {
default: () => 'light',
watch: true
})
watch(theme, (newTheme) => {
console.log('Theme changed:', newTheme)
})
</script>
三、localStorage #
3.1 封装localStorage #
composables/useLocalStorage.ts:
typescript
export const useLocalStorage = <T>(key: string, defaultValue: T) => {
const data = ref<T>(defaultValue) as Ref<T>
if (import.meta.client) {
const stored = localStorage.getItem(key)
if (stored) {
try {
data.value = JSON.parse(stored)
} catch (e) {
console.error(`Failed to parse localStorage item: ${key}`)
}
}
watch(
data,
(newValue) => {
if (newValue === null || newValue === undefined) {
localStorage.removeItem(key)
} else {
localStorage.setItem(key, JSON.stringify(newValue))
}
},
{ deep: true }
)
}
const clear = () => {
data.value = defaultValue
if (import.meta.client) {
localStorage.removeItem(key)
}
}
return {
data,
clear
}
}
3.2 使用示例 #
vue
<script setup lang="ts">
interface Settings {
notifications: boolean
autoSave: boolean
fontSize: number
}
const { data: settings, clear } = useLocalStorage<Settings>('settings', {
notifications: true,
autoSave: true,
fontSize: 14
})
const updateSettings = (updates: Partial<Settings>) => {
settings.value = {
...settings.value,
...updates
}
}
</script>
3.3 带过期时间的localStorage #
composables/useLocalStorageEx.ts:
typescript
interface StorageItem<T> {
value: T
expiresAt: number
}
export const useLocalStorageEx = <T>(
key: string,
defaultValue: T,
ttl: number = 24 * 60 * 60 * 1000
) => {
const data = ref<T>(defaultValue) as Ref<T>
if (import.meta.client) {
const stored = localStorage.getItem(key)
if (stored) {
try {
const item: StorageItem<T> = JSON.parse(stored)
if (Date.now() < item.expiresAt) {
data.value = item.value
} else {
localStorage.removeItem(key)
}
} catch (e) {
localStorage.removeItem(key)
}
}
watch(
data,
(newValue) => {
const item: StorageItem<T> = {
value: newValue,
expiresAt: Date.now() + ttl
}
localStorage.setItem(key, JSON.stringify(item))
},
{ deep: true }
)
}
return data
}
四、SessionStorage #
4.1 封装SessionStorage #
composables/useSessionStorage.ts:
typescript
export const useSessionStorage = <T>(key: string, defaultValue: T) => {
const data = ref<T>(defaultValue) as Ref<T>
if (import.meta.client) {
const stored = sessionStorage.getItem(key)
if (stored) {
try {
data.value = JSON.parse(stored)
} catch (e) {
console.error(`Failed to parse sessionStorage item: ${key}`)
}
}
watch(
data,
(newValue) => {
if (newValue === null || newValue === undefined) {
sessionStorage.removeItem(key)
} else {
sessionStorage.setItem(key, JSON.stringify(newValue))
}
},
{ deep: true }
)
}
return data
}
4.2 使用示例 #
vue
<script setup lang="ts">
const formData = useSessionStorage('form-draft', {
title: '',
content: ''
})
const saveDraft = () => {
formData.value = {
title: form.title,
content: form.content
}
}
</script>
五、Pinia持久化 #
5.1 使用插件 #
bash
pnpm add @pinia-plugin-persistedstate
nuxt.config.ts:
typescript
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
piniaPersistedstate: {
cookieOptions: {
maxAge: 60 * 60 * 24 * 30
}
}
})
5.2 Store配置 #
typescript
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
token: null,
preferences: {
theme: 'light',
language: 'zh-CN'
}
}),
persist: {
key: 'user-store',
storage: persistedState.cookiesWithOptions({
maxAge: 60 * 60 * 24 * 30
}),
paths: ['token', 'preferences']
}
})
5.3 选择性持久化 #
typescript
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
couponCode: null,
checkoutStep: 1
}),
persist: {
paths: ['items', 'couponCode']
}
})
5.4 自定义序列化 #
typescript
export const useDateStore = defineStore('dates', {
state: () => ({
createdAt: null as Date | null,
updatedAt: null as Date | null
}),
persist: {
serializer: {
serialize: (state) => JSON.stringify({
...state,
createdAt: state.createdAt?.toISOString(),
updatedAt: state.updatedAt?.toISOString()
}),
deserialize: (value) => {
const state = JSON.parse(value)
return {
...state,
createdAt: state.createdAt ? new Date(state.createdAt) : null,
updatedAt: state.updatedAt ? new Date(state.updatedAt) : null
}
}
}
}
})
六、useState持久化 #
6.1 持久化useState #
composables/usePersistedState.ts:
typescript
export const usePersistedState = <T>(
key: string,
defaultValue: T,
options: {
storage?: 'cookie' | 'localStorage' | 'sessionStorage'
ttl?: number
} = {}
) => {
const { storage = 'localStorage', ttl } = options
const getStoredValue = (): T => {
if (import.meta.server) return defaultValue
let stored: string | null = null
switch (storage) {
case 'cookie':
stored = useCookie(key).value as string | null
break
case 'localStorage':
stored = localStorage.getItem(key)
break
case 'sessionStorage':
stored = sessionStorage.getItem(key)
break
}
if (!stored) return defaultValue
try {
const parsed = JSON.parse(stored)
if (ttl && parsed.timestamp) {
if (Date.now() - parsed.timestamp > ttl) {
return defaultValue
}
}
return parsed.value ?? defaultValue
} catch {
return defaultValue
}
}
const state = useState<T>(key, getStoredValue)
const saveValue = (value: T) => {
if (import.meta.client) {
const toStore = JSON.stringify({
value,
timestamp: Date.now()
})
switch (storage) {
case 'cookie':
useCookie(key, { maxAge: ttl ? ttl / 1000 : undefined }).value = toStore
break
case 'localStorage':
localStorage.setItem(key, toStore)
break
case 'sessionStorage':
sessionStorage.setItem(key, toStore)
break
}
}
}
watch(state, saveValue, { deep: true })
return state
}
6.2 使用示例 #
vue
<script setup lang="ts">
const theme = usePersistedState('theme', 'light', {
storage: 'cookie',
ttl: 30 * 24 * 60 * 60 * 1000
})
const preferences = usePersistedState('preferences', {
fontSize: 14,
notifications: true
})
</script>
七、加密存储 #
7.1 加密localStorage #
composables/useEncryptedStorage.ts:
typescript
import CryptoJS from 'crypto-js'
const SECRET_KEY = 'your-secret-key'
export const useEncryptedStorage = <T>(key: string, defaultValue: T) => {
const encrypt = (data: T): string => {
return CryptoJS.AES.encrypt(JSON.stringify(data), SECRET_KEY).toString()
}
const decrypt = (encrypted: string): T => {
const bytes = CryptoJS.AES.decrypt(encrypted, SECRET_KEY)
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8))
}
const data = ref<T>(defaultValue) as Ref<T>
if (import.meta.client) {
const stored = localStorage.getItem(key)
if (stored) {
try {
data.value = decrypt(stored)
} catch {
data.value = defaultValue
}
}
watch(data, (newValue) => {
localStorage.setItem(key, encrypt(newValue))
}, { deep: true })
}
return data
}
八、最佳实践 #
8.1 选择存储方式 #
| 数据类型 | 推荐存储 | 原因 |
|---|---|---|
| 认证令牌 | Cookie | SSR支持、安全 |
| 用户偏好 | localStorage | 持久化、容量大 |
| 表单草稿 | sessionStorage | 会话级、自动清理 |
| 敏感数据 | 加密存储 | 安全性 |
8.2 数据迁移 #
typescript
const STORAGE_VERSION = 2
export const useMigratedStorage = <T>(key: string, defaultValue: T) => {
const versionKey = `${key}-version`
const currentVersion = localStorage.getItem(versionKey)
if (currentVersion && parseInt(currentVersion) < STORAGE_VERSION) {
localStorage.removeItem(key)
}
localStorage.setItem(versionKey, STORAGE_VERSION.toString())
return useLocalStorage(key, defaultValue)
}
8.3 容量管理 #
typescript
const checkStorageQuota = () => {
if (import.meta.client) {
const testKey = '__storage_test__'
try {
localStorage.setItem(testKey, testKey)
localStorage.removeItem(testKey)
return true
} catch {
return false
}
}
return true
}
const clearOldCache = () => {
if (import.meta.client) {
const keys = Object.keys(localStorage)
const cacheKeys = keys.filter(k => k.startsWith('cache-'))
cacheKeys.forEach(key => {
const item = localStorage.getItem(key)
if (item) {
try {
const { timestamp } = JSON.parse(item)
if (Date.now() - timestamp > 7 * 24 * 60 * 60 * 1000) {
localStorage.removeItem(key)
}
} catch {
localStorage.removeItem(key)
}
}
})
}
}
九、完整示例 #
9.1 用户偏好管理 #
composables/useUserPreferences.ts:
typescript
interface UserPreferences {
theme: 'light' | 'dark' | 'system'
language: string
fontSize: number
notifications: {
email: boolean
push: boolean
sms: boolean
}
privacy: {
analytics: boolean
personalizedAds: boolean
}
}
const DEFAULT_PREFERENCES: UserPreferences = {
theme: 'system',
language: 'zh-CN',
fontSize: 14,
notifications: {
email: true,
push: true,
sms: false
},
privacy: {
analytics: true,
personalizedAds: false
}
}
export const useUserPreferences = () => {
const preferences = usePersistedState<UserPreferences>(
'user-preferences',
DEFAULT_PREFERENCES,
{ storage: 'localStorage' }
)
const theme = computed({
get: () => preferences.value.theme,
set: (value) => { preferences.value.theme = value }
})
const language = computed({
get: () => preferences.value.language,
set: (value) => { preferences.value.language = value }
})
const updatePreferences = (updates: Partial<UserPreferences>) => {
preferences.value = {
...preferences.value,
...updates
}
}
const resetPreferences = () => {
preferences.value = DEFAULT_PREFERENCES
}
watchEffect(() => {
if (import.meta.client) {
const isDark = preferences.value.theme === 'dark' ||
(preferences.value.theme === 'system' &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
document.documentElement.classList.toggle('dark', isDark)
document.documentElement.style.fontSize = `${preferences.value.fontSize}px`
}
})
return {
preferences,
theme,
language,
updatePreferences,
resetPreferences
}
}
十、总结 #
本章介绍了 Nuxt.js 状态持久化:
- Cookie 存储使用
useCookie - localStorage 和 sessionStorage 封装
- Pinia 状态持久化插件
- useState 持久化方案
- 加密存储实现
- 最佳实践和容量管理
合理的持久化策略可以提升用户体验,下一章我们将学习高级特性。
最后更新:2026-03-28