告别alert():聊聊优雅的页面消息提示插件Qmsg

从一个熟悉的弹窗说起

每个写前端的人,大概都经历过这样一幕:表单提交成功,要给用户一个反馈,于是敲下 alert("保存成功")。功能上挑不出毛病,体验上它几乎是"反面教材"的具象化——浏览器原生样式,各个浏览器长得不一样但都不好看;弹出来的瞬间整个页面被锁死,用户除了点确定没有第二条路;提示一旦多了,还得一个一个点掉。

于是下一步通常是找一个 toast 组件。自己从零写一个带动画、带图标、能自动关闭的提示组件,认真做下来几百行起步;从组件库拽一个,前提是项目里本来就有 Vue 或 React 全家桶。可如果手上只是一个纯 HTML 静态页、一个还在跑 jQuery 的老项目,或者一段跑在别人页面里的油猴脚本,为了弹个提示把组件库整个搬进来,怎么算都有点亏。

Qmsg 就是冲着这个缝隙来的:零依赖的原生 JS 消息提示插件,一行 <script> 引入,几行代码就能用上带动画、带图标、位置可调的 toast。

Qmsg 是什么

先交代背景。Qmsg 最初出自开发者"或许吧"(jesseqin)之手,原版是一个 jQuery 插件,发布在 jQuery 插件库上。后来 WhiteSevs 用 TypeScript 把它整个重构了一遍,发布到 npm,也就是今天 GitHub 上的Qmsg。所以严格说它是一份"重构版":功能逻辑延续原版,代码全部用 TS 重写,附带完整的类型定义。

几个基本盘:当前版本 1.7.2,MIT 协议,npm 上已发布 41 个版本,运行时零依赖。体积方面,我把 UMD 压缩版下载下来看了一眼,22,450 字节,22KB 出头——无论引到哪里,都几乎不构成负担。

它提供五种消息类型(infowarningerrorsuccessloading)、九个弹出方位、三十来个配置项,以及一组实例方法。下面挨个说。

图片[1]-告别alert():聊聊优雅的页面消息提示插件Qmsg-风的记忆

几个打动我的设计

默认跑在 Shadow DOM 里

这是我最想单独拿出来讲的一点。Qmsg 默认把消息渲染进 ShadowRoot(配置项 useShadowRoot,默认 true),而不是直接挂在 document.body 下。

好处是双向的样式隔离:宿主页面的 CSS 不会污染你的消息样式,你的消息样式也不会泄漏出去影响页面。对普通项目来说这算锦上添花,但对油猴脚本是实打实的刚需——脚本跑在别人的页面里,对方页面可能有一行 .toast { display: none } 或者更刁钻的选择器,直接挂 body 的方案随时可能翻车,Shadow DOM 把这种风险隔在了门外。

打个比方:普通方案是把提示卡片直接搬进别人家客厅,主人家的装修规则(页面 CSS)随时能管到你头上;Shadow DOM 相当于在客厅里隔出一个独立的小房间,里外的规矩互不干涉。真想调整样式也有口子:style 配置项可以往 ShadowRoot 里追加自定义 CSS,customClass 可以给消息打标记,不需要跟源码较劲。

交互上的小心思

几个细节能看出作者是真的在用这个库:

  • 鼠标悬停会暂停自动关闭的倒计时,移开后再继续(listenEventToPauseAutoClose)。用户还没读完的消息,不会在眼前硬生生消失。
  • maxNums 限制同屏消息数量,默认 5 条。手抖连点十次,页面也不会变成 toast 刷屏现场。
  • 来回切换浏览器标签页时,消息会自动关闭(listenEventToCloseInstance),避免用户回到页面时面对一堆过时提示。

这三个开关默认全开,一行配置都不用写就是这样的行为。

TypeScript 重构带来的体验

整个库是 TS 写的,在编辑器里敲 Qmsg. 会得到完整的方法提示,配置对象也有类型检查。写脚本的时候不用翻文档猜参数名——toast 这种东西,写的时候没人想停下来查文档,这一点比想象中更能省心。

五分钟上手

三种引入方式

模块化项目走 npm:

# 任选其一
pnpm i qmsg
npm i qmsg
import Qmsg from "qmsg";

Qmsg.info("登录成功");

纯 HTML 页面走 CDN,一个 script 标签:

<script src="https://fastly.jsdelivr.net/npm/qmsg@latest/dist/index.umd.min.js"></script>
<script>
  Qmsg.info("这是提示消息");
</script>

油猴脚本则在头部加一行 @require:

// @require https://fastly.jsdelivr.net/npm/qmsg@1.6.0/dist/index.umd.min.js

三条路殊途同归,拿到一个全局的 Qmsg 对象。

五种消息类型

一行一个,语义清晰:

Qmsg.info("常规提示,中性信息");
Qmsg.warning("注意,这里可能有问题");
Qmsg.error("操作失败了");
Qmsg.success("保存成功");
Qmsg.loading("加载中,别急");

实际长这样(在真实页面里引入后截的图,五种类型会从顶部依次堆叠弹出):

图片[2]-告别alert():聊聊优雅的页面消息提示插件Qmsg-风的记忆

九宫格弹出位置

默认从顶部弹出。position 配置项支持九个方位:toplefttoptoprightleftcenterrightbottomleftbottombottomright,大小写不敏感。

