携手创造,一起成长!这是我参加「日新计划 8 月更文应战」的第4天,点击检查活动详情。

前语

前一段时间由于项目需求完结水印功用,于是去了解了相关的内容后,根据 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:之前的旧值,仅在beforeUpdateupdated中可用,不管值是否更改都可用
    • arg:传递给指令的参数 (如果有的话),如在v-my-directive:foo中,参数是"foo"
    • modifiers:一个包括修饰符的目标 (如果有的话),如在v-my-directive.foo.bar中,修饰符目标是{ foo: true, bar: true }
    • instance:运用该指令的组件实例
    • dir:指令的界说目标
  • vnode:代表绑定元素的底层 VNode
  • prevNode:之前的渲染中代表指令所绑定元素的 VNode,仅在beforeUpdateupdated钩子中可用

留意】除了 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 都没加呢 ~~

你知道前端水印功用是怎样完结的吗?

往期文章引荐

  • 2022 你还不会微前端吗 (下) — 揭秘微前端中心原理
  • 2022 你还不会微前端吗 (上) — 从巨石应用到微应用
  • 听说你很了解 Vue3 响应式?
  • 不运用第三方库怎样完结【前端引导页】功用?
  • 前端功用优化到底该怎样做(上)— 开门见山
  • 前端功用优化到底该怎样做(下)— 直捣黄龙
  • 给我完结一个前端的 Excel 导入和导出功用