Vue3之Dom操作——弹框组件封装

387 阅读5分钟

前言

之前我写了一篇关于组件封装的一篇文章,那篇文章中的组件封装还算是规规矩矩按照Vue的标准组件封装来的。都是直接引入使用。

而我们在使用element-plus的时候会发现,有那么一些组件的使用不太一样。在element-plus的弹框组件中,我们使用起来时是调用方法,并且弹框是生成的。

那么我们今天也来简单的写一个类似的组件。

思路分析

首先弹框的出现肯定还是要使用组件的,问题是element的方式是方法调用,所以我们要想办法写一个方法去生成组件实例。并且我们想想,弹框的出现一般都是要脱离目前页面的层级的,所以我们生成的位置在body上。

我们就着手这两点来进行:

组件实例的生成

在我们创建Vue3项目的时候在main.js中我们一定会看见有const app = createApp(App)这么一句,而createApp()传入的App就是App.vue这个根组件。

我们去应用实例 API | Vue.js (vuejs.org)看看这个方法。

image.png

那么我们生成组件的实例就很明确了,我们写好我们的弹框组件。我们就可以通过调用creatApp来生成实例。

生成实例之后我们就需要对实例进行挂载,这时就要使用app.mount()方法,官网上就在createApp那一篇的下面,这里就复述一下作用:

参数可以是一个实际的 DOM 元素或一个 CSS 选择器 (使用第一个匹配到的元素)。返回根组件的实例。如果该组件有模板或定义了渲染函数,它将替换容器内所有现存的 DOM 节点。否则在运行时编译器可用的情况下,容器元素的 innerHTML 将被用作模板。

但是我们并不能直接挂载到body元素上,这是因为我们在body元素上已经挂载了最基本的App.vue的这个实例。所以我们并不能直接app.mount(document.body)

于是,我们就需要手动在body元素上添加元素。这时就要用上appendChild了。但是还有一个问题,我们想使用appendChild就必须得到一个真实dom,我们现在得到的实例还不是真实dom。这时就有人说了,我们创建一个div元素,然后挂载到这个div元素上不就有真实dom了。

没错,这样的确可行,但是我们这里有更好的方法——Document.createDocumentFragment()

image.png

这样我们直接创建的dom也不会有其他的元素干扰,就是我们最纯正的组件。

我们将这一系列操作放入一个工具文件中:

// creat.js
import { createApp } from "vue";

/**
 * 实例创建方法
 * @param {组件实例} Component 
 * @param {组件配置项} option 
 * @returns 
 */
function create(Component, option) {
  const NoticeApp = createApp(Component, option);
  return showNotice(NoticeApp);
}

// 创建真实的dom元素
const showNotice = (app) => {
  // 创建文档片段
  const oFragment = document.createDocumentFragment();
  // 将创建的实例挂载到文档片段上并获取到根组件实例
  const vm = app.mount(oFragment);
  document.body.appendChild(oFragment);
  
  return vm
};

export default create;

组件的销毁

弹框生成之后总不能就一直在页面上了吧,我们还需要去销毁。

这里先看看弹框组件的代码:

<template>
  <div class="box" v-show="state.isShow">
    <h3>{{ title }}</h3>
    <p class="box-content">{{ message }}</p>
  </div>
</template>

<script setup>
import { ref, getCurrentInstance } from "vue";

const props = defineProps({
  title: {
    type: String,
    default: "",
  },
  message: {
    type: String,
    default: "",
  },
  duration: {
    type: Number,
    default: 1000,
  },
});

const state = ref({
  isShow: false,
});

const show = () => {
  state.value.isShow = true;
  setTimeout(() => {
    hide();
  }, props.duration);
};

const hide = () => {
  state.value.isShow = false;
};

defineExpose({
  show,
  state,
});
</script>

这里我们在组件内部去控制显示隐藏,但是这样并不能让我们主动去触发控制组件的显示和隐藏,所以我们需要将show方法通过defineExpose向外抛出,抛出的方法就能在mount那里返回的组件实例对象中获取到,这样就能主动去触发show方法了。

而这里的隐藏我们只是让元素不显示,在页面中弹框的dom元素我们依然能找到。而弹框出现后我们是希望它完全消失的,所以我们要删掉弹框的dom元素。

而我们只有在creat.js可以拿到弹框的dom元素,所以我们也要在这里来进行dom元素的删除。于是我们将控制显示隐藏的state参数也向外抛出,这样state也能在实例对象中拿到。当我们拿到state后我们只需要监听它来进行dom的删除便可。

于是我们就可以从vue中导入watch方法来进行监听,然后写一个删除方法调用app.unmount()来卸载实例。

// create.js
import { watch } from "vue";

const showNotice = (app) => {
    ...
    watch(vm.state, (state) => {
    if(!state.isShow) {
      hideNotice(app)
    }
    })
    ...
};

const hideNotice = (app) => {
  app.unmount()
};

其实我们在组件中使用v-if来控制,就不需要后面这些了。但是我们是手动创建的组件,这里还是要说说卸载的。用v-if的话vue已经帮我们实现了卸载的方法了。

使用

最后,我们直接调用create方法就行:

import Notice from "./components/Notice.vue";
import create from "./utils/create";


const show = () => {
  const notice = create(Notice, {
    title: "弹框标题",
    message: "弹框内容!",
    duration: 2000,
  });
  notice.show();
};

这里我们手动调用show方法,只是想表示我们返回的组件实例在其他组件中也是可以直接调用其中抛出内容的,这里的show方法的调用直接在creat.js里写也是没问题的,写在里面就更像element的调用了。

最后看看效果:

image.png 点击按钮,弹框出现

image.png 两秒后弹框消失

在dom中的表现:

image.png

总结

这次我们把操作dom的方法也讲了,vue写习惯之后像这样最原始的dom操作也不是很熟悉了。加上要把vue组件的虚拟dom变成实体dom,我一开始确实还是没啥头绪的,最后还是看着官方文档一步一步解决了问题。也能感觉现在我解决问题的方式对于以前确实有点进步。