关于前端:你知道前端水印功能是怎么实现的吗

1次阅读

共计 8378 个字符,预计需要花费 21 分钟才能阅读完成。

前言

前一段时间因为我的项目须要实现水印性能,于是去理解了相干的内容后,基于 Vue 的实现了一个 v-watermark 指令实现了对应的性能,其实整体内容并不简单!

那么接下来先来简略理解一些和 Vue 自定义指令相干的内容,作为前置常识的铺垫,而后在逐渐实现对应的性能。

vue 中的自定义指令

以下的内容其实简略理解即可,甚至能够间接跳过,遇到不意识的内容在回头来查看都是能够的,甚至能够间接查看 官网文档

是什么?

Vue3复用代码 有如下三种形式:

  • 组件:组件是次要的构建模块
  • 组合式函数:合式函数则侧重于有状态的逻辑
  • 自定义指令:次要是为了重用波及一般元素的底层 DOM 拜访的逻辑

因而,自定义指令自身也是一种代码逻辑复用的形式,只是着重点在 对底层 DOM 的拜访和操作 上。

指令钩子

VueReactWebpackVite 等中都会存在着对应的 钩子 ,而这些 钩子 实质上就是一些在 特定机会 会被执行的 函数 / 办法 而已,那么在 Vue 自定义指令 中就存在如下钩子:

  • created(el, binding, vnode, prevVnode):在绑定元素的 attribute 前,或事件监听器利用前调用
  • beforeMount(el, binding, vnode, prevVnode):在元素被插入到 DOM 前调用
  • mounted(el, binding, vnode, prevVnode):在绑定元素的父组件,及其所有子节点都挂载实现后调用
  • beforeUpdate(el, binding, vnode, prevVnode):绑定元素的父组件更新前调用
  • updated(el, binding, vnode, prevVnode):在绑定元素的父组件,及他本人的所有子节点都更新后调用
  • beforeUnmount(el, binding, vnode, prevVnode):绑定元素的父组件卸载前调用
  • unmounted(el, binding, vnode, prevVnode):绑定元素的父组件卸载后调用

留神】以上这些钩子与 vue2 中的自定义钩子是有些不同的,具体能够点击对应链接比对查看.

钩子参数

其实间接看参数的命名形式,置信你也能晓得大部分的参数代表什么:

  • el:指令绑定到的元素,意味着可通过 el 间接操作 DOM
  • binding:一个对象,蕴含以下属性

    • value:传递给指令的值,例如在 v-my-directive="1 + 1" 中,值是 2
    • oldValue:之前的旧值,仅在 beforeUpdate 和 updated 中可用,无论值是否更改都可用
    • arg:传递给指令的参数 (如果有的话),如在 v-my-directive:foo 中,参数是 "foo"
    • modifiers:一个蕴含修饰符的对象 (如果有的话),如在 v-my-directive.foo.bar 中,修饰符对象是 {foo: true, bar: true}
    • instance:应用该指令的组件实例
    • dir:指令的定义对象
  • vnode:代表绑定元素的底层 VNode
  • prevNode:之前的渲染中代表指令所绑定元素的 VNode,仅在 beforeUpdate 和 updated 钩子中可用

留神】除了 el 外(因为须要操作 DOM),其余参数都是只读的,不倡议更改,如果须要在不同的钩子间共享信息,举荐通过元素的 dataset 属性实现

实现水印性能

几种实现计划

基于原图生成水印图片(后端)

这种计划就是将 原图片 增加水印之后生成了 新图片,后续在前端页面进行展现是后端接口不返回原图片,而是返回带有水印的图片即可。

这种形式最大的长处就是平安,因为 水印图片 是后端生成的,前端只须要负责展现即可,不需思考多余的问题,且即使在前端页面保留对应图片,拿到的依然不是原图片。

基于 DOM 实现水印成果(前端)

自定义指令钩子十分多,但实际上能应用到的不多,比方最罕用的就是 mountedupdated,在这咱们只须要通过 mounted 即可实现对应的性能,并且外围代码比较简单。

核心内容

  • 创立一个 watermarkDOM 节点,即 div 元素,用于包裹对应的 img 便于展现水印内容
  • 在创立一个 waterbgDOM 节点,即 div 元素

    • waterbg 节点作为 watermark子节点 ,并进行 相对定位 保障 waterbg 最上层显示
    • 将对应的 水印标记 作为 waterbg 节点的 背景图片 展现
    • waterbg 节点设置 pointer-events: none; 实现 点击穿透
  • watermark 节点通过 insertBefore(...) 插入到 img 标签的前一个地位
  • 再将 img 标签挪动到 watermark 节点节中,这样就保障了新创建的 watermark 节点的地位肯定是在本来 img 挂载的地位

成果和代码如下

// main.ts
import {createApp} from 'vue'
import App from './App.vue'
import directives from './directives'

