项目背景:ionic + vue3 + capacitor
需求描述:需要通过 capacitor 提供的 backButton 方法,来监听安卓原生的返回按键事件。
1、bug:使用 capacitor 封装的 backButton 方法来监听安卓原生返回键,如果不是根页面的情况,点击返回键,路由出现了两次回退(正常情况是只回退一次)。
2、addListener(' backButton**', async () => {})使用方法参考 capacitor 官方文档:**
addListener(eventName: 'backButton', listenerFunc: (data: AppUrlOpen) => void) => PluginListenerHandle
监听硬件返回按钮事件(仅限 Android)。监听此事件会禁用默认的返回按钮行为,因此您可能需要手动调用 window.history.back()。如果要关闭应用,请调用 App.exitApp()

3、找到问题所在 
因为文档里说使用这个监听方法会禁用掉原生返回按键的默认行为,可能需要手动添加window.history.back()事件,所以我也认为默认行为被禁用掉,我就自定义返回事件。
TypeScript
const rootPages = [
'/login',
'/tabs/index',
]
/**
* 初始化返回键监听
*/
const initBackButtonListener = async () => {
if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== 'android')
return
// 移除已有监听,避免重复绑定
if (_backButtonListener) {
await _backButtonListener.remove()
}
_backButtonListener = await App.addListener('backButton', async () => {
const currentPath = router.currentRoute.value?.path || ''
const isRootPage = rootPages.includes(currentPath)
try {
// 判断是根页面,则退出应用
if (isRootPage) {
await exitApp()
}
else {
// 不是根页面,返回上一级
window.history.back()
}
}
catch (err) {
console.error('返回键逻辑执行失败:', err)
}
})
}
但是这里我使用这个方法,并没有禁用掉原生返回按钮的默认行为,导致我在点击返回按键的时候,原生按钮触发1次返回,我又自定义了1次返回,两个方法叠加了,于是就出现了开头说的,点击返回按键1次,路由回退2次的现象。这个不知道是组件的bug还是什么导致的,不是很清楚,找了网上各路道友的经验贴,有处理结果的帖子很少,大多都是提的问题。
后面自己不断地安装排查,发现就是上述原因导致,于是我将自定义的路由返回注释掉,判断当前是否为根页面,是根页面,则点击返回按键退出应用;不是根页面,这里不做 window.history.back() 处理,需注释掉,默认原生返回按钮事件,返回上一级页面,功能就正常了。
除此之外,还需注意路由的跳转方式的区别,使用 push 和 replace 也会对退出应用有影响,一级页面互相跳转建议使用 replace 去跳转,避免点击返回键退出应用时,发生路由回退现象。
4、完整代码
appStatus.ts
里面有一个监听应用状态(应用切换前/后台)事件的方法、一个监听返回按键事件的方法和移出全部监听事件的方法。将方法在 App.vue 里面去进行初始化,代码如下:
TypeScript
import { useUserStore } from '@/store/user'
import { App } from '@capacitor/app'
import { Capacitor, type PluginListenerHandle } from '@capacitor/core'
import { defineStore } from 'pinia'
import { useRouter } from 'vue-router'
export const useAppStatusStore = defineStore('appStatus', () => {
// 是否前台,默认可见
const isForeground = ref<boolean>(!document.hidden)
const userStore = useUserStore()
const router = useRouter()
/**
* 清除 Token 并跳转登录页(后台切换时调用)
*/
const clearTokenAndRedirect = () => {
try {
// 清除 Token
userStore.logout()
// 跳转登录页
if (router.currentRoute.value?.path !== '/login') {
router.push('/login').catch((err) => {
console.warn('跳转登录页失败:', err)
})
}
// showToast('登录状态已失效,请重新登录')
}
catch (err) {
console.error('清除 Token 或跳转登录失败:', err)
}
}
// 网页端监听
const handleVisibility = () => {
isForeground.value = !document.hidden
if (document.hidden)
clearTokenAndRedirect()
}
// 应用状态监听器-原生App监听
let _appStateListener: PluginListenerHandle | undefined
let _pauseListener: PluginListenerHandle | undefined
/**
* App原生监听
*/
const initAppStateListener = async () => {
if (Capacitor.isNativePlatform()) {
// 监听 appStateChange 事件,获取app应用状态(后台/前台)
_appStateListener = await App.addListener(
'appStateChange',
(state: { isActive: boolean }) => {
isForeground.value = state.isActive
// console.log('应用状态:', state.isActive ? '前台' : '后台')
// 切后台时清除 Token
if (!state.isActive) {
clearTokenAndRedirect()
}
},
)
// 监听 pause 事件,增强后台检测
_pauseListener = await App.addListener('pause', () => {
// console.log('应用进入暂停(后台)')
clearTokenAndRedirect()
})
}
else {
document.addEventListener('visibilitychange', handleVisibility)
}
}
/**
* 主动退出应用(仅支持安卓),退出前清除 Token
*/
const exitApp = async () => {
if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android') {
userStore.logout() // 退出前清除 Token
await App.exitApp() // 调用主动退出方法
}
}
// 返回键监听器-原生应用监听
let _backButtonListener: PluginListenerHandle | undefined
// 连续按返回键的定时器,避免误触
// let _exitTimer: NodeJS.Timeout | null = null
// 定义根页面,如果还有其他的根页面,往里加就行
const rootPages = [
'/login',
'/tabs/index',
]
/**
* 初始化返回键监听
*/
const initBackButtonListener = async () => {
if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== 'android')
return
// 移除已有监听,避免重复绑定
if (_backButtonListener) {
await _backButtonListener.remove()
}
_backButtonListener = await App.addListener('backButton', async () => {
const currentPath = router.currentRoute.value?.path || ''
const isRootPage = rootPages.includes(currentPath)
try {
// 判断是根页面,则退出应用
if (isRootPage) {
await exitApp()
// // 根页面,则处理退出应用
// if (_exitTimer) {
// // 第二次按返回键:清除定时器 + 退出应用
// clearTimeout(_exitTimer)
// _exitTimer = null
// // 退出前清除 Token
// userStore.logout()
// // 调用 exitApp() 退出应用,仅支持安卓
// await App.exitApp()
// }
// else {
// // 第一次按返回键:提示"再按一次退出"
// showToast('再按一次返回键退出应用')
// _exitTimer = setTimeout(() => {
// _exitTimer = null
// }, 2000) // 2秒内未再次按则重置
// }
}
else {
// 不是根页面,这里不做 window.history.back() 处理,需注释掉,默认原生返回按钮事件,返回上一级页面
// window.history.back()
}
}
catch (err) {
console.error('返回键逻辑执行失败:', err)
}
})
}
// 销毁监听
const removeAllListeners = async () => {
// 销毁应用切换后台及退出应用事件监听
if (_appStateListener)
await _appStateListener.remove()
if (_pauseListener)
await _pauseListener.remove()
// 销毁返回按键事件监听
if (_backButtonListener) {
await _backButtonListener.remove()
_backButtonListener = undefined
}
// if (_exitTimer) {
// clearTimeout(_exitTimer)
// _exitTimer = null
// }
// 移动端移除全部监听
await App.removeAllListeners()
// 网页端移除监听
if (!Capacitor.isNativePlatform()) {
document.removeEventListener('visibilitychange', () => handleVisibility)
}
}
return {
isForeground,
initAppStateListener,
exitApp,
removeAllListeners,
clearTokenAndRedirect,
initBackButtonListener,
}
})
App.vue
当组件挂在后初始化应用状态监听
TypeScript
<script setup lang="ts">
import { useAppStatusStore } from '@/store/appStatus'
import { useUserStore } from '@/store/user'
import { debounce } from 'lodash'
const _ = (window as any).ResizeObserver;
(window as any).ResizeObserver = class ResizeObserver extends _ {
constructor(callback: (...args: any[]) => void) {
callback = debounce(callback, 100)
super(callback)
}
}
const appStatusStore = useAppStatusStore()
const userStore = useUserStore()
const router = useRouter()
if (userStore.userInfo.token) {
router.replace('/tabs')
}
else {
router.replace('/login')
}
onMounted(async () => {
// 初始化应用状态监听
await appStatusStore.initAppStateListener()
// console.log('当前是否前台:', appStatusStore.isForeground)
// 初始化返回键监听
await appStatusStore.initBackButtonListener()
})
// 组件销毁时清理所有监听
onUnmounted(async () => {
await appStatusStore.removeAllListeners()
})
</script>
<template>
<IonApp>
<IonRouterOutlet />
</IonApp>
</template>
以上是使用 capacitor 时踩的坑,自己记录一下攒攒经验,也希望能对你有用哦~