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.jsjavascript// 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 clickshowMsg点我弹提示/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.vuevue!-- plugins/ToastMessage.vue -- template !-- Transition 包裹让提示有淡入淡出动画 nametoast 对应下面的 CSS 类名 -- Transition nametoast !-- 只有 visible 为 true 时才显示 -- div v-ifvisible classtoast-message :classtype !-- 显示消息内容 -- {{ 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 }) } }组件中使用vuetemplate div button clickshowSuccess成功提示/button button clickshowError失败提示/button button clickshowWarning警告提示/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现在任何组件里都能这样用vuetemplate div button clickinstance.proxy.$toast.success(成功了)成功/button button clickinstance.proxy.$toast.error(失败了)失败/button /div /template script setup import { getCurrentInstance } from vue const instance getCurrentInstance() /script如果觉得instance.proxy太啰嗦依然可以用useToast工具函数只是现在它更轻量了。六、完整项目结构textsrc/ ├── plugins/ │ ├── toast.js # 插件入口 │ └── ToastMessage.vue # 消息提示组件 ├── utils/ │ └── toast.js # useToast 工具函数可选 ├── main.js # 注册插件 └── App.vue # 使用插件main.js里的配置javascriptimport { 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挂载全局属性和方法。动态创建组件用createAppmount在 JS 里渲染组件。单例模式用模块级变量保证同一时间只有一个实例。过渡动画用Transition让提示条淡入淡出。这个模式不仅限于消息提示你可以用同样的套路封装全局确认弹窗this.$confirm(确定删除吗)全局加载遮罩this.$loading.show()/this.$loading.hide()全局抽屉面板this.$drawer({ title: 设置, component: SettingsForm })学会了自定义插件你就多了一项“造工具”的能力不再只是用别人的东西而是能自己给项目提供基础设施。有问题评论区说我挨个回。下篇咱们聊权限控制把动态路由和按钮权限都搞定