1
消息提示行为是开发中非常常见的功能,Element 为我们提供了非常好用和美观的消息提示组件。这里就简单学习下 Notice 组件的 CSS 和代码逻辑。

简介

Notice 包括了五类组件:

  • Alert 用于页面中展示重要的提示信息。
  • Loading 加载数据时显示动效。
  • Message 常用于主动操作后的反馈提示。与 Notification 的区别是后者更多用于系统级通知的被动提醒。
  • MessageBox 模拟系统的消息提示框而实现的一套模态对话框组件,用于消息提示、确认消息和提交内容。
  • Notification 悬浮出现在页面角落,显示全局的通知提醒消息。

本文中不同角度来学习这些组件(这些组件有很多相似性,所以一起学习啦~)。

demo

下面是参照 element ui 写的一个小demo,尝试着了解下其中的 CSS
贴代码:

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

<head>
    <meta charset="UTF-8">
    <title>My Notice</title>
    <style>
        .success-color {
            background-color: #f0f9eb;
            color: #67c23a;
        }

        .info-color {
            background-color: #f4f4f5;
            color: #909399;
        }

        .warning-color {
            background-color: #fdf6ec;
            color: #e6a23c;
        }

        .error-color {
            background-color: #fef0f0;
            color: #f56c6c;
        }


        .alert-container {
            position: relative;
            padding: 8px 16px;
            border-radius: 5px;
            opacity: 1;
            align-items: center;
            overflow: hidden;
            display: flex;
        }

        .success-text {
            font-size: 13px;
            line-height: 18px;
            padding: 0 8px;
            color: #67c23a;
        }

        .close {
            font-size: 13px;
            position: absolute;
            top: 12px;
            right: 15px;
            cursor: pointer;
        }

        .loading-background {
            position: fixed;
            z-index: 2000;
            background-color: rgba(26, 26, 26, 0.9);
            margin: 0;
            top: 0;
            right: 0;
            bottom: 0;
            left: 0;
            transition: opacity 0.3s;
            z-index: 2000;
        }

        .loading-div {
            top: 50%;
            position: absolute;
            margin-top: -20px;
            height: 40px;
            width: 100%;
            align-items: center;
            justify-content: center;
            display: flex;

        }

        .loading-text {
            color: #f0f9eb;
            font-size: 15px;
            overflow: hidden;
            z-index: 2001;
        }

        .el-message {
            min-width: 380px;
            box-sizing: border-box;
            border-radius: 4px;
            border-width: 1px;
            border-style: solid;
            border-color: #ebeef5;
            position: fixed;
            left: 50%;
            top: 20px;
            transform: translateX(-50%);
            background-color: #edf2fc;
            transition: opacity 0.3s, transform 0.4s;
            overflow: hidden;
            padding: 15px 15px 15px 20px;
            display: flex;
            align-items: center;
        }
    </style>
</head>

<body>
    <div id="app">
        <div class="alert-container success-color">
            <span class="success-text">成功提示的文案</span>
            <span class="close">X</span>
        </div>
        <button id="showLoading">显示加载中</button>
        <button id="showMessage">显示信息</button>
        <div class="el-message" id="message" style="display:none;">
            <p>这是一条消息</p>
        </div>
    </div>

    <script>
        var alertContainer = document.getElementsByClassName("alert-container")[0]
        var close = document.getElementsByClassName("close")[0]
        close.onclick = function () {
            alertContainer.style = "display:none;"
        }

        var app = document.getElementById("app")
        var bg = document.createElement("div")
        bg.className = "loading-background"
        var div = document.createElement("div")
        div.className = "loading-div"
        var text = document.createElement("span")
        text.className = "loading-text"
        text.textContent = "加载中……"
        div.appendChild(text)
        bg.appendChild(div)


        document.getElementById("showLoading").onclick = function () {
            if (document.getElementsByClassName("loading-background").length > 0) {
                return;
            }
            app.appendChild(bg)
            setTimeout(() => {
                if (document.getElementsByClassName("loading-background").length > 0) {
                    app.removeChild(bg)
                }
            }, 3000)
        }

        document.getElementById("showMessage").onclick = function () {
            document.getElementById("message").style = "display:block;"
            setTimeout(() => {
                document.getElementById("message").style = "display:none;"
            }, 3000)
        }
    </script>
</body>

