Vue2 中封装组件-消息提示 Message

由于我在开发的个人博客前台中需要自行封装许多复用组件,所以跟大家分享一下我认为比较难的组件—消息提示 Message 的整个开发流程及其难点的解决方法。

1.Message 组件的基本介绍

1.1 最终效果图

最终的效果图如下:
Message 组件的最终效果图

1.2 Options 参数

Message 组件的主要参数见下表:

参数说明类型可选值默认值
content消息内容String——“”(空字符串)
type消息类型Stringinfo/error/success/warninfo
duration显示时间Number——2000(ms)
container组件的父容器HTMLElement——document.body
callback回调函数(在消息消失后执行,如果不传则不执行)Function——undefined

1.3 使用方法

因为 Message 组件在我的个人博客系统中会经常使用,所以并不是注册局部组件也不是注册全局组件,而是直接挂载到Vue.prototype这一原型上,后面 vue 实例对象使用起来就会更加方便,只需要调用this.$showMessage()方法。
但我们需要有一个获取 vue 实例对象中的某个 DOM 节点的方法:

  • 此时我们可以借助ref这个属性,通过this.$refs.xxxx来获取该 DOM 节点

具体代码如下:

<template>
  <div class="container" ref="container">
    <button @click="handleClick"></button>
  </div>
</template>

<script>
export default {
  methods: {
    handleClick() {
      this.$showMessage({
        content: "消息提示弹出",
        type: "success",
        duration: 1000,
        container: this.$refs.container,
        callback: () => {
          console.log("消息提示消失,执行回调函数");
        },
      });
    },
  },
};
</script>

<style>
.test-container {
  width: 500px;
  height: 400px;
  border: 2px solid;
  margin: 0 auto;
  position: relative;
}
</style>

2.Message 组件的样式结构

Message 组件的样式结构如图所示:
Message 组件的样式结构图

2.1 HTML 结构

从图中我们可以看出该组件的 HTML 结构还是比较简单的,就是是一个message容器包裹着一个icon图标与content字体内容。

因为结构比较简单,使用我就没写 HTML 代码,而是直接用 JS 代码来生成元素,再通过添加 class 类名的方式来增添组件的样式,再进行相应的业务逻辑控制。
但大家还是可以看一下下面的 HTML 代码,这样可以对该组件的结构有更直观的了解。

<!-- 该组件没写 HTML 代码,而是直接用 JS 代码来生成元素 -->
<!-- 下面的代码只便于读者对该组件的结构有更直观的了解 -->
<div class="message">
  <span class="icon">
    <Icon :type="type"></Icon>
  </span>
  <div>{{content}}</div>
</div>

至于 icon 图标这块我是直接使用了自己已封装好的Icon组件,该组件实现起来比较简单,主要是通过传入 type 这 prop 属性来控制 Icon 图标的类型,我这边就直接贴代码:

Icon.Vue 文件代码如下:

<template>
  <i class="iconfont icon-container" :class="fontClass"></i>
</template>

<script>
  const classMap = {
    home: "iconzhuye",
    success: "iconzhengque",
    error: "iconcuowu",
    close: "iconguanbi",
    warn: "iconjinggao",
    info: "iconxinxi",
    blog: "iconblog",
    code: "iconcode",
    about: "iconset_about_hov",
    weixin: "iconweixin",
    mail: "iconemail",
    github: "icongithub",
    qq: "iconsign_qq",
    arrowUp: "iconiconfonticonfonti2copy",
    arrowDown: "iconiconfonticonfonti2",
    empty: "iconempty",
    chat: "iconliuyan",
  };
  export const types = Object.keys(classMap);
  export default {
    props: {
      type: {
        type: String,
        required: true,
      },
    },
    computed: {
      // 图标类样式
      fontClass() {
        return classMap[this.type];
      },
    },
  };
</script>

<style scoped>
  /* 导入远程iconfont样式库 */
  @import "//at.alicdn.com/t/font_2164449_nalfgtq7il.css";
  .iconfont {
    color: inherit;
    font-size: inherit;
  }
</style>

2.2 CSS 样式

从上面的样式结构图可看出 Message 组件的内部样式也比较简单,主要是:

  • message容器中的子元素居中显示,我是通过 flex 布局是实现的。
  • message容器的背景颜色随 type 这 prop 属性而变化,我是可通过不同 class 类名来进行控制。
  • 另外我使用了 Less 预处理器,并开启了 CSS Modules。

