页面水印的实现以及防删除方案

引言

在企业里为了防止信息泄露和保护知识产权,通常会在页面和图片上添加水印
前端页面水印的添加一般有这几种方式:dom 元素循环、canvas 输出背景图、svg 实现背景图、图片添加水印

dom 元素循环 性能太低也不优雅,一般不采用这种方式
svg 实现背景图 与 canvas 类似,且兼容性不如 canvas
图片添加水印 是针对在图片上加的水印

本篇文章重点讲一下canvas 输出背景图、以及防止删除的方案来生成水印方式

绘制一个水印

目标是实现页面上按照一定的排列展示的水印,那么首先要用 canvas 画出一个水印

我们先在 html 放一个 canvas 标签,对它进行一个绘制

<template>
  <canvas id="water"></canvas>
</template>
onMounted(() => {
  const canvas: HTMLCanvasElement = document.getElementById(
    'water'
  ) as HTMLCanvasElement
  canvas.width = 440
  canvas.height = 400
  const ctx = canvas.getContext('2d')
  if (ctx) {
    ctx.font = '60px PingFang SC'
    ctx.fillStyle = 'rgba(156, 162, 169, 0.3)'
    ctx.rotate(-0.4)
    ctx.fillText('krryguo', 20, 280)
  }
})

页面效果如图:
请添加图片描述
一个水印画出来了,怎么衍生多个水印并添加到指定页面中呢?

输出背景图

直接循环多份代码绘制是不合理的。我们可以利用 background 属性 repeat 特性,将水印展示成多个且平铺在整个页面中

将 canvas 生成的画布输出成base64的字符串,来作为页面的背景图。那么 dom 结构可以不需要 canvas 元素了,动态生成即可

<template>
  <div class="water-mark">首页</div>
</template>
onMounted(() => {
  const canvas: HTMLCanvasElement = document.createElement('canvas')
  canvas.width = 440
  canvas.height = 400
  const ctx = canvas.getContext('2d')
  if (ctx) {
    ctx.font = '60px PingFang SC'
    ctx.fillStyle = 'rgba(156, 162, 169, 0.3)'
    ctx.rotate(-0.4)
    ctx.fillText('krryguo', 40, 200)
  }
  const imgStr = canvas.toDataURL('image/png')

  const waterDom = document.getElementsByClassName(
    'water-mark'
  )[0] as HTMLElement
  waterDom.style.background = `url(${imgStr})`
})

实现的效果图:
请添加图片描述
看到这里,有朋友不高兴了,排列的太整齐,你这水印有问题啊~

最简单解决方式就直接绘制 两个斜对称排列 的水印即可

onMounted(() => {
  const canvas: HTMLCanvasElement = document.createElement('canvas')
  canvas.width = 880 // 原有的基础上增加一倍宽度
  canvas.height = 400
  const ctx = canvas.getContext('2d')
  if (ctx) {
    ctx.font = '60px PingFang SC'
    ctx.fillStyle = 'rgba(156, 162, 169, 0.3)'
    ctx.rotate(-0.4)
    ctx.fillText('krryguo', 40, 200)
    ctx.fillText('krryguo', 350, 556) // 再画一个
  }
  const imgStr = canvas.toDataURL('image/png')

  const waterDom = document.getElementsByClassName(
    'water-mark'
  )[0] as HTMLElement
  waterDom.style.background = `url(${imgStr})`
})

再看效果图:
请添加图片描述

封装一点点细节

interface WatermarkOptions {
  // 宽度
  width?: number
  // 高度
  height?: number
  // 水印内容
  content?: string
  // 水印字体
  font?: string
  // 水印颜色
  color?: string
  // 透明度
  opacity?: number
  // 偏转角度
  degree?: number
  // 偏移量
  x1?: number
  y1?: number
  x2?: number
  y2?: number
}

