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