likes
comments
collection
share

从 antd 5.x 版本的水印组件源码中学到了这个

作者站长头像
站长
· 阅读数 11

前言

antd 5 版本已经出了很久了,但最近一直没有做 PC 端所以一直没有太多关注,前几天忽然想看看新版本出了哪些新的组件,然后就发现了水印组件。因为之前面试时也有人问过水印组件该怎么做,并且怎么能保证安全性和如果使用 css 绘制的话,怎么能不让用户手动去掉,所以就打开了控制台,随后就发现了新的大陆。

不多说直接上图

从 antd 5.x 版本的水印组件源码中学到了这个

当看到这个效果的时候,以为是不是在这里实现了局部刷新,因为在取消其中一个 css 时,确实感觉像局部刷新了一样,所以为了验证是否如此,我打开了源码。

可以看到有个自定义的 hook ,打开后可以看到有个判断条件 ,一个新兴词汇出现 MutationObserver。

什么是 MutationObserver

MDN 给出的答案:MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。

构造函数 MutationObserver()

MutationObserver 是一个构造函数,基本语法:

var observer = new MutationObserver(callback);

接收一个回调函数,生成一个 observer 实例,可以用实例的 observer 方法去监听 DOM 的变化,当 DOM 变化后就会执行 callback 方法。

实例方法

observe()

observe() 方法配置了 MutationObserver 对象的回调方法以开始接收与给定选项匹配的 DOM 变化的通知。根据配置,观察者会观察 DOM 树中的单个 Node,也可能会观察被指定节点的部分或者所有的子孙节点。

基本语法如下:

observer.observe(target[, options])

第一个参数:被观察的 DOM 节点 第二个参数:配置观察该节点的属性 childList、attributes 和 characterData 中,必须有一个参数为 true。否则会抛出 TypeError 异常。

属性说明
subtree当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target。默认值为 false。
childList当为 true 时,监听 target 节点中发生的节点的新增与删除(同时,如果 subtree 为 true,会针对整个子树生效)。默认值为 false。
attributes当为 true 时观察所有监听的节点属性值的变化。默认值为 true,当声明了 attributeFilter 或 attributeOldValue,默认值则为 false。
attributeFilter一个用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知。
attributeOldValue当为 true 时,记录上一次被监听的节点的属性变化;可查阅 MutationObserver 中的 Monitoring attribute values 了解关于观察属性变化和属性值记录的详情。默认值为 false。
characterData当为 true 时,监听声明的 target 节点上所有字符的变化。默认值为 true,如果声明了 characterDataOldValue,默认值则为 false
characterDataOldValue当为 true 时,记录前一个被监听的节点中发生的文本变化。默认值为 false

附上详细的配置项

var targetNode = document.querySelector("#someElement");
var observerOptions = {
  childList: true,  // 观察目标子节点的变化,是否有添加或者删除
  attributes: true, // 观察属性变动
  subtree: true     // 观察后代节点,默认为 false
}

var observer = new MutationObserver(callback);
observer.observe(targetNode, observerOptions);

disconnect()

disconnect() 方法告诉观察者停止观察变动。

基本语法如下:

observer.disconnect()

takeRecords()

takeRecords() 方法返回已检测到但尚未由观察者的回调函数处理的所有匹配 DOM 更改的列表,使变更队列保持为空。此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改。

基本语法如下:

mutationRecords = observer.takeRecords()

返回一个 MutationRecord 对象列表,每个对象都描述了应用于 DOM 树某部分的一次改动。 在调用 disconnect() 方法之前,将未处理的更改进行处理。

MutationRecord 对象列表的属性在此就不一一展示了,给大家传送门,有兴趣的可以去看看。

useMutationObserver

MutationObserver 对象讲完后,再来看 useMutationObserver 这个自定义 hook,就一目了然。

useMutationObserver 对外吐出了三个方法,createObserver、destroyObserver 和 reRendering。

内部创建 instance 用来存放 MutationObserver 实例

const instance = useRef<MutationObserver>()

并在调用该 hook 先进行 destroyObserver 方法,保证唯一性。

useEffect(() => destroyObserver, [])

destroyObserver

const destroyObserver = () => {
  if (instance.current) {
    instance.current.takeRecords();
    instance.current.disconnect();
    instance.current = undefined;
  }
}

该方法用于取消观察,内部执行 takeRecords() 和 disconnect() 方法,并在取消观察后将实例销毁。

createObserver

const createObserver = (target: Node, callback: MutationCallback) => {
  if (MutationObserver) {
    destroyObserver();
    instance.current = new MutationObserver(callback);
    instance.current.observe(target, {
      childList: true,
      subtree: true,
      attributeFilter: ['style', 'class'],
    });
  }
}

该方法用于创建 MutationObserver 实例,在创建前先取消观察,调用 observe() 方法观察目标 DOM 的子节点的变化,是否有添加或者删除,观察后代节点和 style、class 属性是否有变化。

reRendering

const reRendering = (mutation: MutationRecord, watermarkElement?: HTMLElement) => {
  let flag = false;
  // Whether to delete the watermark node
  if (mutation.removedNodes.length) {
    flag = Array.from(mutation.removedNodes).some((node) => node === watermarkElement);
  }
  // Whether the watermark dom property value has been modified
  if (mutation.type === 'attributes' && mutation.target === watermarkElement) {
    flag = true;
  }
  return flag;
}

该方法用于判断是否需要重新渲染,两个条件 1、是否删除水印节点 2、 水印dom属性值是否已修改。

组件整体实现

从 antd 5.x 版本的水印组件源码中学到了这个

核心代码:

const appendWatermark = (base64Url: string, markWidth: number) => {
  if (containerRef.current && watermarkRef.current) {
    destroyObserver();
    watermarkRef.current.setAttribute(
      'style',
      getStyleStr({
        ...getMarkStyle(),
        backgroundImage: `url('${base64Url}')`,
        backgroundSize: `${(gapX + markWidth) * BaseSize}px`,
      }),
    );
    containerRef.current?.append(watermarkRef.current);
    createObserver(containerRef.current, (mutations) => {
      mutations.forEach((mutation) => {
        if (reRendering(mutation, watermarkRef.current)) {
          destroyWatermark();
          // eslint-disable-next-line @typescript-eslint/no-use-before-define
          renderWatermark();
        }
      });
    });
  }
}

整体实现如上图所示,通过 MutationObserver 观察水印变化,当用户更改水印样式或者删除水印 DOM 时,将水印 DOM 删除,再重新绘制,绘制的部分在此就不多讲了,有兴趣的同学可以去看看源码。这样就可以防止用户通过浏览器控制台去掉水印操作。

但是这样真的是绝对安全吗?还请大佬指点?