[关闭]
@wy 2019-07-15T09:58:04.000000Z 字数 2536 阅读 1321

使用Vue.js中的动态组件模板

vuejs


最近在做公司内部使用的建站工具,写完后又做了一版简化的,放在了github上,点击这里查看,欢迎star,目前搭建了个大框出来,后续会再进行完善,继续往里面添加组件和功能。

项目中使用的技术点:
- 使用vue-cli3搭建的项目
- 完全用 typescript 的语法来写
- vue-property-decorator 用装饰器来简化书写
- Vuex做状态管理,记录用户操作数据的过程

以下是成型的建站工具的样子,图片来自互联网。
[图片1]

简单的说一下使用方式,左侧聚集了各种各样的组件图标,拖拽到中间的操作区域,就是最终要呈现的样子,右侧是对选中操作区的某个组件进行设置,例如样式,数据、动画等,也有很多个组件。

在进行建站时,不会把左侧的所有组件都能用上,一个站点可能只会用3-4个组件,要进行设置的组件也只需要3-4个即可。也就是说,在初次加载的时候,不需要把所有的组件全部加载进来。

即便在创建站点后,返回来进行编辑,也只需要在初次加载已经用到的3-4个组件就好了。等到正式发布成为一个站点,只需要预览已经选中的组件,不用加载没用上的组件。

一开始写并不是太顺利,进行了各种尝试,才敲定了最终可行的方式。这里要分享的是如何使用 Vue动态组件 以及 webpack代码分割 来完成功能的过程。

假如需要用到三个组件,分别是:图片、文本、搜索。

使用多个条件判断

  1. <template>
  2. <div class="comment">
  3. <div v-for="item in types">
  4. // 显示图片组件
  5. <com-img v-if="item.type === 'img'" :data="item.data />
  6. // 显示文本组件
  7. <com-text v-if="item.type === 'text'" :data="item.data />
  8. // 显示搜索组件
  9. <com-search v-if="item.type === 'search'" :data="item.data />
  10. </div>
  11. </div>
  12. </template>

以上会根据拖拽不同的组件,区分 type 类型,来判断是否渲染此组件。但如果所支持的组件列表变得越来越长,这就会变得非常混乱和重复。要增加一个组件就要增加一个 v-if 进行类型判断,不够智能。

还会存在这样一个问题,建站工具中,理想状态下拖拽左侧同一个图标多次,会在操作区渲染同一个组件多次,而如果按照上面的代码进行判断,无论拖拽同一个图标多少次,操作区始终只会显示这个组件一次,而且只会按最后一次拖拽过来的数据为准。

使用动态组件

Vue 中提供了动态组件来完成以上功能。

首先把需要的组件都放在一个集合中,根据约定好的类型,作为 key 值,方便查找。

  1. import text from '@/text'
  2. import search from '@/search'
  3. import img from '@/img'
  4. // 组件集合
  5. let components = {
  6. text,
  7. search,
  8. img
  9. }

在模板中根据选中的数据,通过类型找到需要渲染的组件。

  1. // 模板
  2. <div class="item" v-for="item in types">
  3. <component :is="addComponents(item)" :data="item.data"></component>
  4. </div>
  5. // 用到的方法
  6. data(){
  7. return {
  8. types:[] // 存放所需要组件的数据
  9. }
  10. },
  11. methods:{
  12. addComponents(item){
  13. return components[item.type]
  14. }
  15. }

这样就很好的解决了上面的问题,有多少条数据,就渲染几个组件,即便是同一个图标被拖拽了多次,也会在操作区把同一个组件渲染多次。

但这种方式存在两个问题:

第一个问题:会将所有的组件加载进来。不过使用异步组件可以解决这个问题,但不可避免的要手动加载,方式是:

  1. const text = () => import('@/text');
  2. const search = () => import('@/search');
  3. const img = () => import('@/img');

但这会引发第二个问题。

第二个问题:每次添加或删除一个组件时,都会对 types 这个数据进行操作,从而重新会进行 v-for 循环,这导致动态加载的组会重新渲染。这会造成已经给组件设置的数据在重新渲染后会丢失。

使用包装组件

我们可以定义一个包装组件,在包装组件中动态加载渲染所需要的组件。

  1. <template>
  2. <component :is="component" :data="data" v-if="component" />
  3. </template>
  4. <script>
  5. export default {
  6. name: 'dynamic-link',
  7. props: ['data', 'type'],
  8. data() {
  9. return {
  10. component: null,
  11. }
  12. },
  13. computed: {
  14. loader() {
  15. if (!this.type) {
  16. return null
  17. }
  18. return () => import(`templates/${this.type}`)
  19. },
  20. },
  21. mounted() {
  22. this.loader()
  23. .then(() => {
  24. this.component = () => this.loader()
  25. })
  26. .catch(() => {
  27. this.component = () => import('templates/default')
  28. })
  29. },
  30. }
  31. </script>

使用方式和之前一模一样:

  1. <template>
  2. <div class="item" v-for="item in types">
  3. <dynamic-link :type="item.type" :data="item.data"></dynamic-link>
  4. </div>
  5. </template>

这样就很好的解决了上述问题:
1. 不需要把每个组件都引入进来,但需要按照约定设置好目录结构。(当然,你也可以做一个组件集合,根据类型去取对应组件)。
2. 每次渲染不会重新渲染组件,只会渲染给 types 新增类型对应的组件。这是因为其他的数据并没有发生变化,dynamic-link 组件当然不需要重新渲染。

以上按照约定设置好目录结构,目录结构是这样的:

[图]

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注