本文涉及技术点:html
多级 tabs
切换,tab
项不固定,灵活控制 tab
项内容的展现,以下图。 vue
目录结构大概像这样:api
src缓存
components - 公共组件异步
Tabs
组件pages - 容器组件async
views - 视图组件,不固定须要动态引入,能够无限扩展ide
project-exercise工具
从页面元素的可复用性角度考虑,咱们将将组件按类型分为公众组件、容器组件和视图组件。单元测试
根据对页面元素的分析,咱们能够提取选项卡元素为公共组件,由于两个地方用到了选项卡切换,因此根据需求进行封装,代码以下。测试
<!--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
是由于实际分发的组件内容是由动态组件控制的,起到缓存优化的做用。
容器组件分为: 一级选项卡容器和二级选项卡容器,一级选项卡内容展现区域又负责渲染二级选项卡及选项卡对应的内容区域。
<--! 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
组件,效果以下。
二级容器组件是一级容器组件和视图组件的 中间桥梁
,也就是说一级容器选项卡进行切换时都会渲染二级容器组件,二级容器组件主要负责渲染版本列表和版本对应的视图组件。
版本号做为二级选项卡存在,每个一级选项卡的内容展现都会显示相同的版本列表。
<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="renderViewName" v-if="renderViewName" :planId="getPlanId(current.item)"></component>
与一级容器组件不一样的是,传入给 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().then(() => { // 动态加载组件 // this.loaderWiew() 为组件实例 this.renderViewName = () => this.loaderWiew(); }).catch(() => { // 组件不存在时处理 this.renderViewName = () => 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'; }
点个赞或关注下,会不按期分享技术文章。