页面水印以及防删实现

490 阅读2分钟

背景

。。。

实现思路

用一个载体,承载着水印的内容,盖在整个页面上即可。这个载体可以是图片,可以是canvas也可以是一个div。将其CSS属性pointerEvents设置为none即可。

具体代码实现

水印的实现

const WATERMARK_DOM_ID = 'watermark-container';
const div = document.createElement('div');
div.id = WATERMARK_DOM_ID;
div.style.pointerEvents = 'none';
div.style.position = 'fixed';
div.style.top = '0';
div.style.left = '0';
div.style.zIndex = '2147483647';
div.style.opacity = '1';
div.style.width = '100%';
div.style.height = '100%';
div.style.backgroundImage = `url(${canvas.toDataURL('image/png')})repeat left top`;
document.body.appendChild(div);

在这里使用的是div,整个覆盖在整个页面的最上层,z-index值为int的最大值,然后设置一个背景图。所以我们接下来要完成的就是如何做这个背景图。

interface IWatermarkProps {
  /** 水印内容 */
  text: string;
  /** 水印字体大小 */
  fontSize?: number;
  /** 水印字体颜色 */
  color?: string;
  /** 水印高度 */
  watermarkHeight?: number;
  /** 水印宽度 */
  watermarkWidth?: number;
  /** 水印旋转角度 */
  angle?: number;
}

const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (context) {
  context.font = `${fontSize}px Arial`;
  context.fillStyle = color;
  const parentWidth = document.body.clientWidth;
  const parentHeight = document.body.clientHeight;
  canvas.width = parentWidth;
  canvas.height = parentHeight;
  const numClolumns = Math.ceil(parentWidth / watermarkWidth);
  const numRows = Math.ceil(parentHeight / watermarkHeight);
  context.clearRect(0, 0, parentWidth, parentHeight);
  for (let row = 0; row < numClolumns; row++) {
    for (let column = 0; column < numRows; column++) {
      const x = column * watermarkWidth;
      const y = row * watermarkHeight;
      context.save();
      context.translate(x, y + fontSize);
      context.rotate(angle * Math.PI / 180);
      context.fillText(text, x, y + fontSize);
      context.restore();
    }
  }

我们创建了一个canvas图,设置其文字样式,然后计算其一共多少行多少列,用来计算每个水印文字的位置。设置其旋转角度,文案内容设置。
到这里基本上一个水印就完成了。但是别有用心之人可以直接通过F12控制台去删除我们创建的这个div,从而去掉页面的水印。所以我们需要做防删

防删的实现

const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation: any) => {
    if (mutation.removedNodes.length > 0 && mutation.removedNodes[0]?.id === WATERMARK_DOM_ID) {
      // 重新设置水印
      setWatermark();
    }
    if ((mutation.addedNodes.length > 0 && mutation.addedNodes[0]?.id === WATERMARK_DOM_ID) || (mutation.type === 'attributes' && mutation.target.id === WATERMARK_DOM_ID)) {
      setWatermark();
      observer.disconnect();
      observer.observe(document.body, {
        attributes: true, // 观察属性变动
        childList: true,  // 观察目标子节点的变化
        subtree: true     // 观察后代节点
      });
    }
    })
});

observer.observe(document.body, {
attributes: true, // 观察属性变动
childList: true,  // 观察目标子节点的变化
subtree: true     // 观察后代节点
})

原理也非常简单,就是监听body的子属性变化,在水印节点发生变化时,重新设置水印即可。

完整代码

hooks

import React, { useEffect, memo, useCallback } from 'react';

interface IWatermarkProps {
  /** 水印内容 */
  text: string;
  /** 水印字体大小 */
  fontSize?: number;
  /** 水印字体颜色 */
  color?: string;
  /** 水印高度 */
  watermarkHeight?: number;
  /** 水印宽度 */
  watermarkWidth?: number;
  /** 水印旋转角度 */
  angle?: number;
}

export const WATERMARK_DOM_ID = 'watermark-container';

