二次封装 iview Modal的坑

5,049 阅读3分钟
让你明明白白学知识,有代码,有讲解,抄的走,学的会!

假设你正在使用 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内容,但是不给,就会有一段空白,比较难看

 有些坑,你不踩,怎么知道它不会被踏平😝😝😝

相关链接