在前端开发中,网页水印是一个非常常见的需求,主要用于环境标识(如测试环境、生产环境)、版权保护、数据安全等场景。一个合格的网页水印需要满足防删除、自适应窗口、高兼容性等特性,本文将基于 Canvas + DOM 监听的方式,实现一个健壮的网页水印工具,并结合 Vue 项目演示实际使用流程。
一、核心实现思路
网页水印的核心实现逻辑分为以下几步:
- Canvas 生成水印图片:利用 Canvas 绘制水印文字(支持旋转、透明度、字体样式),并转换为 Base64 格式的图片。
- DOM 挂载水印容器:创建一个全屏的 div 容器,将 Canvas 生成的图片作为背景图,挂载到页面根节点(
document.documentElement),确保水印覆盖整个页面。 - 防删除机制:通过定时检测水印元素是否存在,若被删除则重新生成;同时使用随机 ID + 自定义属性双重标记水印元素,提升防删除能力。
- 自适应处理:监听窗口大小变化,当窗口缩放时重新生成水印,保证水印布局不会错位。
- 兼容性处理:添加 try-catch 捕获异常(如 Canvas 的 toDataURL 方法被 CSP 限制),兼容不同浏览器和页面环境。
二、完整代码实现(水印工具类)
我们将水印功能封装为一个独立的工具类 watermark.js,便于在项目中复用:
/**
* 网页水印工具类
* 功能:生成防删除、自适应、高兼容性的网页水印
* 作者:前端无涯https://blog.csdn.net/weixin_44439817?type=blog
* 日期:2025-12-15
*/
let watermark = {}
/**
* 生成水印元素并挂载到页面
* @param {string} str - 水印显示的文字内容
* @returns {string|null} - 水印元素的ID(失败返回null)
*/
let setWatermark = (str) => {
// 生成随机ID,防止被反调试脚本通过固定ID清除
let id = 'wm-' + Math.random().toString(36).substr(2, 9)
try {
// 1. 创建Canvas元素,绘制水印文字
let can = document.createElement('canvas')
// 设置Canvas尺寸(水印的单个重复单元大小)
can.width = 200
can.height = 150
let cans = can.getContext('2d')
// 文字旋转(-20度,斜向显示)
cans.rotate(-20 * Math.PI / 180)
// 文字样式:字体、透明度、颜色
cans.font = '20px Verdana'
cans.fillStyle = 'rgba(100, 100, 100, 0.2)'
// 文字对齐方式
cans.textAlign = 'left'
cans.textBaseline = 'middle'
// 绘制文字(位置:Canvas宽/8,高/2)
cans.fillText(str, can.width / 8, can.height / 2)
// 2. 创建水印容器div
let div = document.createElement('div')
div.id = id
// 添加自定义属性,用于双重检测水印元素
div.setAttribute('data-watermark', 'true')
// 3. 设置水印容器样式(全屏覆盖、层级最高、不可点击)
div.style.pointerEvents = 'none' // 防止水印遮挡页面元素的点击事件
div.style.top = '0px'
div.style.left = '0px'
div.style.position = 'fixed' // 固定定位,跟随窗口滚动
div.style.zIndex = '2147483647' // 最高层级,确保覆盖所有元素
div.style.width = '100vw' // 视口宽度
div.style.height = '100vh' // 视口高度
div.style.display = 'block !important' // 强制显示
div.style.visibility = 'visible !important' // 强制可见
// 4. 将Canvas图片设置为背景图(兼容CSP限制,添加try-catch)
try {
div.style.background = 'url(' + can.toDataURL('image/png') + ') left top repeat'
} catch (e) {
console.error('Watermark canvas toDataURL error:', e)
}
// 5. 挂载到documentElement(比body更稳定,避免body被样式限制)
document.documentElement.appendChild(div)
console.log('Watermark element appended:', id)
return id
} catch (err) {
console.error('Watermark creation failed:', err)
return null
}
}
/**
* 初始化水印(对外暴露的方法,仅允许有效调用一次)
* @param {string} str - 水印文字内容
*/
watermark.set = (str) => {
// 延迟执行(1秒),确保DOM完全加载后再生成水印
setTimeout(() => {
let currentId = setWatermark(str)
// 定时检测(每2秒),防止水印被删除或隐藏
setInterval(() => {
// 检测方式1:通过ID判断水印元素是否存在
if (currentId && document.getElementById(currentId) === null) {
console.warn('Watermark removed, recreating...')
currentId = setWatermark(str)
}
// 检测方式2:通过自定义属性双重验证(防止ID被篡改)
const wm = document.querySelector('div[data-watermark="true"]')
if (!wm) {
currentId = setWatermark(str)
}
}, 2000)
// 窗口大小变化时,重新生成水印(自适应)
window.onresize = () => {
// 仅当水印不存在时重建,避免频繁操作导致闪烁
if (currentId && document.getElementById(currentId) === null) {
setWatermark(str)
}
}
}, 1000) // 延迟1秒,确保页面DOM渲染完成
}
export default watermark
三、Vue 项目中使用水印工具
以 Vue 2/3 项目为例,我们在根组件 App.vue 中引入并使用水印工具,实现全局水印效果:
<template>
<div id="app">
<!-- 路由视图 -->
<router-view />
</div>
</template>
<script>
// 引入水印工具类
import watermark from "@/utils/watermark"
export default {
name: "App",
components: { ThemePicker },
// 挂载完成后初始化水印
mounted() {
// 设置水印内容:这里为“测试环境”,可根据环境动态传入(如生产环境、开发环境)
watermark.set("测试环境-前端无涯")
}
}
</script>
<style scoped>
/* 示例样式:隐藏主题选择器,可根据项目情况删除 */
#app .theme-picker {
display: none;
}
</style>
四、关键优化点解析
本文的水印方案相比传统实现,做了以下关键优化,提升了健壮性和兼容性:
1. 防删除机制升级
- 随机 ID:水印元素的 ID 使用
wm-+ 随机字符串生成,避免被攻击者通过固定 ID 直接删除。 - 双重检测:定时检测时,既通过 ID 检查,又通过自定义属性
data-watermark="true"检查,即使 ID 被篡改,也能检测到水印被删除。 - 定时重建:每 2 秒检测一次,若水印被删除则立即重新生成,让攻击者的删除操作失效。
2. 兼容性提升
- 挂载到
document.documentElement:相比挂载到body,documentElement(html 标签)更稳定,不会因body的样式(如overflow: hidden、position: relative)导致水印显示异常。 - try-catch 捕获异常:包裹 Canvas 的
toDataURL方法,防止因页面 CSP(内容安全策略)限制导致水印生成失败。 - 延迟执行:初始化水印时延迟 1 秒执行,确保页面 DOM 完全加载后再生成水印,避免因 DOM 未加载完成导致挂载失败。
3. 用户体验优化
pointer-events: none:水印容器设置该样式,不会遮挡页面元素的点击、hover 等交互事件,不影响用户操作。- 固定定位 + 视口尺寸:使用
position: fixed+100vw/100vh,确保水印覆盖整个视口,且跟随窗口滚动。 - resize 自适应:监听窗口大小变化,仅在水印不存在时重建,避免频繁操作导致水印闪烁。
4. 样式强制生效
水印容器设置 display: block !important 和 visibility: visible !important,防止被攻击者通过样式隐藏水印。
五、常见问题与解决方案
在实际使用中,可能会遇到以下问题,可参考对应解决方案:
1. 水印不显示
- 检查 Canvas 绘制是否正常:可在控制台打印
can.toDataURL('image/png'),查看是否生成有效的 Base64 字符串。 - 检查水印容器的 z-index:确保 z-index 足够高(本文使用 2147483647,是浏览器允许的最大安全值),未被其他元素覆盖。
- 检查页面 CSP 策略:若控制台报错
toDataURL is not allowed,需在服务端配置 CSP 策略,允许data:协议。
2. 水印被遮挡
- 检查水印容器的
position:确保为fixed,而非absolute(absolute 会受父元素定位影响)。 - 检查页面元素的 z-index:若某些元素的 z-index 超过 2147483647,可适当提高水印的 z-index(但不建议超过浏览器最大值),或调整页面元素的 z-index。
3. 窗口缩放后水印错位
- 本文的方案已监听
window.onresize事件,在窗口缩放时重新生成水印,若仍出现错位,可将resize事件的处理逻辑改为立即重建水印(而非仅在不存在时重建):window.onresize = () => { currentId = setWatermark(str) // 直接重建 }
六、扩展功能(可选)
根据实际需求,可对水印工具进行扩展:
- 自定义水印样式:将字体、颜色、透明度、旋转角度等参数作为配置项传入,支持自定义水印样式。
- 多行水印文字:修改 Canvas 绘制逻辑,支持多行文字水印。
- 动态修改水印内容:添加
watermark.update(str)方法,支持运行时动态修改水印文字。 - 图片水印:在 Canvas 中绘制图片,实现图片 + 文字组合水印。
七、总结
本文实现的网页水印方案,通过 Canvas 生成水印图片,结合 DOM 监听和定时检测机制,实现了防删除、自适应、高兼容性的核心需求。该方案已在 Vue 项目中验证,可直接复用至 React、Angular 等前端框架,也可用于纯 HTML/JS 项目。
在实际项目中,可根据环境(如开发、测试、生产)动态传入水印内容,进一步提升水印的实用性。同时,建议结合后端接口,获取用户信息(如用户名、工号)作为水印内容,实现更精细化的版权保护。