一、开头唠唠:啥是插件?跟我有啥关系?
你用 Vue 的时候,肯定装过别人的插件。比如:
-
Vue Router :
app.use(router)之后,所有组件里都能用this.$router跳转页面。 -
Pinia :
app.use(pinia)之后,所有组件里都能用useXxxStore()管理状态。 -
Element Plus :
app.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":控制组件的显示和隐藏。visible从false变true时触发进入动画,从true变false时触发离开动画。 -
onMounted:组件挂载后立即执行。先把visible设为true(显示),再设一个定时器,到时间后设为false(隐藏)。 -
CSS 里的
position: fixed:固定定位,让提示条始终悬浮在页面上方,不随滚动条移动。
五、第三步:升级插件,让它动态创建组件
现在有了 ToastMessage 组件,但怎么在 JS 里动态创建它并挂到页面上呢?
思路:
-
调用
$toast('消息')时,用createApp创建一个新的 Vue 应用,只包含ToastMessage组件。 -
动态创建一个
<div>作为挂载点,插到<body>里。 -
把组件挂上去,页面就出现提示了。
-
等动画播完,卸载应用、移除 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
解释:
-
currentApp、currentTimer、currentMountPoint三个变量定义在模块最外层,不属于任何函数。这意味着它们在整个应用生命周期内只有一份,所有$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 })
核心思路都一样:
-
用
createApp动态创建组件实例。 -
动态创建挂载点插到 body 里。
-
处理完自动清理。
十、总结
今天我们完整地封装了一个全局消息提示插件,涉及的知识点:
| 步骤 | 干什么 | 关键代码 |
|---|---|---|
| 定义插件 | 写一个带 install 的对象 |
const plugin = { install(app) {...} } |
| 挂载全局方法 | 让所有组件都能调用 | app.config.globalProperties.$toast = ... |
| 动态创建组件 | 在 JS 里渲染 Vue 组件 | createApp(组件, props).mount(div) |
| 单例模式 | 同一时间只显示一个 | 模块级变量存当前实例,新的销毁旧的 |
| 过渡动画 | 提示条淡入淡出 | <Transition> + CSS 类名 |
学会了自定义插件,你就从"用工具的人"变成了"造工具的人"。以后团队里谁需要什么通用功能,你甩一个插件过去,大家都方便。
有问题评论区说,我挨个回。下篇咱们聊权限控制,动态路由、按钮权限一次性搞定!