MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue Pinia 持久化存储与离线支持的最佳实践

2023-12-105.2k 阅读

Vue Pinia 持久化存储概述

在 Vue 应用开发中,状态管理至关重要。Pinia 作为 Vue 3 的轻量级状态管理库,为开发者提供了简洁且高效的状态管理解决方案。而持久化存储则是将应用中的状态数据保存到本地,以便在页面刷新或重新打开应用时,能恢复到之前的状态。

为什么要使用持久化存储

  1. 用户体验提升:用户在使用应用过程中,不希望因意外刷新或关闭应用而丢失已进行的操作和设置。例如,一个待办事项应用,用户添加了一系列任务,如果没有持久化存储,刷新页面后任务列表就会消失,这极大影响用户体验。通过持久化存储,用户再次打开应用时,能看到和之前一样的任务列表,操作得以延续。
  2. 数据一致性:在多页面应用中,不同页面可能依赖相同的状态数据。持久化存储确保了即使在页面切换或重新加载时,数据依然保持一致。比如,一个电商应用,用户在商品列表页选择了筛选条件,进入商品详情页后再返回列表页,通过持久化存储,筛选条件依然保留,保证了用户操作的连贯性。

Pinia 持久化存储的原理

Pinia 的持久化存储主要通过将 store 中的数据存储在浏览器的本地存储(localStorage)或会话存储(sessionStorage)中来实现。当应用初始化时,从存储中读取数据并填充到相应的 store 中。当 store 中的数据发生变化时,同步更新存储中的数据。以一个简单的计数器 store 为例:

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    }
  }
})

要实现持久化存储,我们可以在初始化 store 时从本地存储读取数据,并在数据变化时更新本地存储。如下:

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => {
    const storedCount = localStorage.getItem('counter')
    return {
      count: storedCount? JSON.parse(storedCount) : 0
    }
  },
  actions: {
    increment() {
      this.count++
      localStorage.setItem('counter', JSON.stringify(this.count))
    }
  }
})

在上述代码中,state 函数首先从 localStorage 中读取 counter 的值,如果存在则解析为 JSON 并赋值给 count,否则初始化为 0。increment 方法在更新 count 后,将新值存储到 localStorage 中。

实现 Vue Pinia 持久化存储的具体方法

使用插件实现通用持久化

虽然手动为每个 store 实现持久化存储可行,但对于大型应用,每个 store 都重复这样的代码会显得繁琐且难以维护。这时,我们可以通过编写插件来实现通用的持久化功能。

  1. 创建插件
import { PiniaPluginContext } from 'pinia'

export function persistPlugin({ store }: PiniaPluginContext) {
  const persistedState = localStorage.getItem(store.$id)
  if (persistedState) {
    store.$state = JSON.parse(persistedState)
  }
  store.$subscribe((mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state))
  })
}

在上述代码中,persistPlugin 是一个 Pinia 插件。它首先从 localStorage 中读取与 store.$id 对应的存储数据,并将其赋值给 store.$state。然后,通过 $subscribe 方法监听 store 的变化,一旦有变化,就将最新的 state 存储到 localStorage 中。 2. 使用插件: 在 Vue 应用入口文件(通常是 main.js)中,引入并使用该插件:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { persistPlugin } from './persistPlugin'

const pinia = createPinia()
pinia.use(persistPlugin)

const app = createApp(App)
app.use(pinia)
app.mount('#app')

这样,所有使用 Pinia 的 store 都将自动具备持久化存储功能。

自定义持久化策略

  1. 选择不同的存储方式:除了 localStorage,还可以使用 sessionStoragesessionStorage 的数据在页面会话结束(关闭标签页)时会被清除,适用于一些临时数据的存储。修改插件代码如下:
import { PiniaPluginContext } from 'pinia'

export function sessionPersistPlugin({ store }: PiniaPluginContext) {
  const persistedState = sessionStorage.getItem(store.$id)
  if (persistedState) {
    store.$state = JSON.parse(persistedState)
  }
  store.$subscribe((mutation, state) => {
    sessionStorage.setItem(store.$id, JSON.stringify(state))
  })
}

main.js 中使用该插件:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { sessionPersistPlugin } from './sessionPersistPlugin'

const pinia = createPinia()
pinia.use(sessionPersistPlugin)

const app = createApp(App)
app.use(pinia)
app.mount('#app')
  1. 部分数据持久化:有时,我们可能只希望持久化 store 中的部分数据。例如,在一个用户信息 store 中,用户的登录状态需要持久化,但一些临时的用户设置(如当前展开的菜单)不需要持久化。
import { PiniaPluginContext } from 'pinia'

export function partialPersistPlugin({ store }: PiniaPluginContext) {
  const persistedKeys = ['isLoggedIn']
  const persistedState = localStorage.getItem(store.$id)
  if (persistedState) {
    const storedState = JSON.parse(persistedState)
    persistedKeys.forEach(key => {
      if (storedState[key]) {
        store.$state[key] = storedState[key]
      }
    })
  }
  store.$subscribe((mutation, state) => {
    const partialState: any = {}
    persistedKeys.forEach(key => {
      if (state[key]) {
        partialState[key] = state[key]
      }
    })
    localStorage.setItem(store.$id, JSON.stringify(partialState))
  })
}

在上述代码中,persistedKeys 定义了需要持久化的键。插件在读取和存储数据时,只处理这些键对应的数据。

Vue Pinia 离线支持

在现代 Web 应用开发中,离线支持是提升用户体验的重要特性。用户可能会在网络不稳定或无网络环境下使用应用,这时离线支持能确保应用的部分功能依然可用。

离线支持的原理

  1. Service Workers:Service Workers 是一种在后台运行的脚本,它能拦截网络请求,缓存资源,并在离线时提供这些缓存资源。在 Vue 应用中,Service Workers 可以拦截对 API 的请求,如果请求的资源已缓存,则直接返回缓存数据,否则尝试从网络获取。如果网络不可用且资源未缓存,则返回错误或提示信息。
  2. 缓存策略:常见的缓存策略有几种。
    • Cache - Only:只从缓存中获取资源。这种策略适用于一些静态资源,如样式表、脚本等,这些资源在应用的生命周期内基本不会变化。例如,对于应用的全局样式表 styles.css,可以采用 Cache - Only 策略,在 Service Worker 安装时将其缓存,离线时直接从缓存中读取。
    • Network - Only:只从网络获取资源。这种策略适用于需要实时数据的情况,如获取最新的股票价格。但在离线时,这种策略会导致请求失败。
    • Stale - While - Revalidate:先从缓存中返回资源,同时在后台从网络更新缓存。这种策略适用于一些对数据实时性要求不是特别高的场景,如新闻列表。用户打开应用时,能快速看到缓存的新闻列表,同时 Service Worker 在后台更新缓存,下次用户访问时就能看到最新的新闻。
    • Network - First:先尝试从网络获取资源,如果网络不可用或请求失败,再从缓存中获取。这种策略适用于对数据实时性要求较高,但也希望在离线时有一定可用性的场景,如获取用户的最新订单列表。

为 Vue Pinia 应用添加离线支持

  1. 注册 Service Worker:在 Vue 应用中,首先要注册 Service Worker。在 main.js 中添加如下代码:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()
app.use(pinia)

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service - worker.js')
     .then(registration => {
        console.log('ServiceWorker registration successful with scope: ', registration.scope)
      })
     .catch(err => {
        console.log('ServiceWorker registration failed: ', err)
      })
  })
}

上述代码在页面加载完成后,检查浏览器是否支持 Service Worker,如果支持,则注册 service - worker.js。 2. 编写 Service Workerservice - worker.js 负责缓存资源和拦截请求。

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('my - app - cache')
     .then(cache => cache.addAll([
        '/',
        '/index.html',
        '/styles.css',
        '/scripts.js'
      ]))
  )
})

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
     .then(response => {
        if (response) {
          return response
        }
        return fetch(event.request)
      })
  )
})

install 事件中,打开一个缓存 my - app - cache,并将应用的首页、样式表、脚本等资源缓存起来。在 fetch 事件中,首先尝试从缓存中匹配请求,如果找到则返回缓存的响应,否则从网络获取。

结合 Pinia 持久化存储与离线支持

  1. 离线时使用持久化数据:当应用离线时,Pinia 持久化存储的数据依然可用。例如,一个笔记应用,用户在离线前创建了一些笔记并保存到 Pinia store 中,通过持久化存储,这些笔记数据被保存到本地。在离线状态下,应用可以直接从持久化存储中读取数据并展示给用户。
import { defineStore } from 'pinia'

export const useNoteStore = defineStore('note', {
  state: () => {
    const storedNotes = localStorage.getItem('note')
    return {
      notes: storedNotes? JSON.parse(storedNotes) : []
    }
  },
  actions: {
    addNote(note) {
      this.notes.push(note)
      localStorage.setItem('note', JSON.stringify(this.notes))
    }
  }
})

在离线状态下,useNoteStorenotes 数据依然能从 localStorage 中获取并正常使用。 2. 在线时同步数据:当网络恢复时,需要将持久化存储的数据与服务器进行同步。以笔记应用为例,可以在网络恢复时,将 notes 数据发送到服务器进行更新。

import { defineStore } from 'pinia'
import axios from 'axios'

export const useNoteStore = defineStore('note', {
  state: () => {
    const storedNotes = localStorage.getItem('note')
    return {
      notes: storedNotes? JSON.parse(storedNotes) : []
    }
  },
  actions: {
    async addNote(note) {
      this.notes.push(note)
      localStorage.setItem('note', JSON.stringify(this.notes))
      try {
        await axios.post('/api/notes', this.notes)
      } catch (error) {
        console.error('Failed to sync notes to server:', error)
      }
    }
  }
})

在上述代码中,addNote 方法在添加笔记并更新本地存储后,尝试将数据发送到服务器。如果网络可用,数据将同步到服务器;如果网络不可用,数据依然保存在本地,待网络恢复后再次尝试同步。

处理复杂数据结构的持久化与离线支持

复杂数据结构的持久化

在实际应用中,Pinia store 中的数据结构可能会很复杂,例如包含嵌套对象和数组。以一个电商购物车 store 为例:

import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [
      {
        id: 1,
        name: 'Product 1',
        price: 10,
        quantity: 1,
        options: {
          color: 'Red',
          size: 'M'
        }
      }
    ]
  }),
  actions: {
    addItem(product) {
      const existingItem = this.items.find(item => item.id === product.id)
      if (existingItem) {
        existingItem.quantity++
      } else {
        this.items.push({...product, quantity: 1 })
      }
    }
  }
})

对于这样的复杂数据结构,在持久化存储时需要注意 JSON 序列化和反序列化的正确性。在插件中,可以这样处理:

import { PiniaPluginContext } from 'pinia'

export function complexPersistPlugin({ store }: PiniaPluginContext) {
  const persistedState = localStorage.getItem(store.$id)
  if (persistedState) {
    try {
      store.$state = JSON.parse(persistedState)
    } catch (error) {
      console.error('Error parsing persisted state:', error)
    }
  }
  store.$subscribe((mutation, state) => {
    try {
      localStorage.setItem(store.$id, JSON.stringify(state))
    } catch (error) {
      console.error('Error stringifying state for persistence:', error)
    }
  })
}

上述代码在读取和存储数据时,通过 try - catch 块来处理可能的 JSON 解析和序列化错误。

复杂数据结构在离线时的处理

当应用离线且数据结构复杂时,可能会遇到一些问题,比如如何处理缓存数据的更新。以购物车为例,如果用户在离线时添加了商品,购物车数据发生变化,而缓存中的数据并未更新。这时可以采用如下策略:

  1. 版本控制:在持久化存储数据时,同时存储一个版本号。当数据发生变化时,版本号递增。在 Service Worker 中,根据版本号来判断是否需要更新缓存数据。
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],
    version: 1
  }),
  actions: {
    addItem(product) {
      const existingItem = this.items.find(item => item.id === product.id)
      if (existingItem) {
        existingItem.quantity++
      } else {
        this.items.push({...product, quantity: 1 })
      }
      this.version++
    }
  }
})

在 Service Worker 中:

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
     .then(response => {
        if (response) {
          const cachedVersion = response.headers.get('X - Cart - Version')
          const currentVersion = localStorage.getItem('cart - version')
          if (cachedVersion && currentVersion && cachedVersion < currentVersion) {
            // 更新缓存
            return fetch(event.request)
          }
          return response
        }
        return fetch(event.request)
      })
  )
})

上述代码中,购物车 store 在数据变化时更新版本号。Service Worker 在处理请求时,比较缓存数据的版本号和本地存储的版本号,如果不一致则从网络获取最新数据并更新缓存。

优化持久化存储与离线支持的性能

持久化存储性能优化

  1. 减少存储频率:频繁地读写本地存储会影响性能。可以通过节流或防抖的方式来减少存储操作。例如,使用防抖函数,当 store 数据变化时,延迟一段时间再进行存储操作。
import { PiniaPluginContext } from 'pinia'
import { debounce } from 'lodash'

