利用Vue3指令實現(xiàn)水印背景詳解
正文
頁面水印業(yè)務(wù)相信我們都有遇過,為什么需要給頁面添加水印?為了保護自己的版權(quán)和知識產(chǎn)權(quán),給圖片加上水印一般是為了防止盜圖者用于商業(yè)用途,損害原作者的權(quán)益。那么在我們開發(fā)當中有什么方法可以實現(xiàn)呢?一般分為前端實現(xiàn)和后端實現(xiàn)這兩種方法,本文主要是學(xué)習(xí)前端實現(xiàn)方法:
- 方式一:直接將字體用塊元素包裹,動態(tài)設(shè)置絕對定位,然后通過transform屬性旋轉(zhuǎn)。但是需要考慮一個問題,當圖片過大或圖片過多時會很影響性能,所以就不詳細說這一方式了。
- 方式二:canvas上繪制出字體,設(shè)置好樣式,最后以圖片的樣式導(dǎo)出,用圖片作為水印層的背景圖。
在學(xué)習(xí)水印層之前,我先拋出兩個問題:
- 如果水印文字長,水印可以實現(xiàn)自適應(yīng)嗎?
- 能否限制用戶修改并刪除水???
其實上面這兩個問題是我們做頁面水印需要考慮的兩個核心問題,好的,話不多說,我們一起帶著問題去探索??。
首先定義一個指令,我們要明確兩點:命名(v-water-mask)和綁定值(配置值,option),實現(xiàn)如下:
<div v-water-mask:options="wmOption"></div>
// 配置值
const wmOption = reactive<WMOptions>({
textArr: ['路燈下的光', `${dayjs().format('YYYY-MM-DD HH:mm')}`],
deg: -35,
});
效果如下圖所示:

從上圖中我們可以看出,文字有文本以及時間字符串,水印文字都是傾斜了一定角度,其實就是旋轉(zhuǎn)了一定角度的。那么問題來了,我們可能問這些是怎么設(shè)置的?首先這需要使用指令的時候通過一些配置來實現(xiàn)一些固定值,下面這里都把這些配置都封裝成一個類了,為什么要這樣做?這樣就不用使用的時候每次都要設(shè)定一個默認值,比如通過定義接口來引用這些配置時每次都需要設(shè)置一個默認值:
export class WMOptions {
constructor(init?: WMOptions) {
if (init) {
Object.assign(this, init);
}
}
textArr: Array<string> = ['test', '自定義水印']; // 需要展示的文字,多行就多個元素【必填】
font?: string = '16px "微軟雅黑"'; // 字體樣式
fillStyle?: string = 'rgba(170,170,170,0.4)'; // 描邊樣式
maxWidth?: number = 200; // 文字水平時最大寬度
minWidth?: number = 120; // 文字水平時最小寬度
lineHeight?: number = 24; // 文字行高
deg?: number = -45; // 旋轉(zhuǎn)的角度 0至-90之間
marginRight?: number = 120; // 每個水印的右間隔
marginBottom?: number = 40; // 每個水印的下間隔
left?: number = 20; // 整體背景距左邊的距離
top?: number = 20; // 整體背景距上邊的距離
opacity?: string = '.75'; // 文字透明度
position?: 'fixed' | 'absolute' = 'fixed'; // 容器定位方式(值為absolute時,需要指定一個父元素非static定位)
}
細心的地我們可能會發(fā)現(xiàn)顯示地文本是一個數(shù)組,這樣主要是為了方便分行,聰明地我們可能會問:假如其中一個比較長怎么換行?,別急別急,我們先了解一下指令是怎么定義的:
定義指令:首先定義為一個ObjectDirective對象類型,因為指令也就是通過在不同生命周期中對當前元素做一些操作。
const WaterMask: ObjectDirective = {
// el為當前元素
// bind是當前綁定的屬性,注意地,由于是vue3實現(xiàn),這個值是一個ref類型
beforeMount(el: HTMLElement, binding: DirectiveBinding) {
// 實現(xiàn)水印的核心方法
waterMask(el, binding);
},
mounted(el: HTMLElement, binding: DirectiveBinding) {
nextTick(() => {
// 禁止修改水印
disablePatchWaterMask(el);
});
},
beforeUnmount() {
// 清除監(jiān)聽DOM節(jié)點的監(jiān)聽器
if (observerTemp.value) {
observerTemp.value.disconnect();
observerTemp.value = null;
}
},
};
export default WaterMask;
- waterMask方法:實現(xiàn)水印業(yè)務(wù)細節(jié)呈現(xiàn),對文字的自適應(yīng)換行,根據(jù)頁面元素大小來計算合適寬高值。
- disablePatchWaterMask方法:通過MutationObserver方法監(jiān)聽DOM元素修改,從而阻止用戶取消水印的呈現(xiàn)。
聲明指令:在main文件中定義聲明指令,這樣我們就可以全局使用這個指令了
app.directive('water-mask', WaterMask);
接下來我們來看一一分析水印的兩個核心方法:waterMask和disablePatchWaterMask。
實現(xiàn)水印功能
通過waterMask方法實現(xiàn),waterMask方法主要是做了四件事情:
let defaultSettings = new WMOptions();
const waterMask = function (element: HTMLElement, binding: DirectiveBinding) {
// 合并默認值和傳參配置
defaultSettings = Object.assign({}, defaultSettings, binding.value || {});
defaultSettings.minWidth = Math.min(
defaultSettings.maxWidth!,
defaultSettings.minWidth!
); // 重置最小寬度
const textArr = defaultSettings.textArr;
if (!Util.isArray(textArr)) {
throw Error('水印文本必須放在數(shù)組中!');
}
const c = createCanvas(); // 動態(tài)創(chuàng)建隱藏的canvas
draw(c, defaultSettings); // 繪制文本
convertCanvasToImage(c, element); // 轉(zhuǎn)化圖像
};
獲取配置的默認值:由于開發(fā)者傳參的時候不一定需要把所有配置的傳進來,其實按照本身默認的一些值就行,通過淺拷貝把指令綁定的值傳進來的一起融合一起就可以更新默認的配置:
創(chuàng)建canvas標簽:因為是通過canvas實現(xiàn)的,我們本身是沒有直接在template中呈現(xiàn)這個標簽,所以需要通過document對象創(chuàng)建canvas標簽:
function createCanvas() {
const c = document.createElement('canvas');
c.style.display = 'none';
document.body.appendChild(c);
return c;
}
繪制文本:首先遍歷傳入需要顯示的水印信息,也就是textArr文本數(shù)組,遍歷數(shù)組判斷數(shù)組元素是不是超出了配置的每個水印默認寬高,然后根據(jù)文本元素返回超出文本長度的文本分割數(shù)組,同時把文本最大寬度返回,最后通過切割結(jié)果動態(tài)修改canvas的寬高。
function draw(c: any, settings: WMOptions) {
const ctx = c.getContext('2d');
// 切割超過最大寬度的文本并獲取最大寬度
const textArr = settings.textArr || []; // 水印文本數(shù)組
let wordBreakTextArr: Array<any> = [];
const maxWidthArr: Array<number> = [];
// 遍歷水印文本數(shù)組,判斷每個元素的長度
textArr.forEach((text) => {
const result = breakLinesForCanvas(ctx,text + '',settings.maxWidth!,settings.font!);
// 合并超出最大寬度的分割數(shù)組
wordBreakTextArr = wordBreakTextArr.concat(result.textArr);
// 最大寬度
maxWidthArr.push(result.maxWidth);
});
// 最大寬度排序,最后取最大的最大寬度maxWidthArr[0]
maxWidthArr.sort((a, b) => {
return b - a;
});
// 根據(jù)需要切割結(jié)果,動態(tài)改變canvas的寬和高
const maxWidth = Math.max(maxWidthArr[0], defaultSettings.minWidth!);
const lineHeight = settings.lineHeight!;
const height = wordBreakTextArr.length * lineHeight;
const degToPI = (Math.PI * settings.deg!) / 180;
const absDeg = Math.abs(degToPI);
// 根據(jù)旋轉(zhuǎn)后的矩形計算最小畫布的寬高
const hSinDeg = height * Math.sin(absDeg);
const hCosDeg = height * Math.cos(absDeg);
const wSinDeg = maxWidth * Math.sin(absDeg);
const wCosDeg = maxWidth * Math.cos(absDeg);
c.width = parseInt(hSinDeg + wCosDeg + settings.marginRight! + '', 10);
c.height = parseInt(wSinDeg + hCosDeg + settings.marginBottom! + '', 10);
// 寬高重置后,樣式也需重置
ctx.font = settings.font;
ctx.fillStyle = settings.fillStyle;
ctx.textBaseline = 'hanging'; // 默認是alphabetic,需改基準線為貼著線的方式
// 移動并旋轉(zhuǎn)畫布
ctx.translate(0, wSinDeg);
ctx.rotate(degToPI);
// 繪制文本
wordBreakTextArr.forEach((text, index) => {
ctx.fillText(text, 0, lineHeight * index);
});
}
從上面代碼中我們可以看出繪制文本的核心操作是切割超長文本和動態(tài)修改canvas的寬高。我們接下來看看這兩個操作是如何實現(xiàn)的?
measureText()方法是基于當前字型來計算字符串寬度的。
// 根據(jù)最大寬度切割文字
function breakLinesForCanvas(context: any,text: string,width: number,font: string) {
const result = [];
let maxWidth = 0;
if (font) {
context.font = font;
}
// 查找切割點
let breakPoint = findBreakPoint(text, width, context);
while (breakPoint !== -1) {
// 切割點前的元素入棧
result.push(text.substring(0, breakPoint));
// 切割點后的元素
text = text.substring(breakPoint);
maxWidth = width;
// 查找切割點后的元素是否還有切割點
breakPoint = findBreakPoint(text, width, context);
}
// 如果切割的最后文本還有文本就push
if (text) {
result.push(text);
const lastTextWidth = context.measureText(text).width;
maxWidth = maxWidth !== 0 ? maxWidth : lastTextWidth;
}
return {
textArr: result,
maxWidth: maxWidth,
};
}
// 尋找切換斷點
function findBreakPoint(text: string, width: number, context: any) {
let min = 0;
let max = text.length - 1;
while (min <= max) {
// 二分字符串中點
const middle = Math.floor((min + max) / 2);
// measureText()方法是基于當前字型來計算字符串寬度的
const middleWidth = context.measureText(text.substring(0, middle)).width;
const oneCharWiderThanMiddleWidth = context.measureText(
text.substring(0, middle + 1)
).width;
// 判斷當前文本切割是否超了的臨界點
if (middleWidth <= width && oneCharWiderThanMiddleWidth > width) {
return middle;
}
// 如果沒超繼續(xù)遍歷查找
if (middleWidth < width) {
min = middle + 1;
} else {
max = middle - 1;
}
}
return -1;
}

所以canvas圖形寬為hSinDeg + wCosDeg + settings.marginRight。canvas圖形高為:wSinDeg + hCosDeg + settings.marginBottom。
- 切割超長文本:
- 尋找切割點:通過二分查找方法查詢字符串超長的位置在哪里:
- 動態(tài)修改canvas的寬高:通過旋轉(zhuǎn)的角度值、最大寬度值以及勾股定理一一計算寬度和高度,首先我們需要把旋轉(zhuǎn)的角度轉(zhuǎn)換為弧度值(公式:π/180×角度,也就是 (Math.PI*settings.deg!) / 180 ),我們先看看下圖:
轉(zhuǎn)化圖像:通過對當前canvas配置轉(zhuǎn)化為圖形url,然后配置元素的style屬性。
// 將繪制好的canvas轉(zhuǎn)成圖片
function convertCanvasToImage(canvas: any, el: HTMLElement) {
// 判斷是否為空渲染器
if (Util.isUndefinedOrNull(el)) {
console.error('請綁定渲染容器');
} else {
// 轉(zhuǎn)化為圖形數(shù)據(jù)的url
const imgData = canvas.toDataURL('image/png');
const divMask = el;
divMask.style.cssText = `position: ${defaultSettings.position}; left:0; top:0; right:0; bottom:0; z-index:9999; pointer-events:none;opacity:${defaultSettings.opacity}`;
divMask.style.backgroundImage = 'url(' + imgData + ')';
divMask.style.backgroundPosition =
defaultSettings.left + 'px ' + defaultSettings.top + 'px';
}
}
實現(xiàn)禁止用戶修改水印
我們都知道,如果用戶需要修改html一般都會瀏覽器調(diào)式中的Elements中修改我們網(wǎng)頁的元素的樣式就可以,也就是我們只要監(jiān)聽到DOM元素被修改就可以,控制修改DOM無法生效。
由于修改DOM有兩種方法:修改元素節(jié)點和修改元素屬性,所以只要控制元素的相關(guān)DOM方法中進行相應(yīng)操作就可以實現(xiàn)我們的禁止。而通過disablePatchWaterMask方法主要做了三件事情:
- 創(chuàng)建MutationObserver實例:也就是實例化MutationObserver,這樣才能調(diào)用MutationObserver中的observe函數(shù)實現(xiàn)DOM修改的監(jiān)聽。
- 創(chuàng)建MutationObserver回調(diào)函數(shù):通過傳入的兩個參數(shù),一個當前元素集合和observer監(jiān)聽器。
- 監(jiān)聽需要監(jiān)聽的元素:調(diào)用observer需要傳入監(jiān)聽元素以及監(jiān)聽配置,這個可以參考一下MutationObserver用法配置。
function disablePatchWaterMask(el: HTMLElement) {
// 觀察器的配置(需要觀察什么變動)
const config = {
attributes: true,
childList: true,
subtree: true,
attributeOldValue: true,
};
/* MutationObserver 是一個可以監(jiān)聽DOM結(jié)構(gòu)變化的接口。 */
const MutationObserver =
window.MutationObserver || window.WebKitMutationObserver;
// 當觀察到變動時執(zhí)行的回調(diào)函數(shù)
const callback = function (mutationsList: any, observer: any) {
console.log(mutationsList);
for (let mutation of mutationsList) {
let type = mutation.type;
switch (type) {
case 'childList':
if (mutation.removedNodes.length > 0) {
// 刪除節(jié)點,直接從刪除的節(jié)點數(shù)組中添加回來
mutation.target.append(mutation.removedNodes[0]);
}
break;
case 'attributes':
// 為什么是這樣處理,我們看一下下面兩幅圖
mutation.target.setAttribute('style', mutation.target.oldValue);
break;
default:
break;
}
}
};
// 創(chuàng)建一個觀察器實例并傳入回調(diào)函數(shù)
const observer = new MutationObserver(callback);
// 以上述配置開始觀察目標節(jié)點
observer.observe(el, config);
observerTemp.value = observer;
}

從水印到取消水印(勾選到不勾選background-image):我們發(fā)現(xiàn)mutation.target屬性中的oldValue值就是我們設(shè)置style。

從取消水印到恢復(fù)水?。ú还催x到勾選background-image):我們發(fā)現(xiàn)mutation.target屬性中的oldValue值的background-image被注釋掉了。
從上面兩個轉(zhuǎn)化中,我們就可以直接得出直接賦值當勾選到不勾選是監(jiān)聽到DOM修改的oldValue(真正的style),因為這時候獲取到的才是真正style,反之就不是了,由于我們不勾選時的oldValue賦值給不勾選時的style,所以當我們不勾選時再轉(zhuǎn)化為勾選時就是真正style,從而實現(xiàn)不管用戶怎么操作都不能取消水印。
以上就是利用Vue3指令實現(xiàn)水印背景詳解的詳細內(nèi)容,更多關(guān)于Vue3指令水印背景的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue三元運算之多重條件判斷方式(多個枚舉值轉(zhuǎn)譯)
這篇文章主要介紹了vue三元運算之多重條件判斷方式(多個枚舉值轉(zhuǎn)譯),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09
Vue3.0結(jié)合bootstrap創(chuàng)建多頁面應(yīng)用
這篇文章主要介紹了Vue3.0結(jié)合bootstrap創(chuàng)建多頁面應(yīng)用,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-05-05
Vue3?在<script?setup>里設(shè)置組件name屬性的方法
這篇文章主要介紹了Vue3?在<script?setup>里設(shè)置組件name屬性的方法,本文通過示例代碼給大家介紹的非常詳細,需要的朋友參考下吧2023-11-11
Mint UI組件庫CheckList使用及踩坑總結(jié)
這篇文章主要介紹了Mint UI組件庫CheckList使用及踩坑總結(jié),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-12-12

