目录
第一章 前言
第二章 组件核心技术栈拆解
2.1 基本配置
2.1.1 props
2.1.2 生命周期管理与方法暴露
2.2 Canvas 绘图技术(生成水印底图)
2.2.1 文字拆分(解决过长文字换行问题)
2.2.2 Canvas 绘制多单元水印并转换为 base64
2.3 MutationObserver DOM 篡改监测(防篡改核心)
2.3.1 MutationObserver API 原理
2.3.2 DOM 篡改监测与自动恢复
2.4 防抖(Debounce)性能优化
2.4.1 防抖函数实现
2.4.2 防抖函数的使用
2.5 原生 DOM 操作与样式适配
2.5.1 获取目标容器(水印绑定可动态配置)
2.5.2 容器样式初始化与还原
第三章 组件实现难点与解决方案
3.1 水印 DOM 样式适配
3.2 多行文字水印的居中排版与旋转适配
3.3 防篡改的全面性与安全性保障
3.4 性能与资源管理的平衡
第四章 组件使用示例
第五章 总结与拓展方向
第六章 源码(开箱即用)
第一章 前言
在企业级应用中,水印是保护敏感数据、防止文档外泄的重要手段。普通水印往往存在易被篡改、排版错位、性能不佳等问题,经过小编实践,本文就是要实现这么一个水印组件,不仅实现了相对安全性防篡改,还具备良好的可配置性和性能表现。接下来,小编将从核心技术、实现难点、设计亮点三个维度,结合具体代码片段拆解,让每一个技术点都清晰可落地。
第二章 组件核心技术栈拆解
2.1 基本配置
2.1.1 props
const props = defineProps({
text: { type: String, default: '内部文档 · 禁止外传' },
fontSize: { type: [Number, String], default: 14 },
fontFamily: { type: String, default: 'Microsoft Yahei, sans-serif' },
color: { type: String, default: 'rgba(180, 180, 180, 0.3)' },
rotate: { type: [Number, String], default: -15 },
spacing: { type: [Number, String], default: 50 },
zIndex: { type: [Number, String], default: 999999 },
debounceTime: { type: [Number, String], default: 300 },
container: { type: [String, Object], default: 'body' },
lineHeight: { type: [Number, String], default: 1.5 },
// 新增:水印单元重复数量(控制背景图包含的水印单元数)
repeatCount: { type: [Number, String], default: 2 }
})
2.1.2 生命周期管理与方法暴露
// 暴露方法给父组件调用
defineExpose({ renderWatermark, resetWatermark, destroyWatermark })
// 组件挂载后渲染水印
onMounted(() => renderWatermark())
// 组件卸载后销毁水印,释放资源
onUnmounted(() => destroyWatermark())
- defineProps定义了丰富的配置项,支持Number/String多类型传入,且都设置了合理默认值,降低使用门槛;
- onMounted/onUnmounted对应 Vue3 生命周期,确保水印在组件挂载时创建、卸载时销毁,避免内存泄漏;
- defineExpose暴露核心方法,支持大部分后台业务侧边手动控制水印生命周期,提升组件灵活性。
2.2 Canvas 绘图技术(生成水印底图)
- 组件的水印可视化核心依赖 Canvas API,将文字水印转换为 base64 格式图片,既提升渲染性能,又增加篡改难度。
HTML5 <canvas> 参考手册
HTML5 Canvas
2.2.1 文字拆分(解决过长文字换行问题)
/**
* 拆分过长文字为多行
*/
const _splitTextToLines = (ctx, text, maxWidth) => {
if (!text) return []
const lines = []
let currentLine = ''
const chars = text.split('')
for (const char of chars) {
const testLine = currentLine + char
const testWidth = ctx.measureText(testLine).width // 测量文字宽度
if (testWidth > maxWidth && currentLine) {
lines.push(currentLine)
currentLine = char
} else {
currentLine = testLine
}
}
if (currentLine) lines.push(currentLine)
return lines
}
2.2.2 Canvas 绘制多单元水印并转换为 base64
/**
* 生成多单元水印背景图(核心优化)
*/
const _createCanvasBase64 = () => {
const { text, fontSize, fontFamily, color, rotate, spacing, lineHeight, repeatCount } = props
const fontSizeNum = Number(fontSize)
const lineHeightNum = Number(lineHeight) || 1.5
const spacingNum = Number(spacing)
const repeatCountNum = Number(repeatCount) || 2
// 单个水印单元的尺寸
const singleWidth = spacingNum * 2
const singleMaxTextWidth = singleWidth * 0.8
const canvas = document.createElement('canvas') // 创建离线Canvas
const ctx = canvas.getContext('2d') // 获取2D绘图上下文
// 初始化画布上下文配置
ctx.fillStyle = color
ctx.font = `${fontSizeNum}px ${fontFamily}`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
// 拆分文字并计算单个单元高度
const textLines = _splitTextToLines(ctx, text, singleMaxTextWidth)
const singleHeight = Math.max(spacingNum * 2, textLines.length * fontSizeNum * lineHeightNum)
// 生成包含多个单元的背景图(如2×2)
const canvasWidth = singleWidth * repeatCountNum
const canvasHeight = singleHeight * repeatCountNum
canvas.width = canvasWidth
canvas.height = canvasHeight
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
// 绘制多单元水印(双重循环实现网格布局)
for (let x = 0; x < repeatCountNum; x++) {
for (let y = 0; y < repeatCountNum; y++) {
const unitX = singleWidth * x + singleWidth / 2
const unitY = singleHeight * y + singleHeight / 2
// 保存/恢复上下文状态,避免单元间绘制相互干扰
ctx.save()
ctx.translate(unitX, unitY) // 平移到当前单元中心
ctx.rotate((Number(rotate) * Math.PI) / 180) // 角度转弧度实现旋转
// 绘制多行文字,实现垂直居中
const totalTextHeight = (textLines.length – 1) * fontSizeNum * lineHeightNum
textLines.forEach((line, index) => {
const yOffset = (index – (textLines.length – 1) / 2) * fontSizeNum * lineHeightNum
ctx.fillText(line, 0, yOffset)
})
ctx.restore()
}
}
return canvas.toDataURL('image/png') // 转换为base64格式图片
}
- document.createElement('canvas')创建离线 Canvas,不直接插入 DOM,仅用于生成图片;
- ctx.measureText()测量文字宽度,是实现文字自动换行的核心;
- ctx.rotate()接收弧度值,因此需要将角度转换为(角度 * Math.PI) / 180;
- ctx.save()/ctx.restore()保证每个水印单元的绘制状态独立,避免旋转、平移操作相互干扰;
- canvas.toDataURL('image/png')将 Canvas 内容转换为 base64 图片,用于后续水印 DOM 的背景图。
2.3 MutationObserver DOM 篡改监测(防篡改核心)
- 组件的高安全性核心依赖MutationObserver API,能够精准监听 DOM 树变化,捕获恶意篡改行为。
2.3.1 MutationObserver API 原理
MutationObserver – Web API | MDN
const observerOptions = {
childList: true, // 监听子节点的增、删操作
attributes: true, // 监听元素属性的修改(如style、class、dataset)
subtree: true, // 监听目标节点的所有子树(后代节点)的变化
attributeFilter: ['style', 'class', 'dataset'] // 仅监听指定属性的修改,进一步缩小范围
}
- MutationObserver 是浏览器原生提供的异步 DOM 变化监测 API,用于监听 DOM 树的结构或内容发生的任何改变,并在变化发生后触发指定回调函数。它是替代已废弃的 DOMSubtreeModified 等同步 DOM 事件的高性能方案,也是实现水印防篡改、动态 DOM 监控等功能的核心技术。
- 核心特点:
- 监听机制:MutationObserver 最关键的原理是异步非阻塞监听,它与 onclick、onchange 等同步 DOM 事件有本质区别:
- 使用:
// 实例化:回调函数接收两个参数——变化记录数组、监听器实例
const observer = new MutationObserver((mutations, observerInstance) => {
// mutations:批量收集的 MutationRecord 变化记录数组
// observerInstance:当前 MutationObserver 实例(可用于断开监听等操作)
mutations.forEach(mutation => {
// 处理单个 DOM 变化记录
})
})
// 目标节点(水印组件中的目标容器)
const targetContainer = document.querySelector('#container')
// 启动监听
observer.observe(targetContainer, observerOptions)
2.3.2 DOM 篡改监测与自动恢复
/**
* 初始化DOM篡改监测
*/
const _initMutationObserver = () => {
if (observer.value) observer.value.disconnect() // 断开已有监听,避免重复监听
const observerOptions = {
childList: true, // 监听子节点的增删操作
attributes: true, // 监听元素属性的修改
subtree: true, // 监听子树(后代节点)的变化
attributeFilter: ['style', 'class', 'dataset'] // 只监听指定属性,提升性能
}
// 实例化MutationObserver,传入篡改回调函数
observer.value = new MutationObserver((mutations) => {
let needReRender = false
mutations.forEach((mutation) => {
// 场景1:水印DOM被删除
if (mutation.type === 'childList' && mutation.removedNodes.length) {
const isWatermarkRemoved = Array.from(mutation.removedNodes).some(
(node) => node.dataset?.watermarkId === watermarkId.value
)
if (isWatermarkRemoved) needReRender = true
}
// 场景2:水印DOM的样式/类名/属性被修改
if (
mutation.type === 'attributes' &&
mutation.target.dataset?.watermarkId === watermarkId.value
) {
needReRender = true
}
})
// 一旦检测到篡改,自动重置水印
if (needReRender) resetWatermark()
})
// 开始监听目标容器的DOM变化
observer.value.observe(targetContainer.value, observerOptions)
}
- observerOptions配置监听规则,精准筛选需要监听的 DOM 变化类型,避免无意义的回调触发;
- new MutationObserver((mutations) => {})实例化监听器,mutations参数包含所有触发的 DOM 变化;
- 分别判断childList(节点删除)和attributes(属性修改)两种篡改场景,确保全覆盖;
- 组件销毁前需调用observer.disconnect()断开监听,释放浏览器资源,也就是销毁钩子里执行。
2.4 防抖(Debounce)性能优化
2.4.1 防抖函数实现
/**
* 防抖函数:指定时间内多次触发,仅执行最后一次
*/
const _debounce = (fn, delay) => {
let timer = null // 定时器缓存
return function () {
clearTimeout(timer) // 清除上一次未执行的定时器
timer = setTimeout(() => fn.apply(this, arguments), delay) // 重新设置定时器,延迟执行
}
}
很经典的了,不清楚可以看小编下面的文章:
前端常用算法(一):防抖+节流
2.4.2 防抖函数的使用
/**
* 监听用户操作
*/
const _initUserOperationListener = () => {
const { debounceTime } = props
// 给水印重置方法添加防抖包装
operationHandler.value = _debounce(() => resetWatermark(), Number(debounceTime))
const userEvents = ['click', 'input', 'scroll', 'dragend', 'keydown', 'mouseup', 'touchend']
// 绑定多个用户事件,防抖触发水印重置
userEvents.forEach((event) => {
targetContainer.value.addEventListener(event, operationHandler.value, { passive: true })
})
}
- 防抖的核心是setTimeout和clearTimeout,通过缓存定时器实现 “取消上一次,执行下一次”;
- fn.apply(this, arguments)保证被包装函数的this指向和参数传递不丢失;
- 给用户高频操作(scroll/click等)绑定防抖后的水印重置方法,避免频繁执行 DOM 操作和 Canvas 绘图导致页面卡顿。
2.5 原生 DOM 操作与样式适配
2.5.1 获取目标容器(水印绑定可动态配置)
/**
* 获取目标容器实例
*/
const _getTargetContainer = () => {
let container = null
if (typeof props.container === 'string') {
// 字符串选择器:通过querySelector获取DOM
container = document.querySelector(props.container)
} else if (props.container instanceof HTMLElement) {
// 直接传入HTMLElement实例,直接使用
container = props.container
}
// 容错处理:容器不存在时,默认使用body
if (!container || !(container instanceof HTMLElement)) {
console.warn('水印目标容器不存在,默认使用body')
container = document.body
}
return container
}
2.5.2 容器样式初始化与还原
/**
* 初始化容器样式(添加溢出隐藏和定位)
*/
const _initContainerStyle = () => {
const container = targetContainer.value
if (container !== document.body) {
const computedStyle = window.getComputedStyle(container)
// 保存容器原有样式,用于后续还原
originContainerStyle.value = {
position: computedStyle.position,
overflow: computedStyle.overflow
}
// 确保容器有定位,让水印的absolute定位生效
if (
!['relative', 'absolute', 'fixed', 'sticky'].includes(originContainerStyle.value.position)
) {
container.style.position = 'relative !important'
}
// 确保容器溢出隐藏,避免水印超出容器范围
container.style.overflow = 'hidden !important'
}
}
// 销毁水印时还原容器样式(对应destroyWatermark方法片段)
if (targetContainer.value && targetContainer.value !== document.body) {
targetContainer.value.style.position = originContainerStyle.value.position
targetContainer.value.style.overflow = originContainerStyle.value.overflow
originContainerStyle.value = { position: '', overflow: '' }
}
- 支持两种容器传入方式(字符串选择器 / HTMLElement 实例),提升组件灵活性;
- 保存容器原有样式,挂载水印时临时修改,销毁时还原,避免影响页面原有布局;
- 给非body容器添加relative定位,确保水印的absolute定位相对于容器生效,而不是整个文档。
第三章 组件实现难点与解决方案
3.1 水印 DOM 样式适配
- 需求:水印完整平铺无错位,适配不同容器;不同容器(body/ 普通定宽高元素)、不同屏幕尺寸下,如何保证水印平铺完整、无边缘错位、不影响原有页面布局?
/**
* 创建水印DOM元素(适配完整平铺)
*/
const _createWatermarkDom = () => {
const { zIndex, spacing, repeatCount } = props
const base64Url = _createCanvasBase64()
const container = targetContainer.value
const spacingNum = Number(spacing)
const repeatCountNum = Number(repeatCount) || 2
const singleWidth = spacingNum * 2
const singleHeight = spacingNum * 2
// 区分容器类型,配置不同的宽高和定位方式
const containerWidth = container === document.body ? '100vw' : `${container.offsetWidth}px`
const containerHeight = container === document.body ? '100vh' : `${container.offsetHeight}px`
const positionType = container === document.body ? 'fixed' : 'absolute'
// 背景图尺寸(多单元整体尺寸),确保平铺无错位
const bgSize = `${singleWidth * repeatCountNum}px ${singleHeight * repeatCountNum}px`
const bgPosition = `0 0`
const watermark = document.createElement('div')
// 用!important锁定核心样式,防止被页面其他样式覆盖
watermark.style.cssText = `
position: ${positionType} !important;
top: 0 !important;
left: 0 !important;
width: ${containerWidth} !important;
height: ${containerHeight} !important;
z-index: ${zIndex} !important;
pointer-events: none !important; /* 穿透水印,不影响下方元素交互 */
background-image: url(${base64Url}) !important;
background-repeat: repeat !important; /* 背景图平铺,实现全屏/全容器覆盖 */
background-position: ${bgPosition} !important;
background-size: ${bgSize} !important;
user-select: none !important; /* 禁止选中水印,提升防篡改能力 */
`
watermarkId.value = 'unique-watermark-' + Date.now() // 生成唯一水印ID
watermark.dataset.watermarkId = watermarkId.value // 绑定到dataset,用于篡改识别
return watermark
}
- 解决方案
3.2 多行文字水印的居中排版与旋转适配
- 需求:当水印文字过长时,直接渲染会超出容器范围,旋转后更难保证居中对齐,如何实现优雅的多行排版与垂直居中?
// 1. 拆分文字(已在核心技术部分展示,此处聚焦绘制逻辑)
const textLines = _splitTextToLines(ctx, text, singleMaxTextWidth)
// 2. 计算多行文字总高度与单个水印单元高度
const totalTextHeight = (textLines.length – 1) * fontSizeNum * lineHeightNum
const singleHeight = Math.max(spacingNum * 2, textLines.length * fontSizeNum * lineHeightNum)
// 3. 平移到单元中心,准备旋转
ctx.translate(unitX, unitY)
ctx.rotate((Number(rotate) * Math.PI) / 180)
// 4. 绘制多行文字,实现垂直居中
textLines.forEach((line, index) => {
// 计算每行文字的垂直偏移量,确保整体居中
const yOffset = (index – (textLines.length – 1) / 2) * fontSizeNum * lineHeightNum
ctx.fillText(line, 0, yOffset)
})
- 解决方案
3.3 防篡改的全面性与安全性保障
- 问题描述:恶意用户可能通过多种方式篡改水印(删除 DOM、修改样式、修改属性),如何实现全方位的防篡改覆盖,让篡改操作无效?
// 机制1:高优先级样式锁,防止普通样式覆盖
watermark.style.cssText = `
position: ${positionType} !important;
background-image: url(${base64Url}) !important;
z-index: ${zIndex} !important;
`
// 机制2:MutationObserver精准监测篡改行为(已在核心技术部分展示)
const isWatermarkRemoved = Array.from(mutation.removedNodes).some(
(node) => node.dataset?.watermarkId === watermarkId.value
)
// 机制3:篡改后自动重置水印,让篡改无效
if (needReRender) resetWatermark()
// 机制4:用户操作兜底,防抖监听高频操作,异常时重置水印
operationHandler.value = _debounce(() => resetWatermark(), Number(debounceTime))
- 解决方案
3.4 性能与资源管理的平衡
- 问题描述:水印的监测、渲染、重置可能产生性能开销,如何避免页面卡顿和内存泄漏?
/**
* 销毁水印(还原容器样式,释放所有资源)
*/
const destroyWatermark = (isClearOperationListener = true) => {
// 1. 移除水印DOM
if (watermarkDom.value && watermarkDom.value.parentNode) {
watermarkDom.value.parentNode.removeChild(watermarkDom.value)
watermarkDom.value = null
}
// 2. 断开MutationObserver监听,释放浏览器资源
if (observer.value) {
observer.value.disconnect()
observer.value = null
}
// 3. 移除用户事件监听,避免内存泄漏
if (isClearOperationListener && operationHandler.value) {
const userEvents = ['click', 'input', 'scroll', 'dragend', 'keydown', 'mouseup', 'touchend']
userEvents.forEach((event) => {
targetContainer.value.removeEventListener(event, operationHandler.value)
})
operationHandler.value = null
}
// 4. 还原容器原有样式,不影响页面布局
if (targetContainer.value && targetContainer.value !== document.body) {
targetContainer.value.style.position = originContainerStyle.value.position
targetContainer.value.style.overflow = originContainerStyle.value.overflow
originContainerStyle.value = { position: '', overflow: '' }
}
}
- 解决方案
第四章 组件使用示例
<GeneralWatermark
ref="watermarkRef"
:text="'内部文档 · 禁止外传'"
color="rgba(90, 153, 212, 0.2)"
rotate="-15"
spacing="120"
repeatCount="2"
/>
第五章 总结与拓展方向
这份 Vue3 防篡改水印组件,是前端技术在实际业务场景中的优秀落地实践,每个技术点都有清晰的代码支撑,解决了水印平铺、多行排版、防篡改、性能优化等核心问题,具备高安全性、高可配置性和良好的用户体验。
— 扩展空间:
- 支持动态更新:添加watch监听 props 变化,实现水印配置动态更新,无需手动调用resetWatermark/刷新页面:
watch(props, () => {
resetWatermark()
}, { deep: true })
- 支持图片水印:增加image配置项,支持上传图片作为水印底图,扩展 Canvas 绘图逻辑;
- 兼容低版本浏览器:添加MutationObserver和 Canvas 的降级方案,支持 IE11 等低版本浏览器;
- 增加渐变文字:通过ctx.createLinearGradient()实现渐变水印文字,提升水印美观度。
第六章 源码(开箱即用)
<template>
<!– 无需显性 DOM 节点,水印通过 JS 动态创建挂载到目标容器 –>
</template>
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
const props = defineProps({
text: { type: String, default: '内部文档 · 禁止外传' },
fontSize: { type: [Number, String], default: 14 },
fontFamily: { type: String, default: 'Microsoft Yahei, sans-serif' },
color: { type: String, default: 'rgba(180, 180, 180, 0.3)' },
rotate: { type: [Number, String], default: -15 },
spacing: { type: [Number, String], default: 50 },
zIndex: { type: [Number, String], default: 999999 },
debounceTime: { type: [Number, String], default: 300 },
container: { type: [String, Object], default: 'body' },
lineHeight: { type: [Number, String], default: 1.5 },
// 新增:水印单元重复数量(控制背景图包含的水印单元数)
repeatCount: { type: [Number, String], default: 2 }
})
// 保存核心实例引用
const watermarkDom = ref(null)
const observer = ref(null)
const operationHandler = ref(null)
const watermarkId = ref('')
const targetContainer = ref(null)
const originContainerStyle = ref({ position: '', overflow: '' })
/**
* 获取目标容器实例
*/
const _getTargetContainer = () => {
let container = null
if (typeof props.container === 'string') {
container = document.querySelector(props.container)
} else if (props.container instanceof HTMLElement) {
container = props.container
}
if (!container || !(container instanceof HTMLElement)) {
console.warn('水印目标容器不存在,默认使用body')
container = document.body
}
return container
}
/**
* 拆分过长文字为多行
*/
const _splitTextToLines = (ctx, text, maxWidth) => {
if (!text) return []
const lines = []
let currentLine = ''
const chars = text.split('')
for (const char of chars) {
const testLine = currentLine + char
const testWidth = ctx.measureText(testLine).width
if (testWidth > maxWidth && currentLine) {
lines.push(currentLine)
currentLine = char
} else {
currentLine = testLine
}
}
if (currentLine) lines.push(currentLine)
return lines
}
/**
* 生成多单元水印背景图(核心优化)
*/
const _createCanvasBase64 = () => {
const { text, fontSize, fontFamily, color, rotate, spacing, lineHeight, repeatCount } = props
const fontSizeNum = Number(fontSize)
const lineHeightNum = Number(lineHeight) || 1.5
const spacingNum = Number(spacing)
const repeatCountNum = Number(repeatCount) || 2
// 单个水印单元的尺寸
const singleWidth = spacingNum * 2
const singleMaxTextWidth = singleWidth * 0.8
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// 初始化画布上下文
ctx.fillStyle = color
ctx.font = `${fontSizeNum}px ${fontFamily}`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
// 拆分文字并计算单个单元高度
const textLines = _splitTextToLines(ctx, text, singleMaxTextWidth)
const singleHeight = Math.max(spacingNum * 2, textLines.length * fontSizeNum * lineHeightNum)
// 生成包含多个单元的背景图(如2×2)
const canvasWidth = singleWidth * repeatCountNum
const canvasHeight = singleHeight * repeatCountNum
canvas.width = canvasWidth
canvas.height = canvasHeight
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
// 绘制多单元水印
for (let x = 0; x < repeatCountNum; x++) {
for (let y = 0; y < repeatCountNum; y++) {
const unitX = singleWidth * x + singleWidth / 2
const unitY = singleHeight * y + singleHeight / 2
// 保存当前上下文状态
ctx.save()
// 平移到当前单元中心
ctx.translate(unitX, unitY)
// 旋转
ctx.rotate((Number(rotate) * Math.PI) / 180)
// 绘制多行文字
const totalTextHeight = (textLines.length – 1) * fontSizeNum * lineHeightNum
textLines.forEach((line, index) => {
const yOffset = (index – (textLines.length – 1) / 2) * fontSizeNum * lineHeightNum
ctx.fillText(line, 0, yOffset)
})
// 恢复上下文状态
ctx.restore()
}
}
return canvas.toDataURL('image/png')
}
/**
* 创建水印DOM元素(适配完整平铺)
*/
const _createWatermarkDom = () => {
const { zIndex, spacing, repeatCount } = props
const base64Url = _createCanvasBase64()
const container = targetContainer.value
const spacingNum = Number(spacing)
const repeatCountNum = Number(repeatCount) || 2
const singleWidth = spacingNum * 2
const singleHeight = spacingNum * 2
// 容器尺寸和定位方式
const containerWidth = container === document.body ? '100vw' : `${container.offsetWidth}px`
const containerHeight = container === document.body ? '100vh' : `${container.offsetHeight}px`
const positionType = container === document.body ? 'fixed' : 'absolute'
// 背景图尺寸(多单元整体尺寸)
const bgSize = `${singleWidth * repeatCountNum}px ${singleHeight * repeatCountNum}px`
// 背景图定位(确保左上角对齐)
const bgPosition = `0 0`
const watermark = document.createElement('div')
watermark.style.cssText = `
position: ${positionType} !important;
top: 0 !important;
left: 0 !important;
width: ${containerWidth} !important;
height: ${containerHeight} !important;
z-index: ${zIndex} !important;
pointer-events: none !important;
background-image: url(${base64Url}) !important;
background-repeat: repeat !important;
background-position: ${bgPosition} !important;
background-size: ${bgSize} !important;
user-select: none !important;
border: none !important;
margin: 0 !important;
padding: 0 !important;
opacity: 1 !important;
display: block !important;
`
watermarkId.value = 'unique-watermark-' + Date.now()
watermark.dataset.watermarkId = watermarkId.value
return watermark
}
/**
* 初始化容器样式(添加溢出隐藏)
*/
const _initContainerStyle = () => {
const container = targetContainer.value
if (container !== document.body) {
const computedStyle = window.getComputedStyle(container)
originContainerStyle.value = {
position: computedStyle.position,
overflow: computedStyle.overflow
}
// 确保容器有定位且溢出隐藏
if (
!['relative', 'absolute', 'fixed', 'sticky'].includes(originContainerStyle.value.position)
) {
container.style.position = 'relative !important'
}
container.style.overflow = 'hidden !important'
}
}
/**
* 初始化DOM篡改监测
*/
const _initMutationObserver = () => {
if (observer.value) observer.value.disconnect()
const observerOptions = {
childList: true,
attributes: true,
subtree: true,
attributeFilter: ['style', 'class', 'dataset']
}
observer.value = new MutationObserver((mutations) => {
console.log('mutations', mutations)
let needReRender = false
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.removedNodes.length) {
const isWatermarkRemoved = Array.from(mutation.removedNodes).some(
(node) => node.dataset?.watermarkId === watermarkId.value
)
if (isWatermarkRemoved) needReRender = true
}
if (
mutation.type === 'attributes' &&
mutation.target.dataset?.watermarkId === watermarkId.value
) {
needReRender = true
}
})
if (needReRender) resetWatermark()
})
observer.value.observe(targetContainer.value, observerOptions)
}
/**
* 防抖函数
*/
const _debounce = (fn, delay) => {
let timer = null
return function () {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, arguments), delay)
}
}
/**
* 监听用户操作
*/
const _initUserOperationListener = () => {
const { debounceTime } = props
operationHandler.value = _debounce(() => resetWatermark(), Number(debounceTime))
const userEvents = ['click', 'input', 'scroll', 'dragend', 'keydown', 'mouseup', 'touchend']
userEvents.forEach((event) => {
targetContainer.value.addEventListener(event, operationHandler.value, { passive: true })
})
}
/**
* 渲染水印
*/
const renderWatermark = () => {
destroyWatermark(false)
targetContainer.value = _getTargetContainer()
_initContainerStyle()
watermarkDom.value = _createWatermarkDom()
targetContainer.value.appendChild(watermarkDom.value)
_initMutationObserver()
_initUserOperationListener()
}
/**
* 重置水印
*/
const resetWatermark = () => {
destroyWatermark(false)
watermarkDom.value = _createWatermarkDom()
targetContainer.value.appendChild(watermarkDom.value)
_initMutationObserver()
}
/**
* 销毁水印(还原容器样式)
*/
const destroyWatermark = (isClearOperationListener = true) => {
if (watermarkDom.value && watermarkDom.value.parentNode) {
watermarkDom.value.parentNode.removeChild(watermarkDom.value)
watermarkDom.value = null
}
if (observer.value) {
observer.value.disconnect()
observer.value = null
}
if (isClearOperationListener && operationHandler.value) {
const userEvents = ['click', 'input', 'scroll', 'dragend', 'keydown', 'mouseup', 'touchend']
userEvents.forEach((event) => {
targetContainer.value.removeEventListener(event, operationHandler.value)
})
operationHandler.value = null
}
// 还原容器原有样式
if (targetContainer.value && targetContainer.value !== document.body) {
targetContainer.value.style.position = originContainerStyle.value.position
targetContainer.value.style.overflow = originContainerStyle.value.overflow
originContainerStyle.value = { position: '', overflow: '' }
}
}
// 暴露方法
defineExpose({ renderWatermark, resetWatermark, destroyWatermark })
// 生命周期
onMounted(() => renderWatermark())
onUnmounted(() => destroyWatermark())
</script>
<style scoped>
/* 无额外样式 */
</style>
网硕互联帮助中心





评论前必须登录!
注册