使用 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
都没加呢 ~~