浅谈Vue页面级缓存解决方案feb-alive (下)

feb-alive

github地址
体验链接

Vue页面级缓存解决方案feb-alive (上)

在剖析feb-alive实现之前,希望大家对以下基本知识有一定的了解。

  • keep-alive实现原理
  • history api
  • vue渲染原理
  • vue虚拟dom原理

feb-alive与keep-alive差异性

1. 针对activated钩子差异性

keep-alive配合vue-router在动态路由切换的情况下不会触发activated钩子,因为切换的时候组件没有变化,所以只能通过beforeRouteUpdate钩子或者监听$route来实现数据更新,而feb-alive在动态路由切换时,依然会触发activated钩子,所以用户可以放心的将业务更新逻辑写在activated钩子,不必关心动态路由还是非动态路由的情况。

2. feb-alive是页面级缓存,而keep-alive是组件级别缓存

所以在上文中讲到的使用keep-alive存在的一些限制问题都能够得到有效的解决

实现原理

首先我们的目标很明确,需要开发的是一个页面级别的缓存插件,之前使用keep-alive遇到的诸多问题,归根结底是因为它是一个组件级别的缓存。那么我们就需要寻找每个页面的特征,用来存储我们需要存储的路由组件vnode,这里我们就需要思考什么可以作为每个页面的标记

两种方式:

  • 通过每个url的查询参数来存储key
  • 通过history.state来存储key

方案一:使用查询参数

优点:

可以兼容vue-router的hash模式

缺点:

每个页面的url后面都会带一个查询参数
每次页面跳转都需要重写url

方案二:使用history.state

优点:

无需附带额外的查询参数

缺点:

不支持hash模式

相比方案一明显的缺点,我更较倾向于方案二,舍弃hash模式的兼容性,换来整个插件更加好的用户体验效果。
接下来看下feb-alive的实现,feb-alive组件与上文的keep-alive一样都是抽象组件,结构基本一致,主要区别在于render函数的

实现

// feb-alive/src/components/feb-alive.js
render () {
  // 取到router-view的vnode
  const vnode = this.$slots.default ? this.$slots.default[0] : null
  const disableCache = this.$route.meta.disableCache
  // 如果不支持html5 history则不做缓存处理
  if (!supportHistoryState) {
    return vnode
  }
  // 尝试写入key
  if (!history.state || !history.state[keyName]) {
    const state = {
      [keyName]: genKey()
    }
    const path = getLocation()
    history.replaceState(state, null, path)
  }
  // 有些浏览器不支持往state中写入数据
  if (!history.state) {
    return vnode
  }
  // 指定不使用缓存
  if (disableCache) {
    return vnode
  }
  // 核心逻辑
  if (vnode) {
    const { cache, keys } = this
    const key = history.state[keyName]
    const { from, to } = this.$router.febRecord
    let parent = this.$parent
    let depth = 0
    let cacheVnode = Object.create(null)
    vnode && (vnode.data.febAlive = true)
    while (parent && parent._routerRoot !== parent) {
      if (parent.$vnode && parent.$vnode.data.febAlive) {
        depth++
      }
      parent = parent.$parent
    }

    // 记录缓存及其所在层级
    febCache[depth] = cache

    // /home/a backTo /other
    // 内层feb-alive实例会被保存,防止从/home/a 跳转到 /other的时候内层feb-alive执行render时候,多生成一个实例
    if (to.matched.length < depth + 1) {
      return null
    }
    if (from.matched[depth] === to.matched[depth] && (from.matched.slice(-1)[0] !== to.matched.slice(-1)[0])) {
      // 嵌套路由跳转 && 父级路由
      // /home/a --> /home/b
      // 父路由通过key进行复用
      cache[key] = cache[key] || this.keys[this.keys.length - 1]
      cacheVnode = getCacheVnode(cache, cache[key])
      if (cacheVnode) {
        vnode.key = cacheVnode.key
        remove(keys, key)
        keys.push(key)
      } else {
        this.cacheClear()
        cache[key] = vnode
        keys.push(key)
      }
    } else {
      // 嵌套路由跳转 && 子路由
      // 正常跳转 && 动态路由跳转
      // /a --> /b
      // /page/1 --> /page/2
      vnode.key = `__febAlive-${key}-${vnode.tag}`
      cacheVnode = getCacheVnode(cache, key)
      // 只有相同的vnode才允许复用组件实例,否则虽然实例复用了,但是在patch的最后阶段,会将复用的dom删除
      if (cacheVnode && vnode.tag === cacheVnode.tag) {
        // 从普通路由后退到嵌套路由时,才需要复原key
        vnode.key = cacheVnode.key
        vnode.componentInstance = cacheVnode.componentInstance
        remove(keys, key)
        keys.push(key)
      } else {
        this.cacheClear()
        cache[key] = vnode
        keys.push(key)
      }
    }
    vnode.data.keepAlive = true
  }
  return vnode
}

