如何更高大上使用你的 Vue 弹窗组件?学习学习
背景
假设我现在有一个场景:有一个 父组件、子组件,子组件中有一个弹窗且能在子组件中控制弹窗的显隐,但是我在父组件中也需要去控制这个弹窗的显隐,我应该怎么做呢?
有人说,那你分别在 父组件、子组件 中去各自引入这个弹窗不就行了吗?但是这样的话你就得维护两套显隐的逻辑,还是挺麻烦的而且可能这个弹窗的数据是依赖于子组件的,你在父组件中去引入,好像没啥意义
ref 获取子组件实例
所以很多人的做法就是,将弹窗写在 子组件 中,然后子组件可以控制这个弹窗的显隐,然后父组件那边通过 ref 去获取子组件实例,从而去控制这个弹窗的显隐~
具体的代码如下,首先是弹窗 TestModal 的代码,我这里是用到了 ant-design-vue 的 Modal
// TestModal.vue
<template>
<Modal :visible="visible" :title="title" @cancel="cancel">
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Modal>
</template>
<script lang="ts" setup>
import { Modal } from 'ant-design-vue';
const emits = defineEmits(['update:visible', 'close']);
defineProps<{
visible: boolean;
title: string;
}>();
const cancel = () => {
emits('update:visible', false);
emits('close');
};
</script>
接着是子组件 Child 的代码
// child.vue
<template>
<div>
<Button @click="handleOpen">Child 按钮</Button>
<TestModal :visible="visible" title="我是一个弹窗" @cancel="handleCancel" />
</div>
</template>
<script lang="ts" setup>
import { ref, defineExpose } from 'vue';
import { Button } from 'ant-design-vue';
import TestModal from './TestModal.vue';
const visible = ref(false);
const handleOpen = () => {
visible.value = true;
};
const handleCancel = () => {
visible.value = false;
};
defineExpose({ handleOpen });
</script>
然后是父组件 index.vue 的代码
// index.vue
<template>
<div>
<Button @click="handleOpen">Father 按钮</Button>
<Child ref="child" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Button } from 'ant-design-vue';
import Child from './child.vue';
const child = ref<typeof Child | null>(null);
const handleOpen = () => {
// 父组件中去调用子组件方法,打开弹窗
if (!child.value) return;
child.value.handleOpen();
};
</script>
这样就能实现了 父组件、子组件 都能控制弹窗显隐的效果
思考
但是我们想一下,这样做其实是有缺点的,父组件想要打开弹窗,就得依赖于子组件暴露出来的方法,那如果某一天子组件不需要这个弹窗了,把弹窗相关逻辑都去掉了,那到时父组件就打不开这个弹窗了~那怎么才能让他们能不强绑定,又不用维护两套显隐呢?
命令式弹窗
不知道你们有没有看到那些组件库,打开弹窗有两种方法
- 组件式:比如
- 命令式:比如 Modal.open({ title: ‘标题’ })
其实我们就是要使用到命令式
,我们的预期代码效果是这样的,通过 useCommandComponent
这个 hooks,将 TestModal 二次包装,使其具备命令式控制的功能
// 子组件
<template>
<div>
<Button @click="handleOpen">Child 按钮</Button>
</div>
</template>
<script lang="ts" setup>
import { Button } from 'ant-design-vue';
import TestModal from './TestModal.vue';
import useCommandComponent from 'useCommandComponent';
const testModal = useCommandComponent(TestModal);
const handleOpen = () => {
testModal({
title: '我是一个弹窗',
});
};
</script>
// 父组件
<template>
<div>
<Button @click="handleOpen">Father 按钮</Button>
<Child />
</div>
</template>
<script lang="ts" setup>
import { Button } from 'ant-design-vue';
import Child from './child.vue';
import TestModal from './TestModal.vue';
import useCommandComponent from 'useCommandComponent';
const testModal = useCommandComponent(TestModal);
const handleOpen = () => {
testModal({
title: '我是一个弹窗',
});
};
</script>
实现思路
其实实现思路就是:通过命令式调用,把 TestModal 渲染成一个真实DOM
但是 TestModal 他是一个 Vue 组件,想要渲染成真实 DOM,需要分成几步
- 1、把 TestModal 转化为虚拟DOM,也就是 VNode
- 2、把 VNode 渲染挂载到一个节点上
- 3、把这个节点推入到 body 中
其实这三步对应了三个方法
- 创建虚拟DOM:
createVNode
- 把 VNode 渲染挂载到节点上:
render
- 把节点推入 body:
document.body.appendChild
最终实现源码
import {
Component,
ComponentPublicInstance,
createVNode,
getCurrentInstance,
render,
VNode,
} from 'vue';
import type {
ModalProps } from 'ant-design-vue';
export interface CommandComponent {
(options: ModalProps): VNode;
close: () => void;
}
export const useCommandComponent = <T extends Component>(Component: T): CommandComponent => {
// 获取 app 实例它身上的全局属性
// 比如 pinia $global
const appContext = getCurrentInstance()!.appContext;
// 创建一个容器,来放弹窗
const container = document.createElement('div');
// 自定义关闭方法
const cancel = () => {
render(null, container);
container.parentNode?.removeChild(container);
};
const CommandComponent = (options: ModalProps): VNode => {
// 参数判断
if (!Reflect.has(options, 'visible')) {
options.visible = true;
}
if (typeof options.onCancel !== 'function') {
options.onCancel = cancel;
} else {
const originOnCancel = options.onCancel;
options.onCancel = (e: MouseEvent) => {
originOnCancel?.(e);
cancel();
};
}
// 创建虚拟DOM
const vNode = createVNode(Component, options);
vNode.appContext = appContext;
// 渲染虚拟DOM到容器上
render(vNode, container);
// 将容器推入body
document.body.appendChild(container);
const vm = vNode.component?.proxy as ComponentPublicInstance<ModalProps>;
// 注入 props
for (const prop in options) {
if (Reflect.has(options, prop) && !Reflect.has(vm.$props, prop)) {
vm[prop as keyof ComponentPublicInstance] = options[prop];
}
}
return vNode;
};
CommandComponent.close = close;
return CommandComponent;
};
export default useCommandComponent;
&& !Reflect.has(vm.$props, prop)) {
vm[prop as keyof ComponentPublicInstance] = options[prop];
}
}
return vNode;
};
CommandComponent.close = close;
return CommandComponent;
};
export default useCommandComponent;