vue3中生成水印

tips: 文末有完整示例代码。

领导:『小S啊,我们有个新需求🥸,需要在预览的资源上添加水印,让服务端来加水印成本太高了,在前端渲染的时候把水印加上可以吗🤨?』

小S:『加水印啊,简简单单🤏。我们项目使用的是 Vue3,使用自定义指令一下就可以加好了。领导你看我操作!』

小S说着,就把生产力工具打开了。手速熟练🤠的启动了项目。

小S:『领导你看😈,我先在项目自定义指令的文件夹下新建一个自定义水印指令文件 - watermark.ts。在需要添加水印的目标Dom 挂载时,创建一个 canvas 节点,canvas 的宽高自然要跟 Dom 的大小一样啦,层级也必须是最高的。然后我再给 canvas 里画上水印内容,最后再给 canvas 挂载到目标节点。当然啦,目标节点销毁时也要把 canvas 销毁掉。』

小S一边讲,一边就在生产力工具中敲🫳出了代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import type { Directive, App } from 'vue';
import { nextTick } from 'vue';

const watermarkDirective: Directive = {
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
async mounted(el, binding) {
await createWatermark(el, binding.value.text);
},
async updated(el, binding) {
// 检查是否需要更新水印内容
await updateWatermark(el, binding.value?.text || '');
},
// 绑定元素的父组件卸载后调用
unmounted(el) {
removeWatermark(el);
},
};

/** 创建水印 */
async function createWatermark(el, text: string) {
const canvasEl = document.createElement('canvas');
const newCanvas = !el.querySelector('canvas');

canvasEl.id = 'watermark-canvas';
canvasEl.style.position = 'absolute';
canvasEl.style.top = '0';
canvasEl.style.left = '0';
canvasEl.style.zIndex = '99';
canvasEl.style.pointerEvents = 'none';
el.appendChild(canvasEl);
canvasEl.width = window.screen.width;
canvasEl.height = window.screen.height;
const ctx = canvasEl.getContext('2d');
ctx.rotate((-20 * Math.PI) / 180); //旋转角度
ctx.font = '24px serif';
ctx.fillStyle = 'rgba(180, 180, 180, 0.3)';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
for (let i = -canvasEl.width / 100; i < canvasEl.width / 100; i++) {
for (let j = -canvasEl.height / 200; j < canvasEl.height / 200; j++) {
ctx.fillText(text, i * 300, j * 300);
}
}
}

async function removeWatermark(el) {
await nextTick();
const canvasEl = el.querySelector('#watermark-canvas');
if (canvasEl) {
canvasEl.remove();
}
}

export default watermarkDirective;

小S得意😏的抖着腿,侧身向领导讲到:『这样就可以生成水印啦! 撒花🥳🥳🥳』。

领导🫲🫱:『你这样是可以实现了,但是也仅仅可以防一下小白,稍微懂点前端知识的人,都可以 F12 把控制台打开,选中水印节点,给它哐哐哐删掉。』

小S听了,一拍脑门:『是哦,我怎么没想到呢!嗯……』小S陷入了沉思,如何防止被删掉呢?小S脑子转了3圈后:『领导,我知道怎么做了!DOM3 Event 规范中有一个 MutationObserver,这个接口可以监视 DOM 进行监视,只要我的水印被删掉了,我就赶紧再生成一个水印!』

小S立刻转身,一边思索🤔着逻辑,一边在生产力工具中继续完善:

小S心里想到:『在目标节点挂载,首次添加 canvas 时,我给目标节点添加 MutationObserver 监听,并把实例化的监视器放在目标节点的自定义属性上,监听它的子节点,如果监听到子节点水印被删除,我就再新建一个水印 canvas,插入到目标节点中,对了,还要考虑到我主动删除水印的操作。水印节点也要加监视,不然手动改一下水印的CSS样式,就可以把水印给隐藏掉了。emmm……最后在 目标节点卸载时把监听移除掉。』

小S搞好了,转身给领导讲道:『领导,搞定了!使用的时候只需要引入自定义指令,在需要加水印的节点添加参数就可以啦』

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div v-watermark="watermarkOption">
<img src="xxxx">
</div>
</template>

<script setup lang="ts">
// @ts-ignore
import vWatermark from '/@/directives/watermark';

const watermarkOption = {
text: '小S水印'
}
</script>

领导看着小S加好水印,笑😼着说:『针不错,这就去给你涨工资!』

小S听了,连忙摇头🙀道:『领导,不用,不用,这都是前端切图仔的基本功!』


两周后


领导🥸来到了小S工位前,『啪啪啪!』领导拍了拍小S的办公桌。

小S一个凌冽,急忙😨问道:『领导怎么了,又出 Bug 了吗?』

领导:『哼哼,你看看你的掘金评论区,人家已经把你的水印破解了!你得赶紧修了,修不好就扣你工资!』领导说完就↪转身↩回了办公室。

小S急忙打开掘金,看着评论📖,一脸苦涩,『他们怎么这样啊,都加水印了,还要给水印样式中的 opacity 设成 0,还要把 display 设置为 none。这样是吧?那大家一起掉头发呗。』小S一边吐槽🤬着,一边又打开了二周前的代码。

小S思索着:『既然你加 CSS 属性,那就会触发 canvasCheckWatermark 事件,我在这里边把 CSS 样式复原就行了。嘿嘿。』

小S完善着代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 略略略
async function createWatermark(el, text: string) {
// 略略略
canvasEl.style.position = 'absolute';
canvasEl.style.top = '0';
canvasEl.style.left = '0';
canvasEl.style.zIndex = '99';
canvasEl.style.pointerEvents = 'none';

// 可能造成水印消失的样式, 这些样式在新增、更新水印时进行重置
const revertLs = ['display', 'opacity', 'visible', 'transform', 'clip-path']

revertLs.forEach((v) => {
canvasEl.style[v] = 'revert';
})

newCanvas && el.appendChild(canvasEl);
// 略略略
}
// 略略略

