面对面用Vue造一个Toast轮子(包含测试用例)

2019-09-22  本文已影响0人  取个帅气的名字真好

先上效果图...

toast.gif
toast.png

Toast需求:

用法:

import Vue from 'vue'
import plugin from './plugin/index'
Vue.use(plugin)

this.$toast('操作成功!!!')

Github:欢迎Star


这里是分割线


开始面对面,看代码...

不管在哪里都可以直接使用this.$toast,具体如何实现??? 请看Vue官方文档

1、main.js

Vue.prototype.$toast = function () {
  console.log('我是苏宋霖');
}

2、App.vue

<template>
  <div id="app">
    <button @click="xxx">点我</button>
  </div>
</template>
  export default {
    name: 'app',
    methods: {
      xxx() {
        this.$toast()
      },
    }
}

点击按钮控制台是不是打印出了我是苏宋霖,做到这里你已经成功一半了🌝

clickToast.gif

但是这样并不是我们想要的,使用插件形式install,让用户主动去使用。

继续改造✨

新建plugin/index.js

export default {
  install(Vue,options){
    Vue.prototype.$toast = function(message){
      console.log(message);
    }
  }
}

main.js

import plugin from './plugin/index'
Vue.use(plugin)

使用

this.$toast('我是苏宋霖')

控制一样打印出我是苏宋霖, 说明你距离成功不远了 (这句话跟高中老师一样,经常对我们学生说前脚已经跨进北大的校门了,后来我们毕业了才知道北大是可以随便进出,哪怕登记一下就好)

Vue.use 会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件。

继续改造✨✨

新建components/Toast.vue

<template>
  <div>
      <slot></slot>  
  </div>
</template>
<script>
export default {
  name:'Toast'  
}
</script>

在引入Toast.vue之前有个问题如何在js使用vue实例 ??

1、方应杭的Vue 动态创建实例
2、滴滴的cube-ui专门为这个场景实现了一个create-api, 可以将任意自定义组件制作成调用时动态创建的插件

plugin/index.js 引入Toast.vue

import Toast from '@/components/Toast.vue'
export default {
  install(Vue,options){
    Vue.prototype.$toast = function(message){
      let Constructor = Vue.extend(Toast)
      let toast = new Constructor()
      toast.$mount()
      document.body.appendChild(toast.$el)  //添加到页面中
    }
  }
}

通过👆代码可以实现动态创建实例(别用原生js实现如: let div = document.createElement("div"); document.body.appendChild(div),这样无法使用到vue的各种生命周期及Api)

appendChildToast.gif
继续改造✨✨✨

Toast.vue 文件不变,index.js通过插槽传值给Toast.vue
问题:如果在js中使用插槽传值啊??? 我的天
看文档渲染函数 & JSX
this.$slots.default // 子节点数组 注意这里接收的是数组 数组 数组

import Toast from '@/components/Toast.vue'
export default {
  install(Vue,options){
    Vue.prototype.$toast = function(message){
      let Constructor = Vue.extend(Toast)
      let toast = new Constructor()
      toast.$slots.default = [message] //slots必须要放在mount之前
      toast.$mount()
      document.body.appendChild(toast.$el)
    }
  }
}
slots.gif

添加简单样式

<style lang="scss" scoped>
 $font-size:14px;
  $toast-height:40px;
  $toast-bg:rgba(0, 0, 0, 0.75);
  .toast {
    font-size: $font-size;
    line-height: 1.8;
    height: $toast-height;
    position: fixed;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    color: white;
    background: $toast-bg;
    border-radius: 4px;
    box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.50);
    padding: 0 16px;
  }
</style>
继续改造✨✨✨✨

实现3s后自动关闭,

  props: {
    // 自动关闭
    autoClose: {
      type: Boolean,
      default: true
    },
    // 关闭时间
    autoCloseDelay: {
      type: Number,
      default: 3
    }
  },
  mounted() {
    if (this.autoClose) {
      setTimeout(() => {
        this.close();
      }, this.autoCloseDelay * 1000);
    }
  },
  methods: {
    close() {
      this.$el.remove(); //删除
      this.$destroy(); //清除绑定的一些事件
    }
  }
autoClose.gif
继续改造✨✨✨✨✨

弹出Toast用户可点击关闭

<template>
  <div class="toast">
    <slot></slot>
    <div class="line"></div>
    <span class="close" v-if="closeButton" @click="onClickclose">{{closeButton.text}}</span>
  </div>
