狀態管理是前端應用的"心臟",但很多人一提到 Pinia/Vuex 就頭疼:Store 該怎麼設計?持久化怎麼做?登入態怎麼維護?這篇文章以心動戀聊小程式為例,透過和 AI 的真實對話,展示如何從零搭建一個完整的使用者狀態管理系統。
系列專欄:【AI 編程實戰:TRAE SOLO 全棧開發指南】
本篇主題:Pinia 狀態管理 - 從混亂程式碼到優雅架構
實戰專案:心動戀聊 - AI 戀愛聊天助手
在沒有集中式狀態管理之前,我的程式碼是這樣的:
// 頁面 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')) {
// 已登入邏輯
}
問題顯而易見:
我需要一個集中式的使用者狀態管理:
但我對 Pinia 不熟悉,於是開始和 AI 對話。
我:專案需要使用者狀態管理,用 Pinia 實現。
要管理使用者資訊、登入狀態,支援登入、退出功能。
AI:好的,我來幫你搭建使用者狀態管理。先確認幾個問題:
🤔 問題:
1. 專案技術棧是什麼?(Vue3/UniApp/其他)
2. 使用者資訊需要哪些字段?
3. 登入方式有哪些?(帳號密碼/微信/手機號)
4. 需要持久化嗎?(刷新頁面後保持登入)
請告訴我這些資訊,我來設計 Store 結構。
我:UniApp + Vue3 專案,跑微信小程序。
使用者資訊:user_id、username、avatar、gender、phone_number、vip_status 這些。
登入方式:主要是微信登入,後面可能加手機號登入。
需要持久化,使用者登入後刷新不能退出。
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 風格吧,看起來更簡潔
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();
現在可以測試一下基礎功能。
測試後,我發現一個問題:
我:登入後刷新頁面,狀態丟失了,又變成未登入狀態。
你不是說要支援持久化嗎?
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'], // 只持久化這些
},
}
我:好了,刷新後狀態保持了。但我還有個問題:
token 現在是存在 Store 裡還是 Storage 裡?
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 單獨存
我:現在要加微信登入功能。
流程是:呼叫 uni.login 獲取 code → 發給後端換 token 和使用者資訊。
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);
}
};
我:還有個問題,應用啟動時怎麼恢復登入態?
比如使用者之前登入過,打開應用應該自動是登入狀態。
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>
這樣應用啟動時會自動恢復登入態。
我:使用者資訊裡 gender 是數字(1=男,2=女),
但頁面上要顯示"男生"、"女生",每次都要寫判斷很麻煩。
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>
📌 計算屬性的好處:
我:使用者在"我的"頁面可以修改性別、暱稱,
修改後要同步更新 Store 和伺服器,怎麼做比較好?
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',
});
我:使用者退出登入時,除了清除使用者資訊,還要清除聊天記錄。
聊天記錄在另一個 chatSessionStore 裡,怎麼在 userStore 裡呼叫它?
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 呼叫很多,也可以考慮用事件總線解耦。
| 原則 | 說明 | 示例 |
|---|---|---|
| 單一責任 | 一個 Store 管理一個領域 | userStore、chatStore、uiStore |
| 狀態只讀 | 用 readonly 包裝,防止外部直接修改 | readonly(userInfo) |
| 方法封裝 | 修改狀態統一通過方法,不要直接賦值 | setUserInfo() 而不是 userInfo.value = xxx |
| 計算屬性 | 派生狀態用 computed,避免重複計算 | genderDisplay、isVip |
// ✅ 推薦:持久化配置適配小程序
store.use(
createPersistedState({
storage: {
getItem: uni.getStorageSync,
setItem: uni.setStorageSync,
},
}),
);
// ⚠️ 注意:Token 單獨存儲
uni.setStorageSync('token', token); // 不放 Store
// ⚠️ 注意:敏感資訊不要持久化
{
persist: {
paths: ['userInfo', 'isLoggedIn'], // 明確指定
},
}
// ❌ 錯誤:在 Store 頂層呼叫其他 Store
const chatStore = useChatSessionStore(); // 可能報錯
// ✅ 正確:在方法內部呼叫
const logout = () => {
const chatStore = useChatSessionStore();
chatStore.reset();
};
// App.vue - 應用啟動時初始化
onLaunch(() => {
const userStore = useUserStore();
userStore.initializeUserState();
});
| 階段 | 需求 | 成果 |
|---|---|---|
| 基礎搭建 | 創建 Store | 狀態定義、基礎方法 |
| 數據持久化 | 刷新保持登入 | pinia-plugin-persistedstate 配置 |
| 登入流程 | 微信登入 + 初始化 | wechatLogin、initializeUserState |
| 體驗優化 | 派生狀態 + 統一更新 | genderDisplay、updateUserInfo |
| 架構完善 | 跨 Store 通信 | clearUserInfo 中呼叫 chatStore |
《【AI 編程實戰】第 6 篇:告別複製貼上 - 設計一個優雅的 HTTP 模組》
下一篇繼續對話式協作,教你:
狀態管理的核心不是"用什麼庫",而是如何組織數據和邏輯。透過和 AI 對話,你可以快速理清思路,少走彎路。
這是《AI 編程實戰:TRAE SOLO 全棧開發指南》專欄的第五篇文章
如果這篇文章對你有幫助,請點讚、收藏、轉發!