几个关键的点都加上了注释,现在我们一步一步解析

const vnode = this.$slots.default ? this.$slots.default[0] : null
const disableCache = this.$route.meta.disableCache

此处与上一篇文章分析keep-alive实现一样,在feb-alive组件的render函数中可以通过this.$slots.default[0]获取到嵌套的第一个默认插槽的vnode,也就是router-view组件vnode,同时获取到了路由配置disableCache用来判断用户是否配置改页面启用缓存。

// 如果不支持html5 history 写操作则不做缓存处理
if (!supportHistoryState) {
  return vnode
}
// 尝试写入key
if (!history.state || !history.state[keyName]) {
  const state = {
    [keyName]: genKey()
  }
  const path = getLocation()
  history.replaceState(state, null, path)
}
// 有些浏览器不支持往state中写入数据
if (!history.state) {
  return vnode
}
// 指定不使用缓存
if (disableCache) {
  return vnode
}

首先判断了当前宿主环境是否支持history。之后判断当前页面的history.state是否存在对应的页面key,如果没有则创建,并通过history.replaceState进行key值写入。

最后又做了一层history.state判断,因为有些浏览器不支持history的写入操作。

当宿主环境不支持history的时候直接返回vnode。

当route.meta.disableCache为true时,也直接返回vnode

// 核心逻辑
if (vnode) {
  const { cache, keys } = this
  const key = history.state[keyName]
  const { from, to } = this.$router.febRecord
  let parent = this.$parent
  let depth = 0
  let cacheVnode = Object.create(null)
  vnode && (vnode.data.febAlive = true)
  while (parent && parent._routerRoot !== parent) {
    if (parent.$vnode && parent.$vnode.data.febAlive) {
      depth++
    }
    parent = parent.$parent
  }

  // 记录缓存及其所在层级
  febCache[depth] = cache

  // /home/a backTo /other
  // 由于feb-alive实例会被保存,防止例如/home/a 后退到 /other的时候内层feb-alive执行render时候,多生成一个实例
  if (to.matched.length < depth + 1) {
    return null
  }
  if (from.matched[depth] === to.matched[depth] && (from.matched.slice(-1)[0] !== to.matched.slice(-1)[0])) {
    // ...
  } else {
    // ...
  }
  vnode.data.keepAlive = true
}

首先,我们在每个feb-alive组件的render函数中计算了当前的feb-alive所在层级,这是为了解决嵌套路由的使用。

每个层级的feb-alive组件实例都维护着当前所在层级的路由组件实例的缓存。这样设计,feb-alive组件只需要关心自身所处层级的情况即可,减少了缓存路由实例的成本。

继续分析代码

if (from.matched[depth] === to.matched[depth] && depth !== to.matched.length - 1) {
  // ...
} else {
  // ...
}

Q: 这里的if条件什么时候成立呢?

答案:被包裹组件是嵌套路由中的父级路由组件

例如/home/a -> /home/b,其中home组件在嵌套路由跳转时不应该重新实例化,因为嵌套路由跳转的时候,父路由组件状态应该被保存,而复用home组件,无需主动设置componentInstance,直接进行key设置复用即可

