溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何用Vue3指令實(shí)現(xiàn)水印背景

發(fā)布時(shí)間:2023-05-18 16:36:57 來(lái)源:億速云 閱讀:128 作者:iii 欄目:編程語(yǔ)言

這篇文章主要介紹了如何用Vue3指令實(shí)現(xiàn)水印背景的相關(guān)知識(shí),內(nèi)容詳細(xì)易懂,操作簡(jiǎn)單快捷,具有一定借鑒價(jià)值,相信大家閱讀完這篇如何用Vue3指令實(shí)現(xiàn)水印背景文章都會(huì)有所收獲,下面我們一起來(lái)看看吧。

首先定義一個(gè)指令,我們要明確兩點(diǎn):命名(v-water-mask)和綁定值(配置值,option),實(shí)現(xiàn)如下:

<div v-water-mask:options="wmOption"></div>
// 配置值
const wmOption = reactive<WMOptions>({
  textArr: ['路燈下的光', `${dayjs().format('YYYY-MM-DD HH:mm')}`],
  deg: -35,
});

效果如下圖所示:

如何用Vue3指令實(shí)現(xiàn)水印背景

從上圖中我們可以看出,文字有文本以及時(shí)間字符串,水印文字都是傾斜了一定角度,其實(shí)就是旋轉(zhuǎn)了一定角度的。那么問(wèn)題來(lái)了,我們可能問(wèn)這些是怎么設(shè)置的?首先這需要使用指令的時(shí)候通過(guò)一些配置來(lái)實(shí)現(xiàn)一些固定值,下面這里都把這些配置都封裝成一個(gè)類了,為什么要這樣做?這樣就不用使用的時(shí)候每次都要設(shè)定一個(gè)默認(rèn)值,比如通過(guò)定義接口來(lái)引用這些配置時(shí)每次都需要設(shè)置一個(gè)默認(rèn)值:

export class WMOptions {
  constructor(init?: WMOptions) {
    if (init) {
      Object.assign(this, init);
    }
  }
  textArr: Array<string> = ['test', '自定義水印']; // 需要展示的文字,多行就多個(gè)元素【必填】
  font?: string = '16px "微軟雅黑"'; // 字體樣式
  fillStyle?: string = 'rgba(170,170,170,0.4)'; // 描邊樣式
  maxWidth?: number = 200; // 文字水平時(shí)最大寬度
  minWidth?: number = 120; // 文字水平時(shí)最小寬度
  lineHeight?: number = 24; // 文字行高
  deg?: number = -45; // 旋轉(zhuǎn)的角度 0至-90之間
  marginRight?: number = 120; // 每個(gè)水印的右間隔
  marginBottom?: number = 40; // 每個(gè)水印的下間隔
  left?: number = 20; // 整體背景距左邊的距離
  top?: number = 20; // 整體背景距上邊的距離
  opacity?: string = '.75'; // 文字透明度
  position?: 'fixed' | 'absolute' = 'fixed'; // 容器定位方式(值為absolute時(shí),需要指定一個(gè)父元素非static定位)
}

細(xì)心的地我們可能會(huì)發(fā)現(xiàn)顯示地文本是一個(gè)數(shù)組,這樣主要是為了方便分行,聰明地我們可能會(huì)問(wèn):假如其中一個(gè)比較長(zhǎng)怎么換行?,別急別急,我們先了解一下指令是怎么定義的:

定義指令:首先定義為一個(gè)ObjectDirective對(duì)象類型,因?yàn)橹噶钜簿褪峭ㄟ^(guò)在不同生命周期中對(duì)當(dāng)前元素做一些操作。

const WaterMask: ObjectDirective = {
  // el為當(dāng)前元素
  // bind是當(dāng)前綁定的屬性,注意地,由于是vue3實(shí)現(xiàn),這個(gè)值是一個(gè)ref類型
    beforeMount(el: HTMLElement, binding: DirectiveBinding) {
        // 實(shí)現(xiàn)水印的核心方法
        waterMask(el, binding);
    },
    mounted(el: HTMLElement, binding: DirectiveBinding) {
        nextTick(() => {
          // 禁止修改水印
          disablePatchWaterMask(el);
        });
    },
    beforeUnmount() {
        // 清除監(jiān)聽(tīng)DOM節(jié)點(diǎn)的監(jiān)聽(tīng)器
        if (observerTemp.value) {
          observerTemp.value.disconnect();
          observerTemp.value = null;
        }
    },
};
export default WaterMask;
  • waterMask方法:實(shí)現(xiàn)水印業(yè)務(wù)細(xì)節(jié)呈現(xiàn),對(duì)文字的自適應(yīng)換行,根據(jù)頁(yè)面元素大小來(lái)計(jì)算合適寬高值。

  • disablePatchWaterMask方法:通過(guò)MutationObserver方法監(jiān)聽(tīng)DOM元素修改,從而阻止用戶取消水印的呈現(xiàn)。

聲明指令:在main文件中定義聲明指令,這樣我們就可以全局使用這個(gè)指令了

