前端实现水印(Ant Design Vue 及 原生)

最近在项目中用到了 Ant Design Vue 水印,记录一下用法以便后期查询

以下内容转自 《Ant Design Vue - Watermark 水印

基础用法很简单,套用在代码中即可

<a-watermark content="水印内容"></a-watermark>

通过 content 设置 字符串数组 指定多行文字水印内容

<a-watermark :content="['第一行水印内容', '第二行水印内容']"></a-watermark>

通过 image 指定图片地址,需设置 width 和 height,并上传至少两倍的宽高的 logo 图片地址,以防图片被拉伸

<a-watermark image="图片地址"></a-watermark>

也可通过配置自定义参数实现更多水印效果

<a-watermark v-bind="model"></a-watermark>
<script lang="ts" setup>
    import { reactive } from 'vue';
    const model = reactive({
      content: '水印内容',
      font: { // 文字样式
          color: 'string', // 字体颜色
          fontSize: 'number', // 字体大小
          fontWeight: 'normal | light | weight | number', // 字体粗细
          fontFamily: 'string', // 字体类型
          fontStyle: 'none | normal | italic | oblique', // 字体样式
      },
      zIndex: 11,
      rotate: -22,
      gap: [number, number], // 水印之间的间距
      offset: [number, number], // 水印距离容器左上角的偏移量,默认为 gap/2
    }); 
</script>

实践到此,突然对水印的实现原理产生了困惑,作为一个图层遮罩在全屏,却不影响诸如下拉、表单等元素的交互操作

故有了以下答案


本部分内容转自《前端如何实现水印功能

实现方式(css + 定位):

pointer-events: none 是一个 CSS 属性,用于控制元素是否可以成为鼠标事件的目标,设置后用户无法与该元素进行交互,包括点击、悬停、拖动等

具体效果

  • 点击事件:用户无法点击该元素
  • 悬停事件:鼠标悬停在该元素上时,不会触发任何悬停效果(如改变鼠标指针形状、显示提示信息等)
  • 拖动事件:用户无法拖动该元素
  • 其他事件:所有与鼠标相关的事件(如 mousedownmouseupmousemove 等)都不会在该元素上触发

使用场景

pointer-events: none 通常用于以下场景:

  • 禁用交互:当需要临时禁用某个元素的交互功能时,可以使用 pointer-events: none
  • 覆盖层:在某些情况下,可能需要在页面上覆盖一层半透明的元素,但又不希望该层影响底层元素的交互,这时可以使用 pointer-events: none
  • 视觉效果:在某些视觉效果中,可能需要显示一个元素但不希望用户与其交互(如:水印)
<!DOCTYPE html>
<html lang="en">
<head>
    <style>
        .watermark {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 999;
            // 属性用于禁用元素的鼠标事件,这意味着用户无法与该元素进行交互(如点击、悬停等)
            pointer-events: none; /* 防止水印干扰用户交互 */
            background-repeat: repeat;
            background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200"><text x="50%" y="50%" font-family="Arial" font-size="20" fill="rgba(0, 0, 0, 0.1)" text-anchor="middle" dominant-baseline="middle" transform="rotate(-45, 100, 100)">水印文本</text></svg>'); // 采用固定图片水印
        }
    </style>
</head>
<body>
    <div class="watermark"></div>
</body>
</html>

通过 JavaScript 动态生成水印,可以更灵活地控制水印内容和样式

<script>
    function createWatermark(text) {
        const watermarkContainer = document.getElementById('watermarkContainer');
        // 使用 document.createElementNS 创建一个 SVG 元素
        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        // 设置宽高为200x200,并定义 viewBox 以控制缩放比例
        svg.setAttribute('width', '200');
        svg.setAttribute('height', '200');
        svg.setAttribute('viewBox', '0 0 200 200');
        // 创建一个 <text> 元素,设置其位置为 SVG 中心(x='50%',y='50%')
        const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
        textElement.setAttribute('x', '50%');
        textElement.setAttribute('y', '50%');
        // 设置字体、大小、颜色(半透明黑色)、对齐方式(居中)
        textElement.setAttribute('font-family', 'Arial');
        textElement.setAttribute('font-size', '20');
        textElement.setAttribute('fill', 'rgba(0, 0, 0, 0.1)');
        textElement.setAttribute('text-anchor', 'middle');
        textElement.setAttribute('dominant-baseline', 'middle');
        // 使用 transform 属性将文本旋转 -45°,形成倾斜效果
        textElement.setAttribute('transform', 'rotate(-45, 100, 100)');
        textElement.textContent = text;

        svg.appendChild(textElement);
        // 使用XMLSerializer将SVG元素转换为字符串。使用btoa方法将字符串编码为Base64格式
        const svgString = new XMLSerializer().serializeToString(svg);
        // 将 Base64 字符串嵌入到 data:image/svg+xml;base64,... 中,生成图片 URL
        const base64 = btoa(svgString);
        const url = `data:image/svg+xml;base64,${base64}`;
        // 将生成的图片 URL 设置为 watermarkContainer 的 backgroundImage 属性
        watermarkContainer.style.backgroundImage = `url(${url})`;
    }

    createWatermark('水印文本');
</script>

通过 canvas 动态生成水印

function createWatermark(text = 'Watermark') {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');  
  // 设置画布尺寸
  canvas.width = 400;
  canvas.height = 200; 
  // 绘制水印
  ctx.font = '20px Arial';
  ctx.fillStyle = 'rgba(100, 100, 100, 0.2)';
  ctx.rotate(-Math.PI / 6); // 旋转 -30 度
  ctx.fillText(text, 50, 150);  
  // 生成 Base64 背景图
  return canvas.toDataURL('image/png');
}

