模拟系统的消息提示框而实现的一套模态对话框组件,用于消息提示、确认消息和提交内容。
概述:该组件的结构、原理与Toast 组件类似,所以这篇文章会减少组件开发的介绍,而增加一些Vue.extend
、Vue
生命周期相关源码的解读。
- Message-Box的基本功能实现;
Vue.extend
、$mount
原理以及相应的优化点。
1. 实例
代码
<!-- 基础用法 -->
this.$confirm({
title: "自定义提示",
content: `<h1 style="color: red;">自定义HTML</h1>`,
onConfirm: () => this.$message({
content: "确定"
}),
onCancel: () => this.$message({
type: "warn",
content: "取消"
})
});
this.$alert({
title: "标题名称",
content: "这是一段内容",
onConfirm: () =>
this.$message({
content: "确定"
})
});
复制代码
实例地址:Hover-Tip 实例
代码地址:Github UI-Library
2. 原理
将Message-Box分为两部分:
- Message-Box组件,用于在页面中显示模态框,包含确认、取消、关闭三个操作;
- Vue插件的封装,利用
this.$alert({...})
或this.$confirm({...})
在页面中挂载 Message-Box 组件。
首先开发Message-Box组件,其基本template
如下
<div class="mock" v-if="visible">
<div :class="['message-box']">
<!-- header -->
<h5 class="message-box-header c-size-l">
<span>{{ title }}</span>
<fat-icon
v-if="showClose"
name="close"
class="close-btn"
@click.stop="close"
/>
</h5>
<!-- content -->
<div
class="message-box-content c-size-m"
v-html="content"
>
</div>
<!-- footer -->
<div class="message-box-footer">
<fat-button
size="mini"
v-if="cancelButtonText && type !== 'alert'"
@click.stop="handleClick('cancel')"
>{{ cancelButtonText }}</fat-button>
<fat-button
size="mini"
type="success"
v-if="confirmButtonText"
@click.stop="handleClick('confirm')"
>{{ confirmButtonText }}</fat-button>
</div>
</div>
</div>
复制代码
基本结构非常简单,清晰的三段:
- 最上层的
visible
状态用于控制整个模态框的显示、消失,header部分,包含title
,以及关闭按键; - 中间content部分,包含传入的
content
,为了支持HTML,所以采用v-html
; - 最底层footer部分,包含两个按钮,如果当前模态框的类型是
alert
则只有confirm
,如果为confirm
,则需要添加cancel
。
所涉及的data
、methods
如下
export default {
data() {
return {
// 控制模态框的显示
visible: true
}
},
watch: {
visible(newValue) {
if (!newValue) {
// 过渡结束后注销组件
this.$el.addEventListener('transitionend', this.destroyElement)
}
}
},
mounted() {
document.body.appendChild(this.$el)
},
destroyed() {
this.$el.parentNode.removeChild(this.$el)
},
methods: {
destroyElement() {
this.$destroy()
},
close() {
// 关闭模态框
this.visible = false
},
handleClick(type) {
// 处理对应的点击事件
this.$emit(type);
this.close();
}
}
}
复制代码
可以看到在该组件的mounted
、destroyed
两个生命周期中完成组件在页面中的挂载与注销
mounted() {
document.body.appendChild(this.$el)
},
destroyed() {
this.$el.parentNode.removeChild(this.$el)
}
复制代码
其中注销的顺序是:
- 首先使组件的
visible
状态为false
,使它在页面中消失,然后触发模态框transition
的过度动画; - 之后监听
addEventListener('transitionend', this.destroyElement)
,如果过渡结束,就触发对应的destroyElement
触发组件的生命周期destroyed
,在组件中注销this.$el.parentNode.removeChild(this.$el)
。
相对于注销,挂载则要复杂的一些,由于这部分涉及到了封装,所以一起梳理。
// 引入上述Message-Box组件
import messageBox from './messagebox.vue'
// 生成Message-Box对应的构造器
const Constructor = Vue.extend(messageBox)
function generateInstance(options, type = 'alert') {
let instance = new Constructor({
propsData: Object.assign(options, {
type
}),
}).$mount(document.createElement('div'))
...
return instance
}
复制代码
首先import
模态框组件,然后利用Vue.extend
创建一个Message-Box组件的构造器。
当调用this.$alert
以及this.$confirm
时利用new Constructor
创建一个Message-Box组件,这时显式调用vm.$mount()
手动开启编译,此时会触发组件的mounted
生命周期
mounted() {
document.body.appendChild(this.$el)
}
复制代码
完成在页面中的挂载。
最后一步,利用Vue.use
完成封装,在Vue.prototype
上添加$alert
以及$confirm
方法。
export default {
install(Vue) {
Vue.prototype.$alert = (options = {}) => generateInstance(options)
Vue.prototype.$confirm = (options = {}) => generateInstance(options, 'confirm')
}
}
复制代码
3. 源码
阐述下 Vue.extend
的源码,在 src/core/global-api/extend.js
中,比较关键的点在于:
- 如何通过已有组件
object
生成对应构造器constructor
; - 对于同一个组件的
constructor
,是否存在什么优化。
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
...
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
...
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}
复制代码
从源码中可以看出,Vue.extend
是一个变式的原型式继承
function object(o) {
function F() {}
F.prototype = o
return new F()
}
复制代码
临时的构造函数 function F
为 function VueComponent
,函数中 this._init
指向的是初始化Vue时候的 _init function
在将 F.prototype
指向 Object.create(Super.prototype)
,这样可以继承 Vue 本身原型上的一些方法,最后 return constructor
。
之后 Vue 还做了缓存处理,所以多次利用 Vue.extend
创建Message-Box、Toast、Message时,并不会影响效率/
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
// cache constructor
cachedCtors[SuperId] = Sub
复制代码
- 总结
深究了一下 Vue.extend
背后的原理,以及如何用它来是实现组件。
参考文章:
往期文章:
- 从零实现Vue的组件库(零)- 基本结构以及构建工具
- 从零实现Vue的组件库(一)- Toast 实现
- 从零实现Vue的组件库(二)- Slider 实现
- 从零实现Vue的组件库(三)- Tabs 实现
- 从零实现Vue的组件库(四)- File-Reader 实现
- 从零实现Vue的组件库(五)- Breadcrumb 实现
- 从零实现Vue的组件库(六)- Hover-Tip 实现
- 从零实现Vue的组件库(七)- Message-Box 实现
- 从零实现Vue的组件库(八)- Input 实现
- 从零实现Vue的组件库(九)- InputNumber 实现
- 从零实现Vue的组件库(十)- Select 实现
- 从零实现Vue的组件库(十一)- Date-picker 实现
- 从零实现Vue的组件库(十二)- Table 实现
- 从零实现Vue的组件库(十三)- Pagination 实现
- 从零实现Vue的组件库(十四)- RadioGroup 实现
原创声明: 该文章为原创文章,转载请注明出处。