我眼中的几个神力般的WebAPI

4,212 阅读8分钟

前沿

WebAPI中总有一些能力平时很少用,可是一旦用上就像天降神兵一样,威力无穷。本文主要整理我眼中的三个APIDOM 节点监听网页性能监听窗口可视监听

我会结合公司实际场景分别实现三个Demo,来直观展示他们的效果。 水印监听、模仿network实现资源加载列表、滚动分页。

水印监听:

image.png

模仿network资源加载:

image.png

滚动分页:

ab26b9e8-ba4a-44ef-ba9d-642732a94f3a.gif

一、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 进一步分析:

  1. 先简单创建一个 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();
  1. 创建 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;
`

到此,我们可以得到一个最简单的文字水印,但是我们通过开发工具,可以直接屏蔽水印,如下:

image.png

image.png

  1. 实例化 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

  1. 演示效果如下:

2ed9e482-7409-413b-8fb0-273e9248274b.gif

温馨提示:

  1. 为了保证弹框组件也有水印,需要把水印的 z-index 设置为最大。
  2. 为了保证鼠标事件,不影响网页内容交互,需要设置:pointer-events: none ,也就是事件穿透。

目前发现 van 项目并没有对水印做防篡改。

MDN 文档:developer.mozilla.org/en-US/docs/…

如何给图片添加水印(扩展)?

既然已经讲到了水印,就顺便扩展一下,如果是一张图片想要在发送给别人前,添加水印,如何实现?

  1. 阿里云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编码。

image.png

温馨提示:

  • 如果水印文本是中文还需要进一步转换:btoa(unescape(encodeURIComponent('Marsview')))
  • 阿里云 OSS 文档:help.aliyun.com/zh/oss/user…
  1. 非阿里云图片,添加水印

整体实现思路跟文本类似,一个是在画布中绘制文本,一个是绘制图片。

<!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 演示

  1. 编写一个简单的HTML

image.png

  1. 通过network查看资源加载列表

image.png

一个文档加载、一个js脚本、一个远程图片、一个 fetch、一个 svg 图片,剩下两个为 chrome 扩展插件携带的样式,可以忽略。

  1. 开发一个类似于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
});
  1. 控制台输出结果展示

image.png

  1. 我们对比一下watermark.js加载数据

network返回数据:

image.png

控制台打印数据:

image.png

资源名称:watermark.js

加载耗时:17ms,对应loadTime

功能类型:fetch

资源大小:2.3 K

响应状态:200

上下的数据指标基本是一致的,但是需要注意的是,图片资源取值loadTimenetwork会有差异,针对图片建议使用duration时间作为整个加载和渲染时间。

上面为何会有一次布局位移?

因为图片是远程的,渲染引擎在绘制的时候,只是一个占位节点,所以下面的按钮会贴这占位节点进行绘制,当图片加载完成以后,会自动撑开,从而导致下面的按钮发生布局位移。

相关补充

目前支持的指标类型有:

// PerformanceObserver.supportedEntryTypes
[
    'element', 'event', 'first-input', 'largest-contentful-paint', 'layout-shift', 
    'longtask', 'mark', 'measure', 'navigation', 'paint', 'resource', 
    'visibility-state'
]

我们发现里面有一个事件是visibility-state,当浏览器页签打开关闭时,也会触发,效果如下:

image.png

通过这个能力,可以在在线面试过程中,对浏览器页签切换做统计。

三、IntersectionObserver

Intersection Observer 接口用于观察目标元素与文档可视窗口的交叉变化。常见的场景有:滚动分页、目标元素可见时长(埋点指标会用到)、针对目标在可视窗口外时做功能处理。

背景

最近做一个需求,手机号是加密的,需要点击按钮后,才能展示该内容。于是我封装了独立的脱敏组件,点击按钮时,会通过tooltip进行展示,原本的做法是失去焦点时,手机号会隐藏,但是业务不允许隐藏,希望手动关闭,因为要根据手机号做一些数据对比,同时业务希望页面上下滚动时,手机号一直展示,除非滚动到不可见时,自动隐藏。

这个需求听起来就不好实现,用传统的方式挺麻烦的,最后想到了IntersectionObserver方案。

用法

const intersectionObserver = new IntersectionObserver((entries) => {
    // TODO 
 });
 
// start observing
intersectionObserver.observe(target);

Demo1 演示(目标不可见时隐藏)

1. 编写一个简单的网页

网页包括三部分内容,第一块蓝色背景,中间是文字,第三块是黄色背景。我们需要监听文字的变化。

image.png

2. 添加可视口监听

const observer = new IntersectionObserver(function (entries) {
    console.log(entries)
})
observer.observe(document.querySelector('#target'))

3. 可视口下,查看目标对象状态值

image.png

  • intersectionRatio 为 1 ,表示目标对象和文档窗口交叉比例为 1,也就是完全重叠。
  • isIntersecting 为 true ,表示两个区域是交叉状态,即目标在可视窗口内。

4. 滚动页面至目标元素不可见时,查看状态值

image.png

  • intersectionRatio 为 0 ,表示目标对象和文档窗口交叉比例为 0 ,如果是重叠会返回对应的比例值。

  • isIntersecting 为 false ,表示两个区域是相离状态,即目标在可视窗口外。

针对业务常见下,手机号隐藏情况,在回调函数中,判断 intersectionRatio = 0 时,可以对手机号做隐藏处理。

温馨提示

0 和 1 是 相离和重叠的两个极端情况,如果是交叉,会有一定的比例值,大家根据不同的业务场景做相应的功能处理。

Demo2 演示(滚动加载)

  1. 编写一个简单的网页
<div>
  <div>
    <h1>滚动加载演示</h1>
    <div id="root"></div>
  </div>
  <div id="target">兄弟,到底了</div>
</div>

image.png

  1. 添加监听事件
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 表示只要目标对象有任何交叉(可见),就立刻加载新数据。
  1. 效果查看

ab26b9e8-ba4a-44ef-ba9d-642732a94f3a.gif

总结

有些业务功能,使用最朴素的方式确实可以实现,但是巧用监听器反而事半功倍。

  • 水印防篡改可基于 MutationObserver 实现对节点本身和属性的监听来实现。
  • 阿里云图片官方提供了参数化的方式来一键实现水印功能,但需要对内容进行 base64 编码。
  • PerformanceObserver 可以用来对资源、请求、文档等内容加载时机下的性能指标统计。
  • Intersection Observer 可用于监听目标元素是否可见、做埋点功能、滚动分页功能。

推荐

近期我的开源低代码平台marsview又做了不少更新,大家可以自由下载和在线体验。