引言
在企业里为了防止信息泄露和保护知识产权,通常会在页面和图片上添加水印
前端页面水印的添加一般有这几种方式: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 HTMLCanvasElement
canvas.width = 440
canvas.height = 400
const 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 = 440
canvas.height = 400
const 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 HTMLElement
waterDom.style.background = `url(${imgStr})`
})
实现的效果图:
看到这里,有朋友不高兴了,排列的太整齐,你这水印有问题啊~
最简单解决方式就直接绘制 两个斜对称排列 的水印即可
onMounted(() => {
const canvas: HTMLCanvasElement = document.createElement('canvas')
canvas.width = 880 // 原有的基础上增加一倍宽度
canvas.height = 400
const 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 HTMLElement
waterDom.style.background = `url(${imgStr})`
})
再看效果图:
封装一点点细节
interface WatermarkOptions {
// 宽度
width?: number
// 高度
height?: number
// 水印内容
content?: string
// 水印字体
font?: string
// 水印颜色
color?: string
// 透明度
opacity?: number
// 偏转角度
degree?: number
// 偏移量
x1?: number
y1?: number
x2?: number
y2?: 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 = width
canvas.height = height
const ctx = canvas.getContext('2d')
if (ctx) {
ctx.font = font
ctx.fillStyle = color
ctx.globalAlpha = opacity
// 顺时针旋转的弧度,计算公式: degree * Math.PI / 180
ctx.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 = url
img.setAttribute('crossOrigin', 'Anonymous')
return new Promise((resolve) => {
img.onload = () => {
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
if (ctx) {
ctx.drawImage(img, 0, 0)
ctx.textAlign = textAlign
ctx.textBaseline = textBaseline
ctx.font = font
ctx.fillStyle = fillStyle
switch (position) {
case 'top-end':
x = img.width - x
break
case 'bottom-start':
y = img.height - y
break
case 'bottom-end':
x = img.width - x
y = img.height - y
break
}
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
// 获取所有添加了水印类名的 dom
const containerList: NodeListOf<HTMLElement> = document.querySelectorAll(
`.${className}`
)
if (MutationObserver) {
containerList.forEach((container) => {
let observer = new MutationObserver(() => {
// 获取 class 集合
const classList: DOMTokenList = container.classList
if (!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: string
value: 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
// 获取所有添加了水印类名的 dom
const 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.classList
if (!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
})
}
监听效果查看: