uni-app Pinia 状态管理完全实战:从安装配置到复杂跨端应用架构

2026-05-30 0 710

在开发 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。它带来的不仅是代码量的减少,更是整体架构清晰度和可维护性的质变。

uni-app Pinia 状态管理完全实战:从安装配置到复杂跨端应用架构
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uni-app Pinia 状态管理完全实战:从安装配置到复杂跨端应用架构 https://www.taomawang.com/web/uniapp/2048.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务