【vue3 & ts 】消息弹窗&模态框封装

前言

现在组件化已经是很平常了,市面也有很多的UI库,基本组件的轮子都可以直接使用,但是如果想提升自己的技术水平,学会如何正确的组件封装是必不可少的路,今天和大家聊聊基于 vue3 如何去封装一个组件, 我们自己动手去封装一个消息弹窗 & 模态框, 这里贴一个代码源码以及NPM地址,大家可以自行去阅读源码或者直接安装使用我的轮子,ok,闲言少叙,直接开始

怎么封装

写代码之前一定要先明确思路,如何明确思路呢,我们应该站在使用者角度去,先把怎么用明确了,再确定怎么实现, 从ui角度考虑,其实消息弹窗和模态框应该都是一样的,但是,从使用者角度,用法肯定是不一样的,模态框的用法应该是组件式的, 因为可能需要自定义内容,但是消息弹窗肯定是指令式的,我通过函数,传参即可打开,那么基于我们的思路开始实现我们的 my-message-box

模态框实现

既然消息弹窗和模态框都是一样的, 我们就先从模态框开始封装,这个其实比较简单,模态框分三个部分,header,main, footer, 分别对应标题,内容,操作按钮,其中我们的main和footer直接就做成插槽,当然, 也需要写默认的内容:

typescript 复制代码
<script setup lang="ts">
import { withDefaults } from 'vue'
import close from './close.png'

const props = withDefaults(
  defineProps<{
    modelValue: boolean
    title?: string
    content?: string
    closeBtnText?: string
    confirmBtnText?: string
    isShowConfirmBtn?: boolean
    isShowCloseBtn?: boolean
    closeOnOther?: boolean
  }>(),
  {
    closeBtnText: '取 消',
    confirmBtnText: '确 认',
    isShowConfirmBtn: true,
    isShowCloseBtn: true,
    isDeclarative: false,
    closeOnOther: true
  }
)

const emit = defineEmits<{
  (e: 'update:modelValue', value: boolean): void
  (e: 'confirm', onClose: Function): void
  (e: 'close'): void
}>()

function onConfirm() {
    emit('confirm', onClose)
}
function onClose() {
    emit('update:modelValue', false)
    emit('close')
  
}
function onCloseOnOther() {
  if (props.closeOnOther) {
    onClose()
  }
}
defineExpose()
</script>

<template>
  <div class="--sk-wrapper" v-if="modelValue" @click.self="onCloseOnOther">
    <div class="--sk-wrapper_body">
      <div class="--sk-wrapper_title">
        <span>{{ title }}</span>
        <img
          v-if="closeOnOther"
          @click="onClose"
          class="--sk-wrapper_title_close_icon"
          :src="close"
          alt="x"
        />
      </div>
      <div class="--sk-wrapper_content">
        <slot name="content">
          {{ content }}
        </slot>
      </div>
      <div class="--sk-wrapper_footer">
        <slot name="footer">
          <div class="--sk-wrapper_footer_button_box">
            <div
              v-if="isShowConfirmBtn"
              class="--sk-wrapper_footer_button __primary"
              @click="onConfirm"
            >
              {{ confirmBtnText }}
            </div>
            <div
              v-if="isShowCloseBtn"
              class="--sk-wrapper_footer_button"
              @click="onClose"
            >
              {{ closeBtnText }}
            </div>
          </div>
        </slot>
      </div>
    </div>
  </div>
</template>

<style scoped>
@import './dialog.css';
</style>

代码呢大概就长这样, css可以自行实现,这里指的一提的是最后的defineExpose(), 在我们实现组件库的时候 如果使用了 setup 语法糖, 一定要加上expose, 避免使用者通过 ref 来随意访问我们组件内的内容,从而造成超出意外的调用。那么到这里我们模态框就写好了,基本没什么难度, 接下来有两个重点,如何指令式调用它,以及如何正确的导出组件式调用

指令式调用

在 my-message-box 文件夹下, 我们创建一个 index.ts, 这个文件需要导出一个方法用于指令式调用,同时,还需要导出这个组件,让使用者通过组件式来调用,一个个的来实现,首先实现正确的导出指令,我们需要导出一个方法,里面包含了 alertconfirm, 一个只有确认按钮,一个带有确认和取消,两个方法都是返回 promise,来支持回调, 由于我们通过createApp实现,无法传递emit事件,Dialog组件是通过 v-model 来实现的双向绑定,所以我们只能通过props 来传递这些函数, 这也是我们为什么要说重要的是 正确的导出组件式调用,也是为了避免使用者使用组件式使用组件时传递了指令式的props造成超出预期的结果: 我们再dialog.vue中加入这三个props,并在对应的emit事件中通过判断isDeclarative参数判断这是否为指令式调用,从而实现规避,从而我们的 Dialog组件就改成了这样:

typescript 复制代码
<script setup lang="ts">
import { withDefaults } from 'vue'
import close from './close.png'

const props = withDefaults(
  defineProps<{
    modelValue: boolean
    title?: string
    content?: string
    closeBtnText?: string
    confirmBtnText?: string
    isShowConfirmBtn?: boolean
    isShowCloseBtn?: boolean
    closeOnOther?: boolean
    /***
     * 以下为非组件的Props, 仅提供给指令式
     */
    closeFn?: Function
    confirmFn?: Function
    isDeclarative?: boolean
  }>(),
  {
    closeBtnText: '取 消',
    confirmBtnText: '确 认',
    isShowConfirmBtn: true,
    isShowCloseBtn: true,
    isDeclarative: false,
    closeOnOther: true
  }
)

const emit = defineEmits<{
  (e: 'update:modelValue', value: boolean): void
  (e: 'confirm', onClose: Function): void
  (e: 'close'): void
}>()

