欢迎光临
我们一直在努力

Vue3自定义插件:手把手教你封装一个全局可用的消息提示插件

一、先搞清楚:什么是插件?为什么要写插件?

在说插件之前,你先回忆一个场景。

你在用 Element Plus 的时候,是不是可以直接写 ElMessage.success('操作成功') 或者 ElMessage.error('操作失败')?这个 ElMessage 不用在每个组件里单独引入,而是整个项目任何地方都能直接调用。

这就是插件的作用:把一些常用的功能封装起来,挂载到 Vue 应用实例上,让所有组件都能直接用。

插件能干什么?总结起来就三类事:

  • 注册全局组件:比如把 <MyButton> 注册成全局组件,不用每次 import。

  • 注入全局方法:比如 this.$toast('消息'),在任何组件里都能调。

  • 添加全局指令:比如 v-permission,一键控制按钮权限。

今天咱们就从零开始,写一个属于自己的全局消息提示插件,功能类似 Element Plus 的 ElMessage,但更轻量,代码你自己完全掌控。


二、先写一个最简单的插件雏形

Vue 的插件本质上是一个对象,里面必须有一个 install 方法。Vue 在调用 app.use(插件) 时,会自动执行这个 install 方法,并把 app 实例传进去。

2.1 插件文件 plugins/toast.js

javascript

// plugins/toast.js

// 定义一个插件对象
const ToastPlugin = {
// install 方法是插件的入口,Vue 会自动调用它
// app 就是 createApp 返回的应用实例
install(app) {
// 在 install 里,我们可以给 app 挂载各种东西

// 1. 注册一个全局组件(这里先演示,后面会详细讲)
// app.component('全局组件名', 组件对象)

// 2. 注入一个全局方法,让所有组件都能用 this.$xxx 调用
// app.config.globalProperties 是 Vue3 里挂载全局属性的地方
app.config.globalProperties.$toast = (message) => {
// 先简单点,直接用 alert 弹窗
alert(message)
}

// 3. 注册全局指令(这里先演示)
// app.directive('指令名', 指令对象)
}
}

// 导出插件
export default ToastPlugin

代码逐行解释:

  • const ToastPlugin = { install(app) { … } }:定义一个对象,里面有个 install 方法。Vue 规定插件必须有这个格式。

  • app.config.globalProperties:这是 Vue3 里专门用来挂载全局属性的对象,挂上去之后,任何组件的 this 都能访问到。

  • this.$toast:我们挂了一个 $toast 方法,组件里就能用 this.$toast('消息') 调用了。

2.2 在 main.js 里使用插件

javascript

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import ToastPlugin from './plugins/toast.js'

const app = createApp(App)

// 使用插件:app.use(插件对象)
app.use(ToastPlugin)

app.mount('#app')

2.3 在组件里调用

vue

<!– 任意组件 –>
<template>
<div>
<button @click="showMsg">点我弹提示</button>
</div>
</template>

<script setup>
// 在 <script setup> 里没有 this,需要用 getCurrentInstance 来获取
import { getCurrentInstance } from 'vue'

// 获取当前组件实例
const instance = getCurrentInstance()

function showMsg() {
// 通过 proxy 访问全局属性
// proxy 就是组件实例的代理对象,相当于选项式 API 里的 this
instance.proxy.$toast('你好,这是插件弹出的消息!')
}
</script>

这里有个问题: 在 <script setup> 里没有 this,所以访问全局属性稍微麻烦一点。后面我们会封装一个更方便的调用方式,不用 getCurrentInstance。


三、让插件更强大:用函数式调用,而不是 alert

用 alert 弹消息太丑了,而且不可控。我们的目标是:调用一个函数,页面上出现一个漂亮的提示框,过几秒自动消失。

