你知道前端水印功能是怎么实现的吗?(一)

简介: 你知道前端水印功能是怎么实现的吗?

image.png


前言

前一段时间由于项目需要实现水印功能,于是去了解了相关的内容后,基于 Vue 的实现了一个 v-watermark 指令完成了对应的功能,其实整体内容并不复杂!

那么接下来先来简单了解一些和 Vue 自定义指令相关的内容,作为前置知识的铺垫,然后在逐步完成对应的功能。

vue 中的自定义指令

以下的内容其实简单了解即可,甚至可以直接跳过,遇到不认识的内容在回头来查看都是可以的,甚至可以直接查看 官方文档

是什么?

Vue3复用代码 有如下三种方式:

  • 组件:组件是主要的构建模块
  • 组合式函数:合式函数则侧重于有状态的逻辑
  • 自定义指令:主要是为了重用涉及普通元素的底层 DOM 访问的逻辑

因此,自定义指令本身也是一种代码逻辑复用的方式,只是着重点在 对底层 DOM 的访问和操作 上。

指令钩子

VueReactWebpackVite 等中都会存在着对应的 钩子,而这些 钩子 本质上就是一些在 特定时机 会被执行的 函数/方法 而已,那么在 Vue 自定义指令 中就存在如下钩子:

  • created(el, binding, vnode, prevVnode):在绑定元素的 attribute 前,或事件监听器应用前调用
  • beforeMount(el, binding, vnode, prevVnode):在元素被插入到 DOM 前调用
  • mounted(el, binding, vnode, prevVnode):在绑定元素的父组件,及其所有子节点都挂载完成后调用
  • beforeUpdate(el, binding, vnode, prevVnode):绑定元素的父组件更新前调用
  • updated(el, binding, vnode, prevVnode):在绑定元素的父组件,及他自己的所有子节点都更新后调用
  • beforeUnmount(el, binding, vnode, prevVnode):绑定元素的父组件卸载前调用
  • unmounted(el, binding, vnode, prevVnode):绑定元素的父组件卸载后调用

注意】以上这些钩子与 vue2 中的自定义钩子是有些不同的,具体可以点击对应链接比对查看.

钩子参数

其实直接看参数的命名方式,相信你也能知道大部分的参数代表什么:

  • el:指令绑定到的元素,意味着可通过 el 直接操作 DOM
  • binding:一个对象,包含以下属性
  • value:传递给指令的值,例如在 v-my-directive="1 + 1" 中,值是 2
  • oldValue:之前的旧值,仅在 beforeUpdateupdated 中可用,无论值是否更改都可用
  • arg:传递给指令的参数 (如果有的话),如在 v-my-directive:foo 中,参数是 "foo"
  • modifiers:一个包含修饰符的对象 (如果有的话),如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
  • instance:使用该指令的组件实例
  • dir:指令的定义对象
  • vnode:代表绑定元素的底层 VNode
  • prevNode:之前的渲染中代表指令所绑定元素的 VNode,仅在 beforeUpdateupdated 钩子中可用

注意】除了 el 外(因为需要操作 DOM),其他参数都是只读的,不建议更改,如果需要在不同的钩子间共享信息,推荐通过元素的 dataset 属性实现

实现水印功能

几种实现方案

基于原图生成水印图片(后端)

这种方案就是将 原图片 添加水印之后生成了 新图片,后续在前端页面进行展示是后端接口不返回原图片,而是返回带有水印的图片即可。

image.png

这种方式最大的优点就是安全,因为 水印图片 是后端生成的,前端只需要负责展示即可,不需考虑多余的问题,且即便在前端页面保存对应图片,拿到的仍然不是原图片。

基于 DOM 实现水印效果(前端)

自定义指令钩子非常多,但实际上能使用到的不多,比如最常用的就是 mountedupdated,在这我们只需要通过 mounted 即可实现对应的功能,并且核心代码比较简单。

核心内容

  • 创建一个 watermarkDOM 节点,即 div 元素,用于包裹对应的 img 便于展示水印内容
  • 在创建一个 waterbgDOM 节点,即 div 元素
  • waterbg 节点作为 watermark子节点,并进行 绝对定位 保证 waterbg最上层显示
  • 将对应的 水印标记 作为 waterbg 节点的 背景图片 展示
  • waterbg 节点设置 pointer-events: none; 实现 点击穿透
  • watermark 节点通过 insertBefore(...) 插入到 img 标签的前一个位置
  • 再将 img 标签移动到 watermark 节点节中,这样就保证了新创建的 watermark 节点的位置一定是在原本 img 挂载的位置

效果和代码如下

image.png

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import directives from './directives'
createApp(App)
    .use(directives)
    .mount('#app');
