如何更高大上使用你的Vue弹窗组件

如何更高大上使用你的 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>

这样就能实现了 父组件、子组件 都能控制弹窗显隐的效果

在这里插入图片描述

思考

但是我们想一下,这样做其实是有缺点的,父组件想要打开弹窗,就得依赖于子组件暴露出来的方法,那如果某一天子组件不需要这个弹窗了,把弹窗相关逻辑都去掉了,那到时父组件就打不开这个弹窗了~那怎么才能让他们能不强绑定,又不用维护两套显隐呢?

扫描二维码关注公众号,回复: 17343303 查看本文章

命令式弹窗

不知道你们有没有看到那些组件库,打开弹窗有两种方法

  • 组件式:比如
  • 命令式:比如 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;


猜你喜欢

转载自blog.csdn.net/dfc_dfc/article/details/133915428