基于 React + Canvas 封装水印组件,让你快点下班

825 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

今天是假期的前一天,我猜各位只想要快点关电脑下班回家吧。

image.png

今天我们就来封装一个水印组件吧。很快的,相信我

image.png

1. 生成普通水印

在 useEffect 中,通过 createWaterDom 函数进行水印组件的绘制。使用 createElement 创建一个 div。

import { useEffect } from "react";

const WaterMark = ({ text = "这里是水印" }) => {
  const _text = text;

  useEffect(() => {
    createWaterDom(document.querySelector('#App'));
  }, []);

  const createWaterDom = (element) => {
    let dom = document.createElement("div");
  };
};

export default WaterMark;

为了防止类名重复,需要使用随机函数生成类名。

 // 动态生产 classname
  const nameGenerator = () => {
    let result = "";
    let length = 2 + Math.ceil(Math.random() * 7);
    let dict = [
      "a",
      "b",
      "c",
      "d",
      "e",
      "f",
      "g",
      "h",
      "i",
      "g",
      "k",
      "l",
      "m",
      "n",
      "o",
      "p",
      "q",
      "r",
      "s",
      "t",
      "u",
      "v",
      "w",
      "x",
      "y",
      "z",
    ];

    for (let i = 0; i < length; i++) {
      result += dict[Math.ceil(Math.random() * 26 - 1)] || 'a';
    }
    return result;
  };

  const elementAttributeName = nameGenerator();

createWaterDom 函数会传入一个节点,用于挂载水印元素。handleAddWaterMark 函数生成水印元素。

const createWaterDom = (element) => {
  let dom = document.createElement("div");
  dom.className = elementAttributeName;
  element.appendChild(dom);
  handleAddWaterMark(
    _text,
    document.querySelector(`.${elementAttributeName}`)
  );
};

handleAddWaterMark 函数,使用 canvas 生成水印,并使用 toDataURL 将其转化为 base64 格式的图片。生成之后删除多余的 canvas 元素。

这里面的参数其实可以抽离处理,向外暴露给使用者的。不过,这就要看个人情况了,想要抽离出来也行。

const handleAddWaterMark = (str, element) => {
  let rotate = -25;
  let fontWeight = "normal";
  let fontSize = "14px";
  let fontFamily = "SimHei";
  let fontColor = "rgba(0, 0, 0, 0.05)";
  let rect = {
    width: 370,
    height: 300,
    left: 10,
    top: 150,
  };
  let can = document.createElement("canvas");
  can.className = "mark-canvas";
  let watermarkDiv = element;
  watermarkDiv.appendChild(can);
  can.width = rect.width;
  can.height = rect.height;
  can.style.display = "none";
  can.style.zIndex = "999";

  let cans = can.getContext("2d");
  cans.rotate((rotate * Math.PI) / 180);
  cans.font = `${fontWeight} ${fontSize} ${fontFamily}`;
  cans.fillStyle = fontColor;
  cans.textAlign = "center";
  cans.textBaseline = "middle";
  cans.fillText(str, rect.left, rect.top);
  // 使用 canvas 生成图片
  const styleStr = `height: inherit !important; background-color: transparent !important; transform: inherit !important; visibility: visible !important; display: block !important; position: absolute !important; z-index: 99 !important; opacity: 1 !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; pointer-events: none !important; background-repeat: repeat !important; background-image: url(${can.toDataURL(
    "image/png"
  )}) !important;`;
  watermarkDiv.setAttribute("style", styleStr);
  // 生成之后删除多余的 canvas 元素
  let canvasDom = document.querySelector(".mark-canvas");
  if (canvasDom) {
    canvasDom.parentElement.removeChild(canvasDom);
  }
};

到这一个水印组件就完成了,让我们来看看效果

image.png

是不是很 nice~

但是,少年,你以为这就结束了?

image.png

你以为使用这个的人不知道 F12,delete html 吗?

image.png

所以我们要干嘛呀~~

监听删除。一删除,我们就马上生成,看谁赢!

image.png

2. 监听删除水印节点

MutationObserver接口提供了监视对DOM树所做更改的能力。

MutationObserver 可以观察整个 文档DOM 树的一部分具体 dom 元素,主要是观察元素的 属性、子节点、文本 的变化,并且可以在 DOM 被修改时异步执行回调。

MutationObserver 接口是为了取代废弃的 MutationEvent:

  • DOM Level 2 规范中描述的 MutationEvent 定义了一组会在各种 DOM 变化时触发的事件。由于浏览器事件的实现机制,这个接口出现了严重的性能问题。因此,DOM Level 3 规定废弃了这些事件。
  • MutationObserver 接口更实用、性能更好

引用自:DOM 规范 —— MutationObserver 接口

import { useEffect } from "react";

const WaterMark = ({ text = "这里是水印" }) => {
  const _text = text;

  const containObserver = () => {
    let bodyObserver = new MutationObserver((mutationsList) => {
      // 监听到 dom 节点被删除
      return mutationsList.forEach((mutation) => {
        if (mutation.removedNodes.length > 0) {
          mutation.removedNodes.forEach((_target) => {
            // 删除的节点是水印
            if (_target.className === elementAttributeName) {
              createWaterDom(document.querySelector('#App'))
            }
          })
        }
      })
    });

    bodyObserver.observe(document.querySelector('#App'), {
      childList: true
    })
  }

  useEffect(() => {
    createWaterDom(document.querySelector('#App'));
    containObserver();
  });
  
  // ....
};

