數據持久化
關於數據持久基本是前端許多功能實現上必須了解的東西,常見的如 localStorage、sessionStorage、indexedDB,儲存例如一些用戶資料、記憶狀態、版本資訊、不太會變動的數據....
例如我們重整頁面的時候,我們會不希望
- redux 中的儲存的數據被清空
- client 端已經拿過這些資料的話就不要發請求,不要每次都從 sever 拿資料減少流量
TanStack Query、SWR,這兩個 library 也可以達到類似的效果,視你的專案而定。
要實現持久化, 最簡單的方法是 google search type 套件名稱 persist 就能找到相對應的 library 可以使用,而這篇將以 Redux 為例,講解熱門 state mangerment library 如何不使用其他多餘的套件自己控制持久化數據,
各套件相對應的 library: redux-persist、vuex-persist、pinia-plugin-persistedstate
Core
現今熱門的狀態管理套件基本上都能找到subscribe函數,來監聽 state 的變化,甚至如 zustand 都出了 middleware 可以直接使用 persist,相當於內建在裡面。
以下例子為 React、Vue,常用的狀態管理套件,redux、zustand、vuex、pinia
此篇的核心就是利用 subscribe 的監聽將變動的 state,儲存至 localStorage,然後在初始化 APP 的 create store 的時候再將 localStorage 中的資料拿出來當作初始資料。
這邊我們先寫一個 util 的 function,用來跟 localStorage(依需求選擇使用 localStorage 或是 sessionStorage) 交互
// utils.js
/** 讀取 store
*
* @returns {any|undefined}
*/
export const loadState = () => {
try {
// localStorage 的 key name 可以自己定義或是由傳入的 arguments 來決定,
// 這邊我以 "state" 固定當作 key name
const s = localStorage.getItem('state')
if (s == null) {
return undefined
}
return JSON.parse(s)
} catch (err) {
console.warn('load state failed', err)
return undefined
}
}
/**
* 存儲 store
* @param {object} state
*/
export const saveState = (state) => {
try {
const s = JSON.stringify(state)
localStorage.setItem('state', s)
} catch (err) {
console.warn('save state failed', err)
}
}
Redux
// store.js
import { loadState, saveState } from '@/utils'
import { configureStore } from '@reduxjs/toolkit'
import { combineReducers } from 'redux'
// Slice
import siteReducer from './slice/site'
import todoReducer from './slice/todo'
const reducer = combineReducers({
todo: todoReducer,
site: siteReducer,
})
// 從 localStorage 拿取上次儲存的資料
const preloadedState = loadState()
const store = configureStore({
reducer,
devTools: process.env.NODE_ENV !== 'production',
preloadedState,
})
store.subscribe(() => {
// 第一種寫法: 儲存所有的 state
const state = store.getState()
saveState(state)
// 第二種寫法: 只想儲存某些 reducer (or slice)
// const { site } = store.getState()
// saveState({ site })
})
export default store
zustand
因為本身就有提供 persist 的 middleware 了,所以這邊我們直接拿官方的例子來用
// https://docs.pmnd.rs/zustand/integrations/persisting-store-data
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'
export const useBearStore = create(
persist(
(set, get) => ({
bears: 0,
addABear: () => set({ bears: get().bears + 1 }),
}),
{
name: 'food-storage', // name of the item in the storage (must be unique)
storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
}
)
)
vuex
// store.js
// vue^2.6.14 + vuex 3.6.2
import { loadState, saveState } from '@/utils'
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {
increment(state) {
state.count++
},
},
})
if (localStorage.getItem('state')) {
store.replaceState({ ...store.state, ...loadState() })
}
store.subscribe((_mutation, state) => {
saveState(state)
})
export default store
pinia
- 利用 $patch 將已儲存的資料塞回去 state
- 利用 $subscribe 來監聽 state 的最新狀態並保存在 localStorage 裡
// store/count.ts
// vue^3.3.2 + pinia^2.0.36
import { loadState, saveState } from '@/utils'
import { defineStore, getActivePinia } from 'pinia'
import { computed, ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
const subscribeToStore = async () => {
// 當 pinia 初始化後才開始工作
while (!getActivePinia()) {
await new Promise((resolve) => requestAnimationFrame(resolve))
}
useCounterStore().$patch(loadState())
useCounterStore().$subscribe((mutation, state) => {
console.log({ mutation, state })
saveState(state)
})
}
subscribeToStore()
結論
subscribe 通常是大家在使用狀態管理套件中比較少去注意到的函數,然後不只是只能作持久化,拿來作監測 state 的變化也非常好用。
