我终于发布了自己的第一个npm包——前端添加水印(dc-water-mark)

说来惭愧两年的前端了,才发布了自己的第一个npm包。

前几天项目上有一个添加公司水印的需求。

寻思着封装起来吧

使用流程

自己封装了一个npm包,使用方式如下:

js 复制代码
npm i dc-water-mark

引入依赖后,实例watermark对象即可:

ts 复制代码
export interface Params {
    container: HTMLElement, // 加水印的节点
    width: number, // 单个水印的宽度
    height: number, // 单个水印的高度
    fontSize: number, // 字体大小
    font: string, // 字体
    color: string, // 字体颜色
    content: string, // 水印内容
    rotate: number, // 旋转角度
    zIndex?: number, // 水印层次
    opacity: number, // 水印透明度
    x: number, // 水印位置
    y: number, // 水印位置
    onWatermarkChanged: Function // 水印修改回掉函数
}

const watermark = new Watermark({
    container: 'dom节点',
    content: `watermark`,
    x: 100,
    y: 100,
    color: 'red',
    opacity: '0.5',
    zIndex: 1000,
    rotate: -30,
    fontSize: 20,
    width: 250,
    height: 150,
});
watermark.output();

// 不用的时候销毁
watermark.destroy();

效果图:

github仓库地址,欢迎大家star github.com/duchao-duch...

实现原理

1、使用canvas绘制水印,将canvas设置为一个节点的背景

2、将创建的节点insertBefore到要添加水印的节点。

3、使用MutationObserver监听节点变化防止被篡改。

核心源码如下(复制过去就能用):

ts 复制代码
import { isNullOrUndefined, isFunction, isDom } from './base';
import { Params } from './type';

export default class Watermark {
  params: Params;
  styleStr: string;
  containerObserver: MutationObserver;
  observer: MutationObserver;
  watermarkDiv: HTMLElement;
  flag: boolean;

  constructor(params: Params) {
    this.params = Object.assign(
      {
        container: document.body,
        width: 250,
        height: 150,
        fontSize: 16,
        font: 'microsoft yahei',
        color: '#cccccc',
        content: 'watermark',
        rotate: -30,
        zIndex: 1000,
        opacity: 0.5,
      },
      params,
    );

    // 水印dom样式
    this.styleStr = `
    position:absolute;
    top:0;
    left:0;
    width:100%;
    height:100%;
    z-index:${this.params.zIndex};
    pointer-events:none;
    background-repeat:repeat;
    background-image:url('${this.toDataURL()}')`;

    this.params.x = isNullOrUndefined(params.x) ? this.params.width / 2 : params.x;
    this.params.y = isNullOrUndefined(params.y) ? this.params.height / 2 : params.y;
    this.containerObserver = new MutationObserver((mutationsList, observer) => {
      // 当观察到变动时执行的回调函数
      mutationsList.forEach((mutation) => {
        const watermarkDom = document.getElementsByClassName('open-watermark')[0];
        if (!watermarkDom) {
          // 水印dom被删除,重新创建
          this.createWatermarkDom();
          if (isFunction(this.params.onWatermarkChanged)) {
            // 水印dom被修改时,执行传入的回调
            this.params.onWatermarkChanged(mutation, observer);
          }
        }
      });
    });
    this.observer = new MutationObserver((mutationsList, observer) => {
      // 当观察到变动时执行的回调函数
      mutationsList.forEach((mutation) => {
        const watermarkDom = document.getElementsByClassName('open-watermark')[0];
        if (watermarkDom?.getAttribute('style') !== this.styleStr) {
          // 水印dom样式被修改
          watermarkDom.setAttribute('style', this.styleStr);
          if (isFunction(this.params.onWatermarkChanged)) {
            this.params.onWatermarkChanged(mutation, observer);
          }
        }
      });
    });
  }

  toDataURL() {
    const { width, height, fontSize, font, color, rotate, content, opacity, x, y } = this.params;
    const canvas = document.createElement('canvas');
    canvas.setAttribute('width', `${width}px`);
    canvas.setAttribute('height', `${height}px`);

    const ctx = canvas.getContext('2d');
    if (ctx) {
      ctx.clearRect(0, 0, width, height);
      ctx.textBaseline = 'top';
      ctx.textAlign = 'left';
      ctx.fillStyle = color;
      ctx.globalAlpha = opacity;
      ctx.font = `${fontSize}px ${font}`;
      ctx.translate(x, y);
      ctx.rotate((Math.PI / 180) * rotate);
      ctx.translate(-x, -y - fontSize);
      ctx.fillText(content, x, y + fontSize);
    }

    return canvas.toDataURL();
  }

  // 创建水印dom
  createWatermarkDom() {
    const watermarkDom = document.getElementsByClassName('open-watermark')[0];
    const { container } = this.params;
    if (isDom(container) && !watermarkDom) {
      this.watermarkDiv = document.createElement('div');
      this.watermarkDiv.setAttribute('style', this.styleStr);
      this.watermarkDiv.setAttribute('class', 'open-watermark');
      container.style.position = 'relative';
      container.insertBefore(this.watermarkDiv, container.firstChild);
      this.observer.observe(this.watermarkDiv, {
        attributes: true, // 观察节点属性改变
        childList: true, // 观察子节点改变
        subtree: true, // 观察所有后代节点的childLIst、attributes变化
      });
    }
  }

  output() {
    this.flag = true;
    this.createWatermarkDom();
    // 观察元素
    this.containerObserver.observe(this.params.container, {
      attributes: true,
      childList: true,
      characterData: true,
    });
  }

  destroy() {
    if (!this.watermarkDiv) return;
    this.watermarkDiv.remove();
    this.observer.disconnect();
    this.containerObserver.disconnect();
  }
}
相关推荐
憧憬成为web高手4 小时前
ACTF 12307复现
前端·bootstrap·html
wordbaby5 小时前
Axios 上传大文件崩溃:鸿蒙 RNOH 下 XHR 返回空响应头引发的"假失败"
前端·react native
wordbaby5 小时前
React Native 列表分页实战:下拉刷新与上拉加载的工程化方案
前端·react native
wordbaby5 小时前
脱离 Tab 栏的艺术:React Native 全屏子页面的导航架构实践
前端·react native·harmonyos
陈随易6 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
wordbaby6 小时前
React Native 新架构落地鸿蒙:跨三端政务级应用的工程实践与深度复盘
前端·react native·harmonyos
excel7 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端
ZC跨境爬虫7 小时前
模块化烹饪小程序开发日记 Day7:(菜谱详情接口开发与JSON数据读取全流程)
前端·javascript·css·ui·微信小程序·json
এ慕ོ冬℘゜8 小时前
JS 前端基础面试题
开发语言·前端·javascript
LaughingZhu8 小时前
Product Hunt 每日热榜 | 2026-05-25
前端·人工智能·经验分享·chatgpt·html