Vue3自定义插件:封装一个全局消息提示插件,所有组件都能直接用

一、开头唠唠:啥是插件?跟我有啥关系?

你用 Vue 的时候,肯定装过别人的插件。比如:

  • Vue Routerapp.use(router) 之后,所有组件里都能用 this.$router 跳转页面。

  • Piniaapp.use(pinia) 之后,所有组件里都能用 useXxxStore() 管理状态。

  • Element Plusapp.use(ElementPlus) 之后,所有组件里都能直接用 <el-button> 这些组件。

这些插件都有一个共同特点:只需要在 main.js 里写一行 app.use(插件),整个项目都能用它的功能了。

你有没有想过,自己也能写一个这样的插件?

比如封装一个全局消息提示 功能,像 Element Plus 的 ElMessage.success('操作成功') 那样,任何组件里都能调用,弹出一个小提示,几秒后自动消失。

今天咱们就从零开始,手把手把这个插件写出来。搞懂了这个,你以后就能给自己的项目封装各种好用的工具了。


二、插件到底是什么?为什么长那样?

Vue 的插件本质上就是一个对象 ,这个对象里必须有一个 install 方法。

javascript

复制代码
// 一个最简单的插件长这样
const MyPlugin = {
  install(app) {
    // app 是 Vue 的应用实例,就是 createApp 返回的那个东西
    // 你可以在这里做任何事:注册全局组件、注入全局方法、添加全局指令等
  }
}

当你写 app.use(MyPlugin) 时,Vue 会自动调用 MyPlugin.install(app),把应用实例传进去。然后你在 install 里做的一切操作,都会应用到整个项目。

打个比方: app 就像一个大房子,install 方法就是你在这个房子里装东西------装个门铃(全局方法)、装个书架(全局组件)、贴个标语(全局指令)。装完之后,每个房间(组件)都能用。


三、第一步:先写一个最简单的插件雏形

咱们先从最简单的开始:写一个插件,挂载一个全局方法 $toast,调用它就用 alert 弹窗。

3.1 创建插件文件 plugins/toast.js

javascript

复制代码
// plugins/toast.js

// 定义一个插件对象
const ToastPlugin = {
  // install 是插件的入口,Vue 在使用这个插件时会自动调用它
  // app 参数是 createApp 返回的应用实例
  install(app) {
    // app.config.globalProperties 是 Vue3 里专门用来挂载全局属性的地方
    // 挂上去之后,任何组件的 this 都能访问到
    // 比如这里挂了一个 $toast 方法,组件里就能用 this.$toast() 调用
    app.config.globalProperties.$toast = (message) => {
      // 先简单点,直接用浏览器自带的 alert 弹窗
      alert(message)
    }
  }
}

// 导出插件,让 main.js 能引入
export default ToastPlugin

逐行解释:

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

  • app.config.globalProperties:这是 Vue3 专门提供的"全局属性挂载点"。挂在上面的东西,所有组件都能通过 this 访问到。

  • $toast:前面加个 $ 是 Vue 的约定,表示这是全局方法,和组件自己的方法区分开。

3.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(ToastPlugin)

app.mount('#app')

3.3 在组件里调用

vue

复制代码
<!-- 任意组件 -->
<template>
  <div>
    <button @click="showMsg">点我弹提示</button>
  </div>
</template>

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

// getCurrentInstance 返回当前组件实例
// instance.proxy 就相当于选项式 API 里的 this
const instance = getCurrentInstance()

function showMsg() {
  // 通过 proxy 调用全局方法
  instance.proxy.$toast('你好,这是插件弹出的消息!')
}
</script>

注意:<script setup> 里没有 this,所以要用 getCurrentInstance().proxy 来访问全局属性。稍后我们会封装一个更方便的调用方式。


四、第二步:让提示漂亮起来,写一个消息组件

alert 弹窗太丑了,而且不能自定义样式。我们的目标是:调用一个方法,页面上出现一个漂亮的提示条,带颜色、带图标,几秒后自动消失。

4.1 先写消息提示组件 ToastMessage.vue

vue

复制代码
<!-- plugins/ToastMessage.vue -->
<template>
  <!-- 
    Transition 是 Vue 内置的过渡组件,包在它里面的元素出现和消失时会自动加动画
    name="toast" 表示动画类名以 toast 开头
  -->
  <Transition name="toast">
    <!-- visible 控制显示隐藏,false 时元素会被移除 -->
    <div v-if="visible" class="toast-message" :class="type">
      <!-- 显示消息文字 -->
      {{ message }}
    </div>
  </Transition>
