第五集: 从零开始实现一套pc端vue的ui组件库toast组件

第五集: 从零开始实现(toast组件)

本集定位:
toast组件的实现, 争取一章就说完, 更多弹出相关的东西会在alert组件里面.
下章loading组件.
很开心昨天去现场录制'脱口秀大会',心情愉悦😁.

一: toast需求分析

  1. 弹出提示信息的形式出现, 与alert不同, toast以简单提示为主.
  2. 不用用户操作dom, 通过js操作即可(比如封装axios的时候并没有template等标签).
  3. 优先级要可控, 默认要高一些, 毕竟提示要瞩目一些.
  4. 可自己消失, 可手动消失.
  5. 很常见的情况是用户疯狂点击, 或是事件多次触发, 处理好多弹出的状况与动画.
  6. 及时清除dom结构,比如跳转的时候, 不要造成性能的损耗.

二: 基础结构的搭建

话不多少, 导出文件 Toast/index.js.
导出默认的引入项, 是可以如下简写的.

export { default } from './main/toast.js';

// 由于这个js文件内容比较重要, 我们放在后面讲, 先写结构
toast.vue文件

<template>
// toast弹框没有动画的话就太寒掺了.
// 我这边定义的是一个从上到下掉出来的效果. 
  <transition name="cc-toast-fade">
// 命名规则还是老样子, bem原则.
    <div class="cc-toast"
    // 上面提到的 z-index属性, 对于这种组件来说, 用户必须可控.
         :style="'zIndex:'+zIndex">
         // 用户可以随意插入内容
         // 写成变量形式也可以, 这里用slot是为了见名知意.
      <slot />
      // 下面的是关闭的 'X' icon, 
       <i class="cc-toast__closeButton"
         @click="deleteEl"
         v-if="showButton">
        <ccIcon name='cc-close'/>
      </i>
    </div>
  </transition>
</template>

js部分

import ccIcon from "@/components/Icon/main/icon.vue";
export default {
  name: "toast",
  components: { ccIcon },
  props: {
     zIndex: {
      type: Number,
      default: 10
     },
     showButton: {
      // 是否要关闭的x
      type: Boolean,
      default: true
    },
   }
}

三: 本人z-index的一些看法
从z-index的数值设定上就能看出一个人的做事风格, 比如我之前工作时候带过的几个人, 我甚至看到有些人经常写 999999 , 我不让他这样写, 之后他还跟我反应说, 不这样写不习惯🤷‍♀️, 我说如果要写比他等级更高的怎么办? 他说: 肯定不可能有那种情况.
其综合分析也就明白了, 喜欢这样去书写的人,
一是没有太长远的规划, 没有想到未来的多样性,
二是并没有为团队配合而考虑, 没有为他人书写代码'留有余地'而只顾自己.

本人独立负责的实际项目中试验过, 1-10的z-index已经可以应付绝大多数工程了, 一个中小型项目分成10层真的算很多了, 这里面也有一些技巧, 比如说, 每次设置的时候要设置 1, 3, 5, 7, 9, 这是为了如果以后有特殊情况要放到某两个中间的, 可以留一个插入位, 随意约束好z-index是一个合格工程的开端.

四: scss基本样式的设定
vue-cc-ui/src/style/Toast.scss

@import './common/mixin.scss';
@import "./common/animation.scss";

@include b(toast) {
    position: fixed;
    display: inline-block;
    background-color: white;
    top: 6px;
    // 上方, 居中
    left: 50%;
    padding: 6px 20px;
    transition: all .3s;
    transform: translateX(-50%);
    // 本套ui的精髓😏黑边.
    @include commonShadow($--color-black);
    // 定义了动画的样子
    @at-root {
        @include commonType(cc-toast--);
        .#{$namespace}-toast--big {
            padding: 6px 35px 6px 20px;
        }
        .#{$namespace}-toast-fade-enter,
        .#{$namespace}-toast-fade-leave-active {
            opacity: 0;
            transform: translate(-50%, -100%);
        }
    };
}

五: 提示框的type颜色
提示框需要让用户能够很直观的分辨它的意义, 比如用户没有看到提示文字的时候, 但他看到了红色的提示框, 那他也会知道自己不应该进行刚才的操作.

属性当然就是type了

 <div class="cc-toast"
     :class="[toastType]" // 这里接收
     :style="'zIndex:'+zIndex">
 props: {
    type: {
      type: String,
      default: "nomal"
    },

计算属性里面我们还要规定一些合法值, 以及用户传错了的时候的默认值.

computed: {
    toastType() {
    // 这里我做了 正常, 成功, 失败, 警告
      let ary = ["nomal", "success", "warning", "danger"];
      // 这里写三元也可以, 但不方便扩展, 万一以后要改那,,,,
      // 其实无所谓, 我只是不想写那种长长的代码段
      if (ary.includes(this.type)) {
        return "cc-toast--" + this.type;
      }
      return "cc-toast--nomal";
    }
  },

scss

  @at-root {
        @include commonType(cc-toast--);
    };

之前写的util类, 循环加上彩色边框

@mixin commonType($name) {
  @each $type in (success, warning, danger) {
    .#{$name}#{$type} {
      @include commonShadow($type);
    }
  }
}

效果展示

正常
成功
错误
警告

