Vue3 优雅的模态框封装方案 - 实践

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

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

Vue3 优雅的模态框封装方法 - 实践vue

经过前篇文章的介绍,你们已经了解了虚拟节点和瞬移组件,接下来咱们利用虚拟节点与瞬移组件,封装一个模态框组件。node

首先,得先明确咱们目标,就是咱们想要作出来的效果,就是兼容两个方式调用的模态框组件git

第一种经过 template 直接使用:app

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

第二种是直接 JavaScript 调起:函数

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

从这两段方式能够看出,不管是经过 Modal,仍是经过<model>..</model>,可传入参数都保持一致,因而可知,组件调用方式传参一致,因此咱们首先新建components/Modal/props.ts,在外部定义 props 参数类型:post

/** 模态框固定 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.vue
import { 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>

image.png
image.png

在测试一下,经过 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>

image.png
image.png

到这里,整个模态框的基本逻辑都组成了,在这基础下,就可基于需求下完善模态框与定制内容,也可经过该方法,二次封装 el-dialog 组件,只须要将 components/Modal/index.vue 的逻辑修改一下便可,下一篇文章,咱们在这基础下在进行完善,使得组件能彻底胜任业务需求。