从零开始实现一个基于Vue的MessageBox组件
不知平时用惯了组件库的小伙伴们会不会好奇那些通用组件到底是如何实现的,本文将从零实现一个 MessageBox 消息盒子组件,组件用 vue2 语法写的,不过框架都是一通百通,我相信当你熟知了其实现原理,用任何框架都可以信手拈来!
组件演示及文档地址:wangjunjie000.github.io/jj-ui/#/com…
github地址:github.com/wangjunjie0…
请多多支持!
前言
众所周知,MessageBox 组件是 Web 端项目中经常要用到的组件,ElementUI 组件库中也有此组件,为了熟知其实现原理,以及尽可能的定制化,所以花了点时间写了一个。项目使用的vue版本为 2.6.10,vue-cli版本为 3.12.1,node版本为 14.17.5。因本人能力水平有限,如有错误和建议,欢迎在评论区指出。若本篇文章有帮助到了您,不要吝啬您的小手还请点个赞再走哦!
1 前置知识
Vue.extend() 方法
可能有一些小伙伴对于 Vue.extend() 方法不是很熟悉,这里就先介绍一下吧。
Vue.extend( options ) 是 Vue.js 中的一个全局API,用于创建可复用的组件构造器。该方法的作用是基于传入的选项对象 options 创建一个组件构造器,并返回该构造器。该构造器可以使用 new
关键字创建新的 Vue 实例,相当于是一个组件基类,通过不同的参数和选项来定制组件的行为。
下面是 Vue.extend()
方法的参数选项:
options
: 一个对象,用于配置组件参数选项。其中,包括组件的模板、数据、方法、生命周期钩子等。
下面是一个简单的示例:
// 创建一个名为 test-component 的组件构造器
const TestComponent = Vue.extend({
template: '<div>Hello, {{message}}</div>',
data: function () {
return {
message: 'World'
}
}
})
// 创建一个新的 TestComponent 实例并挂载到指定元素中
new TestComponent().$mount('#app')
在上述示例中,首先通过调用 Vue.extend()
方法创建了一个名为 TestComponent
的组件构造器,该构造器的选项包括 template
和 data
。接着,通过调用 new TestComponent()
创建了一个新的 Vue 实例,并使用 $mount('#app')
方法将该实例挂载到指定的元素中。这将会在 id
为 app
的元素中显示一个文字 Hello, World
。
当然 options 中也能传入一个单文件组件,这里就以本文中要实现的 MessageBox 为例再说明一下:
import Vue from 'vue'
import msgBoxVue from './message-box.vue'
const MessageBoxConstructor = Vue.extend(msgBoxVue)
const MessageBox = (content = '', options) => {
if (!options) {
options = {
content: content,
}
} else if (typeof options === 'object') {
options = Object.assign(
{
content: content,
},
options
)
}
const instance = new MessageBoxConstructor({
data: options,
}).$mount()
document.body.appendChild(instance.$el)
return instance.showConfirm()
}
export default MessageBox
在使用 Vue.extend()
方法创建组件构造器 MessageBoxConstructor
之后,可以通过 new
关键字实例化出不同的 Vue 实例来使用该组件。在 new
实例化时,可以将传递参数作为选项传入构造函数中。上方代码通过 new MessageBoxConstructor(options)
对象来实例化组件时,传入了 {data: options}
,组件构造器中的 data()
函数返回的对象属性会与传递的 options.data
对象属性进行合并,规则如下:
- 以组件数据为基础,在
options.data
中新增属性,原来的属性不会受影响。 - 如果在
options.data
中定义了与组件本身相同的属性名,则以options.data
中的值为准,会覆盖组件本身的属性值。
那么上方 new MessageBoxConstructor(options).mount()会返回什么东西呢?可以这样理解:如果mount() 会返回什么东西呢?可以这样理解:如果 mount()会返回什么东西呢?可以这样理解:如果mount() 没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素 (可以理解为未挂载状态的vue实例对象,这里赋值给了instance
) ,通过instance.$el
可以获取到该 vue 实例对应的 DOM 对象,最后再通过 document.body.appendChild(instance.$el)
将其追加到到 body 元素中就行了。
2 组件代码
packages\message-box\src\message-box.vue
<template>
<transition name="fade">
<div
class="message_box_wrapper"
:class="`message_box_wrapper${_uid}`"
v-if="visible"
:style="{ zIndex: $JJUI.zIndex }"
@click.self="handleWrapperClick"
>
<div class="message_box">
<div class="header">
<span class="title">{{ title }}</span>
<i
class="jj-x icon"
@click="handleAction('cancel')"
v-if="showClose"
></i>
</div>
<div class="body">
<i
class="box_status"
:class="[boxStatusClass]"
v-if="boxStatusClass"
:style="iconStyle"
></i>
<div class="content">
{{ content }}
</div>
</div>
<div class="btn_group">
<jj-button
size="small"
@click="handleAction('cancel')"
v-if="!alert"
>{{ cancelBtnText }}</jj-button
>
<jj-button
ref="confirmBtn"
size="small"
type="primary"
@click="handleAction('confirm')"
>{{ confirmBtnText }}</jj-button
>
</div>
</div>
</div>
</transition>
</template>
<script>
/**
* 基础选择组件,点击后从下方弹出
* @property {String} title 标题
* @property {String} content 内容
* @property {String} confirmBtnText 确认按钮文字,默认‘确定’
* @property {String} cancelBtnText 取消按钮文字,默认‘取消’
* @property {String} type success / info / warning / error
* @property {String} iconClass 自定义图标的类名,会覆盖 type
* @property {Function} callback 若不使用 Promise,可以使用此参数指定 MessageBox 关闭后的回调
* @property {Boolean} showClose MessageBox 是否显示右上角关闭按钮
*/
export default {
data() {
return {
visible: false,
title: '提示',
content: '您确定要删除吗',
confirmBtnText: '确定',
cancelBtnText: '取消',
type: '',
iconClass: '',
showClose: true,
promiseStatus: null, // 没有传 callback 的时候返回promise
callback: null, // 传了 callback 函数时给它赋值
alert: false, // 是否为alert提示框,为true时没有取消按钮,且不能点击遮罩层关闭
}
},
computed: {
boxStatusClass() {
// 传了 iconClass 时覆盖掉 type
if (this.iconClass) {
return this.iconClass
}
const classList = {
warning: 'jj-exclamation-circle-fill',
success: 'jj-check-circle-fill',
info: 'jj-info-circle-fill',
error: 'jj-x-circle-fill',
}
if (classList[this.type]) {
return classList[this.type]
}
return ''
},
iconStyle() {
const colorList = {
warning: '#e6a23c',
success: '#67c23a',
error: '#F56C6C',
info: '#909399',
}
const color = colorList[this.type]
if (color && !this.iconClass) {
return {
color,
}
}
return null
},
},
methods: {
// 普通用法
handleAction(active) {
// 点击确定
if (active === 'confirm') {
// 传了回调函数,非 promise 模式点击确定时
if (this.callback) {
this.callback()
// promise 模式点击确定时
} else {
this.promiseStatus && this.promiseStatus.resolve()
}
this.handleClose()
// 点击取消
} else {
// 传了回调函数,非 promise 模式点击确取消时
if (this.callback) {
this.callback()
// promise 模式点击确定时
} else {
this.promiseStatus && this.promiseStatus.reject()
}
this.handleClose()
}
},
handleClose() {
this.visible = false
this.$modal({ show: false })
},
showConfirm() {
this.visible = true
this.$modal({ show: true, zIndex: this.$JJUI.zIndex - 1 })
this.$nextTick(() => {
this.$refs.confirmBtn.$el.focus()
})
if (!this.callback) {
return new Promise((resolve, reject) => {
this.promiseStatus = { resolve, reject }
})
}
},
handleWrapperClick() {
if (this.alert) {
return
}
this.handleClose()
},
},
}
</script>
<style lang="scss">
.message_box_wrapper {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: auto;
z-index: 2001;
user-select: none;
.message_box {
color: #555;
width: 400px;
position: absolute;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
border-radius: 5px;
background-color: #fff;
padding-bottom: 10px;
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 15px 10px;
.title {
font-size: 18px;
}
.icon {
font-size: 24px;
cursor: pointer;
}
}
.body {
padding: 10px 15px;
font-size: 14px;
display: flex;
align-items: center;
.box_status {
font-size: 24px;
margin-right: 12px;
}
}
.btn_group {
padding: 5px 15px 0;
text-align: right;
}
}
}
// 自定义弹框淡入淡出效果
.fade-enter,
.fade-leave-to {
opacity: 0;
transform: translate3d(0, -20px, 0);
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s;
}
</style>
packages\message-box\src\main.js
import Vue from 'vue'
import msgBoxVue from './message-box.vue'
const MessageBoxConstructor = Vue.extend(msgBoxVue)
/**
* @param {String} content 必传。确认弹窗的提示文本
* @param {Object} options 可选。传递的组件选项对象,和 MessageBoxConstructor 组件类中的选项对象合并
规则:
1:options.data 中新增属性,原来的属性不会受影响。
2:options定义了与 组件类相同的属性名时,以 options.data 中的为准,会覆盖组件本身的属性值。
* @returns
*/
const MessageBox = (content = '', options) => {
if (!options) {
options = {
content: content,
}
} else if (typeof options === 'object') {
options = Object.assign(
{
content: content,
},
options
)
}
const instance = new MessageBoxConstructor({
data: options,
}).$mount()
document.body.appendChild(instance.$el)
return instance.showConfirm()
}
export default MessageBox
packages\message-box\index.js :导出组件
// MessageBox 为函数式组件,这里直接导出 main.js 中的内容
import MessageBox from './src/main.js'
export default MessageBox
packages\index.js: 挂载函数式组件
import MessageBox from './message-box'
const install = function (Vue, options = {}) {
...
Vue.prototype.$msgbox = MessageBox;
...
}
export default {
...
install,
MessageBox,
...
}
main.js :注册组件
import JjUI from '../packages'
Vue.use(JjUI)
这里再说明一下,Vue.use(JjUI) 时发生了什么?JjUI 为 packages\index.js
中导出的对象,它包含了 install 和 MessageBox 方法。Vue.use 时会调用对象的 install 方法,从而执行 Vue.prototype.$msgbox = MessageBox;
将函数式组件 MessageBox
挂载到 Vue 原型上,后续就可以通过调用 this.$msgbox(options)
方法来弹出 MessageBox 组件了。
往期文章回顾:
转载自:https://juejin.cn/post/7234803427041099834