前端实现水印效果
目录
前端实现水印效果
一、问题背景
为了防止信息泄露或知识产权被侵犯,在web的世界里,对于页面和图片等增加水印处理是十分有必要的,水印的添加根据环境可以分为两大类, 前端 浏览器环境添加和 后端 服务环境添加,简单对比一下这两种方式的特点:
前端浏览器加水印:
- 减轻服务端的压力,快速反应
- 安全系数较低,对于掌握一定前端知识的人来说可以通过各种骚操作跳过水印获取到源文件
- 适用场景:资源不跟某一个单独的用户绑定,而是一份资源,多个用户查看,需要在每一个用户查看的时候添加用户特有的水印,多用于某些机密文档或者展示机密信息的页面,水印的目的在于文档外流的时候可以追究到责任人 后端服务器加水印:
- 当遇到大文件密集水印,或是复杂水印,占用服务器内存、运算量,请求时间过长
- 安全性高,无法获取到加水印前的源文件
- 适用场景:资源为某个用户独有,一份原始资源只需要做一次处理,将其存储之后就无需再次处理,水印的目的在于标示资源的归属人 这里我们讨论前端浏览器环境添加
二、实现效果
三、实现方案
1. 重复的dom元素覆盖实现
第一时间想到的方案是在页面上覆盖一个position:fixed的div盒子,盒子透明度设置较低,设置pointer-events: none;样式实现点击穿透,在这个盒子内通过js循环生成小的水印div,每个水印div内展示一个要显示的水印内容,简单实现
这种方案需要要在js内循环创建多个dom元素,既不优雅也影响性能,于是考虑可不可以不生成这么多个元素。
2. canvas画出背景图 MutationObserver监听
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>水印</title>
</head>
<body>
<div id="content">
<p>这是一段内容</p>
</div>
<script>
// 定义水印函数
function addWatermark({
container = document.body, // 水印添加到的容器,默认为 body
width = "300px", // 水印 canvas 的宽度
height = "250px", // 水印 canvas 的高度
textAlign = "center", // 水印文字的对齐方式
textBaseline = "middle", // 水印文字的基线
font = "16px Microsoft Yahei", // 水印文字的字体
fillStyle = "rgba(184, 184, 184, 0.6)", // 水印文字的填充样式
content = "水印", // 水印文字的内容
rotate = "45", // 水印文字的旋转角度
zIndex = 10000, // 水印的 z-index 值
}) {
// 生成水印 canvas
const canvas = document.createElement("canvas");
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
const ctx = canvas.getContext("2d");
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.font = font;
ctx.fillStyle = fillStyle;
ctx.rotate((Math.PI / 180) * rotate);
ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
// 将 canvas 转换为 base64 URL
const base64Url = canvas.toDataURL();
const __wm = document.querySelector('.__wm');
const watermarkDiv = __wm || document.createElement("div");
const styleStr = `
position: fixed;
top: -70px;
left: 0;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
z-index: ${zIndex};
pointer-events: none;
background-repeat: repeat;
background-image: url('${base64Url}')
`;
watermarkDiv.setAttribute("style", styleStr);
watermarkDiv.classList.add("__wm");
//则创建一个 div 并设置样式和类名
if (!__wm) {
container.style.position = 'relative';
container.insertBefore(watermarkDiv, container.firstChild);
}
}
// 调用 addWatermark 函数添加水印
addWatermark({
container: document.getElementById("content"),
width: "300px",
height: "200px",
textAlign: "center",
textBaseline: "middle",
font: "16px Microsoft Yahei",
fillStyle: "rgba(184, 184, 184, 0.3 )",
content: "水印 6512",
rotate: "30",
zIndex: 10000,
});
</script>
</body>
</html>
方法存在一个共同的问题,由于是前端生成 dom 元素覆盖到页面上的,对于有些前端知识的人来说,可以在开发者工具中找到水印所在的元素,将元素整个删掉,以达到删除页面上的水印的目的,针对这个问题,我想到了一个很笨的办法:设置定时器,每隔几秒检验一次我们的水印元素还在不在,有没有被修改,如果发生了变化则再执行一次覆盖水印的方法。网上看到了另一种解决方法:使用 MutationObserver
MutationObserver 是变动观察器,字面上就可以理解这是用来观察节点变化的。Mutation Observer API 用来监视 DOM 变动,DOM 的任何变动,比如子节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。
但是 MutationObserver 只能监测到诸如属性改变、子结点变化等,对于自己本身被删除,是没有办法监听的,这里可以通过监测父结点来达到要求。监测代码的实现:
// 监听容器变化,当容器发生变化时重新调用 addWatermark 函数
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
if (MutationObserver) {
let mo = new MutationObserver(function () {
const **wm = document.querySelector('.**wm');
// 只在**wm 元素变动才重新调用**canvasWM
if ((**wm && **wm.getAttribute('style') !== styleStr) || !__wm) {
// 避免一直触发
mo.disconnect();
mo = null;
console.log('1');
addWatermark({
container: document.getElementById("content"),
width: "300px",
height: "200px",
textAlign: "center",
textBaseline: "middle",
font: "16px Microsoft Yahei",
fillStyle: "rgba(184, 184, 184, 0.3 )",
content: "孙淼 6512",
rotate: "30",
zIndex: 10000,
});
}
});
mo.observe(container, {
attributes: true,
subtree: true,
childList: true
});
}
最终代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>水印</title>
</head>
<body>
<div id="content">
<p>这是一段内容</p>
</div>
<script>
// 定义水印函数
function addWatermark({
container = document.body, // 水印添加到的容器,默认为 body
width = "300px", // 水印 canvas 的宽度
height = "250px", // 水印 canvas 的高度
textAlign = "center", // 水印文字的对齐方式
textBaseline = "middle", // 水印文字的基线
font = "16px Microsoft Yahei", // 水印文字的字体
fillStyle = "rgba(184, 184, 184, 0.6)", // 水印文字的填充样式
content = "水印", // 水印文字的内容
rotate = "45", // 水印文字的旋转角度
zIndex = 10000, // 水印的 z-index 值
}) {
// 生成水印 canvas
const canvas = document.createElement("canvas");
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
const ctx = canvas.getContext("2d");
ctx.textAlign = textAlign;
ctx.textBaseline = textBaseline;
ctx.font = font;
ctx.fillStyle = fillStyle;
ctx.rotate((Math.PI / 180) * rotate);
ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
// 将 canvas 转换为 base64 URL
const base64Url = canvas.toDataURL();
const __wm = document.querySelector('.__wm');
const watermarkDiv = __wm || document.createElement("div");
const styleStr = `
position: fixed;
top: -70px;
left: 0;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
z-index: ${zIndex};
pointer-events: none;
background-repeat: repeat;
background-image: url('${base64Url}')
`;
watermarkDiv.setAttribute("style", styleStr);
watermarkDiv.classList.add("__wm");
//则创建一个 div 并设置样式和类名
if (!__wm) {
container.style.position = 'relative';
container.insertBefore(watermarkDiv, container.firstChild);
}
// 监听容器变化,当容器发生变化时重新调用 addWatermark 函数
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
if (MutationObserver) {
let mo = new MutationObserver(function () {
const __wm = document.querySelector('.__wm');
// 只在__wm元素变动才重新调用__canvasWM
if ((__wm && __wm.getAttribute('style') !== styleStr) || !__wm) {
// 避免一直触发
mo.disconnect();
mo = null;
console.log('1');
addWatermark({
container: document.getElementById("content"),
width: "300px",
height: "200px",
textAlign: "center",
textBaseline: "middle",
font: "16px Microsoft Yahei",
fillStyle: "rgba(184, 184, 184, 0.3 )",
content: "孙淼 6512",
rotate: "30",
zIndex: 10000,
});
}
});
mo.observe(container, {
attributes: true,
subtree: true,
childList: true
});
}
}
// 调用 addWatermark 函数添加水印
addWatermark({
container: document.getElementById("content"),
width: "300px",
height: "200px",
textAlign: "center",
textBaseline: "middle",
font: "16px Microsoft Yahei",
fillStyle: "rgba(184, 184, 184, 0.3 )",
content: "水印 6512",
rotate: "30",
zIndex: 10000,
});
</script>
</body>
</html>