showMessage.module.less文件具体代码如下:

@import "../styles/var.less";
@import "../styles/mixin.less";
.message {
  /*message在container父容器中居中
  该居中方案不能使用flex布局 因为flex会影响到父容器的其他子节点的布局*/
  .self-center();

  z-index: 999; //让该组件的层叠上下文置于顶层
  border-radius: 5px;
  padding: 10px 30px;
  line-height: 2;
  color: #fff;
  box-shadow: -2px 2px 5px rgba(0, 0, 0, 0.5); //增加盒子阴影
  transition: 0.4s; //过渡时间
  white-space: nowrap; //防止宽度被挤压而导致文字分行

  /*message内部子元素垂直居中*/
  display: flex;
  align-items: center;
  /*message容器的初始状态,便于后续增加渐入淡出的效果 */
  transform: translate(-50%, -50% + 25px);
  opacity: 0;
  &-info {
    background: @primary;
  }
  &-success {
    background: @success;
  }
  &-warn {
    background: @warn;
  }
  &-error {
    background: @danger;
  }
}

.icon {
  font-size: 20px;
  margin-right: 7px;
}

var.less变量文件:

// 提供less变量
@danger: #cc3600; // 危险、错误
@primary: #6b9eee; // 主色调、链接
@words: #373737; // 大部分文字、深色文字
@lightWords: #999; // 少部分文字、浅色文字
@warn: #dc6a12; // 警告
@success: #7ebf50; // 成功
@gray: #b4b8bc; // 灰色
@dark: #202020; // 深色

mixin.less混入文件:

