接手一个老系统改造,需求单上只有一句话:页面加全屏水印,防止内部资料被拍照外传。看着像个五分钟的活儿,真正卡住的是后面那句附加条件——必须兼容 IE7,且水印不能盖住页面上的按钮、输入框和下拉框。水印兼容 IE7 这件事,难的从来不是"画"出来,而是"画上去之后别碍事"。现代浏览器一行pointer-events: none就解决的事,到了 IE7 上完全失效,因为 IE10 及以下根本不认这个属性。于是水印层要么被降级成一个"吃事件"的透明盖子,要么就被迫换一种存在形式。这篇内容面向两类人:一类是还在维护内网老系统、被指定兼容低版本 IE 的前端;另一类是虽然不写 IE,但想搞清楚"水印层为什么不挡交互"这个底层逻辑的人。我会把三条可行路线、每一处的参数选择、实际踩到的坑和排查手法完整摊开,代码可以直接拿去改。
1. 先把需求拆开看:三个问题其实是一件事
"水印覆盖控件"这个描述,在实际排查里往往对应两种完全不同的故障,混在一起谈就容易治错病。一种是视觉上的覆盖——半透明水印压在按钮上,按钮整体发灰、文字对比度下降,用户看着像"禁用状态",但点下去其实是有反应的;另一种是交互上的拦截——水印层作为一个独立 DOM 元素横在最上层,鼠标事件被它接走,底下的输入框聚焦不了、链接点不动、下拉框展不开。前者靠调透明度、调层级、调水印密度就能缓解,后者必须动结构或动事件模型。我在需求评审时专门把这两条写在白板上分开讨论,因为它们的解法几乎没有交集。
1.1 水印为什么必须"能看不能摸"
水印的产品定位很明确:它是一层视觉留痕,不是一层操作界面。这意味着它在文档流里的角色应该无限接近于背景——用户能看见,但所有鼠标和键盘行为都应该穿透过去,落到真正的内容上。理解这一点,后面所有的方案取舍就都有了判断标准:任何让水印层参与事件命中的实现,本质上都是错的方向,只是有时候不得不用补丁去修正它。
我见过不少项目把水印做成一个div,position: absolute、z-index: 9999、background: url(wm.png) repeat,然后祈祷用户不会点到被盖住的区域。这种写法在展示型页面上勉强能用,一旦页面上有表单、有操作按钮,问题立刻暴露。更麻烦的是它属于"偶发型故障"——用户点十次有一次没反应,反馈上来就是"系统卡",排查成本极高。
判断一个水印实现是否合格,我一般看四个指标:水印是否随内容滚动、是否遮挡文字可读性、是否影响任何控件的鼠标行为、以及在页面 DOM 结构变化(比如弹窗)后是否还能正常显示。这四条里,第二条和第三条经常互相冲突,这也是为什么需要专门设计,而不是随手加个背景图。
1.2 IE7 的能力清单决定了方案上限
动手之前必须把目标浏览器的能力边界摸清楚,否则设计方案就是空中楼阁。IE7 的很多限制在今天看来匪夷所思,但在内网终端、老版本办公环境里它就是硬约束。我把和水印相关的关键能力整理成下面这张表,后面所有方案都是围绕它推导出来的。
| 特性 | IE7 支持情况 | 对水印方案的影响 |
|---|---|---|
pointer-events: none | 不支持(IE11 才支持 HTML 元素) | 无法用一行 CSS 让水印层不接收事件 |
opacity | 不支持 | 只能用filter: alpha(opacity=xx)做半透明 |
rgba() | 不支持 | 不能用 CSS 颜色做半透明背景 |
| PNG 24 位透明通道 | 支持 | 水印素材可以直接用带 alpha 的 PNG |
position: fixed | 标准模式下支持,但与滤镜配合会抖动 | 固定水印仍建议用 JS 模拟滚动定位 |
document.elementFromPoint | 支持 | 可以做精确的事件命中转发 |
addEventListener | 不支持 | 只能用attachEvent('onxxx', fn) |
CSSbehavior(HTC) | 支持 | 可作为事件穿透的实现手段,但部署成本高 |
| VML | 支持 | 可作为平铺水印的兜底渲染方案 |
select元素层级 | 窗口级控件,永远置顶 | 任何 div 都盖不住它,反过来它也不会被水印遮住 |
filter与层叠上下文 | 加了 filter 的元素会独立成层 | z-index行为会变得不可预测 |
这张表里最容易被忽略的是最后两条。select的置顶特性意味着:如果你的水印层盖在了一个下拉框上,视觉上你会看到一个"破洞"——下拉框穿透了水印显示在最上面,水印在它周围环绕,看起来像是渲染错位。这个现象经常被误判成样式 bug,实际上是 IE 的渲染机制决定的,改样式改不出来,只能改结构。
1.3 三条路线先摆出来对比
明确了约束,方案空间其实就三条:把水印变成背景、把水印层降到底部、让水印层主动让路。三条路各有适用场景,我先把对比摊开,后面的章节再逐个深入。
| 方案 | 核心思路 | 优点 | 局限 |
|---|---|---|---|
| 背景图平铺 | 水印作为容器的background-image | 天然不参与事件命中,无需任何兼容处理 | 会被有背景色的子容器遮挡,需要逐层补 |
| 层级下沉 | 水印层z-index: -1,内容容器在上层 | 结构简单,兼容所有浏览器 | 内容区必须有透明背景,否则水印被完全遮死 |
| 覆盖层加让路 | 水印层置顶,通过 JS 临时隐藏或转发事件 | 视觉上最"满",水印不会被任何内容遮挡 | 实现复杂,有闪烁风险,需要处理边界情况 |
我的实际选择是以背景图平铺为主,覆盖层为补充。原因很直白:一个不需要兼容处理就能在所有浏览器里表现一致的方案,长期维护成本最低。覆盖层方案我会保留,但只用在"页面结构不允许改动"的死角场景里,比如第三方嵌入的 iframe 或者由后端拼装的老页面。
2. 方案取舍:我为什么最后选了"背景图铺"而不是"盖一层"
很多人第一反应是"水印当然要盖在最上面",因为在设计语境里,水印就是最后一层印刷。但网页不是印刷品,网页的"层"自带事件语义,每加一层就多一层拦截风险。把水印从"图层"降级成"底色",等于从根上绕过了整个事件模型——它不再是一个可以被点到的对象。
2.1 覆盖层方案的先天缺陷
覆盖层方案的本质是:用一个绝对定位的div铺满视口,z-index拉到最高,里面放平铺的水印纹理。这个div一旦进入 DOM,就会参与命中测试,鼠标落在它身上时,事件不会继续往下传递。这是浏览器的默认行为,不是 bug。
于是你会遇到一连串连锁反应:输入框点不进去,光标不出现;按钮的 hover 状态不触发,用户以为按钮是灰的;文本选不中,复制粘贴功能形同虚设;下拉框虽然视觉上穿透了,但点击落到水印层上,展不开选项。更隐蔽的是mousedown和click的分离——某些情况下mousedown被水印吃掉,click却落到了目标元素上,导致拖拽排序、双击编辑这类依赖完整事件序列的交互彻底坏掉。
我遇到过一个典型案例:一个带行内编辑的表格页,加了覆盖层水印之后,单元格双击进不去编辑态。排查了两小时才发现是dblclick的两次mousedown都被水印截走了。这类问题用肉眼几乎看不出来,只能靠事件监听器打日志。
2.2 pointer-events: none 在各版本浏览器里的真实支持情况
现代做法很干净,给水印层加一行:
.wm-layer { position: absolute; left: 0; top: 0; right: 0; bottom: 0; z-index: 9999; background: url(./img/wm-tile.png) repeat; pointer-events: none; opacity: .12; }pointer-events: none的含义是"这个元素是鼠标命中的透明体",事件会直接穿透到它下面的元素。这是 SVG 时代就有的属性,后来被引入 CSS,Firefox 3.6+、Safari 4+、Chrome 16+ 都支持。但 IE 家族到 IE10 都没有实现它,IE11 才补上。
| 浏览器 | pointer-events: none用于 HTML 元素 |
|---|---|
| IE10 及以下 | 不支持 |
| IE11 | 支持 |
| Edge 全系 | 支持 |
| Firefox 3.6+ | 支持 |
| Chrome 16+ / Safari 4+ | 支持 |
所以只要目标环境里有 IE10 及以下的用户,这行 CSS 就是无效声明,浏览器会直接忽略它,水印层依然是那个吃事件的盖子。这时候必须准备第二套方案。
还有一点需要提醒:即使在支持pointer-events: none的浏览器上,水印层的视觉遮挡依然存在。半透明层盖在文字上,会把文字的整体对比度往下拉,长时间阅读会累。这是纯视觉问题,事件穿透解决不了它,只能靠降低水印浓度或者改成背景方案。
2.3 背景图平铺方案的代价与应对
把水印做成背景,最大的好处是它从 DOM 层面就不存在了——没有元素,就没有事件,就没有兼容问题。IE7 对背景图的平铺支持非常完善,background-repeat: repeat从 IE4 就有了,PNG 的 alpha 通道从 IE7 开始支持,所以水印素材可以直接用一张带透明度的 PNG,不需要任何滤镜兜底。
代价有两个。第一,背景会被子元素的背景色遮挡。页面里只要有一个白底的卡片、一个灰底的表头,水印在那些区域就消失了,视觉上会出现"水印断层"。第二,背景图默认不打印,如果业务要求打印件也带水印,需要在打印媒体里另做处理。
第一点的应对办法是逐容器叠加:给每一个有背景色的容器都加上同一张水印背景图,位置用background-position对齐,让视觉上连成一片。这听起来笨,但非常有效,而且因为是背景,完全不影响这些容器内部控件的行为。第二点后面单独说。
3. 落地实现一:平铺水印背景
这条路线我用了很多次,稳定性最好。关键在于水印素材怎么做、CSS 怎么写、以及怎么处理那些"不听话"的白色区块。
3.1 水印素材怎么做才能无缝平铺
水印平铺最容易翻车的地方是接缝。如果素材的边缘没有处理好,平铺之后会出现明显的网格线,看起来像页面裂了。做法是:在一个固定尺寸的画布上,把水印文字旋转后居中放置,四周留出足够的安全边距,保证文字的任何部分都不触碰到画布边缘。
尺寸怎么定?假设水印文案是"内部资料 严禁外传"八个字,字号 14px,旋转角度 -25 度。文字本身的宽度约 112px,高度约 14px。旋转之后的包围盒宽度是112 × cos25° + 14 × sin25° ≈ 101 + 6 = 107px,高度是112 × sin25° + 14 × cos25° ≈ 47 + 13 = 60px。也就是说,单个水印的视觉占位大约是 107×60。
平铺单元要在这个基础上加间距。间距太小水印糊成一片,太重;间距太大又显得稀疏,防护感不足。我的经验值是水平和垂直各留 60px 到 80px 的呼吸空间,所以最终单元取240×160比较舒服,密度适中,在 1366×768 的老屏幕上大约能排下 5×4 个水印,视觉上"满而不挤"。
导出时注意两点。第一,格式必须是 PNG-24 带 alpha,不要用 PNG-8,索引色在渐变和抗锯齿上会出现锯齿边。第二,水印本身的灰度要浅。我一般用#999999或者更浅的#AAAAAA,再配合整体的filter: alpha(opacity=12),最终呈现的浓度大约在 5% 到 8% 之间。这个浓度下,正常阅读完全不受影响,但拍照时能被清晰看到,正好卡在需求的平衡点上。
提示:不要在图层面板里直接把文字透明度拉到 10% 再导出,那样 PNG 自带 alpha,后续没法再用 CSS 统一调整浓度。正确做法是导出不透明的浅灰文字,浓度交给 CSS 控制,改起来只动一个数值。
3.2 容器结构与 IE7 的 CSS 写法
最省事的挂载点是body。浏览器有一个特性:如果html元素没有设置背景,body的背景会"传播"到整个画布,覆盖完整的滚动区域,页面再长也不会断。这个行为在 IE7 里同样成立,所以把水印背景直接加在body上,一行 CSS 就能铺满全站。
html { /* 关键:html 不能有背景,否则 body 背景不再传播 */ background: transparent; } body { margin: 0; padding: 0; background-color: #ffffff; background-image: url(./img/wm-tile.png); background-repeat: repeat; background-position: 0 0; }如果页面结构里有一个主容器(比如#app)需要单独控制背景,也可以挂在那里,但要保证这个容器的高度能撑满内容区。IE7 里height: 100%的百分比高度依赖父级有确定高度,链路一断就失效,所以更稳的做法是不设高度,让它被内容自然撑开,背景跟着容器走。
这里有个细节需要注意:IE7 下如果给body加了filter或者zoom之类的属性,会触发 hasLayout,可能导致背景传播行为异常。所以在body上尽量只放背景声明,不要叠加其他怪异属性。
3.3 背景会被白色区块盖住,怎么一层层补回去
实际页面里,body背景往往只能露出一部分。表格有表头底色、卡片有白色背景、弹窗有独立面板、侧边栏有分割色块,这些都会把水印盖掉。视觉上就出现了"水印在有的地方有、有的地方没有"的割裂感。
解决思路是给每个有背景色的容器补上同一张水印图,并且用background-position让它们对齐。这里有个技巧:如果所有容器都从0 0开始平铺,由于各自的定位起点不同,接缝处会出现错位。要真正无缝,需要让背景相对于同一个原点计算。
IE7 不支持background-origin和background-clip这些现代属性,所以只能靠固定偏移量来对齐。我的做法是在构建时给每个容器算好偏移:如果某个卡片距离页面左上角是(x, y),就把它的background-position设为-x px -y px。这样从视觉上,所有容器的水印图案都来自同一张无限大的底图。
.card { background-color: #ffffff; background-image: url(./img/wm-tile.png); background-repeat: repeat; /* 让水印图案与页面原点对齐 */ background-position: -24px -180px; }这个偏移量需要根据实际布局算,写死是可以接受的,因为后台系统的布局通常稳定。如果布局是流式的、位置会变,那就退一步:不强求对齐,改成把水印只在最外层画一次,然后要求所有子容器背景透明。后一种做法对页面样式的侵入更大,需要逐个检查background声明,但一劳永逸。
注意:给容器补水印背景时,不要用
background简写一次性覆盖,容易把原有的background-color冲掉。分开写background-color和background-image,改动范围可控。
3.4 如果连 IE6 也要扛:VML 平铺的写法
虽然这次的需求只写到 IE7,但内网项目经常在最后关头冒出"某某终端还是 IE6"的追加需求,提前知道兜底方案能省一轮返工。IE6 不支持 PNG 的 alpha 通道,透明区域会渲染成灰色块,所以必须用滤镜AlphaImageLoader或者 VML。
AlphaImageLoader有个致命限制:它的sizingMethod只有crop、image、scale三种,全都不支持平铺。想平铺只能用 VML 的v:fill,把type设成tile。
<html xmlns:v="urn:schemas-microsoft-com:vml"> <head> <style> v\:* { behavior: url(#default#VML); } .wm-vml { position: absolute; left: 0; top: 0; width: 100%; height: 100%; z-index: -1; } </style> </head> <body> <v:rect class="wm-vml" fillcolor="none" stroked="false"> <v:fill type="tile" src="./img/wm-tile.gif" opacity="0.08" /> </v:rect> </body> </html>注意素材要换成 GIF 或者不透明的 PNG,因为 IE6 处理不了 alpha。这个方案我只在确实有 IE6 需求时用,VML 的渲染性能和调试体验都很差,能不碰就不碰。
3.5 水印视觉强度调校:透明度与对比度的平衡
水印浓度是个需要现场调的事,纸上算不准。我一般准备三档:alpha(opacity=8)、alpha(opacity=12)、alpha(opacity=18),在真实页面上截图对比。
判断标准是:把页面缩放到 100%,正常阅读正文,如果视线不会被水印吸引走,说明浓度合格;如果感觉文字像蒙了一层灰,说明过了。反过来,把截图缩小到 30%,如果水印还能辨认,说明防护强度够;如果完全看不见,那就太淡了,拍张照稍微一压缩就没了。
还有一个容易被忽略的点:水印的颜色比浓度更影响阅读体验。同样是 10% 的浓度,纯黑水印会让页面发脏,浅灰水印则几乎无感。所以素材本身就要用浅灰,不要用黑色再靠透明度去稀释。
/* IE7 及以下的浓度控制 */ .wm-alpha { filter: progid:DXImageTransform.Microsoft.Alpha(opacity=12); -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(opacity=12)"; zoom: 1; /* 触发 hasLayout,保证滤镜生效 */ } /* 现代浏览器的浓度控制 */ .wm-alpha { opacity: .12; }zoom: 1这行在 IE7 里几乎是必备的,因为滤镜需要元素有 hasLayout 才会生效。但要注意,加了zoom之后元素可能产生意料之外的布局变化,需要实测确认。
4. 落地实现二:覆盖层 + 事件让路兜底
有些页面改不动,比如由后端模板拼装、或者嵌在别人系统里的 iframe,你没法给每个容器补背景。这时候只能回到覆盖层方案,用 JS 补上 IE7 缺失的那部分能力。
4.1 什么时候必须用覆盖层
三种情况我会选择覆盖层:一是页面结构完全不由我控制,无法逐容器加背景;二是水印需要显示在弹窗、浮层之上,而背景方案天然在底层;三是水印内容需要动态生成(比如带上当前时间和访问标识),用背景图就得走接口返回图片,成本太高。
第三种情况其实很常见。如果水印里要带"当前用户标识 + 时间戳"这类动态信息,纯静态背景图就不够用了。这时可以退一步:用一张透明底 + 动态文字的方式——把水印层做成一个绝对定位的容器,里面放一个重复排列的文字网格,容器本身不接收事件。
但在 IE7 里,"容器不接收事件"这件事做不到,只能靠 JS。这就是所谓的"让路"。
4.2 "让路"方案:点下去那一瞬间把水印藏起来
思路很朴素:用户按下鼠标的瞬间,把水印层设为不可见,等点击流程走完再恢复。因为水印层不可见时不参与命中测试,mouseup、click、focus都会自然落到它下面的元素上,整个事件序列完整无缺。
(function () { var layer = document.getElementById('wmLayer'); if (!layer) { return; } // 只在老 IE 上挂载,现代浏览器靠 pointer-events 就够了 if (!document.attachEvent) { return; } var restoreTimer = null; var isHidden = false; function hideLayer() { if (isHidden) { return; } isHidden = true; layer.style.visibility = 'hidden'; // 兜底:万一 mouseup 丢失,1 秒后强制恢复 if (restoreTimer) { clearTimeout(restoreTimer); } restoreTimer = setTimeout(showLayer, 1000); } function showLayer() { if (!isHidden) { return; } isHidden = false; layer.style.visibility = 'visible'; if (restoreTimer) { clearTimeout(restoreTimer); restoreTimer = null; } } layer.attachEvent('onmousedown', function () { hideLayer(); }); document.attachEvent('onmouseup', function () { // 延后一拍,保证 click 事件能正常派发 setTimeout(showLayer, 0); }); })();这里有几个点值得展开。第一,为什么用visibility: hidden而不是display: none?display: none会让元素脱离渲染树,触发重排,虽然水印层是绝对定位不影响其他元素的位置,但重排的代价在 IE7 上还是偏高。visibility: hidden只是不绘制,不影响布局,恢复也更快。
第二,为什么mouseup要绑在document上?因为水印层已经隐藏了,mouseup不会再落在它身上,必须从文档级监听才能捕获到。第三,setTimeout(showLayer, 0)这行很重要,如果同步恢复,click事件的派发可能发生在水印层已经重新可见之后,导致点击又被吞掉。延后一拍能让click先完成。
这个方案的副作用是水印会在点击瞬间"闪"一下。实践下来,如果恢复延迟控制在 50ms 以内,肉眼基本感知不到,因为人眼对短暂消失的容忍度比想象中高。但在滚动 + 点击的复合操作里,偶尔会看到轻微的闪烁,属于可接受范围。
4.3 用 elementFromPoint 做精确命中转发
有些场景不适合"藏起来再恢复",比如拖拽选择文本。这时可以用另一种思路:水印层老老实实接着事件,但接收到之后,主动转交给它下面的元素。
IE 提供了document.elementFromPoint(x, y),传入视口坐标,返回该点最上层的元素。问题在于它返回的往往就是水印层自己,所以要先把水印层临时设为不可见,取到目标元素,再立刻恢复。
function hitTest(x, y, layer) { var oldVisibility = layer.style.visibility; layer.style.visibility = 'hidden'; var target = document.elementFromPoint(x, y); layer.style.visibility = oldVisibility || 'visible'; return target; }拿到目标元素之后,用fireEvent把事件转发过去。
function forwardEvent(target, type, nativeEvent) { if (!target || !target.fireEvent) { return; } var cloned = document.createEventObject(nativeEvent); target.fireEvent('on' + type, cloned); }这里必须提醒一句:fireEvent只能触发绑定在元素上的事件处理器,不会触发浏览器的默认行为。也就是说,你转发一个onclick给<a>标签,它的onclick处理器会执行,但页面不会跳转;转发给<input>,不会自动聚焦;转发给<select>,下拉框不会展开。
所以对不同类型的元素要做特判:
function activate(target) { if (!target) { return; } var tag = (target.tagName || '').toUpperCase(); if (tag === 'A' && target.href) { window.location.href = target.href; } else if (tag === 'INPUT' || tag === 'TEXTAREA') { target.focus(); } else if (tag === 'LABEL') { var id = target.getAttribute('for'); var ctrl = id && document.getElementById(id); if (ctrl) { ctrl.focus(); } } }<select>是个死胡同——IE7 里没有任何脚本手段能主动展开一个下拉框。遇到这种情况只能靠"让路"方案,在mousedown时把水印藏起来,让原生点击自己去展开。
综合来看,elementFromPoint这套组合更适合处理简单的点击和聚焦,不适合替代完整的事件模型。我的做法是:默认用让路方案,只有当页面存在大量依赖坐标命中的自定义组件(比如自绘的树形控件、拖拽面板)时,才叠加 elementFromPoint 做补充。
4.4 滚动跟随:IE7 下别指望 position: fixed
水印要铺满整个视口,如果页面很长,水印层的高度必须跟内容高度一致,否则滚到底部就没了。做法是给水印层设position: absolute,高度取document.body.scrollHeight,在窗口resize和内容变化时重算。
function syncHeight(layer) { var h = Math.max( document.documentElement.scrollHeight || 0, document.body.scrollHeight || 0, document.documentElement.clientHeight || 0 ); layer.style.height = h + 'px'; }如果你希望水印相对视口固定(滚动时水印不动),IE7 标准模式虽然支持position: fixed,但配合滤镜或者页面里有其他 hasLayout 元素时,会出现重绘残影、滚动抖动的问题。我的选择是老老实实用 JS 模拟。
function stickToViewport(layer) { var pending = false; function reposition() { if (pending) { return; } pending = true; setTimeout(function () { var top = document.documentElement.scrollTop || document.body.scrollTop || 0; var left = document.documentElement.scrollLeft || document.body.scrollLeft || 0; layer.style.top = top + 'px'; layer.style.left = left + 'px'; pending = false; }, 30); } if (window.attachEvent) { window.attachEvent('onscroll', reposition); window.attachEvent('onresize', reposition); } reposition(); }30ms 的节流很关键。IE7 的滚动事件触发频率极高,一次滚动能触发几十上百次,不节流的话每一帧都要读写样式,页面会直接卡死。这个数字是我反复试出来的,低于 20ms 效果不明显,高于 50ms 会出现明显的"水印滞后"。
提示:绝对定位层铺满时,用
left/right/top/bottom四向拉伸比width: 100%; height: 100%更稳。IE7 里width: 100%在出现纵向滚动条时容易撑出横向滚动条,四向拉伸不会。
4.5 水印被删掉怎么办:轻量守护
水印的防护价值取决于它是否容易被移除。任何一个会用开发者工具的人,删掉一个div是几秒钟的事。虽然这不是技术能彻底解决的问题,但加一层轻量检测能提高门槛。
function guard(layer) { setInterval(function () { var alive = document.getElementById(layer.id); if (!alive) { // 被移除,重新挂回去 document.body.appendChild(layer); return; } // 被改成透明或者隐藏 if (alive.style.display === 'none' || alive.style.visibility === 'hidden') { alive.style.display = ''; alive.style.visibility = 'visible'; } }, 2000); }2 秒的间隔是个平衡点。太频繁会影响页面性能,太稀疏又起不到作用。但这招只能防住"随手删",防不住有备而来的绕过,这一点在需求沟通时就要说清楚,避免给出不切实际的承诺。
5. 踩坑记录与排查速查表
下面这些现象,我在不同项目里至少各遇到过一次,整理成表方便对照。
| 现象 | 真实成因 | 处置方式 |
|---|---|---|
| 输入框点不进去,光标不出现 | 水印层拦截了mousedown | 改用背景方案,或加水印让路脚本 |
| 按钮 hover 无反应 | 水印层吃掉了mouseover | 同上;检查水印层z-index是否过高 |
| 下拉框展开后水印出现破洞 | IE 的select是窗口级控件,永远置顶 | 无法通过样式解决,只能把水印层避开select区域 |
| 水印在页面底部消失 | 水印层高度只取了一屏 | 用scrollHeight同步高度 |
| 滚动时水印明显滞后 | 滚动事件未节流 | 加 30ms 节流 |
| 水印区域颜色发灰、文字发闷 | 半透明层叠在内容上拉低对比度 | 降低水印浓度,素材改用浅灰 |
| 弹窗打开后水印消失 | 水印层z-index低于弹窗 | 提升水印层z-index,或把水印放在弹窗容器内 |
| 打印出来没有水印 | 浏览器默认不打印背景图 | 打印媒体下改用img标签插入水印 |
IE7 下opacity完全无效 | IE7 不识别该属性 | 换用filter: alpha(opacity=xx) |
加了滤镜后z-index失效 | filter触发了新的层叠上下文 | 把滤镜加在内部子元素上,不放在定位容器上 |
5.1 打印场景的补充处理
后台系统经常要求"打印出来的文件也带水印"。背景图默认不打印是浏览器的通用行为(用户可以在打印设置里勾选"背景图形",但不能指望他们勾)。所以打印媒体里要换一种实现:用img标签把水印图片平铺出来。
@media print { .wm-layer { display: block; position: fixed; left: 0; top: 0; width: 100%; height: 100%; z-index: 9999; } .wm-layer img { position: absolute; opacity: .1; } }position: fixed在打印媒体里是安全的,因为打印不涉及滚动。每张img的位置需要根据 A4 纸张尺寸算好,页边距按毫米换算成像素(1mm 约等于 3.78px)。这部分比较繁琐,但一次做完可以复用。
5.2 几个容易忽略的细节
第一,水印层的 DOM 节点不要放在需要被序列化的容器里。有些老系统会把整个页面innerHTML存进隐藏域做快照,水印层混进去会导致数据暴涨。我一般把水印挂在body的直接子节点,并且加一个明确的id,序列化前先剔除。
第二,attachEvent绑定的事件处理器里,this指向window而不是绑定元素,和addEventListener的行为不一致。写兼容代码时如果依赖this,必须手动绑定,否则会静默出错——这个坑极其隐蔽,因为控制台不报错,只是逻辑不生效。
第三,IE7 下document.body和document.documentElement获取滚动位置、可视高度时经常对不上,必须两边都取再比大小。这个判断我封装成了一个工具函数,用在所有需要读视口尺寸的地方。
function viewportSize() { var d = document; var de = d.documentElement; var b = d.body; return { width: Math.max(de.clientWidth || 0, b.clientWidth || 0), height: Math.max(de.clientHeight || 0, b.clientHeight || 0), scrollTop: de.scrollTop || b.scrollTop || 0, scrollLeft: de.scrollLeft || b.scrollLeft || 0 }; }6. 一点个人经验
折腾过几轮之后,我现在的默认选择是:能上背景就上背景,实在不行才上覆盖层。背景方案的代码量大约只有覆盖层的三分之一,出错率低一个数量级,而且不需要为了兼容旧浏览器去写一堆特判。很多人排斥背景方案,是因为"水印会被白色区块遮住"这件事需要手动补,但补的过程其实是一次页面样式梳理,顺带能发现不少冗余的背景声明。
真正需要覆盖层的场景,我一般只保留两处:一个是全屏弹窗的上层,因为弹窗的z-index通常很高,背景方案盖不住;另一个是需要动态文字水印的时候,用elementFromPoint配合让路脚本。这两处的代码我都会单独抽成一个模块,注释写清楚"仅 IE7 兼容路径",等哪天老终端下线了,删掉这个文件就干净了。
最后一个实用小技巧:调试水印遮挡问题时,先把水印浓度