function isRunCloseFn() {
  if (props.isDeclarative) {
    if (props.closeFn && typeof props.closeFn === 'function') {
      props.closeFn()
      return false
    }
  }
  return true
}
function isRunConfirmFn() {
  if (props.isDeclarative) {
    if (props.confirmFn && typeof props.confirmFn === 'function') {
      props.confirmFn(props.closeFn!)
      return false
    }
  }
  return true
}

function onConfirm() {
  if (isRunConfirmFn()) {
    emit('confirm', onClose)
  }
}
function onClose() {
  if (isRunCloseFn()) {
    emit('update:modelValue', false)
    emit('close')
  }
}
function onCloseOnOther() {
  if (props.closeOnOther) {
    onClose()
  }
}
defineExpose()
</script>

<template>
  <div class="--sk-wrapper" v-if="modelValue" @click.self="onCloseOnOther">
    <div class="--sk-wrapper_body">
      <div class="--sk-wrapper_title">
        <span>{{ title }}</span>
        <img
          v-if="closeOnOther"
          @click="onClose"
          class="--sk-wrapper_title_close_icon"
          :src="close"
          alt="x"
        />
      </div>
      <div class="--sk-wrapper_content">
        <slot name="content">
          {{ content }}
        </slot>
      </div>
      <div class="--sk-wrapper_footer">
        <slot name="footer">
          <div class="--sk-wrapper_footer_button_box">
            <div
              v-if="isShowConfirmBtn"
              class="--sk-wrapper_footer_button __primary"
              @click="onConfirm"
            >
              {{ confirmBtnText }}
            </div>
            <div
              v-if="isShowCloseBtn"
              class="--sk-wrapper_footer_button"
              @click="onClose"
            >
              {{ closeBtnText }}
            </div>
          </div>
        </slot>
      </div>
    </div>
  </div>
</template>

<style scoped>
@import './dialog.css';
</style>

然后我们把实现的方法写入:

typescript 复制代码
import { createApp, h } from 'vue'
import Dialog from './Dialog.vue'

interface MessageBox {
  title?: string
  content?: string
  closeBtnText?: string
  confirmBtnText?: string
}

function MessageBox() {
  return {
    alert(props: MessageBox) {
      return new Promise((resolve) => {
        const container = document.createElement('div')
        const messageBox = createApp(Dialog, {
          ...props,
          modelValue: true,
          confirmFn: onClose,
          closeFn: onClose,
          isDeclarative: true,
          isShowCloseBtn: false
        })
        messageBox.mount(container)
        document.body.appendChild(container)
        function onClose() {
          resolve(undefined)
          messageBox.unmount()
          document.body.removeChild(container)
        }
      })
    },
    confirm(props: MessageBox) {
      return new Promise((resolve, reject) => {
        const container = document.createElement('div')
        const messageBox = createApp(Dialog, {
          ...props,
          modelValue: true,
          closeFn: onClose,
          confirmFn: onConfirm,
          isDeclarative: true
        })
        messageBox.mount(container)
        document.body.appendChild(container)
        function onClose() {
          reject(undefined)
          messageBox.unmount()
          document.body.removeChild(container)
        }
        function onConfirm() {
          resolve(undefined)
          messageBox.unmount()
          document.body.removeChild(container)
        }
      })
    }
  }
}

const useMessageBox = MessageBox()
export { useMessageBox }

接着就是需要导出 Dialog 组件了,这个组件我们之前提到了,需要过滤掉指令调用的props避免造成问题,所以我们需要给他包一层,实现代码就是下面这样,通过 defineComponenth 函数来过滤掉props

typescript 复制代码
import Dialog from './Dialog.vue'

const MessageBox = defineComponent({
  props: {
    modelValue: Boolean,
    title: String,
    content: String,
    closeBtnText: String,
    confirmBtnText: String,
    isShowConfirmBtn: {
      type: Boolean,
      default: () => true
    },
    isShowCloseBtn: {
      type: Boolean,
      default: () => true
    }
  },
  render() {
    return h(
      Dialog,
      {
        ...this.$props,
        'onUpdate:modelValue': (value: boolean) => this.$emit('update:modelValue', value)
      },
      this.$slots
    )
  }
})

export { MessageBox }

通过 defineComponent 来导出这个组件,组件内容通过vue3的 h 渲染函数,这里注意, 由于组件调用是通过 v-model 来实现的开关,那么我们需要把onUpdate:modelValue写到h函数的第二个参数内,第三个参数的slot也不能忘了,因为我们现在导出的并不是 Dialog 组件,而是经过包装的 MessageBox 对象,所以要把参数插槽都给传递过去

文末

至此我们的轮子就造好了, 今天主要分享的是组件的实现思路以及一些注意的要点, 通过 消息弹窗 和 模态框 来举例,如果你看完相信你一定是有收获的, 最后关于造轮子,想提升自己的开发水平其实造轮子是个很好的办法,但是很多人不太会,包括如何打包,打包的配置,工程化,以及发布npm啥的,我看好像也没有文章说的特别清楚,因为这里面的点还蛮多,如果有想自己造轮子发布到npm的,可以在这里留言,如果想看的人多,我后面可能会开一个专栏写一些关于如何造轮子发布到npm

相关推荐
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
mosen8681 小时前
Uniapp去除顶部导航栏-小程序、H5、APP适用
vue.js·微信小程序·小程序·uni-app·uniapp
别拿曾经看以后~2 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
川石课堂软件测试2 小时前
性能测试|docker容器下搭建JMeter+Grafana+Influxdb监控可视化平台
运维·javascript·深度学习·jmeter·docker·容器·grafana
科技探秘人3 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人3 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6