6.1 状态管理的必要性
随着应用规模增大,以下场景会变得棘手:
- 用户登录状态和用户信息需要在多个页面访问
- 购物车数据需要在商品列表、购物车页、结算页三处同步
- 消息未读数需要在 tabBar 图标和消息列表页同步显示
- 主题设置(深色/浅色模式)需要全局生效
这些场景都需要全局共享状态,而不是在每个组件中维护独立的局部状态。
6.2 Pinia 在 uni-app 中的使用
uni-app(Vue 3 + Vite)原生支持 Pinia,使用方式与普通 Vue 3 项目完全一致。
安装与初始化
# 安装 Pinia
npm install pinia
# 如果需要持久化插件
npm install pinia-plugin-persistedstate
// main.ts
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App) // 注意:uni-app 用 createSSRApp
const pinia = createPinia()
app.use(pinia)
return { app, pinia }
}
createSSRApp vs createApp
uni-app 的入口使用 createSSRApp 而不是 createApp,这是为了支持 SSR 和小程序的组件初始化机制。即使你不需要 SSR,也必须使用这个函数。
定义用户 Store(Setup 写法)
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface UserInfo {
id: number
name: string
avatar: string
role: 'user' | 'admin'
}
export const useUserStore = defineStore('user', () => {
// state
const token = ref('')
const userInfo = ref<UserInfo | null>(null)
// getters
const isLoggedIn = computed(() => !!token.value)
const isAdmin = computed(() => userInfo.value?.role === 'admin')
// actions
async function login(credentials: { phone: string; code: string }) {
const res = await uni.request({
url: '/api/auth/login',
method: 'POST',
data: credentials
})
const data = res.data as { token: string; user: UserInfo }
token.value = data.token
userInfo.value = data.user
// 持久化存储
uni.setStorageSync('token', data.token)
uni.setStorageSync('userInfo', JSON.stringify(data.user))
}
function logout() {
token.value = ''
userInfo.value = null
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
uni.reLaunch({ url: '/pages/login/login' })
}
function initFromStorage() {
// 应用启动时从本地存储恢复状态
const savedToken = uni.getStorageSync('token')
const savedUser = uni.getStorageSync('userInfo')
if (savedToken) token.value = savedToken
if (savedUser) {
try { userInfo.value = JSON.parse(savedUser) } catch {}
}
}
return { token, userInfo, isLoggedIn, isAdmin, login, logout, initFromStorage }
})
购物车 Store(含持久化)
// stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface CartItem {
productId: number
name: string
price: number
quantity: number
selected: boolean
}
const CART_STORAGE_KEY = 'cart_items'
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>(loadFromStorage())
const totalCount = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
const selectedTotal = computed(() =>
items.value
.filter(i => i.selected)
.reduce((sum, i) => sum + i.price * i.quantity, 0)
)
function addItem(product: Omit<CartItem, 'quantity' | 'selected'>) {
const existing = items.value.find(i => i.productId === product.productId)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1, selected: true })
}
saveToStorage()
}
function saveToStorage() {
uni.setStorageSync(CART_STORAGE_KEY, JSON.stringify(items.value))
}
function loadFromStorage(): CartItem[] {
try {
const data = uni.getStorageSync(CART_STORAGE_KEY)
return data ? JSON.parse(data) : []
} catch { return [] }
}
return { items, totalCount, selectedTotal, addItem }
})
在页面中使用 Store
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const cartStore = useCartStore()
// storeToRefs 解构后保持响应性
const { userInfo, isLoggedIn } = storeToRefs(userStore)
const { totalCount } = storeToRefs(cartStore)
</script>
<template>
<view>
<text>欢迎,{{ userInfo?.name }}</text>
<text>购物车共 {{ totalCount }} 件</text>
</view>
</template>
6.3 uni.$emit 与 uni.$on — 事件总线
uni-app 内置了一个简单的全局事件总线,适合跨页面、跨组件的轻量级通信,无需引入额外的库:
onLoad 或 onShow 中注册。onUnload 中调用,否则会造成内存泄漏(监听器在页面销毁后仍存在)。// 消息页面:发送新消息通知
function sendMessage(msg: string) {
uni.$emit('newMessage', { content: msg, time: new Date() })
}
// 首页:监听新消息更新未读数
import { onLoad, onUnload } from '@dcloudio/uni-app'
import { ref } from 'vue'
const unreadCount = ref(0)
function onNewMessage() {
unreadCount.value++
}
onLoad(() => {
uni.$on('newMessage', onNewMessage)
})
onUnload(() => {
// 关键:页面卸载时移除监听,防止内存泄漏
uni.$off('newMessage', onNewMessage)
})
事件总线的适用边界
uni.$emit / $on 适合临时的、轻量的跨页面通信。如果一个状态需要在 3 个以上的页面共享,或者数据结构较复杂,应该迁移到 Pinia 管理。过度依赖事件总线会让代码难以追踪和调试。
6.4 本地存储封装
uni-app 的 uni.setStorageSync 等 API 只支持存储字符串(非字符串会被序列化),封装一层通用工具可以减少重复的 JSON 处理:
// utils/storage.ts — 类型安全的本地存储工具
export const storage = {
set<T>(key: string, value: T): void {
try {
uni.setStorageSync(key, JSON.stringify(value))
} catch (e) {
console.error(`[Storage] set ${key} failed:`, e)
}
},
get<T>(key: string, defaultValue?: T): T | null {
try {
const raw = uni.getStorageSync(key)
return raw ? (JSON.parse(raw) as T) : (defaultValue ?? null)
} catch { return defaultValue ?? null }
},
remove(key: string): void {
uni.removeStorageSync(key)
},
clear(): void {
uni.clearStorageSync()
}
}
// 使用示例
storage.set('settings', { theme: 'dark', language: 'zh' })
const settings = storage.get<{ theme: string; language: string }>('settings')
6.5 全局主题状态
// stores/theme.ts
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
import { storage } from '@/utils/storage'
type Theme = 'light' | 'dark' | 'system'
export const useThemeStore = defineStore('theme', () => {
const theme = ref<Theme>(storage.get('theme') ?? 'light')
// 自动持久化
watch(theme, (val) => storage.set('theme', val))
function toggle() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
return { theme, toggle }
})
6.6 小结
- Pinia 是 uni-app(Vue 3)的首选状态管理库,使用 Setup Store 写法更简洁
storeToRefs解构 store 时保持响应性,直接解构会失去响应性- 持久化存储使用
uni.setStorageSync,注意 JSON 序列化处理 - uni.$emit / $on 适合轻量跨组件通信,但必须在 onUnload 中 $off 防止内存泄漏
- 封装通用 storage 工具函数,统一处理 JSON 序列化和错误捕获
- App.vue 的 onLaunch 中调用
initFromStorage,恢复登录态等持久化状态