从0开始手写一个Vue消息弹窗组件

前言

近期闲来无事,就想折腾一点东西,无奈技术水平太低,做不了什么高深复杂的项目,所以还是先从简单的做起吧。平时我们做项目的时候或多或少都会用到一些组件库,比如element、muse之类的,所以这次我想自己手动写一个组件,封装成一个包,可以暴露到外部去调用。

效果预览

废话不多说,这次写的是一个Vue的消息弹窗组建,借鉴了element的message组件,大家可以去看一看element的源码,里面一些组件的实现方式很巧妙,非常值得去学习。话不多说,先来看看最终的实现效果:

不同主题

关闭按钮
不同位置

代码

第一步应该是写一个模板:

<template>
  <transition name="msg-fade" @after-leave="afterLeave">
    <div :class="classes" class="msg-container" v-show="visible" role="alert" @mouseenter="clearTimer" @mouseleave="startTimer">
      <span>{{ message }}</span>
      <i class="msg-icon-wrapper" v-if="showCloseButton" @click="close">
        <img class="msg-icon" src="./close.svg" alt="close" />
      </i>
    </div>
  </transition>
</template>

可以看到,html内容是很简单的,主要是几个标签和一些props,还有几个事件,然后我们再来看js:

<script>
export default {
  name: 'Message',
  props: {
    // 要显示的消息
    message: {
      type: String,
      require: true
    },
    // 主题
    type: {
      type: String,
      default: 'normal'
      // normal success error warning
    },
    // 是否显示关闭按钮
    showCloseButton: {
      type: Boolean,
      default: false
    },
    // 组件出现的位置
    location: {
      type: String,
      default: 'top-center'
      // top-center top-right top-left bottom-center bottom-right bottom-left
    },
    // 自定义样式名
    className: {
      type: String,
      default: ''
    },
    // 持续时间
    duration: {
      type: Number,
      default: 2000
    }
  },
  data () {
    return {
      // 动态修改样式
      classes: [
        {
          'msg-icon-padding': this.showCloseButton
        }, 
        `msg-${this.type}`,
        `msg-${this.location}`,
        this.className
      ],
       // 控制组件显示
      visible: false,
       // 是否关闭
      closed: false,
       // 定时器
      timer: null
   }
  },
  watch: {
    closed (newVal) {
      if (newVal) {
        this.visible = false
      }
    }
  },
  methods: {
    close () {
      this.closed = true
    },
    afterLeave () {
      this.$destroy(true)
      this.$el.parentNode.removeChild(this.$el)
    },
    clearTimer () {
      clearTimeout(this.timer)
    },
    startTimer () {
      const timeout = this.duration
      if (timeout > 0) {
        this.timer = setTimeout(() => {
          if (!this.closed) {
            this.close()
          }
        }, timeout);
      }
    }
  },
  mounted () {
    this.startTimer()
    this.visible = true
  },
  beforeDestroy () {
    this.clearTimer()
  }
}
</script>

可以看到组件的代码逻辑也是非常简单的,props的处理我就不多细说了,可能每一个人的需求都一样。重点要讲一讲的是组件的创建和销毁过程。在组件挂载后,组件会调用startTimer方法生成一个定时器,按照duration指定的时间后调用close方法,close方法会设定data的closed为true,visible监听到closed变为true后就变为false,所以根据v-show这个指令,组件的display样式属性就会变成none。最后Vue官方内置的过渡组件transition会调用一系列钩子函数,我们直接在afterLeave这个钩子上销毁组件和清除dom。看到这里,可能有同学会有疑问了?为啥要这样销毁组件啊,直接用一个prop控制组件显示不就可以了吗?其实对于一般的组件我们是没必要弄成这样,但是我们得思考一下消息弹窗的使用场景啊。一般情况下,消息弹窗都是在完成了某种操作后,弹出来告诉用户操作成功与否,所以我们通常用js控制消息弹窗显示。因此,我们的消息弹窗组件不仅仅是声明式的,更应该是命令式的,直接用js控制显示与否。

很多同学应该都使用过element,element在全局引入的时候会向vue上挂载一个message的方法,一般我们都是this.$message这样来操作的,我的目标也是做成这样子。

import Vue from 'vue'
import Message from './Message.vue'

const messageConstructor = Vue.extend(Message)
let count = 1

const notify = (options = {}) => {
  // 如果Vue运行在服务端,放弃调用
  if (Vue.prototype.$isServer) {
    return
  }
  
  if (typeof options === 'string') {
    options = {
      message: options
    }
  }

  // 创造实例
  const instance = new messageConstructor({
    propsData: options
  })

  // 渲染并挂载到body上
  const vm = instance.$mount()
  document.body.appendChild(vm.$el)
  vm.$el.style.zIndex = count

  // 返回一个主动关闭的方法
  const close = () => {
    vm.closed = true
  }
  return close
}

export default notify

这里我们用了Vue的一个方法,vue.extend,这个方法接受一些配置参数,返回一个可以生产vue实例的构造函数,其实我们平时使用的组件一般都是这些构造函数实例化的对象。代码的逻辑很简单,当notify方法被调用的时候会创造一个message组件的实例对象,同时传入props,最后渲染成dom并挂载到document.body上。最后我们提供一个install方法将notify和message组件全局安装到Vue中:

class Plugin {
  static install (Vue) {
    Vue.prototype.notify = notify
    Vue.mixin({
      components: {
        'oce-message': Message
      }
    })
  }
}
export default Plugin


// 全局注册
import oce-message from 'oce-message'
import Vue from 'vue'

Vue.use(oce-message)

// 使用
<oce-message ... />
this.notify(...)

好了,一个简单的消息弹窗组件就完成了,具体的代码和使用方法在这里oce-message可以找到,欢迎star和提issue。

后续

这个组件有点简陋,其实有一些可以优化的地方:

  1. 样式
  2. 过渡动画
  3. 性能(实例是否可以做成一个单例?)

🤔🤔🤔
我是naecoo,前端打杂工程师,偶尔写写灌水文章...
Github
博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容