水印功能在前端开发中越来越常见,无论是保护版权内容还是防止敏感信息泄露,水印都扮演着重要角色。在Vue3中实现一个基础水印组件其实并不复杂,我们先从最简单的文字水印开始。
创建一个基本的Watermark.vue组件,核心思路是使用Canvas绘制水印内容,然后将其作为背景图应用到目标元素上。下面是最简化的实现代码:
javascript复制<template>
<div ref="containerRef" style="position: relative">
<slot></slot>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
const containerRef = ref()
const watermarkRef = ref()
const props = defineProps({
content: String,
rotate: { type: Number, default: -22 },
zIndex: { type: Number, default: 9 }
})
function renderWatermark() {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// 设置canvas大小
canvas.width = 200
canvas.height = 150
// 绘制水印文字
ctx.font = '16px sans-serif'
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)'
ctx.rotate((Math.PI / 180) * props.rotate)
ctx.fillText(props.content, 20, 60)
// 创建水印元素
if (!watermarkRef.value) {
watermarkRef.value = document.createElement('div')
containerRef.value.appendChild(watermarkRef.value)
}
// 应用水印样式
watermarkRef.value.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background-repeat: repeat;
z-index: ${props.zIndex};
background-image: url('${canvas.toDataURL()}');
`
}
onMounted(renderWatermark)
watch(() => props.content, renderWatermark)
</script>
这个基础版本已经可以实现简单的文字水印效果。使用时只需要在父组件中引入:
javascript复制<Watermark content="Confidential" />
<div>你的内容...</div>
</Watermark>
在实际项目中,我们还需要考虑更多细节。比如水印的密度、旋转角度、透明度等参数都应该支持配置。我在项目中遇到过水印太密集影响阅读体验的问题,后来通过增加gap参数控制水印间距解决了这个问题。
基础水印组件只能显示单行文字,实际业务中我们可能需要更复杂的水印形式。比如多行文字水印或者图片水印。
实现多行文字水印只需要修改content属性支持数组类型:
javascript复制props: {
content: [String, Array]
}
// 在renderWatermark函数中
if (Array.isArray(props.content)) {
props.content.forEach((text, i) => {
ctx.fillText(text, 20, 60 + i * 30)
})
} else {
ctx.fillText(props.content, 20, 60)
}
图片水印的实现稍微复杂一些,需要先加载图片资源:
javascript复制props: {
image: String
}
function renderWatermark() {
if (props.image) {
const img = new Image()
img.onload = () => {
ctx.drawImage(img, 0, 0, 100, 50)
applyWatermark()
}
img.src = props.image
} else {
// 文字水印逻辑
}
}
有些场景需要在整个页面上显示水印,而不仅仅是某个容器内部。我们可以通过fullscreen参数来控制:
javascript复制props: {
fullscreen: Boolean,
fixed: Boolean
}
function applyWatermark() {
const target = props.fullscreen ? document.body : containerRef.value
watermarkRef.value.style.position = props.fixed ? 'fixed' : 'absolute'
target.appendChild(watermarkRef.value)
}
全屏水印的一个常见问题是页面滚动时水印位置不正确。我通过fixed定位解决了这个问题,但要注意fixed定位可能会影响页面其他元素的z-index层级。
现在很多网站都支持暗黑模式,水印也需要相应调整。我们可以通过CSS滤镜实现自动适配:
javascript复制const isDark = ref(false)
function checkDarkMode() {
isDark.value = document.documentElement.classList.contains('dark')
}
function applyWatermark() {
if (isDark.value) {
watermarkRef.value.style.filter = 'invert(1) hue-rotate(180deg)'
}
}
// 监听暗黑模式变化
useMutationObserver(
document.documentElement,
() => {
checkDarkMode()
applyWatermark()
},
{ attributeFilter: ['class'] }
)
这个方案在项目中表现很好,水印颜色会自动适应主题变化,不需要手动切换。
基础水印很容易被用户通过开发者工具删除或修改。为了防止这种情况,我们可以使用MutationObserver监听DOM变化:
javascript复制function initMutationObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.removedNodes.length) {
Array.from(mutation.removedNodes).forEach((node) => {
if (node === watermarkRef.value) {
renderWatermark()
}
})
}
})
})
observer.observe(props.fullscreen ? document.body : containerRef.value, {
childList: true,
subtree: true,
attributes: true
})
}
这个实现有个小问题:当水印被删除时会立即重新创建,导致观察器再次触发。我通过添加一个标志位解决了这个循环触发的问题:
javascript复制let isRendering = false
function renderWatermark() {
if (isRendering) return
isRendering = true
// 渲染逻辑...
setTimeout(() => {
isRendering = false
}, 100)
}
除了删除节点,用户还可能通过修改样式来隐藏水印。我们可以通过定期检查水印样式来防止这种情况:
javascript复制function checkWatermarkStyle() {
if (!watermarkRef.value) return
const styles = window.getComputedStyle(watermarkRef.value)
if (styles.display === 'none' || styles.visibility === 'hidden') {
renderWatermark()
}
}
setInterval(checkWatermarkStyle, 1000)
不过要注意,频繁的样式检查可能会影响性能。在实际项目中,我优化了这个检查逻辑,只有当页面获得焦点时才进行检查。
更高级的防篡改方案是使用Canvas指纹技术。每个设备的Canvas渲染结果都有微小差异,我们可以利用这个特性生成唯一的水印:
javascript复制function generateFingerprint() {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.fillText('Vue3Watermark', 10, 50)
return canvas.toDataURL().slice(-20)
}
// 将指纹信息嵌入水印内容
const fingerprint = generateFingerprint()
props.content += ` ${fingerprint}`
这样即使水印被截图,也能追踪到来源。我在一个金融项目中使用了这个技术,效果很好。
水印组件的一个常见性能问题是重复渲染。特别是在响应式场景下,任何props变化都会触发重新渲染。我通过以下优化显著提升了性能:
javascript复制const debouncedRender = debounce(renderWatermark, 100)
watch(() => [
props.content,
props.rotate,
props.zIndex
], debouncedRender, { deep: true })
另一个优化点是Canvas绘制。高分辨率屏幕需要更大的Canvas尺寸:
javascript复制const ratio = window.devicePixelRatio || 1
canvas.width = 200 * ratio
canvas.height = 150 * ratio
ctx.scale(ratio, ratio)
在移动端实现水印时遇到了几个坑。首先是触摸事件穿透问题:
javascript复制watermarkRef.value.style.pointerEvents = 'none'
其次是移动端浏览器可能会压缩背景图片,导致水印模糊。解决方案是使用足够大的Canvas尺寸和更明显的文字样式。
如果项目使用SSR,直接使用Canvas API会报错。我通过条件渲染解决了这个问题:
javascript复制onMounted(() => {
if (typeof window !== 'undefined') {
renderWatermark()
}
})
在电商项目中,水印需要包含用户ID和时间信息。最初直接在客户端生成这些信息,后来发现存在安全隐患。最终方案是让后端生成带签名的水印数据:
javascript复制async function fetchWatermarkData() {
const res = await fetch('/api/watermark')
const { content, signature } = await res.json()
// 验证签名...
return content
}
另一个坑是水印与模态框的z-index冲突。通过动态计算z-index解决了这个问题:
javascript复制function getMaxZIndex() {
return Math.max(
...Array.from(document.querySelectorAll('body *'))
.map(el => parseInt(window.getComputedStyle(el).zIndex))
.filter(zIndex => !isNaN(zIndex)),
1000
)
}
props.zIndex = getMaxZIndex() + 1
水印组件虽然看起来简单,但在实际项目中需要考虑的细节非常多。从基础实现到防篡改方案,再到性能优化和特殊场景适配,每个环节都可能遇到意料之外的问题。经过多个项目的实践,我发现最可靠的方案是组合使用Canvas渲染、MutationObserver监听和定期校验,同时保持组件的灵活性和可配置性。