vue3 组件篇 WaterMark
文章目录
- 组件介绍
- canvas实现文本转图案
- 插槽方式引入用户的内容
- WaterMark.vue
组件介绍
Watermark(水印) 是一种在页面上叠加半透明文字或图案的技术,常用于保护敏感信息(如文档、图片、网页)的版权或防止未经授权的传播。以下是其核心要点:
1. 信息标识:显示版权、用户信息或保密等级(如 “Confidential”)。
2. 防篡改:通过覆盖页面内容,增加数据泄露的难度。
3. 低干扰:半透明设计避免遮挡主体内容。
Canvas生成:用 Canvas 绘制文本/图案,转为 Base64 图片作为背景。
防删除:通过 MutationObserver 监听 DOM 改动,自动恢复被删除的水印。
canvas实现文本转图案
useWaterMarkBg.ts
import { computed } from 'vue'
interface WaterMarkProps {
text: string,
fontSize: number,
gap: number,
rotate?: number
color?: string,
}
export default function useWaterMarkBg(props: WaterMarkProps) {
return computed(() => {
// 创建一个canvas 通过canvas生成图片
const canvas = document.createElement('canvas')
const ctx: any = canvas.getContext('2d')
const devicePixelRatio = window.devicePixelRatio || 1
const fontSize = props.fontSize * devicePixelRatio
const font = fontSize + 'px serif'
// 获取文字宽度
ctx.font = font;
const { width } = ctx.measureText(props.text)
const canvasSize = Math.max(100, width) + props.gap * devicePixelRatio;
canvas.width = canvasSize
canvas.height = canvasSize
ctx.translate(canvasSize / 2, canvasSize / 2)
ctx.rotate(props.rotate || -Math.PI / 4)
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillStyle = props.color
ctx.font = font
ctx.fillStyle = props.color || '#ccc'
ctx.fillText(props.text, 0, 0)
return {
base64: canvas.toDataURL(),
size: canvasSize,
styleSize: canvasSize / devicePixelRatio
}
})
}
插槽方式引入用户的内容
<template>
<div ref="waterMarkContanier" style="position: relative; z-index: 1" :key="reRender">
<!-- 内容容器的zindex被限制,始终低于水印的zindex,这样就无法通过更改插槽内容的样式,来覆盖水印 -->
<div ref="contentContainer" style="position: relative; z-index: 1">
<slot></slot>
</div>
</div>
</template>
WaterMark.vue
<template>
<div ref="waterMarkContanier" style="position: relative; z-index: 1" :key="reRender">
<!-- 内容容器的zindex被限制,始终低于水印的zindex,这样就无法通过更改插槽内容的样式,来覆盖水印 -->
<div ref="contentContainer" style="position: relative; z-index: 1">
<slot></slot>
</div>
</div>
</template>
<script lang="ts" name="waterMark" setup>
import useWaterMarkBg from '@/utrils/useWaterMarkBg'
import { watch, ref, onMounted, onUnmounted } from 'vue'
const props = defineProps({
text: {
type: String,
default: 'DX UI',
required: true,
},
// 水印的字体大小
fontSize: {
type: Number,
default: 20,
},
// 生成水印的间隙
gap: {
type: Number,
default: 10,
},
// 生成水印的颜色
color: {
type: String,
default: '#ccc',
},
// 水印的层级 默认是0 即水印在内容下方
// 如果水印在内容下方,水印仍可能被有心者遮盖,
// 想要更安全可将水印的层级调高zIndex设置为2以上,
// 但这样会让水印在内容的上方
zIndex: {
type: Number,
default: 0,
},
})
// 通过waterMarkContanier监听dom变化,防止篡改
const waterMarkContanier = ref<HTMLDivElement | null>(null)
// 监听篡改的元素是否是内容元素,如果是就重新渲染组件
const contentContainer = ref<HTMLDivElement | null>(null)
// 水印背景数据
const waterMarkBg = useWaterMarkBg(props)
// 控制水印重新渲染 防止水印被篡改
const domChange = ref(0)
// 控制dom的key重新渲染,防止dom被篡改
const reRender = ref(0)
let div: HTMLDivElement | null = null
watch(
() => [waterMarkContanier.value, domChange.value],
() => {
if (!waterMarkContanier.value) return
// 防水印被篡改
// 如果div存在,就移除
if (div) {
div.remove()
}
// 重新生成水印
div = document.createElement('div')
div.style.backgroundImage = `url(${waterMarkBg.value.base64})`
div.style.position = 'absolute'
div.style.inset = '0'
div.style.zIndex = props.zIndex.toString()
waterMarkContanier.value.appendChild(div)
},
{
immediate: true,
},
)
let observer: MutationObserver | null = null
onMounted(() => {
if (!waterMarkContanier.value) return
observer = new MutationObserver((record) => {
for (let nodeList of record) {
// 如果水印被修改了
if (nodeList.target === div) {
domChange.value++
return
}
// 如果插槽内容元素被修改了,就通过修改key值,强制更新组件
if (nodeList.target === contentContainer.value) {
reRender.value++
return
}
for (let node of nodeList.removedNodes) {
// 如果水印被删除了
if (node === div) {
// 通过控制它触发watch 重新添加水印
domChange.value++
return
}
}
}
})
observer.observe(waterMarkContanier.value, {
attributes: true,
childList: true,
subtree: true,
characterData: true,
})
})
onUnmounted(() => {
observer && observer?.disconnect()
div = null
})
</script>
其中zIndex是关键,很多水印组件其实都没办法绝对控制水印始终正常显示,包括antd的水印组件也是一样。
如果传入的插槽内容有class,只需要给这个class加样式,加背景,加position,加zIndex 就能把水印给覆盖掉。
因为这些组件默认水印都在内容的下方,如果水印始终在内容的上方,通过给waterMark组件传入更高的zIndex改变层级,就能避免这个问题。