项目背景: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()事件,所以我也认为默认行为被禁用掉,我就自定义返回事件。
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 里面去进行初始化,代码如下:
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
当组件挂在后初始化应用状态监听
<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 时踩的坑,自己记录一下攒攒经验,也希望能对你有用哦~