简单实现一个水印插件

一.思路

1.使用类实现,实例化后可以生成一个水印元素并挂载到某个元素上

2.该元素的样式需要计算,主要是背景图(datauri或者url)

3.拓展水印内容(使用canvas绘制的图片)

4.水印元素应该具备一些防修改能力,如修改style、class、id以及删除

二、实现

1.初始化部分

typescript 复制代码
export class Watermark {
  options = {} as WatermarkOptions;
  container: HTMLElement | null;
  watermark: HTMLElement | null;

  constructor(options?: WatermarkOptions) {
    this.options = JSON.parse(JSON.stringify(DEFAULT_OPTIONS));
    this.container = null;
    this.watermark = null;
    this.init(options)
  }

  init (options: WatermarkOptions = {}) {
    this.options = this._mergeOptions(options);

    if (this.watermark && this.container) {
      this.container.removeChild(this.watermark);
    }
    // 挂载容器支持dom、string或者不传
    this.container = options.container
      ? typeof options.container === 'string'
        ? document.querySelector(options.container) || document.body
        : options.container
      : document.body;

    this.watermark = document.createElement('div');
  }

  // 挂载
  mount () {
    if (!this.container || !this.watermark) {
      console.warn('容器元素或挂载元素不存在,挂在失败');
      return;
    }
    this.container.appendChild(this.watermark)
  }

  // 直接使用静态方法生成实例,多一份实例化api
  static mount(options?: WatermarkOptions) {
    const instance = new Watermark(options);
    instance.mount();
    return instance;
  }
  
  _mergeOptions (userOptions: WatermarkOptions = {}) {
    const isObject = (obj: any) => obj && typeof obj === 'object' && !Array.isArray(obj);

    const deepMerge = (target: any, source: any): any => {
      if (isObject(target) && isObject(source)) {
        Object.keys(source).forEach(key => {
          if (isObject(source[key])) {
            if (!target[key]) {
              target[key] = source[key];
            }
            deepMerge(target[key], source[key]);
          } else {
            target[key] = source[key];
          }
        });
      }
      return target;
    };

    return deepMerge(this.options, userOptions);
  }
}

2.样式计算

typescript 复制代码
get _mergedStyle () {
    const { style, image } = this.options;
    if (!style && !image) {
      return defaultStyle;
    }
    const imageStyle = image
      ? typeof image === 'string'
        ? { 'background-image': `url(${image})` }
        : this._renderImageStyle(image)
      : {};
    if (!style) {
      return Object.assign({}, defaultStyle, imageStyle);
    }
    if (typeof style === 'string') {
      const userStyles = style?.split(';').reduce((acc, styleItem) => {
        const [key, value] = styleItem.split(':').map(s => s.trim());
        if (key && value) {
          acc[key] = value;
        }
        return acc;
      }, {} as Record<string, string>);

      return Object.assign({}, defaultStyle, userStyles, imageStyle);
    } else {
      return Object.assign({}, defaultStyle, style, imageStyle);
    }
}

get _mergedStyleText () {
    const style = this._mergedStyle;
    return Object.keys(style).map(key => `${key}: ${style[key]}`).join(';');
}

3.支持canvas绘图

typescript 复制代码
_renderImageStyle (image: WatermarkCanvas) {
    const { watermarkText, watermarkFont, watermarkColor, watermarkOpacity, watermarkRotate, watermarkCanvasHeight, watermarkCanvasWidth } =image
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    if (ctx) {
      const width = watermarkCanvasWidth || 200;
      const height = watermarkCanvasHeight || 200;
      canvas.width = width;
      canvas.height = height;

      ctx.fillStyle = watermarkColor || 'rgba(0, 0, 0, 0.3)';
      ctx.font = watermarkFont || '16px sans-serif';
      ctx.globalAlpha = watermarkOpacity || 0.5;
      ctx.rotate((watermarkRotate || -30) * Math.PI / 180);
      ctx.fillText(watermarkText, 0, height / 2);

      const dataURI = canvas.toDataURL();
      canvas.remove();
      return { 'background-image': `url(${dataURI})` };
    } else {
      canvas.remove();
      return {};
    }
}

4.防修改

需要在初始化时进行监控,并在销毁时释放,此处省了

typescript 复制代码
_styleObserve () {
    if (!this.watermark) return null;
    const self = this;
    const observer = new MutationObserver(function(mutations) {
      observer.disconnect();
      mutations.forEach(function(mutation) {
        if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
          self.watermark!.style.cssText = self._mergedStyleText;
        }
        if (mutation.attributeName === 'class') {
          self.watermark!.className = '';
        }
        if (mutation.attributeName === 'id') {
          self.watermark!.id = '';
        }
      });
      observer.observe(self.watermark!, config);
    });
    // 配置观察选项
    const config = {
      attributes: true,
      attributeFilter: ['style', 'class', 'id'],
    };
    // 开始观察目标节点
    observer.observe(this.watermark, config);
    return observer;
}

_removeObserve() {
    if (!this.container) return null;
    const self = this;
    const observer = new MutationObserver(function(mutations) {
      observer.disconnect();
      mutations.forEach(function(mutation) {
        if (mutation.type === 'childList' && !self.container!.contains(self.watermark)) {
          self.mount();
        }
      });
      observer.observe(self.container!, config);
    });
    // 配置观察选项
    const config = {
      childList: true, // 监听子节点变化
      subtree: false
    };
    // 开始观察目标节点
    observer.observe(this.container, config);
    return observer;
}

三、使用

安装

bash 复制代码
npm i @irises/watermark

使用

typescript 复制代码
import { Watermark } from '@irises/watermark';

const instance1 = Watermark.mount({ container: '#app' });
// 或者
const instance2 = new Watermark({ container: '#app' });
instance2.mount();

// 销毁
instance1.unmount()
instance2.unmount()
相关推荐
noodb软件工作室2 分钟前
支持中文搜索的markdown轻量级笔记flatnotes来了
前端·后端
Catfood_Eason21 分钟前
HTML5 盒子模型
前端·html
小李小李不讲道理27 分钟前
「Ant Design 组件库探索」二:Tag组件
前端·react.js·ant design
1024小神31 分钟前
在rust中执行命令行输出中文乱码解决办法
前端·javascript
wordbaby32 分钟前
React Router v7 中的 `Layout` 组件工作原理
前端·react.js
旺仔牛仔QQ糖33 分钟前
Vue为普通函数添加防抖功能(基于Pinia 插件为action 配置防抖功能 引发思考)
前端·vue.js
lyc23333338 分钟前
鸿蒙Next人脸比对技术:轻量化模型的智能应用
前端
*小雪43 分钟前
vue2使用vue-cli脚手架搭建打包加密方法-JavaScript obfuscator
前端·javascript·vue.js
Coca1 小时前
Vue 3 缩放盒子组件 ScaleBox:实现内容动态缩放与坐标拾取偏移矫正
前端
枫叶kx1 小时前
发布一个angular的npm包(包含多个模块)
前端·npm·angular.js