思路是这样的:

  • 调用 $toast('消息') 时,动态创建一个 Vue 组件实例。

  • 把这个组件挂载到 <body> 下面。

  • 几秒后自动销毁这个组件。

  • 3.1 先写一个消息提示组件 ToastMessage.vue

    vue

    <!– plugins/ToastMessage.vue –>
    <template>
    <!–
    Transition 包裹,让提示有淡入淡出动画
    name="toast" 对应下面的 CSS 类名
    –>
    <Transition name="toast">
    <!– 只有 visible 为 true 时才显示 –>
    <div v-if="visible" class="toast-message" :class="type">
    <!– 显示消息内容 –>
    {{ message }}
    </div>
    </Transition>
    </template>

    <script setup>
    import { ref, onMounted } from 'vue'

    // 接收外部传入的参数
    // message:要显示的消息文本
    // type:消息类型(success、error、warning、info)
    // duration:显示时长,默认 3000 毫秒
    const props = defineProps({
    message: {
    type: String,
    required: true
    },
    type: {
    type: String,
    default: 'info' // 默认是普通信息样式
    },
    duration: {
    type: Number,
    default: 3000
    }
    })

    // 控制组件是否显示
    const visible = ref(false)

    // 组件挂载后立即显示,并在 duration 毫秒后隐藏
    onMounted(() => {
    // 先让组件显示(触发进入动画)
    visible.value = true

    // 到时间后隐藏(触发离开动画)
    setTimeout(() => {
    visible.value = false
    }, props.duration)
    })
    </script>

    <style scoped>
    .toast-message {
    /* 固定定位在页面顶部居中 */
    position: fixed;
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
    padding: 10px 24px;
    border-radius: 4px;
    color: white;
    font-size: 14px;
    z-index: 9999;
    /* 稍微加个阴影,让它浮起来 */
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
    }

    /* 不同类型的背景色 */
    .info {
    background-color: #909399;
    }
    .success {
    background-color: #67c23a;
    }
    .error {
    background-color: #f56c6c;
    }
    .warning {
    background-color: #e6a23c;
    }

    /* ——– 进入和离开的动画 ——– */

    /* 进入的初始状态:向上偏移 20px,透明 */
    .toast-enter-from {
    opacity: 0;
    transform: translate(-50%, -20px);
    }

    /* 离开的结束状态:同样向上偏移并透明 */
    .toast-leave-to {
    opacity: 0;
    transform: translate(-50%, -20px);
    }

    /* 进入和离开的过程:过渡 0.3 秒 */
    .toast-enter-active,
    .toast-leave-active {
    transition: all 0.3s ease;
    }
    </style>

    代码解释:

    • 这个组件接收 message、type、duration 三个参数。

    • 挂载后立即设置 visible = true,触发进入动画。

    • setTimeout 到时间后设置 visible = false,触发离开动画。

    • 四种类型的消息用了不同的背景色。

    3.2 升级插件文件 plugins/toast.js

    现在要在 install 方法里实现动态创建组件的能力。

    javascript

    // plugins/toast.js
    import { createApp } from 'vue'
    import ToastMessage from './ToastMessage.vue'

    const ToastPlugin = {
    install(app) {
    // 定义一个全局方法 $toast
    // 支持两种调用方式:
    // 1. this.$toast('消息文本')
    // 2. this.$toast({ message: '文本', type: 'success', duration: 2000 })
    app.config.globalProperties.$toast = (options) => {
    // 如果传入的是字符串,就转成对象格式
    if (typeof options === 'string') {
    options = { message: options }
    }

    // 解构参数,设置默认值
    const { message, type = 'info', duration = 3000 } = options

    // 1. 创建一个新的 Vue 应用实例,只包含 ToastMessage 组件
    // 把参数通过 props 传进去
    const toastApp = createApp(ToastMessage, {
    message,
    type,
    duration
    })

    // 2. 创建一个 div 作为挂载点
    const mountPoint = document.createElement('div')
    // 把 div 加到 body 的最后面
    document.body.appendChild(mountPoint)

    // 3. 把组件挂载到这个 div 上
    const instance = toastApp.mount(mountPoint)

    // 4. 在组件销毁后,清理 DOM
    // 监听组件的 unmounted 事件,但 Vue3 组件实例不太好直接监听
    // 所以我们用另一种方式:在 duration 过后手动卸载
    setTimeout(() => {
    // 卸载应用实例
    toastApp.unmount()
    // 从 body 中移除 div
    document.body.removeChild(mountPoint)
    }, duration + 500) // 多加 500ms 是为了等离开动画播完
    }
    }
    }

    export default ToastPlugin

    代码逐行解释:

    • createApp(组件, props):创建一个新的 Vue 应用实例,只渲染 ToastMessage 组件,并把 message、type、duration 作为 props 传进去。

    • document.createElement('div'):动态创建一个 div 作为组件的挂载点。

    • document.body.appendChild(mountPoint):把 div 加到页面里。

    • toastApp.mount(mountPoint):把组件挂载到 div 上,这时候页面上就能看到提示了。

    • 在 duration + 500 毫秒后,调用 toastApp.unmount() 卸载组件,并从 body 移除 div。多出的 500ms 是留给离开动画的。

    3.3 在组件里使用(更优雅的方式)

    每次都写 instance.proxy.$toast(…) 太麻烦了,我们封装一个工具函数:

    javascript

    // utils/toast.js
    import { getCurrentInstance } from 'vue'

    // 导出一个函数,组件里直接 import 使用
    export function useToast() {
    // 获取当前组件实例
    const instance = getCurrentInstance()

    // 返回一个对象,包含各种快捷方法
    return {
    toast: (options) => instance.proxy.$toast(options),
    success: (msg) => instance.proxy.$toast({ message: msg, type: 'success' }),
    error: (msg) => instance.proxy.$toast({ message: msg, type: 'error' }),
    warning: (msg) => instance.proxy.$toast({ message: msg, type: 'warning' }),
    info: (msg) => instance.proxy.$toast({ message: msg, type: 'info' })
    }
    }

    组件中使用:

    vue

    <template>
    <div>
    <button @click="showSuccess">成功提示</button>
    <button @click="showError">失败提示</button>
    <button @click="showWarning">警告提示</button>
    </div>
    </template>

    <script setup>
    import { useToast } from '@/utils/toast.js'

    // 解构出需要的方法
    const { success, error, warning } = useToast()

    function showSuccess() {
    success('操作成功!')
    }

    function showError() {
    error('操作失败,请重试')
    }

    function showWarning() {
    warning('请注意,这是警告信息')
    }
    </script>

    效果: 点击按钮,页面顶部会出现对应颜色的提示条,3 秒后自动消失,还带淡入淡出动画。


    四、给插件加上“单例模式”,防止重复创建

    现在的插件有个小问题:如果用户连续点按钮,页面上会同时出现多个提示框,堆叠在一起不太好看。

    更好的体验是:同一时间只显示一个提示,新的提示会替换旧的。

    javascript

    // plugins/toast.js(升级版,支持单例)
    import { createApp } from 'vue'
    import ToastMessage from './ToastMessage.vue'

    // 用变量存当前正在显示的 toast 相关信息
    let currentApp = null // 当前的应用实例
    let currentTimer = null // 当前的销毁定时器
    let currentMountPoint = null // 当前的挂载点

    const ToastPlugin = {
    install(app) {
    app.config.globalProperties.$toast = (options) => {
    if (typeof options === 'string') {
    options = { message: options }
    }
    const { message, type = 'info', duration = 3000 } = options

    // ——– 关键:先销毁旧的,再创建新的 ——–
    if (currentApp) {
    // 清除旧的销毁定时器
    clearTimeout(currentTimer)
    // 卸载旧的应用实例
    currentApp.unmount()
    // 移除旧的 DOM
    document.body.removeChild(currentMountPoint)
    }

    // 创建新的
    const toastApp = createApp(ToastMessage, { message, type, duration })
    const mountPoint = document.createElement('div')
    document.body.appendChild(mountPoint)
    toastApp.mount(mountPoint)

    // 更新当前状态
    currentApp = toastApp
    currentMountPoint = mountPoint
    currentTimer = setTimeout(() => {
    toastApp.unmount()
    document.body.removeChild(mountPoint)
    // 清理引用
    currentApp = null
    currentMountPoint = null
    currentTimer = null
    }, duration + 500)
    }
    }
    }

    export default ToastPlugin

    解释:

    • currentApp、currentTimer、currentMountPoint 三个变量存在模块作用域里,全局共享。

    • 每次调用 $toast 时,先检查有没有旧的实例,有就干掉(清除定时器、卸载应用、移除 DOM)。

    • 然后再创建新的实例。

    • 这样就能保证同一时间只有一个提示框。


    五、让插件支持更多调用方式

    现在已经很好了,但使用 useToast() 还要在每个组件里 import。能不能更简单?其实我们可以把 useToast 也通过插件注入,变成全局可用的。

    javascript

    // plugins/toast.js(最终版)
    import { createApp } from 'vue'
    import ToastMessage from './ToastMessage.vue'

    let currentApp = null
    let currentTimer = null
    let currentMountPoint = null

    // 把核心逻辑抽成一个函数,方便在插件和 useToast 里复用
    function showToast(options) {
    if (typeof options === 'string') {
    options = { message: options }
    }
    const { message, type = 'info', duration = 3000 } = options

    if (currentApp) {
    clearTimeout(currentTimer)
    currentApp.unmount()
    document.body.removeChild(currentMountPoint)
    }

    const toastApp = createApp(ToastMessage, { message, type, duration })
    const mountPoint = document.createElement('div')
    document.body.appendChild(mountPoint)
    toastApp.mount(mountPoint)

    currentApp = toastApp
    currentMountPoint = mountPoint
    currentTimer = setTimeout(() => {
    toastApp.unmount()
    document.body.removeChild(mountPoint)
    currentApp = null
    currentMountPoint = null
    currentTimer = null
    }, duration + 500)
    }

    const ToastPlugin = {
    install(app) {
    // 挂载全局方法
    app.config.globalProperties.$toast = showToast
    // 同时提供四种快捷方法
    app.config.globalProperties.$toast.success = (msg) => showToast({ message: msg, type: 'success' })
    app.config.globalProperties.$toast.error = (msg) => showToast({ message: msg, type: 'error' })
    app.config.globalProperties.$toast.warning = (msg) => showToast({ message: msg, type: 'warning' })
    app.config.globalProperties.$toast.info = (msg) => showToast({ message: msg, type: 'info' })
    }
    }

    export default ToastPlugin

    现在,任何组件里都能这样用:

    vue

    <template>
    <div>
    <button @click="instance.proxy.$toast.success('成功了')">成功</button>
    <button @click="instance.proxy.$toast.error('失败了')">失败</button>
    </div>
    </template>

    <script setup>
    import { getCurrentInstance } from 'vue'
    const instance = getCurrentInstance()
    </script>

    如果觉得 instance.proxy 太啰嗦,依然可以用 useToast 工具函数,只是现在它更轻量了。


    六、完整项目结构

    text

    src/
    ├── plugins/
    │ ├── toast.js # 插件入口
    │ └── ToastMessage.vue # 消息提示组件
    ├── utils/
    │ └── toast.js # useToast 工具函数(可选)
    ├── main.js # 注册插件
    └── App.vue # 使用插件

    main.js 里的配置:

    javascript

    import { createApp } from 'vue'
    import App from './App.vue'
    import ToastPlugin from './plugins/toast.js'

    const app = createApp(App)
    app.use(ToastPlugin) // 一行搞定
    app.mount('#app')


    七、总结

    今天我们完整地封装了一个消息提示插件,涉及到的知识点:

  • 插件的本质:一个对象,里面有 install 方法。

  • app.config.globalProperties:挂载全局属性和方法。

  • 动态创建组件:用 createApp + mount 在 JS 里渲染组件。

  • 单例模式:用模块级变量保证同一时间只有一个实例。

  • 过渡动画:用 <Transition> 让提示条淡入淡出。

  • 这个模式不仅限于消息提示,你可以用同样的套路封装:

    • 全局确认弹窗(this.$confirm('确定删除吗?'))

    • 全局加载遮罩(this.$loading.show() / this.$loading.hide())

    • 全局抽屉面板(this.$drawer({ title: '设置', component: SettingsForm }))

    学会了自定义插件,你就多了一项“造工具”的能力,不再只是用别人的东西,而是能自己给项目提供基础设施。

    有问题评论区说,我挨个回。下篇咱们聊权限控制,把动态路由和按钮权限都搞定!

    赞(0)
    未经允许不得转载:171主机测评 » Vue3自定义插件:手把手教你封装一个全局可用的消息提示插件
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址