如何寫一個全域性的 Notice 元件?

2022-06-10 15:00:36

下面將會實現這樣的效果:

  1. 元件動態建立指令碼:

    NotificationBanner.js

    import Vue from "vue";
    import Notice from "@/components/Noticer/Notice.vue";
    
    function create(Component, props) {
      // 先建立範例
      const vm = new Vue({
        render(h) {
          //h就是createElement,它返回VNode
          return h(Component, { props });
        },
      }).$mount();
      // 手動掛載
    
      // 判斷是否存在container,如果不存在則先建立
      let container;
      container = document.querySelector(".noticer-container");
      if (container == null) {
        container = document.createElement("div");
        container.classList.add("noticer-container");
        container.style.position = "fixed";
        container.style.top = "50px";
        container.style.right = "0px";
        container.style.overflow = "hidden";
        container.style.zIndex = 9999;
    
        document.body.appendChild(container);
      }
    
      container.appendChild(vm.$el);
    
      //銷燬方法
      const comp = vm.$children[0];
      comp.remove = function () {
        container.removeChild(comp["$el"]);
        vm.$destroy();
      };
      comp.show();
      return comp;
    }
    
    Vue.prototype.$notice = {
      error: function (props) {
        create(Notice, Object.assign(props, { type: "error" }));
      },
      info: function (props) {
        create(Notice, Object.assign(props, { type: "info" }));
      },
      success: function (props) {
        create(Notice, Object.assign(props, { type: "success" }));
      },
      warn: function (props) {
        create(Notice, Object.assign(props, { type: "warn" }));
      },
    };
    
    

    這裡有一些值得注意的地方:

    1. container: 的作用是notice的容器,它可以用於定位notice在頁面的哪裡展示,注意notice不應該隨頁面捲動,因此其定位是fixed, 而之所以設定為 overflow:hidden 的原因則是,notice 在出現和移除的時候,發生的動畫偏移,會讓頁面出現橫向卷軸。為了避免重複建立container, 這裡做了一個判斷邏輯。然後所有動態生成的notice範例dom都會通過 appendChild 新增到這個容器。
    2. 在移除的時候, 移除的是 vm.$children[0]["$el"] , 原因是,Notice 模板的實現中,外層套了一個 transition , 而這個transition是並不會渲染dom的。
  2. 建立Notice元件模板:

    元件模板

    <template>
      <transition
        enter-active-class="animate__animated animate__slideInRight"
        leave-active-class="animate__animated animate__slideOutRight"
        @after-leave="afterLeave"
      >
        <div v-if="isShow" class="notice__root">
          <div :class="`notice-type-${type}`" class="noticer">
            {{
              type === "error"
                ? "&#127827;"
                : type === "success"
                ? "&#127808;"
                : type === "warn"
                ? "&#127819;"
                : "&#128051;"
            }}
            : {{ message }}
          </div>
        </div>
      </transition>
    </template>
    <script>
    export default {
      props: {
        title: {
          type: String,
          default: "",
        },
        message: {
          type: String,
          default: "",
        },
        time: {
          type: Number,
          default: 1000,
        },
        type: {
          type: String,
        },
      },
      data() {
        return {
          isShow: false,
        };
      },
      methods: {
        show() {
          this.isShow = true;
          setTimeout(this.hide, this.time);
        },
        hide() {
          this.isShow = false;
        },
        afterLeave() {
          this.remove();
        },
      },
    };
    </script>
    <style lang="less" scoped>
    @error: rgb(255, 30, 30);
    @warn: rgb(240, 192, 0);
    @success: rgb(0, 144, 74);
    @info: rgb(0, 80, 218);
    
    @errorBg: rgb(255, 208, 208);
    @warnBg: rgb(255, 245, 207);
    @successBg: rgb(210, 255, 233);
    @infoBg: rgb(203, 222, 255);
    
    .notice__root {
      user-select: none;
      padding: 5px 50px 5px 5px;
    }
    
    .noticer {
      padding: 5px 20px;
      margin: 10px 0px;
      // margin-right: 50px;
      border-radius: 8px;
      font-size: 16px;
      width: auto;
      min-width: 280px;
      max-width: 300px;
      word-break: break-all;
      text-align: center;
      box-sizing: border-box;
    }
    .notice-type-error {
      color: @error !important;
      border: 2px solid @error;
      box-shadow: 1px 1px 5px 2px @errorBg;
      background-color: @errorBg;
    
      // border: 1px solid red;
    }
    .notice-type-warn {
      color: @warn !important;
      border: 2px solid @warn;
      background-color: @warnBg;
      box-shadow: 1px 1px 5px 2px @warnBg;
    }
    .notice-type-success {
      color: @success !important;
      border: 2px solid @success;
      background-color: @successBg;
      box-shadow: 1px 1px 5px 2px @successBg;
    }
    .notice-type-info {
      color: @info !important;
      border: 2px solid @info;
      background-color: @infoBg;
      box-shadow: 1px 1px 5px 2px @infoBg;
    }
    </style>
    
  3. main.js 中引入執行該指令碼即可

    import Vue from "vue";
    import App from "./App.vue";
    import "animate.css";
    import "@/components/Noticer/NotificationBanner.js";
    
    
    new Vue({
      render: (h) => h(App),
    }).$mount("#app");
    
  4. 程式碼中使用範例:

    if (!this.nickname) {
        this.$notice.error({
            message: "好漢!姓甚名誰?",
            time: 3000,
        });
    } else {
        this.showModal = false;
        this.$notice.info({
            message: this.nickname + "來了!!!",
            time: 3000,
        });
    }
    

動態建立元件的執行邏輯:
當在使用的時候:

this.$notice.error({
    message: "好漢!姓甚名誰?",
    time: 3000,
});

上方程式碼觸發,實際上會觸發 NotificationBanner.js 中的 create函數,該函數建立了一個notice 的元件範例,並在實力上新增了一個remove 方法,然後直接觸發元件中的 show 方法。

notice 模板範例中:

methods: {
    show() {
        this.isShow = true;
        setTimeout(this.hide, this.time);
    },
    hide() {
        this.isShow = false;
    },
    afterLeave() {
        this.remove();
    },
},

show 方法執行,除了展示 notice,立即設定一個延時函數執行 hide 方法。

hide 方法執行, vue 提供的 transition 勾點 afterleave() 會在移除動畫執行完畢後觸發。 這時候,去觸發 remove 方法,將該notice 元件範例移除。