在「同一路由」下,如何实现「组件切换」?

257 阅读2分钟

需求来源

常见的 管理类项目 业务场景中,常需在单一路由下,在多个组件间切换
首先想到的处理方式,可能是 定义多个组件结构,并通过 v-if 控制 显示与隐藏

<template>
  <div>
    <component-1 v-if="curComponent === 'c1'" />
    <component-2 v-else-if="curComponent === 'c2'" />
  </div>
</template>

上述做法,虽同样能实现业务需求,但因其 冗杂的DOM结构,不利于后期维护

是否存在 简化 上述结构的方法呢?

查询 Vue2官方文档,发现动态组件<component>,依参数 is,可决定哪个组件被渲染

<!-- 动态组件由 vm 实例的 `componentId` property 控制 -->  
<component :is="componentId"></component>  
  
<!-- 也能够渲染注册过的组件或 prop 传入的组件 -->  
<component :is="$options.components.child"></component>


考虑到 组件重用组件可扩展性,以 混入 思路构建组件,是较好的选择
PS:虽实际构建的仍是组件,但最终以 mixins 形式引入。较常见的 components 方式引入,前者更有利于对 DOM结构 进行调整

基础

<template>
  <component
    :is="curComponent"
    :enter-params="enterParams"
    @forward="forward"
  />
</template>

<script>
  export default {
    data() {
      return {
        curComponent: 'List',
        enterParams: {},
      }
    },
    methods: {
      forward(comp, data) {
        this.curComponent = comp
        this.enterParams = data || {}
      },
    },
  }
</script>

内容解析

  • curComponententerParams 分别存储 当前渲染组件名称当前组件所需参数集合
  • forward 用于 组件切换。用法示例:this.$emit('forward', 'Detail', { id: 'xxx' })

功能优化

一:非直接子组件,不便使用 forward方法

可选方式

方式一:多层嵌套 挂载事件
<!-- 子组件 -->
<grand-comp @forward="(curComp, enterParams) => { $emit('forward', curComp, enterParams) }">
方式二:使用 全局事件总线
<!-- 为本次定义的混入式组件,添加 额外操作 -->
beforeMount() {
  // 注册事件
  this.$eventbus.$on('forward', this.forward)
},
beforeDestroy() {
  // 销毁事件
  this.$eventbus.$off('forward', this.forward)
}
<!-- 孙组件,使用方式 -->
this.$eventbus.$emit('forward', 'HistoryDetail', { id: 'xxx' })

最终选择

方式二 较好,其利用了 全局事件总线 的优势,无需 多层嵌套 绑定事件,便可实现通信

二:返回操作不便捷

场景描述

现实业务场景中,组件的跳转方向,并不局限于

image.png

某些时候下,还存在 image.png

历史详情 数据本身往往不包含 详情ID
为达到 返回详情页 的目的,往往需要在 历史详情页面 保存 详情ID 等数据
这导致 历史详情页面 混入了 不必要数据,这不符合 高内聚低耦合 要求

实现思路

从 网页路由 得到思路,可以借助 数据缓存,保存 历史跳转信息

相关代码

<template>
  <component
    xxx
    @back="back"
    @go="go"
  />
</template>

<script>
  export default {
    data() {
      return {
        xxx
        history: [],
        curIndex: -1,
      }
    },
    watch: {
      curIndex(index) {
        const { component, params, extraParams } = this.history[index]
        Object.assign(this, {
          curComponent: component,
          enterParams: { ...params, ...extraParams },
        })
      },
    },
    beforeMount() {
      // 为跳转历史补充首跳信息
      this.curComponent &&
        this.history.push({
          component: this.curComponent,
          params: this.enterParams,
          extraParams: {},
        })
      this.curIndex = this.history.length - 1

      xxx
    },
    beforeDestroy() {
      xxx
    },
    methods: {
      forward(comp, data, preExtraData) {
        // 跳转前,补充额外数据(例如:补充tab名称,以供返回时跳转指定tab)
        Object.assign(this.history[this.curIndex].extraParams, preExtraData)

        // 存储跳转信息(默认覆盖curIndex之后的数据)
        this.history.splice(++this.curIndex, Infinity, {
          component: comp,
          params: data ?? {},
          extraParams: {},
        })
      },
      back(data) {
        this.curIndex = Math.max(0, this.curIndex - 1)
        // 补充数据
        Object.assign(this.history[this.curIndex].extraParams, data)
      },
      go(data) {
        this.curIndex = Math.min(this.history.length - 1, this.curIndex + 1)
        // 补充数据
        Object.assign(this.history[this.curIndex].extraParams, data)
      },
    },
  }
</script>

