背景
在业务开发中,遇到用户给自己的页面设置水印的需求。需要做到
- 水印防篡改
- 支持设置各种水印参数
- 内容:支持换行,支持repeat显示
- 大小
- 字体
- 层级
- 旋转度
- 多水印之间的水平和垂直间距
方案对比
| 方案 | 技术实现关键细节 | 安全性 | 复杂度 | 兼容性 | 性能 | 其它 | |
|---|---|---|---|---|---|---|---|
| div | div repeat;shadow dom | Low可通过禁用元素直接去除水印层 | Low | High | Low过多的DOM元素,添加时将会造成一定的卡顿 | ||
| canvas | canvas; canvas to backgroundImage | Low可通过禁用元素直接去除水印层 | Middle | Middle | Low | ||
| svg | svg pattern text | Low可通过禁用元素直接去除水印层 | Middle | High | High | 高清 |
这些方案的安全性都不高
基于性能的考虑,最终选择了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 | 水印水平间距 | 30 | 否 | 50 |
| vertical | 水印垂直间距 | 30 | 否 | 100 |
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实现细节
- 结合旋转度计算字体的宽高,进而计算得出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();
安全性
/**
* 监听元素的修改、删除
* @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 });
}