// 动态创建 style 标签,并将其插入到 head 标签内
const style = document.createElement('style');
style.innerHTML = `
body::after {
  content: '';
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 9999;
  pointer-events: none;
  background: url(${createWatermark()}) repeat;
}
`;
document.head.appendChild(style);

到此,水印的 html 结构依然是 <div class="watermark"></div> ,用户可以很容易的从控制台将它删除

为了解决这个问题还需要追加防删机制

<script>
    // 创建 MutationObserver 实例
    // 初始化一个 DOM 变化观察器,用于监听指定节点的变化
    // 回调函数是所有变更记录的数组
    const observer = new MutationObserver((mutations, observe) => {
      // 遍历所有变更记录
      mutations.forEach((mutation) => {
        // 仅处理有节点被移除的情况
        if (mutation.removedNodes.length) {
          const removed = Array.from(mutation.removedNodes); // 转换节点列表为数组
          if (removed.some((node) => node.classList?.contains("watermark"))) {
            // 重新生成水印并插入 body
            document.body.appendChild(createWatermarkElement());
          }
        }
      });
    });

    // 运行观察器
    observer.observe(document.body, {
        childList: true, // 监听子节点的添加 / 移除
        subtree: true // 监听目标节点所有后代节点的变化
    });
</script>

以上防删机制中用到了 MutationObserver 函数,那么这个函数是个啥呢?

MutationObserver

以下内容截取自《学习HTML5中的MutationObserver

MutationObserver 是一个构造器,接受一个 callback 参数,用来处理节点变化的回调函数,返回两个固定参数 mutationsobserver

  • mutations:节点变化记录列表(sequence
  • observer:构造 MutationObserver 对象

MutationObserver(例:observer) 实例拥有有三个方法,分别如下:

  • observe:设置观察目标,接受两个参数

    • target:观察目标;

    • options:通过对象成员来设置观察选项;

    • childList:设置 true,表示观察目标子节点的变化,比如添加或者删除目标子节点,不包括修改子节点以及子节点后代的变化;

    • attributes:设置 true,表示观察目标属性的改变;

    • characterData:设置 true,表示观察目标数据的改变;

    • subtree:设置为 true,目标以及目标的后代改变都会观察;

    • attributeOldValue:如果属性为true或者省略,则相当于设置为 true,表示需要记录改变前的目标属性值,设置了 attributeOldValue 可以省略attributes 设置;

    • characterDataOldValue:如果 characterData 为 true 或省略,则相当于设置为true,表示需要记录改变之前的目标数据,设置了characterDataOldValue 可以省略 characterData 设置;

    • attributeFilter:如果不是所有的属性改变都需要被观察,并且 attributes 设置为 true 或者被忽略,那么设置一个需要观察的属性本地名称(不需要命名空间)的列表;

      • attributeFilter / attributeOldValue 优先级高于 attributes;
      • characterDataOldValue 优先级高于 characterData;
      • attributes / characterData/childList(或更高级特定项)至少有一项为 true;
      • 特定项存在, 对应选项可以忽略或必须为 true
  • disconnect:阻止观察者观察任何改变

  • takeRecords:清空记录队列并返回里面的内容

上述说明具体示例如下:

var target = document.getElementById('target');

var i = 0;
// MutationObserver 的 callback 回调函数是异步的,只有在全部 DOM 操作完成之后才会调用 callback
var observe = new MutationObserver((mutations, observe) => {
    i++;
    console.log(mutations);
    console.log(i); // 1

    // 回调函数中记录的变动如下
    // type:如果是属性变化,返回 "attributes",如果是一个 CharacterData 节点(Text 节点、Comment 节点)变化,返回 "characterData",节点树变化返回 "childList"
    // target:返回影响改变的节点
    // addedNodes:返回添加的节点列表
    // removedNodes:返回删除的节点列表
    // previousSibling:返回分别添加或删除的节点的上一个兄弟节点,否则返回 null
    // nextSibling:返回分别添加或删除的节点的下一个兄弟节点,否则返回 null
    // attributeName:返回已更改属性的本地名称,否则返回 null
    // attributeNamespace:返回已更改属性的名称空间,否则返回 null
    // oldValue:返回值取决于 type。对于 "attributes",它是更改之前的属性的值。对于 "characterData",它是改变之前节点的数据。对于 "childList",它是 null

    // 其中 type、target 这两个属性不管是哪种观察方式都会有返回值
    // 其他属性返回值与观察方式有关,比如只有当 attributeOldValue 或者 characterDataOldValue 为 true 时 oldValue 才有返回值,只有改变属性时,attributeName 才有返回值等
});

// 只设置 { childList: true} 时,表示观察目标子节点的变化
observe.observe(target, { childList: true }); 
target.childNodes[0].remove(); // 删除节点,可以观察到

// 想要观察到子节点以及后代的变化需设置 {childList: true, subtree: true}
observe.observe(target, { childList: true, subtree: true });
target.childNodes[0].textContent='改变子节点的后代';

// 这个选项主要是用来筛选要观察的属性,比如你只想观察目标 style 属性的变化,这时可以如下设置
observe.observe(target,{ attributeFilter: ['style'], subtree: true});

observe.disconnect(); // 停止观察
observe.takeRecords(); // 清除变动记录
分类: 工作相关

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注