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