vue3 + Ts 封装全局函数式组件教程——确认弹窗组件

前言

函数式组件是没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法,它只是一个接受一些 prop 的函数,下面这篇文章主要给大家介绍了关于Vue3封装全局函数式组件的知识点以及实战案例,需要的朋友可以参考下。

Sayings:

相信大家在 Vue 中考虑复用逻辑的时候经常使用组件化开发,也肯定使用过函数式组件,就是那种在 js 中也能够导入调用的组件。那么如何去封装这么一个函数式组件呢,这篇文章将通过 GtMessageBox 组件详细介绍一下封装的方法,封装之后就能大大提高我们开发的效率了。
介绍前我们先来聊聊前端比较火的ElementUI,它有一个MessageBox 消息弹窗组件,其中有一个确认消息弹窗,是这样用的:
  Click to open the Message Box
import { ElMessage, ElMessageBox } from 'element-plus'
const open = () => {  ElMessageBox.confirm(    'proxy will permanently delete the file. Continue?',    'Warning',    {      confirmButtonText: 'OK',      cancelButtonText: 'Cancel',      type: 'warning',    }  )    .then(() => {      ElMessage({        type: 'success',        message: 'Delete completed',      })    })    .catch(() => {      ElMessage({        type: 'info',        message: 'Delete canceled',      })    })}
首先从组件库中引用相关组件ElMessageBox组件,然后直接在setup中通过 ElMessageBox.confirm 调取确认弹窗,明显confirm是一个函数,同时返回promise对象,将确认与取消绑定了promise对象的两个返回状态。
所以接下来我们就模仿它实现一个自己的确认弹窗组件。(之所以做这样的组件,其实是ElementUI的交互样式修改太过麻烦,但是项目又必须按照公司的UI规范开发,所以自己去实现一个类似功能组件是一个利于前端架构演进的实现路径)。

01——什么是函数式组件?

到目前为止,前端组件类型分为两类:1、声明式组件;2、函数式组件。

很遗憾的一点是:我面试过非常多的说自己擅长组件化开发的前端工程师,在问到函数式组件时却表示没有开发过此类组件同时对原理也是未曾了解过

一、声明式组件

这个组件类型是我们最熟悉的也是最常用的组件了,比如ElementUI中的Button按钮组件是这样用的:
  Click to open the Message Box
import { ElButton } from 'element-plus'

还包括我们自己在项目中写的自定义名称组件且在.vue文件里引用其他.vue文件的就是声明式组件,例如:

      import GtButton from '@/components/GtButton.vue';

二、函数式组件

这个组件类型则是通过调用 API 的方式快速唤起全局的组件,还是以 ElementUI 组件库为例,比如使用 ElMessageBox 组件,调用函数后会直接在页面中渲染对应的弹窗。
通常我们使用函数式组件是在某个交互完成时触发,又或者是在非.vue文件里唤起全局的组件,例如封装axios,在axios.js中使用 Message 组件显示报错信息。
ElMessage.error('Oops, this is a error message.')
三、Vue3中封装函数式组件关键API
# createApp()
创建一个应用实例。
类型:
function createApp(rootComponent: Component, rootProps?: object): App
详细信息:
第一个参数是根组件。第二个参数可选,它是要传递给根组件的 props
# app.mount()
将应用实例挂载在一个容器元素中。
类型:
interface App {  mount(rootContainer: Element | string): ComponentPublicInstance}
详细信息:
参数可以是一个实际的 DOM 元素或一个 CSS 选择器 (使用第一个匹配到的元素)。返回根组件的实例。
如果该组件有模板或定义了渲染函数,它将替换容器内所有现存的 DOM 节点。否则在运行时编译器可用的情况下,容器元素的 innerHTML 将被用作模板。
 SSR 激活模式下,它将激活容器内现有的 DOM 节点。如果出现了激活不匹配,那么现有的 DOM 节点将会被修改以匹配客户端的实际渲染结果。
对于每个应用实例,mount() 仅能调用一次。
# app.unmount()
卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
类型:
interface App {  unmount(): void}
在Js严谨模式下,我们应当在关闭确认弹窗时将已挂载的应用实例进行卸载,避免内存泄漏问题。
四、稍微扩展一下Vue2中封装函数式组件的关键API
# Vue.extend

利用这个基础的 Vue 构造器,能创建Vue子类实例,然而在 Vue3 官方删除了这个方法,但是也提供了新的api:createApp 给我们使用,利用 createApp 就能创建 Vue 应用实例了。


 

