手把手写一个Vue3全局api组件

1,234 阅读3分钟

Vue3正式发布也有一段时间了,关于Vue3的文章也是有一大把,组合式API、Typescript、源码等都介绍的非常详尽,但是关于全局api组件这块好像介绍的比较少,那么接下来就分享一下Vue3中如何开发以及使用一个全局api组件

让我们先来回顾一下vue2中的实现方式

以一个alert组件为例

<template>
  <transition name="alert-fade" @after-leave="destroyElement">
    <div class="alert" v-if="show">
      <div class="ht-modal" @click="close"></div>
      <div class="alert-box" :style="'width:'+ width">
        <div class="alert-header">
          <i class="iconfont icon-error alert-close" @click="close"></i>
        </div>
        <div class="alert-body">
          {{ message }}
        </div>
        <div class="alert-footer">
          <slot name="footer">
            <div class="alert-btn" @click="close">确定</div>
          </slot>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>
export default {
  name: 'Alert',
  props: {
    width: {
      type: String,
      required: false,
      default: '30%',
    },
    msg: {
      type: String,
      required: false,
      default: () => 'some message'
    },
    handleClose: {
      type: Function,
      required: false,
      default: null
    }
  },
  data () {
    return {
      show: true,
      message: this.msg
    }
  },
  methods: {
    close($event) {
      this.show = false
      this.$emit('close', $event)
    },
    destroyElement() {
      this.handleClose && this.handleClose()
    }
  },
}
</script>   

文件命名为alert.vue,在同目录下创建alert.js文件

import Main from './alert.vue'

const Alert = {}
Alert.install = Vue => {
  let instance
  let show = false
  let AlertConstructor = Vue.extend(Main)

  const loadAlert = options => {
    if ( show ) return
    let { handleClose } = options
    options.handleClose = () => {
      handleClose && handleClose()
      show = false
    }

    instance = new AlertConstructor({
      propsData: options
    })
    const component = instance.$mount()

    document.body.appendChild(component.$el)
  }


  Vue.prototype.$alert = loadAlert
}

export default Alert

此处的关键是调用Vue上的extend方法来继承alert组件属性,当然也可以直接创建一个全新的Vue示例来实现同样的效果,因为Vue2中是通过实例化Vue构造函数的方式来创建示例,所以把api方法绑定到构造函数的原型对象上即可实现继承

import Alert from 'alert.js'
import Vue from 'vue'

Vue.use(Alert)
new Vue(...).$mount(el)

完成注册后,我们便可在任意位置通过this来调用此方法来渲染组件

    this.$alert({
      width: '50%',
      msg: '这是一个全局api组件',
      handleClose: function() {
        console.log('我被关闭了!!!')
      }
    })

以上就是对Vue2中如何开发及使用一个api组件的简单回顾,那么Vue3中的实现会有什么区别,我们继续往下

Vue3中通过createApp方法返回实例对象,也取消了extend方法,上面我们有提到可以创建一个全新的实例对象的方式来实现,那么我们依葫芦画瓢来实现一下

import { createApp } from 'vue'
import Main from './alert.vue'

const Alert = {}

Alert.install = app => {

  let instance
  let container = null

  const loadAlert = options => {
    if (container) return
    let { handleClose } = options
    options.handleClose = () => {
      handleClose && handleClose()
      document.body.removeChild(container)
      instance.unmount()
      container = null
    }

    container = document.createElement('div')
    instance = createApp(Main, options)
    instance.mount(container)
    document.body.appendChild(container)
  }

  app.config.globalProperties.$alert = loadAlert
}

export default Alert

跟2的实现有几处不太相同,首先实例化操作通过createApp来实现,其次实例上不再有$mount方法,这让我们挂载dom的操作变的复杂了些,但总体的思路是一致的,最后添加方法的方式也有所不同,2是直接添加到原型对象上,3则是添加到实例应用的config对象的全局属性上,后面注册以及使用方式则完全一致

接下来再继续深入一下,Vue3最重要的一个特性就是组合式api,让我们能够非常灵活的去撰写代码,那么其实我们使用vue组件最终要的原因就是要利用其响应式渲染dom的能力来便携和高效的完成渲染操作,那么与之相关的api主要有createVnode、h以及render,我们也可用通过对这些api的使用来实现同样的效果

import { h, createVNode, render } from 'vue'

h与createVnode方法都返回一个vnode对象,两者效果相同,也可以混用

const vnode = h(Main, options)
const vnode = createApp(Main, options)

最后我们需要调用render方法传入vnode以及要挂载的父元素

render(vnode, container)

最后我们的代码可以改成

import { h, createVNode, render } from 'vue'
import Main from './alert.vue'

const Alert = {}

Alert.install = app => {

  let container = null

  const loadAlert = options => {
    if (container) return
    let { handleClose } = options
    options.handleClose = () => {
      handleClose && handleClose()
      document.body.removeChild(container)
      container = null
    }

    container = document.createElement('div')
    const vnode = h(Main, options)
    //const vnode = createVNode(Main, options)  //也可以使用createVnode方法替换h
    render(vnode,container)
    document.body.appendChild(container)
  }

  app.config.globalProperties.$alert = loadAlert
}

export default Alert

其实不难发现,我们就是把createApp的行为变成了创建vnode,然后调用render方法挂载到container上,那么createApp内部也是执行了相同的操作,我们只是把其中我们需要执行的方法提取了出来,这也是组合式api的优势之一,我需要什么就引入对应api,避免代码冗余

最后我们来分析一下2和3的全局调用方式实现,2中通过原型链继承的方式,通过在Vue构造函数的的原型对象上暴露api方法,在应用单例上,供全局调用;3则是通过Proxy代理的方式改写读取行为,this.$alert访问到的实际上是app.config.globalProperties.$alert方法