前端基于DOM或者Canvas实现页面水印

2024-03-01 1610阅读

温馨提示:这篇文章已超过388天没有更新,请注意相关的内容是否还可用!

🐱 个人主页:不叫猫先生

🙋‍♂️ 作者简介:前端领域新星创作者、阿里云专家博主,专注于前端各领域技术,共同学习共同进步,一起加油呀!

💫系列专栏:vue3从入门到精通、TypeScript从入门到实践

📢 资料领取:前端进阶资料以及文中源码可以找我免费领取

🔥 前端学习交流:博主建立了一个前端交流群,汇集了各路大神,一起交流学习,期待你的加入!(文末有我wx或者私信)

目录

  • 前言
  • 一、vue自定义指令directive讲解
  • 二、基于DOM的实现方式
    • 1. 思路整理
    • 2.新建index.vue
    • 3. 新建`directives`文件
    • 4. 在`directives`文件下创建 `index.ts`文件
    • 5. 在`main.ts`中全局引入
    • 6. 缺点
    • 三、基于Canvas和MutationObserver的实现方式
      • 1. 思路整理
      • 2. 生成水印
      • 3. 使用MutationObserver监听水印
      • 四、成果展示
      • 附:文中用到的js基础知识
        • toDataURL用法

          前言

          我们会看到很多页面带有水印,但是怎么实现呢?当然可以有多种实现方式,本文主要讲解在vue项目中基于DOM或者Cavans实现水印效果,当然还有其他的实现方式,比如在原图片的基础上加上水印生成新的图片,但是这需要后端处理。因为要在vue项目中使用,所以我使用自定义指令可以直接对挂载的dom实现水印效果。

          本文实现水印的项目环境为:vue + vite + ts

          一、vue自定义指令directive讲解

          前面专门有一篇讲解vue2.x与vue3.x中自定义指令详解

          二、基于DOM的实现方式

          1. 思路整理

          • 获取宽高

            (1)获取绑定元素的实际宽度clientWidth

            (2)获取绑定元素实际高度clientHeight

            (3)获取绑定元素的父元素parentElement

          • 创建盒子

            (1)创建一个包裹水印图片的盒子

            (2)创建一个水印图片的盒子

          • 设置盒子样式

            (1)包裹水印盒子宽高为绑定元素的宽高,即clientWidth、clientHeight

            (2)水印盒子设置背景图、旋转度、宽高、点击穿透

          • 设置创建的元素的位置

            (1)水印盒子放到包裹水印图片的盒子里 (包裹水印图片的盒子包裹水印)

            (2)包裹水印图片的盒子放到被绑定元素之前

            (3)被绑定元素放到裹水印图片的盒子里(不然被绑定元素与包裹水印图片的盒子层级同级)

            2.新建index.vue

            将水印的指令放到标签上,设置标签的宽高。水印可以放大div标签上,也可以是img标签上。注意:img才有onload方法,div标签么有。

            import { ref } from "vue";
            
            
                
            .watermaker { width: 400px; height: 400px; } .index-content{ width: 100%; height: 100%; }

            3. 新建directives文件

            在directives文件下创建waterMark.ts 文件,具体内容实现如下:

            import waterImg from "@/assets/vue.svg"
            const directives: any = {
            	mounted(el: HTMLElement) {
                    //如果el元素是img,则可以用el.onload将下面包裹
            		const { clientWidth, clientHeight, parentElement } = el;
            		console.log(parentElement, 'parentElement')
            		const waterMark: HTMLElement = document.createElement('div');
            		const waterBg: HTMLElement = document.createElement('div');
                  
                    //设置waterMark的class和style
            		waterMark.className = `water-mark`;
            		waterMark.setAttribute('style', `
                        display: inline-block;
                        overflow: hidden;
                        position: relative;
                        width: ${clientWidth}px; 
                        height: ${clientHeight}px;`);
            		// 创建waterBg的class和style
            		waterBg.className = `water-mark-bg`;// 方便自定义展示结果
            		waterBg.setAttribute('style', `
                        position: absolute;
                        pointer-events: none;`在这里插入代码片`
            			transform: rotate(45deg);
                        width: 100%;
                        height: 100%;
                        opacity: 0.2;
                        background-image: url(${waterImg}); 
                        background-repeat: repeat;
            			`);
            			
            		// 水印元素waterBg放到waterMark元素中
            		waterMark.appendChild(waterBg);
            		//waterMark插入到el之前,即插入到绑定元素之前
            		parentElement?.insertBefore(waterMark, el);
            		// 绑定元素移入到包裹水印的盒子
            		waterMark.appendChild(el);
            	}
            }
            export default {
            	name: 'watermark',
            	directives
            }
            

            4. 在directives文件下创建 index.ts文件

            import type { App } from 'vue'
            import watermark from './waterMark'
            export default function installDirective(app: App) {
                app.directive(watermark.name, watermark.directives);
            } 
            

            5. 在main.ts中全局引入

            import { createApp } from 'vue'
            import App from './App.vue'
            import directives from './directives'
            const app = createApp(App);
            app.use(directives);
            app.mount('#app');
            

            6. 缺点

            • 直接删除水印元素时,页面中的水印直接就被删除了,当然我们可以用MutationObserver对水印元素进行监听,删除时,我们再立即生成一个水印元素就可以了,具体方面在下面讲解。
            • 如果原始元素本身存在 css 定位等规则,会导致整体布局效果出现影响,因为上面实现排除了原始元素没有定位,所以实现方式不是很严谨,本文具体实现实现如下:
              • 创建一个水印的容器设置为 position:relative
              • 将原有的节点放入到这个容器中
              • 同时创建一个带有水印的 dom 设置为position:absolute ,实现这个水印元素覆盖到原始元素的上层,以实现水印的效果。

                三、基于Canvas和MutationObserver的实现方式

                1. 思路整理

                • 配置水印的具体样式(大小,旋转角度,文字填充)
                • 设置水印(位置)
                • 监听dom变化(防止水印删除后页面不再展示水印)

                  2. 生成水印

                  通过将图片绘制在cavans中,然后通过cavans的toDataURL方法,将图片转为base64编码。

                  // 全局保存 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 = -10;
                    const canvas = globalCanvas || document.createElement("canvas");
                    const ctx = canvas.getContext("2d"); // 获取画布上下文
                    ctx.rotate((rotate * Math.PI) / 180);
                    ctx.font = "16px normal";
                    ctx.fillStyle = "rgba(180, 180, 180, 0.3)";
                    ctx.textAlign = "left";
                    ctx.textBaseline = "middle";
                    ctx.fillText('请勿外传', canvas.width / 3, canvas.height / 2);
                    return canvas.toDataURL("image/png");
                  };
                  

                  3. 使用MutationObserver监听水印

                  使用MutationObserver监听dom变化,MutationObserver详细用法之前已经讲过了,详细可见作为前端你还不懂MutationObserver?那Out了

                  具体监听逻辑如下:

                  • 1.直接删除dom

                    (1)先获取设置水印的dom

                    (2)监听到被删除元素的dom

                    (3)如果他两相等的话就停止观察,初始化(设置水印+启动监控)

                  • 2.删除style中的属性

                    (1)判断删除的是否是标签的属性 (type === “attributes”)

                    (2)判断删除的标签属性是否是在设置水印的标签上

                    (3)判断修改过的style和之前的style对比,不等的话,重新赋值

                    // watermark 样式
                    let style = `
                    display: block;
                    overflow: hidden;
                    position: absolute;
                    left: 0;
                    top: 0;
                    width: 100%;
                    height: 100%;
                    background-repeat: repeat;
                    pointer-events: none;`;
                    //设置水印
                    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) => {
                      console.log(el, 'el')
                      console.log(style, 'style')
                      // console.log(el.parentElement.querySelector('.water-mark'),'el.parentElement')
                      const waterMarkEl = el.parentElement.querySelector(".water-mark");
                      const observer = new MutationObserver((mutationsList) => {
                        console.log(mutationsList, 'mutationsList')
                        if (mutationsList.length) {
                          const { removedNodes, type, target } = mutationsList[0];
                          const currStyle = waterMarkEl.getAttribute("style");
                          // console.log(currStyle, 'currStyle')
                          // 证明被删除了
                        //   (1)直接删除dom
                        //   1.先获取设置水印的dom
                        //   2.监听到被删除元素的dom
                        //   如果他两相等的话就停止观察,初始化(设置水印+启动监控)
                        //   (2) 删除style中的属性
                        //  1 判断删除的是否是标签的属性 (type === "attributes")
                        //  2.判断删除的标签属性是否是在设置水印的标签上
                        //  3.判断修改过的style和之前的style对比,不等的话,重新赋值
                          if (removedNodes[0] === waterMarkEl) {
                            console.log(removedNodes[0])
                            // 停止观察。调用该方法后,DOM 再发生变动,也不会触发观察器。
                            observer.disconnect();
                            //初始化(设置水印,启动监控)
                            init(el, binding);
                          } else if (
                            type === "attributes" &&
                            target === waterMarkEl &&
                            currStyle !== style
                          ) {
                            console.log(currStyle, 'currStyle')
                            console.log(style, '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) {
                      //注意img有onload的方法,如果自定义指令注册在html标签的话,只需要init(el, binding.value)
                        el.onload = init.bind(null, el, binding.value);
                      },
                    };
                    

                    四、成果展示

                    删除水印标签依然还在,除非删除水印注册的标签才能删除水印,但是这样做毫无意义,因为这样做内容也会全部删除掉。

                    前端基于DOM或者Canvas实现页面水印

                    附:文中用到的js基础知识

                    toDataURL用法

                    toDataURL(type, encoderOptions),接收两个参数:

                    • type:图片类型,比如image/png、image/jpeg、image/webp等等,默认为image/png格式
                    • encoderOptions:图片质量的取值范围(0-1),默认值为0.92,当超出界限按默认值0.92
VPS购买请点击我

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

目录[+]