app.directive('water-mask', WaterMask);

接下來(lái)我們來(lái)看一一分析水印的兩個(gè)核心方法:waterMask和disablePatchWaterMask。

實(shí)現(xiàn)水印功能

通過(guò)waterMask方法實(shí)現(xiàn),waterMask方法主要是做了四件事情:

let defaultSettings = new WMOptions();
const waterMask = function (element: HTMLElement, binding: DirectiveBinding) {
  // 合并默認(rèn)值和傳參配置
  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(); // 動(dòng)態(tài)創(chuàng)建隱藏的canvas
  draw(c, defaultSettings); // 繪制文本
  convertCanvasToImage(c, element); // 轉(zhuǎn)化圖像
};

獲取配置的默認(rèn)值:由于開(kāi)發(fā)者傳參的時(shí)候不一定需要把所有配置的傳進(jìn)來(lái),其實(shí)按照本身默認(rèn)的一些值就行,通過(guò)淺拷貝把指令綁定的值傳進(jìn)來(lái)的一起融合一起就可以更新默認(rèn)的配置:

創(chuàng)建canvas標(biāo)簽:因?yàn)槭峭ㄟ^(guò)canvas實(shí)現(xiàn)的,我們本身是沒(méi)有直接在template中呈現(xiàn)這個(gè)標(biāo)簽,所以需要通過(guò)document對(duì)象創(chuàng)建canvas標(biāo)簽:

function createCanvas() {
  const c = document.createElement('canvas');
  c.style.display = 'none';
  document.body.appendChild(c);
  return c;
}

繪制文本:首先遍歷傳入需要顯示的水印信息,也就是textArr文本數(shù)組,遍歷數(shù)組判斷數(shù)組元素是不是超出了配置的每個(gè)水印默認(rèn)寬高,然后根據(jù)文本元素返回超出文本長(zhǎng)度的文本分割數(shù)組,同時(shí)把文本最大寬度返回,最后通過(guò)切割結(jié)果動(dòng)態(tài)修改canvas的寬高。

function draw(c: any, settings: WMOptions) {
  const ctx = c.getContext('2d');
  // 切割超過(guò)最大寬度的文本并獲取最大寬度
  const textArr = settings.textArr || []; // 水印文本數(shù)組
  let wordBreakTextArr: Array<any> = [];
  const maxWidthArr: Array<number> = [];
  // 遍歷水印文本數(shù)組,判斷每個(gè)元素的長(zhǎng)度
  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é)果,動(dòng)態(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)后的矩形計(jì)算最小畫(huà)布的寬高
  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'; // 默認(rèn)是alphabetic,需改基準(zhǔn)線為貼著線的方式
  // 移動(dòng)并旋轉(zhuǎn)畫(huà)布
  ctx.translate(0, wSinDeg);
  ctx.rotate(degToPI);
  // 繪制文本
  wordBreakTextArr.forEach((text, index) => {
    ctx.fillText(text, 0, lineHeight * index);
  });
}

從上面代碼中我們可以看出繪制文本的核心操作是切割超長(zhǎng)文本和動(dòng)態(tài)修改canvas的寬高。我們接下來(lái)看看這兩個(gè)操作是如何實(shí)現(xiàn)的?

measureText()方法是基于當(dāng)前字型來(lái)計(jì)算字符串寬度的。

// 根據(jù)最大寬度切割文字
function breakLinesForCanvas(context: any,text: string,width: number,font: string) {
  const result = [];
  let maxWidth = 0;
  if (font) {
    context.font = font;
  }
  // 查找切割點(diǎn)
  let breakPoint = findBreakPoint(text, width, context);
  while (breakPoint !== -1) {
    // 切割點(diǎn)前的元素入棧
    result.push(text.substring(0, breakPoint));
    // 切割點(diǎn)后的元素
    text = text.substring(breakPoint);
    maxWidth = width;
    // 查找切割點(diǎn)后的元素是否還有切割點(diǎn)
    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,
  };
}
// 尋找切換斷點(diǎn)
function findBreakPoint(text: string, width: number, context: any) {
  let min = 0;
  let max = text.length - 1;
  while (min <= max) {
    // 二分字符串中點(diǎn)
    const middle = Math.floor((min + max) / 2);
    // measureText()方法是基于當(dāng)前字型來(lái)計(jì)算字符串寬度的
    const middleWidth = context.measureText(text.substring(0, middle)).width;
    const oneCharWiderThanMiddleWidth = context.measureText(
      text.substring(0, middle + 1)
    ).width;
    // 判斷當(dāng)前文本切割是否超了的臨界點(diǎn)
    if (middleWidth <= width && oneCharWiderThanMiddleWidth > width) {
      return middle;
    }
    // 如果沒(méi)超繼續(xù)遍歷查找
    if (middleWidth < width) {
      min = middle + 1;
    } else {
      max = middle - 1;
    }
  }
  return -1;
}

如何用Vue3指令實(shí)現(xiàn)水印背景

