对于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 <div> <keep-alive> <router-view></router-view> </keep-alive> </div>
// js const Index = { name: 'Index', template: `<div> 首页 <div> <router-link :to="{ name: 'List' }">Go to List</router-link> </div> </div>`, mounted() { console.warn('Main', 'mounted'); }, }; const List = { name: 'List', template: `<div> <div>列表页</div> <div> <div v-for="item in list" :key="item.id"> <router-link :to="{ name: 'Detail', params: { id: item.id } }"> {{item.name}} </router-link> </div> </div> <div> <div v-for="item in list" :key="item.id"> <router-link :to="{ name: 'Detail', params: { id: item.id } }"> {{item.name}} </router-link> </div> </div> </div>`, data() { return { list: new Array(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'); }, }; const Detail = { name: 'Detail', template: `<div> 详情页 <div> {{$route.params.id}} </div> </div>`, mounted() { console.warn('Detail', 'mounted'); }, }; const routes = [ { path: '', name: 'Main', component: Index }, { path: '/list', name: 'List', component: List }, { path: '/detail/:id', name: 'Detail', component: Detail }, ]; const router = new VueRouter({ routes, }); const app = new Vue({ router, }).$mount('#app');
当我们第一次从首页进入列表页时, mounted 和 activated 将被先后触发,而在此后无论是进入详情页再回退,或是回退到首页再进入列表页,都只会触发 deactivated 生命周期。
keep-alive
includes
keep-alive有一个 includes 选项,这个选项可以接受一个数组,并通过这个数组来决定组件的保活状态:
// keep-alive render () { const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { const name: ?string = getComponentName(componentOptions) const { include, exclude } = this if ( (include && (!name || !matches(include, name))) || (exclude && name && matches(exclude, name)) ) { return vnode } const { cache, keys } = this const key: ?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 } return vnode || (slot && slot[0]) }
这里我注意到,可以动态的修改这个数组,来使得本来处于保活状态的组件/页面失活。
afterEach
那我们可以在什么时候去维护/修改includes数组呢?vue-router提供了 afterEach 方法来添加路由改变后的回调: