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

(编辑:jimmy 日期: 2024/11/19 浏览:2)

对于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: "htmlcode">
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>

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

一句话新闻
一文看懂荣耀MagicBook Pro 16
荣耀猎人回归!七大亮点看懂不只是轻薄本,更是游戏本的MagicBook Pro 16.
人们对于笔记本电脑有一个固有印象:要么轻薄但性能一般,要么性能强劲但笨重臃肿。然而,今年荣耀新推出的MagicBook Pro 16刷新了人们的认知——发布会上,荣耀宣布猎人游戏本正式回归,称其继承了荣耀 HUNTER 基因,并自信地为其打出“轻薄本,更是游戏本”的口号。
众所周知,寻求轻薄本的用户普遍更看重便携性、外观造型、静谧性和打字办公等用机体验,而寻求游戏本的用户则普遍更看重硬件配置、性能释放等硬核指标。把两个看似难以相干的产品融合到一起,我们不禁对它产生了强烈的好奇:作为代表荣耀猎人游戏本的跨界新物种,它究竟做了哪些平衡以兼顾不同人群的各类需求呢?