Vue3:使用服务方式来调用vue组件

4,273 阅读2分钟

在用 elementUI、antdUI 等 ui 框架的时候,都会用到 Message 全局的提示方法,而平时常用的 Vue 组件都是写成 tag 来使用:

<el-button type="primary">主要按钮</el-button>
<el-button type="success">成功按钮</el-button>

而 Message 等组件的使用方式则不同,不用引入 Message 的 vue 组件,用 js 就能直接调用的服务方式:

<template>
    <el-button type="text" @click="open">点击打开 Message Box</el-button>
</template>

<script>
    export default {
        methods: {
            open() {
                this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
                    confirmButtonText: '确定',
                    cancelButtonText: '取消',
                    type: 'warning' 
                }).then(() => {
                    this.$message({ type: 'success', message: '删除成功!' });
                }).catch(() => {
                    this.$message({ type: 'info', message: '已取消删除' });
                });
            }
        }
    }
</script>

那在 Vue3 中,如何实现使用 JS 调用 Vue 组件呢?

正常写法

先写一个正常的组件:

<template>
  <div class="test-comp">
    <div>title: {{title}}</div>
    <div>{{msg}}</div>
    <button @click="onClose">关闭</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    title: String,
    onClose: Function
  },
  setup() {
    const msg = '这是一个测试组件自己的msg'

    return {
      msg
    }
  },
})
</script>

然后在父级组件内调用:

<template>
  <div>js 调用 vue 组件</div>
  <button @click="isshow = true">调用测试组件</button>
  <TestComp
    v-if="isshow"
    title="这是传进来的title"
    :onClose="onClose"
  />
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'
import TestComp from '../components/testComp.vue'

export default defineComponent({
  components: {
    TestComp
  },
  setup() {
    const isshow = ref(false)
    const onClose = () => isshow.value = false

    return {
      isshow,
      onClose
    }
  },
})
</script>

然后看下效果:

GIF.gif

改写JS调用

接下来将上面组件改写成用 js 调用的方式:

在改写之前,还需要了解 vue3 的几个全局 API:

  • defineComponent: 将函数或者对象转换成对象返回
  • h: 将组件对象转换成 vnode
  • render:将 vnode 挂载到节点或者移除节点

另建一个 ts / js 文件,然后将 TestComp 组件进行转换:

首先实现一个 vnode 挂载节点函数和一个移除节点函数:

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

// 生成一个唯一的key
const COMPONENT_CONTAINER_SYMBOL = Symbol('component_container')

/**
 * 创建组件实例对象
 * 返回的实例和调用 getCurrentComponent() 返回的一致
 * @param {*} Component
 */
function createComponent(Component: any, props: any, children: any) {
  // 创建vnode
  const vnode = h(Component, { ...props }, children)
  // 创建组件容器
  const container = document.createElement('div')
  // @ts-ignore 将组件容器挂载到vnode上,方便后续移除
  vnode[COMPONENT_CONTAINER_SYMBOL] = container
  // 将vnode渲染到组件容器内, 在 vue2 的版本中,父级元素是可以传 null 的,但是 vue3 不支持
  render(vnode, container)
  // 返回组件实例
  return vnode.component
}

/**
 * 销毁组件实例对象
 * @param {*} ComponnetInstance 通过createComponent方法得到的组件实例对象
 */
export function unmountComponent(ComponnetInstance: any) {
  // 移除组件节点,render函数的第一个传null,表示为移除动作,会执行unmount方法
  render(null, ComponnetInstance.vnode[COMPONENT_CONTAINER_SYMBOL])
}

引入组件进行挂载节点

import TestComp from './testComp.vue'

// ......
// ......

// 当前场景下是可以省略这一步转换,但是,就类型而言,defineComponent 返回的值有一个合成类型的构造函数
const componentConstructor = defineComponent(TestComp)

// 创建一个变量接收创建的组件实例
let instance: any;

// 创建节点
const showTestComponent = (options: any) => {
  // 创建组件实例对象
  instance = createComponent(componentConstructor, options, null)
  // 添加到body
  document.body.appendChild(instance.vnode.el)
}

// options为组件的props
export const testComp = function (options: any) {
  const close = options.onClose
  // 重新封装close,添加移除元素操作
  options.onClose = () => {
    close && close.call()
    unmountComponent(instance)
  }
  showTestComponent(options)
}

最后在父组件调用

<template>
  <div>js 调用 vue 组件</div>
  <button @click="show">调用测试组件</button>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import { testComp } from '../components/testComp'

export default defineComponent({
  setup() {
    const show = () => {
      testComp({
        title: '这是传进来的title',
        onClose() {
          console.log('close')
        }
      })
    }

    return {
      show
    }
  },
})
</script>

ps: 效果图:

GIF.gif

到这里,使用 js 调用 vue 组件的改写就完成了。这种方式的组件,在某些场景是非常方便的,可以根据特有场景进行组件封装,然后一键调用。