前沿
WebAPI
中总有一些能力平时很少用,可是一旦用上就像天降神兵一样,威力无穷。本文主要整理我眼中的三个API
: DOM 节点监听、网页性能监听、窗口可视监听。
我会结合公司实际场景分别实现三个Demo
,来直观展示他们的效果。 水印监听、模仿network
实现资源加载列表、滚动分页。
水印监听:
模仿network
资源加载:
滚动分页:
一、MutationObserver
MDN
解释:MutationObserver
接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。
MutationObserver
专门用于监听 DOM 树的变化。最常见的应用场景就是网站水印,当然还有少量的白屏检测。目的是为了保护公司数字资产。
水印和 MutationObserver 有何关联?
水印惯用的做法是创建 DOM 节点,生成背景图片,然后平铺而来,但是 DOM 节点是可以人为修改的,从而屏蔽网页水印,既然是改变了 DOM 节点,那自然 MutationObserver 最适合这一类需求。
如何使用
- 实例化 MutationObserver 对象
- 监听目标变化
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// Later, you can stop observing
observer.disconnect();
我们通过一个 Demo 进一步分析:
- 先简单创建一个 canvas 画布
背景: 水印有多种实现方式,甚至你可以不用画布,直接添加文本节点,通过样式,让它铺满屏幕,但这种做法不够优雅,所以日常见到的水印都是通过画布生成一张图片。你可能会问,我直接提供一个做好的图片进行屏幕不是也可以?当然可以,但是你仔细看,你会发现水印上面的文本是动态的,比如飞书文档的水印,会显示你的名字+手机号,因此,我们不能直接使用静态的图片,需要动态制作水印。
// 创建canvas
const canvas = document.createElement('canvas');
// 设置画布尺寸
canvas.width = 100;
canvas.height = 100;
// 创建上下文
const ctx = canvas.getContext('2d');
// 设置绘制起点
ctx.translate(50, 50);
// 向左旋转 30 度
ctx.rotate(-30 * Math.PI / 180);
// 设置填充文本样式
ctx.textAlign = 'center';
ctx.font = '20px Arial';
ctx.fillStyle = '#f16622';
ctx.fillText('Marsview', 0, 10);
// 生产 base64 格式的图片地址
const base64 = canvas.toDataURL();
- 创建 DOM 节点,添加到网页中
背景: 拿到图片以后,就可以给 DOM 节点设置背景图片并平铺。
const wm = document.getElementById('watermark');
if(!wm){
wm = document.createElement('div');
wm.id = 'watermark';
document.body.appendChild(wm);
}
// 设置样式
wm.style = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 99;
pointer-events: none;
background: url(${base64}) repeat;
`
到此,我们可以得到一个最简单的文字水印,但是我们通过开发工具,可以直接屏蔽水印,如下:
- 实例化 MutationObserver ,监听目标
背景: 由于 DOM
节点可以随意修改,因此水印会人为丢失,通过 MutationObserver
监听到变化后,重新绘制。
// 实例化对象
const observer = new MutationObserver((mutationsList) => {
for(let mutation of mutationsList){
// 如果属性发生修改,并且是当前目标对象,则重新绘制
if(mutation.type === 'attributes' && mutation.target === wm){
watchTarget();
}else if(mutation.type === 'childList' && [...mutation.removedNodes].find(item=>item === wm)){
// 如果删除当前水印节点,则重新绘制
watchTarget()
}
}
})
// 监听body的属性变化
const watchTarget = ()=>{
// 断开监听,防止死循环
observer.disconnect();
// 初始化节点
init();
// 重新监听
observer.observe(document.body,{
attributes: true,
childList: true,
subtree: true
})
}
- attributes: 用来监听节点属性变化
- childList: 用来监听子节点内容变化
- Subtree: 用来任意子树的变化
为了防止水印节点被删除,我们监听的是 document.body
,如果只是监听水印节点本身的样式被篡改,那目标可以改成vm
。
- 演示效果如下:
温馨提示:
- 为了保证弹框组件也有水印,需要把水印的 z-index 设置为最大。
- 为了保证鼠标事件,不影响网页内容交互,需要设置:pointer-events: none ,也就是事件穿透。
目前发现 van 项目并没有对水印做防篡改。
如何给图片添加水印(扩展)?
既然已经讲到了水印,就顺便扩展一下,如果是一张图片想要在发送给别人前,添加水印,如何实现?
- 阿里云OSS地址,可以直接生成水印
const img = document.createElement('img')
img.style = 'width: 100%;'
const text = window.btoa('Marsview')
img.src = `https://static.xxx.cn/image/474170dde18ef72b893321cba6fc18e8dde8fb4d.jpg?x-oss-process=image/watermark,text_${text},color_f16622`;
document.body.appendChild(img)
为何需要使用btoa
进行转换?因为阿里云要求必须对字符串进行base64编码。
温馨提示:
- 如果水印文本是中文还需要进一步转换:btoa(unescape(encodeURIComponent('Marsview')))
- 阿里云 OSS 文档:help.aliyun.com/zh/oss/user…
- 非阿里云图片,添加水印
整体实现思路跟文本类似,一个是在画布中绘制文本,一个是绘制图片。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>图片水印</title>
</head>
<body>
<img src="" id="img" style="width: 100%;">
<script>
const img = new Image();
img.src = 'https://static.xxx.cn/image/xxx.jpg'
// 防止资源因为不同源而请求失败
img.crossOrigin = 'anonymous';
async function init(){
const canvas = document.createElement('canvas')
// 等待图片加载完成,用来获取图片的实际宽高
await new Promise(resolve => img.onload = resolve)
// 设置画布尺寸
canvas.width = img.width;
canvas.height = img.height;
// 设置上下文
const ctx = canvas.getContext('2d');
// 绘制图片
ctx.drawImage(img, 0, 0);
// 设置文本绘制的起点
ctx.translate(img.width - 100, img.height - 30)
// 设置文本样式
ctx.font = '28px arial';
ctx.fillStyle = '#7d33ff';
ctx.fillText('Marsview', 0, 10);
// 生产 base64 图片
const base64 = canvas.toDataURL();
// 直接赋值
document.querySelector('img').src = base64;
}
init();
</script>
</body>
</html>
二、PerformanceObserver
PerformanceObserver 接口主要用于观测性能事件。最主要的应用场景就是网页性能统计。
window.performance
对象是在网页加载完成后,主动获取的性能指标数据。而PerformanceObserver
是一种被动的动态的监听方式。下面我们实现一个简单的资源加载列表,类似于Network
的资源列表,包含资源名称、加载时间、持续时间、状态、资源大小。
很多公司可能都有自己的性能监控平台,大多数底层都会基于该
API
实现。
如何使用?
// 创建实例
const observer = new PerformanceObserver(callback);
// 监听事件配置
observer.observe(config);
// 断开监听
observer.disconnect();
Demo 演示
- 编写一个简单的
HTML
- 通过
network
查看资源加载列表
一个文档加载、一个js脚本、一个远程图片、一个 fetch、一个 svg 图片,剩下两个为 chrome 扩展插件携带的样式,可以忽略。
- 开发一个类似于
network
的面板统计功能。
/**
* 检测页面性能
*/
function callback(list){
const entries = list.getEntries();
console.log(entries);
const data = []
for(let entry of entries){
if(['navigation','resource','fetch','other'].includes(entry.entryType)){
data.push({
// 资源名称
name: entry.name.split('/').pop(),
// 加载时间,对应 network 的耗时
loadTime: Math.round(entry.responseEnd - entry.requestStart),
// 度量事件开始到结束的持续时间,实际值要比文档真正加载时间更大
duration: Math.round(entry.duration),
// http 响应状态码
status: entry.responseStatus,
// 静态指标类型
entryType: entry.entryType,
// 功能类型
initiatorType: entry.initiatorType,
// 资源大小
size: Math.round(entry.transferSize/1000*10)/10 + 'K'
})
}else{
// 其他指标类型,我们挑选几个进行打印
data.push({
name: entry.name,
startTime: Math.round(entry.startTime),
entryType: entry.entryType,
duration: Math.round(entry.duration),
renderTime: entry.renderTime ? Math.round(entry.renderTime):'-',
})
}
}
console.table(data)
}
const observe = new PerformanceObserver(callback);
observe.observe({
entryTypes: PerformanceObserver.supportedEntryTypes
});
- 控制台输出结果展示
- 我们对比一下
watermark.js
加载数据
network返回数据:
控制台打印数据:
资源名称:watermark.js
加载耗时:17ms,对应loadTime
功能类型:fetch
资源大小:2.3 K
响应状态:200
上下的数据指标基本是一致的,但是需要注意的是,图片资源取值loadTime
跟network
会有差异,针对图片建议使用duration
时间作为整个加载和渲染时间。
上面为何会有一次布局位移?
因为图片是远程的,渲染引擎在绘制的时候,只是一个占位节点,所以下面的按钮会贴这占位节点进行绘制,当图片加载完成以后,会自动撑开,从而导致下面的按钮发生布局位移。
相关补充
目前支持的指标类型有:
// PerformanceObserver.supportedEntryTypes
[
'element', 'event', 'first-input', 'largest-contentful-paint', 'layout-shift',
'longtask', 'mark', 'measure', 'navigation', 'paint', 'resource',
'visibility-state'
]
我们发现里面有一个事件是visibility-state
,当浏览器页签打开关闭时,也会触发,效果如下:
通过这个能力,可以在在线面试过程中,对浏览器页签切换做统计。
三、IntersectionObserver
Intersection Observer 接口用于观察目标元素与文档可视窗口的交叉变化。常见的场景有:滚动分页、目标元素可见时长(埋点指标会用到)、针对目标在可视窗口外时做功能处理。
背景
最近做一个需求,手机号是加密的,需要点击按钮后,才能展示该内容。于是我封装了独立的脱敏组件,点击按钮时,会通过tooltip
进行展示,原本的做法是失去焦点时,手机号会隐藏,但是业务不允许隐藏,希望手动关闭,因为要根据手机号做一些数据对比,同时业务希望页面上下滚动时,手机号一直展示,除非滚动到不可见时,自动隐藏。
这个需求听起来就不好实现,用传统的方式挺麻烦的,最后想到了IntersectionObserver
方案。
用法
const intersectionObserver = new IntersectionObserver((entries) => {
// TODO
});
// start observing
intersectionObserver.observe(target);
Demo1 演示(目标不可见时隐藏)
1. 编写一个简单的网页
网页包括三部分内容,第一块蓝色背景,中间是文字,第三块是黄色背景。我们需要监听文字的变化。
2. 添加可视口监听
const observer = new IntersectionObserver(function (entries) {
console.log(entries)
})
observer.observe(document.querySelector('#target'))
3. 可视口下,查看目标对象状态值
- intersectionRatio 为 1 ,表示目标对象和文档窗口交叉比例为 1,也就是完全重叠。
- isIntersecting 为 true ,表示两个区域是交叉状态,即目标在可视窗口内。
4. 滚动页面至目标元素不可见时,查看状态值
-
intersectionRatio 为 0 ,表示目标对象和文档窗口交叉比例为 0 ,如果是重叠会返回对应的比例值。
-
isIntersecting 为 false ,表示两个区域是相离状态,即目标在可视窗口外。
针对业务常见下,手机号隐藏情况,在回调函数中,判断 intersectionRatio = 0 时,可以对手机号做隐藏处理。
温馨提示
0 和 1 是 相离和重叠的两个极端情况,如果是交叉,会有一定的比例值,大家根据不同的业务场景做相应的功能处理。
Demo2 演示(滚动加载)
- 编写一个简单的网页
<div>
<div>
<h1>滚动加载演示</h1>
<div id="root"></div>
</div>
<div id="target">兄弟,到底了</div>
</div>
- 添加监听事件
function loadData() {
const root = document.querySelector('#root')
const count = root.childElementCount;
if(count === 30)observer.disconnect();
for(let i = 0; i < 10; i++) {
const p = document.createElement('p')
p.innerText = `这是第${count + i + 1}条数据`
root.appendChild(p)
}
}
loadData();
const observer = new IntersectionObserver(function (entries) {
console.log(entries)
if(entries[0].intersectionRatio > 0) {
loadData()
}
})
observer.observe(document.querySelector('#target'))
- 默认加载 10 条数据。
- 当总条数为 30 时,断开监听,最终会加载 40 条数据。
- intersectionRatio > 0 表示只要目标对象有任何交叉(可见),就立刻加载新数据。
- 效果查看
总结
有些业务功能,使用最朴素的方式确实可以实现,但是巧用监听器反而事半功倍。
- 水印防篡改可基于 MutationObserver 实现对节点本身和属性的监听来实现。
- 阿里云图片官方提供了参数化的方式来一键实现水印功能,但需要对内容进行 base64 编码。
- PerformanceObserver 可以用来对资源、请求、文档等内容加载时机下的性能指标统计。
- Intersection Observer 可用于监听目标元素是否可见、做埋点功能、滚动分页功能。
推荐
近期我的开源低代码平台marsview
又做了不少更新,大家可以自由下载和在线体验。
- Marsview开源
- 体验地址:marsview.cc
- 开源仓库:github.com/JackySoft/m…