02——创建自定义函数式组件——确认弹窗组件

下面将创建一个自定义的确认弹窗组件,先展示一下调用后的效果:

首先我们在 components 文件夹下新建组件文件夹 gtMessageBox ,然后在 gtMessageBox 文件夹中新建两个文件,一个 module.vue 文件用来正常写 vue 的组件【定义好接收的参数、组件的样式】;一个 index.ts 文件用来作为组件的引用出口同时将 module.vue 转化为DOM实例进行展示。

一、封装 GtMessageBox 组件

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

{{ headerText }}

{{content}}

    import { ref, computed, nextTick } from 'vue';import gtButton from '@/components/gtButton/index.vue';const props = defineProps({    width: {        default: 400    },    successText: {        type: String,        default: '确认'    },    cancelText: {        type: String,        default: '取消'    },    headerText: {        type: String,        default: '提示'    },    content: {        type: String,        default: ''    },    // 成功回调函数    successPromise: {        type: Function    },    // 失败回调函数    cancelPromise: {        type: Function    },    hide: {        type: Function    },    // 是否需要右上角关闭按钮    showCancel: {        type: Boolean,        default: true    },    // 弹窗类型:default 默认显示确认、取消/confirm 仅显示确认    type: {        type: String,        default: 'default'    },    // 默认点击遮罩层不关闭    closeByOverlay: {        type: Boolean,        default: false    },    // 传入调用方法    holdOnFn: {        type: Function    },    overlayNode: {        type: Object    },});const isShow = ref(false);  // 弹窗控制const messageBoxWidth = computed(    () =>        ((parseInt(props.width.toString()) / 1920) * document.documentElement.clientWidth).toFixed(3) +        'px');  // 宽度控制
/** * @description: 遮罩层需要消失动画,两个动画效果必须同时触发,所以关闭时直接调用hide方法 * @return {*} */const onAfterLeave = () => {    // props.hide && props.hide();};
/** * @description: 显示弹窗 * @return {*} */const show = () => {    isShow.value = true;    if (props.closeByOverlay) {        props.overlayNode.addEventListener('click', hidden);    }};
/** * @description: 隐藏弹窗 * @return {*} */const hidden = () => {    isShow.value = false;    props.hide();    if (props.closeByOverlay) {        props.overlayNode.removeEventListener('click', hidden);    }};
const successHandle = () => {    if (!props.holdOnFn) {        props.successPromise();        nextTick(() => {            hidden();        });    } else {        props.holdOnFn();        // nextTick(() => {        //     hidden();        // });    }};const cancelHandle = () => {    props.cancelPromise();    nextTick(() => {        hidden();    });}
/** * @description: 将方法暴露出去 * @return {*} */defineExpose({    show});.gt-message-box{    position: fixed;    top: 45%;    left: 50%;    transform: translate(-50%, -50%);    z-index: 99;    .message-box-content{        width: 100%;        padding: 30px;        border-radius: 14px;        background: #FFFFFF;        .content-header{            display: flex;            justify-content: space-between;            align-items: center;            .header-title{                flex: 1;                font-size: 18px;                color: #1A1A1A;                line-height: 24px;            }            .header-close{                flex-shrink: 0;                padding: 0 0 0 24px;                .gticon-a-rongqi69{                    font-size: 24px;                    color: #949494;                }            }        }        .content-body{            display: flex;            justify-content: center;            width: 100%;            padding: 30px 0 40px;            .body-text{                max-width: 100%;                font-size: 14px;                color: #1A1A1A;                line-height: 18px;            }        }        .content-footer{            display: flex;            justify-content: flex-end;            .footer-btn{                margin-left: 20px;            }        }    }}/** * @description: 全局样式 * @return {*} */.gt-message-box-overlay {    position: fixed;    top: 0;    bottom: 0;    left: 0;    right: 0;    z-index: 100;    background: rgba(0, 0, 0, 0.1);    transition: opacity 0.3s ease-out;}.gt-message-box-overlay-enter-from,.gt-message-box-overlay-leave-to {    opacity: 0;}

二、将上述 .vue 创建应用实例

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

这部分在 index.ts 文件中:

import { createApp } from 'vue';import GtMessageBox from '@/components/gtMessageBox/module.vue';
/** * @description: 创建自定义vue组件实例并挂载到Dom上 * @return {*} * @param {*} options */const gtMessageBoxDom = (options = {}) => {    // 创建遮罩层    let overlayNode = createOverlay();
    // 创建弹窗元素节点    const rootNode: HTMLElement = document.createElement('div');    // 在body标签内部插入此元素    rootNode.className = `gt-message-dialog`;    rootNode.style.position = 'fixed';    rootNode.style.zIndex = overlayNodeZIndex + '';    document.body.appendChild(rootNode);
    const hide = function () {        // 显示移除动画        overlayNode.classList.add('gt-message-box-overlay-leave-to');        setTimeout(() => {            overlayNode.classList.remove('gt-message-box-overlay-leave-to');            deleteOverlay(overlayNode);        }, 300);
        // 卸载已挂载的应用实例        setTimeout(() => {            app.unmount();            document.body.contains(rootNode) && document.body.removeChild(rootNode);        }, 300);    };
    // 创建应用实例(第一个参数是根组件。第二个参数可选,它是要传递给根组件的 props)    const app = createApp(GtMessageBox, {        ...options,        hide,        overlayNode    });
    // 将应用实例挂载到创建的 DOM 元素上    return app.mount(rootNode);};
/** * @description: 显示弹窗 * @return {*} * @param {*} options */const showGtMessageBoxDom = (options = {}) => {    return new Promise((resolve, reject) => {        options['successPromise'] = () => {            resolve();        };        options['cancelPromise'] = () => {            reject();        };        const popres: any = gtMessageBoxDom(options);        popres.show();    });};
// 注册插件app.use()会自动执行install函数gtMessageBoxDom.install = app => {    // 注册全局属性,类似于 Vue2 的 Vue.prototype    app.config.globalProperties.$GtMessageBox = options => showGtMessageBoxDom(options);};// 定义confirm方法用于直接调用gtMessageBoxDom.confirm = options => showGtMessageBoxDom(options);
export default gtMessageBoxDom;

三、注册插件(可选择不用这种形式使用组件)
代码如下:
在项目中的main.js文件中:
// Vue3封装全局函数式组件import GtMessageBox from '@/components/gtMessageBox/index';app.use(GtMessageBox); // Vue3封装全局函数式组件
注释:
①:为什么采用调用函数方法的方式去控制显隐
答:目的是为了那个显示与消失的动画效果,当组件创建后需要组件内 isShow 产生变化才能触发 的动画效果,所以这里写了show函数方法。
②:函数式组件的这两个文件之间的联系
答:简单来说,ts 文件传参数及函数给vue文件,均可在 createApp 的第二个参数中传递,vue文件相当于子组件,使用props的方式接收;vue文件传值及函数给ts文件,可以通过 defineExpose 方法暴露出去,ts文件中在应用实例创建完成后,就能拿到暴露出来的属性及方法。
四、调用组件
1、第三步注册插件后在 .vue 文件内获取全局方法。
import { getCurrentInstance } from 'vue';// 获取当前实例,在当前实例相当于 vue2 中的 thisconst { proxy }: any = getCurrentInstance();// 最简单的调用方式,即可出来开头所展示的效果proxy.$GtMessageBox({  content: '此审批已经被删除,请刷新页面!'});// 传递自定义参数,与 module.vue 文件接收的参数对应setTimeout(() => {  proxy.$GtMessageBox({    headerText: '警告',    content: 'Hello World'  })    .then(() => {      console.log(111)    })    .catch(() => {      console.log(222)    })}, 2000);

2、可不注册插件【不进行第三步】,在 .vue 或 .js 文件内直接调用方法。
import GtMessageBox from "@/components/gtMessageBox/index";GtMessageBox.confirm({    content: '此审批已经被删除,请刷新页面!',    holdOnFn: () => {      GtMessageBox.confirm({          content: '此审批已经被删除,请刷新页面!',          type: 'confirm'      })          .then(() => {              console.log(333)              // 卸载已打开的所有弹窗              GtMessageBox.hideAll()          })    }})    .then(() => {      console.log(111)    })    .catch(() => {      console.log(222)    })

五、优化改进

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

第一种解决办法其实就是运用了 单例模式;

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

第二种解决办法实现多个确认弹窗其实就是设置一个初始的层级变量,每创建一个确认弹窗组件变量就自增加1,然后将新的层级变量赋值给新的确认弹窗,这样就实现了多个弹窗共存,但是相应的也需要提供一个卸载所有弹窗的方法。

由于本文篇幅有限所以就不展开了,在下一篇文章会进行详细的讲解及代码示例。

 

03——总结

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

 

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=16762,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?