@yuwenlin/vue-watermark-plus
@yuwenlin/vue-watermark-plus
Vue2 / Vue3 通用水印指令 · 支持多行文本 · 高清适配 · 防误删 · 自适应重排 · 全页水印 · 工厂 API
✨ 特性
- 🎯 Vue2 / Vue3 双支持 —— 一份代码同时兼容,无需选择版本
- 🖼️ Canvas 高清适配 —— 自动跟随
devicePixelRatio,Retina 屏不模糊 - 📝 多行文本 —— 支持
\n分行或字符串数组,行距可调 - 🔒 防误删 ——
MutationObserver监听水印节点,删除/改样式立即恢复 - 📐 自适应重排 ——
ResizeObserver监听宿主尺寸,窗口/侧边栏变化实时重绘 - ⚡ 每元素独立实例 ——
WeakMap私有存储,多水印互不干扰、自动 GC - 🏭 工厂 API ——
createWatermark(defaults)创建独立配置的指令实例 - 🌐 全页水印 ——
mount(options)命令式挂载position: fixed覆盖视口 - 🎨 完整可配 —— rotate / opacity / zIndex / gap / offset / dpr 全部支持
- 🖌️ 双渲染器 —— Canvas(位图,默认)或 SVG(矢量,任意缩放清晰、体积更小)一键切换
- 📏 自适应尺寸 ——
sizeMode: 'auto'按文字实际长度计算 tile 大小(含旋转包围盒),长文本不再被裁切 - 🧱 错开排列 ——
stagger: true砖墙模式,偶数行偏移半格,视觉更自然 - 🔌 TypeScript —— 完整类型声明,编辑器智能提示
- 🪶 零依赖 —— 纯原生 API 实现,压缩后 <10KB
📦 安装
npm install @yuwenlin/vue-watermark-plus
# or
yarn add @yuwenlin/vue-watermark-plus
# or
pnpm add @yuwenlin/vue-watermark-plus
兼容性:Chrome 47+ / Firefox 51+ / Safari 8+ / iOS 8+ / Android 4+(无 ResizeObserver 的环境自动降级,其他功能正常)。库内已避免 Object.assign、Number.isFinite 等 ES2015 运行时方法(改用内置 ES5 实现),老环境无需宿主额外 polyfill。
不支持 IE:Vue3 依赖原生 ES2015
Proxy(无法 polyfill),无法在 IE 运行;且本库构建目标不含 IE、也未在 IE 下测试,故不支持 IE 环境。
🚀 快速开始
Vue 3
// main.js
import { createApp } from 'vue'
import Watermark from '@yuwenlin/vue-watermark-plus'
import App from './App.vue'
const app = createApp(App)
app.use(Watermark) // 注册为 v-watermark
app.mount('#app')
Vue 2
// main.js
import Vue from 'vue'
import Watermark from '@yuwenlin/vue-watermark-plus'
import App from './App.vue'
Vue.use(Watermark) // 注册为 v-watermark
new Vue({ render: h => h(App) }).$mount('#app')
使用
<!-- 最简:字符串 -->
<div v-watermark="'机密文件'">内容</div>
<!-- 完整配置 -->
<div v-watermark="{
text: '机密文件',
color: 'rgba(255, 0, 0, 0.15)',
size: 16,
rotate: -30,
zIndex: 2000
}">
内容
</div>
<!-- 多行文本(数组) -->
<div v-watermark="{ text: ['张三', 'ID: 10086', '2026-09-17'] }">内容</div>
<!-- 多行文本(\n) -->
<div v-watermark="{ text: '张三\nID: 10086' }">内容</div>
<!-- 动态内容(函数) -->
<div v-watermark="() => currentUser.name">内容</div>
<!-- 响应式 -->
<div v-watermark="watermarkConfig">内容</div>
📚 API
指令用法(模板)
v-watermark 支持四种绑定值形态:
| 形态 | 示例 | 说明 |
|---|---|---|
| 字符串 | v-watermark="'机密'" |
等价于 { text: '机密' } |
| 字符串数组 | v-watermark="['a', 'b']" |
等价于 { text: ['a', 'b'] },多行显示 |
| 对象 | v-watermark="{ text: '机密', size: 16 }" |
完整配置 |
| 函数 | v-watermark="() => user.name" |
每次渲染时调用,支持动态计算 |
命令式 API(全页水印)
import { mount, unmount } from '@yuwenlin/vue-watermark-plus'
// 用户登录后挂载全站水印
const dispose = mount({
text: ['张三', 'ID: 10086'],
color: 'rgba(0, 0, 0, 0.08)',
size: 16,
zIndex: 9999
})
// 用户退出登录时移除
dispose()
// 或
unmount()
mount() 会在 <body> 末尾追加一个 position: fixed 的宿主元素,覆盖整个视口,pointer-events: none 不影响交互。
工厂 API(多实例独立配置)
import { createWatermark } from '@yuwenlin/vue-watermark-plus'
// 创建一个专用于"用户身份"的指令
const UserMark = createWatermark({
name: 'user-mark', // 注册名(v-user-mark)
text: '张三',
color: 'rgba(0, 0, 0, 0.08)',
size: 14,
rotate: -22
})
// 创建一个专用于"机密文档"的指令
const DocMark = createWatermark({
name: 'doc-mark', // 注册名(v-doc-mark)
text: '机密',
color: 'rgba(255, 0, 0, 0.15)',
size: 20,
fontWeight: 'bold'
})
app.use(UserMark) // 或 Vue.use(UserMark)
app.use(DocMark)
<div v-user-mark>用户区域</div>
<div v-doc-mark>文档区域</div>
两个指令的默认配置互不影响。
手动销毁
import Watermark from '@yuwenlin/vue-watermark-plus'
// 打印前临时移除某个元素的水印
Watermark.destroy(document.querySelector('.print-area'))
⚙️ 配置项
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
text |
string | string[] | () => string | string[] |
'' |
水印内容;字符串支持 \n 分行,数组自动多行 |
color |
string |
'rgba(0, 0, 0, 0.08)' |
文字颜色,CSS color 语法 |
size |
number |
14 |
字号(px) |
fontFamily |
string |
'Arial, "Microsoft YaHei", sans-serif' |
字体 |
fontWeight |
number | 'normal' | 'bold' | ... |
'normal' |
字重 |
lineHeight |
number |
1.4 |
行高倍数(相对 size),仅多行时生效 |
rotate |
number |
-20 |
旋转角度(度,负数逆时针) |
opacity |
number |
1 |
整体不透明度(0~1) |
zIndex |
number |
1900 |
层级;全页模式默认 9999 |
width |
number | null |
null |
单个 tile 内容宽度(CSS 像素);null 表示按宿主 / colNum 自动计算 |
height |
number | null |
null |
单个 tile 内容高度 |
colNum |
number |
5 |
网格列数(当 width 未指定时用于计算 tile 宽) |
rowNum |
number |
4 |
网格行数 |
gap |
[number, number] |
[0, 0] |
tile 之间的间距 [水平, 垂直] |
offset |
[number, number] |
[0, 0] |
网格起点偏移 [x, y] |
dpr |
number |
0 |
设备像素比;0 表示自动跟随 window.devicePixelRatio(仅 canvas 渲染器需要) |
renderer |
'canvas' | 'svg' |
'canvas' |
渲染器:canvas 位图(兼容性好、支持 DPR)/ svg 矢量(任意缩放清晰、dataURL 更小) |
sizeMode |
'fixed' | 'auto' |
'fixed' |
尺寸模式:fixed 按 colNum/rowNum 或 width/height 固定;auto 按文字实际尺寸自适应(含旋转包围盒),长文本不被裁切。auto 模式下 colNum/rowNum/width/height 被忽略,gap 仍生效 |
stagger |
boolean |
false |
错开排列(砖墙模式):true 时偶数行相对奇数行水平偏移半个 tile 宽度,视觉更自然 |
monitor |
boolean |
true |
是否启用 MutationObserver(防误删/防篡改) |
responsive |
boolean |
true |
是否启用 ResizeObserver(自适应重排) |
水印节点(以及全页模式的 host)只保留行内
style,不设置 class、data-*、aria-*等属性,也不使用 Shadow DOM。节点是宿主的直接子元素,身份由库内部引用维护。
🎨 使用示例
1. 基础水印
<div v-watermark="'内部资料'">...</div>
2. 用户身份水印(含时间戳)
<template>
<div v-watermark="watermarkText">敏感数据</div>
</template>
<script>
export default {
computed: {
watermarkText() {
return [
this.$store.state.user.name,
`ID: ${this.$store.state.user.id}`,
new Date().toLocaleDateString()
]
}
}
}
</script>
3. 登录后全站水印
// store/user.js
import { mount, unmount } from '@yuwenlin/vue-watermark-plus'
let dispose = null
export const actions = {
async login({ commit }, credentials) {
const user = await api.login(credentials)
commit('SET_USER', user)
// 挂载全站水印
dispose = mount({
text: [user.name, `工号:${user.id}`],
color: 'rgba(0, 0, 0, 0.06)',
size: 14,
zIndex: 9999
})
},
logout({ commit }) {
commit('CLEAR_USER')
if (dispose) { dispose(); dispose = null }
}
}
4. 打印前隐藏水印
import Watermark from '@yuwenlin/vue-watermark-plus'
let saved = null
window.addEventListener('beforeprint', () => {
saved = document.querySelector('.print-area div[style*="background-image"]')
// 更稳妥:打印前用 Watermark.destroy(el) 或 unmount()
if (saved) saved.style.display = 'none'
})
window.addEventListener('afterprint', () => {
if (saved) saved.style.display = ''
})
5. 切换到 SVG 渲染器(矢量、更清晰、体积更小)
<div v-watermark="{ text: '机密文件', renderer: 'svg', size: 16 }">...</div>
或全局默认使用 SVG:
import { createWatermark } from '@yuwenlin/vue-watermark-plus'
const Watermark = createWatermark({ renderer: 'svg' })
app.use(Watermark)
Canvas vs SVG 对比:
| 维度 | Canvas(默认) | SVG |
|---|---|---|
| 输出格式 | data:image/png;base64,... |
data:image/svg+xml;charset=utf-8,... |
| 缩放清晰度 | 依赖 DPR 适配,超出 dpr 后会模糊 | 矢量,任意缩放都清晰 |
| dataURL 体积 | 几 KB ~ 几十 KB(base64 PNG) | 几百字节(URL 编码的 XML) |
| 浏览器兼容 | 全部现代浏览器 | 全部现代浏览器(SVG 作为 background-image dataURL) |
| 复杂效果 | 支持滤镜、渐变、图案、图片合成 | 支持但语法更繁琐 |
| 中文字体 | 依赖系统字体 | 依赖系统字体(相同) |
| 内存占用 | 每元素一个 canvas | 无 canvas,更省 |
| 适用场景 | 需要复杂绘制、图片水印 | 纯文本水印、追求极致清晰与体积 |
建议:纯文本水印优先用 SVG;需要图片水印或复杂 Canvas 效果时用 Canvas。
6. 自定义渲染(图片水印)
覆盖 getDataUrl 扩展点即可:
import Watermark from '@yuwenlin/vue-watermark-plus'
Watermark.getDataUrl = function (options, canvas) {
const { width, height, dpr = 1 } = options
const cvs = canvas || document.createElement('canvas')
const ratio = dpr || window.devicePixelRatio || 1
cvs.width = width * ratio
cvs.height = height * ratio
const ctx = cvs.getContext('2d')
ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
ctx.clearRect(0, 0, width, height)
// 画一个红色圆形 logo
ctx.fillStyle = 'rgba(255, 0, 0, 0.2)'
ctx.beginPath()
ctx.arc(width / 2, height / 2, 30, 0, Math.PI * 2)
ctx.fill()
return cvs.toDataURL('image/png')
}
7. 自适应尺寸(长文本不被裁切)
默认 sizeMode: 'fixed' 下,tile 大小由 colNum/rowNum(或显式 width/height)决定,文字过长会超出 tile 被裁切或与相邻水印重叠。改用 sizeMode: 'auto' 后,tile 大小按文字实际渲染尺寸(含旋转后的包围盒)自动计算:
<!-- 长文本自动撑开 tile,不会被裁切 -->
<div v-watermark="{
text: '这是一段很长的水印文字用于演示自适应尺寸',
sizeMode: 'auto',
size: 14,
gap: [40, 40]
}">
内容
</div>
<!-- 多行 + 自适应 -->
<div v-watermark="{
text: ['张三', 'zhangsan@example.com', '2026-09-18 10:30:00'],
sizeMode: 'auto',
gap: [60, 60]
}">
内容
</div>
auto 模式下的行为:
colNum/rowNum/width/height被忽略(tile 大小完全由文字决定)gap仍然生效(控制水印之间的间距)- 内部用
ctx.measureText()精确测量每行宽度,取最大值 - 旋转后的轴对齐包围盒公式:
W = w·|cosθ| + h·|sinθ|,H = w·|sinθ| + h·|cosθ| - 文字块高度 =
(行数 - 1) × 行高 + 字号 ResizeObserver仍然工作,但 auto 模式下 tile 大小不随宿主变化(只取决于文字),所以 resize 时不会重绘(尺寸没变)
fixed vs auto 选择建议:
| 场景 | 推荐 |
|---|---|
| 短文本("机密"、用户名)、需要均匀网格分布 | fixed(默认)—— 通过 colNum/rowNum 精确控制密度 |
| 长文本、多行、内容长度不确定 | auto —— 保证完整显示,用 gap 控制密度 |
| 全页水印(mount) | 两者均可;auto 更适合长用户信息 |
8. 错开排列(砖墙模式)
默认网格排列下,每行水印垂直对齐,规律感强。设置 stagger: true 后,偶数行相对奇数行水平偏移半个 tile 宽度,形成砖墙式错开,视觉更自然、更难被规律性抹除:
<div v-watermark="{
text: '机密',
stagger: true,
size: 16,
colNum: 4,
rowNum: 4
}">
内容
</div>
排列对比:
stagger: false(网格) stagger: true(砖墙)
机密 机密 机密 机密 机密 机密
机密 机密 机密 机密 机密 机密
机密 机密 机密 机密 机密 机密
实现原理:
- tile 高度翻倍(
tileHeight = height × 2),一个 tile 内含 2 行 - 第 1 行文字画在 tile 上半部中央
- 第 2 行文字画在 tile 左右边缘各半个,
background-repeat平铺后相邻 tile 的半个文字拼成完整一个,正好偏移半格 - Canvas 与 SVG 渲染器都支持;与
sizeMode: 'auto'、gap、rotate均可组合
🔌 TypeScript
包内附带完整类型声明(index.d.ts),无需额外安装 @types/*:
import Watermark, { createWatermark, mount, unmount } from '@yuwenlin/vue-watermark-plus'
import type { WatermarkOptions, WatermarkDirective } from '@yuwenlin/vue-watermark-plus'
const options: WatermarkOptions = {
text: '机密',
size: 16,
rotate: -22
}
const directive: WatermarkDirective = createWatermark(options)
🧩 扩展点
指令对象暴露 3 个可覆盖的方法:
| 方法 | 用途 |
|---|---|
defaultOptions() |
返回当前实例的默认配置副本;覆盖可修改默认值 |
getDataUrl(options, canvas) |
生成水印图片 base64 URL;覆盖可实现图片水印 / SVG 水印 |
destroy(el) |
手动销毁某个元素的水印 |
// 覆盖默认配置
Watermark.defaultOptions = () => ({
...Watermark.defaultOptions(), // ⚠️ 递归风险,请自行缓存原实现
color: 'rgba(0, 0, 0, 0.05)'
})
// 更安全的方式:用 createWatermark 创建独立实例
const MyMark = createWatermark({ color: 'rgba(0, 0, 0, 0.05)' })
🌍 SSR / Nuxt
指令的生命周期钩子只在客户端触发,SSR 场景无需特殊处理。
Nuxt 2 (Vue 2):
// plugins/watermark.client.js
import Vue from 'vue'
import Watermark from '@yuwenlin/vue-watermark-plus'
Vue.use(Watermark)
Nuxt 3 (Vue 3):
// plugins/watermark.client.ts
import Watermark from '@yuwenlin/vue-watermark-plus'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(Watermark)
})
命令式 mount() 需在 onMounted 或客户端环境中调用。
⚠️ 能力边界(重要)
本库是客户端水印,只能防误操作和常见 CSS 手段,不能防有心人。
已做的 CSS 防护:
- 水印节点不使用 Shadow DOM,仅为宿主 light DOM 子节点,且只带行内 style(无 class / data 属性)
- 关键行内样式带
!important - 事件驱动核对(无定时器):
<head>内 style/link 增删改、或页面重新可见时,对活跃水印做一次计算样式检查;被 stylesheet 隐藏时写回行内 style
仍可能被更激进的 CSS 打断(例如对宿主直接子节点使用
display:none !important且优先级压过行内!important的场景较少见;把宿主本身隐藏则无法防御)。不追求 Shadow DOM 隔离,以换取更简单的 DOM 结构和更低的运行时成本。
以下场景仍然无法防御:
- 用户在 DevTools 里禁用 JavaScript,或直接删掉宿主 / 整页隐藏宿主(
display:none作用在宿主上) - 用户用 DevTools 手动删除或覆盖水印节点
- 用户通过
Watermark.getDataUrl = () => ''覆盖扩展点 - 用户使用截图工具 + 图像处理后去除水印
- 用户直接读取网络返回的原始数据
真正的敏感数据保护需要:
- 服务端渲染带用户身份的图片(后端出图)
- 数字水印(LSB / DCT 频域嵌入)
- DRM 版权保护
- 数据脱敏 + 权限控制
本库适用于:内部系统防误截图、文档标注、品牌展示、操作留痕提示 等场景。
🔄 从 v1.x / 早期 v2 迁移
| 旧用法 | 现状 |
|---|---|
v-watermark="{ text: '机密' }" |
✅ 仍兼容 |
Watermark.name / Watermark.install |
✅ 仍兼容 |
className / .vue-watermark |
⚠️ 已移除 |
data-v-watermark / aria-hidden |
⚠️ 已移除(节点只有行内 style) |
默认字号 12 |
⚠️ 已改为 14 |
默认字体 Arial |
⚠️ 已改为 Arial, "Microsoft YaHei", sans-serif |
样式/查询迁移:水印节点不再暴露 class 或 data 属性,业务侧请不要再依赖选择器定位水印节点。
- 打印前隐藏:用
Watermark.destroy(el)或全页模式unmount() - 调整观感:改
opacity/zIndex/color等配置,而不是写 CSS 覆盖
恢复旧版视觉默认值(不再包含 className):
const Watermark = createWatermark({
size: 12,
fontFamily: 'Arial'
})
🐛 常见问题
Q1: 水印被 Element Plus / Ant Design 弹层遮挡?
调高 zIndex:
<div v-watermark="{ text: '机密', zIndex: 3000 }">
或使用全页水印 mount({ zIndex: 9999 })。
Q2: 水印在 Retina 屏上模糊?
v2 已自动处理 DPR。如仍有问题,可强制指定更高 DPR:
<div v-watermark="{ text: '机密', dpr: 3 }">
Q3: 水印阻止了元素交互?
不会。水印容器已设置 pointer-events: none。如果发现无法点击,检查是否有其他 CSS 覆盖了此属性。
Q4: 组件销毁后水印残留?
不会。unbind / unmounted 会自动清理。如果发现残留,检查是否有代码手动 append 了水印节点。
Q5: 想让某些页面不显示水印?
用条件渲染:
<div v-if="showWatermark" v-watermark="config">...</div>
或在路由守卫中调用 unmount() 卸载全页水印。
Q6: 如何测试?
水印依赖 canvas 与 MutationObserver。Jest 中需 mock:
// jest.setup.js
HTMLCanvasElement.prototype.getContext = () => ({
setTransform: jest.fn(),
clearRect: jest.fn(),
save: jest.fn(),
restore: jest.fn(),
translate: jest.fn(),
rotate: jest.fn(),
fillText: jest.fn(),
font: '', fillStyle: '', textBaseline: '', textAlign: ''
})
HTMLCanvasElement.prototype.toDataURL = () => 'data:image/png;base64,'
🤝 贡献
欢迎提交 Issue 和 PR。
📄 License
MIT © yuwenlin
Related Packages
Vue directive for custom scrollbar that uses native scroll behavior. Lightweight...
This module is a break away from vue-debounce but specifically written for vue2
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 1.0.0 | ^2.6.0 || | MIT |