</template>
<script>
export default {
  name: "Toast",
  props: {
  //点击关闭
    closeButton: {
      type: Object,
      default() {
        return {
          text: "关闭",
          callback: undefined
        };
      }
    }
  },
 
  methods: {
    close() {
      this.$el.remove(); //删除
      this.$destroy(); //清除绑定的一些事件
    },
    onClickclose() {
      this.close();
      if (this.closeButton && typeof this.closeButton.callback === "function") {
        this.closeButton.callback();
      }
    }
  }
};
</script>

plugin/index.js

import Toast from '@/components/Toast.vue'
export default {
  install(Vue,options){
    Vue.prototype.$toast = function(message,toaseOptions){
      let Constructor = Vue.extend(Toast)
      let toast = new Constructor({
        propsData:toaseOptions
      })
      toast.$slots.default = [message]
      toast.$mount()
      document.body.appendChild(toast.$el)
    }
  }
}

使用

    this.$toast('我是苏宋霖',{
            closeButton:{
            text:'知道了',
            callback(){
              console.log('苏宋霖点击知道了');
            }
          }
        })
closeButton.gif

手动测试发现一个问题,内容过多的时候Toast并没有变化,
关闭按钮被挤压
如下:


溢出.png

改造吧支持多行文字
使用js去给line添加高度,因为父元素的高度变成min-height。

//html 
 <div class="toast" ref="toast">
    <slot></slot>
    <div class="line" ref="line"></div>
    <span class="close" v-if="closeButton" @click="onClickclose">{{closeButton.text}}</span>
  </div>
//js
mounted(){
    this.updateStyle()
},
  methods: {
     updateStyle() {
        this.$nextTick(() => {
          this.$refs.line.style.height =
            `${this.$refs.toast.getBoundingClientRect().height}px`
        })
      },
}
//css 新增的
<style lang="scss" scoped>
$toast-min-height: 40px;
.toast {
  min-height: $toast-min-height;
  .close {
    flex-shrink: 0;
  }
}
</style>
改造后....png

继续改造✨✨✨✨✨✨

Toast的位置
在.toast上加:class="toastClasses",通过props传过来的属性position 动态添加class

//html
 <div class="toast" ref="toast" :class="toastClasses">
    <slot></slot>
    <div class="line" ref="line"></div>
    <span class="close" v-if="closeButton" @click="onClickclose">{{closeButton.text}}</span>
  </div>

//js
props:{
    // 位置
    position: {
      type: String,
      default: "top",
      validator(value) {
        return ["top", "bottom", "middle"].indexOf(value) >= 0;
      }
    }
}

  computed: {
    toastClasses() {
      return {
        [`position-${this.position}`]: true
      };
    }
  },
//css
  &.position-top {
    top: 0;
    transform: translateX(-50%);
  }

  &.position-bottom {
    bottom: 0;
    transform: translateX(-50%);
  }
  &.position-middle {
    top: 50%;
    transform: translate(-50%, -50%);
  }

使用

<template>
  <div id="app">
    <button @click="xxx1">点击 上</button>
    <button @click="xxx2">点击 中</button>
    <button @click="xxx3">点击 下</button>
  </div>
</template>

<script>
  export default {
    name: 'app',
    methods: {
      xxx1(){
        this.xxx('top')
      },
      xxx2(){
         this.xxx('middle')
      },
      xxx3(){
         this.xxx('bottom')
      },
      xxx(position) {
        this.$toast('我是苏宋霖我是苏宋霖我是苏宋霖我是苏宋霖我是苏宋霖我是苏宋霖我是苏宋霖',{
          position,
            closeButton:{
            text:'知道了',
            callback(){
              console.log('苏宋霖点击知道了');
            }
          }
        })
      },
    
    },
  }
</script>

position.gif

新问题:
用户重复点击Toast,会出现多个DOM??


重复DOM.gif

解决方案:
如果已经有一个Toast,就把之前的给删了
plugin/index.js

import Toast from '@/components/Toast.vue'

export default {
  install(Vue,options){
    let currentToast ;
    Vue.prototype.$toast = function(message,toaseOptions){
      if(currentToast){currentToast.close()}//如果有Toast就删除上一个
      currentToast =  createToast({Vue,propsData:toaseOptions,message})
    }
  }
}

function createToast({Vue,propsData,message}) {
  let Constructor = Vue.extend(Toast)
  let toast = new Constructor({ propsData})
  toast.$slots.default = [message]
  toast.$mount()
  document.body.appendChild(toast.$el)
  return toast
}
DOM正常.gif

ps:样式啥的请自行修改。


轮子完毕!!! 接下来测试用例..

未完待续...

提示:

npm 配置淘宝源:npm config set registry https://registry.npm.taobao.org/

上一篇下一篇

猜你喜欢

热点阅读