Vue3封装全局函数式组件


前言

相信大家在 Vue 中考虑复用逻辑的时候经常使用组件化开发,也肯定使用过函数式组件,就是那种在 js 中也能够导入调用的组件。那么如何去封装这么一个函数式组件呢,这篇文章将采用Toast组件简单介绍一下封装的方法,封装之后就能大大提高我们开发的效率了。


一、函数式组件是什么?

简单介绍一下声明式组件与函数式组件,大多数时候我们引入组件都采用声明式的的方式,这里以 Vant 组件库为例,类似 Button 按钮这种就是声明式组件:

<van-button type="primary">主要按钮</van-button>

还有类似 <TheWelcome /> 这种自定义名称且在 .vue 文件里引用其他 .vue 文件的就是声明式组件

<template>
  <main>
    <TheWelcome />
  </main>
</template>

<script setup lang="ts">
import TheWelcome from '../components/TheWelcome.vue';
</script>

而函数式组件则是通过调用 API 的方式快速唤起全局的组件,还是以 Vant 组件库为例,比如使用 Toast 组件,调用函数后会直接在页面中渲染对应的轻提示:

import {
    
     showToast } from 'vant';

showToast('提示内容');

通常我们使用函数式组件是在某个交互完成时触发,又或者是在非.vue文件里唤起全局的组件,例如封装axios,在axios.js中使用Toast组件显示报错信息:

showToast('服务器响应超时,请刷新当前页');

二、创建一个函数式组件

下面将创建一个自己定义的toast组件,由于这个toast组件默认是显示成功的,所以称之为“okToast”,先展示一下调用后的效果:
在这里插入图片描述

1. 封装toast组件

与创建声明式组件一致,在.vue文件里定义好组件接收的参数还有组件的样式。代码如下(示例):

<template>
  <!-- 加一点动画效果 -->
  <transition name="toast" @after-leave="onAfterLeave">
    <div class="toast" v-if="isShow" :style="{ width: toastWidth }">
      <!-- 手动点击隐藏弹窗 -->
      <div v-if="time < 0" class="cancel" @click="hidden"></div>
      <img
        v-if="type === 'success' || type === 'icon'"
        class="img"
        src="../../assets/images/[email protected]"
        alt="success"
      />
      <img v-if="type === 'warn'" class="img" src="../../assets/images/7vip_web_toast_warn.png" alt="warn" />
      <div v-if="content && type !== 'icon'" class="content" :style="{ textAlign }">{
   
   { content }}</div>
    </div>
  </transition>
</template>
<script setup>
  import {
      
       ref, computed } from "vue";

  const props = defineProps({
      
      
    //文案内容,默认success
    content: {
      
      
      type: String,
      default: "success",
    },
    //显示时间,默认2s,传小于0的值不自动消失,需要手动关闭
    time: {
      
      
      type: Number,
      default: 2000,
    },
    //宽度,默认310px,这里考虑传入的宽度可以用字符串也可以用数值,所以没有定义类型
    width: {
      
      
      default: 310,
    },
    //弹窗文案文本对齐方式,默认center
    textAlign: {
      
      
      type: String,
      default: "center",
    },
    //类型,默认图标(√),传'warn'显示(!),传其他值则不显示icon,传'icon'不显示文本
    type: {
      
      
      type: String,
      default: "success",
    },
    //接收的函数方法
    hide: {
      
      
      type: Function,
      default: () => {
      
      },
    },
  });
  // 弹窗显隐控制
  const isShow = ref(false);
  // 宽度控制,由于设计稿宽度是750px的宽度,这里通过计算属性,根据设备屏幕宽度自适应显示弹窗的宽度
  const toastWidth = computed(() => (parseInt(props.width.toString()) / 750) * document.documentElement.clientWidth + "px");
  // 显示弹窗方法
  const show = () => {
      
      
    isShow.value = true;
    if (props.time >= 0) {
      
      
      setTimeout(() => {
      
      
        isShow.value = false;
      }, props.time);
    }
  };
  // 隐藏弹窗方法
  const hidden = () => {
      
      
    isShow.value = false;
  };
  // 弹窗关闭后等动画结束再调用卸载逻辑
  const onAfterLeave = () => {
      
      
	props.hide();
  };
  // 将显示弹窗方法暴露出去
  defineExpose({
      
      
    show,
  });
