前言

使用vue的朋友应该或多或少都知道element-ui组件吧,这篇博客我将使用原生js实现一个“低配”版本的message组件。
开始前先给出element-ui message组件的网址链接,不论是否有使用过,大家都可以参照的对比。
目标message组件

搭建效果环境

为了演示实现效果,所以我们必须得需要一个完整的运行环境,以如下图案例搭建一个基本的环境:
在这里插入图片描述

创建html文件

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>messageComponent</title>
  <link rel="stylesheet" href="./index.css">
  <style>
    body {
      width: 100%;
      height: 1000px;
      overflow: hidden;
    }

    .button {
      width: 100px;
      height: 30px;
      border: none;
      cursor: pointer;
      display: block;
      margin-top: 20px;
    }
  </style>
</head>

<body>
  <button id="button1" class="button" style="background-color:rgb(167, 167, 163);">消息</button>
  <button id="button2" class="button" style="background-color: rgb(12, 199, 137);">成功</button>
  <button id="button3" class="button" style="background-color: rgb(190, 188, 16);">警告</button>
  <button id="button4" class="button" style="background-color: rgb(204, 20, 20);">错误</button>
  <script src="./index.js"></script>
</body>

</html>

这个文件里面创建了我们演示需要的四个button,然后引入了一个css文件以及一个js文件。
这里需要说明的一个地方是设置了body的样式:因为最后的message组件是直接作为body的子元素,所以为了更好的展示效果,这里将body撑满。

创建css文件

.ui-message {
  min-width: 380px;
  border-width: 1px;
  border-style: solid;
  border-color: #EBEEf5;
  background-color: #edf2fc;
  transform: translateX(-50%);
  position: fixed;
  left: 50%;
  top:20px;
  transition: opacity .3s,transform .4s,top .4s;
  padding: 15px 15px 15px 20px;
  display: flex;
  align-items: center;
  border-radius: 4px;
  overflow: hidden;
}

.ui-message-center{
  justify-content: center;
}

.ui-message .message-content{
margin-left: 16px;
margin: 0;
padding: 0;
font-size: 14px;
line-height: 1;
}

.ui-message .close-button{
position: absolute;
top:50%;
right: 15px;
transform: translateY(-50%);
cursor: pointer;
background-image: url("./img/close.png");
width: 12px;
height: 12px;
background-size: 100% 100%;
}

.ui-message-leave{
opacity: 0;
transform: translate(-50%,-100%);
}

.ui-message-enter{
opacity: 1;
transform: translate(-50%,-100%);
}

.ui-message-info .message-content{
color: #909399;
} 

.ui-message-success {
background-color: #f0f9eb;
border-color: #e1f3d8;
}

.ui-message-success .message-content{
color: #67c23c;
}

.ui-message-warning {
background-color: #fdf6ec;
border-color: #faecd8;
}

.ui-message-warning .message-content{
color: #e6a23c;
} 

.ui-message-error {
background-color: #fef0f0;
border-color: #fde2e2;
}

.ui-message-error .message-content{
color: #f56c6c;
}

这个css样式基本上都是我从element-ui那里原版复制过来的,只有一些略微的细节改动。css样式通俗易懂,我就不做解释,但是这里的css布局大家可以稍微留心一下。
这是css中引用的img图片:
在这里插入图片描述

绑定按钮事件

接下我们需要给html文件里创建的buttons绑定其点击事件。

function start() {
  const button1 = document.getElementById('button1');
  const button2 = document.getElementById('button2');
  const button3 = document.getElementById('button3');
  const button4 = document.getElementById('button4');
  const message = new Message();
  button1.addEventListener('click', () => {
    message.setOption();
  });
  button2.addEventListener('click', () => {
    message.setOption({
      message: '我被点了!',
      type: "success",
      duration: 2000,
    });
  });
  button3.addEventListener('click', () => {
    message.setOption({
      message: '我被点了!',
      type: "warning",
      center: true,
      duration: 3000,
    });
  });
  button4.addEventListener('click', () => {
    message.setOption({
      message: '我被点了!',
      showClose: true,
      type: "error",
      duration: 0,
    });
  });
}

上述代码有三个地方需要说明:

  1. 我们可以把环境想象成一个vue组件,所以这里我们只需要创建一个共享message示例即可。
  2. 这里使用message.setOption模拟element-ui的this.$message,其配置选项的格式是和element-ui几乎一致。
  3. 上述四个按钮的配置的所有功能即是此message组件所要实现的全部功能。

创建messaage组件

在这里,此message组件采用es6的class进行实现。
根据上文所搭建的环境,我们知道message的必备方法有两个:construct()和setOption(options)。

