Skip to content
文章目录

实现水印组件

水印组件要达到的目标

  • 要能完全覆盖目标节点
  • 要能防止水印被手动删除
  • 打印预览也要有水印,不能被手动去掉
  • 支持清除

实现思路

  1. 创建一个 canvas 节点
  2. 在 canvas 节点中画一个水印
  3. 将 canvavs 中画的水印转换为 base64 图片
  4. 创建一个 div 节点,并添加到需要添加的水印的节点之下
  5. 将这个创建的 div 节点设置为绝对定位或 fixed 定位(根据你的实际情况来)
  6. 将 top,left,right,bottom 都设置为 0,使这个 div 完全覆盖他的父节点
  7. 将新建的这个 div 的背景设置为 base64 图片,并设置 repeat,使得背景图片能够以循环的方式覆盖整个 div
  8. 设置 css 样式color-adjust:exact,让打印预览的时候,水印也不会消失

实现代码

ts
function clear(watermarkDomId: string, observer: MutationObserver) {
  observer && observer.disconnect()
  const watermarkDom = document.getElementById(watermarkDomId)
  if (watermarkDom && watermarkDom.parentElement) {
    // @ts-ignore
    watermarkDom.disconnectObs && watermarkDom.disconnectObs()
    watermarkDom.parentElement.removeChild(watermarkDom)
  }
}
/**
 * 创建水印
 *
 * @param watermarkDomId 水印id
 * @param options 水印配置
 * @param observer 观察器实例,用于监听水印节点是否被删除
 */
function create(watermarkDomId: string, options: WatermarkOptions, observer: MutationObserver) {
  const { height, width, text, el } = options
  const elDom = typeof el === 'string' ? (document.querySelector(el) as HTMLElement) : el
  if (!elDom) return
  const canvasDom: HTMLCanvasElement = Object.assign(document.createElement('canvas'), { width, height })
  const canvasRender2D = canvasDom.getContext('2d')
  if (!canvasRender2D) return
  canvasRender2D.rotate((Math.PI / 120) * -20)
  canvasRender2D.fillStyle = 'rgba(0, 0, 0, 0.3)'
  canvasRender2D.font = '16px Microsoft Yahei'
  canvasRender2D.textAlign = 'center'
  canvasRender2D.textBaseline = 'middle'
  canvasRender2D.fillText(text, width / 10, height / 1.5)
  // 将canvas转换为base64编码图片
  const base64 = canvasDom.toDataURL('image/png')
  // 创建水印节点
  const watermarkDom = document.createElement('div')
  // 将水印节点作为目标节点的子节点
  elDom.appendChild(watermarkDom)
  watermarkDom.id = watermarkDomId
  observer.observe(elDom, {
    childList: true,
  })
  Object.assign(watermarkDom.style, {
    background: `url(${base64}) left top repeat`,
    'color-adjust': 'exact',
    'print-color-adjust': 'exact',
    '-webkit-print-color-adjust': 'exact',
    position: 'absolute',
    pointerEvents: 'none',
    top: 0,
    right: 0,
    left: 0,
    bottom: 0,
    display: 'block',
    visibility: 'visible',
    // 防止水印被其他元素覆盖
    'z-index': 999,
  })
  const obs = new MutationObserver(() => {
    Object.assign(watermarkDom.style, {
      background: `url(${base64}) left top repeat`,
      'color-adjust': 'exact',
      'print-color-adjust': 'exact',
      '-webkit-print-color-adjust': 'exact',
      position: 'absolute',
      pointerEvents: 'none',
      top: 0,
      right: 0,
      left: 0,
      bottom: 0,
      display: 'block',
      visibility: 'visible',
      // 防止水印被其他元素覆盖
      'z-index': 999,
    })
  })
  // @ts-ignore
  watermarkDom.disconnectObs = () => {
    obs.disconnect()
  }
  obs.observe(watermarkDom, { attributes: true, attributeFilter: ['style'] })
}

export interface WatermarkOptions {
  /**
   * 水印文本
   */
  text: string
  /**
   * canvas的宽。宽度应该大于等于水印文本的宽度
   */
  width: number
  /**
   * canvas的高。高度应该大于等于水印文本的高度
   */
  height: number
  /**
   * 要添加谁赢的html元素或html选择器
   */
  el: HTMLElement | string
}
export function watermarkBuilder(options: WatermarkOptions) {
  // 水印节点的id
  const watermarkDomId = `water-markdom-id-${new Date().getTime()}`
  const observer = new MutationObserver(mutations => {
    mutations.forEach(mutation => {
      for (let i = 0; i < mutation.removedNodes.length; i++) {
        const ele = mutation.removedNodes.item(i) as HTMLElement
        if (ele && ele.id === watermarkDomId) {
          observer.disconnect()
          //@ts-ignore
          if (ele.disconnectObs) {
            // 如果水印节点被删除,则同时去除监听
            //@ts-ignore
            ele.disconnectObs()
          }
          create(watermarkDomId, options, observer)
        }
      }
    })
  })
  return {
    build: () => create(watermarkDomId, options, observer),
    clear: () => clear(watermarkDomId, observer),
  }
}

调用示例

ts
import { watermarkBuilder } from '@/utils/watermarker'

let builder = watermarkBuilder({
  text: '你好张三',
  width: 100,
  height: 100,
  el: document.body,
})
function clearWatermark() {
  builder.clear()
}

const pageContentRef = ref()
function addWatermark(type: number) {
  builder.clear()
  if (type === 1) {
    builder = watermarkBuilder({
      text: '你好张三',
      width: 100,
      height: 100,
      el: document.body,
    })
    builder.build()
  } else {
    builder = watermarkBuilder({
      text: '你好张三',
      width: 100,
      height: 100,
      el: pageContentRef.value!.$el,
    })
    builder.build()
  }
}