在手机端使用vue-router和keep-alive

作者 : 开心源码 本文共5475个字,预计阅读时间需要14分钟 发布时间: 2022-05-12 共141人阅读

对于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 id="app">  <keep-alive>    <router-view></router-view>  </keep-alive></div>
// jsconst 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 style="display: flex;flex-direction: column;height: 100%;">    <div>列表页</div>    <div style="flex: 1;overflow: scroll;">      <div v-for="item in list" :key="item.id">        <router-link style="line-height: 100px;display:block;" :to="{ name: 'Detail', params: { id: item.id } }">          {{item.name}}        </router-link>      </div>    </div>    <div style="flex: 1;overflow: scroll;">      <div v-for="item in list" :key="item.id">        <router-link style="line-height: 100px;display:block;" :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-aliverender () {  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 方法来增加路由改变后的回调:

updateRoute (route: Route) {  const prev = 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方法:

let dir = 1;const includes = [];const routerPush = router.push;router.push = function push(...args) {  dir = 1;  routerPush.apply(router, args);};router.afterEach((to, from) => {  if (dir === 1) {    includes.push(to.name);  } else if (dir === -1) {    includes.pop();  }  dir = -1;});

我们将router.push(当然这里需要劫持的方法不止是push,在此仅用push作为示例)和浏览器的回退行为用不同的 dir 标记,并根据这个值来维护includes数组。

而后,将includes传递给keep-alive组件:

// html<div id="app">  <keep-alive :include="includes">    <router-view></router-view>  </keep-alive></div>// jsconst app = new Vue({  router,  data() {    return {      includes,    };  },}).$mount('#app');

维护滚动

接下来,我们将编写一个 keep-position 指令(directive):

Vue.directive('keep-position', {  bind(el, { value }) {    const parent = positions[positions.length - 1];    const obj = {      x: 0,      y: 0,    };    const key = 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数组:

const positions = [];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({});  } else if (dir === -1) {    includes.pop();    unkeepPosition(positions.pop({}));    restorePosition();  }  dir = -1;});const restorePosition = function () {  Vue.nextTick(() => {    const parent = positions[positions.length - 1];    for (let key in parent) {      const { el, x, y } = parent[key];      el.scrollLeft = x;      el.scrollTop = y;    }  });};const unkeepPosition = function (parent) {  for (let key in parent) {    const obj = parent[key];    obj.el.removeEventListener('scroll', obj.handler);  }};

最后,我们分别给我们的列表加上我们的指令即可以了:

<div style="flex: 1;overflow: scroll;" v-keep-position="'list1'">  <!--  --></div><div style="flex: 1;overflow: scroll;" v-keep-position="'list2'">  <!--  --></div>

本次给大家推荐一个免费的学习群,里面概括移动应用网站开发,css,html,webpack,vue node angular以及面试资源等。
对web开发技术感兴趣的同学,欢迎加入Q群:943129070,不论你是小白还是大牛我都欢迎,还有大牛整理的一套高效率学习路线和教程与您免费分享,同时每天升级视频资料。
最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 在手机端使用vue-router和keep-alive

发表回复