# 中间件：Persist 持久化

URL: https://caijiao.org/zustand/guide/middleware-persist
Source: docs/zustand/guide/middleware-persist.md
Description: 了解如何使用 Zustand 的 persist 中间件将状态持久化到本地存储，包括基本用法、配置选项、自定义存储引擎、部分持久化、状态迁移以及与其他中间件结合使用的方法。

[Zustand](../) 提供了一个强大的 `persist` 中间件，用于将 Store 的状态持久化到本地存储（如 localStorage、sessionStorage 或自定义存储）中。这意味着即使页面刷新或浏览器关闭，应用的状态也能被保存和恢复。

## 什么是 Persist 中间件？

`persist` 中间件允许你：

- **状态持久化**：将 Store 状态保存到浏览器的本地存储
- **自动恢复**：页面刷新后自动恢复之前的状态
- **灵活配置**：支持不同的存储引擎、键名自定义、序列化/反序列化等

## 安装与基本使用

### 安装依赖

`persist` 中间件是 Zustand 的核心功能之一，不需要额外安装。

### 基本用法

```javascript
import { create } from 'zustand'
import { persist } from 'zustand/middleware' // 导入 persist 中间件

// 创建一个带有 persist 中间件的 Store
const useCounterStore = create(
  persist(
    (set, get) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
      reset: () => set({ count: 0 }),
    }),
    {
      name: 'counter-storage', // 存储在本地存储中的键名
    }
  )
)
```

### 在组件中使用

```jsx
function Counter() {
  const { count, increment, decrement, reset } = useCounterStore()

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}
```

现在，当你刷新页面时，计数器的状态会自动从本地存储中恢复。

## 配置选项

`persist` 中间件提供了丰富的配置选项：

```javascript
const useStore = create(
  persist(
    (set, get) => ({
      // Store 定义
    }),
    {
      name: 'my-store', // 存储键名，必填
      storage: localStorage, // 存储引擎，默认为 localStorage
      version: 1, // 版本号，用于迁移
      migrate: (persistedState, version) => {
        // 状态迁移函数
        if (version === 0) {
          // 从版本 0 迁移到版本 1
          return { ...persistedState, newField: 'default' }
        }
        return persistedState
      },
      partialize: (state) => {
        // 部分持久化，只保存需要的数据
        return {
          count: state.count,
          user: state.user,
        }
      },
      serialize: (state) => JSON.stringify(state), // 序列化函数
      deserialize: (str) => JSON.parse(str), // 反序列化函数
    }
  )
)
```

### 主要配置选项说明

| 选项 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `name` | string | - | 存储在本地存储中的键名，必填项 |
| `storage` | Storage | localStorage | 存储引擎，可以是 localStorage、sessionStorage 或自定义存储 |
| `version` | number | 0 | 版本号，用于状态迁移 |
| `migrate` | function | (state, version) => state | 状态迁移函数，当版本号变化时调用 |
| `partialize` | function | (state) => state | 部分持久化函数，用于选择需要保存的状态 |
| `serialize` | function | JSON.stringify | 序列化函数，将状态转换为字符串 |
| `deserialize` | function | JSON.parse | 反序列化函数，将字符串转换为状态 |

## 自定义存储引擎

你可以使用自定义的存储引擎，而不仅仅局限于浏览器的原生存储：

```javascript
// 示例：使用 IndexedDB 作为存储引擎
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import idbKeyval from 'idb-keyval' // 需要安装 idb-keyval 库

// 创建自定义存储引擎
const indexedDBStorage = {
  getItem: async (name) => {
    const value = await idbKeyval.get(name)
    return value || null
  },
  setItem: async (name, value) => {
    await idbKeyval.set(name, value)
  },
  removeItem: async (name) => {
    await idbKeyval.del(name)
  },
}

const useStore = create(
  persist(
    (set, get) => ({
      // Store 定义
    }),
    {
      name: 'my-store',
      storage: indexedDBStorage, // 使用自定义的 IndexedDB 存储
    }
  )
)
```

## 部分持久化

使用 `partialize` 选项，你可以选择只持久化 Store 中的部分状态：

```javascript
const useUserStore = create(
  persist(
    (set, get) => ({
      user: {
        id: 1,
        name: 'John Doe',
        email: 'john@example.com',
      },
      isLoggedIn: true,
      theme: 'dark',
      // 临时状态，不需要持久化
      tempData: 'some temporary data',
      
      updateUser: (user) => set({ user }),
      toggleTheme: () => set((state) => ({ theme: state.theme === 'dark' ? 'light' : 'dark' })),
    }),
    {
      name: 'user-storage',
      partialize: (state) => ({
        // 只持久化这些字段
        user: state.user,
        isLoggedIn: state.isLoggedIn,
        theme: state.theme,
      }),
    }
  )
)
```