『搞定!display/opacity/visible/transform/clip-path 以及定位属性会导致水印被隐藏,定位属性已经强制设置过了,就不再重置了。其他都都设置为 revert,让我看看可以了不?』小S启动了项目,尝试通过 CSS 样式移除水印,发现防删代码已经生效了。『嘿嘿😏』。

小S看了看时间🕔,离下班还有30分钟,『得了,把更新水印的功能也加上!』

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 略略略
const watermarkDirective: Directive = {
// 略略略
async mounted(el, binding) {
await createWatermark(el, binding.value.text);
},
// 更新水印内容
async updated(el, binding) {
await updateWatermark(el, binding.value?.text || '');
}
// 略略略
};
async function createWatermark(el, text: string) {
const canvasEl = el.querySelector('#watermark-canvas') || document.createElement('canvas');
const newCanvas = !el.querySelector('#watermark-canvas');
// 将水印文本存储到元素上
canvasEl.dataset.rendText = text;
// 略略略
}

// 更新水印
async function updateWatermark(el, text: string) {
const canvasEl = el.querySelector('#watermark-canvas');
// 文案未变化,不需更新
if (canvasEl && canvasEl.dataset.rendText === text) return

if (canvasEl && canvasEl.dataset.rendText !== text) {
removeWatermark(el);
}
createWatermark(el, text);
}
// 略略略

小S:『搞定!还有5️⃣分钟下班,不用加班咯,刷刷⛏金,摸一会🐋就下班!』


END


完整示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import type { Directive, App } from 'vue';
import { nextTick } from 'vue';

const watermarkDirective: Directive = {
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
async mounted(el, binding) {
await createWatermark(el, binding.value.text);
},
// 更新水印内容
async updated(el, binding) {
await updateWatermark(el, binding.value?.text || '');
},
// 绑定元素的父组件卸载后调用
unmounted(el) {
removeWatermark(el);
},
};

async function createWatermark(el, text: string) {
const canvasEl = el.querySelector('canvas') || document.createElement('canvas');
const newCanvas = !el.querySelector('canvas');
// 将水印文本存储到元素上
canvasEl.dataset.rendText = text;

if (!el.dataset.mutationObserverParent) {
const mutationObserver = new MutationObserver((records) =>
parentCheckWatermark(records, el, text),
);
mutationObserver.observe(el, {
childList: true,
});
el.dataset.mutationObserverParent = mutationObserver;
}
canvasEl.id = 'watermark-canvas';
canvasEl.style.position = 'absolute';
canvasEl.style.top = '0';
canvasEl.style.left = '0';
canvasEl.style.zIndex = '99';
canvasEl.style.pointerEvents = 'none';

// 可能造成水印消失的样式, 这些样式在新增、更新水印时进行重置
const revertLs = ['display', 'opacity', 'visible', 'transform', 'clip-path']

revertLs.forEach((v) => {
canvasEl.style[v] = 'revert';
})

newCanvas && el.appendChild(canvasEl);
canvasEl.width = window.screen.width * 3;
canvasEl.height = window.screen.height * 3;
const ctx = canvasEl.getContext('2d');
if (!ctx) return;
ctx.rotate((-20 * Math.PI) / 180); //旋转角度
ctx.font = '24px serif';
ctx.fillStyle = 'rgba(180, 180, 180, 0.3)';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
for (let i = -canvasEl.width / 100; i < canvasEl.width / 100; i++) {
for (let j = -canvasEl.height / 200; j < canvasEl.height / 200; j++) {
ctx.fillText(text, i * 300, j * 300);
}
}

if (newCanvas) {
// 水印属性监听
const mutationObserver = new MutationObserver(() => canvasCheckWatermark(el, text));
mutationObserver.observe(canvasEl, {
attributes: true,
});
el.dataset.mutationObserverCanvas = mutationObserver;
}
}

// 更新水印
async function updateWatermark(el, text: string) {
const canvasEl = el.querySelector('#watermark-canvas');
// 文案未变化,不需更新
if (canvasEl && canvasEl.dataset.rendText === text) return

if (canvasEl && canvasEl.dataset.rendText !== text) {
removeWatermark(el);
}
createWatermark(el, text);
}

/** 检查水印是否被删除 */
async function parentCheckWatermark(records, el, text) {
// 主动删除水印不处理
if (el.dataset.focusRemove) return;
const removedNodes = records[0].removedNodes;
let hasDelWatermark = false;
removedNodes.forEach((el) => {
if (el.id === 'watermark-canvas') {
hasDelWatermark = true;
}
});
// 水印被删除了
hasDelWatermark && createWatermark(el, text);
}

/** 检查水印属性是否变化了 */
async function canvasCheckWatermark(el, text) {
// 防止多次触发
if (el.dataset.canvasRending) return;
el.dataset.canvasRending = 'rending';

// 水印canvas属性变化了,重新创建
await createWatermark(el, text);
el.dataset.canvasRending = '';
}

async function removeWatermark(el) {
el.dataset.focusRemove = true;
el.dataset.mutationObserverParent?.disconnect?.();
await nextTick();
const canvasEl = el.querySelector('#watermark-canvas');
if (canvasEl) {
canvasEl.dataset.mutationObserverCanvas?.disconnect?.();
canvasEl.remove();
}
}
export default watermarkDirective;

使用水印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div v-watermark="watermarkOption">
<img src="xxxx">
</div>
</template>

<script setup lang="ts">
// @ts-ignore
import vWatermark from '/@/directives/watermark';

const watermarkOption = {
text: '小S水印'
}
</script>