uniapp跨端状态管理实战——用Pinia拆解多端共享与差异化的困局

2026-07-25 0 106

状态管理这事儿,在纯web应用里已经被讨论得太多了,Vuex和Pinia怎么选、模块怎么拆、要不要用持久化插件,网上随便一搜都是一大把教程。可一旦把战场挪到uniapp里,情况就微妙起来了。你会发现同样一个用户登录状态,在H5上可能需要同步给所有打开的标签页,在小程序里却要老老实实走一遍本地缓存校验,到了App端又得考虑离线场景和原生插件的数据回传。刚开始我还硬着头皮想用一套代码全搞定,结果各种条件编译把自己绕晕了不说,测试的时候还频频翻车。

后来干脆静下心来,重新梳理了一下跨端状态管理真正的难点到底在哪里。结论是:共享的部分其实很薄,真正麻烦的是那些平台独有的行为。与其试图抹平差异,不如大大方方承认差异,然后用一套灵活的结构把它们收纳好。而Pinia的Setup语法配合uniapp的条件编译,恰好提供了一个挺舒服的切入点。

一、为什么不是Vuex,而是Pinia

这个问题其实老生常谈了。Vuex对Vue2的支持确实很完善,但在uniapp的Vue3版本里,Pinia的优势是压倒性的。首先是TypeScript支持,虽然uniapp项目里不一定都用TS,但类型推导带来的编辑器提示实在是香。更关键的一点是,Pinia可以写Setup风格的store,这让我们在组织跨端差异化逻辑时,能够直接用组合式API里那套响应式变量和函数,不用再去记mutation和action的分离。

举个例子,在Vuex里想实现一个根据平台不同返回不同数据的getter,写法大概是下面这样,嵌套在state、mutations、getters里,读起来有些绕:

// Vuex典型写法
const store = {
    state: { platform: '' },
    mutations: { SET_PLATFORM(state, val) { state.platform = val; } },
    getters: {
        welcomeText: (state) => {
            // #ifdef H5
            return '欢迎访问网页版';
            // #endif
            // #ifdef MP-WEIXIN
            return '欢迎使用小程序';
            // #endif
        }
    }
};

换成Pinia的Setup写法,感官上干净很多:

// Pinia Setup写法
export const useAppStore = defineStore('app', () => {
    const platform = ref('');
    const welcomeText = computed(() => {
        // #ifdef H5
        return '欢迎访问网页版';
        // #endif
        // #ifdef MP-WEIXIN
        return '欢迎使用小程序';
        // #endif
        return '欢迎使用应用';
    });
    return { platform, welcomeText };
});

别小看这点书写上的差异,在一个需要频繁按平台调整逻辑的项目里,把状态、计算值、操作方法都平铺在setup函数里,可读性提升不止一点半点。

二、用Pinia Setup模式搭建可扩展的Store

先从一个最常见的场景切入:用户登录状态管理。这个store需要在不同端做以下几件不同的事情:

  1. H5端:登录成功后需要把token存到localStorage,并且监听storage事件,当其他标签页修改了token时,及时同步当前页面的登录状态。
  2. 微信小程序端:token存在wx.setStorageSync里,没有跨标签页问题,但要处理wx.login和wx.getUserProfile(已废弃,但依然有旧项目在用)的逻辑。
  3. App端:token存在plus.storage里,并且要考虑离线时使用缓存的用户信息,同时配合原生的第三方登录SDK。

面对这么一摊子需求,如果没有一个清晰的store框架,很容易在页面里散落无数条件判断。我的做法是定义一个统一的useAuthStore,内部利用条件编译分拆平台实现,但对外暴露同样的方法和响应式属性。

第一步:定义平台无关的Store接口

无论哪个端,外部页面都应该用同样的方式调用store。我们约定好store暴露以下内容:

  • token:当前登录凭证,响应式字符串。
  • userInfo:用户信息对象。
  • isLoggedIn:计算属性,根据token是否有效判断。
  • login(credentials):登录方法,各端实现不同。
  • logout():退出登录。
  • checkSession():检查登录状态是否有效,App和小程序端需要这个。

第二步:实现H5端的Store逻辑

