有时候我们的网站有些内容需要添加上水印,有时候我们浏览下载一些有水印的资料时又想去除水印。本篇文章就来说说关于水印的创建与去除,以及防止删除水印的手段,最后是反制水印防删的方法。
水印的创建
先假设有如下浅蓝色区域的内容需要添加上以掘金 logo 作为单个图像的水印:
该区域使用了一个 div
生成:
html
<div class="content">比如说我是一块需要打上水印的区域</div>
样式如下:
css
.content {
width: 600px;
height: 400px;
background-color: aliceblue;
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
}
我将使用 canvas(关于 canvas 的更多知识,可以移步《canvas 实现卫星绕月动画》) 来创建水印,canvas 的创建在引入的 index.js 中定义,而不是直接在 html 文件使用 <canvas>
,目的在于方便后续的防删除处理 :
javascript
// index.js
class DrawWaterMark {
constructor(width, height) {
// 水印区域的宽高
this.width = width
this.height = height
this.init()
}
init() {
const canvas = document.createElement('canvas')
// 如果不支持 canvas
if (!canvas.getContext) {
return
}
canvas.width = this.width
canvas.height = this.height
// 通过 style 添加样式,避免 css 被直接修改
canvas.style.position = 'absolute'
canvas.style.top = 0
canvas.style.left = 0
canvas.style.opacity = 0.2
bodyNode.appendChild(canvas)
const ctx = canvas.getContext('2d')
this.draw(ctx)
}
draw(ctx) {
// 创建临时 canvas,目的在于控制图片大小
const width = 200
const height = 100
const tempCanvas = document.createElement('canvas')
tempCanvas.width = width
tempCanvas.height = height
const tempCtx = tempCanvas.getContext('2d')
tempCtx.save()
tempCtx.translate(width / 2, height / 2)
tempCtx.rotate((Math.PI / 180) * 30)
const juejin = new Image()
juejin.src = './imgs/logo.svg'
juejin.onload = () => {
tempCtx.drawImage(juejin, 0, 0, 48, 10)
tempCtx.restore()
const pattern = ctx.createPattern(tempCanvas, 'repeat')
ctx.fillStyle = pattern
ctx.fillRect(0, 0, this.width, this.height)
}
}
}
const drawWaterMark = new DrawWaterMark(600, 400)
其中,ctx.createPattern()
是使用指定的图像创建模式的方法:
- 其第 1 个参数为作为重复图像源的对象,它的值可以是个图片元素或 canvas 元素等,也就是说我们可以直接通过
const img = new Image()
,让img.src
的值为 png 或 svg 等图片格式的掘金 logo,然后以img
作为第 1 个参数的值。但是为了控制水印中单个图像的尺寸大小,我创建了一个临时的 canvas 元素tempCanvas
,这样就可以控制tempCanvas
的宽高,并移动旋转其默认坐标空间,让绘制出来的掘金 logo 呈现旋转 30° 的效果。 - 第 2 个参数就是指定如何重复图像,传入
'repeat'
意为在水平和垂直方向均重复图像。
返回值 pattern
可以作为 ctx.fillStyle
的值,最后通过 ctx.fillRect()
绘制出在 x、y 轴方向重复掘金 logo 的水印效果:
水印的删除
想要删除掉如上创建的水印,简直易如反掌,只需小手一点,按下 f12 调出调试工具,然后找到 canvas 元素,鼠标右击,选择"删除元素"即告成功:
或者也可以选择修改 canvas 的 style 样式,调整透明度或者设置上 display: none
等,也可以起到让水印消失的目的:
水印的防删(MutationObserver)
为了防止有人使用以上所示的小诡计删除我们辛苦创建的水印,可以借助 MutationObserver
,它提供了监视对 DOM 树所做更改的能力,之前讲事件循环的时候在微任务队列提到过它。通过 new MutationObserver()
创建一个观察器实例 observer
,传入的 callback
是一个回调函数,会在指定的 DOM 发生变化时调用:
javascript
const callback = mutationsList => {
console.log(mutationsList)
}
const observer = new MutationObserver(callback)
比如我们想监听 <canvas>
节点,先去获取它:
javascript
const canvasNode = document.querySelector('canvas')
然后就可以通过之前创建的实例对象 observer
的 observe()
方法,传入的第 1 个参数为要观察的 DOM 节点, 第 2 个参数是一个配置对象 options:
javascript
observer.observe(canvasNode, {
attributes: true
})
attributes
为 true
表示观察所有监听的节点属性值的变化,当我们去改变 <canvas>
的属性,比如修改了 style
属性,让 opacity
为 0,就会触发 callback
,打印 mutationsList
:
mutationsList
是一个数组(可以同时监听多个节点),里面记录着被修改节点的信息:
如此一来,当有人想通过修改样式的方法去除水印时,就会被我们当场逮捕。但是请注意,当被观察的 <canvas>
本身被删除时,是不会触发 callback
的,所以我们应该监听 <canvas>
节点的父节点 <body>
:
javascript
const bodyNode = document.querySelector('body')
observer.observe(bodyNode, {
childList: true,
subtree: true,
attributes: true
})
childList
为 true
,表示监听 <body>
节点中发生的节点的新增与删除,这样我们就能监听到是否有人偷偷地删除了 <canvas>
:
展开 [MutationRecord]
如下:
让 subtree
为 true
,以表示监听以 <body>
为根节点的整个子树,配合 attributes: true
就能同时监听到作为子节点的 <canvas>
的属性是否改变。
一旦监听到有人对 <canvas>
做了手脚,我们就可以在传入 new MutationObserver(callback)
的 callback
回调中进行处理:
javascript
const callback = mutationsList => {
mutationsList.forEach(recode => {
// 如果是删除 canvas
if (recode.target.localName === 'body') {
recode.removedNodes.forEach(node => {
if (node.localName === 'canvas') {
this.init()
}
})
}
// 如果是修改 canvas 属性
if (recode.target.localName === 'canvas') {
bodyNode.removeChild(recode.target)
}
})
}
处理时分 2 种情况:
- 如果是直接删除了
<canvas>
,那么callback
调用时传入的mutationsList
的数组中,就会有一条记录的target
指向body
,展开target
对象可以看到其有个localName
属性,值为"body"
:
并且 removedNodes
为 NodeList [canvas]
,既然类型为 NodeList,那么对于 removedNodes
就不能随便使用数组方法了,但是依然可以使用 forEach
进行遍历,判断是否存在 localName
为 "canvas"
的成员。如果有,证明删除了 <canvas>
,则再次调用我们定义的 init()
实例方法,再次生成水印。
- 如果是改变了
<canvas>
的属性,那么mutationsList
数组中就会有一条记录的target
指向canvas
。此时我们可以直接删除掉原本的canvas
,就会再次触发callback
,按情况 1 处理。
现在,当有人想去除水印时,就会如下图所示,无功而返:
反制防删
尽管我们做了一些措施防止水印被人去除,但这些措施都是基于 js 的,只需要点击如下图所示的"设置":
勾选上"禁用 JavaScript":
那么依旧可以轻松通过删除 <canvas>
节点或修改属性的方式去除水印,接着奏乐,接着舞。