ywl-watermark-vue基于vue指令实现水印功能(vue2/vue3),防止手动删除水印

在开发vue项目中,可能会根据项目的需求对页面添加水印效果,下面将介绍一种基于vue指令而实现水印的方法(通用于vue2和vue3),利用MutationObserver 监控水印DOM发生变化时,重新渲染水印,防止用户从DOM中直接删除水印。

一、vue指令 ywl-watermark-vue

https://www.npmjs.com/package/ywl-watermark-vue

二、安装使用

npm i ywl-watermark-vue

vue2使用

import Vue from 'vue'
import Watermark from 'ywl-watermark-vue'
 Vue.use(Watermark)      

或者

import Watermark from 'ywl-watermark-vue'
 Vue.directive(Watermark.name, Watermark)

vue3使用

import {createApp, inject} from "vue";
const app = createApp(App);
import Watermark from 'ywl-watermark-vue'
app.use(Watermark)

组件上使用

<div class="home" v-watermark>
    <div class="text-center">
      <img src="@/assets/images/1.webp" alt="">
    </div>
</div>    
<div class="home" v-watermark="{text:'默认文字',color:'red',size:16}">
   hello world
</div> 

三、效果图

效果图

四、实现代码

const globalCanvas = document.createElement('canvas');
const globalWaterMark = document.createElement('div');
let waterMarkObserver = null
let waterMarkStyle = ''

