技术博客

越是喧闹,越是孤独。越是寂寞,越是丰富
The more noisy, the more lonely. The more lonely, the more rich

越是喧闹,越是孤独。越是寂寞,越是丰富
The more noisy, the more lonely. The more lonely, the more rich

技术博客


在 Hybrid App 中如何优雅地处理「后退」操作

2025-10-13 Mendel
前端


现在混合模式的应用很常见。混合应用,即 Hybrid App,通常指的是在原生页面中增加一些 WebView 来展示 Web 页面的这种 App。在形态上可以是部分业务使用 Web 页面,也可以是整个 App 仅保留一个原生外壳,几乎所有功能全部通过 Web 实现,必要的时候通过 JS-Bridge 与原生功能交互。


由于 Web 技术具有跨平台特性,部署比较方便,对于一些经常更新的活动页,或者接入一些第三方平台的页面等场景,使用 Web 技术的 Hybrid 模式就比较合适。


不论是 Android、iOS 还是 Web 平台,都有「后退」(返回上一级)操作,但它们在效果上会有一定差异。本文基于交互设计和前端开发的角度,分析一下如何在 Hybrid App 中优雅的处理「后退」,以尽可能实现更好的用户体验。




01

后退手势



我们知道,Android 和 iOS 设备的「后退」手势是不同的。我们就以手机举例,安卓手机的后退,一般是在屏幕左侧或右侧边缘侧滑出一个标记,滑出一小段距离后松开手指,则执行后退。苹果手机则是仅在屏幕左侧边缘向右滑动(最新的 iOS 26 中一些系统内置 APP 已支持全屏右滑),滑动的过程中当前页面会随着手指位置移动,前一个页面也会渗透出来,当松开手指时如果同时有一个向右的速度,则执行后退。


执行后退时,通常的逻辑是要关闭当前页面,或者关闭当前页面的弹出元素。以某商城 App 为例,用安卓和苹果手机分别打开某个商品详情页,并点开 SKU 选择窗口(浮窗/浮层)后,我们会发现在安卓上操作后退手势可关闭当前 SKU 窗口,而在苹果手机上此时无法使用后退手势,只能点击顶部遮罩关闭 SKU 窗口。演示视频如下:



从个人主观角度看,我更倾向于安卓的体验,因为可以全程通过相同的后退手势依次关闭当前页面栈,不论是关闭当前页面还是关闭页面中的弹出窗口等。而苹果的后退手势是向右侧滑,对于从页面底部弹出的 Sheet 窗口,如果要设计成右滑时 Sheet 同时向下方收回,那从体验上确实不好,所以这种情况硬要适配右滑手势没有必要,所以就仅支持点击 Sheet 中的关闭按钮或者 Sheet 上方的灰色遮罩来关闭。


多数情况下,安卓原生 App 的后退手势都能像上述视频中的处理方式一样,如果页面有弹出窗口则先关闭窗口,然后再关闭当前页。但是对于一些 Hybrid App,也许就没这么乐观了。我们以某银行的生活 App 为例,其中有些页面是通过 WebView 加载的第三方商城 Web 页面。如果在该 Web 页面内弹出了底部窗口或者是打开商品评论界面等,此时操作后退就会发现,并不是关闭了新打开的窗口,而是直接把整个 Web 页都关了,退回了最初的页面。请看下面的视频:



出现这个现象的原因就是,它没有对 Web 页面内部的历史栈(history stack)做后退处理,所以操作后退的时候直接走的默认行为,即关闭当前 WebView 所在的 Activity。我个人觉得这个体验是不太好的。


那么我们如果要做一个 Hybrid App 时,怎么处理好这种后退问题呢?




02

在 Android Hybrid 中处理后退



我们先准备一个简单的 Web 项目作为示例,基于 Vue Router 和 NaiveUI 框架构建,包含一个主页面(Home),一个子页面(Sub)。在 Home 页点击一个按钮即可打开子页面,子页面从右往左滑入,退出时从左向右滑出。演示如下:



其中子页面的入场出场动画是通过 Vue Transition 过渡效果实现,主要 CSS 部分代码如下:


.page-slide-enter-active,.page-slide-leave-active {    transition: transform 0.3s linear;}.page-slide-enter-from,.page-slide-leave-to {    transformtranslateX(100%);}


