个人主页:不叫猫先生
♂️ 作者简介:前端领域新星创作者、阿里云专家博主,专注于前端各领域技术,共同学习共同进步,一起加油呀!
系列专栏:vue3从入门到精通、TypeScript从入门到实践
资料领取:前端进阶资料以及文中源码可以找我免费领取
前端学习交流:博主建立了一个前端交流群,汇集了各路大神,一起交流学习,期待你的加入!(文末有我wx或者私信)
目录
- 前言
- 一、vue自定义指令directive讲解
- 二、基于DOM的实现方式
- 1. 思路整理
- 2.新建index.vue
- 3. 新建`directives`文件
- 4. 在`directives`文件下创建 `index.ts`文件
- 5. 在`main.ts`中全局引入
- 6. 缺点
- 三、基于Canvas和MutationObserver的实现方式
- 1. 思路整理
- 2. 生成水印
- 3. 使用MutationObserver监听水印
- 四、成果展示
- 附:文中用到的js基础知识
- toDataURL用法
前言
我们会看到很多页面带有水印,但是怎么实现呢?当然可以有多种实现方式,本文主要讲解在vue项目中基于DOM或者Cavans实现水印效果,当然还有其他的实现方式,比如在原图片的基础上加上水印生成新的图片,但是这需要后端处理。因为要在vue项目中使用,所以我使用自定义指令可以直接对挂载的dom实现水印效果。
本文实现水印的项目环境为:vue + vite + ts
一、vue自定义指令directive讲解
前面专门有一篇讲解vue2.x与vue3.x中自定义指令详解
二、基于DOM的实现方式
1. 思路整理
- 获取宽高
(1)获取绑定元素的实际宽度clientWidth
(2)获取绑定元素实际高度clientHeight
(3)获取绑定元素的父元素parentElement - 创建盒子
(1)创建一个包裹水印图片的盒子
(2)创建一个水印图片的盒子 - 设置盒子样式
(1)包裹水印盒子宽高为绑定元素的宽高,即clientWidth、clientHeight
(2)水印盒子设置背景图、旋转度、宽高、点击穿透 - 设置创建的元素的位置
(1)水印盒子放到包裹水印图片的盒子里 (包裹水印图片的盒子包裹水印)
(2)包裹水印图片的盒子放到被绑定元素之前
(3)被绑定元素放到裹水印图片的盒子里(不然被绑定元素与包裹水印图片的盒子层级同级)
2.新建index.vue
将水印的指令放到标签上,设置标签的宽高。水印可以放大div
标签上,也可以是img
标签上。注意:img
才有onload
方法,div
标签么有。
<script setup lang="ts">import { ref } from "vue";</script><template><div class="index-content" ><div class="watermaker" v-watermark ></div> <!-- <img v-watermark style="width:400px;height:400px" src="../assets/vue.svg" alt=""> --></div></template><style scoped>.watermaker {width: 400px;height: 400px;}.index-content{width: 100%;height: 100%;}</style>
3. 新建directives
文件
在directives
文件下创建waterMark.ts
文件,具体内容实现如下:
import waterImg from "@/assets/vue.svg"const directives: any = {mounted(el: HTMLElement) {//如果el元素是img,则可以用el.onload将下面包裹const { clientWidth, clientHeight, parentElement } = el;console.log(parentElement, 'parentElement')const waterMark: HTMLElement = document.createElement('div');const waterBg: HTMLElement = document.createElement('div');//设置waterMark的class和stylewaterMark.className = `water-mark`;waterMark.setAttribute('style', `display: inline-block;overflow: hidden;position: relative;width: ${clientWidth}px; height: ${clientHeight}px;`);// 创建waterBg的class和stylewaterBg.className = `water-mark-bg`;// 方便自定义展示结果waterBg.setAttribute('style', `position: absolute;pointer-events: none;`在这里插入代码片`transform: rotate(45deg);width: 100%;height: 100%;opacity: 0.2;background-image: url(${waterImg}); background-repeat: repeat;`);// 水印元素waterBg放到waterMark元素中waterMark.appendChild(waterBg);//waterMark插入到el之前,即插入到绑定元素之前parentElement?.insertBefore(waterMark, el);// 绑定元素移入到包裹水印的盒子waterMark.appendChild(el);}}export default {name: 'watermark',directives}
4. 在directives
文件下创建 index.ts
文件
import type { App } from 'vue'import watermark from './waterMark'export default function installDirective(app: App) {app.directive(watermark.name, watermark.directives);}
5. 在main.ts
中全局引入
import { createApp } from 'vue'import App from './App.vue'import directives from './directives'const app = createApp(App);app.use(directives);app.mount('#app');
6. 缺点
- 直接删除水印元素时,页面中的水印直接就被删除了,当然我们可以用
MutationObserver
对水印元素进行监听,删除时,我们再立即生成一个水印元素就可以了,具体方面在下面讲解。 - 如果原始元素本身存在 css 定位等规则,会导致整体布局效果出现影响,因为上面实现排除了原始元素没有定位,所以实现方式不是很严谨,本文具体实现实现如下:
- 创建一个水印的容器设置为
position:relative
- 将原有的节点放入到这个容器中
- 同时创建一个带有水印的 dom 设置为
position:absolute
,实现这个水印元素覆盖到原始元素的上层,以实现水印的效果。
- 创建一个水印的容器设置为
三、基于Canvas和MutationObserver的实现方式
1. 思路整理
- 配置水印的具体样式(大小,旋转角度,文字填充)
- 设置水印(位置)
- 监听dom变化(防止水印删除后页面不再展示水印)
2. 生成水印
通过将图片绘制在cavans
中,然后通过cavans
的toDataURL
方法,将图片转为base64编码。
// 全局保存 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 = -10;const canvas = globalCanvas || document.createElement("canvas");const ctx = canvas.getContext("2d"); // 获取画布上下文ctx.rotate((rotate * Math.PI) / 180);ctx.font = "16px normal";ctx.fillStyle = "rgba(180, 180, 180, 0.3)";ctx.textAlign = "left";ctx.textBaseline = "middle";ctx.fillText('请勿外传', canvas.width / 3, canvas.height / 2);return canvas.toDataURL("image/png");};
3. 使用MutationObserver监听水印
使用MutationObserver
监听dom变化,MutationObserver
详细用法之前已经讲过了,详细可见作为前端你还不懂MutationObserver?那Out了
具体监听逻辑如下:
- 1.直接删除dom
(1)先获取设置水印的dom
(2)监听到被删除元素的dom
(3)如果他两相等的话就停止观察,初始化(设置水印+启动监控) - 2.删除style中的属性
(1)判断删除的是否是标签的属性 (type === “attributes”)
(2)判断删除的标签属性是否是在设置水印的标签上
(3)判断修改过的style和之前的style对比,不等的话,重新赋值
// watermark 样式let style = `display: block;overflow: hidden;position: absolute;left: 0;top: 0;width: 100%;height: 100%;background-repeat: repeat;pointer-events: none;`;//设置水印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) => {console.log(el, 'el')console.log(style, 'style')// console.log(el.parentElement.querySelector('.water-mark'),'el.parentElement')const waterMarkEl = el.parentElement.querySelector(".water-mark");const observer = new MutationObserver((mutationsList) => {console.log(mutationsList, 'mutationsList')if (mutationsList.length) {const { removedNodes, type, target } = mutationsList[0];const currStyle = waterMarkEl.getAttribute("style");// console.log(currStyle, 'currStyle')// 证明被删除了// (1)直接删除dom// 1.先获取设置水印的dom// 2.监听到被删除元素的dom// 如果他两相等的话就停止观察,初始化(设置水印+启动监控)// (2) 删除style中的属性//1 判断删除的是否是标签的属性 (type === "attributes")//2.判断删除的标签属性是否是在设置水印的标签上//3.判断修改过的style和之前的style对比,不等的话,重新赋值if (removedNodes[0] === waterMarkEl) {console.log(removedNodes[0])// 停止观察。调用该方法后,DOM 再发生变动,也不会触发观察器。observer.disconnect();//初始化(设置水印,启动监控)init(el, binding);} else if (type === "attributes" &&target === waterMarkEl &&currStyle !== style) {console.log(currStyle, 'currStyle')console.log(style, '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) {//注意img有onload的方法,如果自定义指令注册在html标签的话,只需要init(el, binding.value)el.onload = init.bind(null, el, binding.value);},};
四、成果展示
删除水印标签依然还在,除非删除水印注册的标签才能删除水印,但是这样做毫无意义,因为这样做内容也会全部删除掉。
附:文中用到的js基础知识
toDataURL用法
toDataURL(type, encoderOptions)
,接收两个参数:
- type:图片类型,比如
image/png、image/jpeg、image/webp
等等,默认为image/png
格式 - encoderOptions:图片质量的取值范围(0-1),默认值为0.92,当超出界限按默认值0.92