需求来源
常见的 管理类项目 业务场景中,常需在单一路由下,在多个组件间切换
首先想到的处理方式,可能是 定义多个组件结构,并通过 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>
内容解析
curComponent、enterParams分别存储 当前渲染组件名称、当前组件所需参数集合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' })
最终选择
方式二 较好,其利用了 全局事件总线 的优势,无需 多层嵌套 绑定事件,便可实现通信
二:返回操作不便捷
场景描述
现实业务场景中,组件的跳转方向,并不局限于
某些时候下,还存在
历史详情 数据本身往往不包含 详情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用于缓存 已跳转组件相关信息(包含 component、params、extraParams)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用于保存待缓存的 组件namerefreshCache用于在 合适的时机(如返回) 刷新/清空组件缓存
使用示例
<!-- 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>