记一次 Vue 组件设计及使用 computed 动态引入组件

本文涉及技术点:html

  • 动态组件 & 异步组件
  • 内置组件 keep-alive & transition
  • 插槽 slot 及 v-slot

实际场景

多级 tabs 切换,tab 项不固定,灵活控制 tab 项内容的展现,以下图。 vue

image.png

目录结构

目录结构大概像这样:api

  • src缓存

    • components - 公共组件异步

      • Tabs.vue - 封装的 Tabs 组件
      • EmptyView.vue - 空页面组件
      • *.vue - 其余公共组件
    • pages - 容器组件async

      • Index.vue - 主要处理一级 tabs 数据及对应的内容渲染
      • VersionList.vue 主要处理二级 tabs 数据及对应的内容渲染
    • views - 视图组件,不固定须要动态引入,能够无限扩展ide

      • 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-exercisefull-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>

VersionListtemplate 相似一级容器组件,也是引入公共组件 Tags,并经过 props 向其传递 tabs ,告诉公共组件选显示什么样的项卡数据。

接下来,二级选项卡对应的视图组件,也是由动态组件 component 控制(分发传给 Tags 组件中 slot 插槽的内容)。

<component :is="renderViewName" v-if="renderViewName" :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 阶段,处理 fulfilledrejected 两种状态,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';
}

点个赞或关注下,会不按期分享技术文章。

相关文章
相关标签/搜索