在开发 uni-app 跨端应用时,随着页面数量和业务复杂度的增长,组件之间的数据共享和状态同步会成为棘手的问题。传统的 props 透传和事件总线在大型项目中显得力不从心。Vue 3 官方推荐的状态管理库 Pinia 凭借其简洁的 API、完整的 TypeScript 支持以及与组合式 API 的无缝结合,已成为 uni-app 项目的首选方案。本文将带你从零开始,一步步掌握在 uni-app 中集成 Pinia、设计模块化 Store、实现持久化存储以及处理登录令牌等实际业务场景。
一、为什么选择 Pinia 而不是 Vuex
Pinia 是 Vuex 的精神继承者,但它解决了许多 Vuex 长期存在的痛点:
- 更简洁的 API:不再需要 mutations,直接通过 actions 修改状态,代码更直观。
- 完整的 TypeScript 类型推导:无需复杂的类型包装即可获得智能提示。
- 模块化天然支持:每个 Store 文件就是一个独立模块,无需嵌套在 modules 对象中。
- 与组合式 API 完美契合:Store 的定义方式与组合式函数非常相似,学习成本极低。
- 体积更小且性能更好:打包后仅约 2KB,且避免了 Vuex 的响应式本质开销。
在 uni-app 中,无论是开发 iOS、Android 还是各类小程序,Pinia 都能提供一致且高效的状态管理体验。
二、快速起步:在 uni-app 项目中集成 Pinia
如果你使用 HBuilder X 创建的 uni-app 项目(Vue 3 版本),默认已经内置了对 Pinia 的支持依赖。若是通过命令行创建的项目,需要手动安装:
npm install pinia
安装完成后,在项目的入口文件 main.js 中进行注册:
// main.js
import { createSSRApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
export function createApp() {
const app = createSSRApp(App);
const pinia = createPinia();
app.use(pinia);
return {
app,
pinia
};
}
注意 uni-app 的特殊之处:其入口文件导出一个 createApp 函数,因此我们需要在这里创建 Pinia 实例并通过 app.use() 安装。之后,你就可以在任何页面或组件中使用 useStore() 风格的函数来访问状态了。
三、定义你的第一个 Store:用户状态模块
在项目根目录创建 stores 文件夹,并在其中新建 user.js 文件。我们将使用组合式 API 风格来定义 Store:
// stores/user.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
// state —— 使用 ref 定义响应式数据
const token = ref('');
const userInfo = ref(null);
const isLogin = computed(() => !!token.value);
// getters —— 使用 computed 定义派生状态
const userName = computed(() => {
return userInfo.value?.nickname || '未登录';
});
// actions —— 使用普通函数定义业务逻辑
function login(loginData) {
return new Promise((resolve, reject) => {
// 模拟登录请求
uni.request({
url: '/api/login',
method: 'POST',
data: loginData,
success: (res) => {
if (res.data.code === 200) {
token.value = res.data.data.token;
userInfo.value = res.data.data.user;
resolve(res.data);
} else {
reject(res.data);
}
},
fail: reject
});
});
}
function logout() {
token.value = '';
userInfo.value = null;
uni.clearStorageSync(); // 清除本地缓存
uni.reLaunch({ url: '/pages/login/login' });
}
return { token, userInfo, isLogin, userName, login, logout };
});
在页面中使用它:
<template>
<view class="page">
<view v-if="userStore.isLogin">
<text>欢迎回来,{{ userStore.userName }}</text>
<button @click="userStore.logout()">退出登录</button>
</view>
<view v-else>
<button @click="handleLogin">去登录</button>
</view>
</view>
</template>
<script setup>
import { useUserStore } from '@/stores/user.js';
const userStore = useUserStore();
const handleLogin = () => {
uni.navigateTo({ url: '/pages/login/login' });
};
</script>
你会发现 Pinia 的使用方式与组合式 API 非常相似,状态可以直接在模板中解构使用,但直接解构会丢失响应性。如果确实需要解构,必须使用 storeToRefs():
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const { token, isLogin, userName } = storeToRefs(userStore);
// actions 可以直接解构,因为它们不是响应式数据
const { login, logout } = userStore;
四、模块化设计:购物车 Store 完整实现
真实项目通常需要多个 Store 模块。下面我们创建一个购物车 Store,并实现添加商品、修改数量、删除商品以及计算总价等功能。
// stores/cart.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCartStore = defineStore('cart', () => {
const items = ref([]); // 商品列表,每项包含 { id, name, price, count, image }
// 计算总价
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => sum + item.price * item.count, 0);
});
// 计算总数量
const totalCount = computed(() => {
return items.value.reduce((sum, item) => sum + item.count, 0);
});
// 添加商品到购物车
function addItem(product) {
const existing = items.value.find(item => item.id === product.id);
if (existing) {
existing.count++;
} else {
items.value.push({
id: product.id,
name: product.name,
price: product.price,
image: product.image || '',
count: 1
});
}
uni.showToast({ title: '已加入购物车', icon: 'success' });
}
// 更新商品数量
function updateCount(productId, count) {
const item = items.value.find(item => item.id === productId);
if (item) {
item.count = Math.max(1, count);
}
}
// 删除商品
function removeItem(productId) {
const index = items.value.findIndex(item => item.id === productId);
if (index > -1) {
items.value.splice(index, 1);
}
}
// 清空购物车
function clearCart() {
items.value = [];
}
return { items, totalPrice, totalCount, addItem, updateCount, removeItem, clearCart };
});
在商品详情页中调用 addItem 方法:
<template>
<view>
<view class="product-name">{{ product.name }}</view>
<view class="product-price">¥{{ product.price }}</view>
<button @click="cartStore.addItem(product)">加入购物车</button>
</view>
</template>
<script setup>
import { reactive } from 'vue';
import { useCartStore } from '@/stores/cart.js';
const cartStore = useCartStore();
const product = reactive({
id: 101,
name: '优质大米',
price: 49.9,
image: '/static/rice.jpg'
});
</script>
购物车页面则可以直接绑定 items 列表和总价,当状态变化时界面会自动更新,无需手动触发任何事件。
五、状态持久化:让数据在应用重启后依然存在
默认情况下,Pinia 的状态存储在内存中,应用关闭后就会丢失。对于用户令牌、购物车内容这类重要数据,我们需要将其持久化到本地存储中。推荐使用 pinia-plugin-persistedstate 插件,它支持 uni-app 的存储 API,并能自动同步。
首先安装插件:
npm install pinia-plugin-persistedstate
然后在 main.js 中启用:
// main.js
import { createSSRApp } from 'vue';
import { createPinia } from 'pinia';
import { createPersistedState } from 'pinia-plugin-persistedstate';
import App from './App.vue';
export function createApp() {
const app = createSSRApp(App);
const pinia = createPinia();
// 配置持久化插件,使用 uni-app 的存储 API
const piniaPersistedState = createPersistedState({
storage: {
getItem: (key) => uni.getStorageSync(key),
setItem: (key, value) => uni.setStorageSync(key, value),
removeItem: (key) => uni.removeStorageSync(key)
}
});
pinia.use(piniaPersistedState);
app.use(pinia);
return { app, pinia };
}
接下来,在需要持久化的 Store 中添加 persist 配置。修改 user.js:
// stores/user.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
// ... 状态和逻辑同上 ...
return { token, userInfo, isLogin, userName, login, logout };
}, {
persist: {
key: 'user-store', // 存储键名
storage: uni.getStorageSync, // 可省略,已在全局配置
paths: ['token', 'userInfo'] // 仅持久化指定字段
}
});
同样,修改购物车 Store,持久化 items 数组:
// stores/cart.js
export const useCartStore = defineStore('cart', () => {
// ... 定义逻辑 ...
return { items, totalPrice, totalCount, addItem, updateCount, removeItem, clearCart };
}, {
persist: {
key: 'cart-store',
paths: ['items']
}
});
现在,即使用户关闭小程序或应用,下次打开时购物车内容和登录状态依然会被保留。注意,pinia-plugin-persistedstate 在初始化时会用存储中的数据覆盖 state,如果你的 actions 中有依赖初始值的逻辑,建议在 onMounted 中处理。
六、进阶实战:封装带 Token 的请求拦截器
在开发中,我们经常需要在发送请求时自动携带用户令牌,并在令牌过期时统一处理退出登录。利用 Pinia 的 Store,我们可以轻松实现这一点。
首先创建一个通用的请求模块 utils/request.js:
// utils/request.js
import { useUserStore } from '@/stores/user.js';
const BASE_URL = 'https://api.example.com';
export function request(options) {
return new Promise((resolve, reject) => {
const userStore = useUserStore();
const header = {
'Content-Type': 'application/json',
...options.header
};
// 如果有 token 则自动添加 Authorization 头
if (userStore.token) {
header['Authorization'] = `Bearer ${userStore.token}`;
}
uni.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
header,
success: (res) => {
if (res.data.code === 401) {
// 令牌失效,清除登录状态
userStore.logout();
uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' });
reject(new Error('未授权'));
} else {
resolve(res.data);
}
},
fail: reject
});
});
}
现在,在任何需要调用接口的地方直接使用 request 函数,无须手动传递 token:
import { request } from '@/utils/request.js';
async function fetchOrderList() {
try {
const data = await request({
url: '/orders',
method: 'GET'
});
console.log('订单列表:', data);
} catch (error) {
console.error('请求失败:', error);
}
}
这种模式下,token 完全由 Pinia 管理并被请求拦截器自动读取,避免了在每个页面单独传递 token 的繁琐操作。
七、跨页面状态同步与组件通信
Pinia 的 Store 是全局单例的,因此在任何页面或组件中调用相同的 useXxxStore() 都会获得同一个实例。这意味着你可以直接在 A 页面修改状态,B 页面会立即响应。例如,用户在个人中心修改了昵称,首页的头像区域会自动更新。
无需事件总线、也无需 Vuex 的 mapState 辅助函数。组合式 API 风格的调用让跨页面通信变得极其自然:
<!-- 页面 A: 修改昵称 -->
<script setup>
import { useUserStore } from '@/stores/user.js';
const userStore = useUserStore();
const updateNickname = (newName) => {
userStore.userInfo.nickname = newName;
// 页面 B 中绑定的 userName 会立刻更新
};
</script>
在大型项目中,你还可以将一些全局 UI 状态(如加载指示器、网络状态、主题模式)放入专门的 app Store,使得整个应用的状态管理高度集中且易于调试。
八、注意事项与最佳实践
- 不要在组件外直接解构 state:如果在组件外直接解构
const { count } = store,会丢失响应性。始终使用storeToRefs()或在模板中通过store.xxx访问。 - 控制 Store 的粒度:不要将所有状态塞进一个巨型 Store 中。按业务领域拆分为 user、cart、order、settings 等模块,每个模块只管理相关的状态和逻辑。
- 谨慎使用持久化:并非所有状态都需要持久化。过度持久化会增加存储 I/O 开销,并可能在版本升级时造成数据结构不兼容。只持久化关键的、需要跨会话保留的数据。
- actions 中处理副作用:所有涉及异步请求、本地存储操作或业务流转的逻辑都应放在 actions 中,而不是在组件的 setup 函数中直接操作 state。
- 利用 Pinia Devtools 调试:在开发期间,可以安装 Vue Devtools(支持 Pinia 面板),实时查看所有 Store 的状态、时间旅行和 action 调用记录,极大提升调试效率。
九、总结
Pinia 为 uni-app 跨端应用带来了真正现代化的状态管理体验。通过组合式 API 风格的 Store 定义、简洁的模块化设计以及开箱即用的持久化支持,开发者得以将更多精力聚焦在业务逻辑实现上,而非繁琐的状态同步代码。本文从基础配置到购物车、登录令牌等实际案例,已经覆盖了日常开发中的绝大多数场景。
如果你正在启动一个新的 uni-app 项目,或考虑对现有项目进行技术升级,强烈推荐将状态管理迁移到 Pinia。它带来的不仅是代码量的减少,更是整体架构清晰度和可维护性的质变。

