Vue3封装全局Dialog组件的实现方法

 更新时间:2023年06月20日 10:59:55   作者:前端咸鱼翻身  
3封装全局Dialog组件相信大家都不陌生,下面这篇文章主要给大家介绍了关于Vue3封装全局Dialog组件的实现方法,文中通过实例代码介绍的非常详细,需要的朋友可以参考下

系列文章目录

前言

前面的文章介绍了封装 Dialog 弹出框的组件,同时实现了弹窗的链式调用。不过需求总是在变化的,这次需求要在显示了弹窗的基础上不断往上面再显示弹窗,所以来分享一下自己改善后的封装方法,总结一下最新的心得体会。

一、使用方式

全局封装的组件,在vue文件还有js文件当然都是可以使用的,这一点在第一章已经介绍了。下面展示在vue文件内使用的方式,在需要弹窗的地方调用proxy.$popup()方法即可,随便写了写,可以对照下面效果展示来看。

import { getCurrentInstance } from 'vue';

// 获取当前实例,相当于 vue2 中的this
const { proxy } = getCurrentInstance();
const popupFn = () => {
  proxy.$popup({
    content:
      '银河广袤寂静,也孕育了无数美好。此刻,如果您仰望星空,看见那深邃黑暗中若隐若现的点点微芒,便会知晓,那高远无穷的璀璨正是我们共同的期冀。',
    successText: '愿此行,终抵群星',
    // 点击遮罩层是否关闭弹窗,默认false
    closeByOverlay: true
  });
};
const openPopup = () => {
  proxy.$popup({
    content: '生命远不止连轴转和忙到极限,人类的体验远比这辽阔、丰富得多。',
    holdOnFn: popupFn
  });
};

二、效果展示

点击“弹窗”调用的是 openPopup 方法。

请添加图片描述

三、思路及实现方式

请添加图片描述

上面这个动图展示了其运行的原理,做出来之后我回看一下觉得其实细节还真不少,我将一步步介绍自己的思路及实现方法,完整代码在最后贴上,可供大家参考。

1、保留原有弹窗基础上加入新弹窗

vue文件组件里面主要区别在于点击确认按钮时如果参数holdOnFn存在值的话就不执行hidden,而是执行传进来的方法,这样旧弹窗就不会被关闭了,然后在弹窗上加个关闭按钮让其可以调用hidden方法去关闭。js文件主要区别就在于不能使用单例模式了,因为要创建多个弹窗。

// 省略无关代码
const props = defineProps({
  holdOnFn: {
    type: Function
  }
});
// 隐藏弹窗方法
const hidden = () => {
  isShow.value = false;
  props.hide();
};
// 成功按钮
const successHandle = () => {
  props.successBtn();
  if (!props.holdOnFn) {
    nextTick(() => {
      hidden();
    });
  } else {
    props.holdOnFn();
  }
};

2、弹窗之间的层级问题

这里给遮罩层及弹窗加入样式,用fixed定位并且在外层记录当前z-index值,保证新弹窗的值永远大于旧弹窗,关键代码节选:

// 这里数值大一些保证弹窗在所有组件最上方
let overlayNodeZIndex = 3000;
// 创建遮罩层
const createOverlay = () => {
  const overlayNode = document.createElement('div');
  overlayNode.className = 'my-overlay';
  document.body.appendChild(overlayNode);
  overlayNode.style.zIndex = overlayNodeZIndex;
  return overlayNode;
};

const popup = (options = {}) => {
  // 创建遮罩层
  let overlayNode = null;
  overlayNode = createOverlay();

  // 创建弹窗元素节点
  const rootNode = document.createElement('div');
  // 在body标签内部插入此元素
  document.body.appendChild(rootNode);
  rootNode.className = `my-dialog`;
  rootNode.style.position = 'fixed';
  rootNode.style.zIndex = overlayNodeZIndex;
  overlayNodeZIndex += 1;

  // 创建应用实例(第一个参数是根组件。第二个参数可选,它是要传递给根组件的 props)
  const app = createApp(Popup, {
    ...options
  });
  return app.mount(rootNode);
};

3、弹窗关闭所有

有这么一个场景,当用户点了第二个或以上的弹窗之后需要一次性把之前弹窗都关闭掉,所以有了这么一个hideAll方法。那么调用proxy.$popupHide()则可以关闭所有弹窗了。

