【Vue】动态添加组件的两种实现

by Microanswer

标签: vue extend render createElement 动态组件


创建时间:Jun 20, 2019 10:34:16 AM | 最后更新:Oct 16, 2019 10:32:09 AM 


重要声明:本文章仅仅代表了作者个人对此观点的理解和表述。读者请查阅时持自己的意见进行讨论。

注意#,本文重点讲解的是:在同一个界面里,部分组件的动态显示与隐藏相关的功能。亦可以理解为界面展示根据服务端返回数据动态显示出来。如果你在寻求:如何将某个页面在用户点击时才进行加载(从服务端下载页面逻辑js)。请查看这篇文章:【Vue】页面组件懒加载,动态加载组件,按需加载,路由懒加载。

在Vue项目中,想要动态为页面上添加组件,是一件十分容易就能够实现的事情,这必须要得益于Vue框架的强力支持! 但有时候往往就是由于事情变得简单起来后,却突然忘记了要如何去实现。这大概就是对代码走火入魔的一种解释吧!

为页面动态添加组件,在实现区别上,我总结出了2种:

  1. 数据动态渲染已有组件。
  2. 先生成组件再添加到界面上。

〇、提出需求

假设现有一需求:要求实现某标签的添加,删除功能。效果如下:

图1

这种效果想必还是比较合适用来理解组件的动态添加和移除的。 下面将对两种实现方案分别进行讲述。在这之前先预览一下案列项目结构,不用担心,这是使用 vue-cli@3 工具创建的 项目,应该和大多数人的项目目录结构相似:

图2

一、数据动态渲染组件

在 Vue 的官方文档中,条件渲染列表渲染这两处文档直接为你展示了如何通过数据来控制组件的渲染, 不过有一点要注意的是,Vue 官方文档,都是在讲述html文件中如何使用vue,而并没有在单文件组件的篇幅里进行 较多的讲解。虽然用法的确是相同的,但不免理解起来有一层隔阂。

此处,就将以单文件组件解释说明如何完成组件的动态渲染,也就是动态的添加和移除什么的。

1、建立标签组件

使用数据来动态渲染组件,前提是要先有这个组件,所以就先把组件创建出来,在 components 目录下创建名为 tag.vue 的组件,组件代码如下:

<!-- tag.vue -->
<template>
    <div class="tag">
        {{text}}
        <button @click="onCloseClick">×</button>
    </div>
</template>

<script>
    export default {
        name: "tag",
        props: {
            // 标签内容
            text: String
        },
        methods: {
            onCloseClick () {
                // 将删除标签事件暴露除去
                this.$emit("delete", this.text);
            }
        }
    }
</script>

<style scoped>
    .tag {
        display: inline-block;
        margin: 3px;
    }
</style>

这个组件只有简单的删除功能,能够表示意思意思就好!

2、修改页面,实现渲染

修改 App.vue 文件,引入tag组件,然后根据数据对tag组件进行动态的渲染:

<!-- App.vue -->
<template>
    <div>
        <h3>标签管理</h3>
        
        <!-- 标签容器已经被初始化而且内容多余0条,则显示内容 -->
        <div v-if="tags && tags.length > 0">
            <tag v-for="tag in tags" :text="tag" :key="tag" @delete="onTagDelete"/>
        </div>
        
        <!-- 否则,标签只是被初始化了,但是没有数据 -->
        <div v-else-if="tags">没有标签了</div>
        
        <!-- 标签容器还没被初始化 -->
        <div v-else>标签加载中...</div>
        
        <button @click="addTag">添加标签</button>
    </div>
</template>

<script>
    import Tag from './components/tag.vue';
    import Util from './Util';

    export default {
        name: 'app',
        components: {
            Tag
        },
        created() {
            // 从网络加载tag数据.
            Util.getTagsFromNet()
                .then(result => {
                    if (result.code !== 200) {
                        return Promise.reject(new Error(result.message));
                    }

                    // 数据加载成功,将数据赋值给 tags 即可。
                    this.tags = result.data;
                })
                .catch(error => {
                    window.alert(error.message);
                })

        },
        methods: {
            // 添加tag
            addTag() {
                this.tags.push("标签" + this.tags.length);
            },
            onTagDelete(tag) {
                // 从数组中移除。
                this.tags.splice(this.tags.indexOf(tag), 1);
            }
        },
        data() {
            return {
                tags: undefined, // 初始化是没有 tag数据的,在页面打开后,通过网络请求获取标签。
            }
        }
    }
