如何设计一个移动端支持返回键的 Vue 组件
在一些复杂的 Vue 组件中,组件需要根据不同的场景显示不同的视图,视图切换要支持返回键返回到上一个视图,如果使用 vue-rouer,组件就得和view一样挂在一个路径下,的确可以很好解决返回的问题,但是调用组件变成了跳转页面,如果是一个表单页面,那么跳转之前还得临时保存表单的数据,显然这个思路不符合组件的思想。最容易想到的就是 hashchange 或 pushState ,由于可能会和 vue-router 产生冲突,也不是很好的方案。
组件要支持返回,那么肯定需要在浏览历史记录上做变动,前端路由的基础上如何在做历史记录的变动呢?答案就是使用改变路由参数的方式。
驱动视图改变不再是直接给参数赋值,而是通过路由参数的改变。视图改变值也由监听 $route.query 的变化来获取,经过简单封装,代码如下。
<template>
<div>
<div class="page" v-if="page==='page1'">
当前page1<br><br>
<button @click="goPage('page2')">跳转到 page2</button>
</div>
<div class="page" v-else-if="page==='page2'">
当前page2<br><br>
<button @click="goPage('page3')">跳转到 page3</button>
</div>
<div class="page" v-else-if="page==='page3'">
当前page3<br><br>
<button @click="goPage('page4')">跳转到 page4</button>
</div>
<div class="page" v-else-if="page==='page4'">
当前page4<br><br>
</div>
<div class="page" v-else>
当前起始页<br><br>
<button @click="goPage('page1')">跳转到 page1</button>
</div>
</div>
</template>
<script>
export default {
watch: {
'$route.query': {
handler(newVal) {
if (newVal.__component_id__ === this.id) {
this.page = newVal.__component_page__.replace(this.id, '')
}
}
}
},
data() {
return {
id: Math.random().toString(16).slice(2),
page: ''
}
},
methods: {
goPage(page) {
const route = this.$route
this.$router.push({
path: route.path,
query: {
...route.query,
__component_id__: this.id,
__component_page__: this.id + page
}
})
}
}
}
</script>
<style lang="less">
.page {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
</style>
为何参数需要拼接id?
这个id主要是为了解决在某一个视图下刷新页面,无法区分是否为当前组件下的参数,id是组件创建时生成,刷新后,id会变化,进而不会触发内部的page变化。
浏览历史记录变化伴随着一个问题,那就是不断地前进后退,会导致历史记录不断增多,如果管理不好,就会出现无法一步退出到起始页面的问题。因此在这种方案下,需要自己做一个历史记录的管理。
goPage(page, replace) {
const route = this.$route
const index = this.history.indexOf(page)
if (index > -1) {
const len = this.history.length
this.history.splice(index + 1, len)
history.go(-(len - (index + 1)))
return
}
if (replace) {
this.history.splice(this.history.length - 1, 1, page)
} else {
this.history.push(page)
}
this.$router[replace ? 'replace' : 'push']({
path: route.path,
query: {
...route.query,
__component_id__: this.id,
__component_page__: this.id + page
}
})
}
每一次改变page,将page记录到history中,我这里做了一个处理,针对历史记录中存在相同的page,则采用返回机制,而不是跳转,这个可以解决用户连续返回时可以快速返回到起始页,否则由于历史记录一直累加,用户需要点击非常多次返回才能返回到起始页。
上面的代码只考虑了进入不同的page的情况,当用户点击返回键时,这时候需要把history中对应的page给删除掉,保证history的正确性。
watch: {
'$route.query': {
handler(newVal, oldVal) {
if (newVal.__component_id__ === this.id) {
this.page = newVal.__component_page__.replace(this.id, '')
const lastPage = oldVal.__component_page__.replace(this.id, '')
if (lastPage === this.history[this.history.length - 1]) {
this.history.pop()
}
} else {
this.page = ''
this.history = []
}
}
}
},
由于组件为了一套page的浏览记录,因此在任何组件的视图下返回到起始页就变得很简单。
goBack() {
history.go(-this.history.length)
this.history = []
}
最后总结一下整个组件的核心思路:
- 通过改变$route.query实现浏览器历史记录的变化,支持了返回键操作,监听$route.query的变化来改变当前视图值,进而显示不同的组件视图
- 维护一套视图变化的历史记录,保证用户可以快速推出组件,另外可以在任何视图做到一次性退出到起始页