水印功能在企业后台系统里越来越常见,主要用途是防泄密溯源:页面上叠加显示当前登录用户的姓名和工号,一旦有人截图外传,就能通过水印快速定位到泄露源头。用原生js实现页面水印并不复杂,核心思路就是把带有用户信息的文字或图案,以半透明、平铺的方式覆盖在整个页面上,同时阻止用户通过开发者工具随意删除。下面详细介绍三种主流实现方案。

方案一:canvas绘制水印并平铺为背景
这是最经典的实现方式。原理是先用canvas画出一个小的水印单元,包含文字内容、旋转角度和透明度,然后调用toDataURL把它转成base64图片,最后设置成某个覆盖层的背景图并开启repeat平铺。这种方式的优点是兼容性极好,几乎所有支持canvas的浏览器都能运行,而且生成的是位图,渲染性能稳定。
具体实现时需要注意几个细节:水印单元的宽高要根据文字长度计算,避免相邻水印互相重叠;旋转角度一般取-20度到-30度,视觉上更像传统水印;透明度建议控制在0.05到0.15之间,太淡起不到警示作用,太浓会干扰正常阅读。下面是完整的工具函数:
function generateWatermark(options) {
var opts = options || {};
var text = opts.text || '水印文字';
var fontSize = opts.fontSize || 16;
var rotate = opts.rotate || -22;
var opacity = opts.opacity || 0.1;
// 创建canvas并计算水印单元尺寸
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
ctx.font = fontSize + 'px Arial';
var textWidth = ctx.measureText(text).width;
canvas.width = textWidth + 80;
canvas.height = fontSize + 60;
// 重新设置字体(canvas重设宽高后状态会丢失)
ctx.font = fontSize + 'px Arial';
ctx.fillStyle = 'rgba(0, 0, 0, ' + opacity + ')';
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(rotate * Math.PI / 180);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, 0, 0);
// 转成base64并应用到覆盖层
var dataUrl = canvas.toDataURL('image/png');
var layer = document.createElement('div');
layer.id = 'watermark-layer';
layer.style.cssText =
'position:fixed;top:0;left:0;width:100%;height:100%;' +
'pointer-events:none;z-index:99999;' +
'background-image:url(' + dataUrl + ');' +
'background-repeat:repeat;';
document.body.appendChild(layer);
return layer;
}
// 调用示例
generateWatermark({ text: '张三 10086', fontSize: 16 });代码里的pointer-events:none非常关键,它保证水印层不会拦截鼠标事件,用户在页面上点击、滚动都不受影响。另外z-index要设置得足够大,确保水印始终显示在其他元素之上,否则被弹窗盖住就失去意义了。
方案二:SVG背景图生成高清水印
canvas生成的是位图,在高分屏上可能出现模糊。SVG是矢量格式,无论怎么缩放都保持清晰,而且SVG可以直接内联文本,生成的base64字符串比同尺寸的PNG更小。它的实现思路和方案一类似,只是把绘制环节换成了拼装SVG字符串,再通过encodeURIComponent编码后塞进url()里。
SVG方案还有一个隐藏优势:文字内容是动态拼接的,不需要等canvas异步绘制,逻辑上是同步的,代码更直观。缺点是低版本浏览器(比如IE11以下)对SVG背景的支持不完善,如果项目需要兼容老浏览器就要谨慎选择。示例如下:
function createSvgWatermark(text) {
var svgText =
'<svg xmlns="http://www.w3.org/2000/svg" width="200" height="120">' +
'<text x="50%" y="50%" ' +
'font-size="16" fill="rgba(0,0,0,0.12)" ' +
'text-anchor="middle" ' +
'transform="rotate(-22, 100, 60)">' + text + '</text>' +
'</svg>';
var bgUrl = 'url("data:image/svg+xml,' +
encodeURIComponent(svgText) + '")';
var div = document.createElement('div');
div.style.cssText =
'position:fixed;inset:0;pointer-events:none;z-index:99999;' +
'background-image:' + bgUrl + ';background-repeat:repeat;';
document.body.appendChild(div);
return div;
}
createSvgWatermark('李四 9527');拼接SVG字符串时标签里的尖括号必须转义处理,尤其是通过encodeURIComponent编码的场景,双引号和井号等特殊字符也要一并编码,否则背景图会解析失败导致水印不显示。建议在retina屏幕较多的移动端项目中优先考虑这种方案。
方案三:DOM重复元素加MutationObserver防篡改
前两种方案都是背景图平铺,有个共同的软肋:用户打开控制台,选中水印层按Delete键就能删掉。如果业务对防篡改有要求,就需要引入MutationObserver监听DOM变化,一旦发现水印被删除或样式被修改,立即重建水印。这种方式也常和方案一、二组合使用,即背景图负责展示,观察器负责防御。
MutationObserver是浏览器原生的DOM变化监听API,性能开销比旧式的MutationEvent小得多。它的用法是创建观察器实例,指定回调函数,然后调用observe注册要监听的目标节点和事件类型。当目标节点的子节点被增删、属性被修改时,回调就会被触发:
var watermarkLayer = generateWatermark({ text: '王五 12345' });
var observer = new MutationObserver(function () {
// 水印被删或被改时先停止监听,避免无限循环
observer.disconnect();
var existing = document.getElementById('watermark-layer');
if (existing) {
existing.parentNode.removeChild(existing);
}
// 重新生成水印并恢复监听
watermarkLayer = generateWatermark({ text: '王五 12345' });
observe();
});
function observe() {
observer.observe(document.body, {
childList: true, // 监听子节点增删
subtree: true, // 监听整个子树
attributes: true, // 监听属性修改
attributeFilter: ['style', 'class']
});
}
observe();回调里先调用disconnect再重建水印,这一步容易被忽略。如果不这样做,重建水印本身也会触发observer,造成无限递归,页面会直接卡死。另外要注意,前端防御终究是君子协定,用户禁用js或者直接截图后裁掉水印,前端无能为力,真正的强防护需要服务端配合,比如在返回的图片资源上做盲水印。
三种方案对比与选型建议
三个方案各有侧重,可以简单对比一下:canvas方案兼容性最好、生态成熟,适合大多数后台系统;SVG方案清晰度高、体积小,适合高分屏和移动端;MutationObserver方案本身不负责绘制,主要价值在于防篡改,通常与前两者搭配。实际项目中建议canvas或SVG负责渲染,再用observer做一层保护,这是目前开源水印库(比如watermark-js、@watermark-plus等)普遍采用的架构。
还有几个工程化细节值得注意。第一,水印内容不要只写姓名,最好拼上工号和时间戳,溯源信息才完整。第二,如果页面存在多个路由切换,要确保水印层挂在body下而不是组件内部,否则路由跳转时水印会跟着销毁。第三,对于表格、详情页等局部区域加水印的场景,可以把覆盖层的position:fixed改成absolute并限定在容器内,实现原理完全一致。
最后提醒一点,水印只是防泄密的辅助手段,它能提高泄露的溯源效率,但不能从根源上阻止泄露。涉及高敏感数据的系统,还应该配合权限控制、操作日志、页面禁右键、动态水印轮换等措施,形成完整的防护链路。