微客导航 »
文章资讯 »
在移动端使用vue-router和keep-alive的方法示例
在移动端使用vue-router和keep-alive的方法示例
对于web开发和移动端开发,两者在路由上的处理是不同的。对于移动端来说,页面的路由是相当于栈的结构的。vue-router与keep-alive提供的路由体验与移动端是有一定差别的,因此常常开发微信公众号的我想通过一些尝试来将两者的体验拉近一些。
目标
问题
首先一个问题是keep-alive的行为。我们可以通过keep-alive来保存页面状态,但这样的行为对于类似于APP的体验是有些奇怪的。例如我们的应用有首页、列表页、详情页3个页面,当我们从列表页进入详情页再返回,此时列表页应当是keep-alive的。而当我们从列表页返回首页,再次进入列表页,此时的列表页应当在退出时销毁,并在重新进入时再生成才比较符合习惯。
第二个问题是滚动位置。vue-router提供了scrollBehavior来帮助维护滚动位置,但这一工具只能将页面作为滚动载体来处理。但我在实际开发中,喜欢使用flex来布局页面,滚动列表的载体常常是某个元素而非页面本身。
使用环境
对于代码能正确运行的环境,这里严格假定为微信(或是APP中内嵌的web页面),而非通过普通浏览器访问,即:用户无法通过直接输入url来跳转路由。在这样的前提下,路由的跳转是代码可控的,即对应于vue-router的push、replace等方法,而唯一无法干预的是浏览器的回退行为。在这样的前提下,我们可以假定,任何没有通过vue-router触发的路由跳转,是回退1个记录的回退行为。
改造前
这里我列出改造前的代码,是一个非常简单的demo,就不详细说了(这里列表页有两个列表,是为了展示改造后的滚动位置维护):
//css
*{
margin:0;
padding:0;
box-sizing:border-box;
}
html,body{
height:100%;
}
#app{
height:100%;
}
//html
//js
constIndex={
name:'Index',
template:
``,
mounted(){
console.warn('Main','mounted');
},
};
constList={
name:'List',
template:
`
列表页
{{item.name}}
{{item.name}}
`,
data(){
return{
list:newArray(10).fill(1).map((_,index)=>{
return{id:index+1,name:`item${index+1}`};
}),
};
},
mounted(){
console.warn('List','mounted');
},
activated(){
console.warn('List','activated');
},
deactivated(){
console.warn('List','deactivated');
},
};
constDetail={
name:'Detail',
template:
``,
mounted(){
console.warn('Detail','mounted');
},
};
constroutes=[
{path:'',name:'Main',component:Index},
{path:'/list',name:'List',component:List},
{path:'/detail/:id',name:'Detail',component:Detail},
];
constrouter=newVueRouter({
routes,
});
constapp=newVue({
router,
}).$mount('#app');
当我们第一次从首页进入列表页时,mounted和activated将被先后触发,而在此后无论是进入详情页再回退,或是回退到首页再进入列表页,都只会触发deactivated生命周期。
keep-alive
includes
keep-alive有一个includes选项,这个选项可以接受一个数组,并通过这个数组来决定组件的保活状态:
//keep-alive
render(){
constslot=this.$slots.default
constvnode:VNode=getFirstComponentChild(slot)
constcomponentOptions:?VNodeComponentOptions=vnode&&vnode.componentOptions
if(componentOptions){
constname:?string=getComponentName(componentOptions)
const{include,exclude}=this
if(
(include&&(!name||!matches(include,name)))||
(exclude&&name&&matches(exclude,name))
){
returnvnode
}
const{cache,keys}=this
constkey:?string=vnode.key==null
?componentOptions.Ctor.cid+(componentOptions.tag?`::${componentOptions.tag}`:'')
:vnode.key
if(cache[key]){
vnode.componentInstance=cache[key].componentInstance
remove(keys,key)
keys.push(key)
}else{
cache[key]=vnode
keys.push(key)
if(this.max&&keys.length>parseInt(this.max)){
pruneCacheEntry(cache,keys[0],keys,this._vnode)
}
}
vnode.data.keepAlive=true
}
returnvnode||(slot&&slot[0])
}
这里我注意到,可以动态的修改这个数组,来使得本来处于保活状态的组件/页面失活。
afterEach
那我们可以在什么时候去维护/修改includes数组呢?vue-router提供了afterEach方法来添加路由改变后的回调:
updateRoute(route:Route){
constprev=this.current
this.current=route
this.cb&&this.cb(route)
this.router.afterHooks.forEach(hook=>{
hook&&hook(route,prev)
})
}
在这里虽然afterHooks的执行是晚于路由的设置的,但组件的render是在nextTick中执行的,也就是说,在keep-alive的render方法判断是否应当从缓存中获取组件时,组件的保活状态已经被我们修改了。
劫持router.push
这里我们将劫持router的push方法:
letdir=1;
constincludes=[];
constrouterPush=router.push;
router.push=functionpush(...args){
dir=1;
routerPush.apply(router,args);
};
router.afterEach((to,from)=>{
if(dir===1){
includes.push(to.name);
}elseif(dir===-1){
includes.pop();
}
dir=-1;
});
我们将router.push(当然这里需要劫持的方法不止是push,在此仅用push作为示例)和浏览器的回退行为用不同的dir标记,并根据这个值来维护includes数组。
然后,将includes传递给keep-alive组件:
//html
//js
constapp=newVue({
router,
data(){
return{
includes,
};
},
}).$mount('#app');
维护滚动
接下来,我们将编写一个keep-position指令(directive):
Vue.directive('keep-position',{
bind(el,{value}){
constparent=positions[positions.length-1];
constobj={
x:0,
y:0,
};
constkey=value;
parent[key]=obj;
obj.el=el;
obj.handler=function({currentTarget}){
obj.x=currentTarget.scrollLeft;
obj.y=currentTarget.scrollTop;
};
el.addEventListener('scroll',obj.handler);
},
});
并对router进行修改,来维护position数组:
constpositions=[];
router.afterEach((to,from)=>{
if(dir===1){
includes.push(to.name);
positions.push({});
}
...
});
起初我想通过指令来移除事件侦听(unbind)以及恢复滚动位置,但发现使用unbind并不方便,更重要的是指令的几个生命周期在路由跳转到保活的页面时都不会触发。
因此这里我还是使用afterEach来处理路由维护,这样在支持回退多步的时候也比较容易去扩展:
router.afterEach((to,from)=>{
if(dir===1){
includes.push(to.name);
positions.push({});
}elseif(dir===-1){
includes.pop();
unkeepPosition(positions.pop({}));
restorePosition();
}
dir=-1;
});
constrestorePosition=function(){
Vue.nextTick(()=>{
constparent=positions[positions.length-1];
for(letkeyinparent){
const{el,x,y}=parent[key];
el.scrollLeft=x;
el.scrollTop=y;
}
});
};
constunkeepPosition=function(parent){
for(letkeyinparent){
constobj=parent[key];
obj.el.removeEventListener('scroll',obj.handler);
}
};
最后,我们分别给我们的列表加上我们的指令就可以了:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。