• 作者:老汪软件技巧
  • 发表时间:2024-09-15 15:01
  • 浏览量:

1 问题示例

●在前端开发过程中不可避免的需要使用$Modal进行信息提示或进行操作的确认,也不可避免的会遇到需要嵌套使用的场景。在这种场景下,会出现内层的Modal闪现一下会自动关闭,以下是一个简易的问题示例代码


2 问题出现原因

●挂载到Vue原型上的Modal对象,使用它自带的方法比如confirm、error、info等创建一个弹窗实例时,它实际上是一个单例模式。![image.png](~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMOWPt-WJjeerr-Wwj-eZvQ==:q75.awebp?rk3s=f64ab15b&x-expires=1726911974&x-signature=GFd%2FqeBcJacHgP%2BTJnsA0DOaKuk%3D)●点击确定或者取消时,弹窗会自动关闭,这个关闭过程分为两个步骤,一是隐藏弹窗,二是将承载弹窗的dom元素从文档中移除。其中这个移除元素的操作是异步进行的,它会有一个300ms的延迟,有延迟的目的是为了让关闭的动画过渡比较自然。300ms的时间足以完成一个一般的网络请求。![image1.png](~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMOWPt-WJjeerr-Wwj-eZvQ==:q75.awebp?rk3s=f64ab15b&x-expires=1726911974&x-signature=irMGwBUKWouR9pA2qj%2Fg%2FO4xKew%3D)●结合以上两个原因,再结合上述问题示例,confirm确认后进行网络请求,网络请求在300ms内已经完成并打开了错误弹窗this.Modal.error,300ms的时间等待完成后,执行弹窗的销毁(即从文档中移除dom元素)操作,因为这里的提示弹窗是单例模式,confirm和error使用的是同一个dom元素,所以confirm确认按钮自动执行的remove操作连带error弹窗一起从dom中移除了,所以会出现嵌套在内层的弹窗闪现一下之后就消失的问题。

3 问题解决方案3.1 新建模板组件

这种方式就是新建一个vue组件,自己重新实现一遍Modal弹窗,虽然可以解决问题,但是个人觉得不如以下两种方式。我是不推荐这种方式的

3.2 官方文档中提供的思路

●解决方法实际上官方的文档并没有直接针对本次提到的问题去提供解决思路,但是有提示可以阻止单例弹窗关闭的方式,请看图:

我们可以通过传递一个loading参数,阻止弹窗自动关闭,但是这样处理之后,确认按钮会一直处于加载状态,无法再次点击。没关系,可以按照如下方式解决:

const vm = this
this.$Modal.success({
    title: '提示',
    content: 'xxx',
    loading: true,
    onOk () {
        this.buttonLoading = false
        // 确认后的执行逻辑
        /* ... */
    }
})

注意:这里onOk方法一定不能使用箭头函数,因为我们需要确保onOk方法中的this是指向Modal实例的这样我们才能有机会去改Modal实例内部属性(buttonLoading)的值,才能关闭确认按钮的加载状态,如果onOk内需要使用当前组件内部的属性和方法时,可以在外层定义一个中间变量保存当前组件实例的上下文(即当前组件的this)后续在所有逻辑执行完毕后手动调用移除弹窗的方法:

this.$Modal.remove()

以上这种解决方案会存在一些问题,这种问题当然不是程序上的问题,而指的是用户体验方面。我们如果能保证每次请求超过300ms甚至更长时间(严格意义上来说是onOk中的异步逻辑执行的时间),组件库提供的单例modal随便嵌套,不会出现本次描述的问题。如果能保证请求在短时间内完成,使用这种解决方案也无伤大雅。但是这个时间恰恰是在很多时候我们无法控制的一个变量。使用了这种思路的解决方案,如果时间过长,确认的弹窗一直无法关闭,用户体验有点不太好;如果不使用这种方案,万一时间过短,错误信息一闪而过,用户体验更差。那有没有一个更好的方案呢?请看3.3

3.3 创建非单例模式的弹窗

归根结底,该问题出现的主要原因还是因为弹窗是单例。那就另辟蹊径,让弹窗不是单例就好咯!

这里我们就使用iview Modal自带的创建单例Modal的方式。每次去创建一个新的Moda实例。具体代码如下:

3.3.1 封装一个创建弹窗的方法

// import { Modal } from 'view-design'
// 或者
const Modal = Vue.prototype.$Modal
export const createModal = () => {
  let instance = null
  const confirm = (options) => {
    // 自定义的弹窗内容渲染方式
    const render = ('render' in options)
      ? options.render
      : undefined
    // 锁定滚动:弹窗打开时文档是否可滚动,默认不可以
    const lockScroll = ('lockScroll' in options)
      ? options.lockScroll
      : true
    // 创建实例
    instance = Modal.newInstance({
      closable: false,
      maskClosable: false,
      footerHide: true,
      render: render,
      lockScroll
    })
    options.onRemove = function () {
      instance = null
    }
    instance.show(options)
  }
  // 返回各种使用方法,使其用法和iview单例Modal使用方式完全保持一致
  return {
    confirm (props) {
      props.icon = 'confirm'
      props.showCancel = true
      return confirm(props)
    },
    error (props) {
      props.icon = 'error'
      props.showCancel = false
      return confirm(props)
    },
    info: function (props = {}) {
      props.icon = 'info'
      props.showCancel = false
      return confirm(props)
    },
    success: function (props = {}) {
      props.icon = 'success'
      props.showCancel = false
      return confirm(props)
    },
    warning: function (props = {}) {
      props.icon = 'warning'
      props.showCancel = false
      return confirm(props)
    },
    remove: function () {
      if (!instance) {
        return false
      }
      instance.remove()
    }
  }
}

问题实例__示例的作用

3.3.2 使用方式

// 需要先引入createModal方法
handle () {
    let modal = createModal()
    modal.confirm({
        title: '确认信息',
        content: '确认要删除当前行吗?',
        onOk: () => {
            let modal2 = createModal()
            modal2.error({
                title: '错误信息',
                msg: '错误信息内容'
            })
        }
    })
}

3.3.3 考虑使用简便性

以上使用方式其实已经解决了我们的主要问题,但是使用上相较于之前多了几个步骤,一是需要再用到的地方引入createModal方法,二是每次需要通过执行createModal来创建一个modal。

有没有更简便的方式呢?或者说是跟原先的单例模式的用法一样的方式呢?当然有。需要安装如下方式将咱们封装的方法挂载到Vue原型对象上不就可以了嘛!

在程序入口文件中将createModal挂载到Vue原型对象上。

// 这里认为已经引入了createModal方法
Vue.prototype.$createModal = createModal

使用时:

handle () {
    this.$createModal().confirm({
        title: '确认信息',
        content: '确认要删除当前行吗?',
        onOk: () => {
            this.$createModal().error({
                title: '错误信息',
                msg: '错误信息内容'
            })
        }
    })
}

3.3.4 更简便

上一步中的方法感觉还是不是很完美,因为每次还要执行以下方法this.$createModal(),感觉跟使用单例弹窗还是有区别。我们来改写下我们封装的方法

// import { Modal } from 'view-design'
// 或者(推荐)
const _Modal = Vue.prototype.$Modal
const confirm = (options) => {
  // 自定义的弹窗内容渲染方式
  const render = ('render' in options)
    ? options.render
    : undefined
    // 锁定滚动:弹窗打开时文档是否可滚动,默认不可以
  const lockScroll = ('lockScroll' in options)
    ? options.lockScroll
    : true
    // 创建实例
  let instance = _Modal.newInstance({
    closable: false,
    maskClosable: false,
    footerHide: true,
    render: render,
    lockScroll
  })
  options.onRemove = function () {
    instance = null
  }
  instance.show(options)
  return instance
}
// 返回各种使用方法,使其用法和iview单例Modal使用方式基本保持一致
export const Modal = {
  confirm (props) {
    props.icon = 'confirm'
    props.showCancel = true
    return confirm(props)
  },
  error (props) {
    props.icon = 'error'
    props.showCancel = false
    return confirm(props)
  },
  info: function (props = {}) {
    props.icon = 'info'
    props.showCancel = false
    return confirm(props)
  },
  success: function (props = {}) {
    props.icon = 'success'
    props.showCancel = false
    return confirm(props)
  },
  warning: function (props = {}) {
    props.icon = 'warning'
    props.showCancel = false
    return confirm(props)
  },
  remove () {}
}

之后把返回的对象挂载到Vue原型上

// 需要先引入自己封装后的Modal对象
Vue.prototype._Modal = Modal

改写之后我们返回的就不是一个方法,而是一个包含各种方法的对象,相较于原先单例弹窗的使用方式,这种方式稍微发生了一点变化。

这种方式在使用loading参数阻止弹窗自动关闭后,在手动关闭弹窗时与原先有点区别,具体如下:

// 原先的方式
this.$Modal.success({
    title: '提示',
    content: 'xxx',
    loading: true,
    onOk () {
        this.buttonLoading = false
        // 确认后的执行逻辑
        /* ... */
        
        // 一切执行完之后
        this.$Modal.remove()
    }
})
// 我们封装的Modal对象使用方式
let a = this._Modal.success({
    title: '提示',
    content: 'xxx',
    loading: true,
    onOk () {
        this.buttonLoading = false
    
        // onOk后的执行逻辑
        /* ... */
    
        // 一切执行完之后
        a.remove()
    }
})

其他地方与原先的单例Modal的使用方法保持一致即可。

4 解决方案的选择

实际开发中需要采用哪种解决方案,结合问题场景选择即可