纯原生 DOM 版本 toast.js(不需要 MyToast.vue 组件)
/**
* toast 消息提示(纯DOM版本,不依赖SFC组件)
* @param {string} msg 提示文字
* @param {'success'|'error'|'warning'|'info'} [type='info']
* @param {number} [duration=2500]
*/
export function toast(msg, type = 'info', duration = 2500) {
// 1. 创建dom元素
const wrap = document.createElement('div')
wrap.className = 'toast-wrap'
const box = document.createElement('div')
box.className = `toast-box ${type}`
box.innerText = msg
wrap.appendChild(box)
document.body.appendChild(wrap)
// 2. 定时移除dom
const timer = setTimeout(() => {
wrap.remove()
clearTimeout(timer)
}, duration)
}
// 快捷别名
toast.success = (msg, d) => toast(msg, 'success', d)
toast.error = (msg, d) => toast(msg, 'error', d)
toast.warning = (msg, d) => toast(msg, 'warning', d)
toast.info = (msg, d) => toast(msg, 'info', d)
样式,放到全局 css(main.css)
.toast-wrap {
position: fixed;
top: 40px;
left: 0;
right: 0;
display: flex;
justify-content: center;
z-index: 9999;
}
.toast-box {
padding: 10px 20px;
border-radius: 6px;
color: #fff;
font-size: 14px;
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
}
.toast-box.success { background-color: #67c23a; }
.toast-box.error { background-color: #f56c6c; }
.toast-box.warning { background-color: #e6a23c; }
.toast-box.info { background-color: #909399; }
页面直接调用:
import { toast } from '@/utils/toast'
toast.success('保存成功')
✅ 效果和之前组件版一模一样,DOM 直接 append 到 body,没有 Vue 组件参与。
两种方案对比:原生 DOM vs Vue 组件 (h+render+Teleport)
方案 1:直接操作 body DOM(上面这个)
✅优点:
- 最简单,没有 Vue 组件开销,不需要
.vue文件,不需要h/render/Teleport。 - 理解门槛低,就是原生 JS DOM API。
❌缺点:
- 没有响应式:如果 toast 内容要动态变化、倒计时、状态切换,要手动操作
innerText、className。 - 样式不能用
scoped,只能全局 css。 - 没有 Vue 生命周期:关闭就是
wrap.remove(),没有onUnmounted,定时器要自己手动清理,很容易内存泄漏。 - 复杂能力很难写:过渡动画、关闭按钮、队列多条消息、图标,全部要手写 DOM 操作,代码会越来越乱。
- 和 Vue 生态割裂:不能用
v‑if、v‑bind等模板语法。
适合:简单 toast,只展示一段文字,不需要复杂交互。
方案 2:MyToast.vue + h + render + Teleport(上一套方案)
✅优点:
- 完整使用 Vue 能力:模板、scoped 样式、watch、props 校验、生命周期。
- 声明式开发,不用手动操作 DOM。
- 想加动画、图标、复杂布局,直接改 vue 组件即可。
❌缺点:
- 概念多:VNode、h、render、Teleport,学习成本高。
- 有 Vue 组件实例的微小开销。
适合:企业级组件,后续会迭代功能,Element‑Plus ElMessage 就是这套思路。
关键问题:直接操作 body 有什么坑?
- 内存泄漏风险
const timer = setTimeout(()=>wrap.remove(),2500)
// 如果页面卸载的时候toast还没消失,定时器还在跑,dom已经移除,定时器依旧存在。
原生 DOM 版本,没有组件生命周期,页面销毁时你需要自己清除定时器;而 Vue 组件版本,组件卸载会自动清理 watch 里面的 timer。
- DOM 堆积 每次调用
toast()都会document.body.appendChild(wrap),每调用一次新增一个 DOM 节点;简单版本多条 toast 会堆叠,这个本身没问题,但要记得remove()。 - 样式污染 不能用
scoped,class 名字要起独特,避免和页面其他 class 冲突。
什么时候选哪个?
- 快速写个简单提示、小 demo:直接操作 body DOM 完全够用。
- 项目长期维护、后续要加动画、图标、复杂逻辑:用 SFC 组件 + h + render + Teleport。
补充:Teleport 本质底层也是操作
body,只不过DOM 移动操作由 Vue 帮你完成,你不用手写 appendChild。
通俗总结
- 直接操作 body:你手写
document.createElement+body.appendChild,自己管理 DOM。 - Teleport:Vue 内部帮你做 append/remove,你只写模板。
- h+render:帮你动态实例化 Vue 组件。