export default WaterMark;

你以为这就万无一失了吗?

还是刚刚那句话,打开 F12,修改这个 className,也是可以删除滴,啦啦啦啦啦。所以需要监听随机生成的类名。

import { useEffect } from "react";

const WaterMark = ({ text = "这里是水印" }) => {

  //....
  const createWaterDom = (element) => {
    // ....
    // 监听随机生成的类名
    let observer = new MutationObserver((mutationsList) => {
      mutationsList.forEach((item) => {
        if (item.type === "attributes") {
          dom.parentElement.removeChild(dom);
          createWaterDom(document.querySelector('#App'))
        }
      })
    });

    observer.observe(dom, {
      attributes: true,
      childList: true
    })
  };
};

export default WaterMark;

抽离参数,由外界传入,加一点点性能优化,就大功告成啦~~~~

import { useEffect, useCallback } from "react";

const WaterMark = ({ text = "", mountElement = "" }) => {
  const _text = text;

  let containMutationObserver = null;

  const containObserver = useCallback(() => {
    let bodyObserver = new MutationObserver((mutationsList) => {
      // 监听到 dom 节点被删除
      return mutationsList.forEach((mutation) => {
        if (mutation.removedNodes.length > 0) {
          mutation.removedNodes.forEach((_target) => {
            // 删除的节点是水印
            if (_target.className === elementAttributeName) {
              createWaterDom(document.querySelector(`#${mountElement}`))
            }
          })
        }
      })
    });
    containMutationObserver = bodyObserver;
    bodyObserver.observe(document.querySelector(`#${mountElement}`), {
      childList: true
    })
  }, [])

  useEffect(() => {
    createWaterDom(document.querySelector(`#${mountElement}`));
    containObserver();
  }, []);

  // 动态生产 classname
  const nameGenerator = () => {
    let result = "";
    let length = 2 + Math.ceil(Math.random() * 7);
    let dict = [
      "a",
      "b",
      "c",
      "d",
      "e",
      "f",
      "g",
      "h",
      "i",
      "g",
      "k",
      "l",
      "m",
      "n",
      "o",
      "p",
      "q",
      "r",
      "s",
      "t",
      "u",
      "v",
      "w",
      "x",
      "y",
      "z",
    ];

    for (let i = 0; i < length; i++) {
      result += dict[Math.ceil(Math.random() * 26 - 1)] || 'a';
    }
    return result;
  };

  const elementAttributeName = nameGenerator();

  // 使用 canvas 创建水印,并进行添加
  const handleAddWaterMark = (str, element) => {
    let rotate = -25;
    let fontWeight = 'normal';
    let fontSize = '14px';
    let fontFamily = 'SimHei';
    let fontColor = 'rgba(0, 0, 0, 0.05)';
    let rect = {
      width: 370,
      height: 300,
      left: 110,
      top: 150
    }
    let can = document.createElement('canvas');
    can.className = 'mark-canvas';
    let watermarkDiv = element;
    watermarkDiv.appendChild(can);
    can.width = rect.width;
    can.height = rect.height;
    can.style.display = 'none';
    can.style.zIndex = '999';

    let cans = can.getContext('2d');
    cans.rotate((rotate * Math.PI) / 180);
    cans.font = `${fontWeight} ${fontSize} ${fontFamily}`;
    cans.fillStyle = fontColor;
    cans.textAlign = 'center';
    cans.textBaseline = 'middle';
    // 先放弃检查字体的宽度
    cans.fillText(str, rect.left, rect.top);
    // 使用 canvas 生成图片
    const styleStr = `height: inherit !important; background-color: transparent !important; transform: inherit !important; visibility: visible !important; display: block !important; position: absolute !important; z-index: 99 !important; opacity: 1 !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; pointer-events: none !important; background-repeat: repeat !important; background-image: url(${can.toDataURL(
      'image/png'
    )}) !important;`;
    watermarkDiv.setAttribute('style', styleStr);
    // 生成之后删除多余的 canvas 元素
    let canvasDom = document.querySelector('.mark-canvas');
    if (canvasDom) {
      canvasDom.parentElement.removeChild(canvasDom);
    }
  }

  const createWaterDom = (element) => {
    let dom = document.createElement("div");
    // 生成随机动态类名
    dom.className = elementAttributeName;
    // 向你需要的元素中添加水印
    element.appendChild(dom);
    handleAddWaterMark(
      _text,
      document.querySelector(`.${elementAttributeName}`)
    );
    // 监听随机生成的类名
    let observer = new MutationObserver((mutationsList) => {
      mutationsList.forEach((item) => {
        if (item.type === "attributes") {
          containMutationObserver.disconnect();
          dom.parentElement.removeChild(dom);
          createWaterDom(document.querySelector(`#${mountElement}`))
          containMutationObserver.observe(document.querySelector(`#${mountElement}`), {
            childList: true
          })
        }
      })
    });

    observer.observe(dom, {
      attributes: true,
      childList: true
    })
  };
};

export default WaterMark;

如何使用:

<div className="App" id="App">
  <WaterMark
     text="这里是一个水印这里是一个水印这里是一个水印"
     mountElement="App"
  />
</div>