聊一聊如何在 Vue 项目中实现水印功能
作者:熊的猫
https://juejin.cn/post/7132620574198595597
前言
由于项目需要实现水印功能,于是去了解了相关的内容后,基于 Vue
的实现了一个 v-watermark
指令完成了对应的功能,其实整体内容并不复杂!
那么接下来先来简单了解一些和 Vue
自定义指令相关的内容,作为前置知识的铺垫,然后在逐步完成对应的功能。
vue 中的自定义指令
以下的内容其实简单了解即可,甚至可以直接跳过,遇到不认识的内容在回头来查看都是可以的,甚至可以直接查看 官方文档[1]
是什么?
在 Vue3
中 复用代码 有如下三种方式:
组件[2]:组件是主要的构建模块
组合式函数[3]:合式函数则侧重于有状态的逻辑
自定义指令[4]:主要是为了重用涉及普通元素的底层 DOM 访问的逻辑
因此,自定义指令本身也是一种代码逻辑复用的方式,只是着重点在 对底层 DOM 的访问和操作 上。
指令钩子
在 Vue
、React
、Webpack
、Vite
等中都会存在着对应的 钩子,而这些 钩子 本质上就是一些在 特定时机 会被执行的 函数/方法 而已,那么在 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
[5] 中的自定义钩子是有些不同的,具体可以点击对应链接比对查看.
钩子参数
其实直接看参数的命名方式,相信你也能知道大部分的参数代表什么:
el
:指令绑定到的元素,意味着可通过el
直接操作DOM
binding
:一个对象,包含以下属性value
:传递给指令的值,例如在v-my-directive="1 + 1"
中,值是2
oldValue
:之前的旧值,仅在beforeUpdate
和updated
中可用,无论值是否更改都可用arg
:传递给指令的参数 (如果有的话),如在v-my-directive:foo
中,参数是"foo"
modifiers
:一个包含修饰符的对象 (如果有的话),如在v-my-directive.foo.bar
中,修饰符对象是{ foo: true, bar: true }
instance
:使用该指令的组件实例dir
:指令的定义对象
vnode
:代表绑定元素的底层VNode
prevNode
:之前的渲染中代表指令所绑定元素的VNode
,仅在beforeUpdate
和updated
钩子中可用
【注意】除了
el
外(因为需要操作DOM
),其他参数都是只读的,不建议更改,如果需要在不同的钩子间共享信息,推荐通过元素的 dataset[6] 属性实现
实现水印功能
几种实现方案
基于原图生成水印图片(后端)
这种方案就是将 原图片 添加水印之后生成了 新图片,后续在前端页面进行展示是后端接口不返回原图片,而是返回带有水印的图片即可。
这种方式最大的优点就是安全,因为 水印图片 是后端生成的,前端只需要负责展示即可,不需考虑多余的问题,且即便在前端页面保存对应图片,拿到的仍然不是原图片。
基于 DOM 实现水印效果(前端)
自定义指令钩子非常多,但实际上能使用到的不多,比如最常用的就是 mounted
、updated
,在这我们只需要通过 mounted
即可实现对应的功能,并且核心代码比较简单。
核心内容
创建一个
watermark
的DOM
节点,即div
元素,用于包裹对应的img
便于展示水印内容在创建一个
waterbg
的DOM
节点,即div
元素将
waterbg
节点作为watermark
的 子节点,并进行 绝对定位 保证waterbg
在 最上层显示将对应的 水印标记 作为
waterbg
节点的 背景图片 展示为
waterbg
节点设置pointer-events: none;
实现 点击穿透
将
watermark
节点通过insertBefore(...)
插入到img
标签的前一个位置再将
img
标签移动到watermark
节点节中,这样就保证了新创建的watermark
节点的位置一定是在原本img
挂载的位置
效果和代码如下
// 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
实现方式的优点就在于能够动态的设置水印内容,相比于上一种基于固定背景图片的方式更灵活,这种方式也是 语雀 在使用的方式,具体效果如下:
核心步骤
通过
canvas
填充文本,并通过canvas.toDataURL("image/png");
获取到对应的base64
格式的图片将这个
base64
格式的图片作为类名为water-mark
节点的背景图利用
background-repeat: repeat;
让这个图重复填充背景即可为
water-mark
节点设置pointer-events: none;
实现 点击穿透利用对应图片的父元素作为
water-mark
节点的相对定位节点,保证绝对定位的water-mark
节点显式在对应图片之上
效果和代码如下
/********* 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 urlconst 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;
}
使用 MutationObserver 优化
以上提到的两种前端实现方案,都存在一个问题很明显的问题,那就是用于只要用户通过 开发者调试工具 来稍微操作一,旧能够导致水印失效:
删除对应
dom
节点设置对应
dom
节点的css
样式
MutationObserver[7] 接口提供对 DOM
树监听的能力,它能够监听 DOM
树属性、节点本身、子节点等的变化,于是优化的思路就是使用 MutationObserver 去监听外部对应 water-mark
节点的操作,只要监听到了就重新渲染水印效果即可。
效果和代码
【注意】这里最容易踩坑的点就是 MutationObserver 中的条件写得不正确的话会导致死循环.
/********* src/directives/waterMark.ts ***********/ // 全局保存 canvas 和 div ,避免重复创建(单例模式)
const globalCanvas = null;
const globalWaterMark = null;// watermark 样式
let style = `
display: block;
overflow: hidden;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-repeat: repeat;
pointer-events: none;`;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 urlconst url = getDataUrl(binding);// 创建 waterMark 父元素const waterMark = globalWaterMark || document.createElement("div");waterMark.className = `water-mark`; // 方便自定义展示结果style = `${style}background-image: url(${url});`;waterMark.setAttribute("style", style);// 将对应图片的父容器作为定位元素parentElement.setAttribute("style", "position: relative;");// 将图片元素移动到 waterMark 中parentElement.appendChild(waterMark);
};// 监听 DOM 变化
const createObserver = (el: HTMLElement, binding: any) => {const waterMarkEl = el.parentElement.querySelector(".water-mark");const observer = new MutationObserver((mutationsList) => {if (mutationsList.length) {const { removedNodes, type, target } = mutationsList[0];const currStyle = waterMarkEl.getAttribute("style");// 证明被删除了if (removedNodes[0] === waterMarkEl) {observer.disconnect();init(el, binding);} else if (type === "attributes" &&target === waterMarkEl &&currStyle !== style) {waterMarkEl.setAttribute("style", style);}}});observer.observe(el.parentElement, {childList: true,attributes: true,subtree: true,});
};// 初始化
const init = (el: HTMLElement, binding: any = {}) => {// 设置水印setWaterMark(el, binding.value);// 启动监控createObserver(el, binding.value);
};// 定义指令配置项
const directives: any = {mounted(el: HTMLElement, binding: any) {el.onload = init.bind(null, el, binding);},
};export default {name: "watermark",directives,
};
最后
上述过程中我们做了那么多优化,最终的结果看起来比较还算是可以接受,但实际上前端的实现方案终归是不够完美的,比如有心人直接复制图片对应的地址怎么办?又或者是通过开发者调试工具选择禁用 JavaScript
又怎么办呢?
因此,总结下来前端的实现方案是属于:防君子不防小人
的方案,不过也不必过于纠结这一点,毕竟 语雀 这样的网站连 MutationObserver
都没加呢 ~~
参考资料
[1]
官方文档: https://cn.vuejs.org/guide/reusability/custom-directives.html
[2]
组件: https://cn.vuejs.org/guide/essentials/component-basics.html
[3]
组合式函数: https://cn.vuejs.org/guide/reusability/composables.html
[4]
自定义指令: https://cn.vuejs.org/guide/reusability/custom-directives.html
[5]
vue2
: https://v2.cn.vuejs.org/v2/guide/custom-directive.html
[6]
dataset: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
[7]
MutationObserver: https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
- EOF -
聊一聊如何在 Vue 项目中实现水印功能相关推荐
- 如何在vue项目中修改less变量,多主题项目解决方案
如何在vue项目中修改less变量,多主题项目解决方案 参考文章: (1)如何在vue项目中修改less变量,多主题项目解决方案 (2)https://www.cnblogs.com/niubilit ...
- 如何在vue项目中使用Highmaps(vue+Highmaps)
如何在vue项目中使用Highmaps 功能需求 思路 分析 实现 第一步:引入 第二步:介绍一下这个world是个啥. 第三步:调用 注意点(十分重要) 写在最后 功能需求 近日我接到了这么一个需求 ...
- 如何在vue项目中设置首页
如何在vue项目中设置首页 1.首先创建一个vue为后缀名的文件,最好在新建的文件夹下面创建,如下图所示: 说明:一般我们的主要页面代码都是写在vue 项目中的这个src文件夹下,项目中所会引用到的组 ...
- 如何在vue项目中系统的使用iconfont字体图标
如何在vue项目中系统的使用iconfont字体图标 前言 iconfont的特色 官网注册 在项目中使用的方式 前言 今天聊一下前端iconfont的使用方式,前端的图标经过这些年的发展,变得更系统 ...
- 如何在vue项目中使用lodop打印插件
如何在vue项目中使用lodop打印插件 - 简书 先写了个webdemo <%--Created by IntelliJ IDEA.User: AdministratorDate: 2018/ ...
- vux 显示 html,如何在vue项目中使用vux
编者:只需6步,教你如何在vue项目中使用vux, 1.在项目里安装vuxnpm install vux --save2.安装vux-loader (这个vux文档中没有明文跟你说要安装的啦) np ...
- 如何在Vue项目中使用vw实现移动端适配(转)
有关于移动端的适配布局一直以来都是众说纷纭,对应的解决方案也是有很多种.在<使用Flexible实现手淘H5页面的终端适配>提出了Flexible的布局方案,随着viewport单位越来越 ...
- 如何在Vue项目中引入ArcGIS JavaScript API 创建三维可视化地图(含vue项目创建教程)
新手上路之在Vue项目中引入ArcGIS API 视频教程 B站搜索 X北辰北,感谢up主无私的教学~ B站地址:https://www.bilibili.com/video/BV18E411K7B ...
- 如何在Vue项目中应用TypeScript?
一.前言 在VUE项目中应用typescript,我们需要引入一个库vue-property-decorator, # npm 下载 npm i vue-property-decorator -s # ...
最新文章
- 融合基础设施会给私有云带来什么?
- mysql建表的auto_increment_koa2+koa+mysql快速搭建nodejs服务器
- java biginteger转int_如何在不使用java.math.BigInteger的情况下使用Java处理非常大的数字...
- VS2010上使用Qt 5.2.1出现 “There's no Qt version assigned to this project for platform Win32”错误的解决办法
- [POJ 1442]Black Box
- CSS清除默认样式,面试篇
- SpringCloud的Ribbon负载均衡
- html-webpack-template, 一个更好的html web service插件
- Android InputType
- python解释型语言_python是解释型语言吗?会被编译吗?
- 【我的Android进阶之旅】APK反编译教程和工具下载
- IP抓包精准定位教程
- Linux编程经典实例,PyQt4 精彩实例分析 - 实例1 Hello Kitty! _Linux编程_Linux公社-Linux系统门户网站...
- python floor函数_Python floor() 函数 - Python 教程 - 自强学堂
- 2018年中高级前端面试题目小结
- Linux通过黑、白名单限制IP登录
- spoj p104 Matrix-Tree定理
- Java并发工具类:CountDownLatch、Semaphore、CyclicBarrier、Exchanger、Phaser
- 2018年关于人工智能、大数据和分析的十大预测
- Mac电脑安装其他系统
热门文章
- YOLO算法(论文部分翻译+一些总结)
- iOS 键盘限制只能数字和字母
- 并发下php脚本执行,php命令行脚本多进程并发执行
- ubuntu 18.04编译安装PANet
- 管道式广谱感应水处理器详细介绍
- 解析BeanFactory的核心方法——getBean
- Error type 三: Activity Class {.} dose not exist【解决方案】
- OneNote 开发与使用
- 自控力 何常明 读书笔记
- linux权限命令chgrp,Linux常用命令之用户权限管理chmod、chown、chgrp、umask命令讲解...