</script>

<style scoped></style>

运行程序,效果如下:

图3

最终可以看到,这样的方式来实现动态组件渲染,宗旨就是:数据有,组件有,数据无,组件无。这也是Vue框架十分 建议的实现方式。我本人也建议使用这样的方式来实现。

二、先生成组件再添加到界面上

此方法不用事先将组件引入到界面中。而是当数据获取到时,使用数据建立组件,然后将组件添加到界面上。 这种思路显然完全正确无误,不过在Vue项目里,这样虽说可以实现,但是门槛略高。但了解这种方法也会为今后的开发 带来不少的便利。

1、了解 Vue 实例的创建

要进行下一步的讲解,先必须要了解 Vue 实例的创建。在我们平时开发Vue但也项目是,通常都能在 main.js 里看到这样的代码:

// main.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

这很容易理解,因为这样就算创建了我们单页面的整个根Vue实例了。那如果要创建我们自己想要的动态组件, 并且创建出来的组件要可以被加入到这个根实例里面去,又该怎么办?这个时候,就要祭出另一个强悍的方法了:

Vue.extend

这个方法允许我们自己随时创建 vue 实例,但是这个方法的学习门槛实在是高,先示列用法,下面是此方法的一个使用示列:

// 创建组件逻辑
let TagClass = Vue.extend({

    // 向界面渲染的dom方法。
    render (createElement) {
        return createElement(
            'div',
            {
                style: {
                    display: 'inline-block',
                    margin: '3px',
                    backgroundColor: '#d9d9d9'
                }
            },
            [
                this.text,
                createElement(
                    'button',
                    {
                        on: {
                            'click': this.onCloseClick
                        }
                    },
                    '×'
                )
            ]
        );
    },
    methods: {
        onCloseClick () {
            this.$el.remove();
            // 将删除标签事件暴露除去
            this.$emit("delete", this.text);
        }
    },
    data () {
        return {
            text: "文本内容", // 什么这个tag标签使用的文字是传入的标签文字内容
        }
    }
});

// tag 就是想要的组件实例。
let tag = new TagClass();

对单文件组件有所心得的朋友肯定还是对这段代码能够摸清楚一些的,但是这个方法并不是所有人都能一目了然。 其中有:

  1. 没有了 template 模板。
  2. 新增了 render 方法。

现在使用 vue-cli@3 创建的项目,都是只包含vue运行时功能的vue项目,所以,在代码里动态构建的组件,不能使用 template 了,那些组件模板页面其实在build的时候也会转换成render函数实现,所以在代码里动态构建组件只能使用 render 方法来进行页面的渲染。 而这个方法不难,难点可能就是render方法参数 createElement,这个参数它是一个可执行的方法,这个方法有3个参数:

createElement(参数1, 参数2, 参数3);

// 参数1: 可以是别的组件,可以是原生dom组件。
// 参数2: 一个对象,这个对象里的数据将作为 "参数1" 的属性信息写入到组件或dom上。
// 参数3: 数组或字符串或createElement方法返回的内容, 这个部分的数据将作为 "参数1" 的子内容出现。
createElement 参数2 详解

这个方法能够完成任何dom的创建以及嵌套都没问题,难点就在于如何去灵活的使用它,首要一点就是了解参数的具体含义, 参数2 作为dom属性配置,承载着许多的必要功能,下面是截取自vue官网的对象配置详细介绍:

// 参数2 配置对象介绍
{
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML 特性
  attrs: {
    id: 'foo'
  },
  // 组件 prop
  props: {
    myProp: 'bar'
  },
  // DOM 属性
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 属性内,
  // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
  // 需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
  // 赋值,因为 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽的格式为
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层属性
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}

现在,有了对 extend 方法的了解和 createElement 方法的加持,还有什么办不到的。 要了解更多 Vue.extend 方法的详情,请参考:Vue官方API。这里还有一篇描述使用构造函数时如何传递参数的记录《构造函数式组件如何传递参数》。 要了解更多 createElement 方法的详情,请参考:Vue官方文档

2、编写代码,实现效果。

现在可以修改 App.vue 文件,将其修改为先生成组件后显示到界面的逻辑,修改完成后代码如下:

<template>
    <div>
        <h3>标签管理</h3>

        <!-- 放置tag的容器,从网络获取的tags都要放在这个div里面 -->
        <div ref="tagsContext">
            <div v-if="!tags">标签加载中...</div>
            <div v-else-if="tags && tags.length === 0">没有标签</div>
        </div>

        <button @click="addTag">添加标签</button>
    </div>
