vue3封装弹框组件-函数声明方式和命令式

2,962 阅读4分钟

在 Vue 3 中,我们可以使用函数声明方式和命令式方式来封装弹框组件,以实现在应用中方便地显示弹框。本文将介绍如何使用这两种方式来封装一个简单的弹框组件,并提供示例代码和用法说明。

1.函数声明式

先创建子组件dialog.vue

子组件代码

// dialog.vue
<template>
  <div class="mask" v-if="show">
    <div class="modal-content">
      <div class="dialog-header">{{ title }}</div>
      <div class="dialog-body">{{ content }}</div>
      <div class="dialog-footer">
        <el-button @click="onConfirm" type="primary">{{ confirmText }}</el-button>
        <el-button @click="onCancel">{{ cancelText }}</el-button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
const props = defineProps({
  show: {
    type: Boolean,
    default: false
  },
  title: {
    type: String,
    default: ''
  },
  content: {
    type: String,
    default: ''
  },
  confirmText: {
    type: String,
    default: '确定'
  },
  cancelText: {
    type: String,
    default: '取消'
  }
})
const emit = defineEmits(['confirm', 'cancel', 'update:show'])
const onConfirm = () => {
  emit('update:show', false)
  emit('confirm')
}

const onCancel = () => {
  emit('update:show', false)
  emit('cancel')
}
</script>

<style scoped lang="scss">
.mask {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  .modal-content {
    box-shadow:
      0px 12px 32px 4px rgba(0, 0, 0, 0.04),
      0px 8px 20px rgba(0, 0, 0, 0.08);
    background: #fff;
    padding: 16px;
    position: relative;
    width: 500px;
    border-radius: 2px;
    margin: 140px auto 0;
  }
  .dialog-header {
    padding-bottom: 16px;
    color: #303133;
    font-size: 18px;
    font-weight: 500;
    line-height: 24px;
  }
  .dialog-body {
    color: 606266;
    font-size: 14px;
  }
  .dialog-footer {
    margin-top: 16px;
    text-align: right;
  }
}
</style>

父组件代码

父组件调用

<template>
  <div>
    <el-button @click="showModal2">提示2</el-button>
    <Dialog
      v-model:show="showDialog"
      :title="'提示'"
      :content="'这是一段内容'"
      @confirm="confirm"
      @cancel="cancel"
    ></Dialog>
  </div>
</template>

<script setup lang="ts">
import Dialog from './dialog.vue'
const showDialog = ref(false)
const showModal2 = () => {
  showDialog.value = true
}
const confirm = () => {
  console.log('确定')
}

const cancel = () => {
  console.log('取消')
}
</script>

<style scoped lang="scss"></style>

效果展示

image.png

2.命令式

有时候我们不想在模板文件中再次引入子组件,并且传递参数给子组件,我们想要像elementPlus那样,可以直接调用ElMessageBox.alert(),并且可以使用.then.catch,那我们就需要使用命令式声明一个弹框组件。这种方式是通过动态的创建元素和销毁元素来实现,点击按钮创建弹框并append到body里面,这里需要用到挂载,可参考main.ts的写法

// main.ts

import App from './App.vue'
const app = createApp(App)
app.mount('#app')

下面的js文件也是一样,我们引入弹框组件dialog.vue ,import Dialog from './dialog.vue' ,然后const app = createApp(Dialog)createApp的第二个参数接受一个options,可参考官方API。同时为了避免影响其他元素样式,把它appendbody里面,最后挂载app.mount()。点击关闭按钮,销毁弹框元素,卸载组件,app.unmount()
首先还是先创建dialog.vue,代码如下

<template>
  <div class="mask">
    <div class="modal-content">
      <div class="dialog-header">{{ title }}</div>
      <div class="dialog-body">{{ content }}</div>
      <div class="dialog-footer">
        <el-button @click="onConfirm" type="primary">{{ confirmText }}</el-button>
        <el-button @click="onCancel">{{ cancelText }}</el-button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
const props = defineProps({
 
  title: {
    type: String,
    default: ''
  },
  content: {
    type: String,
    default: ''
  },
  confirmText: {
    type: String,
    default: '确定'
  },
  cancelText: {
    type: String,
    default: '取消'
  },
  onConfirm: {
    type: Function,
    default: () => {}
  },
  onCancel: {
    type: Function,
    default: () => {}
  }
})
</script>

