预备知识
首先我们讲如何封装通用工具组件,先要明白一些概念,为什么封装组件,为什么模块化?
模块化开发是一种软件开发方法,通过将复杂的系统拆分为相互独立的模块来简化开发过程。在前端开发中,模块化开发可以使开发人员更方便地组织和维护代码。通过将代码拆分为独立的模块,每个模块负责特定的功能,开发人员可以更轻松地重用代码、提高代码的可维护性,并降低引入错误的风险。
说大白话,就是代码写一起太多了,太复杂,所以需要进行拆分单独模块。而模块化封装有可以细分为两大类一类是工具类组件,另一类业务模块类。
- 工具类组件:这类组件主要提供一些项目无关的功能,例如弹窗、通知等或者ant Design、element-ui组件库。它们的目的是为了提高开发效率和代码复用性。例如,在Vue3中,为了提高开发的效率,可能会封装很多的工具类,如axios和api等,使得开发过程更加酣畅淋漓。
- 业务模块类:是指项目中最后形成的组件,可以直接构成页面的模块。这类组件与具体的业务逻辑紧密相关,可能在一个项目中使用多次,或者在同一系列的项目中使用。但需要注意的是,业务模块的复用性不高,因为涉及到业务数据和特殊逻辑,在不同的项目中可能不适合直接使用。
需求效果
这次主要想讲通过下面案例聊聊对工具组件封装的想法思路,先上效果图 如上图,需求很简单,就是每次点击按钮,对应会弹窗一个通知,通知显示完成后则移除。当然在ant design中有现成的组件:(通知提醒框 Notification - Ant Design),如果想简单调用看官方文档现组件的使用就好,但在这篇文章后续讲的内容如何封装一个通用工具组件感兴趣我们可以继续往下看。
需求构思
基于这个需求,我们可以想到点:
1、这个弹窗通知在整个项目中会使用频繁,最好将弹窗通知封装为一个独立的组件;
2、这个弹窗组件最好不要与业务想关联,最好通过传入的参数,就改变其内容或文字;
3、最好我可以控制他在页面停留时间,根据调用者传入的时间来决定是否显示;
4、最好还要消息移除回调事件,而这个回调事件根据业务的使用者来定义相关逻辑;
5、....
一个新的需求功能最开始我们思考可能不会那么全面,所以我们可以根据想法一点点列举出来,在根据想法一点点实现,化繁为简再从简到繁。
环境准备
根据以上想法先写出这个组件参数,创建一个Notification.js
文件
js
/**
*
* @param {String} title 显示标题
* @param {String} content 显示内容
* @param {Date} type 传入时间
* @param {Namber} duration 过多长时间关闭弹框
* @param {function} callback 回调方法
*/
export default function () {
console.log("这里写我的组件")
}
先写一个简单调用该组件方法test.vue
一个按钮,点击按钮调用Notification
方法在参数中传入我期望输入的值
js
<template>
<div class="test">
<button @click="handleClick">Click</button>
</div>
</template>
script>
import Notification from "./Notification"
export default {
components:{
},
methods:{
handleClick() {
//调用封装的工具类
Notification({title:"标题",content:"这是正文内容,用户调用自定义",type:"info",duration:1000})
},
}
}
</script>
默认值设置
上面操作没问题,这时我们可有写组件功能,由于我们封装的工具类组件所以必须做好默认值边界限制的主要原因是为了确保组件的稳健性 和安全性
js
/**
*
* @param {String} title 显示标题
* @param {String} content 显示内容
* @param {String} type 传入事件
* @param {Namber} duration 过多长时间关闭弹框
* @param {function} callback 回调方法
*/
export default function (options = {}) {
// Notifications
options = {
title: options.title || "",
content: options.content || "",
type: options.type || new Date,
duration: options.duration || 2000,
}
...
配置默认值和字段说明有个好处,则在调用时候按ctrl+右键
可以看到这个函数的方法调用和需要传入参数等。
组件功能
我们可以先看下ant Design Notification
组件是如何运行
如上图,当我点击一个按钮,在F12中可以看到他创建一个div元素,隔一段时间后div元素则被移除了,所以我们也可以按照这种方式来实现这个功能。 先列举大纲
js
//1、创建div
//2、添加class元素,通过设置class来控制这个组件样式
//3、设置div内部内容显示模板格式
//4、设置div移入动画
//5、设置定时器,在指定时间移除div
//6、div移除后执行用户传入函数
在大纲中1、创建div可以document
元素创建,2、由于是封装工具类,所以单独创建Notification.module.less
模块进行管理(你也可以写成css),3、内容模板可以使用模板字符串实现,4、通过class控制组件的运行需要控制两点①显示隐藏opacity
属性进行控制②div动画(从右往左移)transform : translateX(10rem)
,5、通过setTimeout
定时任务到时间后再移除。 先给出完整的Notification.module.less
代码
less
//单条通知消息样式
.inform {
position: absolute;
top: 0px;
right: 0;
width: 9rem;
font-size: 8px;
border: 1px solid #a4a4a4;
margin: 5px;
padding: 5px;
border-radius: 5px;
opacity: 1;//不显示
transition: 0.8s;//动画时长
transform : translateX(10rem);//移动位置
.notTitle{
display: flex;
justify-content: space-between;
}
.notBody{
font-size: 6px;
margin-top: 5px;
}
}
//BFC盒子,存放所有通知消息
#Nots{
overflow: hidden;
position: absolute;
top: 0px;
right: 0;
width: 10rem;
// height: 5rem;
margin: 5px;
padding: 5px;
transition: 1.4s;//动画时长
}
注意 :将文件命名为"文件名.module.less"主要是为了应用 CSS Modules 的技术。CSS Modules 是一种 CSS 编写方式,它可以实现样式的模块化,让类名具有局部作用域,避免类名冲突。这种技术的主要优点是解决了工程中的样式污染问题,并使得模块结构更清晰,代码更简洁易懂。
大白话通过将文件命名为
文件名.module.less
,结合 CSS Modules 的使用,可以有效地管理和维护项目中的样式,提高代码的可读性和可维护性。
js
//引入css样式
import styles from "./notification.module.less";
/**
*
* @param {String} title 显示标题
* @param {String} content 显示内容
* @param {Date} type 时间
* @param {Namber} duration 过多长时间关闭弹框
* @param {function} callback 回调方法
*/
export default function (options = {}) {
// Notifications
options = {
title: options.title || "",
content: options.content || "",
type: options.type || new Date(),
duration: options.duration || 2000,
}
console.log(options.content);
//创建div
var div = document.createElement("div")
//添加class元素
div.className = `${styles['inform']}`;
//在div中添加文字和图标
div.innerHTML = `<div >
<div class="${styles['notTitle']}">
<span >${options.title}</span>
<span >2小时前</span>
</div>
<div class="${styles['notBody']}">
正文内容${options.content}
</div>
</div>`
//将div挂载到页面上
document.body.appendChild(div);
// 浏览器强行渲染
div.clientHeight; // 导致reflow
//设置div可见
div.style.opacity = 1;
// 设置移动位置
div.style.transform = `translateX(0px)`
// 计算下档期添加的div是第几个,设置 duration
options.duration = (divDom.childNodes.length + 1) * options.duration
// 设置过度时间,完成后则移除该元素
setTimeout(() => {
div.style.opacity = 0;
div.style.transform = `translatex(10rem)`;
//在div上添加执行事件()动画执行完成执行transitionend事件
div.addEventListener(
"transitionend", function () {
//动画完成移除div
div.remove();
//判断用户是否传入的完成后回调事件,如果有则进行回调,没有则不进行
options.callback || options.callback();
// once:true 表示该事件执行一次
}, { once: true }
)
}, options.duration)
}
在上面的js代码中对比之前也添加一些内容,在思考没考虑全面的地方,例如在div挂载到页面上document.body.appendChild(div)
;挂载元素后调用div元素的高(div.clientHeight
),在这个地方会触发浏览器reflow 进行页面强行渲染,不强行渲染则会导致js执行完成后才渲染在页面上,就没有动画效果了;在最后定时任务触发,在div上添加动画执行完成执行transitionend
事件,通过 { once: true }
表示该事件执行一遍;在该事件内进行移除div,执行回调函数等操作。
此时页面执行效果
BFC盒子
刚才的图上可以看出一些问题,1、每一次点击的div都是在同一个位置;2、每次div移动下面会出现进度条; 这个时候我们可以从小设置弹窗信息的显示效果和位置创建一个div,设置一个盒子position: absolute;
由于子元素宽度和父元素一致所以会从上往下排列,再设置overflow: hidden;
通过这个属性将这个盒子定位BFC让消息内容不会超出最外层的位置。
备注:一个 BFC 是一个独立的渲染区域,它规定了内部块级元素的布局和渲染方式,并且与外部的布局不相互影响。每个元素默认都是 BFC,但可以通过一些特定的 CSS 属性来创建新的 BFC。
BFC 具有以下特点:
- 内部的盒子会在垂直方向上一个接一个地放置,且边框、外边距和内边距之间不会出现重叠。
- BFC 的区域不会与浮动元素重叠。
- BFC 的区域会阻止垂直方向上的 margin 合并。
- BFC 的区域可以包含浮动元素。
完整代码
下面则是这个组件完整的代码,在notificationdiv组件完成定义一个divDom
的盒子,给divDom设置id=Nots,每次点击都会divDom.appendChild(div);
,将div挂载到divDom
的盒子上 。
js
import styles from "./notification.module.less";
// 设置一个div作为盒子
var divDom = document.createElement("div");
// 给盒子作标记
divDom.id = styles['Nots']
// 在div中将元素添加进去
document.body.append(divDom);
/**
*
* @param {String} title 显示标题
* @param {String} content 显示内容
* @param {Date} type 时间
* @param {Namber} duration 过多长时间关闭弹框
* @param {function} callback 回调方法
*/
export default function (options = {}) {
// Notifications
options = {
title: options.title || "",
content: options.content || "",
type: options.type || new Date(),
duration: options.duration || 2000,
}
console.log(options.content);
//创建div
var div = document.createElement("div")
//添加class元素
div.className = `${styles['inform']}`;
//在div中添加文字和图标
div.innerHTML = `<div >
<div class="${styles['notTitle']}">
<span >${options.title}</span>
<span >2小时前</span>
</div>
<div class="${styles['notBody']}">
正文内容${options.content}
</div>
</div>`
//将div挂载到页面上
divDom.appendChild(div);
// 浏览器强行渲染
div.clientHeight; // 导致reflow
//设置div可见
div.style.opacity = 1;
// 设置移动位置
div.style.transform = `translateX(0px)`
// 计算下档期添加的div是第几个,设置 duration
options.duration = (divDom.childNodes.length + 1) * options.duration
// 设置过度时间,完成后则移除该元素
setTimeout(() => {
div.style.opacity = 0;
div.style.transform = `translatex(10rem)`;
//在div上添加执行事件()动画执行完成执行transitionend事件
div.addEventListener(
"transitionend", function () {
//动画完成移除div
div.remove();
// once:true 表示该事件执行一次
}, { once: true }
)
}, options.duration)
}
最终效果
这个实现相关功能,但是也存在很多问题,作一个真正的通用组件还差点很远,以后会看可能这篇文章有很多错误,所以这里记录我此时的实现思路,在后续完成组件化提供方向。
调用优化
功能没问题,咱们扩展说其他内容:之前我们的调用方式
js
import Notification from "./Notification"
Notification({title:"标题",content:"这是正文内容,用户调用自定义",type:new Date,duration:1000})
但这种调用方式还是不够优雅,当我们完成该组件开发后可以将他注册为全局的组件,具体做法以vue举例子
上面有两张图,第一张是main.js
这个是vue入口文件,他将showMessage
组件通过prototype注入到vue原型对象中,这样在第二张图可以通过this.$showMessage
获取showMessage
组件
js
Vue.prototype.$Notification = Notification;
this.$Notification({
content: "评论成功",
type: "success",
container: this.$refs.container,
callback: function() {
console.log("完成!!!");
},
});
具体原理参考下图,不懂的化可以搜原型对象原型链相关知识。
原型对象实例构造函数关系图