所以canvas圖形寬為hSinDeg + wCosDeg + settings.marginRight。canvas圖形高為:wSinDeg + hCosDeg + settings.marginBottom。

  • 切割超長(zhǎng)文本:

  • 尋找切割點(diǎn):通過(guò)二分查找方法查詢字符串超長(zhǎng)的位置在哪里:

  • 動(dòng)態(tài)修改canvas的寬高:通過(guò)旋轉(zhuǎn)的角度值、最大寬度值以及勾股定理一一計(jì)算寬度和高度,首先我們需要把旋轉(zhuǎn)的角度轉(zhuǎn)換為弧度值(公式:&pi;/180&times;角度,也就是 (Math.PI*settings.deg!) / 180 ),我們先看看下圖:

轉(zhuǎn)化圖像:通過(guò)對(duì)當(dāng)前canvas配置轉(zhuǎn)化為圖形url,然后配置元素的style屬性。

// 將繪制好的canvas轉(zhuǎn)成圖片
function convertCanvasToImage(canvas: any, el: HTMLElement) {
  // 判斷是否為空渲染器
  if (Util.isUndefinedOrNull(el)) {
    console.error('請(qǐng)綁定渲染容器');
  } 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';
  }
}

實(shí)現(xiàn)禁止用戶修改水印

我們都知道,如果用戶需要修改html一般都會(huì)瀏覽器調(diào)式中的Elements中修改我們網(wǎng)頁(yè)的元素的樣式就可以,也就是我們只要監(jiān)聽(tīng)到DOM元素被修改就可以,控制修改DOM無(wú)法生效。

由于修改DOM有兩種方法:修改元素節(jié)點(diǎn)和修改元素屬性,所以只要控制元素的相關(guān)DOM方法中進(jìn)行相應(yīng)操作就可以實(shí)現(xiàn)我們的禁止。而通過(guò)disablePatchWaterMask方法主要做了三件事情:

  • 創(chuàng)建MutationObserver實(shí)例:也就是實(shí)例化MutationObserver,這樣才能調(diào)用MutationObserver中的observe函數(shù)實(shí)現(xiàn)DOM修改的監(jiān)聽(tīng)。

  • 創(chuàng)建MutationObserver回調(diào)函數(shù):通過(guò)傳入的兩個(gè)參數(shù),一個(gè)當(dāng)前元素集合和observer監(jiān)聽(tīng)器。

  • 監(jiān)聽(tīng)需要監(jiān)聽(tīng)的元素:調(diào)用observer需要傳入監(jiān)聽(tīng)元素以及監(jiān)聽(tīng)配置,這個(gè)可以參考一下MutationObserver用法配置。

function disablePatchWaterMask(el: HTMLElement) {
  // 觀察器的配置(需要觀察什么變動(dòng))
  const config = {
    attributes: true,
    childList: true,
    subtree: true,
    attributeOldValue: true,
  };
  /* MutationObserver 是一個(gè)可以監(jiān)聽(tīng)DOM結(jié)構(gòu)變化的接口。 */
  const MutationObserver =
    window.MutationObserver || window.WebKitMutationObserver;
  // 當(dāng)觀察到變動(dòng)時(shí)執(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é)點(diǎn),直接從刪除的節(jié)點(diǎn)數(shù)組中添加回來(lái)
            mutation.target.append(mutation.removedNodes[0]);
          }
          break;
        case 'attributes':
          // 為什么是這樣處理,我們看一下下面兩幅圖
          mutation.target.setAttribute('style', mutation.target.oldValue);
          break;
        default:
          break;
      }
    }
  };
  // 創(chuàng)建一個(gè)觀察器實(shí)例并傳入回調(diào)函數(shù)
  const observer = new MutationObserver(callback);
  // 以上述配置開(kāi)始觀察目標(biāo)節(jié)點(diǎn)
  observer.observe(el, config);
  observerTemp.value = observer;
}

如何用Vue3指令實(shí)現(xiàn)水印背景

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

如何用Vue3指令實(shí)現(xiàn)水印背景

從取消水印到恢復(fù)水印(不勾選到勾選background-image):我們發(fā)現(xiàn)mutation.target屬性中的oldValue值的background-image被注釋掉了。

從上面兩個(gè)轉(zhuǎn)化中,我們就可以直接得出直接賦值當(dāng)勾選到不勾選是監(jiān)聽(tīng)到DOM修改的oldValue(真正的style),因?yàn)檫@時(shí)候獲取到的才是真正style,反之就不是了,由于我們不勾選時(shí)的oldValue賦值給不勾選時(shí)的style,所以當(dāng)我們不勾選時(shí)再轉(zhuǎn)化為勾選時(shí)就是真正style,從而實(shí)現(xiàn)不管用戶怎么操作都不能取消水印。

關(guān)于“如何用Vue3指令實(shí)現(xiàn)水印背景”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對(duì)“如何用Vue3指令實(shí)現(xiàn)水印背景”知識(shí)都有一定的了解,大家如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI