🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

【AI 編程實戰】第 5 篇:Pinia 狀態管理 - 從混亂程式碼到優雅架構

狀態管理是前端應用的"心臟",但很多人一提到 Pinia/Vuex 就頭疼:Store 該怎麼設計?持久化怎麼做?登入態怎麼維護?這篇文章以心動戀聊小程式為例,透過和 AI 的真實對話,展示如何從零搭建一個完整的使用者狀態管理系統。

系列專欄【AI 編程實戰:TRAE SOLO 全棧開發指南】

本篇主題:Pinia 狀態管理 - 從混亂程式碼到優雅架構

實戰專案:心動戀聊 - AI 戀愛聊天助手

一、開篇:狀態管理的痛點

1.1 沒有狀態管理時的混亂

在沒有集中式狀態管理之前,我的程式碼是這樣的:

// 頁面 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 我的第一句話

我:專案需要使用者狀態管理,用 Pinia 實現。
    要管理使用者資訊、登入狀態,支援登入、退出功能。

2.2 AI 先問問題

AI:好的,我來幫你搭建使用者狀態管理。先確認幾個問題:

🤔 問題:
1. 專案技術棧是什麼?(Vue3/UniApp/其他)
2. 使用者資訊需要哪些字段?
3. 登入方式有哪些?(帳號密碼/微信/手機號)
4. 需要持久化嗎?(刷新頁面後保持登入)

請告訴我這些資訊,我來設計 Store 結構。

2.3 我補充資訊

我:UniApp + Vue3 專案,跑微信小程序。
    使用者資訊:user_id、username、avatar、gender、phone_number、vip_status 這些。
    登入方式:主要是微信登入,後面可能加手機號登入。
    需要持久化,使用者登入後刷新不能退出。

2.4 AI 給出執行計畫

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,更靈活

你想用哪種風格?
我:用 Setup 風格吧,看起來更簡潔

2.5 AI 生成基礎程式碼

AI:好的,開始生成程式碼...

✅ 創建 src/store/index.ts
✅ 創建 src/store/user.ts
✅ 修改 main.ts

基礎結構已完成,這是核心程式碼:
// src/store/index.ts
import { createPinia } from 'pinia';

const store = createPinia();

export default store;
export * from './user';
// 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,
  };
});
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 我發現刷新後狀態丟失

測試後,我發現一個問題:

我:登入後刷新頁面,狀態丟失了,又變成未登入狀態。
    你不是說要支援持久化嗎?

3.2 AI 解釋並修復

AI:抱歉,我忘了加持久化配置。

🔍 原因:
Pinia 預設不持久化,狀態只存在記憶體中,刷新就沒了。

🔧 解決方案:
使用 pinia-plugin-persistedstate 插件,自動將狀態同步到 Storage。

⚠️ 小程序注意:
小程序沒有 localStorage,需要用 uni.getStorageSync/setStorageSync。

我來修改配置...
// 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';
// src/store/user.ts(添加持久化配置)
export const useUserStore = defineStore(
  'user',
  () => {
    // ... 原有程式碼不變
  },
  {
    persist: true, // 開啟持久化
  },
);
AI:
✅ 已安裝 pinia-plugin-persistedstate
✅ 已修改 src/store/index.ts
✅ 已修改 src/store/user.ts

現在刷新頁面,登入狀態會保持。

⚠️ 注意:
persist: true 會持久化整個 state。
如果某些狀態不想持久化,可以用 paths 指定:
{
  persist: {
    paths: ['userInfo', 'isLoggedIn'],  // 只持久化這些
  },
}

3.3 我驗證修復成功

我:好了,刷新後狀態保持了。但我還有個問題:
    token 現在是存在 Store 裡還是 Storage 裡?

3.4 AI 解釋 Token 存儲策略

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');

你想用哪種策略?
我:用策略二,Token 單獨存

四、完善登入流程

4.1 我要添加微信登入

我:現在要加微信登入功能。
    流程是:呼叫 uni.login 獲取 code → 發給後端換 token 和使用者資訊。

4.2 AI 設計登入方法

AI:明白,我來添加微信登入方法。

📋 執行計畫:
1. 添加 wechatLogin 方法到 userStore
2. 處理登入成功:保存 token + 設定使用者資訊
3. 處理登入失敗:提示錯誤