// src/directives/index.ts
import type { App } from 'vue'
import watermark from './waterMark'
export default function installDirective(app: App) {
    app.directive(watermark.name, watermark.directives);
} 
// src/directives/waterMark.ts
import waterBgImg from '../assets/water-bg.png'
const directives: any = {
    mounted(el: HTMLElement) {
        el.onload = () => {
            const { clientWidth, clientHeight, parentElement } = el;
            const waterMark: HTMLElement = document.createElement('div');
            const waterBg: HTMLElement = document.createElement('div');
            waterMark.className = `water-mark`;// 方便自定义展示结果
            // 创建 waterMark 父元素
            waterMark.setAttribute('style', `
            display: inline-block;
            overflow: hidden;
            position: relative;
            width: ${clientWidth}px; 
            height: ${clientHeight}px;`);
            // 创建 waterBg 背景元素
            waterBg.className = `water-mark-bg`;// 方便自定义展示结果
            waterBg.setAttribute('style', `
            position: absolute;
            pointer-events: none;
            width: 100%;
            height: 100%;
            opacity: 0.2;
            background-image: url(${waterBgImg}); 
            background-repeat: repeat;`);
            // 为 waterMark 添加对应的子元素
            waterMark.appendChild(waterBg);
            // 将 waterMark 插入到对应的位置
            parentElement?.insertBefore(waterMark, el);
            // 将图片元素移动到 waterMark 中
            waterMark.appendChild(el);
        }
    }
}
export default {
    name: 'watermark',
    directives
}
复制代码

优化实现方式

在上述的实现方式中,实际上至少有两点可优化的点:

  • 所有的样式直接以 字符串 形式出现在 JavaScript 代码中
  • 可以将对应的静态样式部分处理在 css 中,由 .water-mark 来管理
  • 多余的 waterBg 被创建出来
  • 其实完全没必要单独创建这个节点元素,可以直接基于 .water-mark 对应节点的伪类元素来实现

优化后核心代码如下:

/********* src/directives/waterMark.ts ***********/
const directives: any = {
  mounted(el: HTMLElement) {
    el.onload = () => {
      const { clientWidth, clientHeight, parentElement } = el;
      const waterMark: HTMLElement = document.createElement("div");
      // 创建 waterMark 父元素
      waterMark.setAttribute("style", `width: ${clientWidth}px; height: ${clientHeight}px;`);
      waterMark.className = `water-mark`; // 方便自定义展示结果
      // 将 waterMark 插入到对应的位置
      parentElement?.insertBefore(waterMark, el);
      // 将图片元素移动到 waterMark 中
      waterMark.appendChild(el);
    };
  },
};
export default {
  name: "watermark",
  directives,
};
/********* css 部分代码如下  ***********/ 
.water-mark {
  display: inline-block;
  overflow: hidden;
  position: relative;
}
.water-mark::after {
  pointer-events: none;
  position: absolute;
  content: ' ';
  width: 100%;
  height: 100%;
  opacity: 0.2;
  background-image: url("../assets/water-bg.png");
  background-repeat: repeat;
}
复制代码

基于 Canvas 实现水印效果(前端)

基于 Canvas 实现方式的优点就在于能够动态的设置水印内容,相比于上一种基于固定背景图片的方式更灵活,这种方式也是 语雀 在使用的方式,具体效果如下:

image.png

核心步骤

  • 通过 canvas 填充文本,并通过 canvas.toDataURL("image/png"); 获取到对应的 base64 格式的图片
  • 将这个 base64 格式的图片作为类名为 water-mark 节点的背景图
  • 利用 background-repeat: repeat; 让这个图重复填充背景即可
  • water-mark 节点设置 pointer-events: none; 实现 点击穿透
  • 利用对应图片的父元素作为 water-mark 节点的相对定位节点,保证绝对定位的 water-mark 节点显式在对应图片之上

效果和代码如下

image.png

/********* src/directives/waterMark.ts  ***********/ 
// 全局保存 canvas 和 div ,避免重复创建(单例模式)
const globalCanvas = null;
const globalWaterMark = null;
// 获取 toDataURL 的结果
const getDataUrl = ({
  font = "16px normal",
  fillStyle = "rgba(180, 180, 180, 0.3)",
  textAlign,
  textBaseline,
  text = "请勿外传",
}) => {
  const rotate = -20;
  const canvas = globalCanvas || document.createElement("canvas");
  const ctx = canvas.getContext("2d"); // 获取画布上下文
  ctx.rotate((rotate * Math.PI) / 180);
  ctx.font = font;
  ctx.fillStyle = fillStyle;
  ctx.textAlign = textAlign || "left";
  ctx.textBaseline = textBaseline || "middle";
  ctx.fillText(text, canvas.width / 3, canvas.height / 2);
  return canvas.toDataURL("image/png");
};
// 设置水印
const setWaterMark = (el: HTMLElement, binding: any) => {
  const { parentElement } = el;
  // 获取对应的 canvas 画布相关的 base64 url
  const url = getDataUrl(binding);
  // 创建 waterMark 父元素
  const waterMark = globalWaterMark || document.createElement("div");
  waterMark.className = `water-mark`; // 方便自定义展示结果
  waterMark.setAttribute("style", `background-image: url(${url});`);
  // 将对应图片的父容器作为定位元素
  parentElement.setAttribute("style", "position: relative;");
  // 将图片元素移动到 waterMark 中
  parentElement.appendChild(waterMark);
};
const directives: any = {
  mounted(el: HTMLElement, binding: any) {
    el.onload = setWaterMark.bind(null, el, binding.value);
  },
};
export default {
  name: "watermark",
  directives,
};
/*********  css 部分  ***********/ 
.water-mark {
  display: inline-block;
  overflow: hidden;
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  background-repeat: repeat;
}


