前端组件:Vue弹窗组件改造,从模板调用到JS函数调用实现

404 阅读2分钟

Vue弹窗组件改造:从模板调用到JS函数调用实现

前言

前端项目开发中,弹窗组件是必不可少的UI组件之一。项目中现有的弹窗使用方式通常需要在模板中声明组件, 然后通过响应式数据控制显示隐藏。这种方式虽然直观,但在某些场景下使用起来不够灵活和便捷。 所以尝试将弹窗组件改造为支持JS函数调用的方式,定制一些通用的dialog弹窗便捷调用。

核心实现解析

1. 基础弹窗组件 (MyDialog.vue)

首先,我们有一个基础的弹窗组件,基于Vant UI的Dialog组件封装:

<template>
  <div class="dialog" ref="mydialog">
    <van-dialog
      v-model="isShow"
      :showConfirmButton="false"  
      :width="rwidth"
      @opened="opened"
      :close-on-click-overlay="overlay"
      :before-close="beforeClose"
    >
      <div slot="default">
        <slot name="mydefault" v-if="$slots.mydefault" />
        <template v-else>
          <div :class="title ? 'hasTitle' : 'noTitle'">
            {{ title }}
            <img
              src="@/assets/imgs/detail-icon-x.png" 
              class="icon_close"
              @click="hideDialog"
              v-if="closedable"
            />
          </div>
          <div class="content" :class="{ defaultPadding: !contentPadding }">
            <slot />
          </div>
        </template>
      </div>
    </van-dialog>
  </div>
</template>

<script>
import { Dialog } from "vant";

export default {
  name: "MyDialog",
  components: {
    [Dialog.Component.name]: Dialog.Component
  },
  model: {
    prop: 'show',
    event: 'hide'
  },
  props: ["title", "width", "show", "closedable", "contentPadding", "noOverlay"],
  // ... 其他实现
}
</script>

2. JS调用工具函数 (dialog.js)

这是改造的核心部分,通过Vue.extend()动态创建组件实例:

import Vue from 'vue'
import MyDialog from '@/components/MyDialog.vue'

/**
 * 公共dialog弹窗 js调用版
 * @param {Object} options 配置项
 * @param {string} [options.title='提示'] 标题
 * @param {string} options.content 内容
 * @param {number} [options.width=960] 弹窗宽度
 * @param {boolean} [options.closedable=true] 是否可关闭
 * @param {boolean} [options.showCancel=true] 是否显示取消按钮
 * @param {boolean} [options.showConfirm=true] 是否显示确认按钮
 * @param {string} [options.cancelText='取消'] 取消按钮文本
 * @param {string} [options.confirmText='确定'] 确认按钮文本
 * @param {boolean} [options.isHtml=false] 内容是否为HTML
 * @param {Function} [options.onClose] 关闭回调
 * @param {Function} [options.onCancel] 取消回调
 * @param {Function} [options.onConfirm] 确认回调
 * @returns {Object} dialog实例,包含close方法用于关闭弹窗
 */
export function showDialog(options = {}) {
  // 创建一个div容器
  const container = document.createElement('div')
  document.body.appendChild(container)

  // 创建组件构造器
  const DialogConstructor = Vue.extend({
    components: {
      MyDialog
    },
    data() {
      return {
        show: true,
        title: options.title || '提示',
        content: options.content || '',
        width: options.width || 960,
        closedable: options.closedable ?? true,
        showCancel: options.showCancel ?? true,
        showConfirm: options.showConfirm ?? true,
        cancelText: options.cancelText || '取消',
        confirmText: options.confirmText || '确定',
        isHtml: options.isHtml ?? false
      }
    },
    methods: {
      handleClose() {
        options.onClose?.call(this)
        this.close()
      },
      handleCancel() {
        // 回调返回false时阻止自动关闭
        const shouldClose = options.onCancel?.call(this)
        if (shouldClose !== false) {
          this.close()
        }
      },
      handleConfirm() {
        // 回调返回false时阻止自动关闭
        const shouldClose = options.onConfirm?.call(this)
        if (shouldClose !== false) {
          this.close()
        }
      },
      close() {
        this.show = false
        // 等待过渡动画结束后销毁
        setTimeout(() => {
          destroy()
        }, 300)
      },
      // 创建内容VNode
      createContent(h) {
        if (this.isHtml) {
          return h('div', {
            style: {
              fontSize: '16px',
              color: '#666',
              textAlign: 'center',
              marginBottom: '40px',
              marginTop: '20px',
              lineHeight: '24px'
            },
            domProps: {
              innerHTML: this.content
            }
          })
        }
        return h('div', {
          style: {
            fontSize: '16px',
            color: '#666',
            textAlign: 'center',
            marginBottom: '40px',
            marginTop: '20px',
            lineHeight: '24px'
          }
        }, this.content)
      }
    },
    render(h) {
      return h('my-dialog', {
        props: {
          show: this.show,
          title: this.title,
          width: this.width,
          closedable: this.closedable
        },
        on: {
          hide: () => {
            this.handleClose()
          }
        }
      }, [
        h('div', {
          style: {
            padding: '20px 15px'
          }
        }, [
          // 内容区域
          this.createContent(h),
          // 底部按钮区域
          h('div', {
            style: {
              display: 'flex',
              justifyContent: 'center',
              gap: '20px'
            }
          }, [
            this.showCancel && h('div', {
              style: {
                minWidth: '120px',
                height: '46px',
                background: '#f5f5f5',
                borderRadius: '8px',
                fontSize: '16px',
                color: '#666',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                cursor: 'pointer',
                paddingLeft: '10px',
                paddingRight: '10px',
                boxSizing: 'border-box',
                whiteSpace: 'nowrap',
              },
              on: { click: this.handleCancel }
            }, this.cancelText),
            this.showConfirm && h('div', {
              style: {
                minWidth: '120px',
                height: '46px',
                background: '#327bf9',
                borderRadius: '8px',
                fontSize: '16px',
                color: '#fff',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                cursor: 'pointer',
                paddingLeft: '10px',
                paddingRight: '10px',
                boxSizing: 'border-box',
                whiteSpace: 'nowrap',
              },
              on: { click: this.handleConfirm }
            }, this.confirmText)
          ].filter(Boolean))
        ])
      ])
    }
  })

  // 挂载组件
  const instance = new DialogConstructor().$mount(container)

  // 销毁方法
  function destroy() {
    instance.$destroy()
    container.remove()
  }

  // 返回实例和控制方法
  return {
    instance,
    close: () => instance.close(),
    updateContent: (content) => {
      instance.content = content
    },
    updateTitle: (title) => {
      instance.title = title
    }
  }
}

