ionic + vue3 + capacitor遇到backButton问题

项目背景: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 时踩的坑,自己记录一下攒攒经验,也希望能对你有用哦~

相关推荐
qingyun98920 小时前
Web Components 实战:创建自定义比例条组件
前端
GIS之路20 小时前
GDAL 空间关系解析
前端
布列瑟农的星空20 小时前
WebAssembly入门(一)——Emscripten
前端·后端
贵州数擎科技有限公司21 小时前
一批优质 AI 域名转让(.ai)|适合 AI 创业 / 产品 / 公司品牌
前端
小二·21 小时前
微前端架构完全指南:qiankun 与 Module Federation 双方案深度对比(Vue 3 + TypeScript)
前端·架构·typescript
EndingCoder21 小时前
枚举类型:常量集合的优雅管理
前端·javascript·typescript
Electrolux21 小时前
[wllama]纯前端实现大语言模型调用:在浏览器里跑 AI 是什么体验。以调用腾讯 HY-MT1.5 混元翻译模型为例
前端·aigc·ai编程
sanra12321 小时前
前端定位相关技巧
前端·vue
起名时在学Aiifox21 小时前
从零实现前端数据格式化工具:以船员经验数据展示为例
前端·vue.js·typescript·es6