一、引言:为什么选择Pinia而不是Vuex
在Uniapp开发中,随着业务复杂度上升,跨组件和跨页面的数据共享成为核心挑战。虽然Vuex曾是官方推荐的状态管理库,但Pinia凭借其简洁的API、完整的TypeScript支持以及模块化设计,已经成为Vue生态的新标准。Pinia去除了mutations,直接通过actions修改状态,配合Uniapp的多端特性,能够极大提升开发效率和代码可维护性。本文将带你从零搭建一套基于Pinia的模块化状态管理体系,覆盖用户认证、购物车和全局配置三个典型模块,并展示数据持久化与跨页面响应式通信的完整实现。
二、项目初始化与Pinia安装
在已有的Uniapp项目(基于Vue3)中安装Pinia。通过HBuilderX的终端或命令行执行:
npm install pinia
或者使用yarn:
yarn add pinia
安装完成后,在项目根目录的main.js中注册Pinia:
// main.js
import App from './App'
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
return { app, pinia }
}
注意:Uniapp使用createSSRApp以支持服务端渲染环境,Pinia的注册方式与标准Vue3一致。至此,Pinia已经可以在所有页面和组件中使用了。
三、模块化Store设计:拆分用户、购物车与全局配置
合理的模块划分是状态管理的关键。我们创建三个独立的Store模块,每个模块专注一个业务领域。在项目根目录下创建store文件夹,内含各模块文件。
3.1 用户认证模块 user.js
// store/user.js
import { defineStore } from 'pinia'
import { loginApi, getUserInfoApi } from '@/api/user.js'
export const useUserStore = defineStore('user', {
state: () => ({
token: uni.getStorageSync('user_token') || '',
userInfo: null,
isLogin: false
}),
getters: {
userId: (state) => state.userInfo?.id,
nickName: (state) => state.userInfo?.nickname || '未登录'
},
actions: {
async login(username, password) {
try {
const res = await loginApi({ username, password })
this.token = res.token
this.isLogin = true
uni.setStorageSync('user_token', res.token)
await this.fetchUserInfo()
return true
} catch (e) {
console.error('登录失败', e)
return false
}
},
async fetchUserInfo() {
if (!this.token) return
try {
const res = await getUserInfoApi()
this.userInfo = res.data
this.isLogin = true
} catch (e) {
this.logout()
}
},
logout() {
this.token = ''
this.userInfo = null
this.isLogin = false
uni.removeStorageSync('user_token')
uni.reLaunch({ url: '/pages/login/login' })
},
// 初始化时检查本地token并获取用户信息
async initAuth() {
if (this.token) {
await this.fetchUserInfo()
}
}
}
})
3.2 购物车模块 cart.js
// store/cart.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: JSON.parse(uni.getStorageSync('cart_items') || '[]')
}),
getters: {
totalCount: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
totalPrice: (state) => state.items.reduce((sum, item) => sum + item.price * item.quantity, 0).toFixed(2),
checkedItems: (state) => state.items.filter(item => item.checked)
},
actions: {
addItem(product, quantity = 1) {
const exist = this.items.find(item => item.id === product.id)
if (exist) {
exist.quantity += quantity
} else {
this.items.push({ ...product, quantity, checked: true })
}
this.saveToStorage()
},
removeItem(productId) {
this.items = this.items.filter(item => item.id !== productId)
this.saveToStorage()
},
updateQuantity(productId, quantity) {
const item = this.items.find(item => item.id === productId)
if (item) {
item.quantity = quantity
this.saveToStorage()
}
},
toggleCheck(productId) {
const item = this.items.find(item => item.id === productId)
if (item) {
item.checked = !item.checked
this.saveToStorage()
}
},
clearCart() {
this.items = []
uni.removeStorageSync('cart_items')
},
saveToStorage() {
uni.setStorageSync('cart_items', JSON.stringify(this.items))
}
}
})
3.3 全局配置模块 app.js
// store/app.js
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => ({
theme: uni.getStorageSync('app_theme') || 'light',
language: uni.getStorageSync('app_language') || 'zh-CN',
systemInfo: null
}),
getters: {
isDark: (state) => state.theme === 'dark'
},
actions: {
setTheme(theme) {
this.theme = theme
uni.setStorageSync('app_theme', theme)
// 可在此处调用全局主题切换逻辑
},
setLanguage(lang) {
this.language = lang
uni.setStorageSync('app_language', lang)
// 可选:刷新当前页面以应用新语言
},
fetchSystemInfo() {
this.systemInfo = uni.getSystemInfoSync()
}
}
})
上述模块各自独立,通过defineStore的第一个参数作为唯一标识。Getter用于派生状态,Actions处理异步请求或复杂逻辑。购物车模块直接通过uni.setStorageSync实现了简单的数据持久化,保证App重启后数据不丢失。
四、在页面和组件中使用Store
Pinia在Uniapp中的使用与Vue3组件完全一致,通过解构storeToRefs可以保持响应性。以下展示登录页面和购物车列表页面的实践。
4.1 登录页面调用用户Store
<template>
<view>
<input v-model="username" placeholder="用户名" />
<input v-model="password" type="password" placeholder="密码" />
<button @click="handleLogin">登录</button>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { useUserStore } from '@/store/user.js'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { isLogin, nickName } = storeToRefs(userStore)
const username = ref('')
const password = ref('')
const handleLogin = async () => {
const success = await userStore.login(username.value, password.value)
if (success) {
uni.switchTab({ url: '/pages/index/index' })
} else {
uni.showToast({ title: '登录失败', icon: 'none' })
}
}
</script>
4.2 购物车页面动态显示
<template>
<view>
<view v-for="item in cartItems" :key="item.id">
<text>{{ item.name }} x {{ item.quantity }}</text>
<text>¥{{ item.price * item.quantity }}</text>
<button @click="removeItem(item.id)">删除</button>
</view>
<view>总计:¥{{ totalPrice }}({{ totalCount }}件)</view>
</view>
</template>
<script setup>
import { useCartStore } from '@/store/cart.js'
import { storeToRefs } from 'pinia'
const cartStore = useCartStore()
const { items: cartItems, totalCount, totalPrice } = storeToRefs(cartStore)
const removeItem = (id) => {
cartStore.removeItem(id)
}
</script>
通过storeToRefs解构的状态仍然是响应式的,而actions方法可以直接调用。跨页面通信变得异常简单:用户在商品详情页添加购物车后,购物车页面会立即更新,无需任何事件总线或手动同步。
五、实现跨页面实时通信与数据同步
在多页面场景下,例如“详情页”修改商品数量后返回“列表页”,或者“结算页”清空购物车后跳转“首页”,这些跨页面操作都通过Store自动同步。因为Pinia的State是全局单例,不同页面引用同一个Store实例,任何修改都会立即反映到所有使用该Store的组件和页面中。
若需要在页面间传递临时数据(如表单草稿),同样可以建立一个专门的临时Store,而无需通过URL参数传递大量数据。示例:
// store/temp.js
import { defineStore } from 'pinia'
export const useTempStore = defineStore('temp', {
state: () => ({
draftOrder: null
}),
actions: {
setDraft(order) {
this.draftOrder = order
},
clearDraft() {
this.draftOrder = null
}
}
})
在订单填写页设置草稿,在支付确认页读取,提交后清除。全程无需URL参数或本地存储,数据隔离且安全。
六、数据持久化方案与插件扩展
虽然我们在模块内部手动调用了uni.setStorageSync,但对于大型项目,这些重复代码会造成负担。可以编写一个自定义Pinia插件,自动将指定Store的状态同步到本地存储。
// plugins/pinia-persist.js
export function createPersistPlugin(options = {}) {
const { key = 'pinia_state', paths = [] } = options
return ({ store }) => {
// 从本地存储恢复状态
const savedState = uni.getStorageSync(key)
if (savedState) {
try {
const parsed = JSON.parse(savedState)
store.$patch(parsed)
} catch (e) {}
}
// 订阅状态变化并存储
store.$subscribe((mutation, state) => {
let dataToStore = state
if (paths.length > 0) {
dataToStore = {}
paths.forEach(path => {
dataToStore[path] = state[path]
})
}
uni.setStorageSync(key, JSON.stringify(dataToStore))
})
}
}
在创建Pinia实例时注册该插件:
const pinia = createPinia()
pinia.use(createPersistPlugin({ key: 'user_store', paths: ['token', 'userInfo'] }))
这样,指定的Store路径会自动持久化,避免了在每个Action中手动编写存储逻辑。当然,在Uniapp中需要注意各平台存储限制和同步策略。
七、注意事项与最佳实践
- 避免在Getter中执行副作用:Getter应是纯函数,只用于计算派生状态,不要在Getter里修改State或发起API请求。
- 大型列表性能优化:购物车等列表数据如果过多,应考虑分页或虚拟列表,Store本身无性能瓶颈,但页面渲染会受影响。
- 模块间通信:一个Store的Action中可以调用另一个Store的Action,通过引入对应的useStore即可,但应注意避免循环依赖。
- TypeScript支持:Pinia完美支持TypeScript,建议为每个State定义接口,提升代码健壮性和IDE提示。
- 与Uniapp生命周期配合:可在onLaunch或onShow中调用Store的初始化方法,如userStore.initAuth(),确保应用启动时恢复登录态。
- 清理敏感数据:用户退出登录时,应清理所有相关Store的状态,避免残留token被滥用。
八、完整示例:App启动时的初始化流程
在App.vue中,我们可以集中执行各模块的初始化动作:
<script setup>
import { onLaunch } from '@dcloudio/uni-app'
import { useUserStore } from '@/store/user.js'
import { useAppStore } from '@/store/app.js'
onLaunch(async () => {
const appStore = useAppStore()
appStore.fetchSystemInfo()
const userStore = useUserStore()
await userStore.initAuth()
// 根据登录状态决定跳转页面
if (!userStore.isLogin) {
uni.reLaunch({ url: '/pages/login/login' })
}
})
</script>
这样,用户每次打开应用都会自动读取本地Token并获取最新用户信息,实现无感登录。同时系统信息也被缓存到Store中,全局可访问。
九、总结
Pinia为Uniapp带来了极简且强大的状态管理体验。通过模块化拆分,我们可以将用户、购物车、配置等不同领域的状态隔离管理,同时利用Store的全局特性实现跨页面无缝通信。结合本地存储插件,数据持久化也变得自动化。本文从安装配置到模块设计,再到页面使用和高级插件,完整演示了一套可投入生产的Pinia实践方案。在后续项目中,你可以根据业务需求在此基础之上继续扩展,享受Pinia带来的高效开发体验。

