本文涉及技术点:
- 动态组件 & 异步组件
- 内置组件 keep-alive & transition
- 插槽 slot 及 v-slot
实际场景
多级 tabs
切换,tab
项不固定,灵活控制 tab
项内容的展示,如下图。
目录结构
目录结构大概像这样:
-
src
-
components - 公共组件
- Tabs.vue - 封装的
Tabs
组件 - EmptyView.vue - 空页面组件
- *.vue - 其他公共组件
- Tabs.vue - 封装的
-
pages - 容器组件
- Index.vue - 主要处理一级 tabs 数据及对应的内容渲染
- VersionList.vue 主要处理二级 tabs 数据及对应的内容渲染
-
views - 视图组件,不固定需要动态引入,可以无限扩展
-
project-exercise
- Index.vue
- ...
-
-
组件设计
从页面元素的可复用性角度考虑,我们将将组件按类型分为公众组件、容器组件和视图组件。
公共组件
根据对页面元素的分析,我们可以提取选项卡元素为公共组件,因为两个地方用到了选项卡切换,所以根据需求进行封装,代码如下。
<!--src/components/Tags.vue -->
<template>
<el-tabs v-model="active" :type="type" @tab-click="handleClick">
<el-tab-pane v-for="item in tabs" :key="item.id" :name="item.name" :label="item.label"></el-tab-pane>
<transition name="component-fade" mode="out-in">
<keep-alive>
<slot :item="currentTab"></slot>
</keep-alive>
</transition>
</el-tabs>
</template>
我们封装的组件Tags
中,使用elementUI
中的tabs
组件(类库可以随意选择,不要受工具限制)。
公共组件 Tags
由两部分构成:
-
tabs
切换栏 - 切换栏数据由外部控制,通过props
注入。 -
内容展示区域 - 内容展示区域由
slot
进行控制。
之所以 slot
外层包裹 keep-alive
是因为实际分发的组件内容是由动态组件控制的,起到缓存优化的作用。
一级容器组件 Index
容器组件分为: 一级选项卡容器和二级选项卡容器,一级选项卡内容展示区域又负责渲染二级选项卡及选项卡对应的内容区域。
<--! src/pages/Index.vue-->
<template>
<div>
<v-tags :activeName="activeName" :type="tabType" :tabs="tabs" v-slot="current">
<component :is="getCurrentTab(current.item)" :tab="getCurrentTab(current.item)"></component>
</v-tags>
</div>
</template>
<script>
import VTags from '@/components/Tabs';
import EmptyView from '@/components/EmptyView';
import VersionList from './VersionList';
export default {
components: {
VTags,
EmptyView,
ProjectExercise: VersionList,
FullCycle: VersionList
},
data() {
return {
tabType: 'card',
activeName: 'project-exercise',
tabs: [...]
}
},
methods: {
// 根据 tabName 渲染不同组件
getCurrentTab(name) {
const tabName = [
'project-exercise',
'full-cycle'
]
return tabName.includes(name) ? name : 'empty-view';
}
},
}
</script>
一级容器组件做的事情:
- 负责告诉公共组件
Tabs
渲染哪些tabs
数据。 - 负责控制一级选项卡进行切换时,渲染对应的内容组件。
因此通过 props
传入给 Tabs
用来渲染的 tabs
数据可能像这样:
tabs: [
{ id: '0',
label: '项目策划',
name: 'project-exercise'
},
{ id: '1',
label: '需求分析',
name: 'demand-analysis'
},
{ id: '2',
label: '设计编码',
name: 'design-encoding'
},
{ id: '3',
label: '单元测试',
name: 'unit-test'
},
{ id: '4',
label: '组装测试',
name: 'assembly-test'
},
{ id: '5',
label: '确认测试',
name: 'confirmation-test'
},
{ id: '6',
label: '全生命周期',
name: 'full-cycle'
}
]
一级选项卡渲染出来的结果像下图所示。
分发给 Tabs
组件的 slot
插槽的内容通过动态组件 component
控制。
<component :is="getCurrentTab(current.item)" :tab="getCurrentTab(current.item)"></component>
is
属性的值由公共组件 Tabs
传入,传入的值与 name
值对应,由 v-slot
接受。最后对处理传入的值进行匹配操作,如下代码。
methods: {
// 根据 tabName 渲染不同组件
getCurrentTab(name) {
const tabName = [
'project-exercise',
'full-cycle'
]
return tabName.includes(name) ? name : 'empty-view';
}
},
根据需求我们只渲染 project-exercise
、full-cycle
两个选项中的内容,其他选项我们展示一个 EmptyView
组件,效果如下。
二级容器组件 VersionList
二级容器组件是一级容器组件和视图组件的 中间桥梁
,也就是说一级容器选项卡进行切换时都会渲染二级容器组件,二级容器组件主要负责渲染版本列表和版本对应的视图组件。
版本号作为二级选项卡存在,每一个一级选项卡的内容展示都会显示相同的版本列表。
<template>
<div>
<v-tags :type="tabType" :tabs="tabs" v-slot="current">
<component :is="renderView" v-if="renderView" :planId="getPlanId(current.item)"></component>
<!-- <own-view :planId="getPlanId(current.item)"></own-view> -->
</v-tags>
</div>
</template>
<script>
//import OwnView from "../views/project-exercise/";
</script>
VersionList
中 template
类似一级容器组件,也是引入公共组件 Tags
,并通过 props
向其传递 tabs
,告诉公共组件选显示什么样的项卡数据。
接下来,二级选项卡对应的视图组件,也是由动态组件 component
控制(分发传给 Tags
组件中 slot
插槽的内容)。
<component :is="renderView" v-if="renderView" :planId="getPlanId(current.item)"></component>
computed 动态引入异步组件
与一级容器组件不同的是,传入给 is
属性的值不是组件名,而是组件实例,这里渲染的视图组件不是通过固定路径引入,而是通过 import
动态引入的,这里也是本文的重点 computed
动态引入组件, 具体实现代码如下。
<template>
...
</template>
<script>
import VTags from "@/components/Tabs";
import { getProjectPlans } from "@/api";
export default {
props: ["tab"],
components: {
VTags
},
data() {
return {
tabType: "border-card",
tabs: [],
renderView: null,
view: this.tab //tab 名
};
},
watch: {
tab() {
this.view = this.tab;
this.init()
}
},
computed: {
// 通过计算属性动态引入组件
loaderWiew() {
return () => import("../views/" + this.view + "/Index.vue");
}
},
methods: {
// 根据 name 获得 planId
getPlanId(name) {
let filterTabs = this.tabs.filter(item => item.name == name);
if (filterTabs.length) {
return filterTabs[0].id;
}
},
init() {
this.loaderWiew().then(() => {
// 动态加载组件
// this.loaderWiew() 为组件实例
this.renderView = () => this.loaderWiew();
}).catch(() => {
// 组件不存在时处理
this.renderView = () => import("@/components/EmptyView.vue");
});
}
},
mounted() {
this.init();
// 省略通过接口获取版本列表数据的逻辑
}
};
</script>
为什么使用 computed
去动态引入组件,而不是像这样:
<template>
...
</template>
<script>
import VTags from "@/components/Tabs";
const OwnView = import("../views/" + this.view + "/Index.vue");
export default {
components: {
VTags,
OwnView
},
}
</script>
要知道,在 export defaul {}
外部是无法获取 vue
实例的,因此就无法与外部进行通信,获取不到外部传入的 this.view
变量。
因此我们只能通过引入异步组件的概念,来动态引入组件。
首先我们在计算属性中创建异步组件的实例,返回 Promise
。
computed: {
// 返回 Promise
loaderWiew() {
return () => import("../views/" + this.view + "/Index.vue");
}
},
在组件挂载 mouted
阶段,处理 fulfilled
和 rejected
两种状态,fulfilled
正常渲染,rejected
则渲染 EmptyView
组件。
init() {
// this.loaderWiew() 为组件实例
this.loaderWiew().then(() => {
// 动态加载组件
this.renderView = () => this.loaderWiew();
}).catch(() => {
// 组件不存在时处理
this.renderView = () => import("@/components/EmptyView.vue");
});
}
...
mounted() {
this.init();
}
this.view
的值与 views
目录下的子目录匹配,匹配成功,代表成功引入。(后续开发中,视图组件可以无限扩展)
接着,通过 watch
监听数据变化,重新初始化组件。
watch: {
tab() { // tab 通过 props 传入,传入的值与目录名称对应
this.view = this.tab; //
this.init(); // 由变化时,说明二级 tab 进行了切换,重新渲染
}
},
最后,视图组件引入成功,正常渲染。
引入失败,渲染 EmptyView
组件。
最后
一个完整的多级 tabs
切换组件就设计完成了,支持无限 view
层组件扩展,可以根据需求灵活控制 tab
项内容的展示。
getCurrentTab(name) {
const tabName = [
'project-exercise',
'full-cycle'
]
return tabName.includes(name) ? name : 'empty-view';
}
点个赞或关注下,会不定期分享技术文章。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。