const createWatermark = ({
  width = 880,
  height = 400,
  content = 'krryguo',
  font = '60px PingFang SC',
  color = 'rgba(156, 162, 169, 0.3)',
  opacity = 1,
  degree = -23,
  x1 = 40,
  y1 = 200,
  x2 = 350,
  y2 = 556
}: WatermarkOptions): string => {
  const canvas: HTMLCanvasElement = document.createElement('canvas')
  canvas.width = width
  canvas.height = height
  const ctx = canvas.getContext('2d')
  if (ctx) {
    ctx.font = font
    ctx.fillStyle = color
    ctx.globalAlpha = opacity
    // 顺时针旋转的弧度,计算公式: degree * Math.PI / 180
    ctx.rotate((degree * Math.PI) / 180)
    ctx.fillText(content, x1, y1)
    ctx.fillText(content, x2, y2)
  }
  return canvas.toDataURL('image/png')
}

const setWatermarkClass = (url: string, className: string): void => {
  const style = document.createElement('style')
  style.innerHTML = `.${className} {background-image: url(${url});}`
  document.head.appendChild(style)
}

可在有需要加水印的地方调用 setWatermarkClass,传入自定义配置和指定的 class 名,再在对应元素设置该 class 名即可加上水印

onMounted(() => {
  setWatermarkClass(
    createWatermark({
      content: 'krryblog'
    }),
    'my-water-mark'
  )
})
<template>
  <!-- 这里加上 my-water-mark 水印的类名 -->
  <div class="water-mark my-water-mark">首页</div>
</template>

图片加水印

同理,先读取图片,canvas 在图片上绘制水印

import waterUrl from '@/assets/water.jpeg'

const createImgWatermark = async (
  {
    url = '',
    textAlign = 'center',
    textBaseline = 'middle',
    font = '30px PingFang SC',
    fillStyle = '#fff',
    x = 120,
    y = 50,
    position = 'top-start'
  },
  content: string = '这是水印'
) => {
  const canvas: HTMLCanvasElement = document.createElement('canvas')
  const img = new Image()
  img.src = url
  img.setAttribute('crossOrigin', 'Anonymous')
  return new Promise((resolve) => {
    img.onload = () => {
      canvas.width = img.width
      canvas.height = img.height
      const ctx = canvas.getContext('2d')
      if (ctx) {
        ctx.drawImage(img, 0, 0)
        ctx.textAlign = textAlign
        ctx.textBaseline = textBaseline
        ctx.font = font
        ctx.fillStyle = fillStyle
        switch (position) {
          case 'top-end':
            x = img.width - x
            break
          case 'bottom-start':
            y = img.height - y
            break
          case 'bottom-end':
            x = img.width - x
            y = img.height - y
            break
        }
        ctx.fillText(content, x, y)
      }
      resolve(canvas.toDataURL())
    }
  })
}

const setImgWatermark = (url: string, dom: HTMLImageElement) => {
  dom.src = url
}

onMounted(async () => {
  const url = await createImgWatermark({
    url: waterUrl,
    font: '50px PingFang SC',
    x: 160,
    y: 70,
    position: 'bottom-end'
  })
  setImgWatermark(url, document.querySelector('img') as HTMLImageElement)
})
<template>
  <img width="600" />
</template>

效果图:
请添加图片描述

防止水印删除

前端生成水印的安全性是很弱的,懂点前端知识的人都会打开控制台修改去掉水印

这里提供一个方案,禁止用户删除 class 来防止水印删除

window 提供了一个监听器MutationObserver:监视对 DOM 树所做更改的能力
API 方法

  • disconnect()
    阻止 MutationObserver 实例继续接收的通知,直到再次调用其 observe()方法,该观察者对象包含的回调函数都不会再被调用
  • observe()
    配置 MutationObserver 在 DOM 更改匹配给定选项时,通过其回调函数开始接收通知
  • takeRecords()
    从 MutationObserver 的通知队列中删除所有待处理的通知,并将它们返回到 MutationRecord 对象的新 Array 中

先获取有水印类名的NodeList,遍历所有元素为其添加一个MutationObserver监听器来监听 dom 属性的变化,获取目标元素的classList,若不存在水印的 class 类名,就执行添加。执行添加之后要立刻暂停监听disconnect(),防止【添加】操作又触发监听器,最后再执行observe() 重新观察

