在移動端使用vue-router和keep-alive

對於web開發和移動端開發,二者在路由上的處理是不一樣的。對於移動端來講,頁面的路由是至關於棧的結構的。vue-router與keep-alive提供的路由體驗與移動端是有必定差異的,所以經常開發微信公衆號的我想經過一些嘗試來將二者的體驗拉近一些。css

目標

問題

首先一個問題是keep-alive的行爲。咱們能夠經過keep-alive來保存頁面狀態,但這樣的行爲對於相似於APP的體驗是有些奇怪的。例如咱們的應用有首頁、列表頁、詳情頁3個頁面,當咱們從列表頁進入詳情頁再返回,此時列表頁應當是keep-alive的。而當咱們從列表頁返回首頁,再次進入列表頁,此時的列表頁應當在退出時銷燬,並在從新進入時再生成才比較符合習慣。html

第二個問題是滾動位置。vue-router提供了scrollBehavior來幫助維護滾動位置,但這一工具只能將頁面做爲滾動載體來處理。但我在實際開發中,喜歡使用flex來佈局頁面,滾動列表的載體經常是某個元素而非頁面自己。vue

使用環境

對於代碼能正確運行的環境,這裏嚴格假定爲微信(或是APP中內嵌的web頁面),而非經過普通瀏覽器訪問,即:用戶沒法經過直接輸入url來跳轉路由。在這樣的前提下,路由的跳轉是代碼可控的,即對應於vue-router的push、replace等方法,而惟一沒法干預的是瀏覽器的回退行爲。在這樣的前提下,咱們能夠假定,任何沒有經過vue-router觸發的路由跳轉,是回退1個記錄的回退行爲。node

改造前

這裏我列出改造前的代碼,是一個很是簡單的demo,就不詳細說了(這裏列表頁有兩個列表,是爲了展現改造後的滾動位置維護):web

// 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');
複製代碼

當咱們第一次從首頁進入列表頁時,mountedactivated將被前後觸發,而在此後不管是進入詳情頁再回退,或是回退到首頁再進入列表頁,都只會觸發activateddeactivated生命週期。vue-router

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方法判斷是否應當從緩存中獲取組件時,組件的保活狀態已經被咱們修改了。bash

劫持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>
複製代碼
相關文章
相關標籤/搜索