</script>

<style lang="scss" scoped>
  .toast-enter-active,
  .toast-leave-active {
      
      
    transition: opacity 0.3s ease-out;
  }
  .toast-enter-from,
  .toast-leave-to {
      
      
    opacity: 0;
  }
  .toast {
      
      
    position: fixed;
    top: 45%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 99;
    background: #333333;
    border-radius: 20px;
    padding: 40px;
    text-align: center;
    .cancel {
      
      
      background: url("../../assets/images/[email protected]") no-repeat center / contain;
      position: absolute;
      top: 10px;
      right: 10px;
      width: 40px;
      height: 40px;
      &::before {
      
      
        content: "";
        position: absolute;
        top: -10px;
        right: -10px;
        bottom: -10px;
        left: -10px;
      }
    }
    .img {
      
      
      width: 80px;
      height: 80px;
    }
    .content {
      
      
      margin-top: 20px;
      font-size: 32px;
      color: #ffcc99;
      line-height: 30px;
      text-align: initial;
    }
  }
</style>

2. 创建应用实例

这是最关键的步骤,在 Vue2 的时候封装函数式组件使用的是 Vue.extend,利用这个基础的 Vue 构造器,能创建Vue子类实例,然而在 Vue3 官方删除了这个方法,但是也提供了新的api: createApp 给我们使用,利用 createApp 就能创建 Vue 应用实例了。代码如下(示例):

import {
    
     createApp } from "vue";
import OkToast from "./okToast.vue";

const okToast = options => {
    
    
  // 创建元素节点
  const rootNode = document.createElement("div");
  // 在body标签内部插入此元素
  document.body.appendChild(rootNode);
  // 创建应用实例(第一个参数是根组件。第二个参数可选,它是要传递给根组件的 props)
  const app = createApp(OkToast, {
    
    
    ...options,
    hide() {
    
    
      // 卸载已挂载的应用实例
      app.unmount();
      // 删除rootNode节点
      document.body.removeChild(rootNode);
    },
  });
  // 将应用实例挂载到创建的 DOM 元素上
  return app.mount(rootNode);
};

// 注册插件app.use()会自动执行install函数
okToast.install = app => {
    
    
  // 注册全局属性,类似于 Vue2 的 Vue.prototype
  app.config.globalProperties.$okToast = options => okToast(options).show();
};
// 定义show方法用于直接调用
okToast.show = options => okToast(options).show();

export default okToast;

3. 注册插件(可省略)

代码如下(示例):

// main.js
import okToast from './plugins/okToast/index';

app.use(okToast);

Q&A: 补充一些注释

①:为什么采用调用函数方法的方式去控制显隐

答:目的是为了那个显示与消失的动画效果,当组件创建后需要组件内 ”isShow“ 产生变化才能触发<Transition> 的动画效果,所以这里写了show函数方法。

②:函数式组件的这两个文件之间的联系

答:简单来说,js文件传参数及函数给vue文件,均可在 createApp 的第二个参数中传递,vue文件相当于子组件,使用props的方式接收;vue文件传值及函数给js文件,可以通过 defineExpose 方法暴露出去,js文件中在应用实例创建完成后,就能拿到暴露出来的属性及方法。

三、调用

1. 注册插件后在.vue文件内获取全局方法

<script setup>
import {
    
     getCurrentInstance } from 'vue';

// 获取当前实例,在当前实例相当于 vue2 中的 this
const {
    
     proxy }: any = getCurrentInstance();
// 最简单的调用方式,即可出来开头所展示的效果
proxy.$okToast();
// 传递自定义参数,与okToast.vue文件接收的参数对应
setTimeout(() => {
    
    
  proxy.$okToast({
    
    
	content: 'Hello World'
  });
}, 2000);
</script>

2. 可不注册插件,在.vue或.js文件内直接调用方法

import $okToast from "./plugs/okToast";

$okToast.show({
    
    
  type: "warn",
  content: "Network error,try again later",
});

四、优化改进

上面封装的Toast组件在创建多个实例的时候,它们之间是互不干扰的,不会存在组件参数异常的情况。那么实际观察 DOM 元素我们会发现其在 DOM 上是存在多个的,只不过当多次调用的时候,后面的会把前面还没消失的Toast覆盖了,这样效果可能不那么友好。那么就存在两个优化方向:一是当后续出现Toast的时候结束掉前面出现的Toast,二是调整后续Toast出现的位置。

1、单例模式(推荐)

先上代码(示例):

let rootNode = null;
let app = null;
const okToast = options => {
    
    
  const dom = document.body.querySelector('.my-ok-toast');
  if (!dom) {
    
    
    rootNode = document.createElement('div');
    // 给创建的元素设置 class 属性值
    rootNode.className = `my-ok-toast`;
    document.body.appendChild(rootNode);
  } else {
    
    
    // If you want to mount another app on the same host container, you need to unmount the previous app by calling `app.unmount()` first.
    app.unmount();
  }
  app = createApp(OkToast, {
    
    
    ...options,
    hide() {
    
    
      // 卸载已挂载的应用实例
      if (app) {
    
    
        app.unmount();
        app = null;
      }
      // 删除rootNode节点
      if (rootNode) {
    
    
        document.body.removeChild(rootNode);
        rootNode = null;
      }
    }
  });
  return app.mount(rootNode);
};

效果展示:
请添加图片描述

怎么去结束前面出现的Toast呢,我们只需要确保全局只渲染一个Toast弹窗就行,所以可以使用单例模式,单例模式即一个类只能有一个实例。类似Vant的Toast组件,其默认采用了单例模式,即同一时间只会存在一个,这种做法应该是普遍的弹窗做法。

2、多个提示弹窗

先上代码(示例):

// 创建临时变量保存高度值
let top = 0;
const okToast = options => {
    
    
  const rootNode = document.createElement('div');
  // 给创建的元素设置 class 属性值
  rootNode.className = `my-ok-toast`;
  document.body.appendChild(rootNode);
  const dom = document.body.querySelector('.my-ok-toast');
  // 若DOM中存在该元素则将新元素高度往下移动
  if (dom) {
    
    
    top += 120;
    rootNode.style.top = 80 + top + 'px';
  }
  const app = createApp(OkToast, {
    
    
    ...options,
    hide() {
    
    
      app.unmount();
      document.body.removeChild(rootNode);
    }
  });
  return app.mount(rootNode);
};

再将css样式添加到全局上

.my-ok-toast {
    
    
  position: fixed;
  z-index: 99;
  top: 80px;
  left: 50%;
  transform: translateX(-50%);
}

效果展示:
请添加图片描述

这里的做法提供给大家一种思路,实际的动画效果还有待优化,由于本文篇幅有限所以就不展开了,以后遇到这种需求再深入探索吧。


总结

以上就是全部内容,本文简单介绍了 Vue3 函数式组件的封装方法,将其以插件的方式使用app.use() 方法安装在 Vue 上,使其作为全局功能的工具,这就是 Vue3 中逻辑复用的插件 (Plugins) 写法。

如果此篇文章对您有帮助,欢迎您【点赞】、【收藏】!也欢迎您【评论】留下宝贵意见,共同探讨一起学习~


扩展阅读

  1. Vue3 插件
  2. Vant Toast 轻提示

猜你喜欢

转载自blog.csdn.net/m0_55119483/article/details/130100473
今日推荐