前言

我们会看到很多页面带有水印,但是怎么实现呢?当然可以有多种实现方式,本文主要讲解在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​​标签么有。

<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>

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