内容解析

  • history 用于缓存 已跳转组件相关信息(包含 componentparamsextraParams
  • curIndex 表示当前组件相关信息 于history所处索引值
  • back方法 go方法分别用于 返回前一组件前往后一组件(前后关系由相对history索引而来);二者思路均来自于 Vue-Router 同名方法,为其简化版本;二者均仅有一个参数 data,用于 为待返回/前往组件 补充参数

使用示例

基础混入
<script>
  // 引入混入
  import CompBasicMixin from '@/mixin/compBasicMixin'
  // 引入组件
  import List from './list'
  import EditContainer from './components/editContainer'
  import DetailContainer from './components/detailContainer'
  
  export default {
    components: {
      List,
      EditContainer,
      DetailContainer,
    },
    mixins: [CompBasicMixin],
  }
</script>
forward方法
// 基础使用
this.$emit('forward', 'EditContainer', { id: 'xxxxxxx' })
this.$eventbus.$emit('forward', 'DetailContainer', { id: 'xxxxxxx' })

// 特殊使用
// - 当页面存在tabs时,为使得返回时能跳转先前tab,常常将tabName存储到待跳转组件中,以供返回使用
// - 该操作方法,所存在的问题便是:使得无关数据杂糅在组件当中,加剧了组间间的耦合
// - 因而,在forward方法中,额外增加了preExtraData参数
this.$emit('forward', 'xxx', { xxx }, { tabName: 'xxx' })
this.$eventbus.$emit('forward', 'xxx', { xxx }, { tabName: 'xxx' })
back方法

使用场景
某些页面中,存在 历史详情页面 返回 详情页面 的情况(参考上图)
若使用forward方法,则需 额外缓存详情页面 组件名称ID等相关参数
而使用 back方法,则无需关心相关参数(特殊使用除外)

// 基础使用(可用于部分取代 $emit('forward', 'List'))
this.$emit('back')
this.$eventbus('back')

// 特殊使用(绑定参数)
this.$emit('back', { tabName: 'xxx' })	// 仅作示例,不建议以此绑定tabName,建议使用forward方法的额外参数绑定
this.$eventbus.$emit('back', { tabName: 'xxx' })
go方法

back方法

三:组件缓存不便捷

相关代码

<template>
  <keep-alive :include="keepAliveList">
    <component
      xxx
      @refresh-cache="refreshCache"
    />
  </keep-alive>
<template>

<script>
  export default {
    data() {
      return {
        xxx,
        keepAliveList: [],
      }
    },
    beforeMount() {
      xxx
      this.$eventbus.$on('refresh-cache', this.refreshCache)
    },
    beforeDestroy() {
      xxx
      this.$eventbus.$off('refresh-cache', this.refreshCache)
    },
    methods: {
      xxx,
      refreshCache(componentName) {
        if (componentName === undefined) {
          console.error('[refresh-cache] componentName is required')
          return
        }

        const matchIndex = this.keepAliveList.indexOf(componentName)
        if (matchIndex !== -1) {
          this.keepAliveList.splice(matchIndex, 1)

          setTimeout(() => {
            this.keepAliveList.push(componentName)
          }, 0)
        }
      },
    }
  }
</script>

内容解析

  • keepAliveList 用于保存待缓存的 组件name
  • refreshCache 用于在 合适的时机(如返回) 刷新/清空组件缓存

使用示例

<!-- index.vue -->
<script>
  // 引入混入
  import CompBasicMixin from '@/mixin/compBasicMixin'
  // 引入组件
  xxx
  import EditContainer from './components/editContainer'

  export default {
    components: {
      xxx
      EditContainer,
    },
    mixins: [CompBasicMixin],
    data() {
      return {
        keepAliveList: ['TestEdit'],
      }
    },
  }
</script>
<!-- editContainer.vue -->
<script>
  export default {
    name: 'TestEdit',
    xxx
    methods: {
      goBack() {
        xxx
        this.$emit('refresh-cache', 'TestEdit');
      }
    }
  }
</script>

完整代码

<template>
  <keep-alive :include="keepAliveList">
    <component
      :is="curComponent"
      :enter-params="enterParams"
      @forward="forward"
      @back="back"
      @go="go"
      @refresh-cache="refreshCache"
    />
  </keep-alive>
</template>

<script>
  export default {
    data() {
      return {
        curComponent: 'List',
        enterParams: {},
        history: [],
        curIndex: -1,
        keepAliveList: [],
      }
    },
    watch: {
      curIndex(index) {
        const { component, params, extraParams } = this.history[index]
        Object.assign(this, {
          curComponent: component,
          enterParams: { ...params, ...extraParams },
        })
      },
    },
    beforeMount() {
      // 为跳转历史补充首跳信息
      this.curComponent &&
        this.history.push({
          component: this.curComponent,
          params: this.enterParams,
          extraParams: {},
        })
      this.curIndex = this.history.length - 1

      this.$eventbus.$on('forward', this.forward)
      this.$eventbus.$on('back', this.back)
      this.$eventbus.$on('go', this.go)
      this.$eventbus.$on('refresh-cache', this.refreshCache) 
    },
    beforeDestroy() {
      this.$eventbus.$off('forward', this.forward)
      this.$eventbus.$off('back', this.back)
      this.$eventbus.$off('go', this.go)
      this.$eventbus.$off('refresh-cache', this.refreshCache)
    },
    methods: {
      updateCurrent(callback) {
        callback && callback(this.history[this.curIndex])
      },
      forward(comp, data, preExtraData) {
        // 跳转前,补充额外数据(例如:补充tab名称,以供返回时跳转指定tab)
        this.updateCurrent((current) => {
          Object.assign(current.extraParams, preExtraData)
        })

        // 存储跳转信息(默认覆盖curIndex之后的数据)
        this.history.splice(++this.curIndex, Infinity, {
          component: comp,
          params: data ?? {},
          extraParams: {},
        })
      },
      back(data) {
        this.curIndex = Math.max(0, this.curIndex - 1)
        // 补充数据
        this.updateCurrent((current) => {
          Object.assign(current.extraParams, data)
        })
      },
      go(data) {
        this.curIndex = Math.min(this.history.length - 1, this.curIndex + 1)
        // 补充数据
        this.updateCurrent((current) => {
          Object.assign(current.extraParams, data)
        })
      },
      refreshCache(componentName) {
        if (componentName === undefined) {
          console.error('[refresh-cache] componentName is required')
          return
        }

        const matchIndex = this.keepAliveList.indexOf(componentName)
        if (matchIndex !== -1) {
          this.keepAliveList.splice(matchIndex, 1)

          setTimeout(() => {
            this.keepAliveList.push(componentName)
          }, 0)
        }
      },
    },
  }
</script>