这里需要重点关注下父组件实例缓存的技巧

cache[key] = cache[key] || this.keys[this.keys.length - 1]
cacheVnode = getCacheVnode(cache, cache[key])
if (cacheVnode) {
  vnode.key = cacheVnode.key
  remove(keys, key)
  keys.push(key)
} else {
  this.cacheClear()
  cache[key] = vnode
  keys.push(key)
}

我们一步步分析

当我们首次访问/home/a的时候,home组件对应的是层级为0,也就是最外层的feb-alive需要缓存的vnode对象,这里姑且用feb-alive[0]来描述,此时cache[key]取到为undefined,cacheVnode也是undefined,这样会进入到else逻辑,将home组件的vnode缓存到cache[key]中。

当我们从/home/a 跳转到 /home/b 时,针对home组件会再次进入到上面的代码片段

// 取到的是/home/a页面的key
cache[key] = cache[key] || this.keys[this.keys.length - 1]

取到的是/home/a页面的key,所以之后cacheVnode就可以取到/home/a页面访问时存储的home组件的vnode,这个时候只需要将其key赋给当前的home组件的vnode即可,之后Vue在渲染的时候会通过key复用实例。从而保证/home/a -> /home/b 时,会复用home组件实例。

这样我们就实现了嵌套路由中父级路由的复用。

其他情况的话就会走else逻辑

1. 普通路由跳转

/foo -> /bar

2. 动态路由跳转

/page/1 -> /page/2

3. 嵌套路由中的子级路由

/home/foo -> /home/bar 中的foo, bar组件

/home/foo/a -> /home/bar/a 中的foo, bar组件,注意a组件依然会走if逻辑,不过其操作没有太大意义

/home/page/1 -> /home/page/2 中的page组件

针对else这层逻辑和keep-alive一样,非常简单

// 根据规则拼接vnode key
vnode.key = `__febAlive-${key}-${vnode.tag}`

// 获取缓存vnode
cacheVnode = getCacheVnode(cache, key)

// 判断是否命中缓存vnode,此处还必须保证两个vnode的tag相同
if (cacheVnode && vnode.tag === cacheVnode.tag) {
  vnode.key = cacheVnode.key
  vnode.componentInstance = cacheVnode.componentInstance
  remove(keys, key)
  keys.push(key)
} else {
  this.cacheClear()
  cache[key] = vnode
  keys.push(key)
}

此处根据key获取到缓存vnode,如果存在则复用实例并刷新key的顺序,否则缓存当前的vnode,供下次缓存恢复使用。
到此,feb-alive核心逻辑阐述完毕。

参考文档

vue-navigation
Vue.js 技术揭秘

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

(0)

