首页博客音乐

优雅地添加页面水印

svg watermark mutation-observer

原创2022-02-25


所谓前端水印(watermark)指的就是这玩意。

1

说起来我的第一反应还是用canvas来做。

canvas 实现

canvas对于文字的表现个人认为并不算非常友好。一共只有两个 API 可以绘制文字,分别是fillText和strokeText。

同时呢,对于非水平方向上的文字,只能通过画布的rotate来实现。如下图。

2

麻烦的地方在于,canvas的绘制是顺序的,如果是重复绘制文字呢,需要做非常复杂的笛卡尔坐标计算。既便使用translate循环重置坐标原点,仍然不是一种很直接方式。

window.onload = function() {
  // get parent element height and width
  const main = document.querySelector('main');

  const HEIGHT = main.offsetHeight;
  const WIDTH = main.offsetWidth;

  // get cvs and set height and width by parent
  const cvs = document.getElementById('watermark');

  cvs.height = HEIGHT;
  cvs.width = WIDTH;

  const ctx = cvs.getContext('2d');

  ctx.font = "30px Arial";
  ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
  ctx.textBaseline = "top";

  const TEXT_WIDTH = ctx.measureText('alexander').width;

  ctx.translate(5, TEXT_WIDTH * Math.sqrt(2) / 2 + 5);
  ctx.rotate(-Math.PI / 4);
  ctx.fillText('alexander', 0, 0, TEXT_WIDTH);

  ctx.resetTransform();

  // calculate coordinates and repeat
  ctx.translate(5 + 5 + TEXT_WIDTH, TEXT_WIDTH * Math.sqrt(2) / 2 + 5);
  ctx.rotate(-Math.PI / 4);
  ctx.fillText('alexander', 0, 0, TEXT_WIDTH);
}

另一种思路呢就是渲染多个同样的、只包含一个文字的 canvas。像这样。

3

也不好,为什么呢?页面大了之后多了很多canvas,对性能上是有一定影响的。

这不禁令我陷入沉思。没有更好的方法了吗?

4

隔了几天有个小弟问我 svg 的图怎么改颜色。作为一个逼王,我突然嗅到了一个能装逼的机会。

canvas 能做的,svg 也能做,不妨试试?

svg 实现

svg 对于这种重复性的图形或文字渲染是有先天优势的,因为 svg 有一个功能强大的<pattern>。

详见 https://developer.mozilla.org/en-US/docs/Web/SVG/Element/pattern

在<pattern>中定义文字并 fill,听起来就很高大上。

<svg id="watermark" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%">
  <style type="text/css">text { fill: rgba(0,0,0,0.1); font-family: Avenir, Arial, Helvetica, sans-serif; }</style>
  <defs>
    <pattern id="name-pattern" patternUnits="userSpaceOnUse" width="400" height="200">
      <text y="30" font-size="40" id="name">Alexander</text>
    </pattern>
    <pattern id="repeat" href="#name-pattern" patternTransform="rotate(-45)">
      <use href="#name" />
    </pattern>
  </defs>
  <rect width="100%" height="100%" fill="url(#repeat)" />
</svg>

看下效果。

5

有些简单了,搞复杂一点。

<svg id="watermark" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%">
  <style type="text/css">text { fill: rgba(0,0,0,0.1); font-family: Avenir, Arial, Helvetica, sans-serif; }</style>
  <defs>
    <pattern id="pattern" patternUnits="userSpaceOnUse" width="400" height="200">
      <text y="30" font-size="40" id="name">Alexander</text>
    </pattern>
     <pattern href="#pattern">
      <text x="200" y="120" font-size="30" id="at">@alexander</text>
    </pattern>
    <pattern id="repeat" href="#pattern" patternTransform="rotate(-45)">
      <use href="#name" />
      <use href="#at" />
    </pattern>
  </defs>
  <rect width="100%" height="100%" fill="url(#repeat)" />