H5端的实现相对直接,利用localStorage和storage事件。Pinia的Setup函数里可以直接写事件监听。

// stores/auth.js (H5部分)
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useAuthStore = defineStore('auth', () => {
    const token = ref(localStorage.getItem('token') || '');
    const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || 'null'));

    const isLoggedIn = computed(() => !!token.value);

    // H5特有的:监听其他标签页的storage变化
    // #ifdef H5
    window.addEventListener('storage', (event) => {
        if (event.key === 'token') {
            token.value = event.newValue || '';
        }
        if (event.key === 'userInfo') {
            userInfo.value = event.newValue ? JSON.parse(event.newValue) : null;
        }
    });
    // #endif

    function login(credentials) {
        // 模拟登录请求
        return new Promise((resolve) => {
            setTimeout(() => {
                const fakeToken = 'token_' + Date.now();
                const fakeUser = { name: '用户', avatar: '' };
                token.value = fakeToken;
                userInfo.value = fakeUser;
                // H5持久化到localStorage
                // #ifdef H5
                localStorage.setItem('token', fakeToken);
                localStorage.setItem('userInfo', JSON.stringify(fakeUser));
                // #endif
                resolve({ token: fakeToken, user: fakeUser });
            }, 600);
        });
    }

    function logout() {
        token.value = '';
        userInfo.value = null;
        // #ifdef H5
        localStorage.removeItem('token');
        localStorage.removeItem('userInfo');
        // #endif
    }

    function checkSession() {
        // H5不需要主动检查,直接返回token是否存在
        return !!token.value;
    }

    return { token, userInfo, isLoggedIn, login, logout, checkSession };
});

上面这段代码,H5特有的逻辑被条件编译包裹,当编译为小程序或App时,这些代码会被自动剔除。接下来补齐小程序和App的逻辑,同样用条件编译区分。

第三步:分别实现小程序和App的差异部分

微信小程序没有localStorage,用的是wx.setStorageSync,而且登录流程往往需要和wx.login结合获取code再交换后端token。App端则可能用到plus.storage以及第三方SDK。这些差异可以全部塞进同一个store里,通过条件编译让它们共存。

// 继续在 useAuthStore 的 setup 函数中编写
function login(credentials) {
    // #ifdef MP-WEIXIN
    return new Promise((resolve, reject) => {
        wx.login({
            success: (res) => {
                // 用code去后端换取token(此处简化处理)
                const fakeToken = 'mp_token_' + res.code;
                token.value = fakeToken;
                userInfo.value = { name: '微信用户', avatar: '' };
                wx.setStorageSync('token', fakeToken);
                wx.setStorageSync('userInfo', JSON.stringify(userInfo.value));
                resolve({ token: fakeToken, user: userInfo.value });
            },
            fail: reject
        });
    });
    // #endif

    // #ifdef APP-PLUS
    return new Promise((resolve) => {
        // 示例:使用plus.storage
        const fakeToken = 'app_token_' + Date.now();
        token.value = fakeToken;
        userInfo.value = { name: 'App用户', avatar: '' };
        plus.storage.setItem('token', fakeToken);
        plus.storage.setItem('userInfo', JSON.stringify(userInfo.value));
        resolve({ token: fakeToken, user: userInfo.value });
    });
    // #endif
}

function checkSession() {
    // #ifdef MP-WEIXIN
    // 小程序检查本地存储的token是否存在
    const savedToken = wx.getStorageSync('token');
    if (savedToken) {
        token.value = savedToken;
        const savedUser = wx.getStorageSync('userInfo');
        if (savedUser) userInfo.value = JSON.parse(savedUser);
        return true;
    }
    return false;
    // #endif

    // #ifdef APP-PLUS
    const savedToken = plus.storage.getItem('token');
    if (savedToken) {
        token.value = savedToken;
        const savedUser = plus.storage.getItem('userInfo');
        if (savedUser) userInfo.value = JSON.parse(savedUser);
        return true;
    }
    return false;
    // #endif
}

注意这里并没有把所有端的方法全部写死在一个函数里,而是用条件编译让每个端的实现互不干扰。编译时只会保留当前平台的代码,其它部分就像注释一样不存在。这样最终打包出的各端代码体积最小,逻辑也最清晰。

