Vue前进刷新后退不刷新,缓存链实现

这是什么?先看实现效果源码在这html

场景

首先这个交互我也叫不出专业的名字,大体场景是这样的:vue

  1. 如今有一个小商城,从首页(Index)能够进入商品列表页面(List),这个List是一个无限列表,如今用户往下翻,看到某一个商品彷佛比较喜欢,因而点击进入到商品详情页面(Info),看完后以为比较满意。恩,可是货比三家嘛,因而用户返回商品列表页面,准备继续往下浏览,这时若是你将用户以前的列表刷新了会怎么样?要是我是很是不爽的,这意味着我须要从新操做一波,定位到刚刚中意的商品后,继续往下货比三家。
SomePage -> List: List从新加载
List -> Info -> List: List使用缓存
复制代码
  1. 当用户肯定好商品后,决定购买了,因而进入订单页(Form),订单页默认为用户输入了历史收货地址,用户彷佛有想要提醒发货的商家,因而填写了备注,而后检查下订单有误否,发现地址能够再修改一下,因而点击收货地址进入到收货地址列表页(Address),选好后咱们自动为用户返回到订单页并更新收货地址,若是此时备注没有了会怎么样?
SomePage -> Form: Form从新加载
Form -> Address -> Form: Form使用缓存
复制代码

以上是简单举个例子,实际上这相似我在上家公司作移动端(SPA)时碰到的问题,只不过咱们是商家列表,但交互相似,刚开始我是直接用路由方式解决的,相似将Info做为List的子页面,将Address做为Form的子页面。实现是实现了,但太麻烦了,维护麻烦啊😂,而且对组内其它伙伴不太友好。直到某一次我决定对其进行重构,重构前也没有什么好思路,而后Google了一番茅塞顿开:利用keep-alive的include实现,当时知道了思路,我就没往下看了,本身动手丰衣足食😁。node

统一一下词汇,下文中说的类列表页就是List,类详情页就是Infowebpack

踩坑记

刚开始我还踩了一个坑,主要是Vue的keep-alive当时不多接触,都是Copy......git

首先按照以前使用(copy)keep-alive的套路是像这样的:github

<keep-alive>
  <router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"/>
复制代码

而后我将它改形成这样web

<keep-alive>
  <router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<keep-alive :include="include">
  <router-view v-if="!$route.meta.keepAlive"/>
</keep-alive>
复制代码

就是控制这个include,具体的作法是,先在类列表页的路由选项meta中增长一个cacheTo字段,表示对指定页面缓存,值为一个数组,而后:vue-cli

router.beforeEach((to, from, next) => {
  if (isPageLikeList(from)) {
    // 若是from是类列表页面
    const fromCacheTo = from.meta.cacheTo
    if (fromCacheTo.indexOf(to.name) > -1) {
      // 若是to是类详情页面
      // 将from对应的组件设置缓存
    } else {
      // 移除from缓存
    }
  }
  // ...
})
复制代码

但想的很美,写完发现Bug比较严重,类列表页缓存老失效,为何呢?这就要从keep-alive的include机制提及了,因而看了一下keep-alive的源码,源码中有这么一段npm

// ...
  mounted () {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },
  // ...
复制代码

而后继续往下找侦听include的回调逻辑数组

