# 实现一个全局挂载插件

一般,我们在写公共组件时都是使用 component 进行局部或者全局注册,这种写法比较固定,如果想通过 Vue 实例直接调用组件,比如,全局消息弹窗、加载框等直接通过$api 调用,就需要把组件挂载到 Vue 实例,我们可以通过插件+Vue.extend()方法来实现,并且通过此方法可以直接把元素挂载到对应的 DOM 元素上。

# 1、插件使用

创建一个插件,我们只需要提供一个 install 方法,该方法接收两个参数,一个是 Vue 构造器,第二个是插件的参数,我们可以通过这两个参数在 Vue 实例上添加全局方法、属性、组件、指令、实例方法。然后通过 Vue.use(MyPlugin)使用,我们定义的全局属性就挂载到 Vue 实例上了。

除了通过插件挂载,我们也可以直接在 main.js 中直接挂载到 Vue 的原型属性上也行。

// plugin.js
const myPlugin = {
  install = function (Vue, options) {
    // 注册全局组件
    Vue.component('xxx', xxx);
    // 添加全局方法或 property
    Vue.name = 'xxx'
    Vue.myGlobalMethod = function () {
      // 逻辑...
    }
    // 添加全局资源
    Vue.directive('my-directive', {
      bind (el, binding, vnode, oldVnode) {
        // 逻辑...
      }
      ...
    })
    // 添加实例方法
    Vue.prototype.$myMethod = function (methodOptions) {
      // 逻辑...
    }
  }
}
export default MyPlugin

# 2、Vue.extend()方法

Vue.extend()方法使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象,可以动态的去修改组件中的值,不像 component 就直接把组件目标定义死了。并且可以通过它把组件挂载到其他 DOM 元素上。

// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')

# 3、创建一个全局 toast 弹窗

创建一个 toast 弹窗模板,通过传入 props 参数来显示内容,弹窗显示后会自动关闭。关闭前把当前 DOM 移除掉。

// toast.vue
<template>
  <transition name="fade" @after-leave="handleAfterLeave">
    <div class="component-toast-cls" v-show="visible" :style="{ zIndex }">{{ content }}</div>
  </transition>
</template>
<script>
export default {
  name: 'Toast',
  props: {
    content: {
      type: String
    },
    zIndex: {
      type: [Number, String],
      default: 999
    },
    duration: {
      type: Number,
      default: 3000
    }
  },
  data() {
    return {
      visible: false,
      timer: null
    }
  },
  mounted() {
    this.visible = true
    this.startTimer()
  },
  methods: {
    startTimer() {
      this.timer = setTimeout(() => {
        this.visible = false
        clearTimeout(this.timer)
        this.timer = null
      }, this.duration)
    }, // 清除DOM
    handleAfterLeave() {
      this.$destroy(true)
      this.$el.parentNode.removeChild(this.$el)
    }
  },
  beforeDestroy() {
    if (this.timer) clearTimeout(this.timer)
  }
}
</script>
<style lang="scss" scoped>
.component-toast-cls {
  position: fixed;
  top: 50%;
  left: 50%;
  font-size: 14px;
  color: #fff;
  line-height: 24px;
  padding: 8px 20px;
  background-color: rgba(0, 0, 0, .8);
  border-radius: 4px;
  transform: translateX(-50%) translateY(-50%);
}

// 添加动画
.fade-enter-active, .fade-leave-active {
  transition: all .3s ease;
}

.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

自定义一个插件,创建一个全局$Toast方法挂载到Vue上,然后引入我们创建的toast模板,通过Vue.extend()方法构建一个组件,把$Toast 方法接收的参数传入来动态替换 toast 模板 props 的值,在传入之前你可以修改 options 的值来实现自己的逻辑。

// 自定义一个插件

import Vue from 'vue'
// 引入模板
import toast from './toast.vue'

const MyPlugin = {
  // 提供install方法,传入Vue构造器
  install(Vue) {
    // 挂载全局属性
    // 注册全局组件
    Vue.component('Toast', toast) // 挂载全局方法
    Vue.prototype.$Toast = function (options) {
      // 服务端渲染退出
      if (Vue.prototype.$isServer) return

      const Toast = Vue.extend(toast) // 构造函数可以接传值,会替换模板中data、props、methods相同的值
      const ToastInstance = new Toast({
        // data: options 替换目标data数据
        propsData: options // 替换props传参
      }) // $mount()不带参数,会把组件在内存中渲染完毕

      const vm = ToastInstance.$mount() // 挂载到body上
      document.body.appendChild(ToastInstance.$el) // 可以通过ToastInstance.$el获取模板DOM元素
      console.log('插件注册成功', options) // 返回一个主动关闭的方法,在调用后通过一个变量保存来执行

      // 渲染后的实例vm
      const close = () => {
        vm.visible = false
      }
      return close
    }
  }
}

export default MyPlugin

在 main.js 中引入该插件

// main.js
import MyPlugin from '@/components/MyPlugin'
Vue.use(MyPlugin)

最后,就可以在 vue 中使用了

this.$Toast({
  content: '弹窗内容'
})

// 手动关闭
const closeCallback = this.$Toast({
  content: '弹窗内容',
  duration: 10000
})
setTimeout(() => {
  closeCallback()
}, 2000)