核心技术要点

1. Vue.extend() 动态创建组件

const DialogConstructor = Vue.extend({
  // 组件选项
})
const instance = new DialogConstructor().$mount(container)

使用Vue.extend()创建组件构造器,然后实例化并挂载到DOM中。这样可以在运行时动态创建组件实例。

2. render函数实现灵活渲染

通过render函数而不是模板来创建组件,可以更加灵活地控制组件的渲染逻辑:

render(h) {
  return h('my-dialog', {
    props: { /* props */ },
    on: { /* events */ }
  }, [
    // 子节点
  ])
}

3. 回调机制控制弹窗关闭

handleConfirm() {
  const shouldClose = options.onConfirm?.call(this)
  if (shouldClose !== false) {
    this.close()
  }
}

通过检查回调函数的返回值来决定是否自动关闭弹窗,返回false时阻止自动关闭。

4. 内存管理

function destroy() {
  instance.$destroy()
  container.remove()
}

确保组件销毁时正确清理内存,避免内存泄漏。

使用示例

改造后的弹窗使用变得非常简洁:

1. 基础用法

import { showDialog } from "@/utils/dialog";

// 简单提示
showDialog({
  title: '提示',
  content: '操作成功!',
  showCancel: false
});

// 确认对话框
showDialog({
  title: '确认删除',
  content: '确定要删除这条数据吗?',
  onConfirm() {
    console.log("用户点击确定");
    // 执行删除逻辑
    deleteData();
  },
  onCancel() {
    console.log("用户点击取消");
  }
});

2. HTML内容渲染

showDialog({
  title: '详细信息',
  content: `
    <div style="color: red; font-weight: bold;">
      重要提示
    </div>
    <p>这是一段包含HTML标签的内容</p>
  `,
  isHtml: true
});

3. 异步控制关闭

const dialog = showDialog({
  title: "处理中", 
  content: "正在提交数据,请稍候...",
  showCancel: false,
  showConfirm: false
});

// 模拟异步操作
setTimeout(() => {
  dialog.updateContent("提交成功!");
  setTimeout(() => {
    dialog.close();
  }, 1000);
}, 2000);

4. 阻止自动关闭

showDialog({
  title: "表单验证",
  content: "请确认信息无误后提交",
  onConfirm() {
    // 执行表单验证
    if (!validateForm()) {
      alert("表单验证失败,请检查输入");
      return false; // 返回false阻止弹窗关闭
    }
    // 验证通过,允许关闭
    submitForm();
  }
});

改造效果对比

改造前(模板方式)

  • ❌ 需要在每个组件中声明弹窗组件
  • ❌ 需要维护多个响应式状态
  • ❌ 代码冗余,复用性差
  • ❌ 模板复杂度高

改造后(JS调用方式)

  • ✅ 一行代码即可调用弹窗
  • ✅ 无需维护额外状态
  • ✅ 高度可复用
  • ✅ API简洁清晰
  • ✅ 支持动态配置
  • ✅ 支持异步控制

优化思考

  • ❌路由切换时需要做好组件销毁或隐藏