vue使用h函数封装dialog组件,以命令的形式使用dialog组件

场景

有些时候我们的页面是有很多的弹窗
如果我们把这些弹窗都写html中会有一大坨
因此:我们需要把弹窗封装成命令式的形式

命令式弹窗

// 使用弹窗的组件
<template>
  <div>
    <el-button @click="openMask">点击弹窗</el-button>
  </div>
</template>

<script setup lang="ts">
import childTest from '@/components/childTest.vue'
import { renderDialog } from '@/hooks/dialog'
function openMask(){
  // 第1个参数:表示的是组件,你写弹窗中的组件
  // 第2个参数:表示的组件属性,比如:确认按钮的名称等
  // 第3个参数:表示的模态框的属性。比如:模态宽的宽度,标题名称,是否可移动
  renderDialog(childTest,{},{title:'测试弹窗'})
}
</script>
// 封装的弹窗
import { createApp, h } from "vue";
import { ElDialog } from "element-plus";
export function renderDialog(component:any,props:any, modalProps:any){
 const dialog  = h(
    ElDialog,   // 模态框组件
    {
      ...modalProps, // 模态框属性
      modelValue:true, // 模态框是否显示
    }, // 因为是模态框组件,肯定是模态框的属性
    {
      default:()=>h(component, props ) // 插槽,el-dialog下的内容
    }
  )
 console.log(dialog)
  // 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
  const app = createApp(dialog)
  const div = document.createElement('div')
  document.body.appendChild(div)
  app.mount(div)
}
//childTest.vue 组件
<template>
  <div>
    <span>It's a modal Dialog</span>
    <el-form :model="form" label-width="auto" style="max-width: 600px">
    <el-form-item label="Activity name">
      <el-input v-model="form.name" />
    </el-form-item>
    <el-form-item label="Activity zone">
      <el-select v-model="form.region" placeholder="please select your zone">
        <el-option label="Zone one" value="shanghai" />
        <el-option label="Zone two" value="beijing" />
      </el-select>
    </el-form-item>
  </el-form>
  </div>
</template>
<script setup lang="ts">
import { ref,reactive } from 'vue'
const dialogVisible = ref(true)
const form = reactive({
  name: '',
  region: '',
})
const onSubmit = () => {
  console.log('submit!')
}
</script>

01

为啥弹窗中的表单不能够正常展示呢?

在控制台会有下面的提示信息:
Failed to resolve component:
el-form If this is a native custom element,
make sure to exclude it from component resolution via compilerOptions.isCustomElement
翻译过来就是
无法解析组件:el-form如果这是一个原生自定义元素,
请确保通过 compilerOptions.isCustomElement 将其从组件解析中排除

02

其实就是说:我重新创建了一个新的app,这个app中没有注册组件。
因此会警告,页面渲染不出来。

// 我重新创建了一个app,这个app中没有注册 element-plus 组件。
const app = createApp(dialog)

现在我们重新注册element-plus组件。
准确的说:我们要注册 childTest.vue 组件使用到的东西

给新创建的app应用注册childTest组件使用到的东西

我们将会在这个命令式弹窗中重新注册需要使用到的组件

// 封装的弹窗
import { createApp, h } from "vue";
import { ElDialog } from "element-plus";
// 引入组件和样式
import ElementPlus from "element-plus";
// import "element-plus/dist/index.css";
export function renderDialog(component:any,props:any, modalProps:any){
 const dialog  = h(
    ElDialog,   // 模态框组件
    {
      ...modalProps, // 模态框属性
      modelValue:true, // 模态框显示
    }, // 因为是模态框组件,肯定是模态框的属性
    {
      default:()=>h(component, props ) // 插槽,el-dialog下的内容
    }
  )
 console.log(dialog)
  // 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
  const app = createApp(dialog)
  // 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
  app.use(ElementPlus);
  const div = document.createElement('div')
  document.body.appendChild(div)
  app.mount(div)
}

03