目录
相关文章
|
10天前
|
JSON 前端开发 搜索推荐
惊!这些前端技术竟然能让你的网站实现个性化推荐功能!
【10月更文挑战第30天】随着互联网技术的发展,个性化推荐已成为提升用户体验的重要手段。前端技术如JavaScript通过捕获用户行为数据、实时更新推荐结果等方式,在实现个性化推荐中扮演关键角色。本文将深入解析这些技术,并通过示例代码展示其实际应用。
39 4
|
4月前
|
存储 开发框架 前端开发
循序渐进VUE+Element 前端应用开发(18)--- 功能点管理及权限控制
循序渐进VUE+Element 前端应用开发(18)--- 功能点管理及权限控制
|
1月前
|
前端开发 JavaScript
前端中的“+”连接符,居然有鲜为人知的强大功能!
【10月更文挑战第9天】前端中的“+”连接符,居然有鲜为人知的强大功能!
42 0
前端中的“+”连接符,居然有鲜为人知的强大功能!
|
2月前
|
前端开发 API
(WEB前端编辑DWG)在线CAD如何实现图形识别功能
mxcad 提供的图形识别功能可帮助用户快速识别和提取 CAD 图纸中的各种图形,如直线、多段线、弧线、圆及图块,显著提升设计效率。此功能不仅适用于图形分类,还能进行数量统计和快速定位,减少手动操作。用户可通过 API 进行二次开发,自定义识别逻辑。具体步骤包括打开在线示例、选择识别功能、设置识别参数并开始识别。更多开发文档请关注公众号:梦想云图网页 CAD。
|
28天前
|
Web App开发 存储 前端开发
前端开发必备:requestAnimationFrame、setInterval、setTimeout——功能解析与优劣对比
前端开发必备:requestAnimationFrame、setInterval、setTimeout——功能解析与优劣对比
102 0
|
28天前
|
移动开发 前端开发 JavaScript
前端开发实战:利用Web Speech API之speechSynthesis实现文字转语音功能
前端开发实战:利用Web Speech API之speechSynthesis实现文字转语音功能
139 0
|
1月前
|
前端开发 JavaScript Shell
深入解析前端构建利器:webpack核心概念与基本功能全览
深入解析前端构建利器:webpack核心概念与基本功能全览—
23 0
|
2月前
|
存储 JSON 前端开发
node使用token来实现前端验证码和登录功能详细流程[供参考]=‘很值得‘
本文介绍了在Node.js中使用token实现前端验证码和登录功能的详细流程,包括生成验证码、账号密码验证以及token验证和过期处理。
44 0
node使用token来实现前端验证码和登录功能详细流程[供参考]=‘很值得‘
|
2月前
|
前端开发 开发者
在前端开发中,webpack 作为一个强大的模块打包工具,为我们提供了丰富的功能和扩展性
【9月更文挑战第1天】在前端开发中,Webpack 作为强大的模块打包工具,提供了丰富的功能和扩展性。本文重点介绍 DefinePlugin 插件,详细探讨其原理、功能及实际应用。DefinePlugin 可在编译过程中动态定义全局变量,适用于环境变量配置、动态加载资源、接口地址配置等场景,有助于提升代码质量和开发效率。通过具体配置示例和注意事项,帮助开发者更好地利用此插件优化项目。
80 13
|
3月前
|
开发者 Android开发 iOS开发
Xamarin开发者的神器!揭秘你绝不能错过的插件和工具,让你的开发效率飞跃式提升
【8月更文挑战第31天】Xamarin.Forms 是一个强大的框架,让开发者通过单一共享代码库构建跨平台移动应用,支持 iOS、Android 和 Windows。使用 C# 和 XAML,它简化了多平台开发流程,保持一致的用户体验。本指南通过创建一个简单的 “HelloXamarin” 应用介绍 Xamarin.Forms 的基本功能和工作原理。首先配置 Visual Studio 开发环境,然后创建并运行一个包含标题、按钮和消息标签的示例应用,展示如何定义界面布局及处理按钮点击事件。这帮助开发者快速入门 Xamarin.Forms,提高跨平台应用开发效率。
42 0