</html>

代码的运行结果请看 ->这里<-

以上代码实现了

  • Alert 的样式
  • loading 的简单版本
  • message 的无动画版本。

只实现这三个功能的原因是另外两个功能和扩展功能都是基于这个demo扩展的。
Alert 是一个静态的文本内容,只是外部包裹了带样式的容器。可能再多个隐藏按钮和消息图标。
Loading 和 MessageBox 其实的基本逻辑是插入或显示新的内容,并在显示完成后消失。需要注意的就是后面要添加一层遮罩阴影,遮罩如果非全屏使用 position:absolute 而全屏则使用 position:fixed 覆盖。另外就是注意 z-index 属性将组件放到视图最上层。
Message 和 Notification 其实就是文本内容、图标和按钮组合容器的现实和隐藏过程。它们的过渡动画使用的是 vue 的进入/离开 & 列表过渡来实现。

通过几个问题来看源码

其实从样式上,上面的 demo 已经实现了大致的样子了。下面来看看组件的一些逻辑。源码内容比较多,所以就以问答的方式有目的的来看源码。

Alert 的界面实现?

Alert 由图标、文本内容、描述内容和关闭按钮组成:

  <transition name="el-alert-fade">
    <div
      class="el-alert"
      :class="[typeClass, center ? 'is-center' : '']"
      v-show="visible"
      role="alert"
    >
      <!-- 图标 -->
      <i class="el-alert__icon" :class="[ iconClass, isBigIcon ]" v-if="showIcon"></i>
      <div class="el-alert__content">
        <!-- 标题 -->
        <span class="el-alert__title" :class="[ isBoldTitle ]" v-if="title">{{ title }}</span>
        <slot>
          <!-- 插槽,默认插入描述文本 -->
          <p class="el-alert__description" v-if="description">{{ description }}</p>
        </slot>
        <!-- 关闭图标按钮 -->
        <i class="el-alert__closebtn" :class="{ 'is-customed': closeText !== '', 'el-icon-close': closeText === '' }" v-show="closable" @click="close()">{{closeText}}</i>
      </div>
    </div>
  </transition>

组件很简单,注释上都写了~组件还做了 slot 插槽拓展,可以在 <el-alert> 标签内插入自定义内容。

如何显示纯文本、HTML 和 VNode

显示文本使用 {{ text }} 指令来显示内容。
显示HTML使用 v-html 指令来渲染显示。

<!-- 显示文本 -->
<p v-if="!dangerouslyUseHTMLString" class="el-message__content">{{ message }}</p>
<!-- 显示HTML -->
<p v-else v-html="message" class="el-message__content"></p>

使用 vue 的 render 函数生成 VNode 对象传给组件作为组件 slot 插槽的默认显示结果。

  if (isVNode(instance.message)) {
    instance.$slots.default = [instance.message];
    instance.message = null;
  }

组件的过渡动画怎么来的

使用 Notice 系列组件时,发现组件的显示和消失都是有过渡动画的。界面看着更加友好和舒服。实现方式就是使用了 vue 的进入/离开 & 列表过渡来实现效果。

界面的关闭使用的是什么方式?

对于 loading 和 message-box ,在没有界面时会在 body 最后添加组件内容,显示过后使用 v-show="false" (display:none;) 隐藏,随后就调用显示和隐藏界面。

// 将 message-box 组件加入到 body 中
document.body.appendChild(instance.$el);

对于 Alert 由于一开始就显示,只是删除按钮,所以只需修改 v-show 属性隐藏即可。
对于 message 和 notification,这两个组件可以多次弹出,逐个关闭(自动或手动)。所以这两个组件是保存在一个数组中,然后进行渲染的,关闭某个组件就是一个将组件从组件数组中移除的过程。

// id 组件id
// useOnClose 自定义关闭函数
Message.close = function(id, userOnClose) {
  for (let i = 0, len = instances.length; i < len; i++) {
    if (id === instances[i].id) {
      // 找到组件,执行自定义关闭函数并从数组中移除
      if (typeof userOnClose === 'function') {
        userOnClose(instances[i]);
      }
      instances.splice(i, 1);
      break;
    }
  }
};

message-box 的回调函数实现

