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

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

1.Message 组件的基本介绍

1.1 最终效果图

最终的效果图如下:
Vue2 中封装组件-消息提示 Message

1.2 Options 参数

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

参数 说明 类型 可选值 默认值
content 消息内容 String —— “”(空字符串)
type 消息类型 String info/error/success/warn info
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 组件的样式结构如图所示:
Vue2 中封装组件-消息提示 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"); 

结语

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

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

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