简单实现前端页面水印功能

355 阅读4分钟
  1. 创建水印容器,canvas绘制水印内容图,将图片设置为水印容器的背景图平铺。水印容器添加到需要加水印的DOM之内,position定位形式铺满目标DOM
// 需要添加水印的DOM
const boxRef = ref()
// 水印
const watermarkEl = ref(null)
// 水印背景图配置项
const defaultConfig = {
  /** 文本颜色 */
  color: "#c0c4cc",
  /** 文本透明度 */
  opacity: 0.5,
  /** 文本字体大小 */
  size: 16,
  /** 文本字体 */
  family: "serif",
  /** 文本倾斜角度 */
  angle: -20,
  /** 一处水印所占宽度(数值越大水印密度越低) */
  width: 300,
  /** 一处水印所占高度(数值越大水印密度越低) */
  height: 200,
  /** 水印文本*/
  backupText:"水印文本"
}
// canvas绘制返回水印背景图
const createBase64 = () => {
  // 解构配置
  const {color,opacity,size,family,angle,width,height,backupText} = defaultConfig
  // 创建一个画布
  const canvasEl = document.createElement("canvas")
  canvasEl.width = width
  canvasEl.height = height
  //创建 context 对象,getContext("2d") 对象是内建的 HTML5 对象,
  //拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法
  const ctx = canvasEl.getContext("2d")
  if (ctx) {
    ctx.fill = color
    ctx.globalAlpha = opacity
    ctx.font = `${size}px ${family}`
    ctx.rotate((Math.PI/180)*angle)
    ctx.fillText(backupText,0,height/2)
  }
  return canvasEl.toDataURL()
}
// 创建添加水印
const createWatermark = () => {
  // parentEl需要添加水印的dom
  let parentEl = boxRef.value
  if (!watermarkEl.value) {
      // markEl水印容器
      const markEl = document.createElement("div")
      // markEl样式设置
      markEl.setAttribute('style', `background: url(${createBase64()}) left top repeat; position: absolute; top: 0; left: 0; bottom: 0; right: 0; pointer-events: none; z-index: 0;`)
      // ref留存markEl用于后续更新,避免多次绘制
      watermarkEl.value = markEl
  }
  // 将水印添加到目标dom内
  parentEl.appendChild(watermarkEl.value)
  // 监听水印元素和容器元素,下面步骤需要
  addMutationListener()
}
onMounted(() => {
    createWatermark()
})

实现效果:

  1. 步骤1其实已经完成了水印的添加,如果没过多追求就已经实现了简单的水印添加功能。但因为水印也是一个真实存在的DOM,可以通过打开控制台修改水印DOM节点样式属性或删除水印DOM来实现去水印的目的。

解决思路:监听用户修改水印和父级DOM的属性修改、DOM删除等用户行为,重新添加设置水印。JS提供了MutationObserver的相关API,MutationObserver提供了监测针对某一范围内DOM发生变化的即时反应处理能力。具体实现如下

// reactive装载监听器
const observer = reactive({
  watermarkElMutationObserver: undefined,
  parentElMutationObserver: undefined
})
//移除 mutation 监听
const removeListener = (kind = "all")=>{
    if(kind === "mutation"||kind ==="all"){
      observer.parentElMutationObserver?.disconnect();
      observer.parentElMutationObserver = undefined;
      observer.watermarkElMutationObserver?.disconnect();
      observer.watermarkElMutationObserver = undefined;
    }
}
// 清除水印
const clearWatermark = () => {
  if (!boxRef.value || !watermarkEl.value) return
      removeListener()
  try{
    //移除水印元素
    boxRef.value.removeChild(watermarkEl.value)
  }catch{
    //比如在无防御情况下,用户打开控制台删除了这个元素
    console.warn("水印元素已不存在,请重新创建")
  }finally{
    watermarkEl.value = null
  }
}
// 更新水印
const updateWatermark = debounce(()=>{
  clearWatermark()
  createWatermark()
},100)
// MutationObserver监听回调
const mutationCallback = (mutationList)=> {
  //水印的防御 (防止用户手动删除水印或通过css隐藏水印)
  console.log('--回调,发生改变--',mutationList)
  mutationList.forEach(mutation => {
    switch (mutation.type) {
        // dom样式属性发送变化,重置更新水印
        case "attributes":
          mutation.target === watermarkEl.value && updateWatermark()
          break;
        // 子节点发送变化,且存在removeNodes(删除节点行为),重新添加上水印
        case "childList":
            mutation.removedNodes.forEach((item)=>{
                item === watermarkEl.value && boxRef.value.appendChild(watermarkEl.value);
            })
            break
    }
  })
}
// 添加监听
const addMutationListener = () => {
  let parentEl = boxRef.value
  if(!observer.parentElMutationObserver) observer.parentElMutationObserver = new MutationObserver(mutationCallback)
  if(!observer.watermarkElMutationObserver) observer.watermarkElMutationObserver = new MutationObserver(mutationCallback)
  observer.parentElMutationObserver.observe(parentEl, {
    // 观察目标节点属性是否变动,默认为 true
    attributes: true,
    // 观察目标子节点是否有添加或者删除,默认为 false
    childList: true,
    // 是否拓展到观察所有后代节点,默认为 false
    subtree: false
  })
  observer.watermarkElMutationObserver.observe(watermarkEl.value, {
    // 观察目标节点属性是否变动,默认为 true
    attributes: true,
    // 观察目标子节点是否有添加或者删除,默认为 false
    childList: false,
    // 是否拓展到观察所有后代节点,默认为 false
    subtree: false
  })
}

上面步骤简要概述就是在添加MutationObserver后,通过回调函数判断用户行为。删除或修改行为直接将watermarkEl重新添加、更新到水印的父节点,达到保持水印节点存在及样式不被修改。

注意项:在初步实现过程中,对水印的节点DOM想通过class设置样式属性,而未通过js的setAttribute属性设置语句来进行水印样式设置。这会产生一个问题,在控制台中可通过对应class类名样式修改而不会触发MutationObserver的监听回调事件。所以不能给水印节点添加class类名形式实现样式。

不足之处:以上实现水印功能后并非无懈可击,用户可通过页面加载完成后禁用js达到屏蔽水印层的操作。

如何禁用JavaScript?

  1. f12打开浏览器控制台
  2. 按 Ctrl+Shift+P (Windows、Linux) 或 Command+Shift+P (macOS) 打开 命令菜单

  1. 开始键入 javascript,选择“ 禁用 JavaScript [调试器]”,然后按 Enter

  1. 选项卡中的Sources出现黄色警告标识标识已经禁用成功

解决此缺陷只能禁用浏览器调试???