水印相关
- 引言
- 绘制一个水印
- 输出背景图
- 封装一点点细节
- 图片加水印
- 防止水印删除
- 问题
- 解决方案
引言
在企业里为了防止信息泄露和保护知识产权,通常会在页面和图片上添加水印
前端页面水印的添加一般有这几种方式:dom 元素循环、canvas 输出背景图、svg 实现背景图、图片添加水印
dom 元素循环 性能太低也不优雅,一般不采用这种方式
svg 实现背景图 与 canvas 类似,且兼容性不如 canvas
图片添加水印 是针对在图片上加的水印
本篇文章重点讲一下canvas 输出背景图、以及防止删除的方案来生成水印方式
绘制一个水印
目标是实现页面上按照一定的排列展示的水印,那么首先要用 canvas 画出一个水印
我们先在 html 放一个 canvas 标签,对它进行一个绘制
<template><canvas id="water"></canvas>
</template>
onMounted(() => {const canvas: HTMLCanvasElement = document.getElementById('water') as HTMLCanvasElementcanvas.width = 440canvas.height = 400const ctx = canvas.getContext('2d')if (ctx) {ctx.font = '60px PingFang SC'ctx.fillStyle = 'rgba(156, 162, 169, 0.3)'ctx.rotate(-0.4)ctx.fillText('krryguo', 20, 280)}
})
页面效果如图:
一个水印画出来了,怎么衍生多个水印并添加到指定页面中呢?
输出背景图
直接循环多份代码绘制是不合理的。我们可以利用 background 属性 repeat 特性,将水印展示成多个且平铺在整个页面中
将 canvas 生成的画布输出成base64的字符串,来作为页面的背景图。那么 dom 结构可以不需要 canvas 元素了,动态生成即可
<template><div class="water-mark">首页</div>
</template>
onMounted(() => {const canvas: HTMLCanvasElement = document.createElement('canvas')canvas.width = 440canvas.height = 400const ctx = canvas.getContext('2d')if (ctx) {ctx.font = '60px PingFang SC'ctx.fillStyle = 'rgba(156, 162, 169, 0.3)'ctx.rotate(-0.4)ctx.fillText('krryguo', 40, 200)}const imgStr = canvas.toDataURL('image/png')const waterDom = document.getElementsByClassName('water-mark')[0] as HTMLElementwaterDom.style.background = `url(${imgStr})`
})
实现的效果图:
看到这里,有朋友不高兴了,排列的太整齐,你这水印有问题啊~
最简单解决方式就直接绘制 两个斜对称排列 的水印即可
onMounted(() => {const canvas: HTMLCanvasElement = document.createElement('canvas')canvas.width = 880 // 原有的基础上增加一倍宽度canvas.height = 400const ctx = canvas.getContext('2d')if (ctx) {ctx.font = '60px PingFang SC'ctx.fillStyle = 'rgba(156, 162, 169, 0.3)'ctx.rotate(-0.4)ctx.fillText('krryguo', 40, 200)ctx.fillText('krryguo', 350, 556) // 再画一个}const imgStr = canvas.toDataURL('image/png')const waterDom = document.getElementsByClassName('water-mark')[0] as HTMLElementwaterDom.style.background = `url(${imgStr})`
})
再看效果图:
封装一点点细节
interface WatermarkOptions {// 宽度width?: number// 高度height?: number// 水印内容content?: string// 水印字体font?: string// 水印颜色color?: string// 透明度opacity?: number// 偏转角度degree?: number// 偏移量x1?: numbery1?: numberx2?: numbery2?: number
}const createWatermark = ({width = 880,height = 400,content = 'krryguo',font = '60px PingFang SC',color = 'rgba(156, 162, 169, 0.3)',opacity = 1,degree = -23,x1 = 40,y1 = 200,x2 = 350,y2 = 556
}: WatermarkOptions): string => {const canvas: HTMLCanvasElement = document.createElement('canvas')canvas.width = widthcanvas.height = heightconst ctx = canvas.getContext('2d')if (ctx) {ctx.font = fontctx.fillStyle = colorctx.globalAlpha = opacity// 顺时针旋转的弧度,计算公式: degree * Math.PI / 180ctx.rotate((degree * Math.PI) / 180)ctx.fillText(content, x1, y1)ctx.fillText(content, x2, y2)}return canvas.toDataURL('image/png')
}const setWatermarkClass = (url: string, className: string): void => {const style = document.createElement('style')style.innerHTML = `.${className} {background-image: url(${url});}`document.head.appendChild(style)
}
可在有需要加水印的地方调用 setWatermarkClass,传入自定义配置和指定的 class 名,再在对应元素设置该 class 名即可加上水印
onMounted(() => {setWatermarkClass(createWatermark({content: 'krryblog'}),'my-water-mark')
})
<template><!-- 这里加上 my-water-mark 水印的类名 --><div class="water-mark my-water-mark">首页</div>
</template>
图片加水印
同理,先读取图片,canvas 在图片上绘制水印
import waterUrl from '@/assets/water.jpeg'const createImgWatermark = async ({url = '',textAlign = 'center',textBaseline = 'middle',font = '30px PingFang SC',fillStyle = '#fff',x = 120,y = 50,position = 'top-start'},content: string = '这是水印'
) => {const canvas: HTMLCanvasElement = document.createElement('canvas')const img = new Image()img.src = urlimg.setAttribute('crossOrigin', 'Anonymous')return new Promise((resolve) => {img.onload = () => {canvas.width = img.widthcanvas.height = img.heightconst ctx = canvas.getContext('2d')if (ctx) {ctx.drawImage(img, 0, 0)ctx.textAlign = textAlignctx.textBaseline = textBaselinectx.font = fontctx.fillStyle = fillStyleswitch (position) {case 'top-end':x = img.width - xbreakcase 'bottom-start':y = img.height - ybreakcase 'bottom-end':x = img.width - xy = img.height - ybreak}ctx.fillText(content, x, y)}resolve(canvas.toDataURL())}})
}const setImgWatermark = (url: string, dom: HTMLImageElement) => {dom.src = url
}onMounted(async () => {const url = await createImgWatermark({url: waterUrl,font: '50px PingFang SC',x: 160,y: 70,position: 'bottom-end'})setImgWatermark(url, document.querySelector('img') as HTMLImageElement)
})
<template><img width="600" />
</template>
效果图:
防止水印删除
前端生成水印的安全性是很弱的,懂点前端知识的人都会打开控制台修改去掉水印
这里提供一个方案,禁止用户删除 class 来防止水印删除
window 提供了一个监听器MutationObserver:监视对 DOM 树所做更改的能力
API 方法
- disconnect()
阻止 MutationObserver 实例继续接收的通知,直到再次调用其 observe()方法,该观察者对象包含的回调函数都不会再被调用 - observe()
配置 MutationObserver 在 DOM 更改匹配给定选项时,通过其回调函数开始接收通知 - takeRecords()
从 MutationObserver 的通知队列中删除所有待处理的通知,并将它们返回到 MutationRecord 对象的新 Array 中
先获取有水印类名的NodeList,遍历所有元素为其添加一个MutationObserver监听器来监听 dom 属性的变化,获取目标元素的classList,若不存在水印的 class 类名,就执行添加。执行添加之后要立刻暂停监听disconnect(),防止【添加】操作又触发监听器,最后再执行observe() 重新观察
// 添加监听器
const addListioner = (className: string) => {const MutationObserver = window.MutationObserver// 获取所有添加了水印类名的 domconst containerList: NodeListOf<HTMLElement> = document.querySelectorAll(`.${className}`)if (MutationObserver) {containerList.forEach((container) => {let observer = new MutationObserver(() => {// 获取 class 集合const classList: DOMTokenList = container.classListif (!Object.values(classList).includes(className)) {// 如果 classList 中不存在水印的类名,就重新添加container.classList.add(className)// 暂停监听,防止上面的操作又触发监听器observer.disconnect()// 然后再重新开始观察addObserve(observer, container)}})// 每个元素开启观察addObserve(observer, container)})}
}
// 开启观察
const addObserve = (mutation: MutationObserver, container: Element) => {mutation.observe(container, {// 观察器的配置,需要观察属性的变动attributes: true})
}
然后在 onMounted 添加一下监听器
onMounted(async () => {// TODO ...addListioner('my-water-mark')
})
问题
但是这又有一个问题,用户可以在控制台改变水印 class 里面的样式,而 MutationObserver 无法监听。如图:
这样水印说没就直接没啦
解决方案
解决方法:可以使用 style 属性 渲染水印,即 内联样式 ,这样若样式改变了就说明 dom 属性改变,也就可以监听到了
需要注意的是:要设置 !important,把优先级提到最高防止被恶意覆盖,后面的监听也要加上优先级的判断
onMounted(async () => {const bgUrl = createWatermark({content: 'krryblog'})// 使用 style 属性渲染水印const dom = document.querySelector('.water-mark-style') as HTMLElement// 设置样式优先级最高dom.style.setProperty('background-image', `url(${bgUrl})`, 'important')
})
缺点是控制台查看 dom 结构会有一大坨样式在这里…
最后再加上监听 顺带整合了 class 类名渲染、style 属性渲染两种监听方法:
interface StyleType {key: stringvalue: string
}onMounted(async () => {const bgUrl = createWatermark({content: 'krryblog'})// 使用 style 属性渲染水印const dom = document.querySelector('.water-mark-style') as HTMLElement// 设置样式优先级最高dom.style.setProperty('background-image', `url(${bgUrl})`, 'important')addListioner('water-mark-style', {key: 'background-image',value: `url("${bgUrl}")` // js 获取的样式值 url 里面加了 "",所以这里加上比对})
})// 添加监听器
const addListioner = (className: string, style?: StyleType) => {const MutationObserver = window.MutationObserver// 获取所有添加了水印类名的 domconst containerList: NodeListOf<HTMLElement> = document.querySelectorAll(`.${className}`)if (MutationObserver) {containerList.forEach((container: HTMLElement) => {// 每个元素监听const observer = new MutationObserver(() => {let flag = false // 触发改变的标识if (style) {// style 属性渲染水印// 获取 style 属性const styleCss: CSSStyleDeclaration = container.style// 需要比对样式是否存在、样式值是否相同、样式优先级是否最高if (!styleCss.getPropertyValue(style.key) ||styleCss.getPropertyValue(style.key) !== style.value ||styleCss.getPropertyPriority(style.key) !== 'important') {// 重新设置样式styleCss.setProperty(style.key, style.value, 'important')flag = true}} else {// class 类名渲染水印// 获取 class 集合const classList: DOMTokenList = container.classListif (!Object.values(classList).includes(className)) {// 如果 classList 中不存在水印的类名,就重新添加container.classList.add(className)flag = true}}if (flag) {// 暂停监听,防止上面的操作又触发监听器observer.disconnect()// 然后再重新开始观察addObserve(observer, container)}})// 每个元素开启观察addObserve(observer, container)})}
}
// 开启观察
const addObserve = (mutation: MutationObserver, container: Element) => {mutation.observe(container, {// 观察器的配置,需要观察属性的变动attributes: true})
}
监听效果查看: