数据持久化方案:Vuex/Pinia 的灵活后盾
各位观众老爷们,大家好!我是你们的老朋友,代码界的段子手,今天咱们来聊聊 Vuex 和 Pinia 的数据持久化,让你的数据不再“刷新就没了”。
为什么需要持久化?
想象一下,用户辛辛苦苦登录了你的网站,填写了一堆信息,结果刷新一下页面,全没了!这体验简直糟糕透顶。数据持久化就是为了解决这个问题,它能把 Vuex 或 Pinia 里的数据保存到本地,下次打开页面的时候,直接恢复,给用户一个流畅丝滑的体验。
目标:一个通用的持久化方案
咱们的目标是做一个通用的数据持久化方案,它需要满足以下几个条件:
- 支持多种存储介质:
localStorage
、IndexedDB
等等,让你可以根据实际情况选择最合适的存储方式。 - 易于配置: 简单几行代码就能搞定,不需要写大量的 boilerplate 代码。
- 灵活控制: 可以选择需要持久化的 state,而不是一股脑全部保存。
- 兼容性好: 最好能兼容 Vuex 和 Pinia,让你可以平滑切换状态管理方案。
设计思路:插件模式
咱们采用插件模式来实现这个通用的持久化方案。插件可以拦截 Vuex/Pinia 的状态更新,并在状态改变时,将数据保存到指定的存储介质中。同时,插件也能在应用初始化时,从存储介质中读取数据,并恢复到 Vuex/Pinia 的 state 中。
存储介质抽象:策略模式
为了支持多种存储介质,咱们采用策略模式。定义一个 StorageAdapter
接口,不同的存储介质实现这个接口,提供统一的 API。
interface StorageAdapter {
getItem<T>(key: string): T | null;
setItem<T>(key: string, value: T): void;
removeItem(key: string): void;
}
接下来,咱们来实现 localStorage
和 IndexedDB
的 StorageAdapter
。
localStorage 适配器
class LocalStorageAdapter implements StorageAdapter {
getItem<T>(key: string): T | null {
try {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : null;
} catch (error) {
console.error(`Failed to get item from localStorage: ${error}`);
return null;
}
}
setItem<T>(key: string, value: T): void {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`Failed to set item to localStorage: ${error}`);
}
}
removeItem(key: string): void {
try {
localStorage.removeItem(key);
} catch (error) {
console.error(`Failed to remove item from localStorage: ${error}`);
}
}
}
IndexedDB 适配器
IndexedDB 的实现稍微复杂一些,需要处理数据库连接和对象存储。
class IndexedDBAdapter implements StorageAdapter {
private dbName: string;
private storeName: string;
private dbPromise: Promise<IDBDatabase>;
constructor(dbName: string, storeName: string) {
this.dbName = dbName;
this.storeName = storeName;
this.dbPromise = this.openDatabase();
}
private openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = window.indexedDB.open(this.dbName, 1);
request.onerror = () => {
reject(new Error('Failed to open IndexedDB'));
};
request.onsuccess = () => {
resolve(request.result);
};
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const db = (event.target as IDBRequest).result as IDBDatabase;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
});
}
async getItem<T>(key: string): Promise<T | null> {
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readonly');
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.get(key);
request.onerror = () => {
reject(new Error('Failed to get item from IndexedDB'));
};
request.onsuccess = () => {
resolve(request.result || null);
};
});
}
async setItem<T>(key: string, value: T): Promise<void> {
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.put(value, key);
request.onerror = () => {
reject(new Error('Failed to set item to IndexedDB'));
};
request.onsuccess = () => {
resolve();
};
});
}
async removeItem(key: string): Promise<void> {
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.delete(key);
request.onerror = () => {
reject(new Error('Failed to remove item from IndexedDB'));
};
request.onsuccess = () => {
resolve();
};
});
}
}
Vuex 插件实现
import { Store } from 'vuex';
interface VuexPersistenceOptions {
key?: string; // 存储的 key,默认为 'vuex'
storage?: StorageAdapter; // 存储适配器,默认为 localStorage
reducer?: (state: any) => any; // 选择需要持久化的 state
}
function createVuexPersistence(options: VuexPersistenceOptions = {}) {
const {
key = 'vuex',
storage = new LocalStorageAdapter(),
reducer = (state: any) => state,
} = options;
return (store: Store<any>) => {
// 1. 初始化时,从存储中读取数据
const savedState = storage.getItem(key);
if (savedState) {
store.replaceState(Object.assign({}, store.state, savedState)); // 合并已有的 state
}
// 2. 订阅 mutations,在每次 mutation 发生后,保存数据
store.subscribe((mutation, state) => {
const reducedState = reducer(state);
storage.setItem(key, reducedState);
});
};
}
export default createVuexPersistence;
Pinia 插件实现
Pinia 的插件实现方式与 Vuex 类似,但 API 略有不同。
import { PiniaPluginContext } from 'pinia';
interface PiniaPersistenceOptions {
key?: string; // 存储的 key,默认为 'pinia'
storage?: StorageAdapter; // 存储适配器,默认为 localStorage
reducer?: (state: any) => any; // 选择需要持久化的 state
stores?: string[]; // 指定要持久化的 store 的 id 数组
}
function createPiniaPersistence(options: PiniaPersistenceOptions = {}) {
const {
key = 'pinia',
storage = new LocalStorageAdapter(),
reducer = (state: any) => state,
stores,
} = options;
return (context: PiniaPluginContext) => {
const { store } = context;
// 检查当前 store 是否在 stores 数组中
if (stores && !stores.includes(store.$id)) {
return;
}
// 1. 初始化时,从存储中读取数据
const savedState = storage.getItem(`${key}-${store.$id}`);
if (savedState) {
store.$patch(savedState);
}
// 2. 订阅状态变化,在状态改变后,保存数据
store.$subscribe((mutation, state) => {
const reducedState = reducer(state);
storage.setItem(`${key}-${store.$id}`, reducedState);
}, { detached: true });
};
}
export default createPiniaPersistence;
使用方法
Vuex
import Vue from 'vue';
import Vuex from 'vuex';
import createVuexPersistence from './vuex-persist';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
user: {
id: null,
name: '',
},
settings: {
theme: 'light',
language: 'en',
},
},
mutations: {
setUser(state, user) {
state.user = user;
},
setTheme(state, theme) {
state.settings.theme = theme;
},
},
plugins: [
createVuexPersistence({
key: 'my-app',
storage: new LocalStorageAdapter(),
reducer: (state) => ({
user: state.user, // 只持久化 user 状态
settings: {
theme: state.settings.theme,
}, // 只持久化 theme
}),
}),
],
});
export default store;
Pinia
import { createPinia } from 'pinia';
import createPiniaPersistence from './pinia-persist';
const pinia = createPinia();
pinia.use(
createPiniaPersistence({
key: 'my-app',
storage: new LocalStorageAdapter(),
reducer: (state) => ({
// 只持久化 user 的 name 和 id
name: state.name,
id: state.id,
}),
stores: ['user', 'settings'], // 只持久化 user 和 settings store
})
);
export default pinia;
关键代码解释
StorageAdapter
接口: 定义了存储介质的 API,方便切换不同的存储方式。LocalStorageAdapter
和IndexedDBAdapter
:StorageAdapter
的具体实现,分别使用localStorage
和IndexedDB
来存储数据。createVuexPersistence
和createPiniaPersistence
: Vuex 和 Pinia 的插件,负责拦截状态更新,并将数据保存到存储介质中。reducer
函数: 允许你选择需要持久化的 state,避免保存不必要的数据。stores
数组 (Pinia): 指定要持久化的 store 的 id 数组,更细粒度地控制持久化范围。
优点
- 通用性: 支持多种存储介质,可以根据实际情况选择。
- 灵活性: 可以选择需要持久化的 state,避免保存不必要的数据。
- 易用性: 简单几行代码就能搞定,不需要写大量的 boilerplate 代码。
- 可维护性: 代码结构清晰,易于理解和维护。
缺点
- IndexedDB 实现较为复杂: 需要处理数据库连接和对象存储,代码量稍多。
- 需要手动处理序列化和反序列化: 对于复杂的数据结构,可能需要自定义序列化和反序列化逻辑。
扩展
- 加密: 可以对存储的数据进行加密,提高安全性。
- 过期时间: 可以设置数据的过期时间,避免长期占用存储空间。
- 版本控制: 可以对数据进行版本控制,方便数据迁移和回滚。
- 集成 Redux DevTools (Vuex): 可以与 Redux DevTools 集成,方便调试和查看持久化数据。
总结
今天咱们一起实现了一个通用的 Vuex/Pinia 数据持久化方案,它支持多种存储介质,易于配置,灵活控制。希望这个方案能帮助你解决数据持久化的问题,提升用户体验。
记住,代码不是死的,要根据实际情况灵活运用。希望大家多多实践,不断学习,成为代码界的翘楚!下次再见!