问题描述
在项目开发中使用vue-router,经常会遇到使用Modal弹窗的场景,然后点击后退不是Modal关闭,而是页面后退的问题。
比如在移动端,一个画面业内弹出一个Modal,为了关闭Modal,我们习惯性的会按手机回退键,但这时候会发现页面返回到了上一个路由。
解决方案
为了解决这个问题,我们可以这么设计:
在页面中埋一个router-view
,即添加一个临时用的子路由。子路由呈现空的状态,所以我们看不到这个临时路由的任何元素。
下面是路由配置
我们点击显示modal的时,让路由进入子路由。
this.$router.push({name: 'child-place-holder'})
然后在Modal点击关闭按钮的时候,同时调用路由后退,用来实现关闭modal,且关闭子路由,以此来保证路由的正确性了。
this.$router.back();
通过这个解决方案我们解决了:
- 点击后退按钮,会关闭子路由画面,不会关闭Modal所在的画面。
- 点击Modal关闭按钮,正确关闭了Modal和保证了路由的正确。
新的问题
那么问题来了,我们点击了Modal,把Modal显示出来,并进入了子路由,我们按后退键,关闭了子路由,但是Modal还没关掉,这和我们的预期不符。
为了解决这个问题,我先是尝试在vue-router的beforeRouteEnter方法中关闭Modal。
{
data: {
modal_visible: true
},
beforeRouteEnter(to, from, next){
...
this.modal_visble = false
}
}
但是,我发现,关闭子路由并不会触发beforeRouteEnter事件。 所以这个方案不行。
再次解决问题
通过研究发现,我们发现,通过监控$router
,关闭子路由时会触发。我们以这个为出发点寻找解决方案。
export default {
watch: {
$route(to, from) {
},
},
}
$router会有两个参数: to和from。 因为这段代码是写在Modal所在画面中,所以,to肯定是指Modal所在的路由,from则是子路由。
路由每次变化,上面的代码都会执行,比如从Modal所在的父级页面进入Modal页面,上面代码也会执行。这样的话,我们需要判断的是,这变化是不是从子路由转过来的。
to和from两个路由的信息可以说是非常贫瘠了,不太够用,为了判断路由跳转是否来自子路由,我们需要利用路由配置,好在我们可以通过下面的代码来获取路由配置信息。
export default {
watch: {
$route(to, from) {
},
},
computed: {
// 路由配置
routes() {
return this.$router.options.routes;
},
},
};
第一步,通过遍历路由配置,找到当前路由的配置。
export default {
watch: {
$route(to, from) {
// 找到当前的路由配置
const current_route_option = this.find_router_by_name(this.$route.name);
},
},
computed: {
// 路由配置
routes() {
return this.$router.options.routes;
},
},
methods: {
// 根据路由名称查找路由
find_router_by_name(name, routes) {
routes = routes || this.routes || [];
for (var index = 0; index < routes.length; index++) {
const route = routes[index];
if (route.name === name) return route;
if (route.children && route.children.length) {
const findResult = this.find_router_by_name(name, route.children);
if (findResult) return findResult;
}
}
},
},
};
第二步,判断from路由是否是当前路由的子路由
根据from的name,在当前路由的children数组中尝试找到相等的name。如果找到,则是来自子路由,否则不是。
export default {
watch: {
$route(to, from) {
// 找到当前的路由配置
const current_route_option = this.find_router_by_name(this.$route.name);
const is_from_children = this.is_from_children(from, current_route_option);
},
},
computed: {
// 路由配置
routes() {
return this.$router.options.routes;
},
},
methods: {
// 根据路由名称查找路由
find_router_by_name(name, routes) {
routes = routes || this.routes || [];
for (var index = 0; index < routes.length; index++) {
const route = routes[index];
if (route.name === name) return route;
if (route.children && route.children.length) {
const findResult = this.find_router_by_name(name, route.children);
if (findResult) return findResult;
}
}
},
// 判断是否来自(亲)子路由的转过来的
is_from_children(from_route, current_route_options) {
if (!from_route) return false;
if (!current_route_options.children || !current_route_options.children.length) return false;
for (let index = 0; index < current_route_options.children.length; index++) {
const child = current_route_options.children[index];
console.log(child.name, from_route.name);
if (child.name === from_route.name) return true;
}
return false;
},
},
};
第三步,如果是来自子路由,调用指定方法
通过上面的步骤,我们可以判断,路由跳转是否来自子路由,接下来我们要做的就是,如果来自子路由,我们同意执行指定的方法back_from_children
,并把form的路由名称当做参数传入。
export default {
watch: {
$route(to, from) {
// 找到当前的路由配置
const current_route_option = this.find_router_by_name(this.$route.name);
const is_from_children = this.is_from_children(from, current_route_option);
if (is_from_children) this.back_from_children(from.name);
},
},
computed: {
// 路由配置
routes() {
return this.$router.options.routes;
},
},
methods: {
// 当从子路由进来的时候,会调用这个方法
// from_route_name子路由名字
back_from_children(from_route_name) {
// 重写这个方法
},
// 根据路由名称查找路由
find_router_by_name(name, routes) {
routes = routes || this.routes || [];
for (var index = 0; index < routes.length; index++) {
const route = routes[index];
if (route.name === name) return route;
if (route.children && route.children.length) {
const findResult = this.find_router_by_name(name, route.children);
if (findResult) return findResult;
}
}
},
// 判断是否来自(亲)子路由的转过来的
is_from_children(from_route, current_route_options) {
if (!from_route) return false;
if (!current_route_options.children || !current_route_options.children.length) return false;
for (let index = 0; index < current_route_options.children.length; index++) {
const child = current_route_options.children[index];
console.log(child.name, from_route.name);
if (child.name === from_route.name) return true;
}
return false;
},
},
};
最后的使用
上面的代码,我们保存为RouterMixin.js
。
然后在我们的Modal界面引入使用:
然后我们重写我们的back_from_children
方法:
里面的逻辑就很清晰了,如果来自child-place-holder
路由,那么就可以关掉modal了,其他地方来的,则不做关闭操作。
gif图片效果预览
动图中注意url地址栏的变化
还没解决的问题
如果直接访问临时路由的url地址,要点两次后退才能退出Modal所在画面。
总结
这个解决方案,利用了vue的mixin机制。
在Modal所在的画面,路由变化时,都要遍历路由配置这棵树,对大一点规模的应用可能会有写性能影响。
使用时,耦合度相对较高,要记住自己的临时路由的名字,记住定义的方法名进行重写等。
写好mixin文件后,工作量也还能接受。
考虑不周的地方,欢迎大家指正。