Vue3問題:如何在頁面上添加水???
1. 需求分析
為了防止網(wǎng)站信息被盜用,以及維護(hù)版權(quán)標(biāo)識(shí),常常需要在頁面、圖片或視頻上添加獨(dú)特水印,以作區(qū)分。
同時(shí),水印的添加不僅僅滿足于添加,有時(shí)候還要能防止用戶惡意篡改,時(shí)刻保證水印的功效。
所以,這次問題我分為了兩種情況:一種是僅添加水印僅可,另一種是添加水印且要防篡改。
下面我將把實(shí)現(xiàn)一一列出。
2. 實(shí)現(xiàn)步驟
(1)僅添加水印情況
對(duì)于僅需要添加水印的情況,直接使用第三方UI庫中的水印組件即可,簡(jiǎn)單快速。
當(dāng)然,我們也可以選擇自己造輪子,用Canvas來畫,但是對(duì)于工作而言,我覺得這樣應(yīng)該盡量避免。
這里我使用ElementPlus 2.4.0中,新出的Watermark水印組件作為例子。
實(shí)現(xiàn)代碼:
<template>
<el-watermark
:width="130"
:height="30"
image="https://element-plus.org/images/element-plus-logo.svg"
>
<div style="height: 500px" />
</el-watermark>
</template>
效果如下:
當(dāng)然要注意的是,ElementPlus的依賴版本一定要是2.4.0之后的。
(2)添加水印且要防篡改情況
像ElementPlus提供的水印組件,是不支持防篡改功能的。
也就是說,如果有用戶通過瀏覽器的控制臺(tái)進(jìn)行元素屬性的修改,是可以把頁面中的水印隱藏的。所以為了安全起見,是很需要做防篡改處理的。
為了保證自定義水印的靈活性,這里我使用了原生js的寫法,并且代碼參考了渡一官方大佬的文章。
簡(jiǎn)言之,就是利用Canvas繪制水印圖像,以及利用MutationObserver對(duì)象來監(jiān)聽Dom節(jié)點(diǎn)或其子節(jié)點(diǎn)的變化以實(shí)現(xiàn)防篡改處理。
代碼實(shí)現(xiàn)如下:
先寫一個(gè)hook函數(shù)useWatermarkBg,在其中用Canvas繪制水印圖像。
import { computed } from 'vue';
export default function useWatermarkBg (props) {
return computed(() => {
// 創(chuàng)建一個(gè) canvas
const canvas = document.createElement('canvas');
const devicePixelRatio = window.devicePixelRatio || 1;
// 設(shè)置字體大小
const fontSize = props.fontSize * devicePixelRatio;
const font = fontSize + 'px serif';
const ctx = canvas.getContext('2d');
// 獲取文字寬度
ctx.font = font;
const { width } = ctx.measureText(props.text);
const canvasSize = Math.max(100, width) + props.gap * devicePixelRatio;
canvas.width = canvasSize;
canvas.height = canvasSize;
ctx.translate(canvas.width / 2, canvas.height / 2);
// 旋轉(zhuǎn) 45 度讓文字變傾斜
ctx.rotate((Math.PI / 180) * -45);
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.font = font;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 將文字畫出來
ctx.fillText(props.text, 0, 0);
return {
base64: canvas.toDataURL(),
size: canvasSize,
styleSize: canvasSize / devicePixelRatio,
};
});
}
再封裝一個(gè)水印公共組件WaterMark,在其中調(diào)用useWatermarkBg函數(shù)生成水印圖像,以及添加水印、做防篡改處理。
在mounted中,創(chuàng)建MutationObserver實(shí)例,監(jiān)聽水印DOM節(jié)點(diǎn)的變化,在節(jié)點(diǎn)刪除或?qū)傩孕薷臅r(shí)設(shè)置依賴,發(fā)出重新添加水印的通知。
在watchEffect中,進(jìn)行收集依賴,只要依賴變化了,它就會(huì)重新添加水印圖像,達(dá)到防篡改效果。
值得一提的是,因?yàn)樘砑铀〉脑硎墙o頁面添加一個(gè)絕對(duì)定位的重復(fù)水印背景的div,但是,如果這樣我們就不能點(diǎn)擊div下層的元素了。
所以,這里還用了一個(gè)叫pointerEvents的css屬性,設(shè)置值為none,從而使元素不會(huì)接收鼠標(biāo)事件,鼠標(biāo)事件會(huì)透過元素傳遞到下層的元素上。
<template>
<div class="watermark-container" ref="parentRef">
<slot></slot>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, watchEffect } from 'vue';
import useWatermarkBg from '@/hooks/useWatermarkBg.js';
const props = defineProps({
text: {
type: String,
required: true,
default: 'watermark',
},
fontSize: {
type: Number,
default: 40,
},
gap: {
type: Number,
default: 20,
},
});
const bg = useWatermarkBg(props);
const parentRef = ref(null);
const flag = ref(0); // 聲明一個(gè)依賴
let div;
watchEffect(() => {
flag.value; // 將依賴放在 watchEffect 里
if (!parentRef.value) {
return;
}
if (div) {
div.remove();
}
const { base64, styleSize } = bg.value;
div = document.createElement('div');
div.style.backgroundImage = `url(${base64})`;
div.style.backgroundSize = `${styleSize}px ${styleSize}px`;
div.style.backgroundRepeat = 'repeat';
div.style.zIndex = 9999;
div.style.position = 'absolute';
div.style.inset = 0;
// 元素不會(huì)接收鼠標(biāo)事件,鼠標(biāo)事件會(huì)透過元素傳遞到下層的元素上
div.style.pointerEvents = 'none';
parentRef.value.appendChild(div);
});
// 防篡改處理
let ob;
onMounted(() => {
ob = new MutationObserver((records) => {
for (const record of records) {
for (const dom of record.removedNodes) {
if (dom === div) {
flag.value++; // 刪除節(jié)點(diǎn)的時(shí)候更新依賴
return;
}
}
if (record.target === div) {
flag.value++; // 修改屬性的時(shí)候更新依賴
return;
}
}
});
ob.observe(parentRef.value, {
childList: true,
attributes: true,
subtree: true,
});
});
onUnmounted(() => {
ob && ob.disconnect();
div = null;
});
</script>
最后,在需要添加水印的頁面直接使用即可。
<template>
<water-mark>
<video src="@/assets/a.mp4" controls width="500" height="500"></video>
</water-mark>
</template>
<script setup>
import WaterMark from "@/components/WaterMark.vue"
</script>
3. 問題詳解
(1)關(guān)于MutationObserver總結(jié)
MutationObserver 是 JavaScript 中的一個(gè)內(nèi)置對(duì)象,它提供了一種監(jiān)視 DOM(文檔對(duì)象模型)樹變化的能力。
MutationObserver 允許開發(fā)者注冊(cè)一個(gè)回調(diào)函數(shù),當(dāng)觀察的 DOM 節(jié)點(diǎn)或子節(jié)點(diǎn)發(fā)生變化時(shí),會(huì)觸發(fā)這個(gè)回調(diào)函數(shù)。這些變化可以包括節(jié)點(diǎn)的添加、移除、屬性的變化、文本內(nèi)容的改變等。
使用 MutationObserver 可以監(jiān)視特定的 DOM 元素或整個(gè)文檔,并在相關(guān)變化發(fā)生時(shí)執(zhí)行相應(yīng)的操作。這對(duì)于實(shí)時(shí)監(jiān)測(cè)頁面變化、自動(dòng)化測(cè)試、實(shí)現(xiàn)響應(yīng)式 UI 等場(chǎng)景非常有用。
下面是一個(gè)簡(jiǎn)單的示例,演示如何使用 MutationObserver 監(jiān)測(cè)某個(gè)元素的子節(jié)點(diǎn)變化:
// 目標(biāo)元素
var targetElement = document.getElementById('target-element');
// 創(chuàng)建一個(gè) MutationObserver 實(shí)例
var observer = new MutationObserver(function(mutationsList, observer) {
// 當(dāng)變化發(fā)生時(shí)執(zhí)行的回調(diào)函數(shù)
for (var mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('子節(jié)點(diǎn)發(fā)生變化');
console.log(mutation.addedNodes); // 添加的節(jié)點(diǎn)列表
console.log(mutation.removedNodes); // 移除的節(jié)點(diǎn)列表
}
}
});
// 配置觀察選項(xiàng)
var config = { childList: true };
// 開始觀察目標(biāo)元素
observer.observe(targetElement, config);
在上述示例中,我們首先通過 getElementById 獲取目標(biāo)元素,然后創(chuàng)建一個(gè) MutationObserver 實(shí)例,傳入一個(gè)回調(diào)函數(shù)作為參數(shù)?;卣{(diào)函數(shù)會(huì)在目標(biāo)元素的子節(jié)點(diǎn)發(fā)生變化時(shí)被調(diào)用。我們可以在回調(diào)函數(shù)中根據(jù) mutationsList 的內(nèi)容進(jìn)行相應(yīng)的處理。在這個(gè)示例中,我們只關(guān)注子節(jié)點(diǎn)的變化,并打印相關(guān)信息到控制臺(tái)。
最后,我們通過調(diào)用 observe 方法來開始觀察目標(biāo)元素的變化。在 config 對(duì)象中,我們將 childList 屬性設(shè)置為 true,表示我們要監(jiān)測(cè)子節(jié)點(diǎn)的變化。
需要注意的是,MutationObserver 是一個(gè)異步的機(jī)制,它會(huì)在變化發(fā)生后才觸發(fā)回調(diào)函數(shù)。這意味著在開始觀察之前的變化可能不會(huì)被捕獲到。如果需要在開始觀察之前立即獲取當(dāng)前狀態(tài)的變化,可以在創(chuàng)建 MutationObserver 實(shí)例后,使用 observer.takeRecords() 方法來獲取當(dāng)前的變化記錄。
(2)關(guān)于pointer-events的總結(jié)
pointer-events 是一種 CSS 屬性,它控制元素對(duì)鼠標(biāo)事件的響應(yīng)方式。它可以設(shè)置在任何 HTML 元素上,并具有以下幾個(gè)可能的取值:
- auto:元素對(duì)鼠標(biāo)事件作出默認(rèn)的響應(yīng)。即元素會(huì)根據(jù)自身的樣式和內(nèi)容對(duì)鼠標(biāo)事件做出適當(dāng)?shù)捻憫?yīng)。
- none:元素對(duì)鼠標(biāo)事件不做出響應(yīng)。即元素不會(huì)接收鼠標(biāo)事件,鼠標(biāo)事件會(huì)透過元素傳遞到下層的元素上。這相當(dāng)于將元素變?yōu)椴豢牲c(diǎn)擊,即使它位于頁面上方或遮擋其他元素。
- visiblePainted:元素對(duì)鼠標(biāo)事件做出響應(yīng),但鼠標(biāo)事件不會(huì)穿透元素,而是傳遞到下層的元素上。這意味著元素會(huì)接收鼠標(biāo)事件,但不會(huì)阻止下層元素對(duì)鼠標(biāo)事件的響應(yīng)。
- visibleFill:元素對(duì)鼠標(biāo)事件做出響應(yīng),并會(huì)阻止鼠標(biāo)事件傳遞到下層的元素上。這意味著元素會(huì)完全接收鼠標(biāo)事件,并阻止下層元素對(duì)鼠標(biāo)事件的響應(yīng)。
pointer-events 屬性對(duì)于創(chuàng)建交互式的頁面元素非常有用,可以控制元素是否能夠接收和響應(yīng)鼠標(biāo)事件。通過將其設(shè)置為 none,可以使元素在外觀上可見,但不會(huì)干擾下層元素的交互。
需要注意的是,pointer-events 屬性的兼容性有限,可能不支持所有瀏覽器或舊版本的瀏覽器。在使用時(shí),建議先檢查兼容性并提供備用方案或降級(jí)策略。