通过Hooks来封装前端水印函数(MutationObserver、ResizeObserver)

hello,大家好啊!!!昨天被服务端小坑一把,导致昨天不得不加个小班,原本可以舒舒服服的准点下班.....可是......很不爽想喷......但是我不说......哈哈哈!!!怎么一回事呢?之前我们公司的水印是通过配置服务端生成实现的水印服务,昨天有个客户对水印的配置不满意,除了简单的字体和文本信息可配置,满足不了甲方爸爸的需求,让后端改吧,来一句,没时间,排期,无奈领导找到我能不能前端提供可配置组件去实现呢???脑袋嗡嗡的,又补了一句,客户这周要???

水印功能介绍

水印是一种保护知识产权的功能,需要尽可能的防止水印被用户可以篡改、删除等操作。通过这种思想需要具备一定的防止篡改的能力。由于水印的生成还是有比较多的因素决定他的具体效果,需要加配置来满足一个前端水印组件的基本需求,主要的配置有:生成水印的配置(宽、高、水印文本、字体大小、颜色、透明度、倾斜角度等等)、全局水印还是局部水印、是否开启防篡改功能。

基本的思路

  1. 生成水印的背景
  2. 创建水印元素
  3. 设置水印元素
  4. 如何实现水印的防止篡改功能

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看一下

希望对大家有帮助啦~~~

相关推荐
吃杠碰小鸡28 分钟前
lodash常用函数
前端·javascript
emoji11111137 分钟前
前端对页面数据进行缓存
开发语言·前端·javascript
泰伦闲鱼40 分钟前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
m0_748250031 小时前
Web 第一次作业 初探html 使用VSCode工具开发
前端·html
一个处女座的程序猿O(∩_∩)O1 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
m0_748235951 小时前
web复习(三)
前端
迷糊的『迷』1 小时前
vue-axios+springboot实现文件流下载
vue.js·spring boot
web135085886351 小时前
uniapp小程序使用webview 嵌套 vue 项目
vue.js·小程序·uni-app
AiFlutter1 小时前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter
麦兜*1 小时前
轮播图带详情插件、uniApp插件
前端·javascript·uni-app·vue