【AI 编程实战】第 5 篇:Pinia 状态管理 - 从混乱代码到优雅架构

状态管理是前端应用的"心脏",但很多人一提到 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,避免重复计算 genderDisplayisVip

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 关键收获

  1. 不要一次想清楚所有细节,先搭基础框架,遇到问题再补充
  2. 让 AI 解释原理,比如"为什么 Token 不放 Store",理解后才能举一反三
  3. 注意平台差异,小程序没有 localStorage,需要用 uni.getStorageSync
  4. 状态管理不只是存数据,计算属性、方法封装、跨 Store 通信都是关键

8.3 下一篇预告

《【AI 编程实战】第 6 篇:告别复制粘贴 - 设计一个优雅的 HTTP 模块》

下一篇继续对话式协作,教你:

  • 如何设计 HTTP 请求/响应拦截器
  • Token 自动携带和刷新
  • 统一错误处理和 Loading 管理

状态管理的核心不是"用什么库",而是如何组织数据和逻辑。 通过和 AI 对话,你可以快速理清思路,少走弯路。

这是《AI 编程实战:TRAE SOLO 全栈开发指南》专栏的第五篇文章

如果这篇文章对你有帮助,请点赞、收藏、转发!

相关推荐
青莲8432 小时前
Kotlin Flow 深度探索与实践指南——上部:基础与核心篇
android·前端
Bug生活20482 小时前
五年断更,AI助我半天复活小程序
前端·微信小程序·ai编程
狗头大军之江苏分军2 小时前
Node.js 性能优化实践,但老板只关心是否能跑
前端·后端
恋猫de小郭2 小时前
2025 年终醒悟,AI 让我误以为自己很强,未来程序员的转型之路
android·前端·flutter
用泥种荷花2 小时前
【前端学习AI】PromptTemplate的使用
前端
狗头大军之江苏分军2 小时前
Node.js 真香,但每次部署都想砸电脑
前端·javascript·后端
Shi_haoliu2 小时前
inno setup6.6.1实例,制作安装包,创建共享文件夹,写入注册表(提供给excel加载项,此文章解释iss文件)
前端·vue.js·windows·excel
MediaTea2 小时前
Python:实例 __dict__ 详解
java·linux·前端·数据库·python
狗头大军之江苏分军2 小时前
又是一个周末加班夜,前端的我只想哭…
前端