my-blog:https://tuimao233.gitee.io/ma...css
经过前篇文章的介绍,你们已经了解了虚拟节点和瞬移组件,接下来咱们利用虚拟节点与瞬移组件,封装一个模态框组件。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>
在测试一下,经过 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
的逻辑修改一下便可,下一篇文章,咱们在这基础下在进行完善,使得组件能彻底胜任业务需求。