页面通信篇里简单提过 Pinia,但真实项目的状态管理远不止"装个库"。多端 storage 差异、持久化插件适配、登录态设计、模块划分,每一步都有坑。本文把 uni-app 状态管理的完整方案一次性讲透。
一、为什么 uni-app 项目需要状态管理
先看反例——不用状态管理时的典型代码:
// 每个页面都要重复拉用户信息
onShow() {
const userId = uni.getStorageSync('user_id')
const res = await request.get(`/users/${userId}`)
this.user = res
}问题:重复请求、数据不同步(A 页改了昵称 B 页还是旧的、storage 读写散落各处。状态管理的本质是把跨页面共享的响应式数据收口到一个地方。
哪些数据该进全局 store:
| 数据类型 | 示例 | 方案 |
|---|---|---|
| 登录态 | token、用户信息 | store + 持久化 |
| 业务共享状态 | 购物车、选中的收货地址 | store |
| 页面间临时传值 | 列表页→详情页参数 | URL / EventChannel |
| 单页面私有状态 | 表单数据 | data / ref |
判断标准:两个以上无父子关系的页面需要读同一份数据 → 上 store;只在跳转链上传递 → 用页面通信方案。不要为了"显得规范"把所有东西塞进 store。
二、Pinia 集成
// main.js
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
app.use(createPinia())
return { app }
}// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: null
}),
getters: {
isLoggedIn: (state) => !!state.token,
displayName: (state) => state.userInfo?.nickname || '未登录'
},
actions: {
async login(code) {
const res = await request.post('/auth/wx-login', { code })
this.token = res.accessToken
this.userInfo = res.user
},
logout() {
this.token = ''
this.userInfo = null
uni.reLaunch({ url: '/pages/login/login' })
}
}
})// 任意页面使用
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
userStore.isLoggedIn // getter,响应式
await userStore.login(code) // action三、持久化:storage 适配是关键
直接持久化的问题
小程序端没有 localStorage,Pinia 官方的 pinia-plugin-persistedstate 默认走 localStorage,在小程序端直接报错。两个解决路径:
方案一:手写持久化(推荐,无依赖)
// utils/persist.js
import { watch, toRaw } from 'vue'
/**
* 将 store state 持久化到 uni storage
* @param {Store} store pinia store 实例
* @param {string} key 存储键名
*/
export function persistStore(store, key) {
// 启动时恢复
const saved = uni.getStorageSync(key)
if (saved) {
store.$patch(JSON.parse(saved))
}
// 变更时保存(deep watch)
store.$subscribe((mutation, state) => {
try {
uni.setStorageSync(key, JSON.stringify(toRaw(state)))
} catch (e) {
console.warn('持久化失败', e)
}
})
}// stores/index.js —— 统一初始化
import { createPinia } from 'pinia'
import { persistStore } from '@/utils/persist'
import { useUserStore } from './user'
import { useCartStore } from './cart'
const pinia = createPinia()
// 在 App.vue onLaunch 里调用,确保 uni storage 可用
export function initStores() {
persistStore(useUserStore(), 'app:user')
persistStore(useCartStore(), 'app:cart')
}
export default pinia// App.vue
export default {
onLaunch() {
initStores()
}
}$subscribe 是 Pinia 内置的订阅机制,任何 mutation 都会触发,比手动 watch 更可靠。
方案二:persist 插件 + 自定义 storage
如果坚持用 pinia-plugin-persistedstate,给它传入适配 uni storage 的 driver:
// main.js
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(createPersistedState({
storage: {
getItem: (key) => uni.getStorageSync(key) || null,
setItem: (key, value) => uni.setStorageSync(key, value),
// uni storage 没有.removeItem 语义?有:uni.removeStorageSync
}
}))
export function createApp() {
const app = createSSRApp(App)
app.use(pinia)
return { app }
}持久化的粒度控制
不是所有 state 都值得持久化。用户偏好(主题、字体大小)全量持久化;购物车持久化但要注意登录后与服务器合并;临时 UI 状态(弹窗开关)绝不持久化。用插件的话按 store 配置 paths 字段挑选。
四、登录态设计:完整方案
登录态是小程序里最重要的全局状态,完整链路:静默登录 → 检查有效期 → 请求拦截 → 失效处理。
// stores/user.js 完整版
import { defineStore } from 'pinia'
import request from '@/utils/request'
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
refreshToken: '',
userInfo: null,
wxSessionKey: ''
}),
getters: {
isLoggedIn: (state) => !!state.token
},
actions: {
/**
* 静默登录:App 启动即调用,用户无感知
* 小程序 wx.login 换 code → 后端换 openid → 绑定/注册用户 → 发 token
*/
async silentLogin() {
if (this.token) return
try {
const [err, res] = await uni.login({ provider: 'weixin' })
if (err) return
const data = await request.post('/auth/silent-login', { code: res.code })
this.token = data.accessToken
this.refreshToken = data.refreshToken
} catch (e) {
// 静默失败不弹窗,需要用户信息时再引导
console.warn('静默登录失败', e)
}
},
/** 强制登录:访问需要身份的页面时调用 */
async ensureLogin() {
if (this.isLoggedIn) return true
await this.silentLogin()
if (this.isLoggedIn) return true
// 静默失败(未注册),跳登录页
const pages = getCurrentPages()
const current = pages[pages.length - 1]
uni.navigateTo({
url: `/pages/login/login?redirect=/${current.route}`
})
return false
},
async fetchProfile() {
this.userInfo = await request.get('/me')
},
logout() {
this.$reset() // 重置到初始 state
uni.reLaunch({ url: '/pages/login/login' })
}
}
})// App.vue
export default {
onLaunch() {
initStores()
useUserStore().silentLogin()
}
}五、store 模块划分
中大型项目的目录建议:
stores/
├── index.js # pinia 实例 + initStores
├── user.js # 登录态、用户信息
├── cart.js # 购物车
├── app.js # 全局配置:主题、系统信息、定位城市
└── address.js # 收货地址列表划分原则:
- 按业务域拆,不按页面拆(订单相关的状态跟订单 store,即使被 5 个页面用)
- store 之间可以互相引用:
useCartStore内部调用useUserStore检查登录态,Pinia 支持这种组合 - action 里做业务编排:加入购物车 = 检查登录 → 调接口 → 更新 state → toast 提示,这一串逻辑写进 action 而不是页面里,页面只管调用
// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import request from '@/utils/request'
export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
actions: {
async add(goods, count = 1) {
const userStore = useUserStore() // store 组合
const ok = await userStore.ensureLogin()
if (!ok) return
// 本地乐观更新 + 服务端同步
const local = this.items.find(i => i.goodsId === goods.id)
if (local) {
local.count += count
} else {
this.items.push({ ...goods, goodsId: goods.id, count })
}
try {
await request.post('/cart/add', { goodsId: goods.id, count })
} catch (e) {
uni.showToast({ title: '同步失败', icon: 'none' })
}
}
}
})六、多端差异注意点
- storage 大小限制:单 key 上限 1MB,总上限 10MB(微信)。购物车这类大对象要控制规模,必要时只存 ID 列表,详情进 store 后从接口拉
- App 端可以直用 localStorage:但为了代码统一,全端都用 uni.setStorageSync
- storage 是同步 API:批量写入会阻塞逻辑层,高频写入(如实时输入内容)做节流
// 写入节流
let saveTimer = null
store.$subscribe(() => {
clearTimeout(saveTimer)
saveTimer = setTimeout(() => {
uni.setStorageSync(key, JSON.stringify(toRaw(store.$state)))
}, 300)
})总结
- 跨页面共享的响应式数据进 store,跳转传参用页面通信,不要过度集中
- 小程序端持久化必须适配 uni storage:手写
$subscribe方案零依赖最稳 - 登录态标准链路:onLaunch 静默登录 → ensureLogin 强制拦截 → action 内编排业务
- store 按业务域划分,action 做业务编排,store 之间可组合
- storage 有 1MB 单 key 限制,大对象持久化要做节流和规模控制
下一篇讲登录授权的"最后一公里"——手机号验证码、头像昵称填写这些微信持续收紧的接口怎么优雅应对。
评论 (0)