</template>

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

// 接收父组件传来的参数
const props = defineProps({
  // 消息文本,必传
  message: {
    type: String,
    required: true
  },
  // 消息类型,不同类名对应不同背景色
  type: {
    type: String,
    default: 'info'  // 默认是普通信息
  },
  // 显示时长,单位毫秒,默认 3000(3秒)
  duration: {
    type: Number,
    default: 3000
  }
})

// 控制组件显示/隐藏的开关
const visible = ref(false)

// 组件挂载后,立即显示,并在指定时间后隐藏
onMounted(() => {
  // 先把开关打开,触发进入动画
  visible.value = true

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

<style scoped>
.toast-message {
  /* 固定定位,悬浮在页面顶部中央 */
  position: fixed;
  top: 20px;
  left: 50%;
  /* translateX(-50%) 是水平居中的技巧 */
  transform: translateX(-50%);
  padding: 10px 24px;
  border-radius: 4px;
  color: white;
  font-size: 14px;
  /* z-index 设大一点,保证在最上层 */
  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>

逐行解释:

  • <Transition name="toast">:Vue 内置组件,包裹需要动画的元素。name="toast" 意味着我们要用 .toast-enter-from.toast-leave-to 这类类名写动画。

  • v-if="visible":控制组件的显示和隐藏。visiblefalsetrue 时触发进入动画,从 truefalse 时触发离开动画。

  • onMounted:组件挂载后立即执行。先把 visible 设为 true(显示),再设一个定时器,到时间后设为 false(隐藏)。

  • CSS 里的 position: fixed:固定定位,让提示条始终悬浮在页面上方,不随滚动条移动。


五、第三步:升级插件,让它动态创建组件

现在有了 ToastMessage 组件,但怎么在 JS 里动态创建它并挂到页面上呢?

思路:

  1. 调用 $toast('消息') 时,用 createApp 创建一个新的 Vue 应用,只包含 ToastMessage 组件。

  2. 动态创建一个 <div> 作为挂载点,插到 <body> 里。

  3. 把组件挂上去,页面就出现提示了。

  4. 等动画播完,卸载应用、移除 div,清理干净。

5.1 重写 plugins/toast.js

javascript

复制代码
// plugins/toast.js
import { createApp } from 'vue'
import ToastMessage from './ToastMessage.vue'

const ToastPlugin = {
  install(app) {
    // 在全局挂载 $toast 方法
    app.config.globalProperties.$toast = (options) => {
      // 如果传的是字符串,转成对象格式
      // 比如 this.$toast('操作成功') 转成 { message: '操作成功' }
      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 上
      //    此时页面上就会出现提示条了
      toastApp.mount(mountPoint)

      // 4. 在动画结束后清理:卸载应用 + 移除 div
      //    多等 500ms 是为了让离开动画播完
      setTimeout(() => {
        toastApp.unmount()              // 卸载 Vue 应用
        document.body.removeChild(mountPoint) // 从 body 中移除 div
      }, duration + 500)
    }
  }
}

export default ToastPlugin

逐行解释:

  • createApp(ToastMessage, { message, type, duration }):用 createApp 创建一个新的 Vue 应用实例,只渲染 ToastMessage 这一个组件。第二个参数是传给组件的 props

  • document.createElement('div'):纯原生 JS,动态创建一个 <div> 元素。

  • document.body.appendChild(mountPoint):把这个 div 加到 <body> 的最末尾。

  • toastApp.mount(mountPoint):把 Vue 应用挂载到这个 div 上。挂载后,ToastMessage 组件就会被渲染到页面上。

  • duration + 500 毫秒后清理一切。多出的 500ms 是为了等 CSS 离开动画播完。


六、第四步:封装一个更方便的调用工具 useToast

每次都用 instance.proxy.$toast() 太啰嗦了。我们封装一个工具函数,用起来更爽。

6.1 创建 utils/toast.js

javascript

复制代码
// utils/toast.js
import { getCurrentInstance } from 'vue'

// 导出一个函数,组件里直接调用就能拿到各种提示方法
export function useToast() {
  // 获取当前组件实例
  const instance = getCurrentInstance()
  // proxy 就是组件实例的代理对象,等同于选项式 API 里的 this
  const proxy = instance.proxy

  // 返回一个对象,包含各种快捷方法
  return {
    // 通用方法,可以传字符串或完整配置对象
    toast: (options) => proxy.$toast(options),
    
    // 快捷方法:成功提示
    success: (msg) => proxy.$toast({ message: msg, type: 'success' }),
    
    // 快捷方法:错误提示
    error: (msg) => proxy.$toast({ message: msg, type: 'error' }),
    
    // 快捷方法:警告提示
    warning: (msg) => proxy.$toast({ message: msg, type: 'warning' }),
    
    // 快捷方法:普通信息提示
    info: (msg) => proxy.$toast({ message: msg, type: 'info' })
  }
}

6.2 在组件中使用

vue

复制代码
<template>
  <div>
    <button @click="showSuccess">成功提示</button>
    <button @click="showError">失败提示</button>
    <button @click="showWarning">警告提示</button>
    <button @click="showInfo">普通提示</button>
  </div>
</template>

<script setup>
// 引入工具函数
import { useToast } from '@/utils/toast.js'

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

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

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

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

function showInfo() {
  info('这是一条普通消息')
}
</script>

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


七、第五步:防止重复提示叠加(单例模式)

现在有个小问题:如果快速点两个按钮,页面上会同时出现两个提示条,堆叠在一起。

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

7.1 升级 plugins/toast.js,加入单例逻辑

javascript

复制代码
// plugins/toast.js(完整版)
import { createApp } from 'vue'
import ToastMessage from './ToastMessage.vue'

// 用三个模块级变量存当前正在显示的 toast 信息
// 模块级变量在整个应用生命周期内只存在一份,所有调用共享
let currentApp = null        // 当前正在运行的应用实例
let currentTimer = null      // 当前的销毁定时器
let currentMountPoint = null // 当前的挂载点 DOM

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

解释:

  • currentAppcurrentTimercurrentMountPoint 三个变量定义在模块最外层,不属于任何函数。这意味着它们在整个应用生命周期内只有一份,所有 $toast 调用共享。

  • 每次调用 $toast 时,先检查有没有旧的实例在运行。如果有,清除定时器、卸载应用、移除 DOM,确保旧的被彻底清理。

  • 然后再创建新的实例。这样就能保证同一时间只有一个提示条。


八、完整项目结构

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')

九、这个套路还能怎么用?

学会了动态创建组件的套路,你可以封装更多有用的插件:

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

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

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

核心思路都一样:

  1. createApp 动态创建组件实例。

  2. 动态创建挂载点插到 body 里。

  3. 处理完自动清理。


十、总结

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

步骤 干什么 关键代码
定义插件 写一个带 install 的对象 const plugin = { install(app) {...} }
挂载全局方法 让所有组件都能调用 app.config.globalProperties.$toast = ...
动态创建组件 在 JS 里渲染 Vue 组件 createApp(组件, props).mount(div)
单例模式 同一时间只显示一个 模块级变量存当前实例,新的销毁旧的
过渡动画 提示条淡入淡出 <Transition> + CSS 类名

学会了自定义插件,你就从"用工具的人"变成了"造工具的人"。以后团队里谁需要什么通用功能,你甩一个插件过去,大家都方便。

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

相关推荐
PixelBai1 小时前
JSON过滤使用教程:从入门到精通
javascript·chrome·json
用户83134859306981 小时前
Cesium实现实时联动鹰眼缩略图
vue.js·cesium
橘子星1 小时前
树与二叉树:从概念到 JavaScript 实现
前端·javascript·面试
小小高不懂写代码1 小时前
Transformer与注意力机制
前端·人工智能
AiClaw1 小时前
AIClaw 的 Skills 机制:先注入索引,再按需读取完整说明
前端
YHHLAI1 小时前
HTML5 Canvas 从入门到实战:画布绘图 · 帧动画 · 小游戏 · 数据可视化
前端·信息可视化·html5
前端 贾公子1 小时前
npx skills:AI Agent Skill 的 npm,50+ 工具统一的 Skill 管理工具
前端
触底反弹1 小时前
面试官问"Ajax原理",我从XHR讲到async/await,他直接懵了!
前端·面试·架构
Chelsea05221 小时前
PC浏览器在线调试 Android 浏览器教程-chrome://inspect/#devices
android·前端·chrome