Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

图片

介绍两种创建水印的方式,和水印如何避免被修改

我们开发出来的产品,为了避免信息泄露,或者为了版权声明等,都会为页面添加水印。

水印又会分为固定水印和私有水印,例如品牌相关、版权声明之类的,一般是使用固定水印,即任何人打开该页面,水印都是一样的,这种一般是设置背景图片平铺即可,如下图中,用户即使截图传播,也能知道 “腾讯新闻” 的品牌:

有品牌的固定水印

不过今天我们主要讲解的是「私有水印」,即以当前用户的信息、当前时间、当前所在浏览器等信息,来实时生成水印,每个用户产生的水印的是不一样的。

动态生成水印有两种方式:

  • 使用 div 来创建,这种方式方便控制样式;

  • 使用 canvas 来创建,然后导出成背景图,减少 div 元素的使用;

接下来我们一一来说明这两种方式都是如何使用的,并且还会讲解下如何避免水印被删除和篡改。

  1. 使用 div 来创建水印

使用 div 可以创建水印时,首先我们创建出一组的水印,然后再循环生成多个,铺满屏幕即可。

const createWaterItem = ({ texts, width, height }) => {  const div = document.createElement("div");  let html = "";  texts.forEach((text) => {    html += `<p>${text}</p>`;  });  div.innerHTML = html;  div.style.cssText = `width: ${width}px; height: ${height}px`;  div.className = "water-item";  return div;};/** * 通过div来创建水印 */const createWaterMark = ({  texts, // 要展示的文本,string[]  target = document.body, // 水印添加的位置  width = 300, // 单独一小块水印的尺寸  height = 200, // 单独一小块水印的尺寸}) => {  const { offsetHeight, offsetWidth } = target;  const col = Math.ceil(offsetWidth / width);  const row = Math.ceil(offsetHeight / height);  const div = document.createElement("div");  div.style.cssText = `width: ${width * col}px; height: ${offsetHeight}px`;  const water = document.createElement("div");  water.id = "watermark";  const fragment = document.createDocumentFragment();  for (let i = 0; i < col * row; i++) {    fragment.append(createWaterItem({ texts, width, height }));  }  div.appendChild(fragment);  water.appendChild(div);  target.appendChild(water);  const style = document.createElement("style");  style.innerHTML = `#watermark {            position: absolute;            top: 0;            left: 0;            width: 100%;            z-index: 19;            pointer-events: none;            overflow: hidden;            color: #dd0000;          }          #watermark > div {            display: flex;            flex-wrap: wrap;          }          #watermark .water-item {            text-align: center;            transform: rotate(20deg);          }`;  document.head.appendChild(style);};createWaterMark({  texts: [    "蚊子的博客",    new Date().toLocaleString(),    "当前在蚊子的前端博客网站上",  ],});

执行之后,得到的效果:

使用 div 创建的水印

不过由此也能看到,创建出了无数的 dom 元素:

使用 div 创建水印后的 dom 元素

随着页面的增长 (chang),dom 元素也只会越来越多。因此,虽然 div 也能达到水印的效果,但我们并不推荐这种方式。

  1. 使用 canvas 来创建水印

使用 canvas 来创建水印,主要是用 canvas 把文字转成图片,然后再把图片作为背景图进行平铺。

2.1 具体的代码实现

直接上代码。

import dayjs from "dayjs";/** * 获取水印要插入的元素 * @param selector 选择器 */const getRoot = (selector?: string | Element) => {  if (selector) {    if (typeof selector === "string") {      const dom = document.querySelector(selector);      if (dom) {        return dom;      }    } else if (selector) {      return selector;    }  }  return document.body;};const waterMarkId = 'water-mark';/** * 创建水印 * @param texts 水印的文案 * @param root 水印插入的位置 * @returns 样式 */const create = (texts: string[], root: Element) => {  const dpr = window.devicePixelRatio;  const width = 400 * dpr;  const height = 300 * dpr;  const fontSize = 18;  const rotate = -20; // 水印倾斜角度,单位度  const parentRect = root.getBoundingClientRect();  const canvas = document.createElement("canvas");  canvas.style.width = `${width / dpr}px`;  canvas.style.height = `${height / dpr}px`;  canvas.width = width;  canvas.height = height;  canvas.style.display = "none";  document.body.appendChild(canvas);  const ctx = canvas.getContext("2d");  if (!ctx) {    return "";  }  ctx.translate(width / 2, height / 2);  ctx.rotate((rotate * Math.PI) / 180);  ctx.translate(-width / 2, -height / 2);  ctx.font = `${fontSize}px Verdana`;  // ctx.fillStyle = "rgba(17, 17, 17, 0.2)";  ctx.fillStyle = "rgba(191, 191, 191, 0.5)";  // 设置上下左右居中  ctx.textAlign = "center";  ctx.textBaseline = "middle";  // 为避免多行文本重叠到一块  const half = Math.floor(texts.length / 2);  texts.push(dayjs().format("YYYY/MM/DD HH:mm:ss"));  texts.forEach((text, index) => {    const diff = (fontSize + 8) * Math.abs(index - half);    const y = index <= half ? height / 2 - diff : height / 2 + diff;    ctx.fillText(text, width / 2, y);  });  // 这里为了可以直接使用,就把样式写在了js中,您也可以把样式单独提取出来  const style: any = {    top: 0,    left: 0,    width: `${parentRect.width}px`,    height: `${parentRect.height}px`,    "background-image": `url(${canvas.toDataURL("image/png")})`, // 将生成的图片作为背景图    "background-repeat": "repeat-y", // 当时我们的项目要求是只向下平铺,您可以自行修改    "background-position": "center top",    position: "absolute",    "z-index": 99,    "pointer-events": "none",  };  const cssText = Object.keys(style)    .map((key) => `${key}: ${(style as any)[key]}`)    .join(";");  const waterDom = document.getElementById(waterMarkId);  if (waterDom) {    waterDom.parentNode?.removeChild(waterDom);  }  const div = document.createElement("div");  div.id = waterMarkId;  div.style.cssText = cssText;  root.appendChild(div);  canvas.parentNode?.removeChild(canvas);  return cssText;};/** * 创建水印 * @param texts 水印的文案 * @param selector 水印所在的容器,不传时则默认body * @returns 取消监听水印变化的函数 */const createWaterMark = (texts: string[], selector?: string | Element) => {  const root = getRoot(selector);  create(texts, root);};

调用:

createWaterMark([  "蚊子的博客",  new Date().toLocaleString(),  "当前在蚊子的前端博客网站上",]);

加油

2.2 如何让文字上下左右居中

渲染的文本默认是在左上角,但我们有多行文本,每行文本都需要居中显示,而且还有轻微的旋转角度。如果完全以左上角为坐标旋转的话,会有部分文字被遮挡。

我们首先通过translate()方法,将新的起点偏移到中间,然后再进行渲染,再回到最初的起点:

ctx.translate(width / 2, height / 2);ctx.rotate((rotate * Math.PI) / 180);ctx.translate(-width / 2, -height / 2);

关于 canvas 中的文字的上下左右居中的问题。canvas 有 textAlign 和 textBaseline 两个属性设置文字的对齐方式,这两个属性是用来设置文本行内整体的对齐方式。

canvas 中 textAlign 和 textBaseline 的对齐方式

若想上下左右居中时,可以这样设置:

ctx.textAlign = "center";ctx.textBaseline = "middle";

当只有一行文本时,没什么问题。若有多行本文时,就会发现多行文本重叠到一起。这就需要自己根据文字的大小,进行 y 轴上的偏移:

const half = Math.floor(texts.length / 2); // 获取多行文本的中间位置,决定是向上偏移,还是向下偏移texts.forEach((text, line) => {  // 根据文字大小和该行的行数,算出偏移量  const diff = (fontSize + 8) * Math.abs(line - half);  // 获取该行文字描绘的y轴坐标:当小于一半时,向上偏移;否则向下偏移  const y = line <= half ? height / 2 - diff : height / 2 + diff;  ctx.fillText(text, width / 2, y);});

最终形成的水印图片效果:

水印图片的效果

  1. 避免水印被删除和篡改

我们为了防止用户使用开发者工具之类的工具,删除或者修改节点的样式去除水印,这时候可以用到 MutationObserver 构造函数,它可以创建并返回一个新的 MutationObserver 它会在指定的 DOM 发生变化时被调用。

在上面的 createrWaterMark() 中:

const createWaterMark = (texts: string[], selector?: string | Element) => {  const root = getRoot(selector);  const originalCssText = create(texts, root); // 创建水印,并留存样式,用作后续的样式对比  // 为避免水印被删除或样式被修改,这里监听dom节点的变化  const observer = new MutationObserver(() => {    const waterMarkDom = document.getElementById(waterMarkId);    if (waterMarkDom) {      // 水印还在,但被修改了样式,重新设置样式      const newStyle = waterMarkDom.getAttribute("style");      if (originalCssText !== newStyle) {        waterMarkDom.setAttribute("style", originalCssText);      }    } else {      // 该水印已被删除,重新创建      create(texts, root);    }  });  observer.observe(root, {    attributes: true, // 开启监听属性    childList: true, // 开启监听子节点    subtree: true, // 开启监听子节点下面的所有节点  });  // 返回一个 destory 方法,用于在 useEffect() 中取消该监听  return () => {    observer.disconnect();  };};

当检测到水印的样式被修改,或者水印被删除,则会重置样式,或者重新创建水印。但这也只是简单的检测,并不能完全保证水印被修改。

腾讯新闻构建高性能的 react 同构直出方案

腾讯新闻抢金达人活动 node 同构直出渲染方案的总结

仿 Vue2.x 中的双向数据绑定实现

▼我是来自腾讯的前端开发工程师,

长按识别二维码关注,与大家共同学习▼