6: 渲染到页面上(说了这个才能引出如何关闭它)

vue-cc-ui/src/components/Toast/main/toast.js
这里我会写很详细的注释!!!

// 1: 把写好的组件引进来, 根据打包的规则, 其实引得已经是处理好的js代码了
import toast from './toast.vue';
export default {
// 2: 配合use方法
  install(Vue) {
// 3: 为了保证全局都能控制弹出toast, 我们只能原型编程
// 这个options就是将来我们调用他的时候, 传的参数.
    Vue.prototype.$ccToast = function(options) {
    // 4: 使用基础 Vue 构造器,创建一个“子类”。
      let Constructor = Vue.extend(toast),node;
      // 5: 我们传入了配置项
      if (typeof options === 'object' && options instanceof Object) {
      // 6: 实例化这个组件
        node = new Constructor({
        // 7: 相当于你在定义data里面的数据
          propsData: options
        });
        // 8: 提取需要弹出的信息到默认插槽
        // 这里为什么要用数组我解释一下
        // 因为这里我们传入的是'结构'
        // 所以他可能多个, 也可能是嵌套,
        // 所以vue 分析它必须要用数组的形式
        // 也可以说他要的是 '子元素的集合'.
        // 这方面有兴趣可以去看render的实现.
        node.$slots.default = [options.message];
         // 5: 我们传入一串字符串
      } else if (typeof options === 'string') {
      // 6: 直接实例化即可
        node = new Constructor();
        node.$slots.default = [options];
      }
      // 9: 调用函数的生命周期
      node.vm = node.$mount();
      // 10: 让他显示出出来
      node.vm.visible = true
      // 11: 无情插入
      // 如果有特殊需求的同学, 不想让他插入body, 而是通过传入来定也是不错的.
      document.body.appendChild(node.$el);
    };
  }
};

7: 清除组件, 也可以说是关闭

  1. 用户点击x之后, 需要移除这个dom结构而不是隐藏
  2. 用户设置了不显示x 则需要倒计时关闭
  3. 元素采用后者遮盖前者的方式显示.
  4. 思前想后没有定义clearAll这个函数, 原因如下:
    之前我做的工程总是出现这样的问题, 比如一个人很谨慎, 他在写完一个提示框之后, 会写一个clearAll(), 把所有的提示都清除, 这有涉及到了工程化, 组件定义的这个clearAll会无差别的清除所有显示的, 导致他清除了别人的提示信息, 而这种bug并不容易发现, 不是很喜欢这种太绝对函数.

x 绑定click事件.

<i class="cc-toast__closeButton"
         @click="deleteEl"
         v-if="showButton">
        <ccIcon name='cc-close'/>
      </i>
// 专门负责清理工作的函数
deleteEl() {
// 先触发动画效果, 不要直接rm, 不然效果会很差.
      this.visible = false;
      setTimeout(() => {
      // 下这样才能清理干净一个组件👇
       // 这里是去除元素
        this.$el.remove();
       // 完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。
        this.$destroy();
      }, 300);
    },

scss

// 这个'x'还需要调节一下样式,  
// 并且浪一下, 赋予hover时候疯狂旋转的动画, 充满朝气
.#{$namespace}-toast__closeButton {
        display: flex;
        cursor: pointer;
        position: absolute;
        align-items: center;
        justify-content: center;
        top: 0;
        right: 0;
        bottom: 0;
        padding: 0 5px;
        &:hover {
            animation: rotating .5s infinite linear;
        }
    }

用户设置了自动关闭, 过期时间,

  props: {
    closeTime: {
          // 关闭的延时
          type: Number,
          default: 2000
        },
    }
// 初始化是否设置了时间的操作
  initAutoClose() {
// 万一用户传了个负数, 还是替他abs一下吧.
      if (this.autoClose && Math.abs(this.closeTime) > 0) {
      // 这里要用变量接一下, 方便停止他
        this.timer = setTimeout(() => {
          this.deleteEl();
        }, Math.abs(this.closeTime));
      }
    },  

用户想详细看看提示内容, 把鼠标放在了toast上, 并不点击关闭

<div class="cc-toast"
         :class="[
         toastType,{
          'cc-toast--big':showButton
         }]"
         :style="'zIndex:'+zIndex"
         // 移入进来就别关闭倒计时了
         @mousemove="clearTimer"
         // 移出就从新走一遍倒计时初始化
         @mouseleave="initAutoClose"
         v-show="visible">
clearTimer() {
      clearTimeout(this.timer);
    },

用户想点击键盘的esc键, 关闭这个组件

mounted() {
    this.initAutoClose();
    // 开始就要添加对用户键盘事件的监听
    document.addEventListener("keydown", this.keydown);
  },
keydown(e) {
      if (e.keyCode === 27) {
        this.deleteEl();
      }
    }

// 既然有document的操作, 当然就要有去除的操作.

 beforeDestroy() {
      // 万一用户写了个很长的时间, 虽然element没有对此进行处理, 但我还是想写一下.
      this.clearTimer()
      document.removeEventListener('keydown', this.keydown);
    }

end 到这里为止, 一个健康的toast也就可以正常使用了

感谢您的阅读, 喜欢的话就收藏吧, 让我们一起终生学习.

下章loading组件.

github:github
个人网站:链接描述

阅读 1.7k

推荐阅读