</template>

<script>
    import Util from './Util';

    export default {
        name: 'app',
        components: {},
        created() {
            // 从网络加载tag数据.
            Util.getTagsFromNet()
                .then(result => {
                    if (result.code !== 200) {
                        return Promise.reject(new Error(result.message));
                    }

                    // 数据加载成功,将数据赋值给 tags 即可。
                    this.tags = result.data;

                    // 拿到了数据,进行渲染:
                    for (let i = 0; i < this.tags.length; i++) {
                        this.initTagComponentAndShowTag(this.tags[i]);
                    }
                })
                .catch(error => {
                    window.alert(error.message);
                })

        },
        methods: {
            // 添加tag
            addTag() {
                let tagText = "标签" + this.tags.length;
                this.tags.push(tagText);
                this.initTagComponentAndShowTag(tagText);
            },
            // 有tag被删除
            onTagDelete(tag) {
                // 从数组中移除。
                this.tags.splice(this.tags.indexOf(tag), 1);
            },
            // 初始化一个tag组件并且显示到界面上。
            initTagComponentAndShowTag (tagText) {
                let tag = Util.newTag(tagText);

                // 添加删除监听
                tag.$on("delete", this.onTagDelete);

                // 挂载组件,即创建虚拟dom。
                tag.$mount();

                // 将组件添加到显示tag的div里面
                this.$refs.tagsContext.appendChild(tag.$el);
            }
        },
        data() {
            return {
                tags: undefined, // 初始化是没有 tag数据的,在页面打开后,通过网络请求获取标签。
            }
        }
    }
</script>

<style scoped></style>

再次运行程序,得到效果,与使用数据动态渲染完全一致:

图4

这次就不再是先使用 Tag 组件了,为了避嫌,我甚至连 Tag 组件都没有引入。可以看到,在网络数据获取完成时, 通过循环构造组件并添加到了界面上,在 initTagComponentAndShowTag 方法中,使用Util.newTag 方法完成了组件的动态创建。由于组件的创建方法属于比较低层次级别方法,所以与界面逻辑剥离开。整个 Util.js 的代码如下:

import Vue from 'vue';

export default {

    newTag (tagText) {
        // 创建组件逻辑
        let TagClass = Vue.extend({

            // 向界面渲染的dom方法。
            render (createElement) {
                return createElement(
                    'div',
                    {
                        style: {
                            display: 'inline-block',
                            margin: '3px',
                            backgroundColor: '#d9d9d9'
                        }
                    },
                    [
                        this.text,
                        createElement(
                            'button',
                            {
                                on: {
                                    'click': this.onCloseClick
                                }
                            },
                            '×'
                        )
                    ]
                );
            },
            methods: {
                onCloseClick () {
                    // 移除dom
                    this.$el.remove();
                    // 然后将删除标签事件暴露除去
                    this.$emit("delete", this.text);
                }
            },
            data () {
                return {
                    text: tagText, // 为这个tag标签使用的文字是传入的标签文字内容
                }
            }
        });

        return new TagClass();
    },

    // 模拟从网络获取tag数据。
    getTagsFromNet () {
        return new Promise((resolve) => {
            setTimeout(() => {
                let tags = [];
                for (let i = 0; i < 6; i++) {
                    tags.push("标签" + i);
                }
                resolve({
                    code: 200,
                    data: tags,
                    msg: 'success'
                });
            }, 2000);
        });
    }
}

代码中注释比较详细,应该能了解其中含义。不论时那种方案最终都能实现效果,但是显然第一种方案代码更加优雅, 第二种代码显得比较凌乱(也许是我写的比较凌乱)。从开发复杂度上来讲,第一种也是远胜第二种。不过了解了第二种 方法,收益也是非常的肥厚!

三、归纳总结

这两种方法建议使用第一种去实现组件的添加或移除,第二种方案作为知识点要掌握。其中难点在于对Vue实例的本质了解、 对Vue渲染方法的了解,以及对Vue实例的使用熟悉程度。其中本例最重要的就是结合使用 Vue.extendcreateElement 方法。

本文中涉及的项目资料你可以在百度网盘下载, 提取码:m3y1


全文完, 转载请注明出处。 对你有帮助?不如赞一个吧:
发表评论(发表评论需要登录,你现在还没有登录。)

评论列表 (1条)

导航加载中…