Qmsg.info("右下角弹出", { position: "bottomright" });

九个方位各弹一条的效果:

图片[3]-告别alert():聊聊优雅的页面消息提示插件Qmsg-风的记忆

全局配置

如果一批消息要共享同样的行为,用 Qmsg.config() 统一设置,后面的调用就不用每次都传一遍:

Qmsg.config({
  showClose: true,  // 显示右上角关闭图标
  timeout: 5000,    // 自动关闭时长 5s
});

常用配置速查

配置项总共三十来个,完整清单建议直接看Qmsg,这里挑日常用得最多的:

参数类型默认说明
contentstring消息内容
isHTMLbooleanfalse内容是否按 HTML 渲染
timeoutnumber2500自动关闭的等待时长,毫秒
autoClosebooleantrue是否自动关闭
showClosebooleanfalse是否显示右上角关闭图标
showIconbooleantrue是否显示左侧图标
positionstringtop弹出方位,九选一
maxNumsnumber5同屏最多显示的消息条数
animationbooleantrue是否启用弹出动画
onCloseFunctionnull关闭时触发的回调
stylestring追加进 ShadowRoot 的自定义 CSS
customClassstring挂到消息上的自定义类名
useShadowRootbooleantrue是否渲染进 ShadowRoot
zIndexnumber50000消息的 z-index 层级

实例方法与 loading 的正确姿势

每次调用 Qmsg.info() 之类的方法,都会返回一个消息实例。拿住它,就能在消息弹出之后继续操作:

var aMsg = Qmsg.info("这是一条 info 消息");

aMsg.setText("改一下文案");         // 修改文本
aMsg.setHTML("<b>支持 HTML</b>");   // 修改为 HTML
aMsg.close();                       // 关闭,触发 onClose 回调
aMsg.destroy();                     // 销毁,不触发回调

全局还有个 Qmsg.closeAll(),把页面上所有消息一锅端,包括设置了 autoClose: false 的。

这里有个值得单独拎出来的设计:loading 默认是 autoClose: false,不会自动关闭,需要手动 close()。原因不难想——加载时长的决定权在业务手里,库里没法替你猜。常见的写法是配合异步任务:

var loading = Qmsg.loading("正在提交");

// ...做点什么,比如一个 fetch
setTimeout(function () {
  loading.close();
  Qmsg.success("提交完成");
}, 2000);

如果确实想让 loading 自动消失,显式传 autoClose: true 覆盖默认值即可。

油猴脚本作者的宝藏

图片[4]-告别alert():聊聊优雅的页面消息提示插件Qmsg-风的记忆

Qmsg 在油猴圈子里出镜率不低,npm 的关键字里干脆直接写着 TamperMonkeyScriptCatVioletMonkey。原因在前文其实都埋了:UMD 格式天然适配 @require,一行引入;Shadow DOM 隔离,不怕宿主页面的样式干扰。

一个最短可用的脚本骨架长这样:

// ==UserScript==
// @name         我的第一个 Qmsg 脚本
// @match        https://example.com/*
// @require      https://fastly.jsdelivr.net/npm/qmsg@1.6.0/dist/index.umd.min.js
// @grant        none
// ==/UserScript==

(function () {
  Qmsg.config({ timeout: 3000 });

  // 页面加载完,给个提示
  window.addEventListener("load", function () {
    Qmsg.success("脚本已就绪");
  });
})();

比起脚本圈流行的另一种做法——往页面里注入几行 div 和 CSS 自己拼提示——Qmsg 的好处是样式稳定、类型齐全。而且作者维护得勤,写作本文时仓库最近仍有提交,兼容性上的坑大概率不需要自己踩。

顺手看了一眼源码

动笔前我翻了翻它的 src 目录,结构相当清爽,从命名就能对上职责:

模块职责
QmsgCore消息创建与生命周期主流程
QmsgInst / InstHandler / InstStorage实例对象、实例操作与存储管理
QmsgAnimation进出场动画
QmsgCSS样式注入(ShadowRoot 内)
QmsgEvent事件逻辑:悬停暂停、标签页切换关闭等
QmsgIcon内置 SVG 图标
QmsgDefaultConfig默认配置
CompatibleProcessing运行环境兼容处理

构建链是 Rollup,产出 UMD、ESM、CJS 三种格式外加一份 index.d.ts;lint 用 oxlint,浏览器兼容靠 browserslist 和 eslint-plugin-compat 把关。对一个体量不大的工具库来说,这套工程化属于"该有的都有",不是那种写着玩、半年就断更的画风。

什么情况下我会选它

总结一下选型判断。适合的场景:

  • 纯静态页面、没有任何构建链的传统项目,需要一个像样的提示;
  • 油猴、脚本猫这类用户脚本,要在别人的地盘上弹消息;
  • 对依赖体积敏感,或者干脆有"零依赖洁癖"的项目。

不那么适合的场景:项目已经在用 Element Plus、Ant Design 或 shadcn 这类组件生态,提示组件用生态内的就好——组件库的 toast 能跟主题变量联动,还能吃到统一的设计语言,这是 Qmsg 替代不了的。

没有组件库的地方,Qmsg 是把"像样的提示"做到最便宜的方式之一;写油猴脚本的话,它几乎是无脑之选。

© 版权声明
THE END
喜欢就支持一下吧
点赞7赞赏 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容