最近要实现一个页面内的水印效果,想了一下主要问题在以下几个方面:
- 如何实现文字水印效果?
- 如何将文字水印图片设置为背景,且不影响界面正常交互?
- 怎么尽可能的防止去除水印。
如何实现文字水印效果?
1. 通过坐标计算 遍历生成文字水印元素
// 百度 文心一言页面水印生成部分代码
function a(e, t) {
var n, a, i;
function o(e) {
return o = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(e) {
return typeof e
}
: function(e) {
return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e
}
,
o(e)
}
a = [],
n = function() {
var e, t = {}, n = !1, a = {
watermark_id: "wm_div_id",
watermark_prefix: "mask_div_id",
watermark_txt: "测试水印",
watermark_x: 20,
watermark_y: 20,
watermark_rows: 0,
watermark_cols: 0,
watermark_x_space: 50,
watermark_y_space: 50,
watermark_font: "微软雅黑",
watermark_color: "black",
watermark_fontsize: "18px",
watermark_alpha: .15,
watermark_width: 100,
watermark_height: 100,
watermark_angle: 15,
watermark_parent_width: 0,
watermark_parent_height: 0,
watermark_parent_node: null,
watermark_nowrap: !1,
monitor: !0
}, i = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver, r = function(t) {
n ? n = !1 : (e && 1 === t.length || 1 === t.length && t[0].removedNodes.length >= 1) && u(e)
}, l = void 0 !== i, s = l ? new i(r) : null, c = {
childList: !0,
attributes: !0,
subtree: !0
}, u = function() {
for (var e = arguments.length, t = new Array(e), n = 0; n < e; n++)
t[n] = arguments[n];
if (1 === t.length && "object" === o(t[0])) {
var i = t[0] || {};
for (key in i)
i[key] && a[key] && i[key] === a[key] || (i[key] || 0 === i[key]) && (a[key] = i[key])
}
var r = document.getElementById(a.watermark_id);
r && r.parentNode && r.parentNode.removeChild(r);
var u = document.getElementById(a.watermark_parent_node)
, d = u || document.body
, m = Math.max(d.scrollWidth, d.clientWidth)
, p = Math.max(d.scrollHeight, d.clientHeight)
, v = t[0] || {}
, h = d
, f = 0
, y = 0;
v.watermark_parent_width || v.watermark_parent_height ? h && (f = h.offsetTop || 0,
y = h.offsetLeft || 0,
a.watermark_x = a.watermark_x + y,
a.watermark_y = a.watermark_y + f) : h && (f = h.offsetTop || 0,
y = h.offsetLeft || 0);
var g = document.getElementById(a.watermark_id)
, E = null;
if (g)
g.shadowRoot && (E = g.shadowRoot);
else {
(g = document.createElement("div")).id = a.watermark_id,
g.setAttribute("style", "pointer-events: none !important; display: block !important; visibility: visible !important; opacity: 1 !important; position: static !important"),
E = "function" == typeof g.attachShadow ? g.attachShadow({
mode: "closed"
}) : g;
var x = d.children
, w = Math.floor(Math.random() * (x.length - 1));
x[w] ? d.insertBefore(g, x[w]) : d.appendChild(g)
}
a.watermark_cols = parseInt((m - a.watermark_x) / (a.watermark_width + a.watermark_x_space), 10);
var k, b = parseInt((m - a.watermark_x - a.watermark_width * a.watermark_cols) / a.watermark_cols, 10);
a.watermark_x_space = b ? a.watermark_x_space : b,
a.watermark_rows = parseInt((p - a.watermark_y) / (a.watermark_height + a.watermark_y_space), 10);
var S, I, C, N = parseInt((p - a.watermark_y - a.watermark_height * a.watermark_rows) / a.watermark_rows, 10);
a.watermark_y_space = N ? a.watermark_y_space : N,
u ? (k = a.watermark_x + a.watermark_width * a.watermark_cols + a.watermark_x_space * (a.watermark_cols - 1),
S = a.watermark_y + a.watermark_height * a.watermark_rows + a.watermark_y_space * (a.watermark_rows - 1)) : (k = y + a.watermark_x + a.watermark_width * a.watermark_cols + a.watermark_x_space * (a.watermark_cols - 1),
S = f + a.watermark_y + a.watermark_height * a.watermark_rows + a.watermark_y_space * (a.watermark_rows - 1));
for (var T = 0; T < a.watermark_rows; T++) {
C = u ? f + a.watermark_y + (p - S) / 2 + (a.watermark_y_space + a.watermark_height) * T : a.watermark_y + (p - S) / 2 + (a.watermark_y_space + a.watermark_height) * T;
for (var _ = 0; _ < a.watermark_cols; _++) {
I = u ? y + a.watermark_x + (m - k) / 2 + (a.watermark_width + a.watermark_x_space) * _ : a.watermark_x + (m - k) / 2 + (a.watermark_width + a.watermark_x_space) * _;
var B = document.createElement("div")
, D = document.createTextNode(a.watermark_txt);
B.appendChild(D),
B.id = a.watermark_prefix + T + _,
B.style.webkitTransform = "rotate(-" + a.watermark_angle + "deg)",
B.style.MozTransform = "rotate(-" + a.watermark_angle + "deg)",
B.style.msTransform = "rotate(-" + a.watermark_angle + "deg)",
B.style.OTransform = "rotate(-" + a.watermark_angle + "deg)",
B.style.transform = "rotate(-" + a.watermark_angle + "deg)",
B.style.visibility = "",
B.style.position = "absolute",
B.style.left = I + "px",
B.style.top = C + "px",
B.style.overflow = "hidden",
B.style.zIndex = "9999999",
B.style.opacity = a.watermark_alpha,
B.style.fontSize = a.watermark_fontsize,
B.style.fontFamily = a.watermark_font,
B.style.color = a.watermark_color,
B.style.textAlign = "center",
B.style.width = a.watermark_width + "px",
B.style.height = a.watermark_height + "px",
B.style.display = "block",
B.style["-ms-user-select"] = "none",
a.watermark_nowrap && (B.style.overflow = "initial",
B.style["white-space"] = "nowrap"),
E.appendChild(B)
}
}
(void 0 === t[0].monitor ? a.monitor : t[0].monitor) && l && s.observe(d, c)
}, d = function() {
if (1 === arguments.length && "object" === o(arguments.length <= 0 ? void 0 : arguments[0])) {
var e = (arguments.length <= 0 ? void 0 : arguments[0]) || {};
for (key in e)
e[key] && a[key] && e[key] === a[key] || (e[key] || 0 === e[key]) && (a[key] = e[key])
}
var t = document.getElementById(a.watermark_id);
t.parentNode.removeChild(t),
s.disconnect()
};
t.init = function(t) {
e = t,
u(t),
window.addEventListener("onload", (function() {
u(t)
}
)),
window.addEventListener("resize", (function() {
u(t)
}
))
}
,
t.load = function(t) {
e = t,
u(t)
}
,
t.remove = function() {
n = !0,
d()
}
,
s = new i((function(t) {
if (e && 1 === t.length || 1 === t.length && t[0].removedNodes.length >= 1)
u(e);
else {
var n = document.getElementById(a.watermark_parent_node);
if (n) {
var i = getComputedStyle(n).getPropertyValue("width")
, o = getComputedStyle(n).getPropertyValue("height");
i === m.width && o === m.height || (m.width = i,
m.height = o,
u(e))
}
}
}
)),
c = {
childList: !0,
attributes: !0,
subtree: !0,
attributeFilter: ["style", "class"],
attributeOldValue: !0
};
var m = {
width: 0,
height: 0
};
return t
}(),
void 0 === (i = "function" == typeof n ? n.apply(t, a) : n) || (e.exports = i)
}
2. 使用canvas画出水印图片
-
创建一个 canvas 元素,并获取其 2D 上下文。
-
根据传入的选项设置 canvas 元素的宽度和高度。
-
设置 ctx 上下文的字体、文本颜色、文本对齐方式和文本基线。
-
计算文本宽度和高度,以及对角线长度。
-
将 ctx 上下文移动到 canvas 中心,并旋转一定角度,以便在整个画布上分布水印。
-
使用循环在 canvas 中绘制文本,直到覆盖整个画布。
-
将 canvas 转换为 data URL,并将其设置为容器元素的背景。
interface WatermarkOptions {
text: string;
fontSize?: string;
color?: string;
angle?: number;
spacing?: number;
}
class Watermark {
private readonly options: WatermarkOptions;
private readonly container: HTMLElement;
constructor(container: HTMLElement, options: WatermarkOptions) {
this.container = container;
this.options = {
fontSize: '24px',
color: 'rgba(0, 0, 0, 0.1)',
angle: -30,
spacing: 200,
...options,
};
this.init();
}
private init() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx) {
const { text, fontSize, color, angle, spacing } = this.options;
const canvasWidth = this.container.offsetWidth + spacing;
const canvasHeight = this.container.offsetHeight + spacing;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
ctx.font = fontSize + ' Arial';
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const textWidth = ctx.measureText(text).width;
const textHeight = parseInt(fontSize) + 10;
const diagonalLength = Math.sqrt(canvasWidth * canvasWidth + canvasHeight * canvasHeight);
ctx.translate(canvasWidth / 2, canvasHeight / 2);
ctx.rotate((angle * Math.PI) / 180);
for (let x = -diagonalLength; x < diagonalLength; x += textWidth + spacing) {
for (let y = -diagonalLength; y < diagonalLength; y += textHeight + spacing) {
ctx.fillText(text, x, y);
}
}
const dataUrl = canvas.toDataURL();
this.container.setAttribute("style", "pointer-events: none !important; display: block !important; visibility: visible !important; opacity: 1 !important; position: fixed !important")
this.container.style.backgroundImage = `url(${dataUrl})`;
}
}
}
export default Watermark;
如何将文字水印图片设置为背景,且不影响界面正常交互?
css 属性 pointer-events: none
pointer-events 属性用于设置元素是否对鼠标事件做出反应。
默认值: none
默认值: auto
继承: 无
动画: no。
版本: CSS3
JavaScript 语法: object.style.all="initial"
CSS 语法
pointer-events: auto|none;
auto 默认值,设置该属性链接可以正常点击访问。
none 元素不能对鼠标事件做出反应
initial 设置该属性为它的默认值,查看更多initial
inherit 从父元素继承该属性, 查看更多 inherit
怎么尽可能的防止去除水印。
这个问题之前一直思考过,难点就是如何监听开发者工具是否被打开? 网上搜了发现相关文章比较少,但还是找到一篇比较靠谱的方案:
可以查阅 前端 Chrome 反调试方法 最终实现代码:
const consoleOpenCallback = () => {
window.location = 'about:blakn';
}
setInterval(() => {
const before = new Date();
(function(){}).constructor('debugger')();
// debugger;
const after = new Date();
const cost = after.getTime() - before.getTime();
if (cost > 100) {
consoleOpenCallback();
}
}, 1000)
如何防止别人反反调试 绕过上面的检测去删除水印元素?
通过 MutationObserver API 来监听 DOM 节点的删除。MutationObserver API 提供了一种异步机制,可以观察 DOM 树的变化,并在节点被删除时触发回调函数。
以下是一个示例代码,演示如何使用 MutationObserver 监听 DOM 节点的删除:
// 选择要观察变化的目标节点
const targetNode = document.getElementById('target');
// 创建一个 MutationObserver 实例
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList' && mutation.removedNodes.length > 0) {
console.log('节点被删除了:', mutation.removedNodes[0]);
// 在这里将删除的元素再次append到目标节点父元素即可
}
});
});
// 配置观察选项
const config = { childList: true };
// 开始观察目标节点
observer.observe(targetNode, config);