前言
前一段时间因为我的项目须要实现水印性能,于是去理解了相干的内容后,基于 Vue
的实现了一个 v-watermark
指令实现了对应的性能,其实整体内容并不简单!
那么接下来先来简略理解一些和 Vue
自定义指令相干的内容,作为前置常识的铺垫,而后在逐渐实现对应的性能。
vue 中的自定义指令
以下的内容其实简略理解即可,甚至能够间接跳过,遇到不意识的内容在回头来查看都是能够的,甚至能够间接查看 官网文档
是什么?
在 Vue3
中 复用代码 有如下三种形式:
- 组件:组件是次要的构建模块
- 组合式函数:合式函数则侧重于有状态的逻辑
- 自定义指令:次要是为了重用波及一般元素的底层 DOM 拜访的逻辑
因而,自定义指令自身也是一种代码逻辑复用的形式,只是着重点在 对底层 DOM 的拜访和操作 上。
指令钩子
在 Vue
、React
、Webpack
、Vite
等中都会存在着对应的 钩子,而这些 钩子 实质上就是一些在 特定机会 会被执行的 函数/办法 而已,那么在 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 实现水印成果(前端)
自定义指令钩子十分多,但实际上能应用到的不多,比方最罕用的就是 mounted
、updated
,在这咱们只须要通过 mounted
即可实现对应的性能,并且外围代码比较简单。
核心内容
- 创立一个
watermark
的DOM
节点,即div
元素,用于包裹对应的img
便于展现水印内容 在创立一个
waterbg
的DOM
节点,即div
元素- 将
waterbg
节点作为watermark
的 子节点,并进行 相对定位 保障waterbg
在 最上层显示 - 将对应的 水印标记 作为
waterbg
节点的 背景图片 展现 - 为
waterbg
节点设置pointer-events: none;
实现 点击穿透
- 将
- 将
watermark
节点通过insertBefore(...)
插入到img
标签的前一个地位 - 再将
img
标签挪动到watermark
节点节中,这样就保障了新创建的watermark
节点的地位肯定是在本来img
挂载的地位
成果和代码如下
// main.tsimport { createApp } from 'vue'import App from './App.vue'import directives from './directives'createApp(App) .use(directives) .mount('#app');// src/directives/index.tsimport type { App } from 'vue'import watermark from './waterMark'export default function installDirective(app: App) { app.directive(watermark.name, watermark.directives);} // src/directives/waterMark.tsimport 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
都没加呢 ~~