前端实现水印的两种方式(DOM和Canvas)

news2024/9/22 11:27:23

🐱 个人主页:不叫猫先生
🙋‍♂️ 作者简介:前端领域新星创作者、阿里云专家博主,专注于前端各领域技术,共同学习共同进步,一起加油呀!
💫系列专栏:vue3从入门到精通、TypeScript从入门到实践
📢 资料领取:前端进阶资料以及文中源码可以找我免费领取
🔥 前端学习交流:博主建立了一个前端交流群,汇集了各路大神,一起交流学习,期待你的加入!(文末有我wx或者私信)

目录

  • 前言
  • 一、vue自定义指令directive讲解
  • 二、基于DOM的实现方式
    • 1. 思路整理
    • 2. 新建`directives`文件
    • 3. 在`directives`文件下创建 `index.ts`文件
    • 4. 在`main.ts`中全局引入
    • 5. 缺点
  • 三、基于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. 新建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
}

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

import type { App } from 'vue'
import watermark from './waterMark'

export default function installDirective(app: App) {
    app.directive(watermark.name, watermark.directives);
} 

4. 在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');

5. 缺点

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

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

1. 思路整理

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

2. 生成水印

通过将图片绘制在cavans中,然后通过cavanstoDataURL方法,将图片转为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);
  },
};

四、成果展示

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

<script setup lang="ts">
import { ref } from "vue";
</script>
<template>
    <div class="index-content" >
        <div class="watermaker" v-watermark ></div>
         <!-- <img v-watermark style="width:400px;height:400px" src="../assets/vue.svg" alt=""> -->
    </div>
</template>

<style scoped>
.watermaker {
    width: 400px;
    height: 400px;
}
.index-content{
    width: 100%;
    height: 100%;
}
</style>

在这里插入图片描述

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

toDataURL用法

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/182136.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python 海象运算符

✅作者简介&#xff1a;人工智能专业本科在读&#xff0c;喜欢计算机与编程&#xff0c;写博客记录自己的学习历程。 &#x1f34e;个人网站&#xff1a;小嗷犬的技术小站 &#x1f34a;个人信条&#xff1a;为天地立心&#xff0c;为生民立命&#xff0c;为往圣继绝学&#xf…

图纸版本混乱?BOM表管理困难?看SolidWorks PLM如何高效助力产品数据管理

“随着集团的日益壮大&#xff0c;我们越来越重视信息化系统的建设工作&#xff0c;但在研发生产的过程中我们经常会遇到图纸版本混乱、数据查找不便的问题&#xff1b;特别是在产品设计好后&#xff0c;还需要花费很多时间手动整理BOM表&#xff0c;整理期间由于数据量太大&am…

2023年,对人工智能的思考与展望

近些年来&#xff0c;人工智能的话题一次次的冲上热榜&#xff0c;而在前段时间内&#xff0c;chatgpt以及midjourney又一次冲上了热搜&#xff0c;在海内外引起广泛的讨论&#xff0c;我个人在研究了近一个多月的技术文档和文献资料后&#xff0c;也对人工智能的未来有了很多的…

STC32G 单片机通用串行通信接口及其编程

STC32G 系列单片机有4个全双工通用串行通信接口&#xff0c;串口1与串口2既有异步通信功能、又有同步通信功能&#xff0c;串口1与串口2都可进行SPI通信&#xff0c;SPI是一个全双工高速同步串行&#xff1b;通信总线串口3、串口4只有异步通信功能。本文将重点讨论其异步通信&a…

hexo个人博客搭建+butterfly主题配置(雏形版本)

前提&#xff1a; 1. 有一个属于自己的GitHub账号 2. 安装好了git 3. 安装好了node 一、安装hexo 1. 建立一个文件夹 Blog&#xff08;可以自己取名字&#xff09;&#xff0c;进入文件夹标右键打开 Git Bush Here&#xff0c;安装Hexo&#xff1a; npm install -g hexo-…

中智股份冲刺上交所上市:半年收入约87亿元,计划募资37亿元

近日&#xff0c;中智经济技术合作股份有限公司&#xff08;下称“中智股份”&#xff09;预披露招股书&#xff0c;准备在上海证券交易所主板上市&#xff0c;中信证券为其保荐机构。 本次冲刺上市&#xff0c;中智经济计划募资37.42亿元&#xff0c;将用于中智人力资源研发运…

函数的连续性和间断点——“高等数学”

各位CSDN的uu们你们好呀&#xff0c;今天小雅兰的内容是高等数学中的函数的连续性和间断点&#xff0c;好的&#xff0c;那现在就让我们进入函数的连续性和间断点的世界吧 一、函数的连续性 1.函数增量 2.连续的定义 3.单侧连续 二、例题&#xff08;函数的连续性&#xff09; …

JDK动态代理CGLIB动态代理

代理模式 是通过代理对象访问目标对象&#xff0c;这样可以在目标对象基础上增强额外的功能&#xff0c;如添加权限&#xff0c;访问控制和审计等功能。 房产中介代替业主卖房 静态代理 静态代理中代理类与被代理类都需要实现同一个接口&#xff0c;这就说明我们的一个静态代…

MFC|Toolbox内控件简单介绍

参考&#xff1a; MFC控件工具箱 &#xff08;https://blog.csdn.net/Hubz131/article/details/77684910&#xff09; 对应工具的超链接是本人搜到认为较易理解的单个控件介绍。 Pointer&#xff1a;就是普通的鼠标&#xff0c;默认状态Button&#xff1a;按钮&#xff0c;用…

stm32f091芯片的学习总结

摘自芯片手册&#xff08;外加自己的思考&#xff09; 发现网上使用这种芯片的人较少或者说解释这种芯片的电路具体怎么画的人较少&#xff0c;本来想直接借鉴的&#xff0c;发现没有找到&#xff0c;于是我自己来写一篇。 一、概述 该芯片提供标准通信接口(两个i2c&#xf…

基于STM32的FreeRTOS开发(2)----Cube工程的FreeRTOS配置

为什么使用Cube进行FreeRTOS配置 STM32Cube是STMicroelectronics提供的一种软件工具&#xff0c;用于配置和生成STM32微控制器的固件。它提供了一个图形化用户界面&#xff0c;可以轻松配置微控制器的各种功能和外设&#xff0c;并生成初始化代码。使用Cube可以大大简化微控制…

[GNN] 图神经网络入门

GNN和GCN的入门公式一、GNN的计算二、GCN的计算跟随B站课程【GNN图神经网络最牛教程】学不会up直接下跪&#xff01;图神经网络快速入门教程&#xff08;GNN/GCN&#xff09;的笔记 一、GNN的计算 对于一个图来说&#xff0c;要更新它自身的特征&#xff0c;也要更新它邻接节点…

前端websocket劫持漏洞(CSWSH)

0x00 什么是ws劫持 在Websocket的业务中&#xff0c;其中常见的漏洞是ws劫持&#xff0c;全称为跨站点CSWSH(Cross-Site WebSocket Hijacking)跨站WebSocket劫持漏洞。 WebSocket概念 WebSocket是通过HTTP启动的双向、全双工通信协议。它们通常用于流式传输数据和其他异步流量…

深度卷积神经网络、池化层、为什么使用卷积

目录1.深度卷积神经网络(a deep convolutional neural network)输入图像的维度是&#xff0c;如果&#xff0c;计算输出图像维度公式&#xff1a;。s表示步幅&#xff0c;p表示填充的层数。filters的通道数是和输入图像的通道数保持一致的。分析上图案例&#xff1a;第一层卷积…

MySQL基本查询案例练习

目录 一.案例1 需求 解决代码 二.案例2 需求 解决代码 一.案例1 创建一个学生表&#xff0c;插入以下数据 insert into student values(1,张明,男,89,78,90), (2,李静,男,77,73,60), …

golang map原理

简介本文主要通过探究在golang 中map的数据结构及源码实现来学习和了解map的特性&#xff0c;共包含map的模型探究、存取、扩容等内容。欢迎大家共同讨论。Map 的底层内存模型在 goland 的源码中表示 map 的底层 struct 是 hmap&#xff0c;其是 hashmap 的缩写type hmap struc…

“华为杯”研究生数学建模竞赛2005年-【华为杯】A题:交通网络的通行时间预测与最优路径决策(附获奖论文)

赛题描述 A: Highway Traveling time Estimate and Optimal Routing Ⅰ Highway traveling time estimate is crucial to travelers. Hence, detectors are mounted on some of the US highways. For instance, detectors are mounted on every two-way six-lane highways o…

树与二叉树深度剖析(一)

一. 树简介 1. 定义 (1) 树结构是一种非线性存储结构&#xff0c;存储的是具有“一对多”关系的数据元素的集合。 (2) 树&#xff08;Tree&#xff09;是n(n≥0)个节点&#xff08;Node&#xff09;的有限集合。在任意一颗非空树中&#xff0c;有且仅有一个特定的成为根(Root)…

【快速幂】876. 快速幂求逆元

876. 快速幂求逆元 文章目录题目描述输入格式&#xff1a;输出格式&#xff1a;数据范围输入样例输出样例方法&#xff1a;快速幂解题思路代码复杂度分析&#xff1a;题目描述 给定 n 组 ai,pia_i,p_iai​,pi​&#xff0c;其中 pip_ipi​ 是质数&#xff0c;求 aia_iai​ 模 …

MySQL 8.0.31中使用MySQL Workbench提示配置文件错误信息

MySQL 8.0.31中使用MySQL Workbench提示配置文件错误信息 Error opening configuration file UnicodeDecodeError:‘gbk’ coded can’t decode byte 0x92 in position 5004: illegal multibyte sequence 配置文件之前安装MySQL Server的时候编码格式好像改了, 才使的MySQL W…