Better

Ethan的博客,欢迎访问交流

在 Vue 中函数式调用组件

在使用 Element UI 时,经常会用到函数式调用的组件的方式,比如一个提示框,对于这些通用的组件,个人觉得最好的方式还是不需要对页面业务代码造成侵入,所幸在 Element UI 中就是这么做的,那么他是如何实现的呢

尝试一

我们还是需要定义组件,然后将组件封装在自己的代码中,然后对外提供一个函数。定义组件如下

<template>
    <div class="message-box">{{message}}</div>
</template>
<script>
export default {
    props: ['message']
}
</script>

创建message-box.js

import Vue from 'vue'

// 得到构造函数
const MessageBoxConstructor = Vue.extend(require('./message-box.vue'))

let pool = [] // 已经创建元素加入池子,避免浪费

// 得到实例,优先从池子中取
const getInstance = () => {
    if (pool.length > 0) {
        let instance = pool[0];
        pool.splice(0, 1);
        return instance;
    }
    return new MessageBoxConstructor({
        el: document.createElement('div')
    })
}

// 已创建的元素回到池子中
const returnToPool = instance => {
    if (instance) {
        pool.push(instance)
    }
}

const removeDom = (instance) => {
    if (instance.$el && instance.$el.parentNode) {
        instance.$el.parentNode.removeChild(instance.$el);
    }
}

MessageBoxConstructor.prototype.close = function () {
    this.visible = false;
    removeDom(this)
    this.closed = true;
    returnToPool(this);
}

const MessageBox = {
    notice(options = {}) {
        let duration = options.duration || 3000;
        let instance = getInstance()
        instance.closed = false;
        clearTimeout(instance.timer);
        // 组件需要参数
        instance.message = options.message
        document.body.appendChild(instance.$el)
        Vue.nextTick(() => {
            instance.visible = true
            instance.timer = setTimeout(() => {
                if (instance.closed) return;
                instance.close()
            }, duration);
        })
    }
}

export default MessageBox

尝试二

上述方式我觉得有两个不完美的地方

  1. 如果在同一时间显示多个,由于引用的同一个组件,那么样式就是固定,那么他们就会互相覆盖,很明显,这不是很好
  2. 我们虽然采用的池子复用已经创建好的组件,但是消息一多,会创建很多的组件

我的思考是:有没有办法,我们只管理一个呢,消息的显示与销毁还是交给 Vue 组件的响应式系统进行管理,于是构建组件如下

<template>
    <div class="message-box">
        <div class="item" v-for="item in messages">{{item.message}}</div>
    </div>
</template>
<script>
export default {
    props: ['messages']
}
</script>

这里有个点需要注意,由于模版只能有一个根节点,因此也不能直接在根节点上使用v-for,否则不能初始化组件成功

直接看修改后的代码

import Vue from 'vue'

// 这里踩了个坑,模版文件根元素不能直接v-for,否则构造函数执行有问题
const MessageBoxConstructor = Vue.extend(require('./message-box.vue'))

var instance, messages = []

// 单例模式
const getInstance = () => {
    if (!instance) {
        instance = new MessageBoxConstructor({
            el: document.createElement('div')
        })
    }
    return instance
}

// 为空的清除dom
const removeDom = () => {
    if (instance.$el && instance.$el.parentNode) {
        instance.$el.parentNode.removeChild(instance.$el);
    }
}

const MessageBox = function () {
    let instance = getInstance()
    // 加入响应式管理
    instance.messages = messages
}

MessageBox.prototype.notice = function (options = {}) {
    if(messages.length === 0) {
        // 避免污染dom
        document.body.appendChild(instance.$el)        
    }
    let duration = options.duration || 3000;    
    messages.unshift(options)
    Vue.nextTick(() => {
        options.timer = setTimeout(function() {
            var index = messages.findIndex(item => item === this)
            messages.splice(index, 1)
            if(messages.length === 0) {
                removeDom()
            }
        }.bind(options), duration);
    })
}

export default new MessageBox()

这里我去除了组件池,而是改用单例模式。

背景

首先为什么会有这种需求呢?

毕竟 Vue 推荐的就是声明式的组件使用方式,其实对于一般组件,这样使用并没有问题,但对于全屏类的弹窗组件,如果在一个层级嵌套很深的子组件中使用,仍然通过声明式的方式,很可能它的样式会受到父元素某些 CSS 的影响导致渲染不符合预期。这类组件最好的使用方式就是挂载到 body 下,但是我们如果是声明式地把这些组件挂载到最外层,对它们的控制也非常不灵活。其实最理想的方式是动态把这类组件挂载到 body 下。

针对该提示组件,我们完成了函数式调用编写,但有没有方法,可以直接将声明式组件转成函数式组件使用呢?对此可以了解下 cube-ui,其 createAPI 就是干这个事情的。

参考



留言