您好,登錄后才能下訂單哦!
這篇文章主要介紹了怎么用Vue3指令實現水印背景的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇怎么用Vue3指令實現水印背景文章都會有所收獲,下面我們一起來看看吧。
頁面水印業務相信我們都有遇過,為什么需要給頁面添加水印?為了保護自己的版權和知識產權,給圖片加上水印一般是為了防止盜圖者用于商業用途,損害原作者的權益。那么在我們開發當中有什么方法可以實現呢?一般分為前端實現和后端實現這兩種方法,本文主要是學習前端實現方法:
方式一:直接將字體用塊元素包裹,動態設置絕對定位,然后通過transform屬性旋轉。但是需要考慮一個問題,當圖片過大或圖片過多時會很影響性能,所以就不詳細說這一方式了。
方式二:canvas上繪制出字體,設置好樣式,最后以圖片的樣式導出,用圖片作為水印層的背景圖。
在學習水印層之前,我先拋出兩個問題:
如果水印文字長,水印可以實現自適應嗎?
能否限制用戶修改并刪除水印?
其實上面這兩個問題是我們做頁面水印需要考慮的兩個核心問題,好的,話不多說,我們一起帶著問題去探索????。
首先定義一個指令,我們要明確兩點:命名(v-water-mask)和綁定值(配置值,option),實現如下:
<div v-water-mask:options="wmOption"></div> // 配置值 const wmOption = reactive<WMOptions>({ textArr: ['路燈下的光', `${dayjs().format('YYYY-MM-DD HH:mm')}`], deg: -35, });
效果如下圖所示:
從上圖中我們可以看出,文字有文本以及時間字符串,水印文字都是傾斜了一定角度,其實就是旋轉了一定角度的。那么問題來了,我們可能問這些是怎么設置的?首先這需要使用指令的時候通過一些配置來實現一些固定值,下面這里都把這些配置都封裝成一個類了,為什么要這樣做?這樣就不用使用的時候每次都要設定一個默認值,比如通過定義接口來引用這些配置時每次都需要設置一個默認值:
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; // 旋轉的角度 0至-90之間 marginRight?: number = 120; // 每個水印的右間隔 marginBottom?: number = 40; // 每個水印的下間隔 left?: number = 20; // 整體背景距左邊的距離 top?: number = 20; // 整體背景距上邊的距離 opacity?: string = '.75'; // 文字透明度 position?: 'fixed' | 'absolute' = 'fixed'; // 容器定位方式(值為absolute時,需要指定一個父元素非static定位) }
細心的地我們可能會發現顯示地文本是一個數組,這樣主要是為了方便分行,聰明地我們可能會問:假如其中一個比較長怎么換行?,別急別急,我們先了解一下指令是怎么定義的:
定義指令:首先定義為一個ObjectDirective對象類型,因為指令也就是通過在不同生命周期中對當前元素做一些操作。
const WaterMask: ObjectDirective = { // el為當前元素 // bind是當前綁定的屬性,注意地,由于是vue3實現,這個值是一個ref類型 beforeMount(el: HTMLElement, binding: DirectiveBinding) { // 實現水印的核心方法 waterMask(el, binding); }, mounted(el: HTMLElement, binding: DirectiveBinding) { nextTick(() => { // 禁止修改水印 disablePatchWaterMask(el); }); }, beforeUnmount() { // 清除監聽DOM節點的監聽器 if (observerTemp.value) { observerTemp.value.disconnect(); observerTemp.value = null; } }, }; export default WaterMask;
waterMask方法:實現水印業務細節呈現,對文字的自適應換行,根據頁面元素大小來計算合適寬高值。
disablePatchWaterMask方法:通過MutationObserver方法監聽DOM元素修改,從而阻止用戶取消水印的呈現。
聲明指令:在main文件中定義聲明指令,這樣我們就可以全局使用這個指令了
app.directive('water-mask', WaterMask);
接下來我們來看一一分析水印的兩個核心方法:waterMask和disablePatchWaterMask。
通過waterMask方法實現,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('水印文本必須放在數組中!'); } const c = createCanvas(); // 動態創建隱藏的canvas draw(c, defaultSettings); // 繪制文本 convertCanvasToImage(c, element); // 轉化圖像 };
獲取配置的默認值:由于開發者傳參的時候不一定需要把所有配置的傳進來,其實按照本身默認的一些值就行,通過淺拷貝把指令綁定的值傳進來的一起融合一起就可以更新默認的配置:
創建canvas標簽:因為是通過canvas實現的,我們本身是沒有直接在template中呈現這個標簽,所以需要通過document對象創建canvas標簽:
function createCanvas() { const c = document.createElement('canvas'); c.style.display = 'none'; document.body.appendChild(c); return c; }
繪制文本:首先遍歷傳入需要顯示的水印信息,也就是textArr文本數組,遍歷數組判斷數組元素是不是超出了配置的每個水印默認寬高,然后根據文本元素返回超出文本長度的文本分割數組,同時把文本最大寬度返回,最后通過切割結果動態修改canvas的寬高。
function draw(c: any, settings: WMOptions) { const ctx = c.getContext('2d'); // 切割超過最大寬度的文本并獲取最大寬度 const textArr = settings.textArr || []; // 水印文本數組 let wordBreakTextArr: Array<any> = []; const maxWidthArr: Array<number> = []; // 遍歷水印文本數組,判斷每個元素的長度 textArr.forEach((text) => { const result = breakLinesForCanvas(ctx,text + '',settings.maxWidth!,settings.font!); // 合并超出最大寬度的分割數組 wordBreakTextArr = wordBreakTextArr.concat(result.textArr); // 最大寬度 maxWidthArr.push(result.maxWidth); }); // 最大寬度排序,最后取最大的最大寬度maxWidthArr[0] maxWidthArr.sort((a, b) => { return b - a; }); // 根據需要切割結果,動態改變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); // 根據旋轉后的矩形計算最小畫布的寬高 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,需改基準線為貼著線的方式 // 移動并旋轉畫布 ctx.translate(0, wSinDeg); ctx.rotate(degToPI); // 繪制文本 wordBreakTextArr.forEach((text, index) => { ctx.fillText(text, 0, lineHeight * index); }); }
從上面代碼中我們可以看出繪制文本的核心操作是切割超長文本和動態修改canvas的寬高。我們接下來看看這兩個操作是如何實現的?
measureText()方法是基于當前字型來計算字符串寬度的。
// 根據最大寬度切割文字 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; } // 如果沒超繼續遍歷查找 if (middleWidth < width) { min = middle + 1; } else { max = middle - 1; } } return -1; }
所以canvas圖形寬為hSinDeg + wCosDeg + settings.marginRight。canvas圖形高為:wSinDeg + hCosDeg + settings.marginBottom。
切割超長文本:
尋找切割點:通過二分查找方法查詢字符串超長的位置在哪里:
動態修改canvas的寬高:通過旋轉的角度值、最大寬度值以及勾股定理一一計算寬度和高度,首先我們需要把旋轉的角度轉換為弧度值(公式:π/180×角度,也就是 (Math.PI*settings.deg!) / 180 ),我們先看看下圖:
轉化圖像:通過對當前canvas配置轉化為圖形url,然后配置元素的style屬性。
// 將繪制好的canvas轉成圖片 function convertCanvasToImage(canvas: any, el: HTMLElement) { // 判斷是否為空渲染器 if (Util.isUndefinedOrNull(el)) { console.error('請綁定渲染容器'); } else { // 轉化為圖形數據的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'; } }
我們都知道,如果用戶需要修改html一般都會瀏覽器調式中的Elements中修改我們網頁的元素的樣式就可以,也就是我們只要監聽到DOM元素被修改就可以,控制修改DOM無法生效。
由于修改DOM有兩種方法:修改元素節點和修改元素屬性,所以只要控制元素的相關DOM方法中進行相應操作就可以實現我們的禁止。而通過disablePatchWaterMask方法主要做了三件事情:
創建MutationObserver實例:也就是實例化MutationObserver,這樣才能調用MutationObserver中的observe函數實現DOM修改的監聽。
創建MutationObserver回調函數:通過傳入的兩個參數,一個當前元素集合和observer監聽器。
監聽需要監聽的元素:調用observer需要傳入監聽元素以及監聽配置,這個可以參考一下MutationObserver用法配置。
function disablePatchWaterMask(el: HTMLElement) { // 觀察器的配置(需要觀察什么變動) const config = { attributes: true, childList: true, subtree: true, attributeOldValue: true, }; /* MutationObserver 是一個可以監聽DOM結構變化的接口。 */ const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; // 當觀察到變動時執行的回調函數 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) { // 刪除節點,直接從刪除的節點數組中添加回來 mutation.target.append(mutation.removedNodes[0]); } break; case 'attributes': // 為什么是這樣處理,我們看一下下面兩幅圖 mutation.target.setAttribute('style', mutation.target.oldValue); break; default: break; } } }; // 創建一個觀察器實例并傳入回調函數 const observer = new MutationObserver(callback); // 以上述配置開始觀察目標節點 observer.observe(el, config); observerTemp.value = observer; }
從水印到取消水印(勾選到不勾選background-image):我們發現mutation.target屬性中的oldValue值就是我們設置style。
從取消水印到恢復水印(不勾選到勾選background-image):我們發現mutation.target屬性中的oldValue值的background-image被注釋掉了。
從上面兩個轉化中,我們就可以直接得出直接賦值當勾選到不勾選是監聽到DOM修改的oldValue(真正的style),因為這時候獲取到的才是真正style,反之就不是了,由于我們不勾選時的oldValue賦值給不勾選時的style,所以當我們不勾選時再轉化為勾選時就是真正style,從而實現不管用戶怎么操作都不能取消水印。
關于“怎么用Vue3指令實現水印背景”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“怎么用Vue3指令實現水印背景”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。