const Watermark = ({
  text,
  fontSize = 20,
  color = 'rgb(0, 0, 0, 0.08)',
  watermarkHeight = 150,
  watermarkWidth = 200,
  angle = -20 }: IWatermarkProps) => {
  const setWatermark = useCallback(() => {
    const watersDoms = document.querySelectorAll(`#${WATERMARK_DOM_ID}`);
    if (watersDoms.length) {
      watersDoms.forEach((item) => {
        document.body.removeChild(item);
      });
    }
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');

    if (context) {
      const parentWidth = document.body.clientWidth;
      const parentHeight = document.body.clientHeight;
      canvas.width = parentWidth;
      canvas.height = parentHeight;
      const numClolumns = Math.ceil(parentWidth / watermarkWidth);
      const numRows = Math.ceil(parentHeight / watermarkHeight);
      context.font = `${fontSize}px Arial`;
      context.fillStyle = color;
      context.clearRect(0, 0, parentWidth, parentHeight);
      for (let row = 0; row < numClolumns; row++) {
        for (let column = 0; column < numRows; column++) {
          const x = column * watermarkWidth;
          const y = row * watermarkHeight;
          context.save();
          context.translate(x, y + fontSize);
          context.rotate(angle * Math.PI / 180);
          context.fillText(text, x, y + fontSize);
          context.restore();
        }
      }
      const div = document.createElement('div');

      div.id = WATERMARK_DOM_ID;
      div.style.pointerEvents = 'none';
      div.style.position = 'fixed';
      div.style.top = '0';
      div.style.left = '0';
      div.style.zIndex = '2147483647';
      div.style.opacity = '1';
      div.style.width = '100%';
      div.style.height = '100%';
      div.style.backgroundImage = `url(${canvas.toDataURL('image/png')})repeat left top`;

      document.body.appendChild(div);
    }
  }, [text, fontSize, color, watermarkHeight, watermarkWidth, angle]);

  useEffect(() => {
    setWatermark();
  }, [setWatermark]);

  useEffect(() => {
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation: any) => {
        if (mutation.removedNodes.length > 0 && mutation.removedNodes[0]?.id === WATERMARK_DOM_ID) {
          setWatermark();
        }
        if ((mutation.addedNodes.length > 0 && mutation.addedNodes[0]?.id === WATERMARK_DOM_ID) || (mutation.type === 'attributes' && mutation.target.id === WATERMARK_DOM_ID)) {
          setWatermark();
          observer.disconnect();
          observer.observe(document.body, {
            attributes: true, // 观察属性变动
            childList: true,  // 观察目标子节点的变化
            subtree: true     // 观察后代节点
          });
        }
      })
    });

    observer.observe(document.body, {
      attributes: true, // 观察属性变动
      childList: true,  // 观察目标子节点的变化
      subtree: true     // 观察后代节点
    })

    return () => {
      observer.disconnect();
    }
  }, []);
  return <></>;
}

export default memo(Watermark);

class

import React, { Component } from 'react';

interface IWatermarkProps {
  /** 水印内容 */
  text: string;
  /** 水印字体大小 */
  fontSize?: number;
  /** 水印字体颜色 */
  color?: string;
  /** 水印高度 */
  watermarkHeight?: number;
  /** 水印宽度 */
  watermarkWidth?: number;
  /** 水印旋转角度 */
  angle?: number;
}

export const WATERMARK_DOM_ID = 'watermark-container';

class Watermark extends Component<IWatermarkProps> {
  componentDidMount() {
    this.setWatermark();
  }

  componentDidUpdate(prevProps: IWatermarkProps) {
    if (
      prevProps.text !== this.props.text ||
      prevProps.fontSize !== this.props.fontSize ||
      prevProps.color !== this.props.color ||
      prevProps.watermarkHeight !== this.props.watermarkHeight ||
      prevProps.watermarkWidth !== this.props.watermarkWidth ||
      prevProps.angle !== this.props.angle
    ) {
      this.setWatermark();
    }
  }

  componentWillUnmount() {
    const watersDoms = document.querySelectorAll(`#${WATERMARK_DOM_ID}`);
    if (watersDoms.length) {
      watersDoms.forEach((item) => {
        document.body.removeChild(item);
      });
    }
  }

  setWatermark = () => {
    const watersDoms = document.querySelectorAll(`#${WATERMARK_DOM_ID}`);
    if (watersDoms.length) {
      watersDoms.forEach((item) => {
        document.body.removeChild(item);
      });
    }
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');

    if (context) {
      const parentWidth = document.body.clientWidth;
      const parentHeight = document.body.clientHeight;
      canvas.width = parentWidth;
      canvas.height = parentHeight;
      const numClolumns = Math.ceil(parentWidth / this.props.watermarkWidth);
      const numRows = Math.ceil(parentHeight / this.props.watermarkHeight);
      context.font = `${this.props.fontSize}px Arial`;
      context.fillStyle = this.props.color;
      context.clearRect(0, 0, parentWidth, parentHeight);
      for (let row = 0; row < numClolumns; row++) {
        for (let column = 0; column < numRows; column++) {
          const x = column * this.props.watermarkWidth;
          const y = row * this.props.watermarkHeight;
          context.save();
          context.translate(x, y + this.props.fontSize);
          context.rotate(this.props.angle * Math.PI / 180);
          context.fillText(this.props.text, x, y + this.props.fontSize);
          context.restore();
        }
      }
      const div = document.createElement('div');

      div.id = WATERMARK_DOM_ID;
      div.style.pointerEvents = 'none';
      div.style.position = 'fixed';
      div.style.top = '0';
      div.style.left = '0';
      div.style.zIndex = '2147483647';
      div.style.opacity = '1';
      div.style.width = '100%';
      div.style.height = '100%';
      div.style.backgroundImage = `url(${canvas.toDataURL('image/png')})repeat left top`;

      document.body.appendChild(div);
    }
  }

  render() {
    return <></>;
  }
}

export default Watermark;

后话

可能细心的小伙伴会发现,代码中不仅导出了组件还导出了我们的水印div的id,这个原因也很简单,因为水印通常是在用户登录后才会有的,所以可能在用户未登录或者登出的情况下需要我们移除页面水印,但是这部分工作可能就要放在我们的前端页面鉴权的逻辑那边。例如:

import { WATERMARK_DOM_ID } from 'Watermark';
useEffect(() => {
  if (!userId) {
    const waterDoms = document.querySelectorAll(`#${WATERMARK_DOM_ID}`);
    if (!!waterDoms.length) {
      waterDoms.forEach((it) => {
        document.body.removeChild(it);
      });
    }
  }
}, [userId]);

意思就是这么个意思,具体细节不要较真。