export function debouncePersistPlugin({ store }: PiniaPluginContext) {
  const persistedState = localStorage.getItem(store.$id)
  if (persistedState) {
    store.$state = JSON.parse(persistedState)
  }
  const debouncedSetItem = debounce((state) => {
    localStorage.setItem(store.$id, JSON.stringify(state))
  }, 300)
  store.$subscribe((mutation, state) => {
    debouncedSetItem(state)
  })
}

在上述代码中,debounce 函数延迟 300 毫秒执行 localStorage.setItem 操作,避免了因频繁变化导致的多次存储。 2. 压缩存储数据:对于较大的数据结构,可以在存储前进行压缩。例如,使用 lz-string 库对数据进行压缩。

import { PiniaPluginContext } from 'pinia'
import LZString from 'lz - string'

export function compressedPersistPlugin({ store }: PiniaPluginContext) {
  const persistedState = localStorage.getItem(store.$id)
  if (persistedState) {
    const decompressed = LZString.decompress(persistedState)
    if (decompressed) {
      store.$state = JSON.parse(decompressed)
    }
  }
  store.$subscribe((mutation, state) => {
    const compressed = LZString.compress(JSON.stringify(state))
    localStorage.setItem(store.$id, compressed)
  })
}

上述代码在存储数据前使用 LZString.compress 进行压缩,读取时使用 LZString.decompress 进行解压缩。

离线支持性能优化

  1. 缓存管理:合理管理 Service Worker 的缓存,避免缓存过多无用数据。可以定期清理缓存,例如在 Service Worker 的 activate 事件中进行缓存清理。
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys()
     .then(cacheNames => {
        return Promise.all(
          cacheNames.filter(cacheName => cacheName.startsWith('my - app - cache') && cacheName!== 'my - app - cache - v2')
           .map(cacheName => caches.delete(cacheName))
        )
      })
  )
})

上述代码在 Service Worker 激活时,删除除了 my - app - cache - v2 以外的其他以 my - app - cache 开头的缓存。 2. 预缓存优化:对于一些重要的资源,可以在 Service Worker 安装时进行预缓存。但要注意不要预缓存过多资源,导致安装时间过长。可以根据用户的使用频率和资源的重要性来选择预缓存的资源。例如,对于一个新闻应用,预缓存首页、常用样式和脚本,而对于一些不常用的页面资源,可以采用按需缓存的方式。

应对持久化存储与离线支持的常见问题

持久化存储的兼容性问题

  1. 不同浏览器的差异:虽然 localStoragesessionStorage 在现代浏览器中得到广泛支持,但在一些旧版本浏览器中可能存在兼容性问题。例如,IE 浏览器对 localStorage 的支持存在一些限制,如存储容量较小等。为了解决这些问题,可以使用 polyfill 库,如 localStorage - polyfill,它能在不支持的浏览器中模拟 localStorage 的功能。
  2. 存储容量限制localStoragesessionStorage 都有存储容量限制,不同浏览器的限制可能不同,一般在 5MB 左右。如果存储的数据量过大,可能会导致存储失败。这时可以考虑将数据进行分块存储,或者使用 IndexedDB 等其他存储方式,IndexedDB 提供了更大的存储容量和更灵活的数据存储结构。

离线支持的网络切换问题

  1. 网络恢复时的同步:当网络从离线状态恢复时,应用需要将离线期间产生的变化同步到服务器。这可能会遇到同步冲突等问题。例如,在一个多人协作的文档编辑应用中,用户 A 和用户 B 在离线时对同一文档进行了不同的修改,网络恢复时可能会出现冲突。为了解决这个问题,可以采用版本控制和冲突解决算法。在每次同步时,服务器返回最新的版本号,客户端根据版本号来判断是否存在冲突,并采用相应的冲突解决策略,如以服务器数据为准,或者提示用户手动解决冲突。
  2. 实时更新体验:在网络恢复后,如何提供实时更新的体验也是一个问题。例如,一个实时聊天应用,当网络恢复时,需要快速获取最新的聊天消息。可以通过 WebSocket 等技术实现实时通信,当网络恢复时,重新建立 WebSocket 连接,及时获取最新数据并更新应用界面。

通过上述对 Vue Pinia 持久化存储与离线支持的深入探讨和实践,开发者可以为 Vue 应用提供更强大、稳定且用户体验良好的状态管理和离线功能。在实际开发中,需要根据应用的具体需求和场景,灵活选择和优化相关技术,以达到最佳的应用性能和用户体验。