相关推荐

  • 浅谈Vue页面级缓存解决方案feb-alive(上)

    feb-alive github地址 体验链接 使用理由 开发者无需因为动态路由或者普通路由的差异而将数据初始化逻辑写在不同的钩子里beforeRouteUpdate或者activated 开发者无需手动缓存页面状态,例如通过localStorage或者sessionStorage缓存当前页面的数据 feb-alive会帮你处理路由meta信息的存储与恢复 为什么开发feb-laive? 当我们通过Vue开发项目时候,是否会有以下场景需求? /a跳转到/b 后退到/a时候,希望从缓存中恢复页面

  • 浅谈Vue页面级缓存解决方案feb-alive (下)

    feb-alive github地址 体验链接 Vue页面级缓存解决方案feb-alive (上) 在剖析feb-alive实现之前,希望大家对以下基本知识有一定的了解. keep-alive实现原理 history api vue渲染原理 vue虚拟dom原理 feb-alive与keep-alive差异性 1. 针对activated钩子差异性 keep-alive配合vue-router在动态路由切换的情况下不会触发activated钩子,因为切换的时候组件没有变化,所以只能通过befor

  • 浅谈Vue单页面做SEO的四种方案

    目录 1.Nuxt 服务端渲染应用部署 (SSR服务器渲染) 优势: 不足:(开发中遇到的坑) 2.Nuxt 静态应用部署 优势: 不足: 3.预渲染prerender-spa-plugin 优势: 不足: 4.Phantomjs 针对爬虫做处理 优势: 不足: 总结 众所周知,Vue SPA单页面应用对SEO不友好,当然也有相应的解决方案,通过查找资料,大概有以下4种方法.(本人只用过第一,第三种方案) 1.Nuxt 服务端渲染应用部署 (SSR服务器渲染) 关于服务器渲染:Vue官网介绍 ,

  • 浅谈vue同一页面中拥有两个表单时,的验证问题

    问题:如果vue的同一个页面拥有两个表单.验证第一个表单时没有通过就切换到第二个,那么第二个表单会出现验证错误的信息 我们可以通过为两个表单添加ref属性 之后在通过调用resetFields()方法来解决问题 代码如下 <el-form :model="form" :rules="rules" ref="form" label-width="100px"> this.$refs["form"]

  • 浅谈vue异步数据影响页面渲染

    今天遇到一个问题,要保证页面渲染前请求的数据已经得到了 由于user是在异步请求之后保存在session中,而在页面渲染时session中还没有user,页面直接报错. 因此我希望能在所有请求都得到后再去做页面的渲染. 1.先把id为app的div用v-if="appShow",定义appShow为false进行隐藏,避免渲染 2.写计数器,每1ms进行一次查询,如果session中已经有user,删除过滤器,移除滤布,appShow为true,开始渲染页面,这样可以保证页面的正常渲染

  • 浅谈vue单页面中有多个echarts图表时的公用代码写法

    html中: <div class="charts1"/> <div class="charts2"/> <div class="charts3"/> <div class="charts4"/> <div class="charts5"/> <div class="charts6"/> <div class=

  • 浅谈Django 页面缓存的cache_key是如何生成的

    页面缓存 e.g. @cache_page(time_out, key_prefix=key_prefix) def my_view(): ... 默认情况下,将使用配置中的default cache cache_page 装饰器是由缓存中间件 CacheMiddleware 转换而来的 CacheMiddleware 继承了 UpdateCacheMiddleware 和 FetchFromCacheMiddleware UpdateCacheMiddleware 继承自 Middleware

  • 浅谈uniapp页面跳转的解决方案

    目录 1.uniapp常用跳转API 2.微信小程序页面跳转API 3.其他页面跳转回tabbar页面的方法 4.页面来回跳转保持数据的方法 正常的页面跳转的api大家应该都清楚,但是涉及到多页面来回跳转以及返回到导航页的时候就需要一些技巧来进行处理,之前找了挺多文章也没有很详细的介绍,本文就详细说说页面跳转的那些事. 1.uniapp常用跳转API API 作用 uni.navigateTo 保留当前页面,跳转到应用内的某个页面,使用uni.navigateBack可以返回到原页面. uni.

  • 浅谈VUE项目打包后运行页面一片白问题

    目录 1.说明 2.问题说明 3.解决 3.1.index.js 3.2.utils.js 3.3.webpack.prod.conf.js 4.总结 1.说明 我们用VUE搭建一个脚手架后,在IDEA等工具中开发时,启动都没有什么问题,但是项目开发完成之后,可能需要部署上线,所以需要进行打包操作了,一般都是用下面命令进行打包: npm run build 打包过程一般没有什么问题,然后就会在工程目录下生成一个[dist]文件夹,里面就是我们打包好的文件,把这些文件部署到Nginx中或者Tomc

  • 浅谈Vue.js

    vue.js的总体评价"简单却不失优雅,小巧而不乏大匠" Vue.js简介 Vue.js的作者为Evan You(尤雨溪),任职于Google Creative Lab,虽然Vue是一个个人项目,但在发展前景上个人认为绝不输于Google的AngularJs,下面我会将Vue与Angular(Angular 1.0+版本)做一些简单的比较. Vue的主要特点就和它官网(http://cn.vuejs.org/)所介绍的那样: (1) 简洁 (2) 轻量 (3)快速 (4) 数据驱动 (

随机推荐