UniApp + Vue3 持久化登录实现方案
📖 前言
在移动应用开发中,用户体验是至关重要的。用户退出应用到后台后,重新打开应用时如果还需要重新登录,这无疑会大大降低用户体验。本文将详细介绍如何在 UniApp + Vue3 项目中实现持久化登录功能,让用户退出后台再次进入应用时仍然保持登录状态。
🎯 需求分析
核心需求:
- 用户登录成功后,将登录信息持久化保存
- 应用退出到后台后,重新打开时自动恢复登录状态
- 已登录用户打开应用时,自动跳转到首页,而不是停留在登录页
技术要点:
- 数据持久化存储(本地存储)
- uni-app应用生命周期(onLaunch、onShow)
- 状态恢复和自动跳转逻辑
🏗️ 核心实现思路
1. 数据持久化存储
使用 UniApp 的 uni.setStorageSync API 将登录相关信息保存到本地存储:
- Token:用户身份凭证
- 用户信息:用户基本信息
- 其他相关数据:如房屋信息、配置信息等
2. 应用生命周期管理
利用 UniApp 的生命周期钩子实现自动恢复:
- onLaunch:应用首次启动时恢复状态
- onShow:应用从后台恢复时恢复状态
- onHide:应用进入后台时无需额外操作
3. 自动跳转逻辑
检测到已登录且当前在登录页时,自动跳转到首页,并防止重复跳转。
💻 技术实现详解
一、Store 状态管理(stores/user.ts)
1.1 基础结构
typescript
import { defineStore } from "pinia";
import { ref } from "vue";
export const useUserStore = defineStore("user", () => {
const token = ref('')
const userInfo = ref<any>(null)
const verifiedHouse = ref<any>(null)
// ... 其他代码
})
1.2 Token 管理方法
typescript
// 从本地存储获取token
const getToken = () => {
try {
const localToken = uni.getStorageSync('user_token')
if (localToken) {
token.value = localToken
}
return token.value
} catch (error) {
console.error('获取token失败:', error)
return ''
}
}
// 设置token并保存到本地存储
const setToken = (val: string) => {
token.value = val
try {
uni.setStorageSync('user_token', val)
} catch (error) {
console.error('保存token失败:', error)
}
}
// 清除token和本地存储
const clearToken = () => {
token.value = ''
try {
uni.removeStorageSync('user_token')
} catch (error) {
console.error('清除token失败:', error)
}
}
要点说明:
setToken()时同步保存到本地存储getToken()时优先从本地存储读取,确保数据持久化- 清除 token 时同时清除本地存储
1.3 用户信息管理
typescript
// 设置用户信息
const setUserInfo = (info: any) => {
userInfo.value = info;
try {
uni.setStorageSync('user_info', info);
} catch (error) {
console.error('保存用户信息失败:', error)
}
}
// 清除用户信息
const clearUserInfo = () => {
userInfo.value = null;
try {
uni.removeStorageSync('user_info');
} catch (error) {
console.error('清除用户信息失败:', error)
}
}
1.4 恢复登录状态方法
typescript
// 恢复登录状态(从本地存储恢复所有登录相关的信息)
const restoreLoginState = () => {
// 恢复token
getToken()
// 恢复用户信息
try {
const localUserInfo = uni.getStorageSync('user_info')
if (localUserInfo) {
userInfo.value = localUserInfo
}
} catch (error) {
console.error('恢复用户信息失败:', error)
}
// 恢复房屋信息
try {
const localHouseInfo = uni.getStorageSync('verifiedHouse')
if (localHouseInfo) {
verifiedHouse.value = localHouseInfo
}
} catch (error) {
console.error('恢复房屋信息失败:', error)
}
}
核心作用:
- 统一恢复所有登录相关的状态
- 在应用启动和后台恢复时调用
- 确保内存中的状态与本地存储同步
1.5 Store 初始化
typescript
// 初始化时自动从本地存储恢复数据
getToken()
try {
const localUserInfo = uni.getStorageSync('user_info')
if (localUserInfo) {
userInfo.value = localUserInfo
}
} catch (error) {
console.error('获取用户信息失败:', error)
}
return {
token, getToken, setToken, clearToken,
userInfo, setUserInfo, clearUserInfo,
verifiedHouse, setHouseInfo, getHouseInfo, clearHouseInfo,
restoreLoginState
}
二、App.vue 应用级处理
2.1 全局变量定义
typescript
<script setup lang="ts">
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import { nextTick } from 'vue'
import { checkPermission } from '@/common/permission'
import { useUserStore } from '@/stores/user'
// 防止重复跳转的标志位
let isNavigating = false
// 记录是否是首次启动
let isFirstLaunch = true
设计说明:
isNavigating:防止并发跳转导致的路由冲突isFirstLaunch:区分首次启动和后台恢复,避免重复处理
2.2 onLaunch 处理逻辑
typescript
onLaunch(() => {
// 1. 恢复登录状态
const userStore = useUserStore()
userStore.restoreLoginState()
// 2. 延迟检查并跳转(确保页面已加载)
setTimeout(() => {
try {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage) {
const currentRoute = '/' + currentPage.route
const currentToken = userStore.getToken() || uni.getStorageSync('user_token')
// 如果已登录且当前在登录页,跳转到首页
if (currentToken && currentRoute === '/pages/login/index' && !isNavigating) {
isNavigating = true
// 使用 nextTick 确保在路由拦截器注册之后执行
nextTick(() => {
uni.reLaunch({
url: '/pages/Home/HomeIndex',
success: () => {
isNavigating = false
},
fail: () => {
isNavigating = false
}
})
})
}
}
} catch (error) {
// 静默处理错误
}
}, 300)
// 3. 添加路由拦截器
uni.addInterceptor('navigateTo', {
invoke(e: any) {
return checkPermission(e.url)
}
})
uni.addInterceptor('redirectTo', {
invoke(e: any) {
return checkPermission(e.url)
}
})
uni.addInterceptor('reLaunch', {
invoke(e: any) {
return checkPermission(e.url)
}
})
uni.addInterceptor('switchTab', {
invoke(e: any) {
return checkPermission(e.url)
}
})
})
关键点解析:
- 延迟检查 :使用
setTimeout(300ms)确保页面栈已初始化完成 - nextTick 使用:确保路由拦截器注册完成后再执行跳转
- 条件判断 :
- 检查是否有 token
- 检查当前是否在登录页
- 检查是否正在跳转中(
!isNavigating)
- 标志位管理 :跳转前后设置和重置
isNavigating
2.3 onShow 处理逻辑
typescript
onShow(() => {
// 1. 恢复登录状态
const userStore = useUserStore()
userStore.restoreLoginState()
// 2. 首次启动时跳过(onLaunch 已处理)
if (isFirstLaunch) {
isFirstLaunch = false
return
}
// 3. 后台恢复时检查跳转
setTimeout(() => {
try {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage) {
const currentRoute = '/' + currentPage.route
const currentToken = userStore.getToken() || uni.getStorageSync('user_token')
// 如果已登录且当前在登录页,跳转到首页
if (currentToken && currentRoute === '/pages/login/index' && !isNavigating) {
isNavigating = true
uni.reLaunch({
url: '/pages/Home/HomeIndex',
success: () => {
isNavigating = false
},
fail: () => {
isNavigating = false
}
})
}
}
} catch (error) {
// 静默处理错误
}
}, 300)
})
onHide(() => {
// 应用进入后台时,token已经保存在本地存储中,无需额外操作
})
设计说明:
- 首次启动时
onShow也会触发,但此时onLaunch已处理跳转,需要跳过 - 只在真正的后台恢复时(非首次启动)才执行跳转检查
- 使用相同的延迟和检查逻辑,保持一致性
三、登录页兜底处理(pages/login/index.vue)
typescript
onMounted(async () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight
// 检查登录状态,如果已登录则跳转到首页
// 注意:App.vue 已经处理了自动跳转逻辑,这里只做兜底检查
const tokenFromStore = store.getToken()
const tokenFromStorage = uni.getStorageSync('user_token')
const token = tokenFromStore || tokenFromStorage
// 延迟检查,给 App.vue 的跳转逻辑一个执行机会
// 如果 App.vue 没有跳转,这里作为兜底
if (token) {
setTimeout(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage) {
const currentRoute = '/' + currentPage.route
// 如果还在登录页,说明 App.vue 没有跳转,这里执行跳转
if (currentRoute === '/pages/login/index') {
uni.reLaunch({
url: '/pages/Home/HomeIndex'
})
return
}
}
}, 500)
return
}
// 未登录,继续登录流程
await initAliSDK()
})
设计说明:
- 延迟更长(500ms):给 App.vue 的跳转逻辑(300ms)足够的执行时间
- 兜底机制:确保即使 App.vue 的跳转失败,登录页也能处理
- 双重检查:从 Store 和本地存储两个来源获取 token,提高可靠性
🔄 完整流程图
scss
┌─────────────────────────────────────────────────────────┐
│ 用户登录成功 │
└──────────────────┬──────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────┐
│ 保存 token 到本地存储 (uni.setStorageSync) │
│ - user_token: token │
│ - user_info: 用户信息 │
│ - verifiedHouse: 房屋信息 │
└──────────────────┬──────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────┐
│ 用户退出应用到后台 │
└──────────────────┬──────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────┐
│ 用户重新打开应用 │
└──────────────────┬──────────────────────────────────────┘
│
↓
┌──────────┴──────────┐
│ │
↓ ↓
┌──────────────┐ ┌──────────────┐
│ onLaunch │ │ onShow │
│ 触发 │ │ 触发 │
└──────┬───────┘ └──────┬────────┘
│ │
↓ ↓
┌──────────────┐ ┌──────────────┐
│restoreLogin │ │restoreLogin │
│State() │ │State() │
└──────┬───────┘ └──────┬────────┘
│ │
↓ ↓
┌──────────────┐ ┌──────────────┐
│ 延迟 300ms │ │首次启动? │
│ 检查跳转 │ │是 → 跳过 │
└──────┬───────┘ │否 → 延迟300ms │
│ │ 检查跳转 │
↓ └──────┬────────┘
┌──────────────┐ │
│检查条件: │ ↓
│1. 有token? │ ┌──────────────┐
│2. 在登录页? │ │检查条件: │
│3. 未在跳转? │ │1. 有token? │
└──────┬───────┘ │2. 在登录页? │
│ │3. 未在跳转? │
↓ └──────┬───────┘
┌──────────────┐ │
│是 → 跳转首页 │ ↓
│否 → 保持现状 │ ┌──────────────┐
└──────────────┘ │是 → 跳转首页 │
│否 → 保持现状 │
└───────────────┘
│
↓
┌───────────────────────────┐
│ 登录页 onMounted (兜底) │
│ 延迟 500ms 检查 │
│ 仍在登录页?→ 执行跳转 │
└───────────────────────────┘
⚠️ 关键注意事项
1. 时序问题
问题: 页面栈可能在应用启动时还未完全初始化
解决方案:
- 使用
setTimeout延迟检查(300ms) - 确保
getCurrentPages()能正确获取当前页面
typescript
setTimeout(() => {
const pages = getCurrentPages()
// 此时页面栈已初始化完成
}, 300)
2. 避免重复跳转
问题: onLaunch 和 onShow 可能同时触发跳转,导致冲突
解决方案:
- 使用
isNavigating标志位防止并发跳转 - 首次启动时
onShow跳过处理 - 跳转成功后重置标志位
typescript
let isNavigating = false
if (currentToken && !isNavigating) {
isNavigating = true
uni.reLaunch({
url: '/pages/Home/HomeIndex',
success: () => {
isNavigating = false
}
})
}
3. 路由拦截器时序
问题: 跳转时路由拦截器可能还未注册完成
解决方案:
- 使用 Vue 的
nextTick确保拦截器已注册
typescript
nextTick(() => {
uni.reLaunch({ url: '/pages/Home/HomeIndex' })
})
4. 兜底机制
问题: 某些边缘情况下 App.vue 的跳转可能失效
解决方案:
- 在登录页的
onMounted中添加兜底检查 - 延迟时间(500ms)长于 App.vue(300ms),确保兜底
5. 错误处理
原则: 登录状态恢复不应该阻塞应用启动
实现:
- 使用 try-catch 包裹可能出错的操作
- 本地存储操作失败时使用默认值,不抛错
typescript
try {
const localToken = uni.getStorageSync('user_token')
if (localToken) {
token.value = localToken
}
} catch (error) {
console.error('获取token失败:', error)
return ''
}
scss
### 4. 登录状态监听
```typescript
// 监听登录状态变化
watch(() => userStore.token, (newToken, oldToken) => {
if (newToken && !oldToken) {
// 登录成功
emit('login-success')
} else if (!newToken && oldToken) {
// 退出登录
emit('logout')
}
})
📝 总结
本文详细介绍了一种在 UniApp + Vue3 项目中实现持久化登录的完整方案。核心思路是:
- 数据持久化:使用本地存储保存登录信息
- 状态恢复:在应用生命周期钩子中恢复状态
- 自动跳转:检测到已登录时自动跳转
- 兜底机制:多层级检查确保可靠性