// 添加监听器
const addListioner = (className: string) => {
  const MutationObserver = window.MutationObserver
  // 获取所有添加了水印类名的 dom
  const containerList: NodeListOf<HTMLElement> = document.querySelectorAll(
    `.${className}`
  )
  if (MutationObserver) {
    containerList.forEach((container) => {
      let observer = new MutationObserver(() => {
        // 获取 class 集合
        const classList: DOMTokenList = container.classList
        if (!Object.values(classList).includes(className)) {
          // 如果 classList 中不存在水印的类名,就重新添加
          container.classList.add(className)
          // 暂停监听,防止上面的操作又触发监听器
          observer.disconnect()
          // 然后再重新开始观察
          addObserve(observer, container)
        }
      })
      // 每个元素开启观察
      addObserve(observer, container)
    })
  }
}
// 开启观察
const addObserve = (mutation: MutationObserver, container: Element) => {
  mutation.observe(container, {
    // 观察器的配置,需要观察属性的变动
    attributes: true
  })
}

然后在 onMounted 添加一下监听器

onMounted(async () => {
  // TODO ...
  addListioner('my-water-mark')
})

问题

但是这又有一个问题,用户可以在控制台改变水印 class 里面的样式,而 MutationObserver 无法监听。如图:
请添加图片描述
这样水印说没就直接没啦

解决方案

解决方法:可以使用 style 属性 渲染水印,即 内联样式 ,这样若样式改变了就说明 dom 属性改变,也就可以监听到了

需要注意的是:要设置 !important,把优先级提到最高防止被恶意覆盖,后面的监听也要加上优先级的判断

onMounted(async () => {
  const bgUrl = createWatermark({
    content: 'krryblog'
  })

  // 使用 style 属性渲染水印
  const dom = document.querySelector('.water-mark-style') as HTMLElement
  // 设置样式优先级最高
  dom.style.setProperty('background-image', `url(${bgUrl})`, 'important')
})

缺点是控制台查看 dom 结构会有一大坨样式在这里…
请添加图片描述
最后再加上监听 顺带整合了 class 类名渲染、style 属性渲染两种监听方法

interface StyleType {
  key: string
  value: string
}

onMounted(async () => {
  const bgUrl = createWatermark({
    content: 'krryblog'
  })

  // 使用 style 属性渲染水印
  const dom = document.querySelector('.water-mark-style') as HTMLElement
  // 设置样式优先级最高
  dom.style.setProperty('background-image', `url(${bgUrl})`, 'important')
  addListioner('water-mark-style', {
    key: 'background-image',
    value: `url("${bgUrl}")` // js 获取的样式值 url 里面加了 "",所以这里加上比对
  })
})

// 添加监听器
const addListioner = (className: string, style?: StyleType) => {
  const MutationObserver = window.MutationObserver
  // 获取所有添加了水印类名的 dom
  const containerList: NodeListOf<HTMLElement> = document.querySelectorAll(
    `.${className}`
  )
  if (MutationObserver) {
    containerList.forEach((container: HTMLElement) => {
      // 每个元素监听
      const observer = new MutationObserver(() => {
        let flag = false // 触发改变的标识
        if (style) {
          // style 属性渲染水印
          // 获取 style 属性
          const styleCss: CSSStyleDeclaration = container.style
          // 需要比对样式是否存在、样式值是否相同、样式优先级是否最高
          if (
            !styleCss.getPropertyValue(style.key) ||
            styleCss.getPropertyValue(style.key) !== style.value ||
            styleCss.getPropertyPriority(style.key) !== 'important'
          ) {
            // 重新设置样式
            styleCss.setProperty(style.key, style.value, 'important')
            flag = true
          }
        } else {
          // class 类名渲染水印
          // 获取 class 集合
          const classList: DOMTokenList = container.classList
          if (!Object.values(classList).includes(className)) {
            // 如果 classList 中不存在水印的类名,就重新添加
            container.classList.add(className)
            flag = true
          }
        }
        if (flag) {
          // 暂停监听,防止上面的操作又触发监听器
          observer.disconnect()
          // 然后再重新开始观察
          addObserve(observer, container)
        }
      })
      // 每个元素开启观察
      addObserve(observer, container)
    })
  }
}
// 开启观察
const addObserve = (mutation: MutationObserver, container: Element) => {
  mutation.observe(container, {
    // 观察器的配置,需要观察属性的变动
    attributes: true
  })
}

监听效果查看:
请添加图片描述

  • 21
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值