一、先搞清楚:什么是插件?为什么要写插件?
在说插件之前,你先回忆一个场景。
你在用 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 }))
学会了自定义插件,你就多了一项“造工具”的能力,不再只是用别人的东西,而是能自己给项目提供基础设施。
有问题评论区说,我挨个回。下篇咱们聊权限控制,把动态路由和按钮权限都搞定!



