页面水印怎样实现?怎样防止被轻易删除?

先理解原理和示例,再练习完整口述与追问。

明水印:用 Canvas 或 SVG 绘制包含用户名、时间的旋转文字,转成图片作为全屏覆盖层的重复背景,设置 pointer-events: none 不影响操作。防删除:MutationObserver 监听水印节点被删除或样式被修改时重新生成。前端水印只能增加删除成本,追溯泄露要依靠服务端生成的图片水印或暗水印。

水印层固定定位、覆盖全屏、z-index 足够高,pointer-events: none 让点击穿透到下面的内容。Canvas 绘制后 toDataURL 得到 Base64 图片,作为 background-image 平铺。MutationObserver 监听 body 的子节点变化(水印被删除)和水印节点的属性变化(样式被改为隐藏),检测到后恢复;对于 Shadow DOM 中的水印,用户更难通过元素面板修改。

用户仍可通过禁用 JS、开发者工具删除观察者、截图后修图等方式去除水印,因此重要文件应由服务端在导出或预览时把水印合成进图片或 PDF;暗水印通过微调像素或频域信息嵌入标识,肉眼不可见但可提取。

js
function createWatermark(text) {
  const canvas = Object.assign(document.createElement('canvas'), { width: 240, height: 160 })
  const ctx = canvas.getContext('2d')
  ctx.rotate(-Math.PI / 9)
  ctx.font = '14px sans-serif'
  ctx.fillStyle = 'rgba(0, 0, 0, 0.12)'
  ctx.fillText(text, 10, 110)
  const style = `position:fixed;inset:0;z-index:9999;pointer-events:none;background:url(${canvas.toDataURL()}) repeat`
  const layer = document.createElement('div')
  layer.style.cssText = style
  document.body.appendChild(layer)
  const observer = new MutationObserver(() => {
    if (!document.body.contains(layer)) document.body.appendChild(layer)
    if (layer.style.cssText !== style) layer.style.cssText = style
  })
  observer.observe(document.body, { childList: true })
  observer.observe(layer, { attributes: true, attributeFilter: ['style', 'class', 'hidden'] })
  return () => { observer.disconnect(); layer.remove() }
}

恢复操作本身也会触发观察者,回调中先比较再修改,避免无限循环。

水印文字应包含用户标识以便追溯,但不要放手机号等完整隐私数据;打印时浏览器可能不打印背景,需要 print-color-adjust: exact。

我会用 Canvas 画带用户名和时间的倾斜文字,转成图片作为一个全屏固定层的平铺背景,pointer-events: none 不影响操作。防删除用 MutationObserver 监听 body 子节点和水印节点的样式变化,被删或被改就恢复,恢复前先比较避免循环。但前端水印只能提高删除成本,真正需要追溯的文件应该由服务端在图片或 PDF 中合成水印,或使用暗水印。