construct()

直接上代码:

constructor() {
    // 消息队列
    this.messageQueue = [];
    // 设置默认值
    this.position = 'top';
    this.message = '';
    this.type = '';
    this.duration = 3000;
    this.body = document.getElementsByTagName('body')[0];
    this.id = 0;
  }

construct里总共有七个属性,message、type、duration以及position这四个属性分别设置其默认值,设置默认值的最主要目的是,因为这些属性的必备属性但是用户可能并未提供,此时就直接使用此默认值。
为什么上文中没有position这个配置项,因为此组件目前只支持top版本的配置,但是可以继续扩展botttom、left、以及right的配置版本。
至于body属性,上文描述到因为需要将创建的message组件添加到body,这里我为了方便便直接在construct里创建了。
messageQuene和id是什么东西,消息队列?,没错,就是它,为什么有这玩意,那就得从message的功能原理开始说明了。
在这里插入图片描述
使用element-ui的message组件,在保证duration一致的前提下,一个一个的message从上往下弹出,然后又一个接一个的从上往下消失,先进先出,这是什么,这不就是队列吗,message队列,那不就是消息队列。
如果只是简单的先进先出,那么也不需要id,但是由于每个message的duration大小不确定,即先进的message不一定先离开,所以这里需要一个id来记录并标识每个message,便于在消息队列里进行快速查找。

setOption(options)

这个方法是此组件最核心的方法,在给出代码前,我们先简单思考一下,这个方法里面我们都需要做些什么事情。
首先,作为一个ui组件,第一步我们需要对options进行参数验证并处理,然后我们就可以创建messageDom,接下来我们需要根据options里面的不同配置来给messageDom设置不同的属性,最后一步也是最重要的一步,messagequeue相关的事件处理。
Talk is cheap,show you my code:

setOption(options) {
    if (typeof options !== "object") {
      options = {};
    }
    const messageDom = document.createElement('div');
    messageDom.classList.add('ui-message');
    messageDom.classList.add('ui-message-leave');
    if (options.center === true) {
      messageDom.classList.add('ui-message-center');
    }
    const targetId = this.id;
    this.messageQueue.push({
      id: targetId,
      messageDom,
    });
    this.setType(messageDom, options.type);
    this.createTextDom(messageDom, options.message);
    this.setCurrentMessageDom();
    this.body.appendChild(messageDom);
    //增加新增动画
    setTimeout(() => {
      messageDom.classList.remove('ui-message-leave');
    }, 100);
    let i = null;
    if (options.showClose === true) {
      i = document.createElement('i');
      i.classList.add('close-button');
      messageDom.appendChild(i);
    }
    const time = isNaN(Number(options.duration)) ? this.duration : Number(options.duration);
    // 如果duration为0则不需要setTimeout
    let timeId = -1;
    if (time !== 0) {
      timeId = setTimeout(() => {
        this.removeMessageDom(messageDom, targetId);
      }, time);
    }
    if (options.showClose === true) {
      i.addEventListener('click', () => {
        this.removeMessageDom(messageDom, targetId);
        if (targetId !== -1) {
          clearTimeout(timeId);
        }
      });
    }
    this.id++;
  }
removeMessageDom(messageDom, targetId) {
    const startIndex = this.messageQueue.findIndex(message => message.id === targetId);
    this.messageQueue.splice(startIndex, 1);
    this.updateMessageDom(startIndex);
    //增加移除动画
    messageDom.classList.add('ui-message-leave');
    setTimeout(() => {
      this.body.removeChild(messageDom);
    }, 400);
  }

上述代码,有三个地方我需要补充说明:

  1. 这里我通过一个全局变化的id和局部不变的targetId来准备定位到需要被移除的messageDom在messageQueue中的位置。这种处理异步的思想与我之前写的一篇异步获取数据博客的内容有着异曲同工之处。异步获取数据问题
  2. 需要保存并记录下每个messageDom的timeId,因为我们在手动关闭(提前移除)messageDom需要及时清除掉此定时器。
  3. 在每个messageDom被移除后,需要及时更新messageQueue的其它所有messageDom的状态,为了提高效率只需要更新被移除的messageDom后面的所有的messageDom的状态即可。

完整的js代码

class Message {
  constructor() {
    // 消息队列
    this.messageQueue = [];
    // 设置默认值
    this.position = 'top';
    this.message = '';
    this.type = '';
    this.duration = 3000;
    this.body = document.getElementsByTagName('body')[0];
    this.id = 0;
  }

