相信大家在 Vue 中考虑复用逻辑的时候经常使用组件化开发,也肯定使用过函数式组件,就是那种在 js 中也能够导入调用的组件。那么如何去封装这么一个函数式组件呢,这篇文章将采用Toast组件简单介绍一下封装的方法,封装之后就能大大提高我们开发的效率了。
简单介绍一下声明式组件与函数式组件,大多数时候我们引入组件都采用声明式的的方式,这里以 Vant 组件库为例,类似 Button 按钮这种就是声明式组件:
<van-button type="primary">主要按钮van-button>
还有类似
这种自定义名称且在 .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”,先展示一下调用后的效果:
与创建声明式组件一致,在.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>
这是最关键的步骤,在 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;
代码如下(示例):
// main.js
import okToast from './plugins/okToast/index';
app.use(okToast);
答:目的是为了那个显示与消失的动画效果,当组件创建后需要组件内 ”isShow“ 产生变化才能触发
的动画效果,所以这里写了show函数方法。
答:简单来说,js文件传参数及函数给vue文件,均可在 createApp
的第二个参数中传递,vue文件相当于子组件,使用props的方式接收;vue文件传值及函数给js文件,可以通过 defineExpose
方法暴露出去,js文件中在应用实例创建完成后,就能拿到暴露出来的属性及方法。
<script setup>
import { getCurrentInstance } from 'vue';
// 获取当前实例,在当前实例相当于 vue2 中的 this
const { proxy }: any = getCurrentInstance();
// 最简单的调用方式,即可出来开头所展示的效果
proxy.$okToast();
// 传递自定义参数,与okToast.vue文件接收的参数对应
setTimeout(() => {
proxy.$okToast({
content: 'Hello World'
});
}, 2000);
</script>
import $okToast from "./plugs/okToast";
$okToast.show({
type: "warn",
content: "Network error,try again later",
});
上面封装的Toast组件在创建多个实例的时候,它们之间是互不干扰的,不会存在组件参数异常的情况。那么实际观察 DOM 元素我们会发现其在 DOM 上是存在多个的,只不过当多次调用的时候,后面的会把前面还没消失的Toast覆盖了,这样效果可能不那么友好。那么就存在两个优化方向:一是当后续出现Toast的时候结束掉前面出现的Toast,二是调整后续Toast出现的位置。
先上代码(示例):
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组件,其默认采用了单例模式,即同一时间只会存在一个,这种做法应该是普遍的弹窗做法。
先上代码(示例):
// 创建临时变量保存高度值
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%);
}
这里的做法提供给大家一种思路,实际的动画效果还有待优化,由于本文篇幅有限所以就不展开了,以后遇到这种需求再深入探索吧。
回过头来看了下调用方式确实不够优雅
proxy.$okToast({
content: 'Hello World'
});
而 vant 可以直接
showToast('提示内容');
所以有必要进行优化一下,针对传入参数的类型进行区分即可,下面是代码示例:
const toastFun = options => {
if(typeof options === 'object' && options !== null) {
okToast(options).show();
} else {
okToast({ content: String(options) }).show();
}
};
okToast.install = app => {
app.config.globalProperties.$okToast = options => toastFun(options);
};
// 定义show方法用于直接调用
okToast.show = options => toastFun(options);
以上就是全部内容,本文简单介绍了 Vue3 函数式组件的封装方法,将其以插件的方式使用app.use() 方法安装在 Vue 上,使其作为全局功能的工具,这就是 Vue3 中逻辑复用的插件 (Plugins) 写法。
如果此篇文章对您有帮助,欢迎您【点赞】、【收藏】!也欢迎您【评论】留下宝贵意见,共同探讨一起学习~