现在我们发现可以正常展示弹窗中的表单了。因为我们注册了element-plus组件。
但是我们发现又发现了另外一个问题。
弹窗底部没有取消和确认按钮。
需要我们再次通过h函数来创建

关于使用createApp创建新的应用实例

在Vue 3中,我们可以使用 createApp 来创建新的应用实例
但是这样会创建一个完全独立的应用
它不会共享主应用的组件、插件等。
因此我们需要重新注册

弹窗底部新增取消和确认按钮

我们将会使用h函数中的插槽来创建底部的取消按钮

// 封装的弹窗
import { createApp, h } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";

export function renderDialog(component: any, props: any, modalProps: any) {
  // 创建弹窗实例
  const dialog = h(
    ElDialog,
    {
      ...modalProps,
      modelValue: true,
    },
    {
      // 主要内容插槽
      default: () => h(component, props),
      // 底部插槽
      footer:() =>h(
        'div',
        { class: 'dialog-footer' },
        [
          h(
            ElButton, 
            {
              onClick: () => {
                console.log('取消')
              }
            },
            () => '取消'
          ),
          h(
            ElButton,
            { 
              type: 'primary',
              onClick: () => {
                console.log('确定')
              }
            },
            () => '确定'
          )
        ]
      )
    }
  );
  // 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
  const app = createApp(dialog)
  // 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
  app.use(ElementPlus);
  const div = document.createElement('div')
  document.body.appendChild(div)
  app.mount(div)
}

04

点击关闭弹窗时,需要移除之前创建的div

卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div。
2个地方需要移除:1,点击确认按钮。 2,点击其他地方的关闭
05

关闭弹窗正确销毁相关组件

// 封装的弹窗
import { createApp, h } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";

export function renderDialog(component: any, props: any, modalProps: any) {
  console.log('111')
  // 创建弹窗实例
  const dialog = h(
    ElDialog,
    {
      ...modalProps,
      modelValue: true,
      onClose: ()=> {
        console.log('关闭的回调')
        app.unmount() // 这样卸载会让动画消失
        // 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
        document.body.removeChild(div)
      }
    },
    {
      // 主要内容插槽
      default: () => h(component, props),
      // 底部插槽
      footer:() =>h(
        'div',
        { 
          class: 'dialog-footer',
         
        },
        [
          h(
            ElButton, 
            {
              onClick: () => {
                console.log('点击取消按钮')
                // 卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
                app.unmount() // 这样卸载会让动画消失
                // 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
                document.body.removeChild(div)
              }
            },
            () => '取消'
          ),
          h(
            ElButton,
            { 
              type: 'primary',
              onClick: () => {
                console.log('确定')
              }
            },
            () => '确定'
          )
        ]
      )
    }
  );
  // 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
  const app = createApp(dialog)
  // 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
  app.use(ElementPlus);
  // 这个div元素在在销毁应用时需要被移除哈
  const div = document.createElement('div')
  document.body.appendChild(div)
  app.mount(div)
}

06

点击确认按钮时验证规则

有些时候,我们弹窗中的表单是需要进行规则校验的。
我们下面来实现这个功能点
传递的组件

<template>
  <el-form
    ref="ruleFormRef"
    style="max-width: 600px"
    :model="ruleForm"
    :rules="rules"
    label-width="auto"
  >
    <el-form-item label="Activity name" prop="name">
      <el-input v-model="ruleForm.name" />
    </el-form-item>
    <el-form-item label="Activity zone" prop="region">
      <el-select v-model="ruleForm.region" placeholder="Activity zone">
        <el-option label="Zone one" value="shanghai" />
        <el-option label="Zone two" value="beijing" />
      </el-select>
    </el-form-item>
    
    <el-form-item label="Activity time" required>
      <el-col :span="11">
        <el-form-item prop="date1">
          <el-date-picker
            v-model="ruleForm.date1"
            type="date"
            aria-label="Pick a date"
            placeholder="Pick a date"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
      <el-col class="text-center" :span="2">
        <span class="text-gray-500">-</span>
      </el-col>
      <el-col :span="11">
        <el-form-item prop="date2">
          <el-time-picker
            v-model="ruleForm.date2"
            aria-label="Pick a time"
            placeholder="Pick a time"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
    </el-form-item>

    <el-form-item label="Resources" prop="resource">
      <el-radio-group v-model="ruleForm.resource">
        <el-radio value="Sponsorship">Sponsorship</el-radio>
        <el-radio value="Venue">Venue</el-radio>
      </el-radio-group>
    </el-form-item>
    <el-form-item label="Activity form" prop="desc">
      <el-input v-model="ruleForm.desc" type="textarea" />
    </el-form-item>

  </el-form>
