Vue3 组件封装进阶,简简单单实现一个弹窗表单组件
在日常开发的过程中通常我们会遇到一个填写表单信息的弹窗,这个时候新手会无所畏惧的直接就将这个弹框写到页面中,稍微有点追求的会将这个弹框封装成一个组件,但是还是会觉得自己的组件封装的不够好,今天我就带大家一起来封装一个相对好用的弹窗表单组件。
1. 弹窗组件封装
通常我们使用弹出框的时候都是有组件库的,我喜欢用的就是element-ui
,所以我们就以element-ui
的Dialog
组件为基础来封装我们的弹窗组件。
在弹窗组件封装中我见过很多小伙伴的封装方案,先来看看他们的封装方案:
1.1 封装方案一
<template>
<el-dialog v-model="visible">
</el-dialog>
</template>
<script setup>
import { ref } from 'vue'
const visible = ref(false)
const open = () => {
visible.value = true
}
const close = () => {
visible.value = false
}
defineExpose({
open,
close
})
</script>
这种代码大家一看就能看明白,我就不写注释了;
然后在父组件中直接使用ref
引用子组件的open
和close
方法就可以实现弹窗的打开和关闭了。
这种封装不能说不好,但是给人的感觉就是不够优雅,而且在父组件中使用的时候也不够方便,需要引用子组件的open
和close
方法。
1.2 封装方案二
<template>
<el-dialog v-model="visible">
</el-dialog>
</template>
<script setup>
import {ref, watch} from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const dialogVisible = ref(false)
watch(
() => props.visible,
(val) => {
dialogVisible.value = val
}
)
</script>
这种方案相对于上面哪一种要稍微优雅一点,可以通过父组件传入的visible
进行控制弹窗组件的开启和关闭;
但是上面这两种方案其实都有很大的缺陷,就是element-ui
的Dialog
组件的控制开启和关闭是通过v-model
来控制的;
这样就会导致一个问题,在组件内部关闭了Dialog
无法通知给父组件,于是我看到了很多小伙伴又对Dialog
组件加上了close
事件的监听,同时还为组件加上了close
的事件;
<template>
<el-dialog v-model="visible" @close="close">
</el-dialog>
</template>
<script setup>
import {ref, watch} from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const dialogVisible = ref(false)
watch(
() => props.visible,
(val) => {
dialogVisible.value = val
}
)
const emits = defineEmits(['close'])
const close = () => {
emits('close')
}
</script>
<!-- 父组件 -->
<template>
<div>
<MyDialog :visible="visible" @close="close"/>
</div>
</template>
<script setup>
import MyDialog from './MyDialog.vue'
import { ref } from 'vue'
const visible = ref(false)
const close = () => {
visible.value = false
}
</script>
真的是非常令人头大的代码,但是由于对Vue
的理解不够,自己也不知道应该怎么优化这个代码才好,所以我今天站出来了;
1.3 我的方案
<template>
<el-dialog v-model="visible">
</el-dialog>
</template>
<script setup>
import {computed} from 'vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['update:modelValue'])
const visible = computed({
get: () => props.modelValue,
set: (val) => {
emits('update:modelValue', val)
}
})
</script>
<!-- 父组件 -->
<template>
<div>
<MyDialog v-model="visible"/>
</div>
</template>
<script setup>
import MyDialog from './MyDialog.vue'
import { ref } from 'vue'
const visible = ref(false)
</script>
这一个基础知识点,v-model
在Vue3
中就是两个东西的组合:
- 组件定义的
modelValue
属性 - 组件定义的
update:modelValue
事件
有了这两个东西,组件就可以使用v-model
来进行双向绑定了;
而且Vue3
还可以支持多个v-model
,只需要在组件中定义多个props
和emits
就可以了;
emits
中的事件名称必须是以update:
开头的,非modelValue
的属性的双向绑定就通过v-model:propName
来进行绑定,这个下面介绍;
2. 表单组件封装
表单的封装那可是大头,不仅是大头,还叫人头大,因为表单是需要数据支持的,这个数据来源肯定是需要父组件传入;
二父组件不管传入的是数据ID
让组件去请求数据,还是直接传入数据
,然后对数据进行一个深拷贝
,再给组件的表单来使用;
这些方法都感觉怪怪的,因为修改的数据还是无法及时的回馈给父组件,都是一种掩耳盗铃的做法;
当然还有很多小伙伴用了一些其他的骚操作实现双向绑定,例如使用全局状态管理器
,或者使用自定义的依赖注入
,或者其他的跨组件通讯等方式;
都能达到效果,都很棒,但是肯定都不是很合理的解法,来看看我的:
<template>
<el-dialog
title="Title"
v-model="dialogVisible"
>
<el-form ref="form" :model="formData" :rules="rules">
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" placeholder="请输入姓名"/>
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="formData.gender">
<el-radio :label="1">男</el-radio>
<el-radio :label="0">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number v-model="formData.age"/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</template>
</el-dialog>
</template>
<script setup>
import {computed, ref} from 'vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
formData: {
type: Object,
default: () => ({})
}
})
const emits = defineEmits([
'update:modelValue',
'update:formData',
'cancel',
'submit'
])
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emits('update:modelValue', value),
})
const rules = {
name: {required: true, message: '请输入名称', trigger: 'blur'}
}
const formData = computed(() => {
return new Proxy(props.formData, {
set(target, prop, newValue) {
emits('update:formData', {
...target,
[prop]: newValue
})
return true;
}
})
})
const handleCancel = () => {
dialogVisible.value = false;
emits('cancel')
}
const form = ref(null)
const handleSubmit = () => {
form.value.validate(valid => {
if (valid) {
emits('submit')
}
})
}
</script>
<!-- 父组件 -->
<template>
<div>
<el-button @click="openDialog">打开弹框</el-button>
<DialogForm
v-model="dialogVisible"
v-model:form-data="formData"
@submit="handleSubmit"
/>
</div>
</template>
<script setup>
import {ref} from 'vue'
import DialogForm from "./components/DialogForm.vue";
const dialogVisible = ref(false)
const openDialog = () => {
dialogVisible.value = true
}
const formData = ref({
name: '田八',
age: 18,
gender: 1
})
const handleSubmit = () => {
console.log(formData.value)
}
</script>
这里核心还是在于计算属性,我使用了两个v-model
,第一个就是控制弹框的,就不多说了;
核心是第二个,核心代码如下:
const formData = computed(() => {
// 使用计算属性返回一个代理对象
return new Proxy(props.formData, {
// 代理对象只需要拦截 set 操作即可
set(target, prop, newValue) {
// 直接提交 自定义 双向绑定事件
emits('update:formData', {
...target,
// 通过展开运算符解构所有对象,然后通过 自定义 属性名覆盖修改之后的属性
[prop]: newValue
})
return true;
}
})
})
核心还是使用了computed
属性,但是这次没有使用computed
属性的set
属性,而是使用了Proxy
的set
拦截器;
这样我们就巧妙的实现了一个双向绑定的表单数据,使用起来效果也是非常的流畅,来看看效果:
指令级别的封装
虽然封装的是挺舒服的,但是还有又小伙伴不太满足,都封装成这样了,页面上还是得插一个标签,太麻烦了,我就像直接使用js
代码来进行打开弹框;
真是一群刁民,不过我喜欢,有钻研精神肯定是好的,那就再套一层封装让大家舒服;
但是这种方式进行封装还是比较麻烦的,可能是我技术还不到家,但是我功能可以实现呀,看代码:
<template>
<el-dialog
title="Title"
v-bind="$attrs"
v-model="dialogVisible"
>
<el-form ref="form" :model="formData" :rules="rules">
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" placeholder="请输入姓名"/>
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="formData.gender">
<el-radio :label="1">男</el-radio>
<el-radio :label="0">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number v-model="formData.age"/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</template>
</el-dialog>
</template>
<script setup>
import {ref} from 'vue'
import {
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElRadioGroup,
ElRadio,
ElInputNumber,
ElButton
} from 'element-plus'
const props = defineProps({
formData: {
type: Object,
default: () => ({})
}
})
const emits = defineEmits([
'cancel',
'submit'
])
const dialogVisible = ref(false)
const rules = {
name: {required: true, message: '请输入名称', trigger: 'blur'}
}
const formData = ref({
...props.formData
});
const handleCancel = () => {
dialogVisible.value = false;
emits('cancel')
}
const form = ref(null)
const handleSubmit = () => {
form.value.validate(valid => {
if (valid) {
emits('submit', formData.value)
}
})
}
</script>
首先可以看到的是组件的代码所有的状态现在都是内部维护了,因为通过指令打开的方式,参数都是传入的,无法响应式,只能在组件内部进行响应式;
其次就是组件中使用的组件都需要在组件的内部注册一次才能正常使用;
完成这个之后,还需要封装一层指令级别的代码:
import { h, render } from 'vue'
import Dialog from './DialogForm.vue'
const divDom = document.createElement('div')
document.body.appendChild(divDom);
const dialog = (option) => {
return new Promise((resolve, reject) => {
const onSubmit = (data) => {
render(null, divDom)
resolve(data)
}
const onCancel = () => {
render(null, divDom)
reject(new Error('取消'))
}
const vNode = h(Dialog, {
...option,
modelValue: true,
onSubmit,
onCancel,
onClose: onCancel
})
render(vNode, divDom)
})
}
export default dialog
这里使用了Vue
内部提供的两个函数,一个是h
函数,一个是render
函数,然后还需要定义一个挂载的容器;
然后内部返回一个Promise
,对应的用于控制组件的开启关闭的回调,具体的可以看代码中写的注释,这里就不多说了;
在父组件中使用也很简单,如下:
<template>
<div>
<el-button type="primary" @click="openDialog">打开弹框</el-button>
</div>
</template>
<script setup>
import {ref} from 'vue'
import dialog from "./components/DialogForm.js";
const formData = ref({
name: '田八',
age: 18,
gender: 1
})
const openDialog = () => {
dialog({
formData: formData.value,
}).then((data) => {
console.log(data)
})
}
</script>
来看看效果:
总结
本篇使用了Vue3
封装了一个完整的弹窗表单组件,同时提供了多种方式进行封装,其中使用了很多技巧和知识点;
但是这还只是一个很浅层的封装,并没有用很多的技术,只是多用了几个API,而且并没有使用插槽等其他的语法,只是将原本单文件中的内容进行了抽离;
组件封装远不止于此,还有很多技巧和规范,学无止境。
转载自:https://juejin.cn/post/7275230782545199141