// 定义临时数组存放关闭弹窗的方法
let hideArr = [];

const popup = (options = {}) => {
  // 省略无关代码
  const hide = function () {
    // 关闭一个弹窗则从hideArr中取出一个
    hideArr.pop();
  };

  // 每创建一个弹窗将其卸载方法存入hideArr数组中
  hideArr.push(hide);

  const app = createApp(Popup, {
    ...options,
    hide
  });
  return app.mount(rootNode);
};
const okFun = (options = {}) => {
    popup(options).show();
};

// 隐藏所有弹窗,遍历然后调用
const hideAll = () => {
  hideArr.forEach(e => {
    e();
  });
};

popup.install = app => {
  app.config.globalProperties.$popup = options => okFun(options);
  app.config.globalProperties.$popupHide = hideAll;
};
popup.show = options => okFun(options);
popup.hideAll = hideAll;

export default popup;

4、弹窗滚动相关问题

这个问题其实在第二章也有探讨过,采取了文档固定定位加记录高度的方法实现固定背景层,避免滚动穿透问题,不过由于现在弹窗不止一个了,文档上的fixed定位加上的时机以及去掉的时机就得考虑好,所以写法要相应改进一下。正好上面hideAll方法存下来的数组里面方法的个数正好代表着目前弹窗的个数,所以可以根据数组长度做出判断。

let hideArr = [];
let appScrollTop = 0;
let bodyTop = 0;

// 创建弹窗调用的方法
const popup = (options = {}) => {
  // 禁止app滚动逻辑
  const appDOM = document.querySelector('#app');
  // 此处hideArr用作弹窗计数
  if (hideArr.length === 0) {
    // 记录滚动高度
    appScrollTop = appDOM.scrollTop;
    // 记录文档高度,兼容移动端浏览器,防止偏移
    bodyTop = document.scrollingElement.scrollTop;
    appDOM.style.position = 'fixed';
    appDOM.style.bottom = appScrollTop + 'px';
    // 兼容ios手机fixed定位不生效,得加overflow
    appDOM.style.overflow = 'visible';
  }
  // 关闭弹窗的方法
  const hide = function () {
    // 解除app滚动逻辑
    // 关闭一个弹窗则从hideArr中取出一个
    hideArr.pop();
    // 此处hideArr用作弹窗计数
    if (hideArr.length === 0) {
      // 解除app元素滚动,移除样式
      const appDOM = document.querySelector('#app');
      appDOM.style.removeProperty('position');
      appDOM.style.removeProperty('bottom');
      appDOM.style.removeProperty('overflow');
      // 恢复文档滚动位置
      appDOM.scrollTop = appScrollTop;
      document.scrollingElement.scrollTop = bodyTop;
      // 恢复默认值
      overlayNodeZIndex = 3000;
      appScrollTop = 0;
      bodyTop = 0;
    }
  };
  // 每创建一个弹窗将其卸载方法存入hideArr数组中
  hideArr.push(hide);
  
  const app = createApp(Popup, {
    ...options,
    hide
  });
  return app.mount(rootNode);
};

5、遮罩层加上动画效果

①:创建遮罩层的时候

const createOverlay = () => {
  const overlayNode = document.createElement('div');
  overlayNode.className = 'my-overlay';
  // 添加初始显示的过渡效果
  overlayNode.classList.add('my-overlay-enter-from');
  // 在body标签内部插入此元素
  document.body.appendChild(overlayNode);
  const remove = () => {
    overlayNode.classList.remove('my-overlay-enter-from');
  };
  // requestAnimationFrame在浏览器下一次重绘之前执行
  requestAnimationFrame(remove);
  overlayNode.style.zIndex = overlayNodeZIndex;
  return overlayNode;
};

②:关闭弹窗的时候

const hide = function () {
  // 显示移除动画
  overlayNode.classList.add('my-overlay-leave-to');
  setTimeout(() => {
    overlayNode.classList.remove('my-overlay-leave-to');
    // 移除遮罩层
    deleteOverlay(overlayNode);
  }, 300);
}

③:弹窗遮罩层样式