// 提供混合样式
.self-center(@pos: absolute) {
  position: @pos;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

3. Message 组件的业务逻辑

Message 组件虽然看起来比较简单,但这组件在许多情况下都要使用,要考虑其通用性,所以该组件的业务逻辑还是比较复杂的,我认为主要的难点有:

  • 获取 Icon 组件根元素的 DOM 节点
  • Message 组件的渐入淡出的动态效果

3.1 获取 Icon 组件根元素 DOM 节点

如果我们直接导入 Icon.Vue 组件文件并直接进行使用的话,得到的会是一个 Vue 实例对象,而且我们无法通过该对象来操作其根元素 DOM 节点。
所以此时我们要借助 vue 中的 render 渲染函数进行封装一个工具函数——getComponentRootDom。

getComponentRootDom.js文件具体代码如下:

import Vue from "vue";
/**
	获取某个组件渲染的Dom根元素
*/
export default function (comp, props) {
  const vm = new Vue({
    render: (h) => h(comp, { props }),
  });
  vm.$mount();
  return vm.$el;
}

3.2 Message 组件的渐入淡出的动态效果

3.2.1 渐入效果

渐入效果的代码其实是很简单的,但会出现渐入效果丢失的问题。
在我大量参阅资料后,发现是浏览器异步渲染机制所导致。(后续我也会对该部分内容进行详细的讲解,敬请期待)

  • 渐入效果丢失主要原因是:当时正处于 message 容器刚加入 container 父容器的时刻,message 容器尚未渲染完成,所以后面的样式代码会直接覆盖前面的样式代码。
  • 解决办法:在初始状态和正常位置状态之间加入一段会导致**重排(reflow)**的代码如:读取 DOM 节点的位置信息等操作。message.clientHeight;

目前对浏览器异步渲染机制不熟悉的朋友,参考以下两篇博客:

渐入效果的代码如下:

/*
message容器初始状态的样式代码:
transition: 0.4s;//过渡时间
transform: translate(-50%, -50% + 25px);
opacity: 0;
*/
//渐入效果:初始状态 --> 正常位置状态
container.appendChild(message); // 将message容器加入到父容器中
message.clientHeight; //造成reflow导致浏览器强行渲染
// 正常位置状态的样式
message.style.opacity = 1;
message.style.transform = `translate(-50%, -50%)`;
3.2.2 淡出效果

淡出效果的代码也很简单,主要难点有:

  • 何时删除 message 容器,监听什么事件?
    • 参阅资料后发现动画结束之后会触发transitionend事件。

有了这个事件就后面的处理很好办了,我们可以先使用setTimeout方法进行延迟durationms,在监听 message 容器的transitionend事件进行元素删除与执行回调函数的操作。
淡出效果的代码如下:

/*
正常位置状态: message容器的样式代码:
transition: 0.4s;//过渡时间
message.style.opacity = 1;
message.style.transform = `translate(-50%, -50%)`;
*/

// 淡出效果:正常位置状态 --> 消失状态
//message容器动画的过渡时间
const transitionDuration = parseFloat(
  getComputedStyle(message).transitionDuration
);
//进行延迟(duration + transitionDuration)ms
setTimeout(() => {
  //消失状态的样式
  message.style.opacity = 0;
  message.style.transform = "translate(-50%, -50% - 25px)";
  //监听transitionend事件
  message.addEventListener(
    "transitionend",
    function () {
      message.remove(); //删除message容器
      callback && callback(); // 有回调函数就直接执行
    },
    { once: true }
  );
}, duration + transitionDuration);

3.3 业务逻辑的完整代码

下面我们来看看 message 组件业务逻辑的完整代码。
showMessage.js文件代码如下:

import getComponentRootDom from "./getComponentRootDom";
import Icon from "@/components/Icon";
import styles from "./showMessage.module.less";

/**
 * 消息提示
 * @param {String} content 消息内容
 * @param {String} type 消息类型  info  error  success  warn
 * @param {Number} duration 多久后消失
 * @param {HTMLElement} container 容器,消息会显示到该容器的正中间;如果不传,则显示到整个页面的正中间
 * @param {Function} callback 回调函数,该函数会在弹出消息消失后执行,如果不传,则不执行
 */
export default function (options = {}) {
  //设置参数的默认值
  const content = options.content || "";
  const type = options.type || "info";
  const duration = options.duration || 2000;
  const container = options.container || document.body;
  const callback = options.callback || undefined;

  //JS代码生成message元素
  const message = document.createElement("div");
  //得到Icon组件的根元素DOM节点
  const iconDom = getComponentRootDom(Icon, {
    type,
  });
  //message容器中增加相应的子元素
  message.innerHTML = `<span class="${styles.icon}">${iconDom.outerHTML}</span><div>${content}</div>`;

  //添加样式
  message.classList.add(styles.message); //添加message类名
  message.classList.add(styles[`message-${type}`]); //添加消息类型类名

  // 由于需要满足 子绝父相 这一条件来进行居中定位
  // 所以需要判断容器的position值
  if (options.container) {
    if (getComputedStyle(container).position === "static") {
      container.style.position = "relative";
    }
  }
  container.appendChild(message); // 将message容器加入到父容器中

  //渐入效果:初始状态 --> 正常位置状态
  message.clientHeight; //造成reflow导致浏览器强行渲染
  // 正常位置状态的样式
  message.style.opacity = 1;
  message.style.transform = `translate(-50%, -50%)`;

  // 淡出效果:正常位置状态 --> 消失状态
  //message容器动画的过渡时间
  const transitionDuration = parseFloat(
    getComputedStyle(message).transitionDuration
  );
  //进行延迟(duration + transitionDuration)ms
  setTimeout(() => {
    //消失状态的样式
    message.style.opacity = 0;
    message.style.transform = "translate(-50%, -50% - 25px)";
    //监听transitionend事件
    message.addEventListener(
      "transitionend",
      function () {
        message.remove(); //删除message容器
        callback && callback(); // 有回调函数就直接执行
      },
      { once: true }
    );
  }, duration + transitionDuration);
}

4. Message 组件的挂载方法

由于 Message 组件在我的个人博客系统中会经常使用,为了使用起来更简单与灵活,所以并没有选择注册局部组件注册全局组件这两个常用方法,而是选择直接挂载到Vue.prototype这一原型上,后面 vue 实例对象使用起来就会更加方便,调用起来也更加灵活,只需要调用this.$showMessage()方法。

main.js 入口文件相关代码如下:

import Vue from "vue";
import App from "./App.vue";
//引入消息弹窗方法 并挂载到vue原型对象
import showMessage from "./utils/showMessage";
Vue.prototype.$showMessage = showMessage;
new Vue({
  render: (h) => h(App),
}).$mount("#app");

结语

这是我目前所了解的知识面中最好的解答,当然也有可能存在一点的误区。

所以如果对本文存在疑惑,可以去评论区进行留言,欢迎大家指出文中的错误观点。

码字不易,觉得有帮助的朋友点赞,关注走一波。

Logo

前往低代码交流专区

更多推荐