</svg>

5 1

为了不增加额外的DOM节点,我们用binary的方式将其作为CSS background-image至body。

const rawSVGString = '<svg id="watermark" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%">
  <style type="text/css">text { fill: rgba(0,0,0,0.1); font-family: Avenir, Arial, Helvetica, sans-serif; }</style>
  <defs>
    <pattern id="pattern" patternUnits="userSpaceOnUse" width="400" height="200">
      <text y="30" font-size="40" id="name">Alexander</text>
    </pattern>
    <pattern href="#pattern">
      <text x="200" y="120" font-size="30" id="at">@alexander</text>
    </pattern>
    <pattern id="repeat" href="#pattern" patternTransform="rotate(-45)">
      <use href="#name" />
      <use href="#at" />
    </pattern>
  </defs>
  <rect width="100%" height="100%" fill="url(#repeat)" />
  </svg>';

document.body.style.backgroundImage = `url('data:image/svg+xml;base64,${window.btoa(rawSVGString)}')`;

6

并不优雅

弄完之后我发现了一个重大的漏洞,就是如果用户是同样略懂前端的装逼犯,他就会按F12打开Console把你body的样式改了,水印就没了。

有没有什么方法能阻止用户去hack Console呢?

这个还真没有。

但是同样地,为什么不换个思路呢————比如有没有什么listener可以监听用户去hack呢?

这个可以有,那就是Mutation Observer。 https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

简单来说,这个东西就是允许用户自定义观察者,并对mutation做出反应。

const observer = new MutationObserver((mutationsList, observer) => {
  // mutation回调
});

observer.observe(
  document.querySelector("body"), // 作为观察者的节点
  {  attributes: true } // 需要观察的属性
);

我们来优化一下。

const rawSVGString =
  '<svg id="watermark" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%"><style type="text/css">text { fill: rgba(0,0,0,0.1); font-family: Avenir, Arial, Helvetica, sans-serif; }</style><defs><pattern id="pattern" patternUnits="userSpaceOnUse" width="400" height="200"><text y="30" font-size="40" id="name">Alexander</text></pattern><pattern href="#pattern"><text x="200" y="120" font-size="30" id="at">@alexander</text></pattern><pattern id="repeat" href="#pattern" patternTransform="rotate(-45)"><use href="#name" /><use href="#at" /></pattern></defs><rect width="100%" height="100%" fill="url(#repeat)" /></svg>';

document.body.style.backgroundImage = `url('data:image/svg+xml;base64,${window.btoa(
  rawSVGString
)}')`;

const observer = new MutationObserver((mutationsList, observer) => {
  for (const mutation of mutationsList) {
    if (mutation.type === "attributes" && mutation.attributeName === "style") {
      document.body.style.backgroundImage = `url('data:image/svg+xml;base64,${window.btoa(
        rawSVGString
      )}')`;
    }
  }
});

observer.observe(document.querySelector("body"), {
  attributes: true,
  attributeFilter: ["style"]
});

现在你怎么改body的样式watermark都消不掉了。因为任何对body的hack都会导致watermark重新绘制。

Mutation Observer and Event Listener

随着负责了一些大型项目的架构设计,逐渐对设计模式有了“不只限于背定义”的理解。

简言之,Mutation Observer是观察者模式,但是EventListener是观察者模式吗?看着像,但我个人认为不算是观察者模式。

第一,观察者模式下,观察者观察的是某个object的变化。所以Mutation Observer肯定符合。但是EventListener监听的是事件,换言之更像是一种hooks。数据驱动和事件驱动在本质是有区别的。

第二,观察者模式下的任何变化,会通知到所有的观察者;而EventListener则是去触发调度器(Mediator)。

第三,观察者模式是可以是一对多的关系,而EventListener因为存在一个event-handler的map,是一对一的关系。


如文章标明“原创”,转载请联系笔者

侵权直接起诉

编程易秃,打赏植发