三、页面中的使用与注意细节

在页面里使用这个store就非常统一了,不必再关心底层到底走了哪个分支。

<template>
    <view class="page">
        <template v-if="auth.isLoggedIn">
            <text>欢迎回来,{{ auth.userInfo.name }}</text>
            <button @tap="handleLogout">退出登录</button>
        </template>
        <template v-else>
            <button @tap="handleLogin">点击登录</button>
        </template>
    </view>
</template>

<script setup>
import { useAuthStore } from '@/stores/auth';
const auth = useAuthStore();

const handleLogin = async () => {
    try {
        await auth.login({ username: 'test', password: '123' });
    } catch (e) {
        console.error('登录失败', e);
    }
};

const handleLogout = () => {
    auth.logout();
};
</script>

这里有个容易被忽略的细节:在小程序端,checkSession方法需要在onLaunch或首页的onShow里主动调用一次,用来恢复本地存储的登录状态。H5端因为初始化时直接从localStorage读取了token,通常不需要额外调用。这个差异可以封装在store内部的一个初始化方法里,用条件编译决定是否执行。

// 在useAuthStore里增加一个init方法
function init() {
    // #ifdef MP-WEIXIN || APP-PLUS
    checkSession();
    // #endif
}

// 返回时增加init
return { token, userInfo, isLoggedIn, login, logout, checkSession, init };

然后在App.vueonLaunch里调用auth.init()就好了。H5端这个方法什么也不做,但接口统一。

四、跨Tab页同步状态——H5端的额外挑战

上文提到H5端用storage事件同步标签页,这个方案在绝大多数情况下工作良好,但有一个边界情况:如果用户在当前标签页登录,storage事件在自己这里不会触发,只有其它标签页会收到。这意味着同一个标签页内部的响应式同步靠的就是直接修改token.value,这没问题。但如果你在另一个标签页里退出登录,当前标签页的storage监听器才会把token清掉。

还有一点,storage事件只在其他标签页修改存储时触发,同源页面之间的消息广播也可以考虑用Broadcast Channel API,不过兼容性在小程序和App端就不用想了。H5里如果追求更实时的同步,可以在store里结合BroadcastChannel来补充。但我们这个场景,storage事件已经够用了,没必要过度设计。

// 可选:H5中同时使用BroadcastChannel增强同步
// #ifdef H5
if ('BroadcastChannel' in window) {
    const channel = new BroadcastChannel('auth-sync');
    channel.onmessage = (event) => {
        if (event.data.type === 'logout') {
            token.value = '';
            userInfo.value = null;
        }
    };
    // 在logout里发送消息
    const originalLogout = logout;
    logout = () => {
        originalLogout();
        channel.postMessage({ type: 'logout' });
    };
}
// #endif

这种增强是可选的,不实现也完全不影响使用。

五、总结:跨端状态管理的核心思路

走完这一整套流程,会发现跨端状态管理其实没有那么玄乎。关键在于两条原则:

  • 接口统一,实现隔离:对外部页面而言,store提供的方法和属性在各端完全相同。内部用条件编译把平台相关的实现藏起来,编译后的代码干净不臃肿。
  • 差异化逻辑尽早收敛:不要等到页面里用一堆if (platform === 'xxx')去判断,把所有差异都收口在store里,页面只做最简单的调用和展示。

Pinia的Setup语法让这个过程变得很顺手。每次需要针对某个平台写特殊逻辑时,直接用条件编译包裹几行代码就行,不用去改整体架构。这种灵活性,恰好踩中了跨端开发里最需要的那块地——既不让差异扩散到整个项目,也不因为过度抽象而增加理解成本。

下次再碰到那种“在这个端要这样,在那个端要那样”的需求,不妨先停下来想一下,这块逻辑能不能塞进store的条件编译块里。很多时候,把差异关进笼子,剩下的代码就自然清爽了。这套心法,比记住任何具体API都更有用。

uniapp跨端状态管理实战——用Pinia拆解多端共享与差异化的困局
收藏 (0) 打赏

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

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

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

淘吗网 uniapp uniapp跨端状态管理实战——用Pinia拆解多端共享与差异化的困局 https://www.taomawang.com/web/uniapp/2418.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

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

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