hello,大家好啊!!!昨天被服务端小坑一把,导致昨天不得不加个小班,原本可以舒舒服服的准点下班.....可是......很不爽想喷......但是我不说......哈哈哈!!!怎么一回事呢?之前我们公司的水印是通过配置服务端生成实现的水印服务,昨天有个客户对水印的配置不满意,除了简单的字体和文本信息可配置,满足不了甲方爸爸的需求,让后端改吧,来一句,没时间,排期,无奈领导找到我能不能前端提供可配置组件去实现呢???脑袋嗡嗡的,又补了一句,客户这周要???
水印功能介绍
水印是一种保护知识产权的功能,需要尽可能的防止水印被用户可以篡改、删除等操作。通过这种思想需要具备一定的防止篡改的能力。由于水印的生成还是有比较多的因素决定他的具体效果,需要加配置来满足一个前端水印组件的基本需求,主要的配置有:生成水印的配置(宽、高、水印文本、字体大小、颜色、透明度、倾斜角度等等)、全局水印还是局部水印、是否开启防篡改功能。
基本的思路
- 生成水印的背景
- 创建水印元素
- 设置水印元素
- 如何实现水印的防止篡改功能
Hook封装函数的介绍
- hooks是什么? Vue3的hooks就是函数的一种写法,将独立的js功能代码抽离封装起来使用。Vue3中的hooks有点类似Vue2的mixins,相对于mixins来说,hooks的代码复用性更强,逻辑更加清晰。
- hooks的优点: hooks作为独立逻辑的组件封装,其内部属性、函数和外部组件具有响应式依附作用。使用Vue3组合API去封装的可复用性强,高内聚低耦合。
下面让我们开始探索如何去实现吧。。。
简单做一些准备工作
ts
import { ref, onBeforeUnmount, type Ref } from 'vue'
// 定义一个配置的接口
interface DefaultConfig {
color: string,
opacity: number,
size: number,
angle: number,
width: number,
height: number,
text: string,
defense: boolean
}
export function useWaterMark(parentEl: Ref<HTMLElement | null) {
// 初始化一个默认配置
let config: DefaultConfig = {
color: "hotpink",
opacity: 0.8,
size: 56,
angle: -45,
width: 120,
height: 120,
text: '内部资源,禁止转载',
defense: false
}
// 定义一个合并的参数
let mergeConfig: DefaultConfig
// 创建水印元素
let let waterMarkEl: HTMLElement | null = null
// 创建观察器
const observer: Observer = {}
// 根据配置获取背景颜色
const getWaterMarkBgByBase64 = () => {}
// 创建水印元素,需要根据配置来判断是全局水印还是局部水印
const createWaterMarkEl = () => {}
// 设置水印
const setWaterMark = () => {}
// 添加MutationObserve观察器来监听水印dom是否被修改、删除
const addMutationListener = () => {}
// 监听视口的变化,重新计算水印dom的宽高
const addResizeListener = () => {}
// 添加监听器
const addListener = () => {}
// 移除监听器
const removeListener = () => {}
// 清除水印
const clearWaterMark = () => {}
// 更新水印Dom元素
const updateWatermarkEl = () => {}
// 刷新水印
const updateWaterMark = () => {}
// 防篡改功能切换
const tonggleDefenseWaterMark = () => {}
// 组件卸载清除水印
onBeforeUnmount(() => {
clearWaterMark()
})
return {
setWaterMark,
clearWaterMark,
tonggleDefenseWaterMark
}
}
准备工作做好了,接下来就是让大家一个一个的填空了...
生成水印的背景
水印的实质就是给水印的DOM添加一个背景颜色,通过设置样式来实现水印效果,所以我们需要这么一个背景颜色。 不管什么配置,我们最终是得到一个base64!
首先我们有一个默认的配置config,还有调用hook方法的时候传入的配置,两者合并就是最终的水印配置mergeConfig
ts
const getWaterMarkBgByBase64 = () => {
const { width, height, size, text, angle, color, opacity } = mergeConfig
// 创建一个canvas, 设置相关属性,转化成base64
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = width
canvas.height = height
if (!!ctx) {
ctx.fillStyle = color
ctx.globalAlpha = opacity
ctx.font = `${size}px`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((Math.PI / 180) * angle)
ctx.fillText(text, 0, 0)
}
return canvas.toDataURL()
}
这样我们就得到了一个base64,可以为后面水印元素设置背景颜色了
创建水印元素
注意:这里需要根据配置来判断是局部水印还是全局水印
ts
// 创建水印元素:要判断是全局水印 还是 局部水印
const createWaterMarkEl = () => {
// 判断是全局水印还是局部水印
const isBody = parentEl.value!.tagName.toLowerCase() === bodyEl.value.tagName.toLowerCase()
const { clientWidth, clientHeight } = parentEl.value!
waterMarkEl = document.createElement('div')
waterMarkEl.style.pointerEvents = 'none'
waterMarkEl.style.top = '0'
waterMarkEl.style.left = '0'
waterMarkEl.style.position = isBody ? 'fixed' : 'absolute'
waterMarkEl.style.zIndex = '9999'
waterMarkEl.style.inset = '0'
updateWatermarkEl({ width: clientWidth, height: clientHeight })
// 设置水印元素的相对定位
parentEl.value!.style.position = isBody ? '' : 'relative'
parentEl.value!.appendChild(waterMarkEl)
}
通过MutationObserver来创建观察器
介绍一下这个API:
MutationObserver
,它可以去监听一个元素的变化,不仅仅是监听元素本身的属性,还可以监听这个元素下面子元素的属性。我们要知道要监听的是哪一个元素?
ts
const addMutationListener = (targetNode: HTMLElement) => {
// 我们需要监听水印Dom和水印Dom的父级元素
const mutationCallback = (mutations: MutationRecord[]) => {
console.log('mutations ===== ', mutations)
mutations.forEach((mutation: MutationRecord) => {
switch(mutation.type) {
case "attributes":
// 水印元素属性被修改
if (mutation.target === waterMarkEl) {
// ....
}
break;
case "childList":
// 水印元素被删除
mutation.removedNodes.forEach((itemNode) => {
if (itemNode === waterMarkEl) {
// ...
}
})
break;
}
})
}
observer.waterMarkElMutationObserver = new MutationObserver(mutationCallback)
observer.parentElMutationObserver = new MutationObserver(mutationCallback)
observer.waterMarkElMutationObserver.observe(
waterMarkEl!,
{
attributes: true,
childList: false,
subtree: false
}
)
observer.parentElMutationObserver.observe(
targetNode,
{
attributes: false,
childList: true,
subtree: false
}
)
}
针对浏览器窗口缩放时刷新水印dom
监听浏览器窗口变化我们常用
resize
事件,这里推荐一个新的APIResizeObserver
,创建一个浏览器视口观察器
ts
const addResizeListener = (target: HTMLElement) => {
const resizeObserverCallback = () => {}
// 创建观察器
observer.parentElResizeObserver = new ResizeObserver(resizeObserverCallback)
observer.parentElResizeObserver?.observe(target)
}
添加监听
这里是否开启监听,根据配置的
defense
去做判断
ts
const addListener = (targetNode: HTMLElement) => {
if (mergeConfig.defense) {
if (!observer.waterMarkElMutationObserver && !observer.parentElMutationObserver) addMutationListener(targetNode)
} else {
removeListener()
}
if (!observer.parentElResizeObserver) addResizeListener(targetNode)
}
设置水印
这样核心的功能基本完成了,需要暴露出一个方法,来设置水印
ts
const setWaterMark = (props: Partial<DefaultConfig> = {}) => {
mergeConfig = { ...config, ...props }
// 创建和更新水印元素
createWaterMarkEl()
// 监听水印元素是否被篡改
addListener(parentEl.value)
}
这样我们的功能基本就差不多实现了,来试一下效果。。。
创建一个Demo试一试我们的hook
vue
<script lang="ts" setup>
import { ref } from "vue"
import { useWaterMark } from "./useWaterMark/index"
const waterMarkRef = ref<HTMLElement | null>(null)
const { setWaterMark, clearWaterMark, tonggleDefenseWaterMark } = useWaterMark(waterMarkRef)
const {
setWaterMark: setGlobalWatermark,
clearWaterMark: clearGlobalWatermark,
tonggleDefenseWaterMark: tonggleDefenseGlobalWaterMark
} = useWaterMark()
</script>
<template>
<div class="app-container">
<div ref="waterMarkRef" class="water-mark" />
<el-button-group>
<el-button type="primary" @click="setWaterMark({ text: '测试创建局部水印' })">创建局部水印</el-button>
<el-button type="warning" @click="tonggleDefenseWaterMark">切换防篡改功能</el-button>
<el-button type="danger" @click="clearWaterMark">清除局部水印</el-button>
</el-button-group>
<el-button-group>
<el-button type="primary" @click="setGlobalWatermark({ text: '测试创建全局水印' })">创建全局水印</el-button>
<el-button type="warning" @click="tonggleDefenseGlobalWaterMark">切换防篡改功能</el-button>
<el-button type="danger" @click="clearGlobalWatermark">清除全局水印</el-button>
</el-button-group>
</div>
</template>
创建一下水印吧。。。
这样我们就完美的实现了基本的创建水印的功能了。。。
最后优化了一下,完整的Hook代码如下:
ts
import { ref, onBeforeUnmount, type Ref } from 'vue'
import { debounce } from 'lodash-es'
interface DefaultConfig {
color: string,
opacity: number,
size: number,
angle: number,
width: number,
height: number,
text: string,
defense: boolean
}
type Observer = {
waterMarkElMutationObserver?: MutationObserver
parentElMutationObserver?: MutationObserver
parentElResizeObserver?: ResizeObserver
}
// 获取body
const bodyEl = ref<HTMLElement>(document.body)
export function useWaterMark(parentEl: Ref<HTMLElement | null> = bodyEl) {
let config: DefaultConfig = {
color: "hotpink",
opacity: 0.8,
size: 56,
angle: -45,
width: 120,
height: 120,
text: '内部资源,禁止转载',
defense: false
}
let mergeConfig: DefaultConfig
let waterMarkEl: HTMLElement | null = null
// 创建一个观察器
const observer: Observer = {
waterMarkElMutationObserver: undefined,
parentElMutationObserver: undefined,
parentElResizeObserver: undefined
}
// 根据配置,创建base64的背景颜色
const getWaterMarkBgByBase64 = () => {
const { width, height, size, text, angle, color, opacity } = mergeConfig
// 创建一个canvas, 设置相关属性,转化成base64
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = width
canvas.height = height
if (!!ctx) {
ctx.fillStyle = color
ctx.globalAlpha = opacity
ctx.font = `${size}px`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((Math.PI / 180) * angle)
ctx.fillText(text, 0, 0)
}
return canvas.toDataURL()
}
// 创建水印元素:要判断是全局水印 还是 局部水印
const createWaterMarkEl = () => {
// 判断是全局水印还是局部水印
const isBody = parentEl.value!.tagName.toLowerCase() === bodyEl.value.tagName.toLowerCase()
const { clientWidth, clientHeight } = parentEl.value!
waterMarkEl = document.createElement('div')
waterMarkEl.style.pointerEvents = 'none'
waterMarkEl.style.top = '0'
waterMarkEl.style.left = '0'
waterMarkEl.style.position = isBody ? 'fixed' : 'absolute'
waterMarkEl.style.zIndex = '9999'
waterMarkEl.style.inset = '0'
updateWatermarkEl({ width: clientWidth, height: clientHeight })
// 设置水印元素的相对定位
parentEl.value!.style.position = isBody ? '' : 'relative'
parentEl.value!.appendChild(waterMarkEl)
}
// 设置水印
const setWaterMark = (props: Partial<DefaultConfig> = {}) => {
mergeConfig = { ...config, ...props }
if (!parentEl.value) return
// 创建水印前,先清除水印
clearWaterMark()
// 创建和更新水印元素
waterMarkEl ? updateWatermarkEl() : createWaterMarkEl()
// 监听水印元素是否被篡改
addListener(parentEl.value)
}
// 通过MutationObserver来监听水印dom是否被修改、删除
const addMutationListener = (targetNode: HTMLElement) => {
// 创建观察回调
const mutationCallback = debounce((mutations: MutationRecord[]) => {
console.log('mutations ===== ', mutations)
// 防止水印主要是:水印元素被删除 和 通过CSS将水印隐藏
mutations.forEach((mutation: MutationRecord) => {
switch(mutation.type) {
case "attributes":
// 水印元素属性被修改
mutation.target === waterMarkEl && updateWaterMark()
break;
case "childList":
// 水印元素被删除
mutation.removedNodes.forEach((itemNode) => {
itemNode === waterMarkEl && updateWaterMark()
})
break;
}
})
}, 200)
// 创建观察器
if (!!waterMarkEl) {
observer.waterMarkElMutationObserver = new MutationObserver(mutationCallback)
observer.waterMarkElMutationObserver.observe(waterMarkEl!, { attributes: true, childList: false, subtree: false })
}
observer.parentElMutationObserver = new MutationObserver(mutationCallback)
observer.parentElMutationObserver.observe(targetNode, { attributes: false, childList: true, subtree: false })
}
// 监听视口变化,重新计算水印的宽高
const addResizeListener = (target: HTMLElement) => {
const resizeObserverCallback = debounce(() => {
const { clientWidth, clientHeight } = target
updateWatermarkEl({
width: clientWidth,
height: clientHeight
})
}, 200)
// 创建观察器
observer.parentElResizeObserver = new ResizeObserver(resizeObserverCallback)
observer.parentElResizeObserver?.observe(target)
}
const removeListener = () => {
observer.waterMarkElMutationObserver?.disconnect()
observer.waterMarkElMutationObserver = undefined
observer.parentElMutationObserver?.disconnect()
observer.parentElMutationObserver = undefined
observer.parentElResizeObserver?.disconnect()
observer.parentElResizeObserver = undefined
}
const addListener = (targetNode: HTMLElement) => {
if (mergeConfig.defense) {
if (!observer.waterMarkElMutationObserver && !observer.parentElMutationObserver) addMutationListener(targetNode)
} else {
removeListener()
}
if (!observer.parentElResizeObserver) addResizeListener(targetNode)
}
// 别篡改了水印元素,先清除 再创建
const clearWaterMark = () => {
if (!parentEl.value || !waterMarkEl) return;
// 移除所有的监听
removeListener()
// 移除水印元素
try {
parentEl.value.removeChild(waterMarkEl)
} catch {
// 捕获水印元素不存在的报错
console.warn('水印元素不存在,移除失败')
} finally {
waterMarkEl = null
}
}
// 更新水印元素
const updateWatermarkEl = (options: Partial<{ width: number, height: number }> = {}) => {
if (!waterMarkEl) return
waterMarkEl.style.background = `url(${getWaterMarkBgByBase64()}) 200px 200px repeat`
options.width && (waterMarkEl.style.width = `${options.width}px`)
options.height && (waterMarkEl.style.height = `${options.height}px`)
}
// 刷新水印
const updateWaterMark = debounce(() => {
clearWaterMark()
createWaterMarkEl()
addMutationListener(parentEl.value!)
}, 200)
// 切换水印防篡改功能
const tonggleDefenseWaterMark = () => {
if (!parentEl.value || !mergeConfig) return
mergeConfig.defense = !mergeConfig.defense
if (mergeConfig.defense) {
addListener(parentEl.value)
} else {
removeListener()
}
}
// 组件卸载要清除水印
onBeforeUnmount(() => {
clearWaterMark()
})
return {
setWaterMark,
clearWaterMark,
tonggleDefenseWaterMark
}
}
结束标语
水印的需求无非还是在如何防止篡改上面,这才是核心,但是说到底也就是几点: 1.如何得到base64,通过配置、上传图片文件、还是canvas画出来都可以 2.确定插入水印的区域 3.如何防止水印被删除、修改,无非就是对MutationObserver
这个API是否熟悉
最主要的一点是对Vue3自定义Hook的封装使用,本文也是看了pany
的启发,大家有兴趣可以去pany的github看一下
希望对大家有帮助啦~~~