背景
。。。
实现思路
用一个载体,承载着水印的内容,盖在整个页面上即可。这个载体可以是图片,可以是canvas也可以是一个div。将其CSS属性pointerEvents设置为none即可。
具体代码实现
水印的实现
const WATERMARK_DOM_ID = 'watermark-container';
const div = document.createElement('div');
div.id = WATERMARK_DOM_ID;
div.style.pointerEvents = 'none';
div.style.position = 'fixed';
div.style.top = '0';
div.style.left = '0';
div.style.zIndex = '2147483647';
div.style.opacity = '1';
div.style.width = '100%';
div.style.height = '100%';
div.style.backgroundImage = `url(${canvas.toDataURL('image/png')})repeat left top`;
document.body.appendChild(div);
在这里使用的是div,整个覆盖在整个页面的最上层,z-index值为int的最大值,然后设置一个背景图。所以我们接下来要完成的就是如何做这个背景图。
interface IWatermarkProps {
/** 水印内容 */
text: string;
/** 水印字体大小 */
fontSize?: number;
/** 水印字体颜色 */
color?: string;
/** 水印高度 */
watermarkHeight?: number;
/** 水印宽度 */
watermarkWidth?: number;
/** 水印旋转角度 */
angle?: number;
}
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (context) {
context.font = `${fontSize}px Arial`;
context.fillStyle = color;
const parentWidth = document.body.clientWidth;
const parentHeight = document.body.clientHeight;
canvas.width = parentWidth;
canvas.height = parentHeight;
const numClolumns = Math.ceil(parentWidth / watermarkWidth);
const numRows = Math.ceil(parentHeight / watermarkHeight);
context.clearRect(0, 0, parentWidth, parentHeight);
for (let row = 0; row < numClolumns; row++) {
for (let column = 0; column < numRows; column++) {
const x = column * watermarkWidth;
const y = row * watermarkHeight;
context.save();
context.translate(x, y + fontSize);
context.rotate(angle * Math.PI / 180);
context.fillText(text, x, y + fontSize);
context.restore();
}
}
我们创建了一个canvas图,设置其文字样式,然后计算其一共多少行多少列,用来计算每个水印文字的位置。设置其旋转角度,文案内容设置。
到这里基本上一个水印就完成了。但是别有用心之人可以直接通过F12控制台去删除我们创建的这个div,从而去掉页面的水印。所以我们需要做防删。
防删的实现
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation: any) => {
if (mutation.removedNodes.length > 0 && mutation.removedNodes[0]?.id === WATERMARK_DOM_ID) {
// 重新设置水印
setWatermark();
}
if ((mutation.addedNodes.length > 0 && mutation.addedNodes[0]?.id === WATERMARK_DOM_ID) || (mutation.type === 'attributes' && mutation.target.id === WATERMARK_DOM_ID)) {
setWatermark();
observer.disconnect();
observer.observe(document.body, {
attributes: true, // 观察属性变动
childList: true, // 观察目标子节点的变化
subtree: true // 观察后代节点
});
}
})
});
observer.observe(document.body, {
attributes: true, // 观察属性变动
childList: true, // 观察目标子节点的变化
subtree: true // 观察后代节点
})
原理也非常简单,就是监听body的子属性变化,在水印节点发生变化时,重新设置水印即可。
完整代码
hooks
import React, { useEffect, memo, useCallback } from 'react';
interface IWatermarkProps {
/** 水印内容 */
text: string;
/** 水印字体大小 */
fontSize?: number;
/** 水印字体颜色 */
color?: string;
/** 水印高度 */
watermarkHeight?: number;
/** 水印宽度 */
watermarkWidth?: number;
/** 水印旋转角度 */
angle?: number;
}
export const WATERMARK_DOM_ID = 'watermark-container';
const Watermark = ({
text,
fontSize = 20,
color = 'rgb(0, 0, 0, 0.08)',
watermarkHeight = 150,
watermarkWidth = 200,
angle = -20 }: IWatermarkProps) => {
const setWatermark = useCallback(() => {
const watersDoms = document.querySelectorAll(`#${WATERMARK_DOM_ID}`);
if (watersDoms.length) {
watersDoms.forEach((item) => {
document.body.removeChild(item);
});
}
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (context) {
const parentWidth = document.body.clientWidth;
const parentHeight = document.body.clientHeight;
canvas.width = parentWidth;
canvas.height = parentHeight;
const numClolumns = Math.ceil(parentWidth / watermarkWidth);
const numRows = Math.ceil(parentHeight / watermarkHeight);
context.font = `${fontSize}px Arial`;
context.fillStyle = color;
context.clearRect(0, 0, parentWidth, parentHeight);
for (let row = 0; row < numClolumns; row++) {
for (let column = 0; column < numRows; column++) {
const x = column * watermarkWidth;
const y = row * watermarkHeight;
context.save();
context.translate(x, y + fontSize);
context.rotate(angle * Math.PI / 180);
context.fillText(text, x, y + fontSize);
context.restore();
}
}
const div = document.createElement('div');
div.id = WATERMARK_DOM_ID;
div.style.pointerEvents = 'none';
div.style.position = 'fixed';
div.style.top = '0';
div.style.left = '0';
div.style.zIndex = '2147483647';
div.style.opacity = '1';
div.style.width = '100%';
div.style.height = '100%';
div.style.backgroundImage = `url(${canvas.toDataURL('image/png')})repeat left top`;
document.body.appendChild(div);
}
}, [text, fontSize, color, watermarkHeight, watermarkWidth, angle]);
useEffect(() => {
setWatermark();
}, [setWatermark]);
useEffect(() => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation: any) => {
if (mutation.removedNodes.length > 0 && mutation.removedNodes[0]?.id === WATERMARK_DOM_ID) {
setWatermark();
}
if ((mutation.addedNodes.length > 0 && mutation.addedNodes[0]?.id === WATERMARK_DOM_ID) || (mutation.type === 'attributes' && mutation.target.id === WATERMARK_DOM_ID)) {
setWatermark();
observer.disconnect();
observer.observe(document.body, {
attributes: true, // 观察属性变动
childList: true, // 观察目标子节点的变化
subtree: true // 观察后代节点
});
}
})
});
observer.observe(document.body, {
attributes: true, // 观察属性变动
childList: true, // 观察目标子节点的变化
subtree: true // 观察后代节点
})
return () => {
observer.disconnect();
}
}, []);
return <></>;
}
export default memo(Watermark);
class
import React, { Component } from 'react';
interface IWatermarkProps {
/** 水印内容 */
text: string;
/** 水印字体大小 */
fontSize?: number;
/** 水印字体颜色 */
color?: string;
/** 水印高度 */
watermarkHeight?: number;
/** 水印宽度 */
watermarkWidth?: number;
/** 水印旋转角度 */
angle?: number;
}
export const WATERMARK_DOM_ID = 'watermark-container';
class Watermark extends Component<IWatermarkProps> {
componentDidMount() {
this.setWatermark();
}
componentDidUpdate(prevProps: IWatermarkProps) {
if (
prevProps.text !== this.props.text ||
prevProps.fontSize !== this.props.fontSize ||
prevProps.color !== this.props.color ||
prevProps.watermarkHeight !== this.props.watermarkHeight ||
prevProps.watermarkWidth !== this.props.watermarkWidth ||
prevProps.angle !== this.props.angle
) {
this.setWatermark();
}
}
componentWillUnmount() {
const watersDoms = document.querySelectorAll(`#${WATERMARK_DOM_ID}`);
if (watersDoms.length) {
watersDoms.forEach((item) => {
document.body.removeChild(item);
});
}
}
setWatermark = () => {
const watersDoms = document.querySelectorAll(`#${WATERMARK_DOM_ID}`);
if (watersDoms.length) {
watersDoms.forEach((item) => {
document.body.removeChild(item);
});
}
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (context) {
const parentWidth = document.body.clientWidth;
const parentHeight = document.body.clientHeight;
canvas.width = parentWidth;
canvas.height = parentHeight;
const numClolumns = Math.ceil(parentWidth / this.props.watermarkWidth);
const numRows = Math.ceil(parentHeight / this.props.watermarkHeight);
context.font = `${this.props.fontSize}px Arial`;
context.fillStyle = this.props.color;
context.clearRect(0, 0, parentWidth, parentHeight);
for (let row = 0; row < numClolumns; row++) {
for (let column = 0; column < numRows; column++) {
const x = column * this.props.watermarkWidth;
const y = row * this.props.watermarkHeight;
context.save();
context.translate(x, y + this.props.fontSize);
context.rotate(this.props.angle * Math.PI / 180);
context.fillText(this.props.text, x, y + this.props.fontSize);
context.restore();
}
}
const div = document.createElement('div');
div.id = WATERMARK_DOM_ID;
div.style.pointerEvents = 'none';
div.style.position = 'fixed';
div.style.top = '0';
div.style.left = '0';
div.style.zIndex = '2147483647';
div.style.opacity = '1';
div.style.width = '100%';
div.style.height = '100%';
div.style.backgroundImage = `url(${canvas.toDataURL('image/png')})repeat left top`;
document.body.appendChild(div);
}
}
render() {
return <></>;
}
}
export default Watermark;
后话
可能细心的小伙伴会发现,代码中不仅导出了组件还导出了我们的水印div的id,这个原因也很简单,因为水印通常是在用户登录后才会有的,所以可能在用户未登录或者登出的情况下需要我们移除页面水印,但是这部分工作可能就要放在我们的前端页面鉴权的逻辑那边。例如:
import { WATERMARK_DOM_ID } from 'Watermark';
useEffect(() => {
if (!userId) {
const waterDoms = document.querySelectorAll(`#${WATERMARK_DOM_ID}`);
if (!!waterDoms.length) {
waterDoms.forEach((it) => {
document.body.removeChild(it);
});
}
}
}, [userId]);
意思就是这么个意思,具体细节不要较真。