createApp(App)
    .use(directives)
    .mount('#app');


// src/directives/index.ts
import type {App} from 'vue'
import watermark from './waterMark'

export default function installDirective(app: App) {app.directive(watermark.name, watermark.directives);
} 

// src/directives/waterMark.ts
import waterBgImg from '../assets/water-bg.png'

const directives: any = {mounted(el: HTMLElement) {el.onload = () => {const { clientWidth, clientHeight, parentElement} = el;

            const waterMark: HTMLElement = document.createElement('div');
            const waterBg: HTMLElement = document.createElement('div');

            waterMark.className = `water-mark`;// 不便自定义展现后果

            // 创立 waterMark 父元素
            waterMark.setAttribute('style', `
            display: inline-block;
            overflow: hidden;
            position: relative;
            width: ${clientWidth}px; 
            height: ${clientHeight}px;`);

            // 创立 waterBg 背景元素
            waterBg.className = `water-mark-bg`;// 不便自定义展现后果
            waterBg.setAttribute('style', `
            position: absolute;
            pointer-events: none;
            width: 100%;
            height: 100%;
            opacity: 0.2;
            background-image: url(${waterBgImg}); 
            background-repeat: repeat;`);

            // 为 waterMark 增加对应的子元素
            waterMark.appendChild(waterBg);
            // 将 waterMark 插入到对应的地位
            parentElement?.insertBefore(waterMark, el);
            // 将图片元素挪动到 waterMark 中
            waterMark.appendChild(el);
        }
    }
}


export default {
    name: 'watermark',
    directives
}

优化实现形式

在上述的实现形式中,实际上至多有两点可优化的点:

  • 所有的款式间接以 字符串 模式呈现在 JavaScript 代码中

    • 能够将对应的动态款式局部解决在 css 中,由 .water-mark 来治理
  • 多余的 waterBg 被创立进去

    • 其实齐全没必要独自创立这个节点元素,能够间接基于 .water-mark 对应节点的伪类元素来实现

优化后外围代码如下:

/********* src/directives/waterMark.ts ***********/

const directives: any = {mounted(el: HTMLElement) {el.onload = () => {const { clientWidth, clientHeight, parentElement} = el;

      const waterMark: HTMLElement = document.createElement("div");

      // 创立 waterMark 父元素
      waterMark.setAttribute("style", `width: ${clientWidth}px; height: ${clientHeight}px;`);
      waterMark.className = `water-mark`; // 不便自定义展现后果

      // 将 waterMark 插入到对应的地位
      parentElement?.insertBefore(waterMark, el);
      // 将图片元素挪动到 waterMark 中
      waterMark.appendChild(el);
    };
  },
};

export default {
  name: "watermark",
  directives,
};

/********* css 局部代码如下  ***********/ 
.water-mark {
  display: inline-block;
  overflow: hidden;
  position: relative;
}
.water-mark::after {
  pointer-events: none;
  position: absolute;
  content: ' ';
  width: 100%;
  height: 100%;
  opacity: 0.2;
  background-image: url("../assets/water-bg.png");
  background-repeat: repeat;
}

基于 Canvas 实现水印成果(前端)

基于 Canvas 实现形式的长处就在于可能动静的设置水印内容,相比于上一种基于固定背景图片的形式更灵便,这种形式也是 语雀 在应用的形式,具体成果如下:

外围步骤

  • 通过 canvas 填充文本,并通过 canvas.toDataURL("image/png"); 获取到对应的 base64 格局的图片
  • 将这个 base64 格局的图片作为类名为 water-mark 节点的背景图

    • 利用 background-repeat: repeat; 让这个图反复填充背景即可
    • water-mark 节点设置 pointer-events: none; 实现 点击穿透
    • 利用对应图片的父元素作为 water-mark 节点的绝对定位节点,保障相对定位的 water-mark 节点显式在对应图片之上

成果和代码如下

/********* src/directives/waterMark.ts  ***********/ 

// 全局保留 canvas 和 div,防止反复创立(单例模式)const globalCanvas = null;
const globalWaterMark = null;

// 获取 toDataURL 的后果
const getDataUrl = ({
  font = "16px normal",
  fillStyle = "rgba(180, 180, 180, 0.3)",
  textAlign,
  textBaseline,
  text = "请勿外传",
}) => {
  const rotate = -20;
  const canvas = globalCanvas || document.createElement("canvas");
  const ctx = canvas.getContext("2d"); // 获取画布上下文

  ctx.rotate((rotate * Math.PI) / 180);
  ctx.font = font;
  ctx.fillStyle = fillStyle;
  ctx.textAlign = textAlign || "left";
  ctx.textBaseline = textBaseline || "middle";
  ctx.fillText(text, canvas.width / 3, canvas.height / 2);

  return canvas.toDataURL("image/png");
};

