0

0

怎么用Vue3指令实现水印背景

WBOY

WBOY

发布时间:2023-05-12 23:07:14

|

2301人浏览过

|

来源于亿速云

转载

页面水印业务相信我们都有遇过,为什么需要给页面添加水印?为了保护自己的版权和知识产权,给图片加上水印一般是为了防止盗图者用于商业用途,损害原作者的权益。那么在我们开发当中有什么方法可以实现呢?一般分为前端实现和后端实现这两种方法,本文主要是学习前端实现方法:

  • 方式一:直接将字体用块元素包裹,动态设置绝对定位,然后通过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,
});

效果如下图所示:

怎么用Vue3指令实现水印背景

从上图中我们可以看出,文字有文本以及时间字符串,水印文字都是倾斜了一定角度,其实就是旋转了一定角度的。那么问题来了,我们可能问这些是怎么设置的?首先这需要使用指令的时候通过一些配置来实现一些固定值,下面这里都把这些配置都封装成一个类了,为什么要这样做?这样就不用使用的时候每次都要设定一个默认值,比如通过定义接口来引用这些配置时每次都需要设置一个默认值:

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标签:

皮卡智能
皮卡智能

AI驱动高效视觉设计平台

下载
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;
}

怎么用Vue3指令实现水印背景

所以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;
}

怎么用Vue3指令实现水印背景

从水印到取消水印(勾选到不勾选background-image):我们发现mutation.target属性中的oldValue值就是我们设置style。

怎么用Vue3指令实现水印背景

从取消水印到恢复水印(不勾选到勾选background-image):我们发现mutation.target属性中的oldValue值的background-image被注释掉了。

从上面两个转化中,我们就可以直接得出直接赋值当勾选到不勾选是监听到DOM修改的oldValue(真正的style),因为这时候获取到的才是真正style,反之就不是了,由于我们不勾选时的oldValue赋值给不勾选时的style,所以当我们不勾选时再转化为勾选时就是真正style,从而实现不管用户怎么操作都不能取消水印。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

16

2026.03.11

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

23

2026.03.10

Kotlin Android模块化架构与组件化开发实践
Kotlin Android模块化架构与组件化开发实践

本专题围绕 Kotlin 在 Android 应用开发中的架构实践展开,重点讲解模块化设计与组件化开发的实现思路。内容包括项目模块拆分策略、公共组件封装、依赖管理优化、路由通信机制以及大型项目的工程化管理方法。通过真实项目案例分析,帮助开发者构建结构清晰、易扩展且维护成本低的 Android 应用架构体系,提升团队协作效率与项目迭代速度。

75

2026.03.09

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

95

2026.03.06

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

218

2026.03.05

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

420

2026.03.04

AI安装教程大全
AI安装教程大全

2026最全AI工具安装教程专题:包含各版本AI绘图、AI视频、智能办公软件的本地化部署手册。全篇零基础友好,附带最新模型下载地址、一键安装脚本及常见报错修复方案。每日更新,收藏这一篇就够了,让AI安装不再报错!

168

2026.03.04

Swift iOS架构设计与MVVM模式实战
Swift iOS架构设计与MVVM模式实战

本专题聚焦 Swift 在 iOS 应用架构设计中的实践,系统讲解 MVVM 模式的核心思想、数据绑定机制、模块拆分策略以及组件化开发方法。内容涵盖网络层封装、状态管理、依赖注入与性能优化技巧。通过完整项目案例,帮助开发者构建结构清晰、可维护性强的 iOS 应用架构体系。

222

2026.03.03

C++高性能网络编程与Reactor模型实践
C++高性能网络编程与Reactor模型实践

本专题围绕 C++ 在高性能网络服务开发中的应用展开,深入讲解 Socket 编程、多路复用机制、Reactor 模型设计原理以及线程池协作策略。内容涵盖 epoll 实现机制、内存管理优化、连接管理策略与高并发场景下的性能调优方法。通过构建高并发网络服务器实战案例,帮助开发者掌握 C++ 在底层系统与网络通信领域的核心技术。

33

2026.03.03

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Vue3.x 工具篇--十天技能课堂
Vue3.x 工具篇--十天技能课堂

共26课时 | 1.6万人学习

Vue3.x 核心篇--十天技能课堂
Vue3.x 核心篇--十天技能课堂

共30课时 | 1.6万人学习

Vue3.x新特性篇--十天基础课堂
Vue3.x新特性篇--十天基础课堂

共20课时 | 1.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号