当前位置: 首页 > Web前端 > HTML

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

时间:2023-03-28 00:52:43 HTML

前言前段时间因为项目需要实现水印功能,在了解了相关内容后,基于Vue实现了一个v-watermark命令来完成相应的功能。其实整体内容并不复杂!那么我们先简单了解一下Vue的自定义命令相关的一些内容,作为前置知识的基础,然后逐步完成相应的功能。vue中自定义说明下面的内容其实很容易理解,甚至可以直接跳过。遇到不懂的可以稍后查,甚至可以直接查。官方文件是什么?Vue3中有三种代码复用方式:组件:组件是主要的构建块复合函数:格式良好的函数关注有状态逻辑自定义指令:主要是复用涉及底层DOM访问公共元素的逻辑因此,自定义指令本身是也是一种代码逻辑复用的方式,只关注底层DOM的访问和操作。指令钩子在Vue、React、Webpack、Vite等中都有对应的钩子,而这些钩子本质上是在特定的时间会执行的函数/方法,所以Vue自定义指令中存在如下钩子:created(el,binding,vnode,prevVnode):在绑定元素的属性之前调用,或者在应用事件监听器之前调用:在绑定元素的父组件及其所有子节点挂载后调用beforeUpdate(el,binding,vnode,prevVnode):在绑定元素的父组件更新之前调用updated(el,binding,vnode,prevVnode):在绑定元素的父组件及其所有子节点更新后调用beforeUnmount(el,binding,vnode,prevVnode):callunmounted(el,绑定在绑定元素的父组件被卸载前,vnode,prevVnode):调用之后绑定元素的父组件被卸载[注意]以上hooks与vue2中的自定义hooks有些不同,可以点击相应链接对比查看。钩子参数其实直接就是指参数命名方式,相信大家也能知道大部分参数代表什么:el:指令绑定的元素,也就是说可以直接通过el操作DOM。binding:一个包含以下属性的对象value:传递给指令的值,例如在my-directive="1+1",值为2oldValue:之前的旧值,只在beforeUpdate和updated时可用,不管值是否改变arg:传递给指令的参数(如果有),如在v-my-directive:foo中,参数为“foo”modifiers:包含修饰符(如果有)的对象,如在v-my-directive.foo.bar,修饰符对象为{foo:true,bar:true}instance:使用该指令的组件实例dir:指令的定义对象vnode:表示绑定指定元素的底层VNodeprevNode:代表上一次渲染中指令绑定的元素的VNode,只在beforeUpdate和updatedhooks中可用【注】除了el(因为需要操作DOM),其他参数都是读取的-only,不建议更改,如果需要在不同的hook之间共享信息,建议通过元素的dataset属性实现watermark功能。几种实现方案都是根据原图(后台)生成水印图片。该方案是对原图加水印生成新图,后续在前端页面显示为后台界面不返回原图,而是返回带水印的图片。这种方式最大的好处就是安全,因为水印图片是后台生成的,前端只需要负责显示即可,不需要考虑多余的问题,即使对应的图片保存在前端页面,得到的图片还是不是原图。有很多基于DOM的自定义指令钩子实现水印效果(前端),但实际上能用的并不多。比如最常用的挂载和更新。这里我们只需要使用mounted来实现相应的功能,核心代码比较简单。核心内容创建了一个水印DOM节点,即一个div元素,用来包裹相应的img来显示水印内容。创建一个waterbgDOM节点,即div元素将waterbg节点作为watermark的子节点,进行绝对定位,保证waterbg在最上面显示显示对应watermark作为waterbg的背景图节点。设置指针事件:无;为waterbg节点实现点击。通过insertBefore(...)将watermark节点插入到img标签之前的位置,然后将插入的img标签移动到watermark节点部分,这样可以保证新创建的watermark节点一定是在原来的img挂载位置.效果和代码如下#app');//src/directives/index.tsimporttype{App}from'vue'importwatermarkfrom'./waterMark'exportdefaultfunctioninstallDirective(app:App){app.directive(watermark.name,watermark.指令);}//src/directives/waterMark.tsimportwaterBgImgfrom'../assets/water-bg.png'const指令:any={mounted(el:HTMLElement){el.onload=()=>{const{clientWidth,clientHeight,parentElement}=el;常量erMark:HTMLElement=document.createElement('div');constwaterBg: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;背景图像:url(${waterBgImg});背景重复:重复;`);//为waterMark添加对应的子元素waterMark.appendChild(waterBg);//将waterMark插入相应位置parentElement?.insertBefore(waterMark,el);//将图片元素移动到水印上waterMark.appendChild(el);}}}exportdefault{name:'watermark',directives}优化的实现在上面的实现中,其实至少有两点可以优化:所有的样式都以字符串的形式直接出现在JavaScript代码中,对应的静态样式可以在css中处理,由.water-mark管理。创建了冗余的waterBg。其实不需要单独创建这个节点元素。你可以直接根据.water-mark对应节点的伪类元素优化核心代码如下:/********src/directives/waterMark.ts*************/const指令:any={mounted(el:HTMLElement){el.onload=()=>{const{clientWidth,clientHeight,parentElement}=el;constwaterMark:HTMLElement=document.createElement("div");//创建waterMark父元素waterMark.setAttribute("style",`width:${clientWidth}px;height:${clientHeight}px;`);waterMark.className=`水印`;//方便自定义显示结果//将WaterMark插入相应位置parentElement?.insertBefore(waterMark,el);//将图像元素移动到水印中waterMark.appendChild(el);};},};exportdefault{name:"水印",directives,};/*********css部分代码如下************/.water-mark{display:inline-block;overflow:隐藏;位置:相对;}.water-mark::after{指针事件:无;位置:绝对;内容:'';宽度:100%;高度:100%;不透明度:0.2;背景图像:url("../assets/water-bg.png");background-repeat:repeat;}基于Canvas实现水印效果(前端)基于Canvas实现的优点是可以动态设置水印内容,相比于前面那种基于固定背景图片的方法更加灵活,这种方法也是语雀使用的方法,具体效果如下:核心步骤是通过canvas填充文字,并获取对应的base64格式图片通过canvas.toDataURL("image/png");格式中的图片作为类名水印节点的背景图片,使用background-repeat:repeat;让这张图片重复填充背景和足尖r-events:水印节点无;实现点击通过并使用对应图片的父元素作为水印节点的相对定位节点,保证绝对定位的水印节点显式显示在对应图片上。效果及代码如下/********src/directives/waterMark.ts************///全局保存canvas和div,避免重复创建(单例模式)constglobalCanvas=null;constglobalWaterMark=null;//获取toDataURL的结果constgetDataUrl=({font="16pxnormal",fillStyle="rgba(180,180,180,0.3)",textAlign,textBaseline,text="不扩散",})=>{constrotate=-20;constcanvas=globalCanvas||document.createElement("画布");constctx=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);returncanvas.toDataURL("image/png");};//设置水印constsetWaterMark=(el:HTMLElement,binding:any)=>{const{parentElement}=el;//获取对应画布相关的base64url??consturl=getDataUrl(binding);//创建waterMark父元素constwaterMark=globalWaterMark||document.createElement("div");waterMark.className=`water-mark`;//方便自定义显示结果waterMark.setAttribute("style",`background-image:url(${url});`);//使用对应图片的父容器作为定位元素parentElement.setAttribute("style","position:relative;");//将图片元素移动到waterMarkparentElement.appendChild(waterMark);};constdirectives:any={mounted(el:HTMLElement,binding:any){el.onload=setWaterMark.bind(null,el,binding.value);},};exportdefault{name:"watermark",directives,};/********csssection*************/.water-mark{display:inline-堵塞;溢出:隐藏;位置:绝对;左:0;顶部:0;宽度:100%;高度:100%;事件:无;background-repeat:repeat;}使用MutationObserver优化上面提到的两个前端实现,有一个很明显的问题,就是用户只要通过开发者调试工具稍微操作一下,旧的就会导致水印失效:删除对应的dom节点,设置dom节点对应的css样式的MutationObserver接口,提供监控DOM树的能力。它可以监控DOM树属性、节点本身和子节点的变化。监听水印节点对应的外部操作,只要监听到就重新渲染水印效果效果及代码【注意】这里最容易踩到的是如果MutationObserver中的条件写错了,会导致死循环。/*********src/directives/waterMark.ts**********///全局保存canvas和div,避免重复创建(单例模式)constglobalCanvas=null;constglobalWaterMark=null;//watermarkstyleletstyle=`display:block;overflow:hidden;position:absolute;left:0;top:0;width:100%;height:100%;background-repeat:repeat;pointer-events:none;`;constgetDataUrl=({font="16pxnormal",fillStyle="rgba(180,180,180,0.3)",textAlign,textBaseline,text="不扩散",})=>{constrotate=-20;constcanvas=globalCanvas||document.createElement("画布");constctx=canvas.getContext("2d");//获取画布上下文ctx.rotate((rotate*Math.PI)/180);ctx.font=字体;ctx.fillStyle=fillStyle;ctx.textAlign=文本对齐||“左边”;ctx.textBaseline=文本基线||“中间”;ctx.fillText(文本,canvas.width/3,canvas.height/2);返回canvas.toDataURL("image/png");};constsetWaterMark=(el:HTMLElement,binding:any={})=>{const{parentElement}=el;//获取对应画布相关的base64url??consturl=getDataUrl(binding);//创建waterMark父元素constwaterMark=globalWaterMark||文档.createElement("div");waterMark.className=`水印`;//方便自定义显示结果style=`${style}background-image:url(${url});`;waterMark.setAttribute("样式",样式);//使用对应图片的父容器作为定位元素parentElement.setAttribute("style","position:relative;");//将图像元素移动到waterMarkparentElement.appendChild(waterMark);};//监控DOM变化constcreateObserver=(el:HTMLElement,binding:any)=>{constwaterMarkEl=el.parentElement.querySelector(".water-标记”);constobserver=newMutationObserver((mutationsList)=>{if(mutationsList.length){const{removedNodes,type,target}=mutationsList[0];constcurrStyle=waterMarkEl.getAt致敬(“风格”);//proofremovedif(removedNodes[0]===waterMarkEl){observer.disconnect();初始化(el,绑定);}elseif(type==="attributes"&&target===waterMarkEl&&currStyle!==style){waterMarkEl.setAttribute("style",style);}}});observer.observe(el.parentElement,{childList:true,attributes:true,subtree:true,});};//初始化constinit=(el:HTMLElement,binding:any={})=>{//设置水印setWaterMark(el,binding.value);//开始监听createObserver(el,binding.value);};//定义指令配置项constdirectives:any={mounted(el:HTMLElement,binding:any){el.onload=init.bind(null,el,捆绑);},};exportdefault{name:"watermark",directives,};最后,我们在上面的过程中做了这么多的优化,最后的结果看起来还可以,但实际上前端的实现方案毕竟不完善,比如有兴趣的人直接复制图片怎么办地址?或者如果您选择通过开发??者调试工具禁用JavaScript怎么办?所以综上所述,前端实现方案属于君子不防小人的方案,但在这一点上没必要过于纠结。毕竟语雀这样的网站连MutationObserver都没有加~~