前端SVG水印实现背景水印

501 阅读2分钟

背景

在业务开发中,遇到用户给自己的页面设置水印的需求。需要做到

  • 水印防篡改
  • 支持设置各种水印参数
    • 内容:支持换行,支持repeat显示
    • 大小
    • 字体
    • 层级
    • 旋转度
    • 多水印之间的水平和垂直间距

方案对比

方案技术实现关键细节安全性复杂度兼容性性能其它
divdiv repeat;shadow domLow可通过禁用元素直接去除水印层LowHighLow过多的DOM元素,添加时将会造成一定的卡顿
canvascanvas; canvas to backgroundImageLow可通过禁用元素直接去除水印层MiddleMiddleLow
svgsvg pattern textLow可通过禁用元素直接去除水印层MiddleHighHigh高清

这些方案的安全性都不高

基于性能的考虑,最终选择了SVG方案,同时通过MutationObserver - Web API 接口参考 | MDN 来解决篡改的问题

开发实现

参数设计

字段解释默认值必须举例
selector容器默认body'#id'
value水印文字无;多水印用\n分隔水印测试
fillstyle水印填充样式#000000'rgba(192, 192, 192, 0.6)'
font水印字体"bold 20px Serif"
zIndex水印层级99999
rotate水印的旋转度-45-45
horizontal水印水平间距3050
vertical水印垂直间距30100

svg实现核心

通过svg pattern text来实现

<svg width="100%" height="100%" style="font-size: 14px;font-weight: normal;font-family: 微软雅黑;opacity: 0.2;fill: rgb(27 5 5);">
    <defs>
        <pattern id="watermark-pattern" width="202.517447389386" height="165.68587868595915" patternUnits="userSpaceOnUse">
            <text x="60.846221405874644" y="76.24293896150985" transform="rotate(20, 101.258723694693, 82.84293934297958), translate(0, 0)" text-anchor="start" alignment-baseline="hanging">我是水印</text>
       </pattern>
    </defs>
    <rect x="0" y="0" width="100%" height="100%" fill="url(#watermark-pattern)" style="pointer-events: none;"></rect>
</svg>

我们只需要根据水印参数,来绘制svg的pattern即可,repeat水印的工作交到 svg rect用fill来实现

pattern实现细节

image.png

  • 结合旋转度计算字体的宽高,进而计算得出pattern的宽高
  • 如果水印字体有多行,即pattern内含有多个text, 则需要结合参数计算多个text间距

text宽高计算

通过添加到body得到实际的宽高

const span = document.createElement('span');
const style = `font: ${font}; word-break: keep-all; white-space: nowrap;`;
span.setAttribute('style', style);
span.appendChild(document.createTextNode(text.replace(/\s/g, String.fromCharCode(960))));
spanRect = span.getBoundingClientRect();

安全性

MutationObserver

 /**
    * 监听元素的修改、删除
    * @param {*} element 监听的元素
    * @param {*} callback 发生修改、删除时的回调
    */
    private bindObserve(element: HTMLElement, callback: Function) {
        this.observer && this.observer.disconnect();
        // 避免用户手动修改
        this.observer = new MutationObserver((mutationRecords) => {
            if (mutationRecords && mutationRecords.length) {
                const result = mutationRecords.some(record => {
                    return element.contains(record.target)
                });

                if (result) {
                    callback(mutationRecords);
                }
            }
        });
        this.observer.observe(element, { childList: true, subtree: true, characterData: true, attributes: true });
    }

效果

image.png

代码链接

github.com/Li-Sparrow/…