在移动端使用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 相关文章推荐
用JS剩余字数计算的代码
Jul 03 Javascript
JS中数组Array的用法示例介绍
Feb 20 Javascript
jQuery对下拉框,单选框,多选框的操作
Feb 21 Javascript
JavaScript基础教程之alert弹出提示框实例
Oct 16 Javascript
基于JS实现翻书效果的页面切换样式
Feb 16 Javascript
极简主义法编写JavaScript类
Nov 02 Javascript
vue实现商城上货组件简易版
Nov 27 Javascript
vue 虚拟dom的patch源码分析
Mar 01 Javascript
微信小程序 授权登录详解(附完整源码)
Aug 23 Javascript
javascript中的相等操作符(==与===区别)
Dec 21 Javascript
vue数据更新UI不刷新显示的解决办法
Aug 06 Javascript
JS性能优化实现方法及优点进行
Aug 30 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
smarty半小时快速上手入门教程
2014/10/27 PHP
推荐几款用 Sublime Text 开发 Laravel 所用到的插件
2014/10/30 PHP
ThinkPHP开发框架函数详解:C方法
2015/08/14 PHP
php中分页及SqlHelper类用法实例
2017/01/12 PHP
php+webSoket实现聊天室示例代码(附源码)
2017/02/17 PHP
php创建多级目录与级联删除文件的方法示例
2019/09/12 PHP
javascript的对话框详解与参数
2007/03/08 Javascript
运算符&amp;&amp;的三个不同层次
2013/04/07 Javascript
js获取下拉列表框中的value和text的值示例代码
2014/01/11 Javascript
jquery 扑捉回车键事件代码
2014/04/24 Javascript
jquery通过扩展select控件实现支持enter或focus选择的方法
2015/11/19 Javascript
原生js页面滚动延迟加载图片
2015/12/20 Javascript
Vue 过渡(动画)transition组件案例详解
2017/01/22 Javascript
Vue.js 2.0 移动端拍照压缩图片预览及上传实例
2017/04/27 Javascript
jQuery实现radio第一次点击选中第二次点击取消功能
2017/05/15 jQuery
三分钟学会用ES7中的Async/Await进行异步编程
2018/06/14 Javascript
Three.js实现简单3D房间布局
2018/12/30 Javascript
微信小程序按钮点击跳转页面详解
2019/05/06 Javascript
JS去除字符串最后的逗号实例分析【四种方法】
2019/06/20 Javascript
vue瀑布流组件实现上拉加载更多
2020/03/10 Javascript
wxpython 学习笔记 第一天
2009/02/09 Python
用python代码做configure文件
2014/07/20 Python
python批量修改图片大小的方法
2018/07/24 Python
实例讲解python中的协程
2018/10/08 Python
python 使用值来排序一个字典的方法
2018/11/16 Python
解决yum对python依赖版本问题
2019/07/05 Python
python 进程间数据共享multiProcess.Manger实现解析
2019/09/23 Python
python爬取股票最新数据并用excel绘制树状图的示例
2021/03/01 Python
Darphin迪梵官网: 来自巴黎,植物和精油调制的护肤品牌
2016/10/11 全球购物
英国快时尚女装购物网站:PrettyLittleThing
2018/08/15 全球购物
Shopee越南:东南亚与台湾电商平台
2019/02/03 全球购物
中专自我鉴定范文
2013/10/16 职场文书
经济信息系毕业生自荐信范文
2014/03/15 职场文书
党小组推荐意见
2015/06/02 职场文书
pytorch中的model=model.to(device)使用说明
2021/05/24 Python
JS前端使用canvas实现扩展物体类和事件派发
2022/08/05 Javascript