后台类系统组件——通知

需求

一个后台系统经常需要提示用户个各种信息,有后端返回的错误,也有前端验证的提示,还有操作成功后的提示。不同的提示应该有不同的颜色或外形来区别,同时这些提示不应该打断用户的操作。

对于前端开发者而言,这种小组件应该尽量的轻量,无依赖(不随系统框架变化而影响),方便业务中调用(良好的api设计)。

下面我分享一下我在工作中写的一个通知组件(非常简单,还有许多可以改进的地方)。

预览图

针对不同的信息类型,我设置了三种颜色,可以分别通过error(), warn(), success() 来调用。

以前我使用的是 bootstrap 的 modal 组件作为通知的载体。这个 modal 会自带一个遮照,再通知消失之前用户必须手动点击遮照或者等待3秒,以继续操作,体验非常不好。于是在重构的时候我放弃了 bootstrap 的 modal。

这个 Notify 组件可以自定义持续事件,所以调用的api可以设计成:

notify(msg, option)

{string} msg: 通知的消息。
{object} option: 自定义参数: type, duration。

实现

首先,我们定义一个 Notify 类。

1
2
3
class Notify {
}

然后是constructor()方法:

1
2
3
4
5
6
constructor() {
this.defaultOption = {
duration: 3000, // 持续时间
type: 'warn' // 提示的类型
};
}

这里先简单地定义了默认的参数。

然后是createContainer()方法:

1
2
3
4
5
createContainer() {
this.container = document.createElement('div');
this.container.className = 'dd-notifies'; // 加上dd前缀是为了避免和其他ui库或者业务样式冲突
document.body.appendChild(this.container);
}

这个方法只是在body里面插入了一个div,作为通知的容器。

constructor()中加入createContainer()

1
2
3
4
5
6
7
constructor() {
this.createContainer(); // 在这里
this.defaultOption = {
duration: 3000,
type: 'warn'
};
}

这样当我们实例化 Notify 类时,就会自动在 body 中插入一个 div。

然后,我们定义一个add()方法,用来将通知弹窗加入容器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
add(msg, opt) {
const option = { ...this.defaultOption, ...opt }; // 混合自定义参数与默认参数
const notify = document.createElement('div');
const typeClassName = 'dd-notify-' + option.type; // 根据 type 定义类名
notify.className = 'dd-notify dd-notify-active ' + typeClassName;
const msgNode = document.createTextNode(msg);
notify.appendChild(msgNode);
this.container.appendChild(notify);
setTimeout(() => notify.classList.remove('dd-notify-active'), 0); // 注意,看后文解释
this.hide(notify, option.duration);
}

add()方法里我们创建了一个div作为通知的主体,根据传入的 type 不同,赋予不同的类名。并将它插入到通知容器中,还处理了一下进场动画。最后调用hide()来关闭通知。

这里我在去除 dd-notify-active 类名的时候,将这段代码放到了setTimeout中。这是因为浏览器的渲染机制,浏览器会一次性执行完所有非读取元素几何信息的代码,再渲染元素。如果我们这里不把这段代码放入setTimeout中,浏览器直接渲染的通知元素是不会带有dd-notify-active的,因此将不能触发动画。
如果再移除类名之前,我们调用getBoundingClientRect(),浏览器也会立即渲染元素,这样也是可以的。

1
2
3
4
5
6
hide(notify, duration) {
setTimeout(() => {
notify.classList.add('dd-notify-active');
setTimeout(() => this.container.removeChild(notify), this.animationDuration);
}, duration);
}

hide()方法里用到了 animationDuration,这是动画持续时间,我们把它加入到constructor()

1
2
3
4
5
6
7
8
constructor() {
this.createContainer();
this.defaultOption = {
duration: 3000,
type: 'warn'
};
this.animationDuration = 240; // 动画时间,这部分代码和 css 耦合了。
}

最后让我们来暴露对外的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let notifyInstance;
// 获取实例
function getNotifyInstance() {
notifyInstance = notifyInstance || new Notify(); // 单例
return notifyInstance;
}
const notifyTypes = ['error', 'warn', 'success'];
// 包装add(),方便直接使用error(), warn(), success() 调用。
notifyTypes.map(type => {
window[type] = function(msg, opt) {
const notify = getNotifyInstance();
notify.add(msg, { type: type, ...opt });
};
});

notifyInstance 和 getNotifyInstance() 可以得到单例的 Notify 类。
然后定义不同的 notifyTypes,用这些不同的 type 包装 Notify 的 add()
在 demo 中我用的是 window 对象,实际代码中,为了模块化,我们可以这样:

1
2
3
exports[type] = function() {
// ...
}

优化

现在动画时间是耦合的,js 中有,css 中也有,可以将这部分 css 抽出来,放到 js 中来定义。

在线demo