状态持久化方案深度解析
全面解析前端状态持久化的各种方案,从 localStorage 基础到 redux-persist、vuex-persist 再到自定义实现,构建完整的离线状态管理知识体系。
一句话概括
状态持久化是将前端运行时状态同步到持久化存储(localStorage、IndexedDB、Cookie 等)的技术,确保用户在刷新页面或重新打开应用时能够恢复之前的操作状态,是提升用户体验的关键基础设施。
背景与意义
默认情况下,前端应用的状态是「活在内存中」的——刷新页面,一切归零。对于某些场景这没问题,但对于很多现实需求来说,这是一个严重的体验问题:
- 用户填写了复杂的表单,不小心刷新页面——所有输入内容丢失
- 用户在购物车中添加了 5 件商品,关闭浏览器再打开——购物车空空如也
- 用户设置了深色主题和语言偏好——每次访问都需要重新设置
状态持久化的目标就是在「运行时状态的易失性」和「用户期望的持久性」之间建立桥梁。
概念与定义
持久化(Persistence):将内存中的数据写入持久化存储介质(磁盘、数据库等)的过程。在前端特指将 JavaScript 运行时状态同步到浏览器的持久化存储 API。
水合(Hydration):持久化的反向过程——从存储介质读取数据恢复到运行时状态。通常在应用初始化时执行。
存储适配器(Storage Adapter):封装不同存储后端(localStorage、sessionStorage、AsyncStorage 等)的统一接口。
版本迁移(Migration):当持久化数据的格式发生变化时,将旧数据迁到新格式的过程。
最小示例
最简单的持久化实现——手动持久化一个计数器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 保存
const count = 42
localStorage.setItem('count', String(count))
// 读取
const savedCount = Number(localStorage.getItem('count')) || 0
// 在 React 中的应用
function usePersistedState(key, defaultValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key)
return stored !== null ? JSON.parse(stored) : defaultValue
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue]
}
// 使用
function Counter() {
const [count, setCount] = usePersistedState('counter', 0)
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
}
核心知识点拆解
1. 存储引擎深度对比
浏览器提供了多种持久化存储方式,各有优劣:
| 特性 | localStorage | sessionStorage | Cookie | IndexedDB | Cache API |
|---|---|---|---|---|---|
| 容量 | 5-10MB | 5-10MB | 4KB | 不限(通常 250MB+) | 不限 |
| 数据类型 | 字符串 | 字符串 | 字符串 | 结构化数据 | 网络请求 |
| 同步/异步 | 同步 | 同步 | 同步 | 异步 | 异步 |
| 作用域 | 协议+域名+端口 | 协议+域名+端口+标签页 | 可配置路径 | 协议+域名+端口 | 协议+域名+端口 |
| 过期 | 手动删除 | 标签页关闭 | 可设置 | 手动删除 | 手动删除 |
| Worker 访问 | ✅ | ❌ | ❌ | ✅ | ✅ |
IndexedDB 的基础使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// IndexedDB 封装工具
class IndexedDBStore {
constructor(dbName, storeName, version = 1) {
this.dbName = dbName
this.storeName = storeName
this.version = version
this.db = null
}
open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version)
request.onupgradeneeded = (event) => {
const db = event.target.result
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName, { keyPath: 'id' })
}
}
request.onsuccess = (event) => {
this.db = event.target.result
resolve()
}
request.onerror = (event) => reject(event.target.error)
})
}
get(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, 'readonly')
const store = transaction.objectStore(this.storeName)
const request = store.get(id)
request.onsuccess = () => resolve(request.result)
request.onerror = (e) => reject(e.target.error)
})
}
set(id, value) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, 'readwrite')
const store = transaction.objectStore(this.storeName)
const request = store.put({ id, value })
request.onsuccess = () => resolve()
request.onerror = (e) => reject(e.target.error)
})
}
delete(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, 'readwrite')
const store = transaction.objectStore(this.storeName)
const request = store.delete(id)
request.onsuccess = () => resolve()
request.onerror = (e) => reject(e.target.error)
})
}
}
2. redux-persist 的完整配置
redux-persist 是 Redux 生态中最广泛使用的持久化方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// store.js - redux-persist 完整配置
import { configureStore, combineReducers } from '@reduxjs/toolkit'
import {
persistStore,
persistReducer,
FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'
// 持久化配置
const persistConfig = {
key: 'root', // storage 根键名
storage, // 存储引擎
whitelist: ['auth', 'cart', 'theme'], // 只持久化这些 reducer
blacklist: ['notifications', 'ui'], // 排除这些 reducer
version: 2, // 版本号,用于数据迁移
stateReconciler: autoMergeLevel2, // 状态合并策略
timeout: 5000, // 超时时间(毫秒)
writeFailHandler: (err) => {
console.error('Persist failed:', err)
},
}
// 带嵌套 reducer 的持久化
const rootReducer = combineReducers({
auth: persistedAuthReducer,
cart: persistedCartReducer,
theme: persistedThemeReducer,
notifications: notificationsReducer, // 不持久化
ui: uiReducer, // 不持久化(黑名单)
})
const persistedReducer = persistReducer(persistConfig, rootReducer)
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// redux-persist 的 action 不满足可序列化检查
// 需要忽略这些 action type
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
})
export const persistor = persistStore(store)
// App.js 中
import { PersistGate } from 'redux-persist/integration/react'
function App() {
return (
<Provider store={store}>
<PersistGate loading={<LoadingScreen />} persistor={persistor}>
<MainApp />
</PersistGate>
</Provider>
)
}
数据版本迁移:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// migration.js - 数据迁移
const migrations = {
// version 0 → 1:添加新字段
1: (state) => ({
...state,
auth: {
...state.auth,
lastLogin: null, // 新增字段
},
}),
// version 1 → 2:重构数据结构
2: (state) => ({
...state,
cart: {
items: state.cart.items || [],
// 旧数据中 coupon 在根级别,新版本移到 cart 内
coupon: state.coupon || null,
lastUpdated: Date.now(),
},
}),
}
export const persistConfig = {
key: 'root',
storage,
version: 2,
migrate: createMigrate(migrations, { debug: true }),
}
3. vuex-persist 的实现
对于 Vuex,vuex-persist 提供了类似的能力:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// store.js - vuex-persist
import Vuex from 'vuex'
import VuexPersistence from 'vuex-persist'
const vuexLocal = new VuexPersistence({
key: 'vuex-store', // localStorage key
storage: window.localStorage,
reducer: (state) => ({
// 只持久化需要的模块
auth: {
token: state.auth.token,
user: state.auth.user,
},
cart: state.cart,
settings: state.settings,
}),
// 过滤某些 mutation 后不触发持久化
filter: (mutation) => {
return mutation.type !== 'ui/SET_SCROLL_POSITION'
},
// 存储前的数据转换
serializer: {
serialize: (data) => {
// 加密敏感数据
return JSON.stringify(encryptData(data))
},
deserialize: (raw) => {
return decryptData(JSON.parse(raw))
},
},
})
const store = new Vuex.Store({
modules: {
auth, cart, settings, ui, notifications,
},
plugins: [vuexLocal.plugin],
})
Pinia 中的持久化(基于 pinia-plugin-persistedstate):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// stores/auth.js - Pinia 持久化
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const token = ref(null)
function setCredentials(u, t) {
user.value = u
token.value = t
}
function logout() {
user.value = null
token.value = null
}
return { user, token, setCredentials, logout }
}, {
// store 级别的持久化配置(通过 pinia-plugin-persistedstate)
persist: {
key: 'auth-store',
storage: localStorage,
paths: ['token'], // 只持久化 token,不持久化 user
},
})
// 全局配置
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
4. Zustand 的 persist 中间件
Zustand 内置了 persist 中间件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
// 基本持久化
const useBearStore = create(
persist(
(set, get) => ({
bears: 0,
fish: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
addFish: () => set((state) => ({ fish: state.fish + 1 })),
}),
{
name: 'bear-storage',
storage: createJSONStorage(() => localStorage),
// 只持久化部分字段
partialize: (state) => ({
bears: state.bears,
fish: state.fish,
}),
// 版本迁移
version: 3,
migrate: (persistedState, version) => {
if (version === 0) {
// v0: { count: number }
// v1: { bears: number, fish: number }
return {
bears: persistedState.count || 0,
fish: 0,
}
}
if (version === 1) {
// Update to v2: add timestamp
return {
...persistedState,
lastUpdated: Date.now(),
}
}
// Return as is (v2 → v3: unchanged)
return persistedState
},
}
)
)
自定义存储适配器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// custom-storage.ts - IndexedDB 存储适配器
import { createJSONStorage } from 'zustand/middleware'
import { IndexedDBStore } from './IndexedDBStore'
const dbStore = new IndexedDBStore('app-db', 'zustand', 1)
await dbStore.open()
export const indexedDBStorage = createJSONStorage(() => ({
getItem: async (name) => {
const record = await dbStore.get(name)
return record?.value || null
},
setItem: async (name, value) => {
await dbStore.set(name, value)
},
removeItem: async (name) => {
await dbStore.delete(name)
},
}))
// 使用 IndexedDB 作为存储后端
const useStore = create(
persist(
(set) => ({
// large data that wouldn't fit in localStorage
history: [],
cache: {},
}),
{
name: 'large-data',
storage: indexedDBStorage,
}
)
)
实战案例:多级表单的断点续填
下面是电子商务中多步骤表单的完整持久化实现。用户可以在填写过程中随时离开,再回来时继续。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// useFormPersistence.js - 表单持久化 Hook
import { useCallback, useEffect, useRef } from 'react'
const STORAGE_KEY_PREFIX = 'form_draft_'
export function useFormPersistence(formId, formState) {
// 使用防抖,防止频繁写入
const timerRef = useRef(null)
const storageKey = STORAGE_KEY_PREFIX + formId
// 自动保存(组件卸载或页面隐藏时)
useEffect(() => {
function save() {
try {
localStorage.setItem(storageKey, JSON.stringify({
data: formState,
savedAt: Date.now(),
version: 2,
}))
} catch (e) {
// localStorage 满时的降级策略
console.warn('Persistence save failed:', e)
// 可以尝试清除旧数据或使用 sessionStorage 降级
cleanupOldDrafts()
}
}
// 页面关闭/隐藏时保存
window.addEventListener('beforeunload', save)
return () => {
window.removeEventListener('beforeunload', save)
clearTimeout(timerRef.current)
}
}, [formState, storageKey])
// 手动保存(防抖)
const saveDraft = useCallback(() => {
clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => {
localStorage.setItem(storageKey, JSON.stringify({
data: formState,
savedAt: Date.now(),
version: 2,
}))
}, 500)
}, [formState, storageKey])
// 恢复草稿
const loadDraft = useCallback(() => {
try {
const raw = localStorage.getItem(storageKey)
if (!raw) return null
const draft = JSON.parse(raw)
return draft.data
} catch {
localStorage.removeItem(storageKey)
return null
}
}, [storageKey])
// 删除草稿(提交成功后调用)
const clearDraft = useCallback(() => {
localStorage.removeItem(storageKey)
}, [storageKey])
// 草稿信息
const getDraftInfo = useCallback(() => {
try {
const raw = localStorage.getItem(storageKey)
if (!raw) return null
const draft = JSON.parse(raw)
return {
exists: true,
savedAt: draft.savedAt,
age: Date.now() - draft.savedAt,
}
} catch {
return null
}
}, [storageKey])
return { saveDraft, loadDraft, clearDraft, getDraftInfo }
}
// 清理过期草稿
function cleanupOldDrafts() {
const ONE_WEEK = 7 * 24 * 60 * 60 * 1000
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key.startsWith(STORAGE_KEY_PREFIX)) {
try {
const data = JSON.parse(localStorage.getItem(key))
if (Date.now() - data.savedAt > ONE_WEEK) {
localStorage.removeItem(key)
}
} catch {
localStorage.removeItem(key)
}
}
}
}
多步骤表单中的使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
function CheckoutForm() {
const steps = ['shipping', 'payment', 'review']
const [currentStep, setCurrentStep] = useState(0)
const [formData, setFormData] = useState({
shipping: {},
payment: {},
billing: {},
})
const { loadDraft, clearDraft, saveDraft } = useFormPersistence('checkout_v2', formData)
// 初始化:恢复草稿
useEffect(() => {
const draft = loadDraft()
if (draft) {
// 提示用户有未完成的订单
if (confirm('检测到未完成的订单,是否继续填写?')) {
setFormData(draft)
// 跳转到上次中断的步骤
const savedStep = getSavedStep(draft)
if (savedStep !== null) setCurrentStep(savedStep)
}
}
}, [])
// 每次步骤变更时保存
const goToStep = (step) => {
saveDraft() // 保存当前步骤数据
setCurrentStep(step)
}
const handleSubmit = async () => {
await api.submitOrder(formData)
clearDraft() // 提交成功后清除草稿
// 跳转到成功页
}
return (
<div className="checkout-form">
<StepIndicator currentStep={currentStep} steps={steps} />
{currentStep === 0 && (
<ShippingForm
data={formData.shipping}
onChange={(data) => setFormData((prev) => ({ ...prev, shipping: data }))}
/>
)}
{currentStep === 1 && (
<PaymentForm
data={formData.payment}
onChange={(data) => setFormData((prev) => ({ ...prev, payment: data }))}
/>
)}
<div className="step-actions">
{currentStep > 0 && (
<button onClick={() => goToStep(currentStep - 1)}>上一步</button>
)}
{currentStep < steps.length - 1 ? (
<button onClick={() => goToStep(currentStep + 1)}>下一步</button>
) : (
<button onClick={handleSubmit}>提交订单</button>
)}
</div>
</div>
)
}
底层原理(含源码分析)
1. localStorage 的同步陷阱
localStorage 的 getItem 和 setItem 是同步操作,但浏览器实现中涉及磁盘 I/O。当存储大量数据时,同步调用会阻塞主线程:
1
2
3
4
5
6
7
8
9
// localStorage 的阻塞问题
console.time('localStorage write')
for (let i = 0; i < 100; i++) {
localStorage.setItem(`key_${i}`, 'x'.repeat(10000)) // 写入 10KB × 100
}
console.timeEnd('localStorage write')
// → 写入耗时可能达到 50-200ms,期间主线程完全阻塞
// 解决方案:使用 IndexedDB(异步)或分批次写入
异步持久化的价值:对于大型数据(如离线地图数据、用户操作日志),推荐使用 IndexedDB。其异步 API 不会阻塞主线程,并且支持事务和索引。
2. redux-persist 的源码核心
redux-persist 的核心流程分为持久化和水合两个阶段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// redux-persist 核心逻辑简化
function persistReducer(config, reducer) {
return (state, action) => {
if (action.type === 'PERSIST') {
// 持久化阶段:将 state 写入存储
const stored = config.serialize
? config.serialize(state)
: JSON.stringify(state)
config.storage.setItem(config.key, stored)
return state
}
if (action.type === 'REHYDRATE') {
if (action.key === config.key) {
// 水合阶段:从存储读取并还原 state
const restored = config.deserialize
? config.deserialize(action.payload)
: JSON.parse(action.payload)
// 使用 stateReconciler 合并策略
return config.stateReconciler(state, restored)
}
return state
}
// 正常 reducer
return reducer(state, action)
}
}
// PersistGate 组件
function PersistGate({ persistor, children, loading }) {
const [bootstrapped, setBootstrapped] = useState(false)
useEffect(() => {
// 启动水合
persistor.start()
persistor.subscribe(() => {
if (persistor.getState().bootstrapped) {
setBootstrapped(true)
}
})
}, [])
return bootstrapped ? children : loading
}
stateReconciler 的合并策略:
1
2
3
4
5
6
7
8
9
10
11
默认合并(level 0):
存储中的数据完全覆盖当前 state
→ 适合首次启动
自动合并 level 1:
只合并 state 中存在的 key
→ 防止旧数据中的过时 key 污染新 state
自动合并 level 2(推荐):
递归合并嵌套对象
→ 适合有嵌套状态的 reducer
3. 序列化与加密
敏感数据(如 token、用户信息)在持久化时应该加密:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// crypto-utils.js - 持久化加密
const ENCRYPTION_KEY = 'app-specific-key'
function encrypt(text) {
// 轻量级混淆(注意:不是真正的安全加密)
// 真正需要安全时应该使用 Web Crypto API
return btoa(
text.split('').map((char, i) =>
String.fromCharCode(char.charCodeAt(0) ^ ENCRYPTION_KEY.charCodeAt(i % ENCRYPTION_KEY.length))
).join('')
)
}
function decrypt(encoded) {
return atob(encoded).split('').map((char, i) =>
String.fromCharCode(char.charCodeAt(0) ^ ENCRYPTION_KEY.charCodeAt(i % ENCRYPTION_KEY.length))
).join('')
}
// 使用 Web Crypto API 的 AES-GCM 加密(生产级)
async function securelyEncrypt(text, password) {
const encoder = new TextEncoder()
const data = encoder.encode(text)
// 从 password 派生密钥
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveKey']
)
const key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: encoder.encode('unique-salt'),
iterations: 100000,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt']
)
const iv = crypto.getRandomValues(new Uint8Array(12))
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
data
)
// 返回 iv + ciphertext
const result = new Uint8Array(iv.length + encrypted.byteLength)
result.set(iv)
result.set(new Uint8Array(encrypted), iv.length)
return btoa(String.fromCharCode(...result))
}
高频面试题解析
Q1: localStorage 的容量限制(通常 5-10MB)满了怎么办?
考察点:对存储容量限制和降级策略的理解。
答案核心:
当 localStorage 满了时,setItem 会抛出一个 QuotaExceededError。处理方案:
- 存储分层策略:
1 2 3 4 5 6 7 8 9 10 11 12
// 分层存储适配器 class TieredStorage { async set(key, value) { const size = new Blob([value]).size if (size < 100 * 1024) { // < 100KB → localStorage localStorage.setItem(key, value) } else { // ≥ 100KB → IndexedDB await dbStore.set(key, value) } } }
- LRU 缓存淘汰:当 localStorage 满时,自动删除最久未访问的数据
- 压缩存储:使用 lz-string 等压缩算法压缩存储内容(文本通常可压缩 50-70%)
- 提示用户清理:当存储使用超过 80% 时,提示用户清理缓存
Q2: 持久化数据与运行时状态不一致如何处理?(水合冲突)
考察点:对版本迁移和数据一致性的理解。
答案核心:
水合冲突的常见原因和解决方案:
1
2
3
4
5
6
7
8
9
10
11
原因 1:数据结构变化
持久化的旧数据与当前代码期望的新结构不匹配
解决:版本迁移(migration)
原因 2:并发更新
多个标签页打开了同一个应用,各自独立持久化
解决:使用 storage 事件监听跨标签页变化
原因 3:部分更新
只持久化了 store 的一部分,但代码期望完整数据
解决:设置合理的默认值 + 深度合并(autoMergeLevel2)
1
2
3
4
5
6
7
8
9
// storage 事件监听——跨标签页同步
window.addEventListener('storage', (event) => {
if (event.key === 'app-storage') {
// 其他标签页修改了存储内容
const newData = JSON.parse(event.newValue)
// 与当前 state 合并
reconcileState(newData)
}
})
Q3: 如何持久化「不可序列化」的数据?(如 Set、Map、Date、函数)
考察点:对序列化限制和处理技巧的理解。
答案核心:
JSON.stringify 无法处理 Set、Map、Date、函数等特殊类型。解决方案:
- 自定义序列化器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
const serializer = { serialize: (data) => JSON.stringify(data, (key, value) => { if (value instanceof Set) return { __type: 'Set', values: [...value] } if (value instanceof Map) return { __type: 'Map', entries: [...value] } if (value instanceof Date) return { __type: 'Date', timestamp: value.getTime() } return value }), deserialize: (raw) => JSON.parse(raw, (key, value) => { if (value?.__type === 'Set') return new Set(value.values) if (value?.__type === 'Map') return new Map(value.entries) if (value?.__type === 'Date') return new Date(value.timestamp) return value }), }
- 不持久化不可序列化数据:在持久化之前剥离不可序列化的字段
- 使用结构克隆算法:
structuredClone()可以复制大部分数据类型,但无法处理函数和 Symbol - 替代方案:能在运行时重建的数据就不持久化(如计算结果、缓存数据)
总结与扩展
状态持久化是前端工程中「常常被忽略但至关重要」的环节。本文从存储引擎的基础知识出发,覆盖了 redux-persist、vuex-persist、Pinia、Zustand 等主流方案的配置,并通过多步骤表单的案例展示了完整实现。
关键原则:
- 按需持久化:不是所有状态都需要持久化,区分「用户期望恢复的」和「临时性的」
- 降级策略:存储满时要有降级方案,不能因此崩溃
- 版本管理:持久化数据要有版本号,支持迁移
- 敏感数据加密:token、个人信息在持久化时必须加密
扩展思考:
随着 PWA(渐进式 Web 应用)和离线优先(Offline First)模式的普及,状态持久化的需求正在演变。从简单的 localStorage 方案到完整的离线数据同步架构(如 Workbox + IndexedDB + Background Sync),持久化已经从「保存状态」进化为「构建离线体验的基础设施」。
另一个值得关注的趋势是浏览器即将(或正在)推出的 Storage Foundation API——一个更底层的、专为大容量存储设计的 API,有望替代 localStorage 成为新一代持久化标准。