  setType(messageDom, type) {
    if (type === '') {
      messageDom.classList.add('ui-message-info');
    } else if (type === 'success') {
      messageDom.classList.add('ui-message-success');
    } else if (type === 'warning') {
      messageDom.classList.add('ui-message-warning');
    } else if (type === 'error') {
      messageDom.classList.add('ui-message-error');
    } else {
      messageDom.classList.add('ui-message-info');// 默认值
    }
  }

  createTextDom(messageDom, message) {
    const p = document.createElement('p');
    p.classList.add('message-content');
    p.textContent = message || this.message;
    messageDom.appendChild(p);
  }

  removeMessageDom(messageDom, targetId) {
    const startIndex = this.messageQueue.findIndex(message => message.id === targetId);
    this.messageQueue.splice(startIndex, 1);
    this.updateMessageDom(startIndex);
    //增加移除动画
    messageDom.classList.add('ui-message-leave');
    setTimeout(() => {
      this.body.removeChild(messageDom);
    }, 400);
  }

  setOption(options) {
    if (typeof options !== "object") {
      options = {};
    }
    const messageDom = document.createElement('div');
    messageDom.classList.add('ui-message');
    messageDom.classList.add('ui-message-leave');
    if (options.center === true) {
      messageDom.classList.add('ui-message-center');
    }
    const targetId = this.id;
    this.messageQueue.push({
      id: targetId,
      messageDom,
    });
    this.setType(messageDom, options.type);
    this.createTextDom(messageDom, options.message);
    this.setCurrentMessageDom();
    this.body.appendChild(messageDom);
    //增加新增动画
    setTimeout(() => {
      messageDom.classList.remove('ui-message-leave');
    }, 100);
    let i = null;
    if (options.showClose === true) {
      i = document.createElement('i');
      i.classList.add('close-button');
      messageDom.appendChild(i);
    }
    const time = isNaN(Number(options.duration)) ? this.duration : Number(options.duration);
    // 如果duration为0则不需要setTimeout
    let timeId = -1;
    if (time !== 0) {
      timeId = setTimeout(() => {
        this.removeMessageDom(messageDom, targetId);
      }, time);
    }
    if (options.showClose === true) {
      i.addEventListener('click', () => {
        this.removeMessageDom(messageDom, targetId);
        if (targetId !== -1) {
          clearTimeout(timeId);
        }
      });
    }
    this.id++;
  }

  setCurrentMessageDom() {
    const index = this.messageQueue.length - 1;
    const targetDom = this.messageQueue[index].messageDom;
    targetDom.style.zIndex = `${2000 + index}`;
    targetDom.style.top = `${64 * index + 20}px`;
  }

  updateMessageDom(startIndex) {
    for (let i = startIndex; i < this.messageQueue.length; i++) {
      const messageDom = this.messageQueue[i].messageDom;
      messageDom.style.zIndex = `${2000 + i}`;
      // 暂不支持换行功能,换行后获取上一个元素的height和top来更新下一个元素的top
      messageDom.style.top = `${64 * i + 20}px`;
    }
  }
}

function start() {
  const button1 = document.getElementById('button1');
  const button2 = document.getElementById('button2');
  const button3 = document.getElementById('button3');
  const button4 = document.getElementById('button4');
  const message = new Message();
  button1.addEventListener('click', () => {
    message.setOption();
  });
  button2.addEventListener('click', () => {
    message.setOption({
      message: '我被点了!',
      type: "success",
      duration: 2000,
    });
  });
  button3.addEventListener('click', () => {
    message.setOption({
      message: '我被点了!',
      type: "warning",
      center: true,
      duration: 3000,
    });
  });
  button4.addEventListener('click', () => {
    message.setOption({
      message: '我被点了!',
      showClose: true,
      type: "error",
      duration: 0,
    });
  });
}

start();

小结

到此,一个message组件已全部完成,至于上文提到的不同position属性的实现,我这里提供一个思路,有兴趣的朋友可以继而自己实现着玩玩:可以为每一种position创建一个messageQueue,它们的核心执行逻辑是一致的,只不过每种messageQueue更新的css样式不一样罢了。

结语

到此,本篇博客已近尾声,如果有直接想看效果的朋友,直接将html,css,js三个文件串起来然后直接运行html文件即可。这篇博客讲述了一个message组件的核心实现(messageQueue和id),并实现了element-ui中message组件中的大部分配置功能,有兴趣的朋友可以把剩下的配置功能也实现了。
对于本篇文章的内容有疑惑的朋友欢迎评论区留言,我会积极给与答复。后面,我应该还会继续给出一些其它组件的实现原理,一起享受“重复造轮子”的乐趣吧!

Logo

前往低代码交流专区

更多推荐