在移动端使用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 相关文章推荐
jquery动画2.元素坐标动画效果(创建一个图片走廊)
Aug 24 Javascript
ajax异步刷新实现更新数据库
Dec 03 Javascript
jQuery中DOM树操作之使用反向插入方法实例分析
Jan 23 Javascript
jQuery使用$.ajax进行异步刷新的方法(附demo下载)
Dec 04 Javascript
CSS中position属性之fixed实现div居中
Dec 14 Javascript
JavaScript SHA-256加密算法详细代码
Oct 06 Javascript
基于JavaScript实现Tab选项卡切换效果
Nov 24 Javascript
根据Bootstrap Paginator改写的js分页插件
Dec 25 Javascript
node.JS md5加密中文与php结果不一致的解决方法
May 05 Javascript
D3.js进阶系列之CSV表格文件的读取详解
Jun 06 Javascript
Vue前端开发规范整理(推荐)
Apr 23 Javascript
Js跳出两级循环方法代码实例
Sep 22 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
php下尝试使用GraphicsMagick的缩略图功能
2011/01/01 PHP
mysql总结之explain
2012/02/27 PHP
php解析json数据实例
2014/08/19 PHP
PHP中soap的用法实例
2014/10/24 PHP
php序列化函数serialize() 和 unserialize() 与原生函数对比
2015/05/08 PHP
php提取身份证号码中的生日日期以及验证是否为成年人的函数
2015/09/29 PHP
thinkPHP中多维数组的遍历方法
2016/01/09 PHP
thinkPHP3.2使用RBAC实现权限管理的实现
2019/08/27 PHP
javascript 的Document属性和方法集合
2010/01/25 Javascript
基于jquery的二级联动菜单实现代码
2011/04/25 Javascript
JavaScript中的null和undefined解析
2012/04/14 Javascript
浅谈 jQuery 事件源码定位问题
2014/06/18 Javascript
JS仿Windows开机启动Loading进度条的方法
2015/02/26 Javascript
js实现鼠标经过表格行变色的方法
2015/05/12 Javascript
jQuery实现弹出窗口中切换登录与注册表单
2015/06/05 Javascript
Vuejs第八篇之Vuejs组件的定义实例解析
2016/09/05 Javascript
让你彻底掌握es6 Promise的八段代码
2017/07/26 Javascript
JQuery实现table中tr上移下移的示例(超简单)
2018/01/08 jQuery
详解vue2.0+vue-video-player实现hls播放全过程
2018/03/02 Javascript
vue技术分享之你可能不知道的7个秘密
2018/04/09 Javascript
使用RN Animated做一个“添加购物车”动画的方法
2018/09/12 Javascript
Vue Echarts实现可视化世界地图代码实例
2019/05/07 Javascript
vue canvas绘制矩形并解决由clearRec带来的闪屏问题
2019/09/02 Javascript
解决LayUI加上form.render()下拉框和单选以及复选框不出来的问题
2019/09/27 Javascript
在Webpack中用url-loader处理图片和字体的问题
2020/04/28 Javascript
解决ant Design中Select设置initialValue时的大坑
2020/10/29 Javascript
nestjs中异常过滤器Exceptionfilter的具体使用
2021/02/07 Javascript
python实现二维码扫码自动登录淘宝
2016/12/27 Python
Python 实现两个服务器之间文件的上传方法
2019/02/13 Python
python实现网站用户名密码自动登录功能
2019/08/09 Python
python操作toml文件的示例代码
2020/11/27 Python
香港化妆品经销商:我的公主
2016/08/05 全球购物
2014年人事行政工作总结
2014/12/03 职场文书
小学班主任教育随笔
2015/08/15 职场文书
tensorboard 可视化之localhost:6006不显示的解决方案
2021/05/22 Python
Python数据可视化之用Matplotlib绘制常用图形
2021/06/03 Python