状态管理是前端应用的"心脏",但很多人一提到 Pinia/Vuex 就头疼:Store 该怎么设计?持久化怎么做?登录态怎么维护?这篇文章以心动恋聊 小程序为例,通过和 AI 的真实对话,展示如何从零搭建一个完整的用户状态管理系统。
系列专栏 :【AI 编程实战:TRAE SOLO 全栈开发指南】
本篇主题:Pinia 状态管理 - 从混乱代码到优雅架构
实战项目:心动恋聊 - AI 恋爱聊天助手
一、开篇:状态管理的痛点
1.1 没有状态管理时的混乱
在没有集中式状态管理之前,我的代码是这样的:
typescript
// 页面 A:登录后保存用户信息
uni.setStorageSync('userInfo', userData);
uni.setStorageSync('token', token);
uni.setStorageSync('isLoggedIn', true);
// 页面 B:读取用户信息
const userInfo = uni.getStorageSync('userInfo');
const isLoggedIn = uni.getStorageSync('isLoggedIn');
// 页面 C:更新用户信息
const userInfo = uni.getStorageSync('userInfo');
userInfo.avatar = newAvatar;
uni.setStorageSync('userInfo', userInfo);
// 页面 D:判断登录状态
if (uni.getStorageSync('token')) {
// 已登录逻辑
}
问题显而易见:
- 每个页面都在重复读写 Storage
- 状态分散在各处,难以追踪
- 修改一处,容易漏改其他地方
- 没有响应式,页面不会自动更新
1.2 我需要什么
我需要一个集中式的用户状态管理:
- 统一管理用户信息、登录态
- 支持持久化(刷新不丢失)
- 响应式更新(数据变化,页面自动刷新)
- 多种登录方式支持(微信、手机号)
但我对 Pinia 不熟悉,于是开始和 AI 对话。
二、基础 Store 搭建
2.1 我的第一句话
txt
我:项目需要用户状态管理,用 Pinia 实现。
要管理用户信息、登录状态,支持登录、退出功能。
2.2 AI 先问问题
txt
AI:好的,我来帮你搭建用户状态管理。先确认几个问题:
🤔 问题:
1. 项目技术栈是什么?(Vue3/UniApp/其他)
2. 用户信息需要哪些字段?
3. 登录方式有哪些?(账号密码/微信/手机号)
4. 需要持久化吗?(刷新页面后保持登录)
请告诉我这些信息,我来设计 Store 结构。
2.3 我补充信息
txt
我:UniApp + Vue3 项目,跑微信小程序。
用户信息:user_id、username、avatar、gender、phone_number、vip_status 这些。
登录方式:主要是微信登录,后面可能加手机号登录。
需要持久化,用户登录后刷新不能退出。
2.4 AI 给出执行计划
txt
AI:明白了,我来设计 Store 结构。
📋 执行计划:
1. 创建 src/store/index.ts - Pinia 入口配置
2. 创建 src/store/user.ts - 用户状态管理
3. 在 main.ts 中注册 Pinia
📐 Store 结构设计:
src/store/
├── index.ts # Pinia 实例 + 插件配置
└── user.ts # 用户状态管理
├── state # userInfo, isLoggedIn
├── getters # genderDisplay, isVip
└── actions # login, logout, updateUserInfo
🤔 关于语法风格,有两种选择:
1. Options API 风格:state/getters/actions 分开写
2. Setup 风格:用 ref/computed/function,更灵活
你想用哪种风格?
txt
我:用 Setup 风格吧,看起来更简洁
2.5 AI 生成基础代码
txt
AI:好的,开始生成代码...
✅ 创建 src/store/index.ts
✅ 创建 src/store/user.ts
✅ 修改 main.ts
基础结构已完成,这是核心代码:
typescript
// src/store/index.ts
import { createPinia } from 'pinia';
const store = createPinia();
export default store;
export * from './user';
typescript
// src/store/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
// 用户信息类型
interface UserInfo {
user_id: string;
username: string;
avatar: string;
gender?: number; // 1: 男, 2: 女
phone_number?: string;
vip_status: 'none' | 'vip' | 'expired';
}
// 初始状态
const initialUserInfo: UserInfo = {
user_id: '',
username: '',
avatar: '/static/images/default-avatar.png',
gender: undefined,
phone_number: '',
vip_status: 'none',
};
export const useUserStore = defineStore('user', () => {
// ===== 状态 =====
const userInfo = ref<UserInfo>({ ...initialUserInfo });
const isLoggedIn = ref(false);
// ===== 计算属性 =====
const isVip = computed(() => userInfo.value.vip_status === 'vip');
// ===== 方法 =====
/** 设置用户信息 */
const setUserInfo = (info: UserInfo) => {
userInfo.value = { ...initialUserInfo, ...info };
isLoggedIn.value = !!info.user_id;
};
/** 清除用户信息 */
const clearUserInfo = () => {
userInfo.value = { ...initialUserInfo };
isLoggedIn.value = false;
};
/** 退出登录 */
const logout = () => {
clearUserInfo();
};
return {
userInfo,
isLoggedIn,
isVip,
setUserInfo,
clearUserInfo,
logout,
};
});
txt
AI:基础 Store 已创建。使用方式:
// 在组件中
import { useUserStore } from '@/store'
const userStore = useUserStore()
// 读取状态
console.log(userStore.userInfo.username)
console.log(userStore.isLoggedIn)
// 调用方法
userStore.setUserInfo({ user_id: '123', username: '小明', ... })
userStore.logout()
现在可以测试一下基础功能。
三、持久化配置
3.1 我发现刷新后状态丢失
测试后,我发现一个问题:
txt
我:登录后刷新页面,状态丢失了,又变成未登录状态。
你不是说要支持持久化吗?
3.2 AI 解释并修复
txt
AI:抱歉,我忘了加持久化配置。
🔍 原因:
Pinia 默认不持久化,状态只存在内存中,刷新就没了。
🔧 解决方案:
使用 pinia-plugin-persistedstate 插件,自动将状态同步到 Storage。
⚠️ 小程序注意:
小程序没有 localStorage,需要用 uni.getStorageSync/setStorageSync。
我来修改配置...
typescript
// src/store/index.ts(修改后)
import { createPinia } from 'pinia';
import { createPersistedState } from 'pinia-plugin-persistedstate';
const store = createPinia();
// 配置持久化插件,适配小程序存储 API
store.use(
createPersistedState({
storage: {
getItem: uni.getStorageSync,
setItem: uni.setStorageSync,
},
}),
);
export default store;
export * from './user';
typescript
// src/store/user.ts(添加持久化配置)
export const useUserStore = defineStore(
'user',
() => {
// ... 原有代码不变
},
{
persist: true, // 开启持久化
},
);
txt
AI:
✅ 已安装 pinia-plugin-persistedstate
✅ 已修改 src/store/index.ts
✅ 已修改 src/store/user.ts
现在刷新页面,登录状态会保持。
⚠️ 注意:
persist: true 会持久化整个 state。
如果某些状态不想持久化,可以用 paths 指定:
{
persist: {
paths: ['userInfo', 'isLoggedIn'], // 只持久化这些
},
}
3.3 我验证修复成功
txt
我:好了,刷新后状态保持了。但我还有个问题:
token 现在是存在 Store 里还是 Storage 里?
3.4 AI 解释 Token 存储策略
txt
AI:好问题!目前 token 没有放在 Store 里。
🤔 关于 Token 存储,有两种策略:
【策略一】Token 放 Store(持久化到 Storage)
优点:统一管理,响应式
缺点:Store 初始化前无法使用
【策略二】Token 单独存 Storage(推荐)
优点:HTTP 拦截器可以直接读取,不依赖 Store 初始化
缺点:需要单独管理
📌 推荐策略二,原因:
HTTP 请求拦截器需要在应用启动最早期就能获取 token,
此时 Store 可能还没初始化完成。
代码示例:
// 登录成功时
uni.setStorageSync('token', response.token)
setUserInfo(response.user)
// HTTP 拦截器中
const token = uni.getStorageSync('token')
你想用哪种策略?
txt
我:用策略二,Token 单独存
四、完善登录流程
4.1 我要添加微信登录
txt
我:现在要加微信登录功能。
流程是:调用 uni.login 获取 code → 发给后端换 token 和用户信息。
4.2 AI 设计登录方法
txt
AI:明白,我来添加微信登录方法。
📋 执行计划:
1. 添加 wechatLogin 方法到 userStore
2. 处理登录成功:保存 token + 设置用户信息
3. 处理登录失败:提示错误
🤔 问题:
1. 登录 API 接口路径是什么?
2. 登录成功后要不要自动跳转?
3. 需要 loading 和 toast 提示吗?
txt
我:API 是 /api/auth/wechat-login,参数是 { code }。
不需要自动跳转,由调用方决定。
需要 toast 提示成功/失败。
txt
AI:好的,添加登录方法:
typescript
// src/store/user.ts(添加登录方法)
import { toast } from '@/utils/toast';
import { loginByWechat } from '@/api/auth';
export const useUserStore = defineStore(
'user',
() => {
// ... 原有状态和方法
/**
* 微信登录
*/
const wechatLogin = async (code: string) => {
try {
const response = await loginByWechat({ code });
if (response.code !== 200) {
toast.error(response.msg || '登录失败');
throw new Error(response.msg || '登录失败');
}
const { token, user } = response.data;
// 保存 token(单独存 Storage)
uni.setStorageSync('token', token);
// 设置用户信息
setUserInfo(user);
toast.success('登录成功');
return response.data;
} catch (error) {
console.error('微信登录失败:', error);
toast.error('登录失败,请稍后再试');
throw error;
}
};
/**
* 退出登录(更新)
*/
const logout = () => {
clearUserInfo();
uni.removeStorageSync('token'); // 清除 token
toast.success('已退出登录');
};
return {
// ... 原有导出
wechatLogin,
logout,
};
},
{ persist: true },
);
txt
AI:使用方式:
// 页面中调用
const handleWechatLogin = async () => {
try {
// 1. 获取微信 code
const { code } = await new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: resolve,
fail: reject,
})
})
// 2. 调用 Store 登录方法
await userStore.wechatLogin(code)
// 3. 登录成功后的操作(由页面决定)
uni.navigateBack()
} catch (error) {
console.error('登录失败:', error)
}
}
4.3 我追问:登录态初始化
txt
我:还有个问题,应用启动时怎么恢复登录态?
比如用户之前登录过,打开应用应该自动是登录状态。
4.4 AI 添加初始化逻辑
txt
AI:需要添加一个初始化方法,在应用启动时调用。
🔍 初始化逻辑:
1. 检查本地是否有 token 和 userInfo
2. 如果有,恢复登录状态
3. 异步请求最新用户信息(token 可能过期)
4. 如果 token 过期,清除登录态
typescript
// src/store/user.ts(添加初始化方法)
export const useUserStore = defineStore(
'user',
() => {
// ... 原有代码
/**
* 初始化用户状态(应用启动时调用)
*/
const initializeUserState = () => {
const token = uni.getStorageSync('token');
const savedUserInfo = uni.getStorageSync('userInfo');
const savedLoginStatus = uni.getStorageSync('isLoggedIn');
// 检查是否有完整的登录信息
if (token && savedUserInfo?.user_id && savedLoginStatus) {
// 恢复登录态
userInfo.value = savedUserInfo;
isLoggedIn.value = true;
// 异步获取最新用户信息(校验 token 有效性)
loadUserInfo();
} else {
// 清除可能存在的无效数据
clearUserInfo();
uni.removeStorageSync('token');
}
};
/**
* 从服务器加载最新用户信息
*/
const loadUserInfo = async () => {
try {
const response = await apiGetUserInfo();
if (response.code === 200) {
setUserInfo(response.data);
} else {
// token 可能已过期
clearUserInfo();
uni.removeStorageSync('token');
}
} catch (error) {
console.error('获取用户信息失败:', error);
clearUserInfo();
uni.removeStorageSync('token');
}
};
return {
// ... 原有导出
initializeUserState,
loadUserInfo,
};
},
{ persist: true },
);
txt
AI:在 App.vue 的 onLaunch 中调用初始化:
// App.vue
<script setup lang="ts">
import { useUserStore } from '@/store'
const userStore = useUserStore()
onLaunch(() => {
userStore.initializeUserState()
})
</script>
这样应用启动时会自动恢复登录态。
五、计算属性与统一更新
5.1 我需要显示性别文本
txt
我:用户信息里 gender 是数字(1=男,2=女),
但页面上要显示"男生"、"女生",每次都要写判断很麻烦。
5.2 AI 添加计算属性
txt
AI:用计算属性(computed)解决,在 Store 里统一处理。
这样所有页面都可以直接用 userStore.genderDisplay,不用重复判断。
typescript
// src/store/user.ts(添加计算属性)
import { GenderEnum } from 'shared-types';
export const useUserStore = defineStore(
'user',
() => {
// ... 原有状态
// ===== 计算属性 =====
/** 性别显示文本 */
const genderDisplay = computed(() => {
switch (userInfo.value.gender) {
case GenderEnum.MALE: // 1
return '男生';
case GenderEnum.FEMALE: // 2
return '女生';
default:
return '未设置';
}
});
/** 是否是 VIP */
const isVip = computed(() => userInfo.value.vip_status === 'vip');
/** 根据性别获取默认头像 */
const defaultAvatarByGender = computed(() => {
switch (userInfo.value.gender) {
case GenderEnum.MALE:
return '/static/images/avatar-male.png';
case GenderEnum.FEMALE:
return '/static/images/avatar-female.png';
default:
return '/static/images/avatar-default.png';
}
});
return {
// 状态
userInfo,
isLoggedIn,
// 计算属性
genderDisplay,
isVip,
defaultAvatarByGender,
// 方法
// ...
};
},
{ persist: true },
);
txt
AI:使用方式:
<template>
<view>
<text>性别:{{ userStore.genderDisplay }}</text>
<image :src="userStore.userInfo.avatar || userStore.defaultAvatarByGender" />
<text v-if="userStore.isVip">VIP 用户</text>
</view>
</template>
📌 计算属性的好处:
1. 逻辑集中,不用每个页面重复写
2. 响应式,userInfo 变化自动更新
3. 有缓存,多次访问只计算一次
5.3 我追问:更新用户信息
txt
我:用户在"我的"页面可以修改性别、昵称,
修改后要同步更新 Store 和服务器,怎么做比较好?
5.4 AI 添加统一更新方法
txt
AI:添加一个统一的更新方法,封装 API 调用 + Store 更新 + 提示。
这样所有修改用户信息的地方都调用同一个方法,保证一致性。
typescript
// src/store/user.ts(添加更新方法)
import { updateUserInfo as apiUpdateUserInfo } from '@/api/auth';
/**
* 统一更新用户信息
* @param updateData - 要更新的字段
* @param successMessage - 成功提示,默认"修改成功"
*/
const updateUserInfo = async (
updateData: Partial<Pick<UserInfo, 'gender' | 'username' | 'avatar'>>,
successMessage = '修改成功',
) => {
try {
// 1. 调用 API 更新服务器
const response = await apiUpdateUserInfo(updateData);
if (response.code === 200 && response.data) {
// 2. 更新 Store(用服务器返回的最新数据)
setUserInfo(response.data);
// 3. 提示成功
toast.success(successMessage);
} else {
toast.error(response.msg || '修改失败');
}
} catch (error) {
console.error('更新用户信息失败:', error);
toast.error('修改失败,请稍后再试');
}
};
txt
AI:使用方式:
// 修改性别
await userStore.updateUserInfo({ gender: 1 })
// 修改昵称,自定义提示
await userStore.updateUserInfo(
{ username: '新昵称' },
'昵称修改成功'
)
// 同时修改多个
await userStore.updateUserInfo({
username: '新昵称',
avatar: 'https://xxx.com/avatar.png',
})
六、跨 Store 通信
6.1 我遇到跨 Store 问题
txt
我:用户退出登录时,除了清除用户信息,还要清除聊天记录。
聊天记录在另一个 chatSessionStore 里,怎么在 userStore 里调用它?
6.2 AI 解释跨 Store 通信
txt
AI:Pinia 支持在一个 Store 里调用另一个 Store。
⚠️ 注意事项:
1. 必须在 action(方法)内部调用,不能在 Store 定义的顶层调用
2. 这是因为顶层调用时,Pinia 可能还没初始化完成
❌ 错误写法:
const useChatStore = useChatSessionStore() // 顶层调用,可能报错
✅ 正确写法:
const logout = () => {
const chatStore = useChatSessionStore() // 方法内部调用
chatStore.reset()
}
typescript
// src/store/user.ts(跨 Store 调用)
import { useChatSessionStore } from '@/store/chatSession';
export const useUserStore = defineStore(
'user',
() => {
// ...
/**
* 清除用户信息
*/
const clearUserInfo = () => {
userInfo.value = { ...initialUserInfo };
isLoggedIn.value = false;
// 清除本地存储
uni.removeStorageSync('userInfo');
uni.removeStorageSync('token');
uni.removeStorageSync('isLoggedIn');
// 【跨 Store】清除聊天记录
try {
const chatStore = useChatSessionStore();
chatStore.reset();
} catch (error) {
console.warn('清理聊天记录失败:', error);
}
};
// ...
},
{ persist: true },
);
txt
AI:现在退出登录时会自动清除聊天记录。
📌 跨 Store 通信场景:
1. 退出登录 → 清除所有业务数据
2. 登录成功 → 初始化其他 Store
3. VIP 状态变化 → 通知其他模块
如果跨 Store 调用很多,也可以考虑用事件总线解耦。
七、核心经验:状态管理的最佳实践
7.1 Store 设计原则
| 原则 | 说明 | 示例 |
|---|---|---|
| 单一职责 | 一个 Store 管理一个领域 | userStore、chatStore、uiStore |
| 状态只读 | 用 readonly 包装,防止外部直接修改 | readonly(userInfo) |
| 方法封装 | 修改状态统一通过方法,不要直接赋值 | setUserInfo() 而不是 userInfo.value = xxx |
| 计算属性 | 派生状态用 computed,避免重复计算 | genderDisplay、isVip |
7.2 持久化注意事项
typescript
// ✅ 推荐:持久化配置适配小程序
store.use(
createPersistedState({
storage: {
getItem: uni.getStorageSync,
setItem: uni.setStorageSync,
},
}),
)
// ⚠️ 注意:Token 单独存储
uni.setStorageSync('token', token) // 不放 Store
// ⚠️ 注意:敏感信息不要持久化
{
persist: {
paths: ['userInfo', 'isLoggedIn'], // 明确指定
},
}
7.3 跨 Store 通信规则
typescript
// ❌ 错误:在 Store 顶层调用其他 Store
const chatStore = useChatSessionStore(); // 可能报错
// ✅ 正确:在方法内部调用
const logout = () => {
const chatStore = useChatSessionStore();
chatStore.reset();
};
7.4 初始化时机
typescript
// App.vue - 应用启动时初始化
onLaunch(() => {
const userStore = useUserStore();
userStore.initializeUserState();
});
八、总结:对话中学会状态管理
8.1 迭代过程回顾
| 阶段 | 需求 | 成果 |
|---|---|---|
| 基础搭建 | 创建 Store | 状态定义、基础方法 |
| 数据持久化 | 刷新保持登录 | pinia-plugin-persistedstate 配置 |
| 登录流程 | 微信登录 + 初始化 | wechatLogin、initializeUserState |
| 体验优化 | 派生状态 + 统一更新 | genderDisplay、updateUserInfo |
| 架构完善 | 跨 Store 通信 | clearUserInfo 中调用 chatStore |
8.2 关键收获
- 不要一次想清楚所有细节,先搭基础框架,遇到问题再补充
- 让 AI 解释原理,比如"为什么 Token 不放 Store",理解后才能举一反三
- 注意平台差异,小程序没有 localStorage,需要用 uni.getStorageSync
- 状态管理不只是存数据,计算属性、方法封装、跨 Store 通信都是关键
8.3 下一篇预告
《【AI 编程实战】第 6 篇:告别复制粘贴 - 设计一个优雅的 HTTP 模块》
下一篇继续对话式协作,教你:
- 如何设计 HTTP 请求/响应拦截器
- Token 自动携带和刷新
- 统一错误处理和 Loading 管理
状态管理的核心不是"用什么库",而是如何组织数据和逻辑。 通过和 AI 对话,你可以快速理清思路,少走弯路。
这是《AI 编程实战:TRAE SOLO 全栈开发指南》专栏的第五篇文章
如果这篇文章对你有帮助,请点赞、收藏、转发!