原生localStorage到zustand + persist改造
文章目录
- 前言
- ❓ 直接用 `localStorage.getItem()` 分析
- 🚨 不足点:
- ✅ 推荐的做法:使用 Zustand `persist` 插件进行封装
- 📦 安装 persist 插件
- ✅ 改造后的 userStore.ts(推荐风格)
- ✅ 使用方式保持不变:
- 🔄 它还可以:
- 🔁 高级进阶:自定义存储引擎(例如 sessionStorage)
- ✅ 总结1
- ✅ 目标:三件事一起完成:
- 📦 1. 安装中间件(如果还没安装)
- ✨ 2. 改造后的 `userStore.ts`
- 🔁 3. 支持多标签页同步(比如退出登录)
- 📚 进阶建议(以后要做时)
- ✅ 总结2
- ✅ 目标
- ✅ 步骤一:封装一个 `broadcast.ts` 工具
- ✅ 步骤二:修改 `userStore.ts`,结合 `BroadcastChannel`
- ✅ 步骤三:监听广播消息,全局响应
- ✅ 效果演示(完整行为)
- 🔄 可扩展场景(常见)
- 📌 总结
- BroadcastService
- ✅ 第一步:定义类型和服务类
- ✅ 第二步:定义用户消息类型 & 实例化频道
- ✅ 第三步:在 `userStore.ts` 中使用统一服务广播
- ✅ 第四步:在 `main.tsx` 订阅消息,强制同步登录状态
- 🎉 最终你获得了:
- 🧩 下一步可扩展示例
- ✅ 系统通知频道 `notify-channel`
前言
一般在状态管理中,会用localStorage 来持久化 token ,
例如在react 的应用中:**Zustand 中使用原生 localStorage 来持久化 token **
❓ 直接用 localStorage.getItem()
分析
现在的实现方式是:
token: localStorage.getItem('token') || '',
setToken: (token) => {localStorage.setItem('token', token)set({ token })
}
虽然能用,但存在几个不足:
🚨 不足点:
问题 | 说明 |
---|---|
❌ 副作用写在 Store 内部 | localStorage 属于浏览器副作用操作,污染了 store 的纯粹性 |
❌ 不可复用 | 如果将来希望改用 sessionStorage 、IndexedDB 或 cookie,不易统一切换 |
❌ 不支持多标签页同步 | 如果多标签页登录/登出,状态不会自动同步 |
❌ 不支持结构化存储 | 若用户信息是对象,localStorage 只能保存字符串,管理复杂 |
❌ 不支持持久化插件 | Zustand 支持 persist 插件,可以自动存取+序列化/反序列化 |
✅ 推荐的做法:使用 Zustand persist
插件进行封装
Zustand 官方就提供了持久化插件,可以轻松实现持久化并保持 store 的清洁性。
📦 安装 persist 插件
pnpm add zustand-middleware
✅ 改造后的 userStore.ts(推荐风格)
import { create } from 'zustand'
import { persist } from 'zustand/middleware'interface UserState {token: stringsetToken: (token: string) => voidclearToken: () => void
}export const useUserStore = create<UserState>()(persist((set) => ({token: '',setToken: (token) => set({ token }),clearToken: () => set({ token: '' })}),{name: 'user-store', // localStorage keypartialize: (state) => ({ token: state.token }), // 可持久化字段})
)
✅ 使用方式保持不变:
const token = useUserStore(state => state.token)
const setToken = useUserStore(state => state.setToken)
而且内部的持久化逻辑都交给 zustand/middleware
来处理了 ✅
🔄 它还可以:
- 自动从
localStorage
读取初始化值 - 支持序列化复杂对象
- 支持多个状态切片持久化(例如 token、theme、userInfo 分别存储)
- 支持同步事件监听(例如:退出登录广播)
🔁 高级进阶:自定义存储引擎(例如 sessionStorage)
persist((set) => ({ ... }),{name: 'user-store',storage: {getItem: (key) => sessionStorage.getItem(key),setItem: (key, value) => sessionStorage.setItem(key, value),removeItem: (key) => sessionStorage.removeItem(key),}}
)
✅ 总结1
方法 | 是否推荐 | 说明 |
---|---|---|
原生 localStorage 调用 | 🚫 不推荐 | 无法复用、副作用重、扩展性差 |
zustand + persist 插件 | ✅ 推荐 | 大厂常用方式,结构清晰、支持扩展和统一管理 |
✅ 目标:三件事一起完成:
✅ 使用 zustand/persist 插件实现持久化
✅ 支持多标签页 token 同步(退出登录后其他页也退出)
✅ 保持代码整洁,方便后续扩展(支持 userInfo、theme 等)
升级 Zustand
用户状态管理:
📦 1. 安装中间件(如果还没安装)
pnpm add zustand-middleware
✨ 2. 改造后的 userStore.ts
📁 src/store/user.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'interface UserState {token: stringsetToken: (token: string) => voidclearToken: () => void
}export const useUserStore = create<UserState>()(persist((set) => ({token: '',setToken: (token) => {set({ token })// 👇 触发多标签页广播(可选)localStorage.setItem('token-updated', Date.now().toString())},clearToken: () => {set({ token: '' })localStorage.setItem('token-updated', Date.now().toString())},}),{name: 'user-store', // localStorage keypartialize: (state) => ({ token: state.token }), // 只存 token})
)
🔁 3. 支持多标签页同步(比如退出登录)
📁 在 src/main.tsx
中监听 storage
事件
window.addEventListener('storage', (event) => {if (event.key === 'token-updated') {const token = localStorage.getItem('user-store')if (token) {const parsed = JSON.parse(token)const current = parsed.state.tokenconst storeToken = useUserStore.getState().tokenif (storeToken && !current) {// token 被清空,触发登出location.href = '/login'}}}
})
🔄 效果:在 A 页退出登录,B 页会立即刷新跳转回 /login
📚 进阶建议(以后要做时)
需求 | 做法 |
---|---|
保存 userInfo | state.userInfo + partialize 中加上它 |
使用 sessionStorage | 替换 persist 的 storage 为 sessionStorage |
Token 自动刷新 | 配合 Axios 拦截器,统一处理 401 逻辑 |
权限控制 | 加个 roles 字段,配合路由动态控制 |
✅ 总结2
现在的登录状态管理已经是:
- ✅ 持久化(自动保存/恢复 token)
- ✅ 无副作用(副作用交给中间件处理)
- ✅ 多标签页同步(用户体验拉满)
- ✅ 适合的结构(易扩展、可维护、好调试)
进一步:相比传统的 storage 事件,现代浏览器推荐使用 BroadcastChannel API 来实现多标签页之间的通信,性能更高,功能更强,特别适合多标签页状态同步,如:强制登出、通知刷新、全局消息推送等。
✅ 目标
构建一个 强一致性、主动广播、现代化的多标签页通信机制 来同步登录状态。
✅ 步骤一:封装一个 broadcast.ts
工具
📁 src/utils/broadcast.ts
// 全局广播频道(支持跨页面通信)
export const userChannel = new BroadcastChannel('user-channel')// 消息类型(可扩展)
export type UserChannelMessage =| { type: 'logout' }| { type: 'login'; token: string }| { type: 'sync' } // 预留
✅ 步骤二:修改 userStore.ts
,结合 BroadcastChannel
📁 src/store/user.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { userChannel } from '@/utils/broadcast'interface UserState {token: stringsetToken: (token: string) => voidclearToken: () => void
}export const useUserStore = create<UserState>()(persist((set) => ({token: '',setToken: (token) => {set({ token })userChannel.postMessage({ type: 'login', token })},clearToken: () => {set({ token: '' })userChannel.postMessage({ type: 'logout' })},}),{name: 'user-store',partialize: (state) => ({ token: state.token }),})
)
✅ 步骤三:监听广播消息,全局响应
📁 在 src/main.tsx
里添加监听器:
import { userChannel } from '@/utils/broadcast'
import { useUserStore } from '@/store/user'// 跨标签页同步响应:强制登录/登出
userChannel.onmessage = (event) => {const { type, token } = event.dataif (type === 'logout') {useUserStore.getState().clearToken()window.location.href = '/login'}if (type === 'login' && token) {useUserStore.getState().setToken(token)}
}
✅ 效果演示(完整行为)
操作 | 行为 |
---|---|
登录页面点击登录 | 所有标签页同步设置 token |
任意页面点击登出 | 所有标签页立即跳转到 /login ,并清除 token |
不依赖 localStorage 的副作用或轮询 | ☑️ 性能更好 ☑️ 响应更快 ☑️ 可扩展性更强 |
🔄 可扩展场景(常见)
应用 | 实现 |
---|---|
🔐 Token刷新 | 在一个页中刷新 token 后广播 setToken(token) |
🔔 全局提示 | userChannel.postMessage({ type: 'notify', msg }) |
🧠 状态同步 | 多 tab 同步 userInfo、theme、unreadCount 等状态 |
📌 总结
能力 | BroadcastChannel vs localStorage |
---|---|
多标签页同步 | ✅ 强一致性 |
支持主动推送 | ✅ |
跨域支持 | ❌(同源限制) |
性能 | ✅ 高效 |
BroadcastService
接下来我们封装一个大厂常用的 BroadcastService
服务类,支持:
- ✅ 统一管理多个频道
- ✅ 可注册多个监听器,支持解绑
- ✅ 支持类型安全的广播消息
- ✅ 支持扩展多个频道(如
user-channel
、notify-channel
)
✅ 第一步:定义类型和服务类
📁 src/utils/broadcast.ts
type MessageHandler<T> = (msg: T) => void// 通用广播服务类
export class BroadcastService<TMessage> {private channel: BroadcastChannelprivate listeners = new Set<MessageHandler<TMessage>>()constructor(channelName: string) {this.channel = new BroadcastChannel(channelName)this.channel.onmessage = (event: MessageEvent<TMessage>) => {this.listeners.forEach((handler) => {try {handler(event.data)} catch (err) {console.error(`[BroadcastService] handler error`, err)}})}}post(msg: TMessage) {this.channel.postMessage(msg)}subscribe(handler: MessageHandler<TMessage>) {this.listeners.add(handler)}unsubscribe(handler: MessageHandler<TMessage>) {this.listeners.delete(handler)}close() {this.channel.close()this.listeners.clear()}
}
✅ 第二步:定义用户消息类型 & 实例化频道
📁 src/utils/channels.ts
import { BroadcastService } from './broadcast'export type UserChannelMessage =| { type: 'login'; token: string }| { type: 'logout' }export const userChannel = new BroadcastService<UserChannelMessage>('user-channel')
✅ 第三步:在 userStore.ts
中使用统一服务广播
📁 src/store/user.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { userChannel } from '@/utils/channels'interface UserState {token: stringsetToken: (token: string) => voidclearToken: () => void
}export const useUserStore = create<UserState>()(persist((set) => ({token: '',setToken: (token) => {set({ token })userChannel.post({ type: 'login', token })},clearToken: () => {set({ token: '' })userChannel.post({ type: 'logout' })}}),{name: 'user-store',partialize: (state) => ({ token: state.token })})
)
✅ 第四步:在 main.tsx
订阅消息,强制同步登录状态
📁 src/main.tsx
import { userChannel } from '@/utils/channels'
import { useUserStore } from '@/store/user'userChannel.subscribe((msg) => {if (msg.type === 'logout') {useUserStore.getState().clearToken()location.href = '/login'}if (msg.type === 'login' && msg.token) {useUserStore.getState().setToken(msg.token)}
})
🎉 最终你获得了:
- 🔄 多标签页同步(性能更高,无需依赖 localStorage)
- 📦 解耦广播逻辑(支持复用与扩展)
- 🧠 类型安全(可扩展通知、消息、刷新、Theme 切换等)
🧩 下一步可扩展示例
✅ 系统通知频道 notify-channel
export type NotifyChannelMessage = { type: 'alert'; message: string }
export const notifyChannel = new BroadcastService<NotifyChannelMessage>('notify-channel')// notifyChannel.post({ type: 'alert', message: '你有一条新消息!' })