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


Posted in Javascript onDecember 02, 2018

对于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>

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

Javascript 相关文章推荐
枚举JavaScript对象的函数
Dec 22 Javascript
JavaScript将相对地址转换为绝对地址示例代码
Jul 19 Javascript
css结合js制作下拉菜单示例代码
Feb 27 Javascript
js调试工具console.log()方法查看js代码的执行情况
Aug 08 Javascript
JavaScript实现数字数组正序排列的方法
Apr 06 Javascript
TypeScript 学习笔记之基本类型
Jun 19 Javascript
jQuery使用contains过滤器实现精确匹配方法详解
Feb 25 Javascript
jquery请求servlet实现ajax异步请求的示例
Jun 03 jQuery
BootStrap实现文件上传并带有进度条效果
Sep 11 Javascript
在 webpack 中使用 ECharts的实例详解
Feb 05 Javascript
微信小程序实现banner图轮播效果
Jun 28 Javascript
vue+Element实现搜索关键字高亮功能
May 28 Javascript
Angular6 Filter实现页面搜索的示例代码
Dec 02 #Javascript
GOJS+VUE实现流程图效果
Dec 01 #Javascript
JavaScript实现简单轮播图效果
Dec 01 #Javascript
jQuery-ui插件sortable实现自由拖动排序
Dec 01 #jQuery
jquery拖拽自动排序插件使用方法详解
Jul 20 #jQuery
vue实现移动端悬浮窗效果
Dec 01 #Javascript
vue拖拽组件使用方法详解
Dec 01 #Javascript
You might like
Zerg建筑一览
2020/03/14 星际争霸
apache php模块整合操作指南
2012/11/16 PHP
PHP闭包实例解析
2014/09/08 PHP
laravel-admin 在列表页添加自定义按钮的例子
2019/09/30 PHP
解决Laravel自定义类引入和命名空间的问题
2019/10/15 PHP
jquery中获取id值方法小结
2013/09/22 Javascript
使用FlexiGrid实现Extjs表格效果方法分享
2014/12/16 Javascript
浅谈javascript中自定义模版
2015/01/29 Javascript
AngularJS实现textarea记录只能输入规定数量的字符并显示
2016/04/26 Javascript
js实现移动端微信页面禁止字体放大
2017/02/16 Javascript
基于es6三点运算符的使用方法(实例讲解)
2017/10/12 Javascript
Three.js基础学习教程
2017/11/16 Javascript
vue-froala-wysiwyg 富文本编辑器功能
2019/09/19 Javascript
微信小程序开发之获取用户手机号码(php接口解密)
2020/05/17 Javascript
微信小程序仿抖音视频之整屏上下切换功能的实现代码
2020/05/24 Javascript
JavaScript arguments.callee作用及替换方案详解
2020/09/02 Javascript
[03:36]2015国际邀请赛第二日现场精彩集锦
2015/08/06 DOTA
windows下python模拟鼠标点击和键盘输示例
2014/02/28 Python
讲解Python中for循环下的索引变量的作用域
2015/04/15 Python
python避免死锁方法实例分析
2015/06/04 Python
win7上python2.7连接mysql数据库的方法
2017/01/14 Python
python爬虫的一个常见简单js反爬详解
2019/07/09 Python
Django 缓存配置Redis使用详解
2019/07/23 Python
Python 转换文本编码实现解析
2019/08/27 Python
opencv实现简单人脸识别
2021/02/19 Python
解决python运行效率不高的问题
2020/07/20 Python
python将YUV420P文件转PNG图片格式的两种方法
2021/01/22 Python
CSS3实现滚动条动画效果代码分享
2016/08/03 HTML / CSS
一加手机美国官方网站:OnePlus美国
2019/09/19 全球购物
党的群众路线教育实践活动个人对照检查材料
2014/09/22 职场文书
论群众路线学习心得体会
2014/10/31 职场文书
《打电话》教学反思
2016/02/22 职场文书
手把手教你怎么用Python实现zip文件密码的破解
2021/05/27 Python
详细了解java监听器和过滤器
2021/07/09 Java/Android
SpringBoot2零基础到精通之异常处理与web原生组件注入
2022/03/22 Java/Android
Win11安装升级时提示“该电脑必须支持安全启动”
2022/04/19 数码科技