网页水印的作用不是让内容无法被复制,而是给未授权传播增加心理压力与溯源成本。用jQuery生成水印本身并不复杂,真正麻烦的是用户打开开发者工具后,只需选中水印节点按Delete键就能让整层水印消失。如果要让方案具备基本防护能力,通常需要把Canvas背景水印、DOM水印层和MutationObserver自动恢复机制组合起来。下面会从水印渲染方式讲到删除监听,再补充定时校验与样式加固,形成一套前端可用的实现思路。

基于Canvas生成平铺背景水印
第一层水印建议放在body的背景上,这样做的好处是它不属于某个独立文本节点,用户很难通过开发者工具直接选中删除。生成方式并不复杂:创建canvas节点,设置宽度和高度作为单个水印块的尺寸,在画布上写入文字,再进行一定角度旋转,最后通过toDataURL导出图片,把这张图作为背景图平铺到body即可。
背景图方案适合承载“内部资料”“禁止外传”这类重复性文字,因为平铺后视觉上比较均匀,对正文的干扰也小。颜色建议使用低透明度,例如rgba(0,0,0,0.08)到rgba(0,0,0,0.15)之间,这样既能在截图中留下痕迹,又不会遮挡主要阅读区域。旋转角度通常在负20度到负30度之间,倾斜的水印比水平文字更难被简单裁剪掉。
该方案有一个明显短板:用户虽然不能直接删掉背景,但可以通过开发者工具修改body的background-image样式把水印去掉。因此它只能作为基础层,不能单独承担防删除职责,需要与后面的DOM监听机制配合。
function createWatermarkDataURL(options) {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var text = options.text || '内部资料';
var width = options.width || 240;
var height = options.height || 160;
canvas.width = width;
canvas.height = height;
ctx.font = '16px Arial';
ctx.fillStyle = options.color || 'rgba(0,0,0,0.08)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.translate(width / 2, height / 2);
ctx.rotate(-Math.PI / 6);
ctx.fillText(text, 0, 0);
return canvas.toDataURL('image/png');
}
function applyBackgroundWatermark(text) {
var dataUrl = createWatermarkDataURL({ text: text });
$('body').css({
'background-image': 'url(' + dataUrl + ')',
'background-repeat': 'repeat'
});
}
用jQuery创建独立DOM水印层
第二层可以采用固定定位的全屏div,也就是常说的DOM水印层。它覆盖在内容上方,通过pointer-events设置为none来保证用户的点击、选择、拖动等操作不会受到影响。水印层内部可以重复放置多个水印块,也可以继续使用background-image平铺,但保留独立节点更便于后续监听和恢复。
创建时要注意z-index的设置。水印需要覆盖正文,但不能盖住弹窗、提示框等关键交互组件,因此z-index需要结合项目中的层级规范,通常设置一个相对较高的值,例如9999。如果项目里有全局弹窗使用更高层级,就需要适当调整,避免水印压住操作按钮或遮罩层。
独立DOM水印层最核心的价值在于可识别、可检测、可重建。用户可以删除它,但删除行为会留下DOM变化记录,这正是后面MutationObserver要捕捉的内容。另一个细节是水印层应当统一使用固定id,例如jq-watermark,这样监听逻辑只需判断该id是否存在,不必遍历所有节点。
function createWatermarkLayer(text) {
var $layer = $('#jq-watermark');
if ($layer.length === 0) {
$layer = $('<div id="jq-watermark"></div>');
$('body').append($layer);
}
var block = $('<div class="watermark-block"></div>').text(text || '内部资料');
block.css({
fontSize: '16px',
color: 'rgba(0,0,0,0.1)',
transform: 'rotate(-20deg)',
margin: '20px',
whiteSpace: 'nowrap'
});
$layer.empty();
for (var i = 0; i < 40; i++) {
$layer.append(block.clone());
}
$layer.css({
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 9999,
pointerEvents: 'none',
display: 'block',
overflow: 'hidden'
});
return $layer;
}
上面的循环只是为了直观演示重复节点生成。实际项目如果水印数量过多,可能造成DOM节点膨胀,建议使用CSS背景图平铺代替循环插入几十个节点。比如在单个水印层内设置background-image和background-repeat,这样整个页面只需要一个div,既轻量又便于监听。若业务需要不同行不同列错位排列,还可以在每一行放置一个div,再通过CSS控制间距。
用MutationObserver实现删除后自动恢复
MutationObserver是浏览器原生的DOM变化监听接口,jQuery本身没有对它的封装,但两者可以自然配合。监听body的子节点变化,当发现id为jq-watermark的元素不存在时,就重新创建并插入。这个逻辑要在监听回调中执行,同时要避免重复插入,插入前先检查当前页面是否已经有该节点。
除了childList删除,还要关注attributes变化。开发者工具里不一定要删除节点,也可以把水印层的style改为display:none,或者把opacity改成0。监听属性变化后,可以读取目标节点的样式,如果发现被隐藏,就强制恢复显示。attributeFilter建议包含style、class和id,减少不必要的回调触发。
MutationObserver的监听范围设置成document.body,并开启subtree,是因为水印节点可能不是body的直接子元素,某些框架会把挂载点放在更深的容器中。当然,如果水印层始终由body直接管理,关闭subtree可以减少开销。回调函数中不要做复杂DOM操作,只处理水印恢复,避免因为高频变化造成性能问题。
function protectWatermark(text) {
var targetNode = document.body;
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if ($('#jq-watermark').length === 0) {
$('body').append(createWatermarkLayer(text));
}
if (mutation.type === 'attributes' && $(mutation.target).attr('id') === 'jq-watermark') {
var $target = $(mutation.target);
if ($target.css('display') === 'none' || $target.css('visibility') === 'hidden') {
$target.css({
display: 'block',
visibility: 'visible'
});
}
}
});
});
observer.observe(targetNode, {
childList: true,
attributes: true,
subtree: true,
attributeFilter: ['style', 'class', 'id']
});
}
定时校验与样式加固
MutationObserver能覆盖大多数通过开发者工具修改DOM的行为,但有些用户会直接在控制台执行脚本,或者通过浏览器扩展绕过监听。此时可以再增加一个低频定时任务,每隔两秒或三秒检查水印层是否仍然存在且可见。定时器不必太频繁,否则会给页面带来不必要的计算压力。
样式加固主要针对隐藏水印的场景。恢复样式时,可以使用cssText直接写入带!important的完整样式,提高被再次修改的难度。虽然CSS的!important并不能阻止用户在开发者工具里手动修改,但可以防止页面内置的其他样式覆盖水印层基础样式。恢复时的样式最好包含定位、尺寸、层级、事件穿透和显示方式几个必要项。
需要明确的是,前端水印无法做到绝对安全。只要内容能在浏览器中渲染,用户总可以通过截图、拍照、抓包、修改脚本等方式绕过。前端能做的是让删除成本变高,并为后续溯源保留一定线索。因此在实际项目中,不要把水印当作唯一的内容保护手段,更不要因为加了水印就忽略接口鉴权、权限控制和文件访问策略。
function startWatermarkGuard(text) {
setInterval(function() {
var $layer = $('#jq-watermark');
if ($layer.length === 0) {
$('body').append(createWatermarkLayer(text));
}
var display = $layer.css('display');
var visibility = $layer.css('visibility');
var opacity = parseFloat($layer.css('opacity'));
if (display === 'none' || visibility === 'hidden' || opacity === 0) {
$layer.attr('style', 'position:fixed !important;top:0 !important;left:0 !important;width:100% !important;height:100% !important;z-index:9999 !important;pointer-events:none !important;display:block !important;visibility:visible !important;opacity:1 !important;');
}
}, 2000);
}
综合来看,Canvas背景水印负责提供最基础的视觉覆盖,DOM水印层负责可监听和可恢复的节点,MutationObserver和定时校验共同组成删除后的补救机制。jQuery在这里主要承担DOM操作和样式控制,监听部分依赖原生API。这套方案可以在不大幅增加页面复杂度的前提下,把开发者工具删除水印的成功率降到较低水平。
jQuery水印网页水印防删除MutationObserver修改时间:2026-09-30 05:58:26