// 设置水印
const setWaterMark = (el: HTMLElement, binding: any) => {const { parentElement} = el;

  // 获取对应的 canvas 画布相干的 base64 url
  const url = getDataUrl(binding);

  // 创立 waterMark 父元素
  const waterMark = globalWaterMark || document.createElement("div");
  waterMark.className = `water-mark`; // 不便自定义展现后果
  waterMark.setAttribute("style", `background-image: url(${url});`);

  // 将对应图片的父容器作为定位元素
  parentElement.setAttribute("style", "position: relative;");

  // 将图片元素挪动到 waterMark 中
  parentElement.appendChild(waterMark);
};

const directives: any = {mounted(el: HTMLElement, binding: any) {el.onload = setWaterMark.bind(null, el, binding.value);
  },
};

export default {
  name: "watermark",
  directives,
};

/*********  css 局部  ***********/ 
.water-mark {
  display: inline-block;
  overflow: hidden;
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  background-repeat: repeat;
}

应用 MutationObserver 优化

以上提到的两种前端实现计划,都存在一个问题很显著的问题,那就是用于只有用户通过 开发者调试工具 来略微操作一,旧可能导致水印生效:

  • 删除对应 dom 节点
  • 设置对应 dom 节点的 css 款式

MutationObserver 接口提供对 DOM 树监听的能力,它可能监听 DOM 树属性、节点自身、子节点等的变动,于是优化的思路就是应用 MutationObserver 去监听内部对应 water-mark 节点的操作,只有监听到了就从新渲染水印成果即可。

成果和代码

留神】这里最容易踩坑的点就是 MutationObserver 中的条件写得不正确的话会导致死循环.

/********* src/directives/waterMark.ts  ***********/ 

// 全局保留 canvas 和 div,防止反复创立(单例模式)const globalCanvas = null;
const globalWaterMark = null;

// watermark 款式
let style = `
display: block;
overflow: hidden;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-repeat: repeat;
pointer-events: none;`;

const getDataUrl = ({
  font = "16px normal",
  fillStyle = "rgba(180, 180, 180, 0.3)",
  textAlign,
  textBaseline,
  text = "请勿外传",
}) => {
  const rotate = -20;
  const canvas = globalCanvas || document.createElement("canvas");
  const ctx = canvas.getContext("2d"); // 获取画布上下文

  ctx.rotate((rotate * Math.PI) / 180);
  ctx.font = font;
  ctx.fillStyle = fillStyle;
  ctx.textAlign = textAlign || "left";
  ctx.textBaseline = textBaseline || "middle";
  ctx.fillText(text, canvas.width / 3, canvas.height / 2);

  return canvas.toDataURL("image/png");
};

const setWaterMark = (el: HTMLElement, binding: any = {}) => {const { parentElement} = el;

  // 获取对应的 canvas 画布相干的 base64 url
  const url = getDataUrl(binding);

  // 创立 waterMark 父元素
  const waterMark = globalWaterMark || document.createElement("div");
  waterMark.className = `water-mark`; // 不便自定义展现后果
  style = `${style}background-image: url(${url});`;
  waterMark.setAttribute("style", style);

  // 将对应图片的父容器作为定位元素
  parentElement.setAttribute("style", "position: relative;");

  // 将图片元素挪动到 waterMark 中
  parentElement.appendChild(waterMark);
};

// 监听 DOM 变动
const createObserver = (el: HTMLElement, binding: any) => {const waterMarkEl = el.parentElement.querySelector(".water-mark");

  const observer = new MutationObserver((mutationsList) => {if (mutationsList.length) {const { removedNodes, type, target} = mutationsList[0];
      const currStyle = waterMarkEl.getAttribute("style");

      // 证实被删除了
      if (removedNodes[0] === waterMarkEl) {observer.disconnect();
        init(el, binding);
      } else if (
        type === "attributes" &&
        target === waterMarkEl &&
        currStyle !== style
      ) {waterMarkEl.setAttribute("style", style);
      }
    }
  });

  observer.observe(el.parentElement, {
    childList: true,
    attributes: true,
    subtree: true,
  });
};

// 初始化
const init = (el: HTMLElement, binding: any = {}) => {
  // 设置水印
  setWaterMark(el, binding.value);
  // 启动监控
  createObserver(el, binding.value);
};

// 定义指令配置项
const directives: any = {mounted(el: HTMLElement, binding: any) {el.onload = init.bind(null, el, binding);
  },
};

export default {
  name: "watermark",
  directives,
};

最初

上述过程中咱们做了那么多优化,最终的后果看起来比拟还算是能够承受,但实际上前端的实现计划终归是不够完满的,比方有心人间接复制图片对应的地址怎么办?又或者是通过开发者调试工具抉择禁用 JavaScript 又怎么办呢?

因而,总结下来前端的实现计划是属于:防小人不防君子 的计划,不过也不用过于纠结这一点,毕竟 语雀 这样的网站连 MutationObserver 都没加呢 ~~

正文完
 0