</template>

<script lang="ts" setup>
import { reactive, ref } from 'vue'

import type { FormInstance, FormRules } from 'element-plus'

interface RuleForm {
  name: string
  region: string
  date1: string
  date2: string
  resource: string
  desc: string
}
const ruleFormRef = ref<FormInstance>()
const ruleForm = reactive<RuleForm>({
  name: 'Hello',
  region: '',
  date1: '',
  date2: '',
  resource: '',
  desc: '',
})
const rules = reactive<FormRules<RuleForm>>({
  name: [
    { required: true, message: 'Please input Activity name', trigger: 'blur' },
    { min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
  ],
  region: [
    {
      required: true,
      message: 'Please select Activity zone',
      trigger: 'change',
    },
  ],
  date1: [
    {
      type: 'date',
      required: true,
      message: 'Please pick a date',
      trigger: 'change',
    },
  ],
  date2: [
    {
      type: 'date',
      required: true,
      message: 'Please pick a time',
      trigger: 'change',
    },
  ],
  resource: [
    {
      required: true,
      message: 'Please select activity resource',
      trigger: 'change',
    },
  ],
  desc: [
    { required: true, message: 'Please input activity form', trigger: 'blur' },
  ],
})

const submitForm = async () => {
  if (!ruleFormRef.value) {
    console.error('ruleFormRef is not initialized')
    return false
  }
  try {
    const valid = await ruleFormRef.value.validate()
    if (valid) {
      console.log('表单校验通过', ruleForm)
      return Promise.resolve(ruleForm)
    }
  } catch (error) {
    // 为啥submitForm中,valid的值是false会执行catch ?
    // el-form 组件的 validate 方法的工作机制导致的。 validate 方法在表单验证失败时会抛出异常
    console.error('err', error)
    return false
    /**
     * 下面这样写为啥界面会报错呢?
     * return Promise.reject(error)
     * 当表单验证失败时,ruleFormRef.value.validate() 会抛出一个异常。
     * 虽然你用了 try...catch 捕获这个异常,并且在 catch 块中通过 return Promise.reject(error) 返回了一个被拒绝的 Promise
     * 但如果调用 submitForm 的地方没有正确地处理这个被拒绝的 Promise(即没有使用 .catch() 或者 await 来接收错误),
     * 那么浏览器控制台就会显示一个 "Uncaught (in promise)" 错误。
     * 在 catch 中再次 return Promise.reject(error) 是多余的, 直接return false
     * */ 
    /**
     * 如果你这样写
     * throw error 直接抛出错误即可
     * 那么就需要再调用submitForm的地方捕获异常
     * */  
  }
}

defineExpose({
  submitForm:submitForm
})
</script>
// 封装的弹窗
import { createApp, h, ref } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";

export function renderDialog(component: any, props: any, modalProps: any) {
  const instanceElement = ref()
  console.log('111', instanceElement) 
  // 创建弹窗实例
  const dialog = h(
    ElDialog,
    {
      ...modalProps,
      modelValue: true,
      onClose: ()=> {
        console.log('关闭的回调')
        app.unmount() // 这样卸载会让动画消失
        // 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
        document.body.removeChild(div)
      }
    },
    {
      // 主要内容插槽,这里的ref必须接收一个ref
      default: () => h(component, {...props, ref: instanceElement}),
      // 底部插槽
      footer:() =>h(
        'div',
        { 
          class: 'dialog-footer',
         
        },
        [
          h(
            ElButton, 
            {
              onClick: () => {
                console.log('点击取消按钮')
                // 卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
                app.unmount() // 这样卸载会让动画消失
                // 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
                document.body.removeChild(div)
              }
            },
            () => '取消'
          ),
          h(
            ElButton,
            { 
              type: 'primary',
              onClick: () => {
                instanceElement?.value?.submitForm().then((res:any) =>{
                  console.log('得到的值',res)
                })
                console.log('确定')
              }
            },
            () => '确定'
          )
        ]
      )
    }
  );
  // 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
  const app = createApp(dialog)
  // 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
  app.use(ElementPlus);
  // 这个div元素在在销毁应用时需要被移除哈
  const div = document.createElement('div')
  document.body.appendChild(div)
  app.mount(div)
}

07
关键的点:通过ref拿到childTest组件中的方法,childTest要暴露需要的方法

如何把表单中的数据暴露出去

可以通过回调函数的方式把数据暴露出去哈。

// 封装的弹窗
import { createApp, h, ref } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";

export function renderDialog(component: any, props: any, modalProps: any, onConfirm: (data: any) => any ) {
  // 第4个参数是回调函数
  const instanceElement = ref()
  console.log('111', instanceElement) 
  // 创建弹窗实例
  const dialog = h(
    ElDialog,
    {
      ...modalProps,
      modelValue: true,
      onClose: ()=> {
        console.log('关闭的回调')
        app.unmount() // 这样卸载会让动画消失
        // 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
        document.body.removeChild(div)
      }
    },
    {
      // 主要内容插槽,这里的ref必须接收一个ref
      default: () => h(component, {...props, ref: instanceElement}),
      // 底部插槽
      footer:() =>h(
        'div',
        { 
          class: 'dialog-footer',
         
        },
        [
          h(
            ElButton, 
            {
              onClick: () => {
                console.log('点击取消按钮')
                // 卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
                app.unmount() // 这样卸载会让动画消失
                // 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
                document.body.removeChild(div)
              }
            },
            () => '取消'
          ),
          h(
            ElButton,
            { 
              type: 'primary',
              onClick: () => {
                // submitForm 调用表单组件中需要验证或者暴露出去的数据
                instanceElement?.value?.submitForm().then((res:any) =>{
                  console.log('得到的值',res)
                  // 验证通过后调用回调函数传递数据, 如验证失败,res 的值有可能是一个false。
                  onConfirm(res)
                  // 怎么把这个事件传递出去,让使用的时候知道点击了确认并且知道验证通过了
                }).catch((error: any) => {
                  // 验证失败时也可以传递错误信息
                  console.log('验证失败', error)
                })
                console.log('确定')
              }
            },
            () => '确定'
          )
        ]
      )
    }
  );
  // 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
  const app = createApp(dialog)
  // 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
  app.use(ElementPlus);
  // 这个div元素在在销毁应用时需要被移除哈
  const div = document.createElement('div')
  document.body.appendChild(div)
  app.mount(div)
}
<template>
  <div>
    <el-button @click="openMask">点击弹窗</el-button>
  </div>
</template>

<script setup lang="ts">
import childTest from '@/components/childTest.vue'
import { renderDialog } from '@/hooks/dialog'
import { getCurrentInstance } from 'vue';
const currentInstance = getCurrentInstance();
function openMask(){
  console.log('currentInstance',currentInstance)
  renderDialog(childTest,{},{title:'测试弹窗', width: '700'}, (res)=>{
    console.log('通过回调函数返回值', res)
  })
}
</script>

08

点击确定时,业务完成后关闭弹窗

现在想要点击确定,等业务处理完成之后,才关闭弹窗。
需要在使用完成业务的时候返回一个promise,让封装的弹窗调用这个promise
这样就可以知道什么时候关闭弹窗了

// 封装的弹窗
import { createApp, h, ref } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";

export function renderDialog(component: any, props: any, modalProps: any, onConfirm: (data: any) => any ) {
  // 第4个参数是回调函数
  const instanceElement = ref()
  console.log('111', instanceElement) 
  // 创建弹窗实例
  const dialog = h(
    ElDialog,
    {
      ...modalProps,
      modelValue: true,
      onClose: ()=> {
        console.log('关闭的回调')
        app.unmount() // 这样卸载会让动画消失
        // 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
        document.body.removeChild(div)
      }
    },
    {
      // 主要内容插槽,这里的ref必须接收一个ref
      default: () => h(component, {...props, ref: instanceElement}),
      // 底部插槽
      footer:() =>h(
        'div',
        { 
          class: 'dialog-footer',
         
        },
        [
          h(
            ElButton, 
            {
              onClick: () => {
                console.log('点击取消按钮')
                // 卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
                app.unmount() // 这样卸载会让动画消失
                // 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
                document.body.removeChild(div)
              }
            },
            () => '取消'
          ),
          h(
            ElButton,
            { 
              type: 'primary',
              onClick: () => {
                // submitForm 调用表单组件中需要验证或者暴露出去的数据
                instanceElement?.value?.submitForm().then((res:any) =>{
                  console.log('得到的值',res)
                  // 验证通过后调用回调函数传递数据,如验证失败,res 的值有可能是一个false。
                  const callbackResult = onConfirm(res);
                  // 如果回调函数返回的是 Promise,则等待业务完成后再关闭弹窗
                  if (callbackResult instanceof Promise) {
                    // 注意这里的finally,这样写在服务出现异常的时候会有问题,这里是有问题的,需要优化
                    // 注意这里的finally,这样写在服务出现异常的时候会有问题,这里是有问题的,需要优化
                    callbackResult.finally(() => { 
                      // 弹窗关闭逻辑
                      app.unmount()
                      document.body.removeChild(div)
                    });
                  } else {
                    // 如果不是 Promise,立即关闭弹窗
                    app.unmount()
                    document.body.removeChild(div)
                  }
                }).catch((error: any) => {
                  // 验证失败时也可以传递错误信息
                  console.log('验证失败', error)
                })
              }
            },
            () => '确定'
          )
        ]
      )
    }
  );
  // 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
  const app = createApp(dialog)
  // 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
  app.use(ElementPlus);
  // 这个div元素在在销毁应用时需要被移除哈
  const div = document.createElement('div')
  document.body.appendChild(div)
  app.mount(div)
}
<template>
  <div>
    <el-button @click="openMask">点击弹窗</el-button>
  </div>
</template>

<script setup lang="ts">
import childTest from '@/components/childTest.vue'
import { renderDialog } from '@/hooks/dialog'
import { getCurrentInstance } from 'vue';
const currentInstance = getCurrentInstance();
function openMask(){
  console.log('currentInstance',currentInstance)
  renderDialog(childTest,{},{title:'测试弹窗', width: '700'}, (res)=>{
    console.log('通过回调函数返回值', res)
    // 这里返回一个promise对象,这样就可以让业务完成后才关闭弹窗
    return fetch("https://dog.ceo/api/breed/pembroke/images/random")
     .then((res) => {
       return res.json();
     })
     .then((res) => {
        console.log('获取的图片地址为:', res.message);
     });
  })
}
</script>

09

优化业务组件

// 封装的弹窗
import { createApp, h, ref } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";

export function renderDialog(component: any, props: any, modalProps: any, onConfirm: (data: any) => any ) {
  // 关闭弹窗,避免重复代码
  const closeDialog = () => {
    // 成功时关闭弹窗
    app.unmount();
    // 检查div是否仍然存在且为body的子元素,否者可能出现异常
    if (div && div.parentNode) {
      document.body.removeChild(div)
    }
  }
  // 第4个参数是回调函数
  const instanceElement = ref()
  console.log('111', instanceElement) 
  // 创建弹窗实例
  const dialog = h(
    ElDialog,
    {
      ...modalProps,
      modelValue: true,
      onClose: ()=> {
        console.log('关闭的回调')
        app.unmount() // 这样卸载会让动画消失
        // 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
        document.body.removeChild(div)
      }
    },
    {
      // 主要内容插槽,这里的ref必须接收一个ref
      default: () => h(component, {...props, ref: instanceElement}),
      // 底部插槽
      footer:() =>h(
        'div',
        { 
          class: 'dialog-footer',
         
        },
        [
          h(
            ElButton, 
            {
              onClick: () => {
                console.log('点击取消按钮')
                // 卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
                app.unmount() // 这样卸载会让动画消失
                // 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
                document.body.removeChild(div)
              }
            },
            () => '取消'
          ),
          h(
            ElButton,
            { 
              type: 'primary',
              onClick: () => {
                // submitForm 调用表单组件中需要验证或者暴露出去的数据
                instanceElement?.value?.submitForm().then((res:any) =>{
                  console.log('得到的值',res)
                  // 验证通过后调用回调函数传递数据,如验证失败,res 的值有可能是一个false。
                  const callbackResult = onConfirm(res);
                  // 如果回调函数返回的是 Promise,则等待业务完成后再关闭弹窗
                  if (callbackResult instanceof Promise) {
                   
                     callbackResult.then(() => {
                      if(res){
                        console.log('111')
                        closeDialog()
                      }
                    }).catch(error=>{
                      console.log('222')
                      console.error('回调函数执行出错,如:网络错误', error);
                      // 错误情况下也关闭弹窗
                      closeDialog()
                    });
                  } else {
                    // 如果不是 Promise,并且验证时通过了的。立即关闭弹窗
                    console.log('333', res)
                    if(res){
                      closeDialog()
                    }
                  }
                }).catch((error: any) => {
                  console.log('44444')
                  // 验证失败时也可以传递错误信息
                  console.log('验证失败', error)
                })
              }
            },
            () => '确定'
          )
        ]
      )
    }
  );
  // 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
  const app = createApp(dialog)
  // 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
  app.use(ElementPlus);
  // 这个div元素在在销毁应用时需要被移除哈
  const div = document.createElement('div')
  document.body.appendChild(div)
  app.mount(div)
}
<template>
  <div>
    <el-button @click="openMask">点击弹窗</el-button>
  </div>
</template>
<script setup lang="ts">
import childTest from '@/components/childTest.vue'
import { renderDialog } from '@/hooks/dialog'
import { getCurrentInstance } from 'vue';
const currentInstance = getCurrentInstance();
function openMask(){
  console.log('currentInstance',currentInstance)
  renderDialog(childTest,{},{title:'测试弹窗', width: '700'}, (res)=>{
    console.log('通过回调函数返回值', res)
      // 这里返回一个promise对象,这样就可以让业务完成后才关闭弹窗
      return fetch("https://dog.ceo/api/breed/pembroke/images/random")
      .then((res) => {
        return res.json();
      })
      .then((res) => {
          console.log('获取的图片地址为:', res.message);
      });
  })
}
</script>

眼尖的小伙伴可能已经发现了这一段代码。
1,验证不通过会也会触发卸载弹窗
2,callbackResult.finally是不合适的
3.
image

10

最终的代码

// 封装的弹窗
import { createApp, h, ref } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";

export function renderDialog(component: any, props: any, modalProps: any, onConfirm: (data: any) => any ) {
  // 关闭弹窗,避免重复代码
  const closeDialog = () => {
    // 成功时关闭弹窗
    app.unmount();
    // 检查div是否仍然存在且为body的子元素,否者可能出现异常
    if (div && div.parentNode) {
      document.body.removeChild(div)
    }
  }
  // 第4个参数是回调函数
  const instanceElement = ref()
  console.log('111', instanceElement) 
  const isLoading = ref(false)
  // 创建弹窗实例
  const dialog = h(
    ElDialog,
    {
      ...modalProps,
      modelValue: true,
      onClose: ()=> {
        isLoading.value = false
        console.log('关闭的回调')
        app.unmount() // 这样卸载会让动画消失
        // 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
        document.body.removeChild(div)
      }
    },
    {
      // 主要内容插槽,这里的ref必须接收一个ref
      default: () => h(component, {...props, ref: instanceElement}),
      // 底部插槽,noShowFooterBool是true,不显示; false的显示底部 
      footer: props.noShowFooterBool ? null : () =>h(
        'div',
        { 
          class: 'dialog-footer',
        },
        [
          h(
            ElButton, 
            {
              onClick: () => {
                console.log('点击取消按钮')
                // 卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
                app.unmount() // 这样卸载会让动画消失
                // 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
                document.body.removeChild(div)
              }
            },
            () => props.cancelText || '取消'
          ),
          h(
            ElButton,
            { 
              type: 'primary',
              loading: isLoading.value,
              onClick: () => {
                isLoading.value = true
                // submitForm 调用表单组件中需要验证或者暴露出去的数据
                instanceElement?.value?.submitForm().then((res:any) =>{
                  if(!res){
                    isLoading.value = false
                  }
                  console.log('得到的值',res)
                  // 验证通过后调用回调函数传递数据,如验证失败,res 的值有可能是一个false。
                  const callbackResult = onConfirm(res);
                  // 如果回调函数返回的是 Promise,则等待业务完成后再关闭弹窗
                  if (callbackResult instanceof Promise) {
                     callbackResult.then(() => {
                      if(res){
                        console.log('111')
                        closeDialog()
                      }else{
                        isLoading.value = false
                      }
                    }).catch(error=>{
                      console.log('222')
                      console.error('回调函数执行出错,如:网络错误', error);
                      // 错误情况下也关闭弹窗
                      closeDialog()
                    });
                  } else {
                    // 如果不是 Promise,并且验证时通过了的。立即关闭弹窗
                    console.log('333', res)
                    if(res){
                      closeDialog()
                    }else{
                      isLoading.value = false
                    }
                  }
                }).catch((error: any) => {
                  console.log('44444')
                   isLoading.value = false
                  // 验证失败时也可以传递错误信息
                  console.log('验证失败', error)
                })
              }
            },
            () => props.confirmText ||  '确定'
          )
        ]
      ) 
    }
  );
  // 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
  const app = createApp(dialog)
  // 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
  app.use(ElementPlus);
  // 这个div元素在在销毁应用时需要被移除哈
  const div = document.createElement('div')
  document.body.appendChild(div)
  app.mount(div)
}
<template>
  <div>
    <el-button @click="openMask">点击弹窗</el-button>
  </div>
</template>

<script setup lang="ts">
import childTest from '@/components/childTest.vue'
import { renderDialog } from '@/hooks/dialog'
import { getCurrentInstance } from 'vue';
const currentInstance = getCurrentInstance();
function openMask(){
  console.log('currentInstance',currentInstance)
  const otherProps =  {cancelText:'取消哈', confirmText: '确认哈',showFooterBool:true }
  const dialogSetObject = {title:'测试弹窗哈', width: '700', draggable: true}
  renderDialog(childTest,otherProps,dialogSetObject, (res)=>{
    console.log('通过回调函数返回值', res)
    // 这里返回一个promise对象,这样就可以让业务完成后才关闭弹窗
    return fetch("https://dog.ceo/api/breed/pembroke/images/random")
    .then((res) => {
      return res.json();
    })
    .then((res) => {
        console.log('获取的图片地址为:', res.message);
    });
  })
}
</script>

<style lang="scss" scoped>

</style>
<template>
  <el-form
    ref="ruleFormRef"
    style="max-width: 600px"
    :model="ruleForm"
    :rules="rules"
    label-width="auto"
  >
    <el-form-item label="Activity name" prop="name">
      <el-input v-model="ruleForm.name" />
    </el-form-item>
    <el-form-item label="Activity zone" prop="region">
      <el-select v-model="ruleForm.region" placeholder="Activity zone">
        <el-option label="Zone one" value="shanghai" />
        <el-option label="Zone two" value="beijing" />
      </el-select>
    </el-form-item>
    
    <el-form-item label="Activity time" required>
      <el-col :span="11">
        <el-form-item prop="date1">
          <el-date-picker
            v-model="ruleForm.date1"
            type="date"
            aria-label="Pick a date"
            placeholder="Pick a date"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
      <el-col class="text-center" :span="2">
        <span class="text-gray-500">-</span>
      </el-col>
      <el-col :span="11">
        <el-form-item prop="date2">
          <el-time-picker
            v-model="ruleForm.date2"
            aria-label="Pick a time"
            placeholder="Pick a time"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
    </el-form-item>

  
    <el-form-item label="Resources" prop="resource">
      <el-radio-group v-model="ruleForm.resource">
        <el-radio value="Sponsorship">Sponsorship</el-radio>
        <el-radio value="Venue">Venue</el-radio>
      </el-radio-group>
    </el-form-item>
    <el-form-item label="Activity form" prop="desc">
      <el-input v-model="ruleForm.desc" type="textarea" />
    </el-form-item>

  </el-form>
</template>

<script lang="ts" setup>
import { reactive, ref } from 'vue'

import type { FormInstance, FormRules } from 'element-plus'

interface RuleForm {
  name: string
  region: string

  date1: string
  date2: string


  resource: string
  desc: string
}


const ruleFormRef = ref<FormInstance>()
const ruleForm = reactive<RuleForm>({
  name: 'Hello',
  region: '',
  date1: '',
  date2: '',
  resource: '',
  desc: '',
})



const rules = reactive<FormRules<RuleForm>>({
  name: [
    { required: true, message: 'Please input Activity name', trigger: 'blur' },
    { min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
  ],
  region: [
    {
      required: true,
      message: 'Please select Activity zone',
      trigger: 'change',
    },
  ],
  date1: [
    {
      type: 'date',
      required: true,
      message: 'Please pick a date',
      trigger: 'change',
    },
  ],
  date2: [
    {
      type: 'date',
      required: true,
      message: 'Please pick a time',
      trigger: 'change',
    },
  ],
  resource: [
    {
      required: true,
      message: 'Please select activity resource',
      trigger: 'change',
    },
  ],
  desc: [
    { required: true, message: 'Please input activity form', trigger: 'blur' },
  ],
})

const submitForm = async () => {
  if (!ruleFormRef.value) {
    console.error('ruleFormRef is not initialized')
    return false
  }
  try {
    const valid = await ruleFormRef.value.validate()
    if (valid) {
      // 验证通过后,就会可以把你需要的数据暴露出去
      return Promise.resolve(ruleForm)
    }
  } catch (error) {
    // 为啥submitForm中,valid的值是false会执行catch ?
    // el-form 组件的 validate 方法的工作机制导致的。 validate 方法在表单验证失败时会抛出异常
    console.error('err', error)
    return false
    /**
     * 下面这样写为啥界面会报错呢?
     * return Promise.reject(error)
     * 当表单验证失败时,ruleFormRef.value.validate() 会抛出一个异常。
     * 虽然你用了 try...catch 捕获这个异常,并且在 catch 块中通过 return Promise.reject(error) 返回了一个被拒绝的 Promise
     * 但如果调用 submitForm 的地方没有正确地处理这个被拒绝的 Promise(即没有使用 .catch() 或者 await 来接收错误),
     * 那么浏览器控制台就会显示一个 "Uncaught (in promise)" 错误。
     * 在 catch 中再次 return Promise.reject(error) 是多余的, 直接return false
     * */ 
    /**
     * 如果你这样写
     * throw error 直接抛出错误即可
     * 那么就需要再调用submitForm的地方捕获异常
     * */  
  }
}

defineExpose({
  submitForm:submitForm
})
</script>

posted @ 2026-01-13 09:10  南风晚来晚相识  阅读(63)  评论(0)    收藏  举报