在UniApp H5中,刷新页面会清空路由栈是因为浏览器的刷新行为会重新加载整个应用。要实现路由栈的持久化,需要手动保存和恢复路由状态。以下是几种解决方案:
方案1:使用 localStorage 保存路由栈(推荐)
1.1 创建路由管理工具
typescript
// utils/routerManager.ts
import { ref } from 'vue'
interface RouteInfo {
url: string
query?: Record<string, any>
meta?: Record<string, any>
timestamp: number
}
class RouterManager {
private readonly STORAGE_KEY = 'uni_router_stack'
private readonly MAX_STACK_SIZE = 10
private stack: RouteInfo[] = []
constructor() {
this.loadFromStorage()
this.setupListeners()
}
// 监听路由变化
private setupListeners() {
// 监听页面加载
uni.onAppRoute((res) => {
this.addToStack({
url: res.path,
query: res.query,
timestamp: Date.now()
})
})
// 监听页面卸载(返回)
window.addEventListener('popstate', () => {
this.removeLastFromStack()
})
}
// 添加到路由栈
addToStack(route: RouteInfo) {
// 避免重复添加相同路由
const lastRoute = this.stack[this.stack.length - 1]
if (lastRoute && lastRoute.url === route.url) {
return
}
this.stack.push(route)
// 限制栈大小
if (this.stack.length > this.MAX_STACK_SIZE) {
this.stack.shift()
}
this.saveToStorage()
}
// 从栈中移除最后一个
removeLastFromStack() {
if (this.stack.length > 1) {
this.stack.pop()
this.saveToStorage()
}
}
// 清空路由栈(保留首页)
clearStack() {
if (this.stack.length > 0) {
const homePage = this.stack[0]
this.stack = [homePage]
this.saveToStorage()
}
}
// 获取路由栈
getStack() {
return [...this.stack]
}
// 获取上一个路由
getPreviousRoute() {
if (this.stack.length > 1) {
return this.stack[this.stack.length - 2]
}
return null
}
// 保存到本地存储
private saveToStorage() {
try {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.stack))
} catch (e) {
console.error('保存路由栈失败:', e)
}
}
// 从本地存储加载
private loadFromStorage() {
try {
const data = localStorage.getItem(this.STORAGE_KEY)
if (data) {
this.stack = JSON.parse(data)
}
} catch (e) {
console.error('加载路由栈失败:', e)
this.stack = []
}
}
// 恢复路由栈
async restoreStack() {
if (this.stack.length <= 1) return
// 获取当前页面
const currentPages = getCurrentPages()
if (currentPages.length === 0) return
const currentRoute = currentPages[currentPages.length - 1]
const currentPath = currentRoute.route
// 如果不是首页,且路由栈中有历史记录
if (this.stack.length > 1 && currentPath !== this.stack[0].url) {
// 逐页跳转(从第二个开始,跳过首页)
for (let i = 1; i < this.stack.length; i++) {
const route = this.stack[i]
if (route.url !== currentPath) {
await this.navigateToRoute(route)
}
}
}
}
// 跳转到指定路由
private navigateToRoute(route: RouteInfo): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
uni.navigateTo({
url: this.buildUrl(route.url, route.query),
success: resolve,
fail: resolve
})
}, 50) // 添加延迟避免跳转冲突
})
}
// 构建 URL
private buildUrl(path: string, query?: Record<string, any>): string {
if (!query) return `/${path}`
const queryStr = Object.entries(query)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&')
return `/${path}?${queryStr}`
}
}
export const routerManager = new RouterManager()
1.2 在 App.vue 中初始化
vue
<script setup lang="ts">
import { onLaunch, onShow } from '@dcloudio/uni-app'
import { routerManager } from '@/utils/routerManager'
onLaunch(() => {
console.log('App Launch')
// 如果是H5环境,恢复路由栈
// #ifdef H5
setTimeout(() => {
routerManager.restoreStack()
}, 1000) // 等待页面加载完成
// #endif
})
onShow(() => {
console.log('App Show')
})
</script>
1.3 在页面中记录路由
vue
<script setup lang="ts">
import { onLoad, onUnload } from '@dcloudio/uni-app'
import { routerManager } from '@/utils/routerManager'
onLoad((query) => {
// 记录当前页面到路由栈
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
routerManager.addToStack({
url: currentPage.route,
query,
timestamp: Date.now()
})
})
// 监听页面返回
onUnload(() => {
routerManager.removeLastFromStack()
})
</script>
方案2:使用 URL 参数保存路由历史(SEO友好)
2.1 创建路由中间件
typescript
// utils/routeHistory.ts
export class RouteHistory {
private readonly QUERY_KEY = 'history'
private readonly MAX_HISTORY = 5
// 编码路由历史
encodeHistory(routes: Array<{path: string, query?: any}>): string {
const history = routes.map(route => ({
p: route.path,
q: route.query || {}
}))
return btoa(encodeURIComponent(JSON.stringify(history)))
}
// 解码路由历史
decodeHistory(encoded: string): Array<{path: string, query: any}> {
try {
const decoded = decodeURIComponent(atob(encoded))
return JSON.parse(decoded)
} catch {
return []
}
}
// 获取当前路由历史
getCurrentHistory(): Array<{path: string, query: any}> {
const pages = getCurrentPages()
return pages.map(page => ({
path: page.route,
query: page.options || {}
}))
}
// 更新 URL 中的历史参数
updateUrlHistory() {
// #ifdef H5
const history = this.getCurrentHistory()
if (history.length <= 1) return
const encoded = this.encodeHistory(history)
const currentUrl = new URL(window.location.href)
currentUrl.searchParams.set(this.QUERY_KEY, encoded)
// 使用 history.replaceState 更新 URL(不刷新页面)
window.history.replaceState({}, '', currentUrl.toString())
// #endif
}
// 从 URL 恢复路由历史
async restoreFromUrl(): Promise<boolean> {
// #ifdef H5
const url = new URL(window.location.href)
const encodedHistory = url.searchParams.get(this.QUERY_KEY)
if (!encodedHistory) return false
const history = this.decodeHistory(encodedHistory)
if (history.length <= 1) return false
// 获取当前页面
const currentPages = getCurrentPages()
if (currentPages.length === 0) return false
const currentRoute = currentPages[currentPages.length - 1]
// 逐页跳转(跳过当前页)
for (let i = 1; i < history.length; i++) {
const route = history[i]
if (route.path !== currentRoute.route) {
await this.navigateTo(route)
}
}
// 清理 URL 参数
url.searchParams.delete(this.QUERY_KEY)
window.history.replaceState({}, '', url.toString())
return true
// #endif
return false
}
private navigateTo(route: {path: string, query: any}): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
const queryStr = Object.entries(route.query)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&')
const url = `/${route.path}${queryStr ? `?${queryStr}` : ''}`
uni.navigateTo({
url,
success: resolve,
fail: resolve
})
}, 50)
})
}
}
export const routeHistory = new RouteHistory()
2.2 封装路由跳转方法
typescript
// utils/navigate.ts
import { routeHistory } from './routeHistory'
// 封装 navigateTo
export const navigateTo = (options: UniApp.NavigateToOptions) => {
uni.navigateTo({
...options,
success: () => {
// 更新 URL 历史
setTimeout(() => {
routeHistory.updateUrlHistory()
}, 100)
}
})
}
// 封装 redirectTo
export const redirectTo = (options: UniApp.RedirectToOptions) => {
uni.redirectTo({
...options,
success: () => {
setTimeout(() => {
routeHistory.updateUrlHistory()
}, 100)
}
})
}
// 封装 navigateBack
export const navigateBack = (options: UniApp.NavigateBackOptions = {}) => {
uni.navigateBack(options)
}
2.3 在页面中使用
vue
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app'
import { navigateTo } from '@/utils/navigate'
import { routeHistory } from '@/utils/routeHistory'
onLoad(() => {
// 恢复路由历史
// #ifdef H5
routeHistory.restoreFromUrl()
// #endif
})
const goToDetail = () => {
navigateTo({
url: '/pages/detail/detail?id=123'
})
}
</script>
方案3:使用 Vuex/Pinia 配合 localStorage
3.1 Pinia Store
typescript
// stores/routerStore.ts
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
interface RouteItem {
path: string
fullPath: string
query: Record<string, any>
params?: Record<string, any>
timestamp: number
}
export const useRouterStore = defineStore('router', () => {
const stack = ref<RouteItem[]>([])
const isRestoring = ref(false)
// 从 localStorage 加载
const loadFromStorage = () => {
try {
const saved = localStorage.getItem('uni_router_stack')
if (saved) {
stack.value = JSON.parse(saved)
}
} catch (e) {
console.error('加载路由栈失败', e)
}
}
// 保存到 localStorage
const saveToStorage = () => {
if (isRestoring.value) return
try {
localStorage.setItem('uni_router_stack', JSON.stringify(stack.value))
} catch (e) {
console.error('保存路由栈失败', e)
}
}
// 添加路由
const pushRoute = (route: Omit<RouteItem, 'timestamp'>) => {
if (isRestoring.value) return
const routeItem: RouteItem = {
...route,
timestamp: Date.now()
}
// 去重
const lastRoute = stack.value[stack.value.length - 1]
if (!lastRoute || lastRoute.fullPath !== routeItem.fullPath) {
stack.value.push(routeItem)
// 限制栈大小
if (stack.value.length > 10) {
stack.value = stack.value.slice(-10)
}
}
}
// 移除最后一个路由
const popRoute = () => {
if (isRestoring.value || stack.value.length <= 1) return
stack.value.pop()
}
// 清空路由栈(保留首页)
const clearStack = () => {
if (stack.value.length > 0) {
const homeRoute = stack.value.find(route =>
route.path === 'pages/index/index' ||
route.path === 'pages/home/home'
)
stack.value = homeRoute ? [homeRoute] : []
}
}
// 恢复路由栈
const restoreStack = async () => {
if (stack.value.length <= 1) return
isRestoring.value = true
const currentPages = getCurrentPages()
if (currentPages.length === 0) {
isRestoring.value = false
return
}
const currentPage = currentPages[currentPages.length - 1]
// 逐页跳转
for (let i = 1; i < stack.value.length; i++) {
const route = stack.value[i]
// 跳过当前页
if (route.path === currentPage.route) continue
await new Promise<void>((resolve) => {
uni.navigateTo({
url: route.fullPath,
success: resolve,
fail: resolve
})
setTimeout(resolve, 100)
})
}
setTimeout(() => {
isRestoring.value = false
}, 500)
}
// 自动保存到 localStorage
watch(stack, saveToStorage, { deep: true })
// 初始化
loadFromStorage()
return {
stack,
pushRoute,
popRoute,
clearStack,
restoreStack
}
})
3.2 路由守卫
typescript
// utils/routerGuard.ts
import { useRouterStore } from '@/stores/routerStore'
export const setupRouterGuard = () => {
// 监听页面加载
uni.onAppRoute((res) => {
const routerStore = useRouterStore()
routerStore.pushRoute({
path: res.path,
fullPath: `${res.path}${res.query ? '?' + new URLSearchParams(res.query).toString() : ''}`,
query: res.query || {}
})
})
// 监听页面卸载(返回)
const originalNavigateBack = uni.navigateBack
uni.navigateBack = function(options) {
const routerStore = useRouterStore()
routerStore.popRoute()
return originalNavigateBack(options)
}
}
3.3 在 main.ts 中初始化
typescript
// main.ts
import { createSSRApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import { setupRouterGuard } from './utils/routerGuard'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
// 设置路由守卫
setupRouterGuard()
return {
app,
pinia
}
}
方案4:使用 sessionStorage 临时存储(刷新后失效)
4.1 简单实现
javascript
// 在页面显示时保存
onShow(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
// 保存当前路由栈
const stack = pages.map(page => ({
route: page.route,
options: page.options
}))
sessionStorage.setItem('current_route_stack', JSON.stringify(stack))
})
// 页面加载时恢复
onLoad(() => {
// 检查是否需要恢复
const shouldRestore = sessionStorage.getItem('need_restore')
if (shouldRestore === 'true') {
const savedStack = sessionStorage.getItem('current_route_stack')
if (savedStack) {
const stack = JSON.parse(savedStack)
// 跳过首页,逐页跳转
for (let i = 1; i < stack.length; i++) {
const page = stack[i]
const queryStr = page.options ?
Object.entries(page.options)
.map(([key, value]) => `${key}=${value}`)
.join('&') : ''
uni.navigateTo({
url: `/${page.route}${queryStr ? '?' + queryStr : ''}`,
animationType: 'none' // 无动画
})
}
}
// 清除标记
sessionStorage.removeItem('need_restore')
}
})
// 监听页面刷新
// #ifdef H5
window.addEventListener('beforeunload', () => {
sessionStorage.setItem('need_restore', 'true')
})
// #endif
最佳实践建议
-
选择方案1(localStorage):
- 最稳定可靠
- 支持页面完全关闭后恢复
- 需要处理数据过期和清理
-
结合多种方案:
typescript
// 综合方案
class RouterPersistence {
async saveStack() {
// 同时保存到 localStorage 和 URL
localStorage.setItem('router_stack', JSON.stringify(this.stack))
this.updateUrlWithStack()
}
async restoreStack() {
// 1. 尝试从 URL 恢复(最准确)
const fromUrl = await this.restoreFromUrl()
if (fromUrl) return true
// 2. 尝试从 localStorage 恢复
const fromStorage = await this.restoreFromStorage()
if (fromStorage) return true
// 3. 使用默认首页
return false
}
}
-
注意事项:
- 路由栈大小限制(建议5-10层)
- 数据序列化和反序列化安全
- 避免循环跳转
- 处理特殊字符和编码
- 考虑隐私模式(localStorage 不可用)
-
清理策略:
typescript
// 定期清理过期路由
setInterval(() => {
const now = Date.now()
const expiredTime = 30 * 60 * 1000 // 30分钟
routerManager.stack = routerManager.stack.filter(
route => now - route.timestamp < expiredTime
)
}, 5 * 60 * 1000) // 每5分钟检查一次
推荐使用方案1,它提供了最完整的路由栈管理功能,适合大多数H5应用场景。