移动端返回键的正确姿势:先关闭模态框,而不是退出页面

在移动端 Hybrid 或 H5 应用里,用户习惯了“点返回键 → 关闭弹出的模态框”,而不是直接退出页面。但在 Web 开发里,如果我们不处理,物理返回键/滑动返回往往会直接导致页面返回甚至退出,这就带来了糟糕的用户体验。

例如这个场景:用户打开了一个“审批表单”的 modal,但还没填完就顺手一滑触发了浏览器或系统的返回操作,结果往往是整个页面直接退出,表单内容丢失。这对用户来说非常反直觉。

理想的交互应该是:当 modal 打开时,先往浏览器历史栈压入一条记录;当用户点击返回键时,如果此时 modal 仍然打开,就优先关闭它,而只有在 modal 已经关闭的情况下,返回键才真正让页面后退。

history.pushState 方式

以 Naive UI 中 n-modal 为例

1
2
3
4
5
6
7
<n-modal v-model:show="showAgreeModal" 
@after-enter="onModalShow"
@after-leave="onModalHide">
<n-card title="同意申请">
<n-input v-model:value="advice" type="textarea" placeholder="审批意见" />
</n-card>
</n-modal>

在 Naive UI 中使用 n-modal 时,我们希望 modal 打开时往历史栈 pushState,关闭时再回收掉。可以借助组件自带的两个过渡事件:

@after-enter:modal 打开并完成过渡动画后触发,此时调用 history.pushState({ modal: true }, ‘’),确保返回键能感知到 modal 的存在。
@after-leave:modal 关闭并完成动画后触发,此时再调用 history.back() 然后做清理,保证历史栈不会多出冗余记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
mounted() {
window.addEventListener('popstate', this.onPopState)
},
beforeUnmount() {
window.removeEventListener('popstate', this.onPopState)
},
methods: {
onModalShow() {
// 打开 modal 时,往历史栈压一条记录
history.pushState({ __modal__: true }, '')
},
onModalHide() {
// modal 关闭时,退掉这条记录
history.back()
},
onPopState(event) {
if (event.state && event.state.__modal__) {
// 返回键触发时,如果 state 里有 __modal__,说明是 modal
this.showAgreeModal = false
} else {
// 否则才是真正的页面返回
this.$router.back()
}
}
}

手写模态框也是一样,只要在 打开时 pushState,关闭时 back,再配合 popstate 监听,就能做到拦截返回键逻辑。

其他方式