lizhongzhen11 / lizz-blog

前端学习
80 stars 6 forks source link

keep-alive组件之include属性业务实践——动态改变组件keep-alive状态 #28

Open lizhongzhen11 opened 5 years ago

lizhongzhen11 commented 5 years ago

需求

昨天接了个项目,维护别人开发好的后台管理系统。 业务场景:点击左侧菜单栏,右侧渲染对应路由页面并在头部有对应的tags标签,点击tag标签也相当于路由跳转到对应页面。

要求:

  1. 点击菜单栏时,右侧面板顶部需要有与点击过的菜单一一对应的tab/tag标签,点击该标签也是跳转到对应路由,标签可关闭
  2. 点击菜单栏时,将对应页面重新渲染,清空数据
  3. 点击tag标签时,如果已经填写数据,那么tag来回切换时保留数据不清空

分析

这种需求其实比较常见,主要是为了操作方便。 但是如何去做?怎么去思考呢?

假设有a,b,c三个菜单,原先功能是点击a,b,c三个菜单,右侧面板会相应的渲染这三个菜单对应的组件/页面

思考要求1

针对要求1,较为简单,vue相关的ui库大多提供了tab/tag组件,这里我选用tag,毕竟用tab相当于把所有页面组件全放到tabs里面渲染,这样并不好。

这个项目比较坑的就是菜单栏是写死的,不是通过config.js配置的,这里浪费了很多时间,我需要把所有路由以及对应的菜单名称都维护到一个数组route里面,然后当我点击菜单时,根据菜单路由去route中找到对应的信息,然后放到tags数组里面,渲染tag标签。 同时,给tag加个click事件,点击进行路由跳转。

思考要求2

其实目前就是路由跳转重新渲染,暂时还不需要去考虑。

思考要求3

点击tag标签进行路由跳转,但是不能清空数据。 熟悉的vue的人肯定立马想到keep-alive组件。

<keep-alive>
  <router-view></router-view>
</keep-alive>

加上上面代码,试验下,信息确实保留了。

但是,问题也来了! 此时点击菜单栏,无法去重新刷新页面并清空数据了!

如何让点击菜单重新渲染页面和点击tag标签保留页面数据共存呢?

一开始,我想了一种方法:

<keep-alive v-if="isKeepAlive">
  <router-view></router-view>
</keep-alive>
<router-view v-if="!isKeepAlive"></router-view>

通过两个不同的router-view去渲染组件,点击tag标签跳转的走keep-alive,点击菜单跳转不走keep-alive

经过试验,依然不行。当我点击a,b,c三个菜单,右侧有对应的三个tag标签后,我通过点击tag标签切换页面并分别在这三个页面上写点数据,然后点击a菜单去重新渲染并清空a页面,然后我点击tag标签切换到b页面,再点击tag标签切回a页面,发现之前数据依然存在!

这是为何呢? 其实我上面用了两个router-view维护了两种组件渲染方式。也就是说点击tag标签时,所有页面组件都是keep-alive的,点击菜单刷新时,所有页面都是正常渲染的,他们彼此间没法互相交互。

我的困扰点

其实,我困扰的点主要是如何去动态改变组件keep-alive状态,即a页面通过点击tag标签渲染的话,需要加入keep-alive,但是通过菜单渲染的话要取消keep-alive状态!

这样看有点destroy的意思,但是我不可能在几百个vue组件中去destroy!

解决过程

百思不得其解后,试着百度了下,偶然在segmentfault看到个评论: 使用keep-aliveinclude

我立马去官方文档上查看该属性(以前从没用过):https://cn.vuejs.org/v2/api/#keep-alive

当我看到可以通过v-bind去动态改变include时,心中顿时觉得有希望了,不过我需要在之前的route中继续维护组件名称属性——compontName,同时需要在每个路由对应的组件中把name加上,然后测试了下,果然好了!

附上代码:

// main.vue
<keep-alive :include="includes">
  <router-view class="fadeInRight animated"></router-view>
</keep-alive>
// mixins handleTags
import route from '@/config/route'
const handleTags = {
  data () {
    return {
      tags: [], // 缓存所有打开过的菜单页并用tag展示
      includes: [], // 缓存需要keep-alive的组件名,点击菜单刷新页面时从中移除
    }
  },
  mounted() {
    const path = this.currentPath
    const tags = this.tags
    !tags.length && this.addTag(path)
  },
  computed: {
    /**
     * @description 获取当前页面路由
     */
    currentPath () {
      return this.$route.path
    }
  },
  methods: {
    /**
    * @description 选择左侧菜单触发,用于渲染右侧头部tags标签以及去除相应页面的keep-alive
    */
    selectMenu ({index, indexPath}) {
      const i = this.findIndex(this.tags, index, 'path')
      if (index && index.indexOf('/') !== -1 && i === -1) {
        this.addTag(index)
      }
      this.spliceInclude(index)
    },
    /**
     * 
     * @param {*} arr
     * @param {*} path
     * @description 判断该路由是否已经存在 
     */
    findIndex (arr, value, property) {
      return arr.findIndex(item => {
        return property ? item[property] === value : item === value
      })
    },
    /**
     * 
     * @param {*} path
     * @description 添加标签 
     */
    addTag (path) {
      const tags = this.tags
      route.some(item => {
        item.path === path && tags.push(item)
      })
    },
    /**
     * @description 点击tag触发。将所有的tag标签对应的路由组件全部加入keep-alive
     */
    handleClickTag (tag) {
      let componentName
      const tags = this.tags
      tags.forEach(item => {
        componentName = this.getComponentName(item.path)
        !this.includes.includes(componentName) && this.includes.push(componentName)
      })
      this.$nextTick(() => {
        this.$router.push(tag.path)
      })
    },
    /**
     * 
     * @param {*} tag
     * @description 关闭tag触发。
     *              如果只剩一个不给删除;
     *              删除当前tag默认展示前一个tag对应的页面;
     *              如果前面没有tag了,默认展示当前第一个tag;
     *              同时从keep-alive中删除;
     */
    handleClose (tag) {
      const index = this.findIndex(this.tags, tag.path, 'path')
      this.tags.splice(index, 1)
      if (this.currentPath === tag.path) {
        const next = index - 1 >= 0 ? index - 1 : 0
        this.$router.push(this.tags[next].path)
      }
      this.spliceInclude(tag.path)
    },
    /**
     * @description 获取当前路由对应的组件名称
     */
    getComponentName (path) {
      // 获取在route配置中的位置
      const j = this.findIndex(route, path, 'path')
      if (j !== -1) {
        return route[j].componentName
      }
      return
    },
    /**
     * @description 从keep-alive状态的组件数组中删除
     */
    spliceInclude (path) {
      const componentName = this.getComponentName(path)
      const includes = this.includes
      const k =  this.findIndex(includes, componentName)
      k !== -1 && this.includes.splice(k, 1)
    }
  },
}
export default handleTags

// route.js
const route = [
  {
    name: 'a',
    path: '/a',
    componentName: 'a'
  },
  {
    name: 'b',
    path: '/b',
    componentName: 'b'
  },
  {
    name: 'c',
    path: '/c',
    componentName: 'c'
  },
]
pro-xiaoy commented 5 years ago