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 的邏輯修改一下便可,下一篇文章,咱們在這基礎下在進行完善,使得組件能徹底勝任業務需求。

相關文章
相關標籤/搜索