在使用 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
尝试二
上述方式我觉得有两个不完美的地方
- 如果在同一时间显示多个,由于引用的同一个组件,那么样式就是固定,那么他们就会互相覆盖,很明显,这不是很好
- 我们虽然采用的池子复用已经创建好的组件,但是消息一多,会创建很多的组件
我的思考是:有没有办法,我们只管理一个呢,消息的显示与销毁还是交给 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 就是干这个事情的。