## 状态迁移

当你的应用版本升级，Store 结构发生变化时，可以使用 `migrate` 选项进行状态迁移：

```javascript
const useTodoStore = create(
  persist(
    (set, get) => ({
      todos: [],
      addTodo: (text) => set((state) => ({
        todos: [...state.todos, { id: Date.now(), text, completed: false, createdAt: new Date() }],
      })),
      toggleTodo: (id) => set((state) => ({
        todos: state.todos.map((todo) => 
          todo.id === id ? { ...todo, completed: !todo.completed } : todo
        ),
      })),
    }),
    {
      name: 'todo-storage',
      version: 2, // 版本号从 1 升级到 2
      migrate: (persistedState, version) => {
        if (version === 1) {
          // 从版本 1 迁移到版本 2
          return {
            ...persistedState,
            // 为旧的 todos 添加 createdAt 字段
            todos: persistedState.todos.map((todo) => ({
              ...todo,
              createdAt: new Date().toISOString(),
            })),
          }
        }
        return persistedState
      },
    }
  )
)
```

## 与其他中间件结合使用

`persist` 中间件可以与其他 Zustand 中间件结合使用，例如 `immer`：

```javascript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer' // 导入 immer 中间件

const useStore = create(
  persist(
    immer((set, get) => ({
      count: 0,
      user: { name: 'John', age: 30 },
      
      increment: () => set((state) => { state.count += 1 }),
      updateUser: (updates) => set((state) => { 
        Object.assign(state.user, updates) 
      }),
    })),
    {
      name: 'combined-storage',
    }
  )
)
```

## 手动控制持久化

你可以通过 Store 的 `persist` 属性手动控制持久化操作：

```javascript
const useStore = create(
  persist(
    (set, get) => ({
      // Store 定义
    }),
    {
      name: 'my-store',
    }
  )
)

// 组件中使用
function MyComponent() {
  const store = useStore()
  
  // 手动触发持久化
  const forcePersist = () => {
    store.persist.rehydrate() // 强制重新恢复状态
  }
  
  // 清除持久化的状态
  const clearStorage = () => {
    store.persist.clearStorage() // 清除存储中的状态
  }
  
  // 获取持久化的状态
  const getPersistedState = async () => {
    const state = await store.persist.getStorageValue()
    console.log('Persisted state:', state)
  }
  
  return (
    <div>
      <button onClick={forcePersist}>重新恢复状态</button>
      <button onClick={clearStorage}>清除存储</button>
      <button onClick={getPersistedState}>获取持久化状态</button>
    </div>
  )
}
```

## 常见问题与注意事项

### 1. 循环引用问题

`JSON.stringify` 无法处理循环引用，如果你的状态中包含循环引用，需要提供自定义的 `serialize` 函数：

```javascript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

const useStore = create(
  persist(
    (set, get) => ({
      // 包含循环引用的状态
      node: { value: 1, parent: null },
      setParent: () => set((state) => {
        const newNode = { value: 2, parent: state.node }
        return { node: newNode }
      }),
    }),
    {
      name: 'circular-store',
      // 使用自定义序列化函数
      serialize: (state) => {
        // 处理循环引用
        const { node, ...rest } = state
        return JSON.stringify({
          ...rest,
          node: { value: node.value }, // 只序列化必要的字段
        })
      },
    }
  )
)
```

### 2. 性能考虑

- **避免持久化大型对象**：只持久化必要的数据
- **合理使用 partialize**：只保存需要的数据字段
- **考虑使用 IndexedDB**：对于大型数据集，考虑使用 IndexedDB 而不是 localStorage

### 3. 安全性

- **敏感数据**：不要将敏感数据（如密码、令牌）存储在客户端存储中
- **数据验证**：恢复状态时，始终验证数据的有效性

## 最佳实践总结

1. **合理使用**：只持久化必要的状态，避免过度使用
2. **版本管理**：使用版本号和迁移函数管理状态变化
3. **数据验证**：恢复状态时验证数据的有效性
4. **性能优化**：使用 `partialize` 减少持久化的数据量
5. **安全考虑**：避免存储敏感信息

在接下来的章节中，我们将学习 [中间件：Immer 简化状态修改](middleware-immer.md)，了解如何使用 Immer 简化 Zustand 中的状态修改。
