目录
组件的封装我认为要遵循的原则
- 不要使用过多的prop
- 不要轻易更新v-model的值
- watch方法中的内容尽量抽离到具体click或者callback业务上,否则代码多的情况不容易定位是哪里触发了watch方法
- 减少使用watch prop——监听外部传递的prop改变而触发内部代码更新的操作
- 组件抽离尽量精简,业务抽离得彻底,避免父组件反复调用子组件的方法
开发过程中发现一个比较有争议的地方
项目中使用element-ui,经常用到el-dialog组件
那么如果要把弹出对话框的内容封装为一个组件的话,有两种情况
- el-dialog 写在组件外,由父组件控制显示隐藏
- el-dialog 写在组件内部,由组件自己维护对话框显示隐藏
本次,就这两种差异发表个人浅见,讨论那种方式更好
Dialog显隐在组件外维护
<!-- 父组件 -->
<template>
<div>
<el-dialog :visible.sync="showDialog"> <!-- el-dialog写在组件外 -->
<my-component ref="myComponent" @close="showDialog = false"/>
</el-dialog>
</div>
</template>
<script>
import myComponent from '@/myComponent';
export default {
component: { myComponent }
data(){
return {
showDialog: false
}
}
}
</script>
<!-- 子组件 -->
<template>
<div>
<button @click="handleClose">close</button>
</div>
</template>
<script>
export default {
methods: {
handleClick(){
this.$emit('close'); // 子组件点击关闭按钮,通知父组件关闭弹框
}
}
}
</script>
父组件定义data变量,控制dialog对话框的显隐。而对话框的点击的取消事件也需要通过@方法通知父组件更新显隐状态。
当然,也可以使用v-model 或者:prop.sync的方式,使子组件也可以有改变父组件变量的能力,这样也可以,不过底层实现还是通过@的方式,由子组件通知父组件更新变量,看起来像是数据的反向流动。一般还是建议数据单向流动,减少子组件对父组件变量的修改,尤其是复杂组件。
好处:
而这种方式的好处在于,开发父组件时,可以在使用时决定子组件是否需要弹框,需要弹框就套上弹框代码。适用于偶尔要把这个组件做成弹框的情况。
问题:
对话框状态由父组件控制的情况,少量的话还好,多的时候父组件就难免在data中定义更多“show”的变量,这些变量也会在代码的不同角落被修改,增加了代码耦合度。
Dialog 显隐在组件内维护
<!--父组件-->
<template>
<div>
<my-component ref="myComponent" @confirm="(val) => {}"/> <!-- 确定事件将结果通过@传递出来 -->
</div>
</template>
<script>
import myComponent from '@/myComponent';
export default {
component: { myComponent }
data() {},
mounted(){
this.$refs.myComponent.show(); // 通过调用子组件方法这打开对话框,这个方法可以传递一些初始参数,减少prop的使用
this.$refs.myComponent.showDialog = true; // 也可以直接更改子组件showDialog的值(不推荐)
}
}
</script>
<!--子组件-->
<template>
<el-dialog :visible.sync="showDialog" @open="handleOpen"> <!-- 子组件维护对话框显隐 -->
<div>hello world ...</div>
<button @click="showDialog = false">Close</button>
<button @click="$emit('confirm','123123')">Confirm</button>
</el-dialog>
</template>
<script>
export default {
data() {
return {
showDialog: false
}
},
methods: {
show(){
this.showDialog = true;
this.$nextTick(()=> {
// this.$refs.table.query() // el-dialog懒渲染,需要使用nextTick初始化数据
});
},
handleOpen(){ // 由于el-dialog懒渲染,或使用官方提供的open回调
//this.$refs.table.query() // 这样,第一次打开就能获取到this.$refs.的内容了
}
}
}
</script>
个人偏向于写在组件内部,原因是作为对话框弹出时,用户一般是操作不到父组件的内容的,实际上父组件一般只需要一个“打开”对话框的操作,子组件将对话框中的结果传回父组件,关闭对话框的业务逻辑也一般在对话框内进行。
父组件通过调用子组件的show()方法这打开对话框,这个方法也可以传递一些初始参数,以减少prop的使用。
因为我发现在打开对话框时,向组件传递的部分参数,自打开对话框后,父组件一般就不更新这些参数了,不一定要使用prop来传递。
优点:
在抽取这部分参数放到(比如show)方法中传入后,父组件也往往会减少一些data参数的定义(控制show的变量和组件初始化的参数),让父组件业务更加清晰。
问题:
- 使用组件时,显示对话框的入口函数规范问题,而且由于对话框写在组件内部,使用时不可去掉对话框。
- el-dialog懒渲染问题,第一次打开时才开始渲染slot中的内容,this.$refs会找不到,解决:1.使用this.$nextTick 2.使用官网的,在el-dialog中@open/@opened回调中操作DOM
同时使用多个Dialog时的代码风格对比
el-dialog写在父组件
<!-- 对话框状态在父组件维护-->
<template>
<div>
<el-dialog :show.aync="show.comp1">
<my-component1
:param="param1"
@close="handleClose1"
@confirm="handleConfirm1"
/>
</el-dialog>
<el-dialog :show.aync="show.comp2">
<my-component2
:param="param2"
@close="handleClose2"
@confirm="handleConfirm2"
/>
</el-dialog>
<el-dialog :show.aync="show.comp1">
<my-component3
:param="param3"
@close="handleClose3"
@confirm="handleConfirm3"
/>
</el-dialog>
</div>
</template>
<script>
import myComponent1 from '@/myComponent1';
import myComponent2 from '@/myComponent2';
import myComponent3 from '@/myComponent3';
export default {
component: { myComponent1,myComponent2,myComponent3 }
data() {
return {
show: {
comp1: false,
comp2: false,
comp3: false,
},
param1: {
key: 1,
val: 2
},
param2: {
},
param3: {
},
}
},
mounted(){
this.show.comp1 = true;
},
methods: {
handleClose1(){
this.show.comp1 = false;
},
handleClose2(){
this.show.comp2 = false;
},
handleClose3(){
this.show.comp3 = false;
},
handleConfirm1(){
this.show.comp1 = false;
this.show.comp2 = true;
this.param2 = {
key: 'key2',
val: 'val2',
}
}
handleConfirm2(){
this.show.comp2 = false;
this.show.comp3 = true;
this.param3 = {
key: 'key3',
val: 'val3',
}
}
handleConfirm3(){
this.show.comp3 = false;
console.log('确认3');
}
}
}
</script>
父组件代码量显著增多,尤其是有关控制显隐的代码。
el-dialog写在子组件
<!-- 对话框状态在子组件维护 -->
<template>
<div>
<my-component1 ref="myComponent1" @confirm="handleConfirm1"/>
<my-component2 ref="myComponent2" @confirm="handleConfirm2"/>
<my-component3 ref="myComponent3" @confirm="handleConfirm3"/>
</div>
</template>
<script>
import myComponent1 from '@/myComponent1';
import myComponent2 from '@/myComponent2';
import myComponent3 from '@/myComponent3';
export default {
component: { myComponent1,myComponent2,myComponent3 }
data() {
return {
}
},
mounted(){
this.$refs.myComponent1.show(); // 显示对话框1
},
methods: {
// 处理组件1确认事件
handleConfirm1(){
let param = { // 也可以传递参数到子组件
key: 'key2',
val: 'val2'
}
this.$refs.myComponent2.show(param); // 显示对话框2
},
// 处理组件2确认事件
hanldeConfirm2(){
let param = { // 也可以传递参数到子组件
key: 'key3',
val: 'val3'
}
this.$refs.myComponent3.show(param); // 显示对话框3
},
// 处理组件3确认事件
hanldeConfirm3(){
console.log('组件3确认');
}
}
}
</script>
组件内的变量组件内部维护,通过内部方法来控制内部变量的改变(show变量),有点借鉴于JAVA面向对象的编程思想。
这种风格也有点像jQuery了,更加直观。
可复用确认组件
<!--父组件-->
<template>
<div>
<my-component ref="myComponent" />
</div>
</template>
<script>
import myComponent from '@/myComponent';
export default {
component: { myComponent },
mounted(){
// 通过调用子组件方法这打开对话框,这个方法可以传递一些初始参数,减少prop的使用
this.$refs.myComponent.show().then(() => {
// 点击确定回调
}).catch(() => {
// 点击取消回调
});
}
}
</script>
<!--子组件-->
<template>
<el-dialog :visible.sync="showDialog"> <!-- 子组件维护对话框显隐 -->
<div>hello world ...</div>
<button @click="onClose">Close</button>
<button @click="onConfirm">Confirm</button>
</el-dialog>
</template>
<script>
export default {
data() {
return {
showDialog: false,
resolve: null,
reject: null
}
},
methods: {
show(){
this.showDialog = true;
return Promise((resolve,reject) => {
this.resolve =resolve
this.reject =reject
})
},
onConfirm(){
this.showDialog = false
this.resolve()
},
onClose(){
this.showDialog = false
this.reject()
}
}
}
</script>
通过show方法抛出Promise对象,并保存期resolve,reject方法,并在对话框中的确认,取消事件中执行。这样在父元素就可以通过then链判断对话框是否确认和关闭
封装包
类似ui库一样,提供install方法。这样式样上就类似ui库一样,Vue.use的方式来使用。
import Dialog from 'xxx.vue';
export default function(Vue){
Vue.component(Dialog.name, Dialog);
}
为了便于按需加载组件,也需要将原vue组件代码导出。类似 下面两种方式。
方式1
export { default as Dialog } from 'xxx.vue';
方式2
import Dialog from 'xxx.vue';
export default {
component: Dialog,
install(Vue){
Vue.component(Dialog.name,Dialog);
}
}
这样就可以直接获取到组件了。
小结
显然对话框写在子组件内部具有更好的可读性和更低的代码耦合度,使父组件的开发更聚焦于业务逻辑。这也使对话框与子组件业务绑定,无法选择去除对话框。子组件内容初始化需要考虑el-dialog的懒渲染。
对话框由父组件控制则在开发过程中更方便选择是否使用对话框,代价是更高的代码耦合。
以上是一点关于vue对话框的一些小总结,还未完全钻研透彻。比如使用:prop.sync传递参数,控制对话框显示与否,这样也可以在一定程度上减少父组件代码量。
若有错误,敬请指出更正。