由于我在开发的个人博客前台中需要自行封装许多复用组件,所以跟大家分享一下我认为比较难的组件—消息提示 Message 的整个开发流程及其难点的解决方法。
最终的效果图如下:
Message 组件的主要参数见下表:
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
content | 消息内容 | String | —— | “”(空字符串) |
type | 消息类型 | String | info/error/success/warn | info |
duration | 显示时间 | Number | —— | 2000(ms) |
container | 组件的父容器 | HTMLElement | —— | document.body |
callback | 回调函数(在消息消失后执行,如果不传则不执行) | Function | —— | undefined |
因为 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>
Message 组件的样式结构如图所示:
从图中我们可以看出该组件的 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>
从上面的样式结构图可看出 Message 组件的内部样式也比较简单,主要是:
message容器
中的子元素居中显示,我是通过 flex 布局是实现的。message容器
的背景颜色随 type 这 prop 属性而变化,我是可通过不同 class 类名来进行控制。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%); }
Message 组件虽然看起来比较简单,但这组件在许多情况下都要使用,要考虑其通用性,所以该组件的业务逻辑还是比较复杂的,我认为主要的难点有:
如果我们直接导入 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; }
渐入效果的代码其实是很简单的,但会出现渐入效果丢失的问题。
在我大量参阅资料后,发现是浏览器异步渲染机制所导致。(后续我也会对该部分内容进行详细的讲解,敬请期待)
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%)`;
淡出效果的代码也很简单,主要难点有:
transitionend
事件。有了这个事件就后面的处理很好办了,我们可以先使用setTimeout
方法进行延迟duration
ms,在监听 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);
下面我们来看看 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); }
由于 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");
这是我目前所了解的知识面中最好的解答,当然也有可能存在一点的误区。
所以如果对本文存在疑惑,可以去评论区进行留言,欢迎大家指出文中的错误观点。
码字不易,觉得有帮助的朋友点赞,关注走一波。