<style scoped lang="scss">
.mask {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  .modal-content {
    box-shadow:
      0px 12px 32px 4px rgba(0, 0, 0, 0.04),
      0px 8px 20px rgba(0, 0, 0, 0.08);
    background: #fff;
    padding: 16px;
    position: relative;
    width: 500px;
    border-radius: 2px;
    margin: 140px auto 0;
  }
  .dialog-header {
    padding-bottom: 16px;
    color: #303133;
    font-size: 18px;
    font-weight: 500;
    line-height: 24px;
  }
  .dialog-body {
    color: 606266;
    font-size: 14px;
  }
  .dialog-footer {
    margin-top: 16px;
    text-align: right;
  }
}
</style>

跟函数声明方式的差别是:props里面多了onConfirmonCancel。 然后创建dialog.js,代码如下:

import { createApp } from 'vue'

import Dialog from './dialog.vue'


function openModal({ title, content, confirmBtnTxt = '确定', cancelBtnTxt = '取消' }) {
  return new Promise((resolve, reject) => {
    const app = createApp(Dialog, {
      title,
      content,
      confirmBtnTxt,
      cancelBtnTxt,
      onConfirm: () => {
        unmount()
        resolve()
      },
      onCancel: () => {
        unmount()
        reject(new Error())
      }
    })

    // 创建一个挂载容器
    const parentNode = document.createElement('div')
    document.body.appendChild(parentNode)

    // 卸载组件
    const unmount = () => {
      app.unmount()
      document.body.removeChild(parentNode)
    }

    // 挂载组件
    app.mount(parentNode)
  })
}

export default openModal

这时候组件已经创建完成了,看如何使用呢?

如何使用

<template>
  <div>
    <el-button @click="showModal">提示</el-button>
   
  </div>
</template>

<script setup lang="ts">
import openModal from './dialog.js'
const showModal = () => {
  openModal({
    title: '提示',
    content: '这是一段内容'
  })
    .then(() => {
      console.log('确定')
    })
    .catch(() => {
      console.log('取消')
    })
}


</script>

<style scoped lang="scss"></style>

3.jsx封装弹框组件

上面的命令式封装方法,需要写2个文件,.vue.jsjsx只需要一个文件即可。 具体代码和上面的命令式差不多,区别是要使用jsx语法,在使用jsx语法前,要先安装一个插件@vitejs/plugin-vue-jsx,否则会报错

npm i  @vitejs/plugin-vue-jsx

然后再vite.config.ts中引入,

import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
  plugins: [
    vue(),
    vueJsx()
  ],
  })

创建dialog.jsx


// dialog.jsx

import { createApp, defineComponent } from 'vue'
import './modal.scss'
let app, parentNode

const ModalComponent = defineComponent({
  name: 'Modal',
  props: {
    title: {
      type: String,
      default: 'Modal'
    },
    onConfirm: {
      type: Function,
      default: () => {},
      required: true
    },
    onClose: {
      type: Function,
      default: () => {},
      required: true
    }
  },
  setup(props) {
    const handleConfirm = () => {
      props.onConfirm()
      closeModal()
    }

    const handleClose = () => {
      props.onClose()
      closeModal()
    }

    const closeModal = () => {
      app.unmount()
      document.body.removeChild(parentNode)
    }

    return {
      handleConfirm,
      handleClose,
      closeModal
    }
  },
  render() {
    return (
      <div class="modal">
        <div class="modal-content">
          <h2>{this.title}</h2>
          <button onClick={this.handleConfirm}>确定</button>
          <button onClick={this.handleClose}>关闭</button>
        </div>
      </div>
    )
  }
})

export function openModal({ title, onConfirm, onClose }) {
  app = createApp(ModalComponent, {
    title,
    onConfirm,
    onClose
  })

  parentNode = document.createElement('div')
  document.body.appendChild(parentNode)

  app.mount(parentNode)
}

// modal.scss
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;

  .modal-content {
    background-color: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

    h2 {
      margin-bottom: 10px;
    }

    button {
      margin-top: 20px;
      padding: 8px 16px;
      border: none;
      background-color: #007bff;
      color: #fff;
      border-radius: 4px;
      cursor: pointer;

      &:hover {
        background-color: #0056b3;
      }
    }
  }
}

如何使用

<template>
  <div>
    <el-button @click="showModal">tsx提示</el-button>
  </div>
</template>

<script setup lang="ts">
import { openModal } from './dialog.jsx'

const showModal = () => {
  openModal({
    title: '这是一个弹框',
    onConfirm: () => {
      console.log('点击了确定按钮')
    },
    onClose: () => {
      console.log('点击了关闭按钮')
    }
  })
}
</script>

<style scoped lang="scss"></style>