当前位置: 首页 > news >正文

原生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 的纯粹性
❌ 不可复用如果将来希望改用 sessionStorageIndexedDB 或 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


📚 进阶建议(以后要做时)

需求做法
保存 userInfostate.userInfo + partialize 中加上它
使用 sessionStorage替换 persiststorage 为 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-channelnotify-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: '你有一条新消息!' })

相关文章:

  • [密码学基础]密码学发展简史:从古典艺术到量子安全的演进
  • 碰一碰发视频系统源码搭建全解析:定制化开发
  • 芝法酱躺平攻略(21)——kafka安装和使用
  • LabVIEW 程序维护:为何选靠谱团队?
  • 纯FPGA控制AD9361的思路和实现之一 概述
  • JVM 系列:JVM 内存结构深度解析
  • Day10【基于encoder- decoder架构实现新闻文本摘要的提取】
  • 面向对象设计中的类的分类:实体类、控制类和边界类
  • 暨南大学 2024年ACM程序设计校赛 题解与知识点分析
  • SOA 核心三要素:服务、构件与对象的深度解析
  • 毕业答辩的PPT应该包括哪些内容?
  • Grallvm技术介绍
  • 从 LabelImg 到 Label Studio!AI 数据标注神器升级,Web 版真香
  • 【网络初识】从零开始彻底了解网络编程(一)
  • 企业网站安装 SSL安装的必要性
  • C++学习之路,从0到精通的征途:vector类的模拟实现
  • 【网络原理】UDP协议
  • 动手实现文本生成模型:基于 Decoder-only Transformer (PyTorch)
  • 深入实战:使用C++开发高性能RESTful API
  • Flask应用部署通用指南
  • 体坛联播|巴萨三球逆转塞尔塔,CBA季后赛山西横扫广东
  • 经济日报金观平:拥抱中国就是拥抱确定性
  • 中央和国家机关工委建立健全整治形式主义为基层减负长效机制
  • 全国首个医工交叉“MD+PhD”双博士培养项目在沪启动
  • 译者手记|如何量化家庭历史
  • 世卫成员国就《大流行病协议》达成一致,首次演练应对气候诱发的病毒危机