为什么要状态管理

多页面应用里,登录状态、用户信息、购物车等数据需要全局共享。uni-app 的 Vue3 版本推荐使用 Pinia(Vue 官方状态库),用法和 Web 端一致。

一、安装与初始化

1
npm install pinia

main.js 注册:

1
2
3
4
5
6
7
8
9
10
// 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 }
}

二、定义 user store:登录状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
state: () => ({
token: uni.getStorageSync('token') || '',
userInfo: null
}),

getters: {
isLoggedIn: (state) => !!state.token
},

actions: {
setToken(token) {
this.token = token
uni.setStorageSync('token', token)
},

async login(username, password) {
// 实际开发走封装好的 request
const res = await request({ url: '/api/login', method: 'POST', data: { username, password } })
this.setToken(res.token)
this.userInfo = res.user
return res
},

logout() {
this.token = ''
this.userInfo = null
uni.removeStorageSync('token')
uni.reLaunch({ url: '/pages/login/login' })
}
}
})

页面中使用:

1
2
3
4
5
6
<script setup>
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
console.log('是否登录:', userStore.isLoggedIn)
</script>

三、统一请求封装:拦截器 + token + 错误处理

一个带拦截器的 request,是跨端项目的标配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// utils/request.js
const BASE_URL = 'https://api.example.com'

export function request(options) {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token')

uni.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.header
},

success: (res) => {
// 业务码约定:code === 0 成功
if (res.statusCode === 200 && res.data.code === 0) {
resolve(res.data.data)
} else if (res.statusCode === 401) {
// token 失效:清登录态回登录页
uni.removeStorageSync('token')
uni.reLaunch({ url: '/pages/login/login' })
reject(new Error('登录已过期'))
} else {
uni.showToast({ title: res.data.message || '请求失败', icon: 'none' })
reject(new Error(res.data.message || '请求失败'))
}
},

fail: (err) => {
uni.showToast({ title: '网络异常,请检查网络', icon: 'none' })
reject(err)
}
})
})
}

为什么封装成 Promise:uni.request 是回调式 API,封装后页面里可以 await,配合 async/await 让代码和 Web 端一样清爽:

1
2
3
4
5
6
7
8
9
async function loadOrders() {
uni.showLoading({ title: '加载中' })
try {
const list = await request({ url: '/api/orders' })
orders.value = list
} finally {
uni.hideLoading()
}
}

四、组合使用:登录 → 存状态 → 跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script setup>
import { ref } from 'vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
const username = ref('')
const password = ref('')
const loading = ref(false)

async function handleLogin() {
if (!username.value || !password.value) {
uni.showToast({ title: '请输入账号密码', icon: 'none' })
return
}
loading.value = true
try {
await userStore.login(username.value, password.value)
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => uni.switchTab({ url: '/pages/index/index' }), 500)
} catch (e) {
// 错误提示已在 request 里统一处理
} finally {
loading.value = false
}
}
</script>

五、跨端差异注意

  1. H5 端可以 localStorage,小程序不行:统一用 uni.setStorageSync/getStorageSync,别混用;
  2. uni.request 的 header 区分大小写:Content-Type 各端表现可能不同,建议按上面示例统一设置;
  3. 小程序合法域名:正式发布必须在微信公众平台配置 request 合法域名,否则请求全挂;
  4. Pinia 在跨端:Vue3 版 uni-app 完全兼容 Pinia,Vue2 版则用 Vuex。

六、常见坑

  1. uni.showLoading 没 hide:loading 弹窗会一直挂着,用 try/finally 保证隐藏;
  2. 401 重复弹窗:多个请求同时 401 会跳多次登录页,可加”是否正在跳转”标记去重;
  3. token 过期但接口继续打:请求拦截器里 401 直接清 token 并中断,别让后续请求带着旧 token 继续打。

总结

跨端项目的状态管理套路:Pinia 管全局状态(登录态持久化到 storage)→ 统一 request 封装(token 注入、401 处理、错误提示)→ 页面里 async/await 消费。这套组合在 H5、微信小程序、App 端行为一致,是 uni-app 项目的标准架构。