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

在这里插入图片描述

"name": "duang2.0", "version": "0.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { "@ant-design/colors": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-3.2.2.tgz", "integrity": "sha512-YKgNbG2dlzqMhA9NtI3/pbY16m3Yl/EeWBRa+lB1X1YaYxHrxNexiQYCLTWO/uDvAjLFMEDU+zR901waBtMtjQ==", "requires": { "tinycolor2": "^1.4.1" } }, "@ant-design/icons": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-2.1.1.tgz", "integrity": "sha512-jCH+k2Vjlno4YWl6g535nHR09PwCEmTBKAG6VqF+rhkrSPRLfgpU2maagwbZPLjaHuU5Jd1DFQ2KJpQuI6uG8w==" }, "@ant-design/icons-vue": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@ant-design/icons-vue/-/icons-vue-2.0.0.tgz", "integrity": "sha512-2c0QQE5hL4N48k5NkPG5sdpMl9YnvyNhf0U7YkdZYDlLnspoRU7vIA0UK9eHBs6OpFLcJB6o8eJrIl2ajBskPg==", "requires": { "@ant-design/colors": "^3.1.0", "babel-runtime": "^6.26.0" } }, "@babel/code-frame": { "version": "7.14.5", "resolved": "https://registry.nlark.com/@babel/code-frame/download/@babel/code-frame-7.14.5.tgz?cache=0&sync_timestamp=1623280394200&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40babel%2Fcode-frame%2Fdownload%2F%40babel%2Fcode-frame-7.14.5.tgz", "integrity": "sha1-I7CNdA6D9JxeWZRfvxtD6Au/Tts=", "dev": true, "requires": { "@babel/highlight": "^7.14.5" } }, "@babel/compat-data": { "version": "7.14.5", "resolved": "https://registry.nlark.com/@babel/compat-data/download/@babel/compat-data-7.14.5.tgz?cache=0&sync_timestamp=1623280503073&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40babel%2Fcompat-data%2Fdownload%2F%40babel%2Fcompat-data-7.14.5.tgz", "integrity": "sha1-jvTBjljoAcXJXTwcDyh0omgPreo=", "dev": true }, "@babel/core": { "version": "7.14.6", "resolved": "https://registry.nlark.com/@babel/core/download/@babel/core-7.14.6.tgz", "integrity": "sha1-4IFOwalQAy/xbBOich3jmoQW/Ks=", "dev": true, 解析
07-14
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YWL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值