.my-overlay {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 100;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.6);
  transition: opacity 0.3s ease-out;
}
.my-overlay-enter-from,
.my-overlay-leave-to {
  opacity: 0;
}

6、配置点击遮罩层关闭弹窗

根据传入参数对象的closeByOverlay属性,监听遮罩层点击事件即可。

const props = defineProps({
    // 默认点击遮罩层不关闭
    closeByOverlay: {
        type: Boolean,
        default: false,
    },
    overlayNode: {
        type: Object,
    },
})
// 显示弹窗方法
const show = () => {
  isShow.value = true;
  if (props.closeByOverlay) {
    props.overlayNode.addEventListener('click', hidden);
  }
};

四、完整代码环节

1、vue文件

<template>
  <transition name="toast" @after-leave="onAfterLeave">
    <div class="toast" v-if="isShow" :style="{ width: toastWidth }" :class="{ bgc: needBgc }">
      <div v-if="showCancel || props.holdOnFn" class="cancel" @click="hidden"></div>
      <div v-if="content" class="content" :style="{ textAlign }">
        {{ content }}
      </div>
      <div class="operation" v-if="type === 'confirm'">
        <div class="confirm" @click="successHandle">{{ successText }}</div>
      </div>
      <div class="operation" v-if="type === 'confirmAndcancel'">
        <div class="close" @click="cancelHandle">{{ cancelText }}</div>
        <div class="confirm" @click="successHandle">{{ successText }}</div>
      </div>
    </div>
  </transition>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue';
