my-blog:https://tuimao233.gitee.io/ma...

Vue3 优雅的模态框封装办法 - 初探

Vue3 优雅的模态框封装办法 - 实际

通过前篇文章的介绍,大家曾经理解了虚构节点和瞬移组件,接下来咱们利用虚构节点与瞬移组件,封装一个模态框组件。

首先,得先明确咱们指标,就是咱们想要做进去的成果,就是兼容两个形式调用的模态框组件

第一种通过 template 间接应用:

  <model v-model="show" title="题目" @confirm="onConfirm" @clone="onClone">    我是模态框文字  </model>

第二种是间接 JavaScript 调起:

Modal({title: '题目', content: '我是模态框文字'})    .then(()=> {    })    .catch(()=> {    })

从这两段形式能够看出,无论是通过 Modal,还是通过<model>..</model>,可传入参数都保持一致,由此可见,组件调用形式传参统一,所以咱们首先新建components/Modal/props.ts,在内部定义 props 参数类型:

/** 模态框固定 props 参数, 用于调用模态框胜利|敞开|销毁 */export const modalProps = {  // 是否展现组件  modelValue: Boolean,  // 组件隐没时(移除实例)  vanish: Function,  // 组件调用胜利事件  resolve: Function,  // 组件调用失败事件  reject: Function}/** 组件内传入 props 参数, 用于模态框自定义性能 */export const componentProps = {  // 模态框题目  title: String,  // 模态框内容  content: String}/** 组件内所有 Props 参数, 合并参数 */export const props = {...modalProps, ...componentProps}

这一步实现之后,咱们在创立components/Modal/index.vue,导入 props 类型:

<template>  <div></div></template><script lang="ts">import { defineComponent } from 'vue'import { props } from './props'export default defineComponent({  props})</script><style lang="scss" scoped></style>

到这一步后,咱们在定义一个通过js代码渲染组件的办法:

// components/Modal/utils.vueimport { Component, h, render } from "vue"/** * 渲染组件实例 * @param Constructor 组件 * @param props 组件参数 * @returns 组件实例 */export const renderInstance = (Constructor: Component, props: Record<string, any>) => {  // 创立组件容器, 这一步是必须的, 在销毁组件时会应用到  const container = document.createElement('div')  // 在 props 增加组件隐没钩子, 移除以后实例, 将销毁办法提供给组件  // 这里不须要调用 document.body.removeChild(container.firstElementChild)  // 因为调用 render(null, container) 为咱们实现了这项工作  props.vanish = () => {    render(null, container)  }  // 创立虚构节点, 渲染组件  const vnode = h(Constructor, props)  render(vnode, container)  // 增加子元素(组件)至父元素  document.body.appendChild(container.firstElementChild)}

渲染办法定义实现后,咱们就能够先把通过 js 调起的办法给做了:

import { ExtractPropTypes, ref } from "vue"import Index from './index.vue'import { componentProps } from './props'import { renderInstance } from "./utils"/** 组件 Props 类型, ExtractPropTypes 可将 Constructor 转换为对应值类型 */type Props = ExtractPropTypes<typeof componentProps>/** 组件调用 resolve 返回后果 */type Result = { path: string }[]/** * 模态框调用办法 * @param props  * @returns {Promise} */export const Modal = (props: Props) => {  return new Promise<Result>((resolve, reject) => {    renderInstance(Index, {      // 这里 modelValue, 为了使组件可批改, 须要传入 ref      // 留神这块中央,咱们将这个值设置为 true 为了调起即间接展现组件      modelValue: ref(true),      ...props, resolve, reject    })  })}

这里须要留神的是,通过 h 函数创立的实例,其 props 在组件中,无奈通过 emit 批改,批改会生效,所以为了解决这个问题,须要在调起办法传入 modelValue Ref

接下来咱们进行欠缺components/Modal/index.vue组件的模态框逻辑:

<template>  <teleport to="body">    <!-- after-leave 组件动画完结时, 调用销毁组件(如果有的话) -->    <transition name="fade" @after-leave="vanish">      <div class="base-model__mask" v-show="show">        <div class="base-model__content">          <div class="base-model__title">{{ title }}</div>          <!-- 插入自定义插槽, 这里判断默认插槽有没有应用 -->          <!-- 如果应用, 则渲染插槽, 如果没有, 则渲染 content -->          <slot v-if="$slots['default']" />          <template v-else>{{ content }}</template>          <div class="base-model__control">            <span @click="onConfirm">确定</span>            <span @click="onClone">敞开</span>          </div>        </div>      </div>    </transition>  </teleport></template><script lang="ts">import { defineComponent, computed, isRef, nextTick, watch } from 'vue'import { props } from './props'export default defineComponent({  props,  setup: (props, { emit }) => {    // 组件显示的数据双向代理    const modelValue = computed({      get: () => <boolean>props.modelValue,      set: () => emit('update:modelValue')    })    // Modal 办法调用传入 props 无奈通过 emit 批改    // 所以如果传入间接是一个 ref 则间接应用    const show = isRef(props.modelValue) ? props.modelValue : modelValue    // 如果初始化为 true , 切换状态让动画失常显示    if (show.value) {      show.value = false      nextTick(() => show.value = true)    }    // 敞开事件, 调用 reject, 为了兼容模板上间接应用组件, 还要在调用一次 clone 事件    const onClone = () => {      props.reject?.()      emit('clone')      show.value = false    }    // 确定事件, 调用 resolve, 为了兼容模板上间接应用组件, 还要在调用一次 confirm 事件    const onConfirm = () => {      props.resolve?.()      emit('confirm')      show.value = false    }    return { show, onConfirm, onClone }  }})</script><style lang="scss" scoped>.base-model__mask {  position: fixed;  left: 0;  top: 0;  width: 100%;  height: 100%;  background-color: rgba(0, 0, 0, 0.4);}.base-model__content {  position: absolute;  border-radius: 20px;  width: 600px;  height: 300px;  background-color: #ffffff;  top: 50%;  left: 50%;  transform: translate(-50%, -50%);  padding: 20px;}.base-model__control {  position: absolute;  right: 0;  bottom: 20px;  span {    margin-right: 20px;  }}/* 组件动画 start */.fade-enter-active,.fade-leave-active {  transition: opacity 0.2s;}.fade-enter-from,.fade-leave-to {  opacity: 0;}.fade-enter-top,.fade-leave-from {  opacity: 1;}/* 组件动画 end */</style>

到了这里,咱们能够测试一下组件调用是否失常,例如,咱们通过应用 template 组件形式调用:

<template>  <img alt="Vue logo" src="./assets/logo.png" @click="show = true" />  <modal @clone="onClone" @confirm="onConfirm" v-model="show" title="我是题目" >    啦啦啦我是自定义内容  </modal></template><script lang="ts">import { defineComponent, ref } from 'vue'import Modal from './components/Modal/index.vue';export default defineComponent({  components: { Modal },  setup: () => {    const show = ref(false)    const onClone = () => {      console.log('模态框点击敞开')    }    const onConfirm = () => {      console.log('模态框点击确认')    }    return { onClone, onConfirm, show }  }})</script>


在测试一下,通过 JavaScript 调用模态框:

<template>  <img alt="Vue logo" src="./assets/logo.png" @click="onClick" /></template><script lang="ts">import { defineComponent, ref } from 'vue'import { Modal } from './components/Modal';export default defineComponent({  components: {  },  setup: () => {    const onClick = () => {      Modal({title: '我是题目~~~', content: '我是内容~~~'})        .then(() => {          console.log('组件调用胜利')        })        .catch(() => {          console.log('组件调用失败')        })    }    return {onClick}  }})</script>


到这里,整个模态框的根本逻辑都组成了,在这根底下,就可基于需要下欠缺模态框与定制内容,也可通过该办法,二次封装 el-dialog 组件,只须要将 components/Modal/index.vue 的逻辑批改一下即可,下一篇文章,咱们在这根底下在进行欠缺,使得组件能齐全胜任业务需要。