对于 message-box 有两种函数回调:callback 函数和 Promise 函数。

  if (typeof Promise !== 'undefined') {
    return new Promise((resolve, reject) => {

      msgQueue.push({
        options: merge({}, defaults, MessageBox.defaults, options),
        callback: callback,
        resolve: resolve,
        reject: reject
      });

      showNextMsg();
    });
  } else {
    msgQueue.push({
      options: merge({}, defaults, MessageBox.defaults, options),
      callback: callback
    });

    showNextMsg();
  }

当然,到这一步只是将组件配置和回调函数组成对象,并没有执行。执行实在 showNextMsg 方法中。showNextMsg 方法用于组合当前组件 options 并写入到 DOM 中,然后显示组件。其中有这么一段关于回调的:

      // 如果 currentMsg.options.callback 为 undefined
      if (options.callback === undefined) {
        instance.callback = defaultCallback;
      }

所以再看看 defaultCallback 函数对象:

const defaultCallback = action => {
  if (currentMsg) {
    let callback = currentMsg.callback;
    if (typeof callback === 'function') {
      if (instance.showInput) {
        callback(instance.inputValue, action);
      } else {
        callback(action);
      }
    }
    if (currentMsg.resolve) {
      if (action === 'confirm') {
        if (instance.showInput) {
          currentMsg.resolve({ value: instance.inputValue, action });
        } else {
          currentMsg.resolve(action);
        }
      } else if (action === 'cancel' && currentMsg.reject) {
        currentMsg.reject(action);
      }
    }
  }
};

这里就可以看到回调函数和 Promise 的调用和传参过程。当组件“关闭”的时候执行 callback 方法回调:

      doClose() {
        ……
        setTimeout(() => {
          if (this.action) this.callback(this.action, this);
        });
      },

至此实现了回调及其传参。

如何实现显示多个 Notification 向下偏移插入?

偏移量计算在 Notification 的构造方法中计算获得当前组件 verticalOffset

const Notification = function(options) {
  // 服务器渲染
  if (Vue.prototype.$isServer) return;
  options = options || {};
  const userOnClose = options.onClose; // 自定义关闭
  const id = 'notification_' + seed++; // 组件 id
  const position = options.position || 'top-right'; // 位置
  // 关闭事件
  options.onClose = function() {
    Notification.close(id, userOnClose);
  };
  // 组件实例
  instance = new NotificationConstructor({
    data: options
  });
  // vnode
  if (isVNode(options.message)) {
    instance.$slots.default = [options.message];
    options.message = 'REPLACED_BY_VNODE';
  }
  instance.id = id;
  instance.vm = instance.$mount();
  // 添加实例
  document.body.appendChild(instance.vm.$el);
  instance.vm.visible = true;
  instance.dom = instance.vm.$el;
  instance.dom.style.zIndex = PopupManager.nextZIndex();
  // 偏移量计算
  let verticalOffset = options.offset || 0;
  instances.filter(item => item.position === position).forEach(item => {
    verticalOffset += item.$el.offsetHeight + 16;
  });
  verticalOffset += 16;
  instance.verticalOffset = verticalOffset;
  // 传入数组
  instances.push(instance);
  return instance.vm;
};

偏移量 verticalOffset 在组件 package\notification/src/main.vue 中使用。

    <div :style="positionStyle" ></div>
    computed: {
      // 正则匹配 position 是 top 还是 bottom
      verticalProperty() {
        return /^top-/.test(this.position) ? 'top' : 'bottom';
      },
      // 最后返回的内联样式
      positionStyle() {
        return {
          [this.verticalProperty]: `${ this.verticalOffset }px`
        };
      }
    }

如果 positionbottom-left 偏移量 verticalOffset 为 20,那么返回的内联样式就是:

{
  bottom: 20px;
}

至此实现了偏移的功能。

最后

这里简单学习了一下 Notice 系列组件的样式和逻辑。从中学到了:

  • 用 CSS 设计样式和动态修改样式属性。
  • 用了许多 DOM 的操作
  • 使用已有轮子 —— 用到了 Vue 的 directive、 transition 和 render。
  • 组件设计方面,学到了使用数组管理多个同类组件;使用 <slot> 标签给开发者预留拓展空间,使用构造函数的方法来拓展组件逻辑并定义一些快捷方法。

个人感觉学习一些成熟组件的源码能够学到不少东西,收获不少。


VioletJack
931 声望161 粉丝

专注于Vue前端开发的学习和分享