接着,我们准备一个 Android 的空项目,创建一个 Activity,其中填满一个全屏的 WebView,并加载刚刚的 Web 项目地址(本示例 Web 项目可以直接通过 https://mengchen.cc/hybrid-demo.html 访问,有需要的可以使用)。我们发现,默认情况下,如果 WebView 里打开了 Sub 子页面,当操作后退时,并不是关闭子页面而是关闭整个 App,就像前面的视频中那样。


解决方式也简单,对 WebView 所在的 Activity 重写后退事件的处理方法。如果当前 WebView 的历史栈不为空(即可以执行后退时),执行 WebView 的后退,否则再执行默认的后退行为。主要代码如下:


public void onBackPressed() {    if (webview.canGoBack()) {        webview.goBack();    } else {        super.onBackPressed();    }}


不过这个 onBackPressed 方法已经过时了,最新的写法要通过 

getOnBackPressedDispatcher().addCallback 实现,使用方式基本与上述代码相同,这里不再展开说明。


加上这段代码再次运行后就会发现,当 Sub 页面打开时,操作后退,执行的是关闭 Sub 页,再次后退,才会关闭 App。这是因为 Vue Router 是基于 History API 的(也可选 Hash 模式),当打开 Sub 页时会往 history stack 中 push 一条记录,使得这个页面可以后退。此时 webview.canGoBack() 为 true,webview.goBack() 相当于调用页面内的 history.back() 执行后退,然后被 Vue Router 处理为关闭 Sub 页面,这样就实现了顺畅的后退交互。如下视频所示:



那么问题又来了,像前文所述的在页面底部弹出浮层的情景,能不能实现在操作后退时关闭它呢?


目前主流的前端框架通常都有弹窗、抽屉等浮层组件,本文以基于 Vue 的 NaiveUI 框架为例,它的抽屉组件 Drawer、弹窗组件 Modal 在默认情况下,都不能通过浏览器的后退实现关闭。如果想要实现原生 App 那种操作后退时先关闭浮层,该怎么做呢?


这里分为两种情况:在主页面中关闭浮层和在子页面中关闭浮层。两者处理方式有些差异。


先说在子页面中的处理。Vue Router 有一个钩子函数 beforeRouteLeave,在路由即将离开之前触发,如果函数返回 false 则将阻止路由离开。因此利用这个特性,我们可以在函数中检查当前是否有浮层,如果有,先关闭它们。主要代码如下:


export default {  // ......  data() {      return {          showDrawerfalse,  //是否展示抽屉      }  },  beforeRouteLeave() {      //如果有抽屉,先关闭抽屉,并阻止路由离开      if (showDrawer) {          showDrawer = false          return false      }      return true  }  // ......  }
<n-drawer v-model:show="showDrawer">  <!-- drawer content --></n-drawer>


对于主页面,由于它本身不受路由的控制,因此不能通过 beforeRouteLeave 的方式拦截路由离开。而是需要自己通过 History API 手动操作历史栈来实现后退控制。主要步骤如下:


  • 为 window 绑定 popstate 事件,在事件函数中检测如果有浮层,则关闭浮层

  • 为页面内的浮层组件绑定 show/hide 事件,在 show 的时候向 history stack 添加一条记录,在 hide 的时候且不是由 popstate 触发的情况下弹出一条记录


主要代码如下:

<n-drawer v-model:show="showDrawer"    @after-enter="onDrawerShow"     @after-leave="onDrawerHide">  <!-- drawer content --></n-drawer>
export default {    data() {        return {            showDrawerfalse,            hideDrawerByHistoryfalse        }    },    methods: {        onPopState() {            if (this.showDrawer) {                this.hideDrawerByHistory = true                this.showDrawer = false            }        },        onDrawerShow() {            history.pushState({}, '')        },        onDrawerHide() {            if (!this.hideDrawerByHistory) {                history.back()            }            this.hideDrawerByHistory = false        }    },    mounted() {        window.addEventListener('popstate'this.onPopState)    }}


这样,不论是主页面还是子页面,都可以通过后退来关闭浮层,在 Android 上基本实现了比较好的交互体验。完整演示视频如下:






03

在 iOS Hybrid 中处理后退




在 iOS 开发中,需要对 WebView 启用后退导航手势,才可以在 Hybrid App 内使用返回手势。


webView.allowsBackForwardNavigationGestures = true


开启后,如果 WebView 的历史栈内有数据,则返回手势会自动触发 WebView 的后退。但是由于 iOS 的后退本身自带动画效果,会与 Web 中的子页面退出动画冲突,效果如下所示:



出现这个问题的原因是,iOS 的后退手势控制执行完成动画(iOS 原生动画)后,才通知给 WebView 执行了后退,此时页面中的 Vue Router 接管了后退处理,重新执行了退出动画(CSS 动画),所以看到了两次动画。此时就需要对 Vue 的动画做一些特殊处理,例如当前环境如果是 iOS,则使用另一套 Transition 模式,仅保留进场动画,不设置出场动画。倘若用户不是通过后退手势而是正常点击子页面中的后退按钮的话,则继续使用原有动画。


针对 iOS 环境的一套 Transition 样式:

.page-slide-ios-enter-from {    transformtranslateX(100%);}
.page-slide-ios-enter-active {    transition: transform 0.3s linear;}
.page-slide-ios-leave-from {    /* 设置 display:none 可以解决iOS中页面退出瞬间闪烁的问题 */    display: none;}


子页面点击后退按钮时向主页面发送消息:

export default {    data() {        return {            isIPhone/iphone/i.test(navigator.userAgent)        }    },    methods: {        goBack() {            if (this.isIPhone) {                this.$emit("goBackOnIPhone")            }            history.back()        }    }}


主页面接收消息后变更动画名,短暂后再恢复为原有名称。

<RouterView v-slot="{ Component }" @goBackOnIPhone="switchTransitionName">    <Transition :name="transitionName">        <component :is="Component" />    </Transition></RouterView>
switchTransitionName() {    this.transitionName = "page-slide"    setTimeout(() => {        this.transitionName = "page-slide-ios"    }, 500)}


做了这些就可以解决后退动画冲突的问题。但是对于页面内的浮层,iOS 的后退手势并不适合处理它们的关闭,所以需要在之前的代码中增加判断,忽略 iOS 环境下对浮层的额外处理。最终我们实现在 iOS Hybrid 中的演示效果如下:



综上所述,我们深入探讨了在 Hybrid App 中统一并优化“后退”交互的可行方案,以实现让 Web 页面在原生容器中拥有接近原生的导航体验。本文涉及到了 Web、Android、iOS 三端的技术,综合性较强,有些地方可能有疏漏或不足,也欢迎大家批评指正。



(全文完)