const props = defineProps({
  content: {
    type: String,
    default: ''
  },
  width: {
    default: 640
  },
  textAlign: {
    type: String,
    default: 'center'
  },
  type: {
    type: String,
    default: 'confirm'
  },
  hide: {
    type: Function
  },
  successText: {
    type: String,
    default: '确认'
  },
  cancelText: {
    type: String,
    default: '取消'
  },
  successBtn: {
    type: Function
  },
  cancelBtn: {
    type: Function
  },
  // 是否需要右上角关闭按钮
  showCancel: {
    type: Boolean,
    default: false
  },
  // 是否需要背景
  needBgc: {
    type: Boolean,
    default: true
  },
  // 传入调用方法
  holdOnFn: {
    type: Function
  },
  // 默认点击遮罩层不关闭
  closeByOverlay: {
    type: Boolean,
    default: false
  },
  overlayNode: {
    type: Object
  }
});
// 弹窗控制
const isShow = ref(false);
// 宽度控制
const toastWidth = computed(
  () =>
    ((parseInt(props.width.toString()) / 750) * document.documentElement.clientWidth).toFixed(3) +
    'px'
);
// 显示弹窗方法
const show = () => {
  isShow.value = true;
  if (props.closeByOverlay) {
    props.overlayNode.addEventListener('click', hidden);
  }
};
// 将方法暴露出去
defineExpose({
  show
});
// 隐藏弹窗方法
const hidden = () => {
  isShow.value = false;
  props.hide();
  if (props.closeByOverlay) {
    props.overlayNode.removeEventListener('click', hidden);
  }
};
// 由于遮罩层需要消失动画,两个动画效果必须同时触发,所以关闭时直接调用hide方法
const onAfterLeave = () => {
  //   props.hide();
};
const successHandle = () => {
  props.successBtn();
  if (!props.holdOnFn) {
    nextTick(() => {
      hidden();
    });
  } else {
    props.holdOnFn();
  }
};
const cancelHandle = () => {
  props.cancelBtn();
  nextTick(() => {
    hidden();
  });
};
</script>
<style lang="scss" scoped>
@mixin expand-btn {
  &::before {
    content: '';
    position: absolute;
    top: -10px;
    right: -10px;
    bottom: -10px;
    left: -10px;
  }
}
.toast {
  position: fixed;
  top: 45%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 99;
  text-align: center;
  .cancel {
    background: url('../../assets/images/quxiao@2x.png') no-repeat center / contain;
    position: absolute;
    top: 10px;
    right: 10px;
    width: 15px;
    height: 15px;
    @include expand-btn;
  }
  .content {
    color: #ffcc99;
    max-height: 50vh;
    overflow-y: scroll;
  }
  .operation {
    position: relative;
    display: flex;
    justify-content: space-around;
    align-items: center;
    margin-top: 15px;
    &::before {
      content: '';
      position: absolute;
      top: -50%;
      right: -50%;
      bottom: -50%;
      left: -50%;
      border-top: 1px solid #666;
      transform: scale(0.5);
    }
    .close {
      position: relative;
      width: 100%;
      padding: 10px 0;
      color: rgba($color: #ffcc99, $alpha: 0.9);
      &::after {
        content: '';
        position: absolute;
        top: -50%;
        right: -50%;
        bottom: -50%;
        left: -50%;
        border-right: 1px solid #666;
        transform: scale(0.5);
      }
    }
    .confirm {
      width: 100%;
      padding: 10px 0;
      color: rgba($color: #ffcc99, $alpha: 0.9);
    }
  }
}
.bgc {
  background: #333333;
  border-radius: 20px;
  padding: 25px 20px 0;
}
.toast-enter-active,
.toast-leave-active {
  transition: all 0.3s ease-out;
}
.toast-enter-from,
.toast-leave-to {
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.8);
}
</style>

2、js文件

import { createApp } from 'vue';
import Popup from './Popup.vue';

let overlayNodeZIndex = 3000;
let hideArr = [];
let appScrollTop = 0;
let bodyTop = 0;

// 创建遮罩层
const createOverlay = () => {
  const overlayNode = document.createElement('div');
  overlayNode.className = 'my-overlay';

  // 添加初始显示的过渡效果
  overlayNode.classList.add('my-overlay-enter-from');

  // 在body标签内部插入此元素
  document.body.appendChild(overlayNode);
  
  const remove = () => {
    overlayNode.classList.remove('my-overlay-enter-from');
  };
  // requestAnimationFrame在浏览器下一次重绘之前执行
  requestAnimationFrame(remove);

  overlayNode.style.zIndex = overlayNodeZIndex;

  return overlayNode;
};
// 移除遮罩层
const deleteOverlay = overlayNode => {
  if (overlayNode) {
    document.body.removeChild(overlayNode);
  }
};

const popup = (options = {}) => {
  // 禁止app滚动
  const appDOM = document.querySelector('#app');
  // 此处hideArr用作弹窗计数
  if (hideArr.length === 0) {
    // 记录滚动高度
    appScrollTop = appDOM.scrollTop;
    // 记录文档高度,兼容移动端浏览器,防止偏移
    bodyTop = document.scrollingElement.scrollTop;
    appDOM.style.position = 'fixed';
    appDOM.style.bottom = appScrollTop + 'px';
    // 兼容ios手机fixed定位不生效,得加overflow
    appDOM.style.overflow = 'visible';
  }
  // 创建遮罩层
  let overlayNode = createOverlay();

  // 创建弹窗元素节点
  const rootNode = document.createElement('div');
  // 在body标签内部插入此元素
  document.body.appendChild(rootNode);
  rootNode.className = `my-dialog`;
  rootNode.style.position = 'fixed';
  rootNode.style.zIndex = overlayNodeZIndex;
  overlayNodeZIndex += 1;

  const hide = function () {
    // 显示移除动画
    overlayNode.classList.add('my-overlay-leave-to');
    setTimeout(() => {
      overlayNode.classList.remove('my-overlay-leave-to');
      deleteOverlay(overlayNode);
    }, 300);
    // 解除body滚动
    // 关闭一个弹窗则从hideArr中取出一个
    hideArr.pop();
    // 此处hideArr用作弹窗计数
    if (hideArr.length === 0) {
      // 解除app元素滚动,移除样式
      const app = document.querySelector('#app');
      app.style.removeProperty('position');
      app.style.removeProperty('bottom');
      app.style.removeProperty('overflow');
      // 恢复文档滚动位置
      app.scrollTop = appScrollTop;
      document.scrollingElement.scrollTop = bodyTop;
      // 恢复默认值
      overlayNodeZIndex = 3000;
      appScrollTop = 0;
      bodyTop = 0;
    }
    // 卸载已挂载的应用实例
    setTimeout(() => {
      app.unmount();
      document.body.removeChild(rootNode);
    }, 300);
  };

  // 每创建一个弹窗将其卸载方法存入hideArr数组中
  hideArr.push(hide);

  // 创建应用实例(第一个参数是根组件。第二个参数可选,它是要传递给根组件的 props)
  const app = createApp(Popup, {
    ...options,
    hide,
    overlayNode
  });

  // 将应用实例挂载到创建的 DOM 元素上
  return app.mount(rootNode);
};

// 显示弹窗
const okFun = (options = {}) => {
  return new Promise((resolve, reject) => {
    options.successBtn = () => {
      resolve();
    };
    options.cancelBtn = () => {
      reject();
    };
    popup(options).show();
  });
};

// 隐藏所有弹窗
const hideAll = () => {
  hideArr.forEach(e => {
    e();
  });
};

popup.install = app => {
  // 注册全局属性,相当于 Vue2 的this
  app.config.globalProperties.$popup = options => okFun(options);
  app.config.globalProperties.$popupHide = hideAll;
};
// 定义两个方法用于js文件直接调用
popup.show = options => okFun(options);
popup.hideAll = hideAll;

export default popup;

总结

以上就是全部内容,本文通过封装可多次弹出的 Dialog 组件进一步探索了 Vue3 函数式组件的封装方法。暂时我把自己遇到的问题都列举出来了,但是可能还有些地方考虑不够周全,代码不够整洁优雅,我相信还有更好的实现方式,这也是我一直追求的,与君共勉!

到此这篇关于Vue3封装全局Dialog组件的实现方法的文章就介绍到这了,更多相关Vue3封装全局Dialog组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 浅谈vue中关于checkbox数据绑定v-model指令的个人理解

    浅谈vue中关于checkbox数据绑定v-model指令的个人理解

    这篇文章主要介绍了浅谈vue中关于checkbox数据绑定v-model指令的个人理解,v-model用于表单的数据绑定很常见,下面就来详细的介绍一下
    2018-11-11
  • uni-app自定义导航栏按钮|uniapp仿微信顶部导航条功能

    uni-app自定义导航栏按钮|uniapp仿微信顶部导航条功能

    这篇文章主要介绍了uni-app自定义导航栏按钮|uniapp仿微信顶部导航条,需要的朋友可以参考下
    2019-11-11
  • nginx如何配置vue项目history的路由模式(非根目录)

    nginx如何配置vue项目history的路由模式(非根目录)

    这篇文章主要介绍了nginx如何配置vue项目history的路由模式(非根目录),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-10-10
  • 在Vue中实现二维码生成与扫描功能的方法

    在Vue中实现二维码生成与扫描功能的方法

    二维码是一种广泛应用于各种场合的编码方式,它可以将信息编码成一张二维图案,方便快捷地传递信息,在Vue.js中,我们可以使用一些库和组件来实现二维码的生成和扫描,本文将介绍如何在Vue中实现二维码的生成和扫描的方法
    2023-06-06
  • 详解key在Vue3和Vue2的不同之处

    详解key在Vue3和Vue2的不同之处

    key属性是一个特殊的属性,用于标识每个节点的唯一性。在Vue2.x版本中的key和Vue3.x版本中的key有很大的不同,那么在这篇文章中,我们将会讨论Vue2中的key和Vue3中的key的区别
    2023-04-04
  • 复刻画龙产品vue实现新春气泡兔

    复刻画龙产品vue实现新春气泡兔

    这篇文章主要为大家介绍了复刻画龙产品之使用vue实现新春气泡兔示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • vue实现简单表格组件实例详解

    vue实现简单表格组件实例详解

    vue的核心思想就是组件,什么是组件呢?按照我的理解组件就是装配页面的零件,vue三大核心组件 路由 状态管理,路由控制页面的渲染,页面由组件组成,数据有vuex进行管理和改变。下面我会以一个简单的案例来说
    2017-04-04
  • Vue的Options用法说明

    Vue的Options用法说明

    这篇文章主要介绍了Vue的Options用法说明,具有很好的参考价值,希望对大家有所
    2020-08-08
  • vue+vux vux安装出现错误问题及解决

    vue+vux vux安装出现错误问题及解决

    这篇文章主要介绍了vue+vux vux安装出现错误问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • vue请求服务器数据后绑定不上的解决方法

    vue请求服务器数据后绑定不上的解决方法

    今天小编就为大家分享一篇vue请求服务器数据后绑定不上的解决方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-10-10

最新评论