@yuwenlin/vue-watermark-plus

Vue2 / Vue3 通用水印指令,支持多行文本、高清适配、防误删、自适应重排、全页水印、工厂函数与命令式 API
197
Install
npm install @yuwenlin/vue-watermark-plus
Latest Version:1.0.0
Vue:2 · 3
License:MIT
Last Updated:Sep 20, 2026
Links: npm
Maintainer: yuwenlin

@yuwenlin/vue-watermark-plus

Vue2 / Vue3 通用水印指令 · 支持多行文本 · 高清适配 · 防误删 · 自适应重排 · 全页水印 · 工厂 API

npm version license vue


✨ 特性

  • 🎯 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-watermark-plus

基于 Vue2/Vue3 强力水印插件✨,支持Canvas/SVG双渲染模式🎨,内置防篡改机制📝,采...

6,727 22
vuebar

Vue directive for custom scrollbar that uses native scroll behavior. Lightweight...

279,585 634
vue2-debounce

This module is a break away from vue-debounce but specifically written for vue2

22,361 0
@composy/image-editor-vue

Vue components for @composy/image-editor-core

0

Version History

Version Released Vue License
1.0.0 ^2.6.0 || ^3.0.0 MIT