在 web 的世界里,对于图片文档等增加水印处理是十分有必要的。水印的添加根据环境可以分为两大类,前端浏览器环境添加和后端服务环境添加。 通过 canvas 创建一张含有水印信息的背景图片,通过 hooks 函数插入到页面中。

对外暴露方法

  • 设置水印 setWatermark
  • 清除水印 clear

核心功能

  • 创建水印 createWatermark
  • 更新水印 updateWatermark
  • 根据文字创建 canvas 背景图 createBase64
export function useWatermark(
  appendEl: Ref<HTMLElement | null> = ref(document.body) as Ref<HTMLElement>,
) {
  // 清除水印
  const clear = () => {};
  // 创建 canvas 背景图
  function createBase64(str: string) {}

  // 更新水印
  function updateWatermark() {}
  // 创建水印
  const createWatermark = (str: string) => {};
  // 设置水印
  function setWatermark(str: string) {}

  return { setWatermark, clear };
}
创建 canvas 背景图
function createBase64 (str: string) {
  const can = document.createElement('canvas')
  const width = 300
  const height = 240
  Object.assign(can, { width, height })

  const cans = can.getContext('2d')
  if (cans) {
    cans.rotate((-20 * Math.PI) / 120);
    cans.font = '15px Vedana';
    cans.fillStyle = 'rgba(0, 0, 0, 0.15)';
    cans.textAlign = 'left';
    cans.textBaseline = 'middle';
    cans.fillText(str, width / 20, height);
  }
  return can.toDataURL('image/png')
}
创建水印

动态创建一个 div 标签,设置绝对定位,整个浏览器窗口铺满,若已存在水印层,则直接调用 updateWatermark 方法更新水印。

const id = domSymbol.toString()
const watermarkEl = shallowRef<HTMLElement>()
  
const createWatermark = (str: string) => {
  if (unref(watermarkEl)) {
    updateWatermark({ str })
    return id
  }

  const div = document.createElement('div')
  watermarkEl.value = div
  div.id = id
  div.style.pointerEvents = 'none'
  div.style.top = '0px';
  div.style.left = '0px';
  div.style.position = 'absolute';
  div.style.zIndex = '100000';

  const el = unref(appendEl)
  if (!el) return id

  const { clientHeight: height, clientWidth: width } = el
  updateWatermark({ str, width, height })
  el.appendChild(div)
  return id
}
更新水印

使用 createBase64 方法创建 Base64 格式的图片来铺满整个窗口。

function updateWatermark(
  options: {
    width?: number;
    height?: number;
    str?: string;
  } = {},
) {
  const el = unref(watermarkEl);
  if (!el) return;
  if (isDef(options.width)) {
    el.style.width = `${options.width}px`;
  }
  if (isDef(options.height)) {
    el.style.height = `${options.height}px`;
  }
  if (isDef(options.str)) {
    el.style.background = `url(${createBase64(options.str)}) left top repeat`;
  }
}
设置水印

在设置水印时,不仅需要创建水印,还需要设置 resize 监听来更新水印的位置,以及 Vue 生命周期中,卸载页面时清除水印等操作。

function setWatermark(str: string) {
  createWatermark(str);
  addResizeListener(document.documentElement, func);
  const instance = getCurrentInstance();
  if (instance) {
    onBeforeUnmount(() => {
      clear();
    });
  }
}

const func = useRafThrottle(function () {
  const el = unref(appendEl);
  if (!el) return;
  const { clientHeight: height, clientWidth: width } = el;
  updateWatermark({ height, width });
});
清除水印

清除水印时,需要移出窗口的监听函数。

const clear = () => {
  const domId = unref(watermarkEl);
  watermarkEl.value = undefined;
  const el = unref(appendEl);
  if (!el) return;
  domId && el.removeChild(domId);
  removeResizeListener(el, func);
};

组件使用

import { defineComponent, ref } from 'vue'
import { useWatermark } from '/@/hooks/web/useWatermark'

export default defineComponent({
  setup () {
    const { setWatermark, clear } = useWatermark();
    return {
      setWatermark,
      clear,
    }
  }
})

利用 ResizeObserver Polyfill 库实现DOM调整大小监听和移除监听

window.resize 事件能帮我们监听窗口大小的变化。但是 resize 事件会在一秒内触发将近60次,所以很容易在改变窗口大小时导致性能问题。换句话说,window.resize 事件通常是浪费的,因为它会监听每个元素的大小变化(只有window对象才有resize事件),而不是具体到某个元素的变化。

ResizeObserver API 使用了观察者模式,即常说的发布-订阅模式, 来监听一个DOM节点的变化,这种变化包括但不仅限于:某个节点的出现和隐藏、某个节点的大小变化

  1. 安装依赖
npm install resize-observer-polyfill --save-dev
  1. 封装方法
// src/utils/event/index.ts

import ResizeObserver from 'resize-observer-polyfill';

const isServer = typeof window === 'undefined';

/* istanbul ignore next */
function resizeHandler(entries: any[]) {
  for (const entry of entries) {
    // 通过 entry.target 访问监听的 DOM 对象 上的 __resizeListeners__ 属性, 遍历存储的监听回调
    const listeners = entry.target.__resizeListeners__ || [];
    if (listeners.length) {
      listeners.forEach((fn: () => any) => {
        fn();
      });
    }
  }
}

/* istanbul ignore next */
// 接受DOM元素和方法
export function addResizeListener(element: any, fn: () => any) {
  if (isServer) return;
  if (!element.__resizeListeners__) {
    // 在 DOM 元素对象上,设置 __resizeListeners__ 属性,存储监听器(回调函数)。
    element.__resizeListeners__ = [];
    // 监听DOM元素的变化
    element.__ro__ = new ResizeObserver(resizeHandler);
    // 监听指定的 Element 或 SVGElement
    element.__ro__.observe(element);
  }
  element.__resizeListeners__.push(fn);
}

/* istanbul ignore next */
export function removeResizeListener(element: any, fn: () => any) {
  if (!element || !element.__resizeListeners__) return;
  element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1);
  if (!element.__resizeListeners__.length) {
    // 取消 element.__ro__ 观察者身上所有的元素的观察
    element.__ro__.disconnect();
  }
}