今天我們一塊來做一個“彈窗消息提醒”插件。
彈窗消息演示
喏,就是這麼一個效果。
1. 分析
- 當消息被觸發的時候,會有一個自上而下的淡入過程。
- 在持續了一段時間後會自動的消失,或者是需要用戶來手動的點擊關閉按鈕。
- 在消息消失的時候,會有一個自下而上的淡出過程。
- 消息是可以疊加彈出的,最新的消息會排在消息列表的最後面。
- 當前面的消息消失後,後面的消息會有一個向上滑動效果。
然後消息本身是有三部分組成
- 消息圖標,用來區分不同類型的消息。
- 消息文本。
- 關閉按鈕,並不是所有消息都需要關閉按鈕。
2. 實現樣式
那麼,不管我們是用原生js還是vue,首先呢,我們都需要把這個消息的基本樣式給寫出來,然後再通過js來控制消息的彈出和關閉。
所以,我們先來寫html和css。
<!-- message.html -->
<!-- 這個css是我引用阿里的一些字體圖標,請戳: https://www.iconfont.cn/ -->
<link rel="stylesheet" href="http://at.alicdn.com/t/font_1117508_wxidm5ry7od.css">
<link rel="stylesheet" href="./message.css">
<script src="./message.js"></script>
<!-- 消息外層容器,因爲消息提醒基本上是全局的,所以這裏用id,所有的彈出消息都是需要插入到這個容器裏邊的 -->
<div id="message-container">
<div class="message">
<!-- 消息圖標 icon icon-success對應我的阿里字體圖標的font-class -->
<div class="type icon icon-success"></div>
<!-- 消息文本 -->
<div class="text">這是一條正經的消息~</div>
<!-- 關閉按鈕 -->
<div class="close icon icon-close"></div>
</div>
<div class="message">
<div class="type icon icon-error"></div>
<div class="text">這是一條正經的消息~</div>
</div>
</div>
/* message.css */
#message-container {
position: fixed;
left: 0;
top: 0;
right: 0;
/* 採用flex彈性佈局,讓容器內部的所有消息可以水平居中,還能任意的調整寬度 */
display: flex;
flex-direction: column;
align-items: center;
}
#message-container .message {
background: #fff;
margin: 10px 0;
padding: 0 10px;
height: 40px;
box-shadow: 0 0 10px 0 #eee;
font-size: 14px;
border-radius: 3px;
/* 讓消息內部的三個元素(圖標、文本、關閉按鈕)可以垂直水平居中 */
display: flex;
align-items: center;
}
#message-container .message .text {
color: #333;
padding: 0 20px 0 5px;
}
#message-container .message .close {
cursor: pointer;
color: #999;
}
/* 給每個圖標都加上不同的顏色,用來區分不同類型的消息 */
#message-container .message .icon-info {
color: #0482f8;
}
#message-container .message .icon-error {
color: #f83504;
}
#message-container .message .icon-success {
color: #06a35a;
}
#message-container .message .icon-warning {
color: #ceca07;
}
#message-container .message .icon-loading {
color: #0482f8;
}
大概是這麼一個效果
初始效果
3. 實現動畫
接下來要做的就是這個消息的彈出和消失動畫,我們還是用css來實現。
想要在css裏邊實現自定義的動畫,首先需要用@keyframes來定義一個動畫規則,然後再通過animation屬性把動畫應用到某個元素上就可以了。
所謂的動畫規則其實就是一個動畫序列,或者可以理解爲一個個的關鍵幀,而關鍵幀的內部就是你想改變的css屬性,你可以在關鍵幀裏邊寫上幾乎任何的css屬性,當動畫被應用的時候,這些css屬性就會根據各個關鍵幀做出相應的變換。
那我們先用@keyframes來寫一個動畫規則吧
/* message.css */
/* 這個動畫規則我們就叫做message-move-in吧,隨後我們會用animation屬性在某個元素上應用這個動畫規則。 */
@keyframes message-move-in {
0% {
/* 前邊分析過了,彈出動畫是一個自上而下的淡入過程 */
/* 所以在動畫初始狀態要把元素的不透明度設置爲0,在動畫結束的時候再把不透明度設置1,這樣就會實現一個淡入動畫 */
opacity: 0;
/* 那麼“自上而下”這個動畫可以用“transform”變換屬性結合他的“translateY”上下平移函數來完成 */
/* translateY(-100%)表示動畫初始狀態,元素在實際位置上面“自身一個高度”的位置。 */
transform: translateY(-100%);
}
100% {
opacity: 1;
/* 平移到自身位置 */
transform: translateY(0);
}
}
然後我們再定義一個和message
元素同級的類move-in
,把message-move-in
這個動畫規則給應用到move-in
類上,這樣我們需要讓哪個消息彈出,就只需要在消息的類上加一個move-in
就行。
/* message.css */
#message-container .message.move-in {
/* animation屬性是用來加載某個動畫規則 請參考 https://developer.mozilla.org/zh-CN/docs/Web/CSS/animation */
animation: message-move-in 0.3s ease-in-out;
}
我們來看下怎麼用這個move-in
:
應用動畫
可以看到,只需要在某個message上追加一個move-in
就能實現彈出動畫。
那麼,消失動畫也是一個套路,只不過跟彈出動畫反過來而已。
/* message.css */
@keyframes message-move-out {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-100%);
}
}
#message-container .message.move-out {
animation: message-move-out 0.3s ease-in-out;
/* 讓動畫結束後保持結束狀態 */
animation-fill-mode: forwards;
}
animation-fill-mode: forwards;
這個是幹嘛的呢?因爲動畫結束後默認會回到元素的最初狀態,在這裏表現的是消失後又出現了,如圖:
動畫結束後的默認狀態
所以animation-fill-mode: forwards;
是爲了讓動畫結束後保持這個結束狀態,也就是不在顯示了。
動畫結束後保持結束狀態
4. 編寫js插件
那麼,在寫js之前呢,我們先來思考一下,如果你是插件的使用者,你想怎麼來調用這個插件?
我們的插件很簡單,就是在需要的時候彈出一個消息,假設插件他提供給我們的是一個類,就叫做Message
吧,並且他內部有一個show
方法,那麼只要使用者實例化這個類後,調用他的show
方法,然後傳入不同的參數就可以彈出一個消息了。而且我們所實例化的對象可以是全局唯一的。
<!-- message.html -->
<!-- 省略... -->
<script>
// message可以定義爲全局對象,項目中可以直接調用。
const message = new Message();
message.show({
type: 'success',
text: '點個關注不迷路~'
});
</script>
所以呢,我們要先寫一個Message類,並且必須要實現一個show方法。
/* message.js */
class Message {
constructor() {
}
show({ type = 'info', text = '' }) {
}
}
這裏我直接用了es6的class
關鍵詞,其實他的內部還是原型鏈的形式。用class
呢,可以讓我們更直觀的瞭解這個類。
根據我們在第一部分的分析,所有的消息元素都是需要在js中創建的,所以我們不需要使用者來寫任何html代碼,那麼我們只需要在對象被實例化new Message()
的時候,就去創建消息容器message-container
,後續在調用show
方法時候,直接把消息插入到message-container
內部即可。
/* message.js */
class Message {
/**
* 構造函數會在實例化的時候自動執行
*/
constructor() {
const containerId = 'message-container';
// 檢測下html中是否已經有這個message-container元素
this.containerEl = document.getElementById(containerId);
if (!this.containerEl) {
// 創建一個Element對象,也就是創建一個id爲message-container的dom節點
this.containerEl = document.createElement('div');
this.containerEl.id = containerId;
// 把message-container元素放在html的body末尾
document.body.appendChild(this.containerEl);
}
}
show({ type = 'info', text = '' }) {
}
}
這樣,我們調用const message = new Message()
的時候會在dom中自動的插入一個message-container
節點。
那麼,最重要的還是我們的show
方法:
- 創建一個消息節點,並把它追加到
message-container
容器的末尾。- 設定一個時間,在這個時間結束後自動的將消息移除。
- 監聽“關閉按鈕”的
click
事件,來讓用戶可以手動的移除消息。
我們一步一步來。
4.1 創建一個消息節點,並把它追加到message-container
容器的末尾。
class Message {
// 省略...
show({ type = 'info', text = '' }) {
// 創建一個Element對象
let messageEl = document.createElement('div');
// 設置消息class,這裏加上move-in可以直接看到彈出效果
messageEl.className = 'message move-in';
// 消息內部html字符串
messageEl.innerHTML = `
<span class="icon icon-${type}"></span>
<div class="text">${text}</div>
<div class="close icon icon-close"></div>
`;
// 追加到message-container末尾
// this.containerEl屬性是我們在構造函數中創建的message-container容器
this.containerEl.appendChild(messageEl);
}
我們來調用下試試~
<!-- message.html -->
<!-- 省略... -->
<button class="btn">彈窗消息提醒</button>
<script>
// message可以定義爲全局對象,項目中可以直接調用。
const message = new Message();
document.querySelector('.btn').addEventListener('click', () => {
message.show({
type: 'success',
text: '點個關注不迷路~'
});
});
</script>
彈出成功
4.2 設定一個時間,在這個時間結束後自動的將消息移除。
// message.js
class Message {
// 省略...
show({ type = 'info', text = '', duration = 2000 }) {
// 省略...
// 用setTimeout來做一個定時器
setTimeout(() => {
// Element對象內部有一個remove方法,調用之後可以將該元素從dom樹種移除!
messageEl.remove();
}, duration);
}
}
消息被自動移除
可以看到,消息在過了2秒後,自動的從dom樹中移除了,不過呢並沒有動畫,還記得前邊我們寫了move-out
類嗎?這個類和message
是同級的。現在我們只需要在定時結束後把這個類應用到message
元素上就行。
// message.js
class Message {
// 省略...
show({ type = 'info', text = '', duration = 2000 }) {
// 省略...
// 用setTimeout來做一個定時器
setTimeout(() => {
// 首先把move-in這個彈出動畫類給移除掉,要不然會有問題,可以自己測試下
messageEl.className = messageEl.className.replace('move-in', '');
// 增加一個move-out類
messageEl.className += 'move-out';
// 這個地方是監聽動畫結束事件,在動畫結束後把消息從dom樹中移除。
// 如果你是在增加move-out後直接調用messageEl.remove,那麼你不會看到任何動畫效果
messageEl.addEventListener('animationend', () => {
// Element對象內部有一個remove方法,調用之後可以將該元素從dom樹種移除!
messageEl.remove();
});
}, duration);
}
}
注意觀察dom樹的變化:
消失動畫
4.3 監聽“關閉按鈕”的click
事件,來讓用戶可以手動的移除消息。
有時候呢,我們希望消息能夠一直展示,直到用戶來手動的關閉掉,那麼首先我們要加一個參數,用來控制是否展示這個關閉按鈕。
// message.js
class Message {
// 省略...
show({ type = 'info', text = '', duration = 2000, closeable = false }) {
// 創建一個Element對象
let messageEl = document.createElement('div');
// 設置消息class,這裏加上move-in可以直接看到彈出效果
messageEl.className = 'message move-in';
// 消息內部html字符串
messageEl.innerHTML = `
<span class="icon icon-${type}"></span>
<div class="text">${text}</div>
`;
// 是否展示關閉按鈕
if (closeable) {
// 創建一個關閉按鈕
let closeEl = document.createElement('div');
closeEl.className = 'close icon icon-close';
// 把關閉按鈕追加到message元素末尾
messageEl.appendChild(closeEl);
// 監聽關閉按鈕的click事件,觸發後將調用我們的close方法
// 我們把剛纔寫的移除消息封裝爲一個close方法
closeEl.addEventListener('click', () => {
this.close(messageEl)
});
}
// 追加到message-container末尾
// this.containerEl屬性是我們在構造函數中創建的message-container容器
this.containerEl.appendChild(messageEl);
// 只有當duration大於0的時候才設置定時器,這樣我們的消息就會一直顯示
if (duration > 0) {
// 用setTimeout來做一個定時器
setTimeout(() => {
this.close(messageEl);
}, duration);
}
}
/**
* 關閉某個消息
* 由於定時器裏邊要移除消息,然後用戶手動關閉事件也要移除消息,所以我們直接把移除消息提取出來封裝成一個方法
* @param {Element} messageEl
*/
close(messageEl) {
// 首先把move-in這個彈出動畫類給移除掉,要不然會有問題,可以自己測試下
messageEl.className = messageEl.className.replace('move-in', '');
// 增加一個move-out類
messageEl.className += 'move-out';
// 這個地方是監聽動畫結束事件,在動畫結束後把消息從dom樹中移除。
// 如果你是在增加move-out後直接調用messageEl.remove,那麼你不會看到任何動畫效果
messageEl.addEventListener('animationend', () => {
// Element對象內部有一個remove方法,調用之後可以將該元素從dom樹種移除!
messageEl.remove();
});
}
}
我們來調用下試試~
<!-- message.html -->
<!-- 省略... -->
<button class="btn">彈窗消息提醒</button>
<script>
// message可以定義爲全局對象,項目中可以直接調用。
const message = new Message();
document.querySelector('.btn').addEventListener('click', () => {
message.show({
type: 'warning',
text: '點我旁邊的叉叉試試',
duration: 0, // 不會自動消失
closeable: true, // 可手動關閉
});
});
</script>
可手動關閉的消息
其實已經寫的差不多了,不過還是有一些小問題,比如當我們彈出兩個甚至更多消息的時候,如果前邊的消息消失後,下面的消息會直接跳到上面的位置,很僵硬,沒有任何的滑動,如圖:
很僵硬有木有
我們可以通過css的transition
屬性來讓meesage
的高度逐漸變小,這樣下面的元素就會根據變化來逐漸上移。
/* message.css */
/* 省略... */
#message-container .message {
background: #fff;
margin: 10px 0;
padding: 0 10px;
height: 40px;
box-shadow: 0 0 10px 0 #ccc;
font-size: 14px;
border-radius: 3px;
/* 讓消息內部的三個元素(圖標、文本、關閉按鈕)可以垂直水平居中 */
display: flex;
align-items: center;
/* 增加一個過渡屬性,當message元素的高度和margin變化時候將會有一個過渡動畫 */
transition: height 0.2s ease-in-out, margin 0.2s ease-in-out;
}
/* 省略... */
然後我們只需要在Message類的close方法中做一下改變:
close(messageEl) {
// 首先把move-in這個彈出動畫類給移除掉,要不然會有問題,可以自己測試下
messageEl.className = messageEl.className.replace('move-in', '');
// 增加一個move-out類
messageEl.className += 'move-out';
// move-out動畫結束後把元素的高度和邊距都設置爲0
// 由於我們在css中設置了transition屬性,所以會有一個過渡動畫
messageEl.addEventListener('animationend', () => {
messageEl.setAttribute('style', 'height: 0; margin: 0');
});
// 這個地方是監聽transition的過渡動畫結束事件,在動畫結束後把消息從dom樹中移除。
messageEl.addEventListener('transitionend', () => {
// Element對象內部有一個remove方法,調用之後可以將該元素從dom樹種移除!
messageEl.remove();
});
}
看效果:
很平滑有木有
結尾
好了,基本上已經寫好了,不過爲了各個瀏覽器的兼容性,建議大家用babel
轉碼,如果想發佈,可以用webpack
把js和css一塊打包。
不過我們還是少考慮了一個場景,現在的關閉消息都是對象內部調用close
方法來實現,如果我們希望外部能夠控制消息的關閉呢,比如我請求服務器時候彈出一個loading
的消息,現在服務器返回數據後,我怎麼來關閉這個消息呢。