由于我在开发的个人博客前台中需要自行封装许多复用组件,所以跟大家分享一下我认为比较难的组件—消息提示 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 节点具体代码如下:
从图中我们可以看出该组件的 HTML 结构还是比较简单的,就是是一个message
容器包裹着一个icon
图标与content
字体内容。
因为结构比较简单,使用我就没写 HTML 代码,而是直接用 JS 代码来生成元素,再通过添加 class 类名的方式来增添组件的样式,再进行相应的业务逻辑控制。
但大家还是可以看一下下面的 HTML 代码,这样可以对该组件的结构有更直观的了解。
<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 = `${styles.icon}">${iconDom.outerHTML}${content}`;
//添加样式
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");
这是我目前所了解的知识面中最好的解答,当然也有可能存在一点的误区。
所以如果对本文存在疑惑,可以去评论区进行留言,欢迎大家指出文中的错误观点。
码字不易,觉得有帮助的朋友点赞,关注走一波。