让你明明白白学知识,有代码,有讲解,抄的走,学的会!
假设你正在使用 iview , 如果你一个页面的业务稍微复杂一点,一定会采到 iview Modal组件的坑
废话不多说, 直接上场景
场景
页面A, 有很多内容是以弹窗的形式展现, 我特意画了一个稍微恶心一点的场景, 1-9 都是你要以Modal形式展示的 模态框中的内容,以及确定按钮, 取消按钮, 还有模态框右上角的 关闭各种事件都是不一样的, 总之,就是告诉你, 你就是要写9个模态框,表现你的业务场景
一般写 vue ,我们知道要将一些逻辑抽离成组件,这样解构清晰,代码可维护度高, 那么 iview 中的Modal坑就在于 右上角那个关闭按钮
下面的代码 父组件没啥变化, 注意子组件
代码
父组件 parent.vue
<template>
<div>
<h3>父组件</h3>
<Button @click='showModal = !showModal'>打开</Button>
<son @closeModal='closeModal' :isShow='showModal' :id='id'></son>
</div>
</template>
<script>
import son from './son'
export default {
components: {
son
},
data () {
return {
showModal: false,
id: 0
}
},
methods: {
closeModal () {
// 模拟不同业务ID, 作为弹窗页面调用接口的入参
this.id = Date.now()
this.showModal = false
}
}
}
</script>
通常,有人会写下面的几种错误的
子组件 - 错误写法 -1
<template>
<Modal title='弹窗' v-model='isShow' @on-cancel='close' @on-ok='handleOk'>
你好
</Modal>
</template>
<script>
export default {
name: 'son',
props: {
isShow: {
type: Boolean
}
},
methods: {
close () {
// 一些和业务相关的代码 ....
this.$emit('closeModal')
},
handleOk () {
// 一些和业务相关的代码 ....
this.$emit('closeModal')
}
}
}
</script>
点击,确定按钮, 取消按钮, 右上角关闭按钮
感觉写法正常, 数据从父组件传递给子组件, 子组件通过 $emit 去修改父组件的数据
可控制台就是报下面的错误 很明显, 告诉你,你违背了 vue 中 ‘单向数据流’的思想, 有人说,我明明是 @on-cancel, @on-ok 都监听了啊, 怎么还会这样
原因如下:
iview 这2个事件, 在你点击了 确定 / 取消 按钮, Son组件是先将 isShow 改变了,再执行你传递的 2个事件对应的处理函数
那是不是说,子组件自己改变了 props数据的值, props只能通过 父组件去改, 这就违背了 ‘单向数据流’
这就是iview的坑, 有人就说了, 那我不按照上面的写法,我怎么去关闭模态框
就衍生出下面的第二个坑
子组件 - 错误写法 - 2
<template>
<Modal title='弹窗' v-model='isShow'>
你好
<div slot='footer'>
<Button @click='close'>取消</Button>
<Button @click='handleOk'>确定</Button>
</div>
</Modal>
</template>
<script>
export default {
name: 'son',
props: {
isShow: {
type: Boolean
}
},
methods: {
close () {
// 一些和业务相关的代码 ....
this.$emit('closeModal')
},
handleOk () {
// 一些和业务相关的代码 ....
this.$emit('closeModal')
}
}
}
</script>
那我 slot 写法, 不让iview 自己去管理 确定按钮和取消按钮的逻辑, 我自己来,不就解决了
点击确定按钮,点击取消按钮, 妥妥的, 是按照 ‘单向数据流’ 规则来的, 点击右上角 × ,哦吼! 还是报警告, 什么鬼
没错。 右上角那个 × 还是 iview 自己在管理 Modal的状态值, 也就是 Son 组件自己在修改 isShow 的值,就不行
有人说, 我去监听 @on-cancel 去补救一下, 不就行了, 实际上,我上面 错误示例1 已经解释清楚了, iview 是先改了isShow 的值, 再去执行你 @on-cancel 的回调函数, 所以, 仍旧会错
子组件 - 错误写法 -3
既然上面的那个 Modal的 v-model 值这么麻烦,还有坑,那我直接将 Modal写在父组件里,不就行了,然后Modal中的内容,通过引入组件的形式去完成, 这样可以了吧!
告诉你, 不行
Parent.vue
<template>
<div>
<h3>父组件</h3>
<Button @click='showModal = !showModal'>打开</Button>
<div v-if='showModal'>
<Modal v-model='showModal' title='演示'>
<!-- son这个就是你剥离出去的业务代码内容 -->
<son></son>
</Modal>
</div>
</div>
</template>
<script>
import son from './son'
export default {
components: {
son
},
data () {
return {
showModal: false
}
},
methods: {
closeModal () {
this.showModal = false
}
}
}
</script>
上面的写法,是将 Modal的状态提取到父组件中了, 那么是不是说,在 parent.vue中, 是自己在修改 Modal的状态,就不存在什么问题了, 但是, 在实际中,会发现, Modal里面的内容和外面的内容,会存在断层, 就是 div 和Modal , div先消失, Modal后消失。 有错落感, 特别明显, 应该是 Modal有过渡效果导致的
所以,这个也是有问题的
有人又说了 我不要上面外层的 div, 直接在 pareng.vue 中写 9个 Modal, 发现没, 我要最想一起抽离出去的,还有 Modal的 @on-cancel 和 @on-ok 这部分业务, 现在只是将UI层的抽离到组件中了, 逻辑层的没抽离出去,是这么个现状吧, 这样的话, 问题解决的不是很优雅
子组件正确写法
<template>
<Modal title='弹窗' v-model='showModal' @on-cancel='rightClose'>
你好
<div slot='footer'>
<Button @click='close'>取消</Button>
<Button @click='handleOk'>确定</Button>
</div>
</Modal>
</template>
<script>
import {getDetail} from '@/api/news'
export default {
name: 'son',
props: {
isShow: {
type: Boolean
},
id: {
type: Number
}
},
data () {
return {
showModal: false
}
},
watch: {
id(newVal) {
// 当ID变了以后,mounted是不会再次执行的,mounted只执行一次,除非父组件将Son组件销毁重新创建
this.init()
},
isShow (newVal, oldVal) {
this.showModal = newVal
}
},
mounted() {
this.init()
},
methods: {
// 假设这里要调用获取详情的数据
async init() {
let res = await getDetail({id: this.id})
// ...
},
close () {
// 一些和业务相关的代码 ....
this.$emit('closeModal')
},
async handleOk () {
// 一些和业务相关的代码 ...., 比如去请求数据, 拿到结果返回值以后,再关闭
let res = await fetch('/test-api')
if(res) {
// 数据修改成功
this.$emit('closeModal')
} else {
// 提示数据修改失败
}
},
rightClose() {
console.log('右上角的关闭,Son组件自己改变了ShowModal的状态')
// 一些和业务相关的代码 ....
// 更新父组件的状态
this.$emit('closeModal')
}
}
}
</script>
v-model父子组件双向数据绑定在Modal中的应用
上面的写法,我能不能再简洁一点,答案是: 可以的
父组件
<template>
<div>
<Button @click='openModal'>打开</Button>
<Son v-model='isShowModal' :info='info'></Son>
</div>
</template>
<script>
import Son from './son'
export default {
name: 'iview-modal',
components: {
Son
},
data() {
return {
isShowModal: false,
info: {
name: '张三',
age: 12,
time: 0
}
}
},
methods: {
openModal() {
this.isShowModal = true
this.info.time = Date.now()
}
}
}
</script>
子组件
<template>
<Modal v-model='open' @on-cancel='handle(false)' @on-ok='handle(true)'>
<div>
<Form>
<FormItem label='姓名'>{{info.name}}</FormItem>
<FormItem label='年龄'>{{info.age}}</FormItem>
<FormItem label='查看时间'>{{info.time}}</FormItem>
</Form>
</div>
</Modal>
</template>
<script>
export default {
name: 'son',
props: {
value: {
type: Boolean,
default: false
},
info: {
type: Object,
default: () => {
return {}
}
}
},
data() {
return {
open: false
}
},
watch: {
value(val) {
this.open = val
}
},
methods: {
handle(status) {
this.$emit('input', false)
}
}
}
</script>
上面父子组件这种双向数据绑定,看起来是比较美好, 语法也比较简洁
场景: 子组件单纯的做数据展示,这个没啥问题
局限性: 如果你在去定按钮点击的时候,做点其他的事情,你发现,你还是得在父组件写一个事件 比如: 某个简单的字段编辑, 你需要将弹窗中收集的的数据回写到父组件,你就需要在父组件显示写 input 事件, 然后处理额外的事情
父组件
<template>
<div>
<Button @click='openModal'>打开</Button>
<Son v-model='isShowModal' :info='info' @input='input'></Son>
</div>
</template>
<script>
import Son from './son'
export default {
name: 'iview-modal',
components: {
Son
},
data() {
return {
isShowModal: false,
info: {
name: '张三',
age: 12,
time: 0
}
}
},
methods: {
openModal() {
this.isShowModal = true
this.info.time = Date.now()
},
input(status, obj) {
// 关闭弹窗以后,我想要更新父组件的数据,或者我要去调用接口拉取数据
// 总之有一些额外的工作需要父组件去处理,就需要显示定义 input事件
// 场景1: 拉取数据 fetch('http://www.xxx/api/getData')
// 场景2: 子组件弹窗中的数据更新父组件到父组件中
// this.info.name = obj.name
}
}
}
</script>
子组件调用,传递额外的参数给父组件
子组件
handle(status) {
this.$emit('input', false, {
name: '李四'
})
}
使用什么样的方式最终还是取决于你的应用场景,最合适的就是最好的, 灵活运用vue 的语法糖,解决应用场景的实际问题才是硬道理
总结
废话不哆嗦,直接上总结:
- slot 写法,替代iview 默认的 footer 先改造 确定/ 取消 按钮, 避免子组件直接修改父组件的状态
- 换一个 Son 组件的内部变量(或者叫状态)showModal, 去控制 Son组件的显示隐藏 , Son组件自己去修改自己的 data数据, 妥妥的,没毛病吧; 那是不是说,右上角的 × 在点击以后,是自己改变了自己的状态呢, 是的,就是这个道理, 再通过 $emit 去和父组件通讯
- watch 去监听 props ,然后将props的值 赋值给 Son 组件自己的 showModal, 去同步状态给Son组件
Modal用的不爽的地方
1、API写法直接调用
this.$Modal.confirm({
title: ''
})
不能让 footer 操作按钮居中,没有API, 也就是没有提供可配置的项
template 写法也没有配置可以简单的让 footer居中,要自己写 style='text-align: center'
2、Modal: API调用的, 点击遮罩层,不能关闭。也就是默认是配置了 :closable="false" 不方便
3、 template写法, 不要body内容, 就会有一段空白, 因为 modal 的 body 是 上下 padding 15px;
<Modal>
<!-- 这里不放内容 -->
<div slot='footer'>
<!-- 自定义Modal的footer -->
</div>
</Modal>
有的场景就是不想要body内容,但是不给,就会有一段空白,比较难看
有些坑,你不踩,怎么知道它不会被踏平😝😝😝
相关链接