前言
现在组件化已经是很平常了,市面也有很多的UI库,基本组件的轮子都可以直接使用,但是如果想提升自己的技术水平,学会如何正确的组件封装是必不可少的路,今天和大家聊聊基于 vue3 如何去封装一个组件, 我们自己动手去封装一个消息弹窗 & 模态框, 这里贴一个代码源码以及NPM地址,大家可以自行去阅读源码或者直接安装使用我的轮子,ok,闲言少叙,直接开始
怎么封装
写代码之前一定要先明确思路,如何明确思路呢,我们应该站在使用者角度去,先把怎么用明确了,再确定怎么实现, 从ui角度考虑,其实消息弹窗和模态框应该都是一样的,但是,从使用者角度,用法肯定是不一样的,模态框的用法应该是组件式的, 因为可能需要自定义内容,但是消息弹窗肯定是指令式的,我通过函数,传参即可打开,那么基于我们的思路开始实现我们的 my-message-box
模态框实现
既然消息弹窗和模态框都是一样的, 我们就先从模态框开始封装,这个其实比较简单,模态框分三个部分,header,main, footer, 分别对应标题,内容,操作按钮,其中我们的main和footer直接就做成插槽,当然, 也需要写默认的内容:
typescript
<script setup lang="ts">
import { withDefaults } from 'vue'
import close from './close.png'
const props = withDefaults(
defineProps<{
modelValue: boolean
title?: string
content?: string
closeBtnText?: string
confirmBtnText?: string
isShowConfirmBtn?: boolean
isShowCloseBtn?: boolean
closeOnOther?: boolean
}>(),
{
closeBtnText: '取 消',
confirmBtnText: '确 认',
isShowConfirmBtn: true,
isShowCloseBtn: true,
isDeclarative: false,
closeOnOther: true
}
)
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', onClose: Function): void
(e: 'close'): void
}>()
function onConfirm() {
emit('confirm', onClose)
}
function onClose() {
emit('update:modelValue', false)
emit('close')
}
function onCloseOnOther() {
if (props.closeOnOther) {
onClose()
}
}
defineExpose()
</script>
<template>
<div class="--sk-wrapper" v-if="modelValue" @click.self="onCloseOnOther">
<div class="--sk-wrapper_body">
<div class="--sk-wrapper_title">
<span>{{ title }}</span>
<img
v-if="closeOnOther"
@click="onClose"
class="--sk-wrapper_title_close_icon"
:src="close"
alt="x"
/>
</div>
<div class="--sk-wrapper_content">
<slot name="content">
{{ content }}
</slot>
</div>
<div class="--sk-wrapper_footer">
<slot name="footer">
<div class="--sk-wrapper_footer_button_box">
<div
v-if="isShowConfirmBtn"
class="--sk-wrapper_footer_button __primary"
@click="onConfirm"
>
{{ confirmBtnText }}
</div>
<div
v-if="isShowCloseBtn"
class="--sk-wrapper_footer_button"
@click="onClose"
>
{{ closeBtnText }}
</div>
</div>
</slot>
</div>
</div>
</div>
</template>
<style scoped>
@import './dialog.css';
</style>
代码呢大概就长这样, css可以自行实现,这里指的一提的是最后的defineExpose()
, 在我们实现组件库的时候 如果使用了 setup 语法糖, 一定要加上expose, 避免使用者通过 ref 来随意访问我们组件内的内容,从而造成超出意外的调用。那么到这里我们模态框就写好了,基本没什么难度, 接下来有两个重点,如何指令式调用它,以及如何正确的导出组件式调用
指令式调用
在 my-message-box 文件夹下, 我们创建一个 index.ts, 这个文件需要导出一个方法用于指令式调用,同时,还需要导出这个组件,让使用者通过组件式来调用,一个个的来实现,首先实现正确的导出指令,我们需要导出一个方法,里面包含了 alert
和 confirm
, 一个只有确认按钮,一个带有确认和取消,两个方法都是返回 promise,来支持回调, 由于我们通过createApp实现,无法传递emit事件,Dialog组件是通过 v-model 来实现的双向绑定,所以我们只能通过props 来传递这些函数, 这也是我们为什么要说重要的是 正确的导出组件式调用,也是为了避免使用者使用组件式使用组件时传递了指令式的props造成超出预期的结果: 我们再dialog.vue中加入这三个props,并在对应的emit事件中通过判断isDeclarative参数判断这是否为指令式调用,从而实现规避,从而我们的 Dialog组件就改成了这样:
typescript
<script setup lang="ts">
import { withDefaults } from 'vue'
import close from './close.png'
const props = withDefaults(
defineProps<{
modelValue: boolean
title?: string
content?: string
closeBtnText?: string
confirmBtnText?: string
isShowConfirmBtn?: boolean
isShowCloseBtn?: boolean
closeOnOther?: boolean
/***
* 以下为非组件的Props, 仅提供给指令式
*/
closeFn?: Function
confirmFn?: Function
isDeclarative?: boolean
}>(),
{
closeBtnText: '取 消',
confirmBtnText: '确 认',
isShowConfirmBtn: true,
isShowCloseBtn: true,
isDeclarative: false,
closeOnOther: true
}
)
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', onClose: Function): void
(e: 'close'): void
}>()
function isRunCloseFn() {
if (props.isDeclarative) {
if (props.closeFn && typeof props.closeFn === 'function') {
props.closeFn()
return false
}
}
return true
}
function isRunConfirmFn() {
if (props.isDeclarative) {
if (props.confirmFn && typeof props.confirmFn === 'function') {
props.confirmFn(props.closeFn!)
return false
}
}
return true
}
function onConfirm() {
if (isRunConfirmFn()) {
emit('confirm', onClose)
}
}
function onClose() {
if (isRunCloseFn()) {
emit('update:modelValue', false)
emit('close')
}
}
function onCloseOnOther() {
if (props.closeOnOther) {
onClose()
}
}
defineExpose()
</script>
<template>
<div class="--sk-wrapper" v-if="modelValue" @click.self="onCloseOnOther">
<div class="--sk-wrapper_body">
<div class="--sk-wrapper_title">
<span>{{ title }}</span>
<img
v-if="closeOnOther"
@click="onClose"
class="--sk-wrapper_title_close_icon"
:src="close"
alt="x"
/>
</div>
<div class="--sk-wrapper_content">
<slot name="content">
{{ content }}
</slot>
</div>
<div class="--sk-wrapper_footer">
<slot name="footer">
<div class="--sk-wrapper_footer_button_box">
<div
v-if="isShowConfirmBtn"
class="--sk-wrapper_footer_button __primary"
@click="onConfirm"
>
{{ confirmBtnText }}
</div>
<div
v-if="isShowCloseBtn"
class="--sk-wrapper_footer_button"
@click="onClose"
>
{{ closeBtnText }}
</div>
</div>
</slot>
</div>
</div>
</div>
</template>
<style scoped>
@import './dialog.css';
</style>
然后我们把实现的方法写入:
typescript
import { createApp, h } from 'vue'
import Dialog from './Dialog.vue'
interface MessageBox {
title?: string
content?: string
closeBtnText?: string
confirmBtnText?: string
}
function MessageBox() {
return {
alert(props: MessageBox) {
return new Promise((resolve) => {
const container = document.createElement('div')
const messageBox = createApp(Dialog, {
...props,
modelValue: true,
confirmFn: onClose,
closeFn: onClose,
isDeclarative: true,
isShowCloseBtn: false
})
messageBox.mount(container)
document.body.appendChild(container)
function onClose() {
resolve(undefined)
messageBox.unmount()
document.body.removeChild(container)
}
})
},
confirm(props: MessageBox) {
return new Promise((resolve, reject) => {
const container = document.createElement('div')
const messageBox = createApp(Dialog, {
...props,
modelValue: true,
closeFn: onClose,
confirmFn: onConfirm,
isDeclarative: true
})
messageBox.mount(container)
document.body.appendChild(container)
function onClose() {
reject(undefined)
messageBox.unmount()
document.body.removeChild(container)
}
function onConfirm() {
resolve(undefined)
messageBox.unmount()
document.body.removeChild(container)
}
})
}
}
}
const useMessageBox = MessageBox()
export { useMessageBox }
接着就是需要导出 Dialog 组件了,这个组件我们之前提到了,需要过滤掉指令调用的props避免造成问题,所以我们需要给他包一层,实现代码就是下面这样,通过 defineComponent
和 h
函数来过滤掉props
typescript
import Dialog from './Dialog.vue'
const MessageBox = defineComponent({
props: {
modelValue: Boolean,
title: String,
content: String,
closeBtnText: String,
confirmBtnText: String,
isShowConfirmBtn: {
type: Boolean,
default: () => true
},
isShowCloseBtn: {
type: Boolean,
default: () => true
}
},
render() {
return h(
Dialog,
{
...this.$props,
'onUpdate:modelValue': (value: boolean) => this.$emit('update:modelValue', value)
},
this.$slots
)
}
})
export { MessageBox }
通过 defineComponent 来导出这个组件,组件内容通过vue3的 h 渲染函数,这里注意, 由于组件调用是通过 v-model 来实现的开关,那么我们需要把onUpdate:modelValue
写到h函数的第二个参数内,第三个参数的slot也不能忘了,因为我们现在导出的并不是 Dialog 组件,而是经过包装的 MessageBox 对象,所以要把参数插槽都给传递过去
文末
至此我们的轮子就造好了, 今天主要分享的是组件的实现思路以及一些注意的要点, 通过 消息弹窗 和 模态框 来举例,如果你看完相信你一定是有收获的, 最后关于造轮子,想提升自己的开发水平其实造轮子是个很好的办法,但是很多人不太会,包括如何打包,打包的配置,工程化,以及发布npm啥的,我看好像也没有文章说的特别清楚,因为这里面的点还蛮多,如果有想自己造轮子发布到npm的,可以在这里留言,如果想看的人多,我后面可能会开一个专栏写一些关于如何造轮子发布到npm