🤔 問題:
1. 登入 API 接口路徑是什麼?
2. 登入成功後要不要自動跳轉?
3. 需要 loading 和 toast 提示嗎?
我:API 是 /api/auth/wechat-login,參數是 { code }。
    不需要自動跳轉,由呼叫方決定。
    需要 toast 提示成功/失敗。
AI:好的,添加登入方法:
// 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 },
);
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 我追問:登入態初始化

我:還有個問題,應用啟動時怎麼恢復登入態?
    比如使用者之前登入過,打開應用應該自動是登入狀態。

4.4 AI 添加初始化邏輯

AI:需要添加一個初始化方法,在應用啟動時呼叫。

🔍 初始化邏輯:
1. 檢查本地是否有 token 和 userInfo
2. 如果有,恢復登入狀態
3. 異步請求最新使用者資訊(token 可能過期)
4. 如果 token 過期,清除登入態
// 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 },
);
AI:在 App.vue 的 onLaunch 中呼叫初始化:
// App.vue
<script setup lang="ts">
import { useUserStore } from '@/store';

const userStore = useUserStore();

onLaunch(() => {
  userStore.initializeUserState();
});
</script>

這樣應用啟動時會自動恢復登入態。

五、計算屬性與統一更新

5.1 我需要顯示性別文本

我:使用者資訊裡 gender 是數字(1=男,2=女),
    但頁面上要顯示"男生"、"女生",每次都要寫判斷很麻煩。

5.2 AI 添加計算屬性

AI:用計算屬性(computed)解決,在 Store 裡統一處理。

這樣所有頁面都可以直接用 userStore.genderDisplay,不用重複判斷。
// 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 },
);
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 我追問:更新使用者資訊

我:使用者在"我的"頁面可以修改性別、暱稱,
    修改後要同步更新 Store 和伺服器,怎麼做比較好?

5.4 AI 添加統一更新方法

AI:添加一個統一的更新方法,封裝 API 呼叫 + Store 更新 + 提示。

這樣所有修改使用者資訊的地方都呼叫同一個方法,保證一致性。
// 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('修改失敗,請稍後再試');
  }
};
AI:使用方式:
// 修改性別
await userStore.updateUserInfo({ gender: 1 });

// 修改暱稱,自訂提示
await userStore.updateUserInfo(
  { username: '新暱稱' },
  '暱稱修改成功'
);

// 同時修改多個
await userStore.updateUserInfo({
  username: '新暱稱',
  avatar: 'https://xxx.com/avatar.png',
});

六、跨 Store 通信

6.1 我遇到跨 Store 問題

我:使用者退出登入時,除了清除使用者資訊,還要清除聊天記錄。
    聊天記錄在另一個 chatSessionStore 裡,怎麼在 userStore 裡呼叫它?

6.2 AI 解釋跨 Store 通信

AI:Pinia 支援在一個 Store 裡呼叫另一個 Store。

⚠️ 注意事項:
1. 必須在 action(方法)內部呼叫,不能在 Store 定義的頂層呼叫
2. 這是因為頂層呼叫時,Pinia 可能還沒初始化完成

❌ 錯誤寫法:
const useChatStore = useChatSessionStore();  // 頂層呼叫,可能報錯

✅ 正確寫法:
const logout = () => {
  const chatStore = useChatSessionStore();  // 方法內部呼叫
  chatStore.reset();
};
// 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 },
);
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 持久化注意事項

// ✅ 推薦:持久化配置適配小程序
store.use(
  createPersistedState({
    storage: {
      getItem: uni.getStorageSync,
      setItem: uni.setStorageSync,
    },
  }),
);

// ⚠️ 注意:Token 單獨存儲
uni.setStorageSync('token', token); // 不放 Store

// ⚠️ 注意:敏感資訊不要持久化
{
  persist: {
    paths: ['userInfo', 'isLoggedIn'],  // 明確指定
  },
}

7.3 跨 Store 通信規則

// ❌ 錯誤:在 Store 頂層呼叫其他 Store
const chatStore = useChatSessionStore(); // 可能報錯

// ✅ 正確:在方法內部呼叫
const logout = () => {
  const chatStore = useChatSessionStore();
  chatStore.reset();
};

7.4 初始化時機

// 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 全棧開發指南》專欄的第五篇文章

如果這篇文章對你有幫助,請點讚、收藏、轉發!


原文出處:https://juejin.cn/post/7587738151658881024


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝8   💬7   ❤️5
247
🥈
我愛JS
📝1   💬8   ❤️2
63
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付