element 源碼學習五 —— Notice 系列組件學習

消息提示行爲是開發中很是常見的功能,Element 爲咱們提供了很是好用和美觀的消息提示組件。這裏就簡單學習下 Notice 組件的 CSS 和代碼邏輯。

簡介

Notice 包括了五類組件:css

  • Alert 用於頁面中展現重要的提示信息。
  • Loading 加載數據時顯示動效。
  • Message 經常使用於主動操做後的反饋提示。與 Notification 的區別是後者更多用於系統級通知的被動提醒。
  • MessageBox 模擬系統的消息提示框而實現的一套模態對話框組件,用於消息提示、確認消息和提交內容。
  • Notification 懸浮出如今頁面角落,顯示全局的通知提醒消息。

本文中不一樣角度來學習這些組件(這些組件有不少類似性,因此一塊兒學習啦~)。html

demo

下面是參照 element ui 寫的一個小demo,嘗試着瞭解下其中的 CSS
貼代碼:vue

<!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>

代碼的運行結果請看 ->這裏<-node

以上代碼實現了數組

  • Alert 的樣式
  • loading 的簡單版本
  • message 的無動畫版本。

只實現這三個功能的緣由是另外兩個功能和擴展功能都是基於這個demo擴展的。
Alert 是一個靜態的文本內容,只是外部包裹了帶樣式的容器。可能再多個隱藏按鈕和消息圖標。
Loading 和 MessageBox 其實的基本邏輯是插入或顯示新的內容,並在顯示完成後消失。須要注意的就是後面要添加一層遮罩陰影,遮罩若是非全屏使用 position:absolute 而全屏則使用 position:fixed 覆蓋。另外就是注意 z-index 屬性將組件放到視圖最上層。
Message 和 Notification 其實就是文本內容、圖標和按鈕組合容器的現實和隱藏過程。它們的過渡動畫使用的是 vue 的進入/離開 & 列表過渡來實現。服務器

經過幾個問題來看源碼

其實從樣式上,上面的 demo 已經實現了大體的樣子了。下面來看看組件的一些邏輯。源碼內容比較多,因此就以問答的方式有目的的來看源碼。app

Alert 的界面實現?

Alert 由圖標、文本內容、描述內容和關閉按鈕組成:dom

<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> 標籤內插入自定義內容。ide

如何顯示純文本、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> 標籤給開發者預留拓展空間,使用構造函數的方法來拓展組件邏輯並定義一些快捷方法。

我的感受學習一些成熟組件的源碼可以學到很多東西,收穫很多。

相關文章
相關標籤/搜索