function pruneCache (keepAliveInstance: any, filter: Function) {
  // 对于include,filter逻辑为:include包含组件名时返回true,不然返回false
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      // 若组件不在include范围中
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

function pruneCacheEntry ( cache: VNodeCache, key: string, keys: Array<string>, current?: VNode ) {
  const cached = cache[key]
  // 销毁组件
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  // 清除组件的缓存
  cache[key] = null
  remove(keys, key)
}
复制代码

大体流程是,include变化后(不论是新增了仍是减小了),若是组件不在include中而且当前的cache[组件name]是有缓存的,就执行pruneCacheEntry,销毁组件并清除组件的缓存。

那么这对我上述的逻辑有什么影响呢?按照我以前的流程,动态控制include,这样作删除组件缓存是没有问题,可是新增就会有问题了,由于新增操做发生在从类列表页离开进入到类详情页以前,此时类列表页已经存在了,而后源码中侦听include的流程中也只有清除缓存的操做。

知道了问题,如何解决就很清晰了

实现

分析

咱们能够将对include的操做放到类列表页进入前(router.beforeEach),这样就能有效缓存类列表页了,其实这东西就是这么简单,先无论三七二十一,只要是类列表页,进入以前都将其缓存,离开时判断新路由是不是该类列表页指定的类详情页,若是不是就清除缓存。那么在实现以前,我们先将场景再细化一下,以前简直比玩具还玩具......

SomePage -> List: List从新加载

// 第一种
List -> Info -> List: List使用缓存

// 第二种
List -> Info -> SomePage -> List: List从新加载

// 第三种,不限层级
List -> Info -> OtherList -> OtherInfo -> ... -> List: List、Info、OtherList...使用缓存

其中第三种,从OtherInfo返回OtherList时,OtherInfo应该是要被清除缓存的,依次类推。也就是说,返回时不保留以前的缓存
复制代码

如今,咱们根据例举的场景将大体逻辑肯定一下。这里有一个点是很明确的,那就是: 咱们只须要关心路由切换时的tofrom,从这2个路由对象着手去解决。那么这里能够例举出4中状况:

  1. tofrom都不是类列表页
  2. tofrom都是类列表页
  3. to是类列表页
  4. from是类列表页

如今根据这4种状况细化一下

  1. 第一种状况,tofrom都不是类列表页
    • 不须要缓存,而且能够将以前的缓存所有清除
  2. 第二种状况,tofrom都是类列表页
    • to不在from的配置中,清空缓存,同时要新增to缓存;
    • 不然,保留from的缓存,新增to缓存
  3. 第三种状况,to是类列表页
    • from不在to的配置中,清空缓存,新增to缓存
    • 不然,无需任何处理
  4. 第四种
    • to不在from的配置中,清空缓存

如何判断是不是类列表页?

[
  {
    path: '/list',
    name: 'list',
    meta: {
      cacheTo: ['info']
    }
    // ...
  },
  {
    path: '/info',
    name: 'info',
    // ...
  }
]
复制代码

如上,在路由中维护一个字段如cacheTo,若是配置了组件名,就认为是类列表页面

具体实现

逻辑理清楚咯,接下来具体实现,咱们将它作得通用一点(可拔插),而且尽可能保证对原项目有较小的侵入性,我将它命名为VKeepAliveChain(缓存链)

首先,若是像踩坑记那样维护include,不太具有可拔插的特性,我还得去搞一个store,那么这个include可使用Vue.observable处理

// VKeepAliveChain.js
const state = Vue.observable({
  caches: []
})
const clearCache = () => {
  if (state.caches.length > 0) {
    state.caches = []
  }
}
const addCache = name => state.caches.push(name)
复制代码

为了不像踩坑记那样直接使用<keep-alive :include="include">,咱们实现一个函数式组件来解决

// VKeepAliveChain.js
export const VKeepAliveChain = {
  install (Vue, options = { key: 'cacheTo' }) {
    const { key } = options

    // 支持一下自定义key
    if (key) {
      cacheKey = key
    }
    
    // 直接透传children,因此会像keep-alive同样,只拿第一个组件节点
    const component = {
      name: 'VKeepAliveChain',
      functional: true,
      render (h, { children }) {
        return h(
          'keep-alive',
          { props: { include: state.caches } },
          children
        )
      }
    }

    Vue.component('VKeepAliveChain', component)
  }
}
复制代码

如今咱们来实现缓存控制的主要逻辑,因为要利用router.beforeEach,约定了尽可能小的侵入性,这里能够merge一下

// VKeepAliveChain.js
const defaultHook = (to, from, next) => next()
export const mergeBeforeEachHook = (hook = defaultHook) => {
  return (to, from, next) => {
    // 缓存控制逻辑
    // 1. 都不是类列表页
    // 清空缓存
    // 2. 都是类列表页
    // 若`to`不在`from`的配置中,清空缓存,同时要新增`to`缓存
    // 保留`from`的缓存,新增`to`缓存
    // 3. 新路由是类列表页
    // 若`from`不在`to`的配置中,清空缓存,新增`to`缓存
    // 不然,无需处理
    // 4. 旧路由是类列表页
    // 若`to`不在`from`的配置中,清空缓存

    const toName = to.name
    const toCacheTo = (to.meta || {})[cacheKey]
    const isToPageLikeList = toCacheTo && toCacheTo.length > 0
    const fromName = from.name
    const fromCacheTo = (from.meta || {})[cacheKey]
    const isFromPageLikeList = fromCacheTo && fromCacheTo.length > 0

    if (!isToPageLikeList && !isFromPageLikeList) {
      clearCache()
    } else if (isToPageLikeList && isFromPageLikeList) {
      if (fromCacheTo.indexOf(toName) === -1) {
        clearCache()
      }
      addCache(toName)
    } else if (isToPageLikeList) {
      if (toCacheTo.indexOf(fromName) === -1) {
        clearCache()
        addCache(toName)
      }
    } else if (isFromPageLikeList) {
      if (fromCacheTo.indexOf(toName) === -1) {
        clearCache()
      }
    }
    return hook(to, from, next)
  }
}
复制代码

那么整个缓存链的功能就实现了,同时我将它发不到了npm v-keep-alive-chain上。

食用方式

首先引入并注册它

// main.js
import { mergeBeforeEachHook, VKeepAliveChain } from 'v-keep-alive-chain'

Vue.use(VKeepAliveChain, {
  key: 'cacheTo' // 可选的 默认为cacheTo
})

// 若是你没有注册过beforeEach
router.beforeEach(mergeBeforeEachHook())

// 若是有注册beforeEach
router.beforeEach(mergeBeforeEachHook((to, from, next) => {
  next()
}))
复制代码

而后在App.vue(视你的状况而定)中

<keep-alive>
  <router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<VKeepAliveChain>
  <router-view v-if="!$route.meta.keepAlive"/>
</VKeepAliveChain>
复制代码

接着在router中配置你的需求

[
  {
    path: '/list',
    name: 'list',
    meta: {
      cacheTo: ['info']
    }
    // ...
  },
  {
    path: '/info',
    name: 'info',
    // ...
  }
]
复制代码

而后就能愉快的玩耍了

注意事项

  1. 路由配置不能少了name属性,而且这个name须要和组件name同样
  2. cacheTo优先级小于keepAlive,因此,处理这种需求的页面不要设置keepAlive
  3. 能够设置2个页面以前仅在相互切换时缓存,不过我还没发现可用的场景
  4. webpack、vue-cli通常我是拿来即用的,打包的脚手架是vue-cli4.0,不多深刻研究这些东西,而后我看了下发布到npm包的源码,发现有不少无用的polyfill被打进去了,致使Gzip的包都有4Kb多,暂时尚未找到解决方法,知晓的朋友麻烦告知一下啊😂

文章写的比较快,若有什么错误,能够下方留言咯

朋友,看到这里,但愿文章对你有启发,本人很是欢迎技术交流,若是你以为文章对你有用,还请给老弟一个👍,平时我是不在意这些个的,但因为我立刻要投递简历了,须要点东西撑门面,没得办法,履历太差了,谢谢你,愿生活带给你美好!!!

相关文章
相关标签/搜索