在移动端使用vue-router和keep-alive的方法示例

 更新时间:2018年12月02日 09:10:38   作者:前端全栈开发学习  
这篇文章主要介绍了在移动端使用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
<div id="app">
 <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 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-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 方法来添加路由改变后的回调:

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>

// js
const 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>

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • vue2.0使用Sortable.js实现的拖拽功能示例

    vue2.0使用Sortable.js实现的拖拽功能示例

    本篇文章主要介绍了vue2.0使用Sortable.js实现的拖拽功能示例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下。
    2017-02-02
  • vue获取input输入值的问题解决办法

    vue获取input输入值的问题解决办法

    这篇文章主要介绍了vue获取input输入值的问题解决办法的相关资料,希望通过本文能帮助到大家,让大家掌握这样的问题,需要的朋友可以参考下
    2017-10-10
  • vue项目支付功能代码详解

    vue项目支付功能代码详解

    这篇文章主要介绍了vue项目支付功能,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-01-01
  • element中el-cascader级联选择器只有最后一级可以多选

    element中el-cascader级联选择器只有最后一级可以多选

    本文主要介绍了element中el-cascader级联选择器只有最后一级可以多选,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-01-01
  • Vue中的v-for列表循环示例详解

    Vue中的v-for列表循环示例详解

    循环使用v-for指令,v-for指令需要以site in sites形式的特殊语法,sites是源数据数组并且site是数组元素迭代的别名,下面这篇文章主要给大家介绍了关于Vue中v-for列表循环的相关资料,需要的朋友可以参考下
    2022-11-11
  • Vue.js中使用${}实现变量和字符串的拼接方式

    Vue.js中使用${}实现变量和字符串的拼接方式

    这篇文章主要介绍了Vue.js中使用${}实现变量和字符串的拼接方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-07-07
  • 详解Vue 开发模式下跨域问题

    详解Vue 开发模式下跨域问题

    本篇文章主要介绍了Vue 开发模式下跨域问题,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • vue日常开发基础Axios网络库封装

    vue日常开发基础Axios网络库封装

    这篇文章主要为大家介绍了vue日常开发基础Axios网络库封装示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • element中一个单选框radio时的选中和取消代码详解

    element中一个单选框radio时的选中和取消代码详解

    这篇文章主要给大家介绍了关于element中一个单选框radio时的选中和取消的相关资料,文中通过图文以及代码示例介绍的非常详细,对大家的学习或者工作具有一定的参考价值,需要的朋友可以参考下
    2023-09-09
  • Vue之Pinia状态管理

    Vue之Pinia状态管理

    这篇文章主要介绍了Vue中Pinia状态管理,Pinia开始于大概2019年,其目的是设计一个拥有 组合式 API 的 Vue 状态管理库,Pinia本质上依然是一个状态管理库,用于跨组件、页面进行状态共享,感兴趣的同学可以参考阅读
    2023-04-04

最新评论