// 定义指令配置项
export default {
  // ----- vue2 ----
  // 初始化设置
  bind (el, binding, vnode) {
    binding.def?.init(el, binding)
  },
  // 元素插入父元素时调用
  inserted (el, binding, vnode) {
  },
  // 组件更新时调用
  update (el, binding, vnode) {
  },
  // 组件及子组件更新后调用
  componentUpdated (el, binding, vnode) {
  },
  // 解绑时调用
  unbind (el, binding, vnode) {
    // 停止观察。调用该方法后,DOM 再发生变动,也不会触发观察器
    waterMarkObserver && waterMarkObserver.disconnect();
  },
  // ----- vue2 ----
  // ----- vue3 ----
  // 在绑定元素的 attribute 或事件监听器被应用之前调用
  created (el, binding, vnode, prevNode) {
    binding.dir?.init(el, binding)
  },
  // 当指令第一次绑定到元素并且在挂载父组件之前调用。
  beforeMount () {
  
  },
  // 在绑定元素的父组件被挂载后调用,大部分自定义指令都写在这里。
  mounted () {
  
  },
  // 在更新包含组件的 VNode 之前调用。
  beforeUpdate () {
  
  },
  // 在包含组件的 VNode 及其子组件的 VNode 更新后调用。
  updated () {
  
  },
  // 在卸载绑定元素的父组件之前调用
  beforeUnmount () {
  
  },
  // 当指令与元素解除绑定且父组件已卸载时,只调用一次。
  unmounted () {
    // 停止观察。调用该方法后,DOM 再发生变动,也不会触发观察器
    waterMarkObserver && waterMarkObserver.disconnect();
  },
  // ----- vue3 ----
  
  /**
   * 初始化水印
   * @param el
   * @param binding
   */
  init (el, binding) {
    // 设置水印
    binding.def?.setWaterMark(el, binding) || binding.dir?.setWaterMark(el, binding);
    // 启动监控
    binding.def?.createObserver(el, binding) || binding.dir?.createObserver(el, binding);
  },
  /**
   * 默认配置
   * @returns {{fontFamily: string, color: string, size: number, text: string}}
   */
  defaultOptions () {
    return {
      // 水印文字
      text: '默认水印文字',
      // 水印文字颜色
      color: 'rgba(150, 150, 150, 1)',
      // 水印字体大小
      size: 12,
      // 水印字体类型
      fontFamily: 'Arial',
    }
  },
  /**
   * 水印容器class名
   * @returns {string}
   */
  waterMarkName () {
    return 'lx-water-mark'
  },
  /**
   * 水印容器样式
   * @returns {string}
   */
  waterMarkStyle () {
    let style = {
      'display': 'block',
      'overflow': 'hidden',
      'position': 'absolute',
      'left': '0px',
      'top': '0px',
      'z-index': 100000,
      'font-size': '12px',
      'background-repeat': 'repeat',
      'background-position': 'center',
      'pointer-events': 'none',
      'width': '100%',
      'height': '100%'
    }
    let styleArr = Object.keys(style).map((key) => {
      return `${key}:${style[key]}`
    })
    return styleArr.join(';') + ';'
  },
  /**
   * 设置水印
   * @param el
   * @param binding
   */
  setWaterMark (el, binding) {
    const parentEl = el;
    const {width, height} = parentEl?.getBoundingClientRect();
    // 拼接配置
    let defaultOptions = binding.def?.defaultOptions() || binding.dir?.defaultOptions()
    if (Object.prototype.toString.call(binding.value) === '[object Object]') {
      defaultOptions = Object.assign(defaultOptions, {
        text: binding.value.text || defaultOptions.text,
        color: binding.value.color || defaultOptions.color,
        size: binding.value?.size?.toString().replace('px', '') || defaultOptions.size,
        fontFamily: binding.value.fontFamily || defaultOptions.fontFamily,
      })
    }
    // 获取对应的 canvas 画布相关的 base64 url
    const url = binding.def?.getDataUrl(defaultOptions) || binding.dir?.getDataUrl(defaultOptions);
    // 创建 waterMark 父元素
    const waterMark = globalWaterMark || document.createElement('div');
    waterMark.className = binding.def?.waterMarkName() || binding.dir?.waterMarkName(); // 方便自定义展示结果
    waterMarkStyle = `${binding.def?.waterMarkStyle() || binding.dir?.waterMarkStyle()};background-image: url(${url})`;
    waterMark.setAttribute('style', waterMarkStyle);
    // 如果父元素有自己的stayle 则获取后和自定义的拼接,并避免重复添加
    let currStyle = parentEl?.getAttribute('style') ? parentEl?.getAttribute('style') : '';
    currStyle = currStyle?.includes('position: relative') ? currStyle : currStyle + 'position: relative;';
    // 将对应图片的父容器作为定位元素
    parentEl?.setAttribute('style', currStyle);
    // 将图片元素移动到 waterMark 中
    parentEl?.appendChild(waterMark);
  },
  /**
   * 生成水印图片,返回一个包含图片展示的数据 URL
   * @param options
   * @returns {string} 水印图片:base64-url
   */
  getDataUrl (options) {
    const {text, size, fontFamily, color} = options;
    const rotate = -20;
    const canvas = globalCanvas || document.createElement('canvas');
    const ctx = canvas.getContext('2d'); // 获取canvas画布的绘图环境
    canvas.width = 300; // 单个水印大小,宽度
    canvas.height = 150; // 高度
    
    ctx.fillStyle = 'rgba(0, 0, 0, 0)'; // 背景填充色
    ctx.fillRect(0, 0, 300, 150); // 填充区域大小
    
    ctx.save();
    ctx.font = `${size}px ${fontFamily}`; // 文字字体大小
    ctx.fillStyle = color; // 文字颜色
    ctx.translate((300) / 2, (150) / 2); // 平移,旋转的中心点
    ctx?.rotate((rotate * Math.PI) / 360); // 水印旋转角度
    ctx.textBaseline = 'middle'; // 垂直居中
    ctx.textAlign = 'center'; // 水平居中
    ctx?.fillText(text, 0, 0);
    ctx.restore();
    ctx.clip();
    return canvas.toDataURL('image/png');
  },
  /**
   * 添加观察者,监听DOM变化,用 MutationObserver 对水印元素进行监听,删除、属性变化时,再立即生成一个水印元素
   * @param el
   * @param binding
   */
  createObserver (el, binding) {
    const className = binding.def?.waterMarkName() || binding.dir?.waterMarkName();
    const waterMarkEl = el.querySelector(`.${className}`);
    waterMarkObserver = new MutationObserver((mutationsList) => {
      if (mutationsList.length) {
        const {removedNodes, type, target} = mutationsList[0];
        const currStyle = waterMarkEl?.getAttribute('style');
        // 证明被删除了
        if (removedNodes[0] === waterMarkEl) {
          // 停止观察。调用该方法后,DOM 再发生变动,也不会触发观察器
          waterMarkObserver.disconnect();
          // 重新初始化(设置水印,启动监控)
          binding.def?.init(el, binding) || binding.dir?.init(el, binding);
        } else if (type === 'attributes' && target === waterMarkEl && currStyle !== waterMarkStyle) {
          waterMarkEl.setAttribute('style', waterMarkStyle);
        }
      }
    });
    waterMarkObserver.observe(el, {attributes: true, childList: true, subtree: true, attributeOldValue: true});
  }
};

在watermark的index.js文件中引入水印js(./main/index.js)

import Watermark from './main/index.js'

Watermark.install = function (Vue) {
  Vue.directive('watermark', Watermark)
}
Watermark.name = 'watermark'
export default Watermark

在这里插入图片描述