Vue3自定义编程式弹窗

前端开发中,弹窗是必不可少的重要组件,一般我们会选择使用流行的组件库提供的弹窗组件,比如Element Plusiview UIAnt Design Vue

模板式弹窗

使用这些组件库提供的弹窗,通常我们需要在template中书写Modal的代码,以iview举例:

html 复制代码
<template>
  <Button type="primary" @click="modal1 = true">Display dialog box</Button>
  <Modal
    v-model="modal1"
    title="Common Modal dialog box title"
    @on-ok="ok"
    @on-cancel="cancel">
    <p>Content of dialog</p>
    <p>Content of dialog</p>g
    <p>Content of dialog</p>
  </Modal>
</template>
<script>
  export default {
    data () {
      return {
        modal1: false
      }
    },
    methods: {
      ok () {
        this.$Message.info('Clicked ok');
      },
      cancel () {
        this.$Message.info('Clicked cancel');
      }
    }
  }
</script>

模板式弹窗的缺陷

试想,如果我们一个页面需要很多弹窗,那就得写很多这样的模板,不利于阅读和维护。于是我们可能会将Modal抽离到单独的组件中,就变成了这样:

html 复制代码
<template>
  <Button type="primary" @click="showAdd = true">新增</Button>
  <Button type="primary" @click="showEdit = true">编辑</Button>
  
  <AddUserModal v-if="showAdd"></AddUserModal>
  <EditUserModal v-if="showEdit"></EditUserModal>
</template>
<script>
  export default {
    data () {
      return {
        showAdd: false,
        showEdit: false
      }
    },
  }
</script>

这样看上去没问题,但是实际运行到时候会发现,弹窗关闭的时候,动画没了...

那我们不用v-if,自己实现一个v-model:show来控制弹窗的显示呗,那就变成这样:

html 复制代码
<template>
  <Button type="primary" @click="showAdd = true">新增</Button>
  <Button type="primary" @click="showEdit = true">编辑</Button>
  
  <AddUserModal v-model:show="showAdd"></AddUserModal>
  <EditUserModal v-model:show="showEdit"></EditUserModal>
</template>
<script>
  export default {
    data () {
      return {
        showAdd: false,
        showEdit: false
      }
    },
  }
</script>

嗯,关闭的时候有动画了,但又会发现一个新的问题,弹窗组件的生命周期函数不太好用了,有时候我们可能会在每次弹窗打开的时候做一些初始化工作,比如从后台获取数据

js 复制代码
onMounted(() => {
  fetchData()
})

你会发现弹窗显示的时候,fetchData并不会调用。原因是EditUserModal组件已经跟随父组件一起挂载了,所以不会再触发onMounted了,如果你的需求是每次弹窗显示的时候,都要调一次接口的话,就只能监听show的变化了。

js 复制代码
watch(show, (val) => {
  if (val) {
    fetchData()
  }
}, { immediate: true})

模板式弹窗除了上面的问题以外,还有一个另我最不能忍受的痛点,就是如果某个弹窗组件需要在多个页面使用的时候,那你每一个页面都要在模板中声明它,并为它绑定show变量。这非常繁琐,而且有些时候,我们控制弹窗显示的逻辑不在vue组件内,去访问show变量可能不太方便。这时候,如果我们能通过函数调用创建弹窗就太好了,比如iview可以这样:

js 复制代码
this.$Modal.info({
  title: 'xxx用户协议',
  content: 'xxx内容'
});

这样我们就能不依赖模板声明,直接在任何地方显示弹窗了。但可惜,它的自定义程度不高,仅能传入简单的文本或者render函数,对复杂的弹窗无能为力。Element UI甚至完全不支持这种使用方式。

所以我们需要自定义一个编程式弹窗。

编程式弹窗

编程式弹窗也叫命令式弹窗,即通过函数命令创建的弹窗。像这样:

html 复制代码
<script setup lang="ts">
  import UserAgreementComponent from './UserAgreementComponent.vue'
  import Modal from '@/hooks/useModal'
  
  const Modal = useModal()
  
  function onClick() {
    Modal.create({
      title: '用户服务协议',
      content: UserAgreementComponent,
    })
  }
</script>

这样就不受模板限制,可以在任何js执行的地方调用方法创建弹窗,不需要管理show变量,弹窗的复用也很方便,而且弹窗的挂载跟弹窗的显示是同步的。

怎么实现编程式弹窗

首先,我们需要封装一个Modal组件,作为基础的弹窗组件模板,这里可以完全自己实现,也可以使用第三方组件,我这里选择使用Antd的弹窗组件,减少一些代码编写。

html 复制代码
<script setup lang="ts">
import { ref, computed } from 'vue'

const props = defineProps({
  title: {
    type: String,
  },
  content: {
    type: Object,
  },
  footer: {
    type: Array,
    default: () => []
  },
  onClosed: {
    type: Function,
  }
})

const show = ref(true)

const contentIsText = computed(() => {
  return typeof props.content === 'string'
})

function close() {
  show.value = false
}

function closed() {
  // 弹窗关闭后,通知Service销毁dom
  props.onClosed?.()
}

// 暴露close方法,便于外部调用
defineExpose({
  close
})
</script>

<template>
  <a-modal v-model:open="show" :title="title" centered @cancel="closed">
    <component v-if="!contentIsText" :is="content" v-bind="componentParams" @close="close"></component>
    <div v-if="contentIsText" v-html="content"></div>
  </a-modal>
</template>

然后,封装一个创建弹窗的服务,Vue3中我们可以用组合式函数

ts 复制代码
import Modal from '@/components/modal.vue'
import { createVNode, getCurrentInstance, render } from 'vue'

export default function useModal() {
  const appContext = getCurrentInstance()?.appContext

  const create = (props: any) => {

    // 创建弹窗所在的父容器
    const container = document.createElement('div')
    document.body.appendChild(container)

    const destroy = () => {
      // 延迟dom的销毁,不然会影响弹窗的关闭动画
      setTimeout(() => {
        render(null, container)
        document.body.removeChild(container)
      }, 300)
    }

    // 用Modal组件生成虚拟节点
    const vnode = createVNode(Modal, {
      ...props,
      onClosed: () => {
        destroy()
      }
    })

    // 关键:绑定当前上下文,否则无法在Modal中使用当前项目引入的插件、组件
    vnode.appContext = appContext!

    // 将虚拟节点插入dom
    render(vnode, container)

    // 提供一个主动关闭的方法
    const close = () => {
      // 先调用Modal组件暴露的close方法,关闭弹窗
      vnode.component?.exposed?.close()
      // 销毁dom
      destroy()
    }

    return {
      close
    }
  }

  return {
    create
  }
}

最后,就可以在业务组件中使用了

ts 复制代码
<script setup lang="ts">
  import UserAgreementComponent from './UserAgreementComponent.vue'
  import Modal from '@/hooks/useModal'
  
  const Modal = useModal()
  
  function onClick() {
    const modal = Modal.create({
      title: '用户服务协议',
      content: UserAgreementComponent,
      componentParams: {
        id: 1,
        name: '张三'
      }
    })
    // 主动关闭
    modal.close()
  }

  function onClick2() {
    Modal.create({
      title: '提醒',
      content: '检查到新版本,请更新',
    })
  }
</script>

上面就实现了一个简单的编程式弹窗,在你的业务中,你可以为它提供更多的属性来进行更丰富的细节控制。比如用closable来控制是否显示右上角关闭按钮,使用mask-closable来控制是否允许点击遮罩层关闭弹窗等等。

其中,底部按钮是弹窗重要的组成部分,这里有两种实现方式。

一、在Modal模板中写死按钮,提供一些属性进行微调:

html 复制代码
<template>
  <a-modal v-model:open="show" :title="title" centered @cancel="closed">
    <component v-if="!contentIsText" :is="content" v-bind="componentParams" @close="close"></component>
    <div v-if="contentIsText" v-html="content"></div>
    <template #footer>
      <a-button @click="onCancel">{{ cancelText || '取消' }}</a-button>
      <a-button type="primary" @click="onOk">{{ okText || '确定' }}</a-button>
    </template>
  </a-modal>
</template>
ts 复制代码
<script setup lang="ts">
  ...
  function onClick() {
    const modal = Modal.create({
      title: '用户服务协议',
      content: UserAgreementComponent,
      cancelText: '不同意',
      onCancel: () => {
        modal.close()
      },
      okText: '同意',
      onOk: () => {
        // do something
        modal.close()
      },
    })
  }
</script>

二、从外部传入按钮数组

html 复制代码
<template>
  <a-modal v-model:open="show" :title="title" centered @cancel="closed">
    <component v-if="!contentIsText" :is="content" v-bind="componentParams" @close="close"></component>
    <div v-if="contentIsText" v-html="content"></div>
    <template #footer>
      <a-button v-for="item in footer" :key="item.text" :type="item.type" 
        @click="item.onClick()">
        {{ item.text }}
      </a-button>
    </template>
  </a-modal>
</template>
ts 复制代码
<script setup lang="ts">
  ...
  function onClick() {
    const modal = Modal.create({
      title: '用户服务协议',
      content: UserAgreementComponent,
      footer: [
        {
          text: '不同意',
          type: 'default',
          onClick: () => {
            modal.close()
          },
        },
        {
          text: '同意',
          type: 'primary',
          onClick: () => {
            modal.close()
          },
        },
      ]
    })
  }
</script>

你还可以将footer设置为空数组,在UserAgreementComponent组件内部自定义底部按钮。但是这样的话,你可能需要在UserAgreementComponent内部进行弹窗的关闭。可以这样:

html 复制代码
<script setup lang="ts">
const emit = defineEmits(['close'])
function onOk() {
  // do something
  close()
}

function close() {
  emit('close')
}
</script>
<template>
  <div>
    <div>用户协议xxxx</div>
    <div class="footer">
      <a-button @click="close">不同意</a-button>
      <a-button type="primary" @click="onOk">同意</a-button>
    </div>
  </div>
</template>

至此,我们就封装了一个支持高度自定义的弹窗组件,希望它能为你的开发提供一些便利。

相关推荐
写不出来就跑路8 分钟前
基于 Vue 3 的智能聊天界面实现:从 UI 到流式响应全解析
前端·vue.js·ui
OpenTiny社区11 分钟前
盘点字体性能优化方案
前端·javascript
FogLetter15 分钟前
深入浅出React Hooks:useEffect那些事儿
前端·javascript
Savior`L15 分钟前
CSS知识复习4
前端·css
0wioiw030 分钟前
Flutter基础(前端教程④-组件拼接)
前端·flutter
花生侠1 小时前
记录:前端项目使用pnpm+husky(v9)+commitlint,提交代码格式化校验
前端
一涯1 小时前
Cursor操作面板改为垂直
前端
我要让全世界知道我很低调1 小时前
记一次 Vite 下的白屏优化
前端·css
1undefined21 小时前
element中的Table改造成虚拟列表,并封装成hooks
前端·javascript·vue.js