追着双标黑子胖揍 發表於 2026-1-5 11:08:00

神级JS API,谁用谁好用

<h1 data-id="heading-0">🧑‍💻 写在开头</h1>
<p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<div>
<div>
<h2 data-id="heading-0">1. ResizeObserver</h2>
<p><code>ResizeObserver</code> 是一个浏览器原生的 JavaScript API,用于<strong>监听 DOM 元素尺寸的变化</strong>。它类似于 <code>MutationObserver</code>,但专门用于观察元素的大小(宽高)变化,而无需依赖 <code>window.resize</code> 事件(后者只对视口变化有效)。</p>
<h3 data-id="heading-1">🧩 基本用法</h3>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const resizeObserver = new ResizeObserver(entries =&gt; {
for (let entry of entries) {
    const { width, height } = entry.contentRect;
    console.log(`元素尺寸:${width} x ${height}`);
   
    // entry.target 是被观察的 DOM 元素
    console.log('目标元素:', entry.target);
}
});

// 开始观察某个元素
resizeObserver.observe(document.querySelector('#my-element'));

// 可选:观察多个元素
// resizeObserver.observe(element1);
// resizeObserver.observe(element2);</pre>
</div>
<div>
<div>
<h3 data-id="heading-2">📦 entry.contentRect vs getBoundingClientRect()</h3>
<ul>
<li><code>entry.contentRect</code>:表示<strong>内容区域</strong>(不包括 padding、border、margin),类似于&nbsp;<code>getComputedStyle().width/height</code>&nbsp;的计算结果。</li>
<li>如果你需要包括 border 和 padding 的尺寸,可以结合&nbsp;<code>entry.target.getBoundingClientRect()</code>&nbsp;使用。</li>
</ul>
<h3 data-id="heading-3">🛑 停止观察</h3>
</div>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 停止观察某个元素
resizeObserver.unobserve(element);

// 停止观察所有元素并释放资源
resizeObserver.disconnect();</pre>
</div>
<div>
<div>
<blockquote>
<p><strong>建议</strong>:在组件销毁(如 React 的 <code>useEffect</code> 清理函数、Vue 的 <code>onBeforeUnmount</code>)时调用 <code>disconnect()</code>,避免内存泄漏。</p>
</blockquote>
<h3 data-id="heading-4">✅ 使用场景</h3>
<ol>
<li><strong>响应式组件</strong>:当容器尺寸变化时动态调整子元素(如图表、Canvas、视频)。</li>
<li><strong>自定义滚动条或布局</strong>:监听内容区域变化以更新 UI。</li>
<li><strong>替代&nbsp;<code>window.onresize</code></strong>:更精确地响应<strong>特定元素</strong>的尺寸变化,而非整个窗口。</li>
<li><strong>Web Components / 封装组件</strong>:内部自动适配父容器大小。</li>
</ol>
<h3 data-id="heading-5">🌐 浏览器兼容性</h3>
<ul>
<li>✅ Chrome 64+</li>
<li>✅ Firefox 69+</li>
<li>✅ Safari 13.1+</li>
<li>✅ Edge 79+</li>
<li>❌ IE 不支持(需 polyfill)</li>
</ul>
<blockquote>
<p>兼容性已非常广泛,现代项目可放心使用。</p>
</blockquote>
<h3 data-id="heading-6">🛠️ Polyfill(如需支持旧浏览器)</h3>
<p>可通过 GitHub - juggle/resize-observer 提供的 polyfill:</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">npm install @juggle/resize-observer</pre>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">import ResizeObserver from '@juggle/resize-observer';

// 如果原生不支持,则使用 polyfill
if (!window.ResizeObserver) {
window.ResizeObserver = ResizeObserver;
}</pre>
</div>
<h3 data-id="heading-7">示例:React 中使用</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">import { useEffect, useRef } from 'react';

function MyComponent() {
const containerRef = useRef(null);

useEffect(() =&gt; {
    const observer = new ResizeObserver(entries =&gt; {
      for (let entry of entries) {
      console.log('新宽度:', entry.contentRect.width);
      }
    });

    if (containerRef.current) {
      observer.observe(containerRef.current);
    }

    return () =&gt; {
      observer.disconnect(); // 清理
    };
}, []);

return &lt;div ref={containerRef}&gt;可变尺寸容器&lt;/div&gt;;
}</pre>
</div>
<div>
<div>
<h2 data-id="heading-8">2.IntersectionObserver</h2>
<p><code>IntersectionObserver</code> 是一个强大的浏览器原生 API,用于<strong>异步监听目标元素与祖先元素(或视口)的交叉(相交)状态变化</strong>。它常用于实现<strong>懒加载、无限滚动、曝光统计、动画触发</strong>等场景,性能远优于传统的 <code>scroll</code> 事件监听。</p>
<h3 data-id="heading-9">🧩 基本用法</h3>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const observer = new IntersectionObserver((entries, observer) =&gt; {
entries.forEach(entry =&gt; {
    // entry.target:被观察的 DOM 元素
    // entry.isIntersecting:是否与根(viewport 或 root)相交
    // entry.intersectionRatio:相交区域占目标元素的比例(0 ~ 1)
    // entry.intersectionRect:相交区域的矩形信息
    // entry.boundingClientRect:目标元素相对于视口的位置
    // entry.rootBounds:根元素的边界(通常是视口)

    if (entry.isIntersecting) {
      console.log('元素进入视口:', entry.target);
      // 例如:加载图片、触发动画
    } else {
      console.log('元素离开视口');
    }
});
});

// 开始观察某个元素
observer.observe(document.querySelector('#my-element'));</pre>
</div>
<h3 data-id="heading-10">⚙️ 配置选项(可选)</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const options = {
root: null, // 默认为视口(viewport);可设为某个祖先元素
rootMargin: '0px', // 类似 CSS margin,扩展或收缩根的边界(支持负值)
threshold: 0.5 // 触发回调的相交比例阈值(0 ~ 1),可为数字或数组
};

const observer = new IntersectionObserver(callback, options);</pre>
</div>
<div>
<div>
<h4 data-id="heading-11"><code>threshold</code>&nbsp;示例:</h4>
<ul>
<li><code>threshold: 0</code>:只要有一点进入就触发(默认)。</li>
<li><code>threshold: 1</code>:完全进入才触发。</li>
<li><code>threshold: </code>:在 0%、25%、50%... 时都触发。</li>
</ul>
<h3 data-id="heading-12">🛑 停止观察</h3>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">observer.unobserve(element); // 停止单个元素
observer.disconnect();      // 停止所有并释放资源</pre>
</div>
<blockquote>
<p>建议:在组件销毁时调用&nbsp;<code>disconnect()</code>,防止内存泄漏。</p>
</blockquote>
<h3 data-id="heading-13">✅ 典型应用场景</h3>
<h4 data-id="heading-14">1.&nbsp;图片懒加载</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const imgObserver = new IntersectionObserver((entries) =&gt; {
entries.forEach(entry =&gt; {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src; // 从 data-src 加载真实图片
      imgObserver.unobserve(img); // 加载后停止观察
    }
});
});

document.querySelectorAll('img').forEach(img =&gt; {
imgObserver.observe(img);
});</pre>
</div>
<div>
<div>
<h4 data-id="heading-15">2.&nbsp;<strong>滚动到底部自动加载(无限滚动)</strong></h4>
<p>观察一个“哨兵”元素(如分页加载提示),当它进入视口时触发加载。</p>
<h4 data-id="heading-16">3.&nbsp;<strong>曝光埋点 / 广告可见性统计</strong></h4>
<p>当广告或内容区域进入视口一定比例时,上报“曝光”事件。</p>
<h4 data-id="heading-17">4.&nbsp;<strong>滚动动画(如 AOS 效果)</strong></h4>
<p>元素进入视口时添加 CSS 动画类。</p>
<h3 data-id="heading-18">🌐 浏览器兼容性</h3>
<ul>
<li>✅ Chrome 51+</li>
<li>✅ Firefox 55+</li>
<li>✅ Safari 12.1+</li>
<li>✅ Edge 15+</li>
<li>❌ IE 不支持(需 polyfill)</li>
</ul>
<blockquote>
<p>现代浏览器支持良好,移动端也广泛可用。</p>
</blockquote>
<h3 data-id="heading-19">🛠️ Polyfill(兼容旧浏览器)</h3>
<p>官方推荐 polyfill(由 W3C 团队维护):</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">npm install intersection-observer</pre>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 在应用入口引入(自动填充 window.IntersectionObserver)
import 'intersection-observer';</pre>
</div>
<blockquote>
<p>注意:polyfill 会回退到&nbsp;<code>scroll</code>&nbsp;+&nbsp;<code>getBoundingClientRect()</code>,性能较差,仅用于兼容。</p>
</blockquote>
<hr>
<h3 data-id="heading-20">💡 与&nbsp;<code>ResizeObserver</code>&nbsp;/&nbsp;<code>MutationObserver</code>&nbsp;对比</h3>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260105104718483-718593116.png" alt="ScreenShot_2026-01-05_104413_685" loading="lazy"></p>
<p>&nbsp;</p>
<div>
<div>
<p>三者互补,常结合使用。</p>
<h3 data-id="heading-21">📌 小技巧</h3>
<ul>
<li>使用&nbsp;<code>rootMargin: '100px'</code>&nbsp;可以<strong>提前触发</strong>(在元素距离视口还有 100px 时就加载)。</li>
<li>在&nbsp;<code>&lt;img loading="lazy"&gt;</code>&nbsp;普及的今天,简单图片懒加载可直接用 HTML 属性,但复杂逻辑仍需&nbsp;<code>IntersectionObserver</code>。</li>
</ul>
<h2 data-id="heading-22">3.Page Visibility</h2>
<p><code>Page Visibility API</code> 是一个浏览器原生 API,用于检测<strong>当前网页是否对用户可见</strong>(即是否处于前台标签页或被最小化/切换到后台)。它可以帮助开发者优化性能、节省资源,或实现特定业务逻辑(如暂停视频、停止轮询、统计停留时长等)。</p>
<hr>
<h3 data-id="heading-23">🧩 核心属性与事件</h3>
<h4 data-id="heading-24">1.&nbsp;<code>document.visibilityState</code></h4>
<p>返回当前页面的可见性状态,可能值包括:</p>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260105104759389-1785958803.png" alt="ScreenShot_2026-01-05_104754_463" loading="lazy"></p>
<div>
<div>
<blockquote>
<p>实际开发中主要关注 <code>'visible'</code> 和 <code>'hidden'</code>。</p>
</blockquote>
<h4 data-id="heading-25">2.&nbsp;<code>document.hidden</code>(已废弃,建议用&nbsp;<code>visibilityState</code>)</h4>
<ul>
<li><code>true</code>:页面不可见</li>
<li><code>false</code>:页面可见</li>
</ul>
<blockquote>
<p>⚠️ 虽仍可用,但 MDN 建议使用 <code>visibilityState</code>。</p>
</blockquote>
<h4 data-id="heading-26">3.&nbsp;<code>visibilitychange</code>&nbsp;事件</h4>
<p>当页面可见性状态改变时触发。</p>
<h3 data-id="heading-27">✅ 基本用法示例</h3>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">function handleVisibilityChange() {
if (document.visibilityState === 'visible') {
    console.log('页面回到前台');
    // 恢复视频播放、重启定时器、刷新数据等
} else if (document.visibilityState === 'hidden') {
    console.log('页面进入后台');
    // 暂停视频、停止轮询、保存状态等
}
}

// 监听可见性变化
document.addEventListener('visibilitychange', handleVisibilityChange);</pre>
</div>
<h3 data-id="heading-28">🌟 典型应用场景</h3>
<h4 data-id="heading-29">1.&nbsp;暂停/恢复媒体播放</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const video = document.querySelector('video');

document.addEventListener('visibilitychange', () =&gt; {
if (document.hidden) {
    video.pause();
} else {
    video.play();
}
});</pre>
</div>
<h4 data-id="heading-30">2.&nbsp;停止不必要的轮询或定时任务</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">let intervalId;

function startPolling() {
intervalId = setInterval(fetchData, 5000);
}

function stopPolling() {
clearInterval(intervalId);
}

document.addEventListener('visibilitychange', () =&gt; {
if (document.hidden) {
    stopPolling();
} else {
    startPolling();
}
});

startPolling(); // 初始启动</pre>
</div>
<h4 data-id="heading-31">3.&nbsp;用户停留时长统计</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">let startTime = Date.now();
let totalVisibleTime = 0;

document.addEventListener('visibilitychange', () =&gt; {
if (document.hidden) {
    totalVisibleTime += Date.now() - startTime;
} else {
    startTime = Date.now();
}
});

// 页面卸载时上报总可见时长
window.addEventListener('beforeunload', () =&gt; {
totalVisibleTime += Date.now() - startTime;
sendToAnalytics({ visibleTime: totalVisibleTime });
});</pre>
</div>
<div>
<div>
<h4 data-id="heading-32">4.&nbsp;<strong>节省资源(如 Canvas 动画、WebGL)</strong></h4>
<p>在页面不可见时暂停渲染循环,减少 CPU/GPU 消耗。</p>
<h3 data-id="heading-33">🌐 浏览器兼容性</h3>
<ul>
<li>✅ Chrome 13+</li>
<li>✅ Firefox 10+</li>
<li>✅ Safari 7+</li>
<li>✅ Edge 12+</li>
<li>✅ iOS Safari / Android Browser(现代版本)</li>
</ul>
<blockquote>
<p>兼容性极佳,几乎所有现代浏览器都支持。</p>
</blockquote>
<h3 data-id="heading-34">⚠️ 注意事项</h3>
<ul>
<li>
<p><strong>不保证精确性</strong>:在某些系统(如 macOS 快速切换)中,状态切换可能有微小延迟。</p>
</li>
<li>
<p><strong>不是用户活跃度检测</strong>:页面可见 ≠ 用户正在看(用户可能切到其他应用但浏览器窗口仍在前台)。</p>
</li>
<li>
<p><strong>与&nbsp;<code>blur</code>/<code>focus</code>&nbsp;事件的区别</strong>:</p>
<ul>
<li><code>window.onfocus</code>&nbsp;/&nbsp;<code>window.onblur</code>:监听<strong>窗口焦点</strong>(如切换到其他应用)。</li>
<li><code>visibilitychange</code>:监听<strong>标签页是否可见</strong>(即使窗口有焦点,但标签页在后台也算 hidden)。</li>
<li>两者可结合使用以获得更全面的状态判断。</li>
</ul>
</li>
</ul>
<hr>
<h3 data-id="heading-35">🔍 扩展:结合&nbsp;<code>focus</code>/<code>blur</code>&nbsp;更精准判断</h3>
</div>
<div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">let isPageVisible = !document.hidden;
let isWindowFocused = !document.hasFocus();

window.addEventListener('focus', () =&gt; {
isWindowFocused = true;
if (isPageVisible) {
    console.log('用户很可能正在看页面');
}
});

window.addEventListener('blur', () =&gt; {
isWindowFocused = false;
});

document.addEventListener('visibilitychange', () =&gt; {
isPageVisible = !document.hidden;
});</pre>
</div>
</div>
</div>
<h2 data-id="heading-36">4.Web Share API</h2>
<p><code>Web Share API</code>&nbsp;是一个现代浏览器提供的原生 API,允许网页调用操作系统级别的分享功能,让用户将内容(如链接、文本、标题等)快速分享到设备上安装的其他应用(如微信、邮件、短信、笔记等)。</p>
<h3 data-id="heading-37">✅ 基本用法</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">if (navigator.share) {
navigator.share({
    title: '分享标题',
    text: '分享的描述文字',
    url: 'https://example.com'
})
.then(() =&gt; {
    console.log('分享成功');
})
.catch((error) =&gt; {
    if (error.name === 'AbortError') {
      console.log('用户取消了分享');
    } else {
      console.error('分享失败:', error);
    }
});
} else {
// 回退方案:显示自定义分享按钮或提示
alert('您的浏览器不支持 Web Share API,请手动复制链接');
}</pre>
</div>
<div>
<div>
<blockquote>
<p>⚠️ <strong>必须在用户手势触发的上下文中调用</strong>(如点击事件),否则会抛出安全错误。</p>
</blockquote>
<hr>
<h3 data-id="heading-38">🔐 安全与限制</h3>
<ul>
<li><strong>仅限安全上下文</strong>:必须在&nbsp;<code>HTTPS</code>(或&nbsp;<code>localhost</code>)下使用。</li>
<li><strong>用户手势要求</strong>:只能在&nbsp;<code>click</code>、<code>touchend</code>&nbsp;等用户操作回调中调用。</li>
<li><strong>字段非全部必需</strong>:但至少要提供&nbsp;<code>title</code>、<code>text</code>、<code>url</code>&nbsp;中的一个(推荐提供&nbsp;<code>url</code>)。</li>
<li><strong>无法控制目标应用</strong>:分享目标由操作系统决定,开发者无法指定(如“只分享到微信”)。</li>
</ul>
<h3 data-id="heading-39">📱 支持情况(截至 2025 年)</h3>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260105105005533-705919763.png" alt="ScreenShot_2026-01-05_105000_853" loading="lazy"></p>
<h3 data-id="heading-40">🧩 高级用法:分享文件(Web Share API Level 2)</h3>
<p>现代浏览器(Chrome 89+ 等)支持分享文件(如图片、PDF):</p>
<br>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">if (navigator.canShare &amp;&amp; navigator.canShare({ files: })) {
await navigator.share({
    title: '图片分享',
    files: // File 对象数组
});
}</pre>
</div>
<blockquote>
<p>注意:文件必须来自用户选择(如&nbsp;<code>&lt;input type="file"&gt;</code>)或由网页生成,不能是任意网络文件。</p>
</blockquote>
<h3 data-id="heading-41">🔄 回退方案(Fallback)</h3>
<p>当不支持 Web Share 时,可提供复制链接或自定义分享按钮:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">function fallbackShare(url) {
const input = document.createElement('input');
input.value = url;
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
alert('链接已复制到剪贴板');
}</pre>
</div>
<h3 data-id="heading-42">📦 在框架中使用(React 示例)</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">function ShareButton({ url, title, text }) {
const handleShare = async () =&gt; {
    if (navigator.share) {
      try {
      await navigator.share({ url, title, text });
      } catch (err) {
      console.warn('分享被取消或失败', err);
      }
    } else {
      fallbackShare(url);
    }
};

return (
    &lt;button onClick={handleShare}&gt;
      分享
    &lt;/button&gt;
);
}</pre>
</div>
<div>
<div>
<h3 data-id="heading-43">🚀 优势</h3>
<ul>
<li><strong>原生体验</strong>:使用系统分享面板,用户熟悉且支持所有已安装应用。</li>
<li><strong>无需第三方 SDK</strong>:避免集成微信、微博等 SDK 的复杂性。</li>
<li><strong>隐私友好</strong>:不收集用户分享行为数据(除非你自己上报)。</li>
</ul>
<hr>
<h3 data-id="heading-44">📌 小贴士</h3>
<ul>
<li>测试时可在 Chrome DevTools 的&nbsp;<strong>Device Mode(设备模拟)</strong> &nbsp;中查看分享弹窗。</li>
<li>在 PWA 中使用效果最佳,可实现“类原生”分享体验。</li>
</ul>
<h2 data-id="heading-45">5. Wake Lock</h2>
<p><code>Wake Lock API</code> 是一个现代 Web API,允许网页<strong>防止设备进入休眠状态</strong>(如屏幕变暗、锁屏),常用于需要长时间保持活跃的场景,例如:</p>
<ul>
<li>视频播放器(避免播放时屏幕关闭)</li>
<li>导航应用(持续显示路线)</li>
<li>扫码/AR 应用(保持摄像头活跃)</li>
<li>阅读器/电子书(长时间阅读不锁屏)</li>
</ul>
<h3 data-id="heading-46">🔒 两种锁类型(目前主要支持&nbsp;<code>screen</code>)</h3>
</div>
<br>
<div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 1. Screen Wake Lock(屏幕唤醒锁) ← 当前唯一广泛支持的类型
// 2. System Wake Lock(系统唤醒锁) ← 尚未标准化,基本不可用</pre>
</div>
<blockquote>
<p>目前&nbsp;只有&nbsp;<code>screen</code>&nbsp;类型&nbsp;在主流浏览器中可用。</p>
</blockquote>
<hr>
<h3 data-id="heading-47">✅ 基本用法(Screen Wake Lock)</h3>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">let wakeLock = null;

async function requestWakeLock() {
try {
    // 请求屏幕唤醒锁
    wakeLock = await navigator.wakeLock.request('screen');
    console.log('Wake Lock 已激活');

    // 监听释放事件(如页面隐藏、用户锁屏)
    wakeLock.addEventListener('release', () =&gt; {
      console.log('Wake Lock 已释放');
    });

} catch (err) {
    console.error('Wake Lock 请求失败:', err);
}
}

// 在用户交互后调用(如点击按钮)
document.getElementById('keepAwakeBtn').addEventListener('click', requestWakeLock);</pre>
</div>
<blockquote>
<p>⚠️&nbsp;必须由用户手势触发(如&nbsp;<code>click</code>),不能在页面加载时自动请求。</p>
</blockquote>
<hr>
<h3 data-id="heading-48">🛑 释放锁(可选,通常自动释放)</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">if (wakeLock) {
await wakeLock.release(); // 显式释放
wakeLock = null;
}</pre>
</div>
<blockquote>
<p>锁会在以下情况自动释放:</p>
<ul>
<li>页面进入后台(<code>visibilitychange</code>&nbsp;→&nbsp;<code>hidden</code>)</li>
<li>浏览器标签页关闭</li>
<li>用户手动锁屏</li>
<li>页面失去焦点(部分浏览器)</li>
</ul>
</blockquote>
<hr>
<h3 data-id="heading-49">🌐 浏览器兼容性(截至 2025 年)</h3>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260105105159569-1670019793.png" alt="ScreenShot_2026-01-05_105153_319" loading="lazy"></p>
<p>&nbsp;</p>
<div>
<div>
<blockquote>
<p><strong>移动端 Chrome(Android)支持最好</strong>,iOS Safari <strong>完全不支持</strong>。</p>
</blockquote>
<p>可通过 caniuse.com/wake-lock 查看最新状态。</p>
<hr>
<h3 data-id="heading-50">🛡️ 安全与权限要求</h3>
<ul>
<li><strong>必须在 HTTPS 下使用</strong>(localhost 除外)</li>
<li><strong>必须由用户手势触发</strong>(如点击、触摸)</li>
<li><strong>仅在页面可见时有效</strong>(页面切到后台会自动释放)</li>
<li><strong>不会绕过系统锁屏密码</strong>,仅防止屏幕变暗/休眠</li>
</ul>
<hr>
<h3 data-id="heading-51">💡 实际应用场景示例</h3>
<h4 data-id="heading-52">场景:视频播放时不锁屏</h4>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const video = document.querySelector('video');

video.addEventListener('play', async () =&gt; {
if ('wakeLock' in navigator) {
    try {
      wakeLock = await navigator.wakeLock.request('screen');
    } catch (err) {
      console.warn('无法保持屏幕常亮:', err);
    }
}
});

video.addEventListener('pause', () =&gt; {
if (wakeLock) wakeLock.release();
});</pre>
</div>
<h4 data-id="heading-53">场景:结合 Page Visibility 自动管理</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">document.addEventListener('visibilitychange', () =&gt; {
if (document.hidden &amp;&amp; wakeLock) {
    wakeLock.release(); // 页面隐藏时主动释放
}
});</pre>
</div>
<div>
<div>
<h3 data-id="heading-54">🔄 降级方案(Fallback)</h3>
<p>在不支持 Wake Lock 的环境(如 iOS):</p>
<ul>
<li>提示用户“请手动关闭自动锁屏”</li>
<li>使用全屏 API(<code>requestFullscreen()</code>)有时可延长屏幕活跃时间(非可靠)</li>
<li>对于视频,可尝试使用&nbsp;<code>&lt;video playsinline webkit-playsinline&gt;</code>&nbsp;等属性优化体验</li>
</ul>
<hr>
<h3 data-id="heading-55">📌 注意事项</h3>
<ul>
<li><strong>不要滥用</strong>:长时间保持唤醒会显著增加耗电。</li>
<li><strong>始终提供关闭选项</strong>:让用户能手动禁用“保持唤醒”。</li>
<li><strong>测试真实设备</strong>:模拟器行为可能与真机不同。</li>
</ul>
<hr>
<h3 data-id="heading-56">🔍 检测是否支持</h3>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">if ('wakeLock' in navigator) {
// 支持 Wake Lock API
}</pre>
</div>
<div>
<h2 data-id="heading-57">6. Broadcast Channel</h2>
<p><code>BroadcastChannel</code> 是一个现代 Web API,允许<strong>同源(same-origin)的不同浏览器上下文</strong>(如多个标签页、iframe、Web Worker)之间进行<strong>简单、高效的跨文档通信</strong>。 它类似于“发布-订阅”模式:一个上下文发送消息,所有监听同一频道的其他上下文都能收到。</p>
<hr>
<h3 data-id="heading-58">🧩 基本用法</h3>
<h4 data-id="heading-59">1. 创建频道并监听消息</h4>
<div class="code-block-extension-header">
<div class="code-block-extension-headerLeft">
<div class="code-block-extension-foldBtn">
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 所有页面/worker 使用相同的频道名
const channel = new BroadcastChannel('my-app-channel');

// 监听来自其他上下文的消息
channel.addEventListener('message', (event) =&gt; {
console.log('收到消息:', event.data);
});

// 或使用 onmessage
// channel.onmessage = (event) =&gt; { ... };</pre>
</div>
<h4 data-id="heading-60">2. 发送消息</h4>
</div>
</div>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 任意同源页面或 worker 中
channel.postMessage({ type: 'USER_LOGIN', userId: 123 });</pre>
</div>
<h4 data-id="heading-61">3. 关闭频道(可选,推荐在页面卸载时调用)</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">window.addEventListener('beforeunload', () =&gt; {
channel.close(); // 释放资源
});</pre>
</div>
<div>
<div>
<blockquote>
<p>✅ <strong>自动广播</strong>:消息会发送给<strong>所有</strong>监听 <code>'my-app-channel'</code> 的同源上下文(包括发送者自己,除非你过滤)。</p>
</blockquote>
<hr>
<h3 data-id="heading-62">🔐 安全限制</h3>
<ul>
<li>
<p><strong>同源策略</strong>:只有协议 + 域名 + 端口完全相同的页面才能通信。</p>
<ul>
<li><code>https://example.com/page1</code>&nbsp;和&nbsp;<code>https://example.com/page2</code>&nbsp;✅</li>
<li><code>https://example.com</code>&nbsp;和&nbsp;<code>https://sub.example.com</code>&nbsp;❌</li>
<li><code>http://localhost:3000</code>&nbsp;和&nbsp;<code>http://localhost:8080</code>&nbsp;❌</li>
</ul>
</li>
<li>
<p><strong>不支持跨域</strong>:不能用于跨域 iframe 通信(此时应考虑 <code>postMessage</code> + <code>origin</code> 验证)。</p>
</li>
</ul>
<hr>
<h3 data-id="heading-63">✅ 典型应用场景</h3>
<h4 data-id="heading-64">1.&nbsp;<strong>用户登录/登出同步</strong></h4>
<p>当用户在一个标签页登录,其他标签页自动更新状态:</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 登录页
channel.postMessage({ type: 'AUTH_CHANGED', user: { id: 1, name: 'Alice' } });

// 其他页面
channel.onmessage = (e) =&gt; {
if (e.data.type === 'AUTH_CHANGED') {
    if (e.data.user) {
      updateUI(e.data.user); // 显示用户信息
    } else {
      logoutAllTabs(); // 用户登出
    }
}
};</pre>
</div>
<div>
<div>
<h4 data-id="heading-65">2.&nbsp;<strong>多标签页状态同步</strong></h4>
<ul>
<li>购物车变更</li>
<li>主题切换(深色/浅色模式)</li>
<li>语言切换</li>
</ul>
<h4 data-id="heading-66">3.&nbsp;<strong>通知其他标签页刷新数据</strong></h4>
<p>例如后台管理页更新后,通知前台页面重新拉取配置。</p>
<h4 data-id="heading-67">4.&nbsp;<strong>与 Web Worker 通信</strong></h4>
<p>主线程和多个 worker 可通过 BroadcastChannel 广播消息。</p>
<hr>
<h3 data-id="heading-68">🌐 浏览器兼容性(截至 2025 年)</h3>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260105105608254-806763365.png" alt="ScreenShot_2026-01-05_105603_663" loading="lazy"></p>
<div>
<div>
<blockquote>
<p>⚠️ <strong>Safari 在 15.4 之前完全不支持</strong>,如需兼容旧版 iOS,需使用 <code>localStorage</code> + <code>storage</code> 事件作为 fallback。</p>
</blockquote>
<hr>
<h3 data-id="heading-69">🔄 降级方案(Fallback for older browsers)</h3>
<p>利用 <code>localStorage</code> 的 <code>storage</code> 事件实现类似广播:</p>
</div>
<br>
<div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 发送消息(fallback)
function broadcastFallback(message) {
localStorage.setItem('broadcast-msg', JSON.stringify({
    ...message,
    timestamp: Date.now()
}));
}

// 接收消息(其他标签页会触发 storage 事件)
window.addEventListener('storage', (e) =&gt; {
if (e.key === 'broadcast-msg') {
    const message = JSON.parse(e.newValue);
    console.log('Fallback 收到:', message);
}
});</pre>
</div>
<blockquote>
<p>缺点:只能传递字符串,且&nbsp;<code>storage</code>&nbsp;事件不会在当前标签页触发(正好避免自己收到自己发的消息)。</p>
</blockquote>
<hr>
<h3 data-id="heading-70">🆚 与其他通信方式对比</h3>
</div>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260105105642431-232859617.png" alt="ScreenShot_2026-01-05_105637_931" loading="lazy"></p>
<div>
<div>
<h3 data-id="heading-71">💡 小技巧</h3>
<ul>
<li><strong>避免无限循环</strong>:如果多个页面都响应消息并再次广播,可能形成循环。建议使用&nbsp;<code>type</code>&nbsp;字段区分消息来源或添加防重机制。</li>
<li><strong>结构化克隆</strong>:<code>postMessage</code>&nbsp;支持传输&nbsp;<code>ArrayBuffer</code>、<code>Blob</code>、<code>Map</code>&nbsp;等(遵循结构化克隆算法),不只是 JSON。</li>
</ul>
<hr>
<h3 data-id="heading-72">📦 在框架中使用(React 示例)</h3>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">import { useEffect } from 'react';

function useBroadcastChannel(channelName, onMessage) {
useEffect(() =&gt; {
    const channel = new BroadcastChannel(channelName);
    channel.onmessage = onMessage;

    return () =&gt; {
      channel.close();
    };
}, );
}

// 使用
function App() {
useBroadcastChannel('theme-channel', (e) =&gt; {
    if (e.data.type === 'THEME_CHANGE') {
      document.body.className = e.data.theme;
    }
});

const changeTheme = (theme) =&gt; {
    new BroadcastChannel('theme-channel').postMessage({
      type: 'THEME_CHANGE',
      theme
    });
};

return &lt;button onClick={() =&gt; changeTheme('dark')}&gt;切换深色&lt;/button&gt;;
}</pre>
</div>
<p><code>BroadcastChannel和 Vuex / Redux</code></p>
<h4 data-id="heading-73">🔍 核心区别</h4>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260105105714347-1312500288.png" alt="ScreenShot_2026-01-05_105709_802" loading="lazy"></p>
<p>&nbsp;</p>
<div>
<div>
<h4 data-id="heading-74">🧩 举个例子说明差异</h4>
<h5 data-id="heading-75">场景:用户登录后,所有打开的标签页都要显示用户名</h5>
<ul>
<li>
<p><strong>用 <code>BroadcastChannel</code></strong>:</p>
<ul>
<li>标签页 A 登录 → 通过&nbsp;<code>channel.postMessage({ type: 'LOGIN', user })</code>&nbsp;广播。</li>
<li>标签页 B、C(即使没用 Vue/React)监听到消息 →&nbsp;<strong>各自更新自己的 UI</strong>。</li>
<li>每个页面<strong>独立维护自己的状态</strong>,只是通过消息“同步”了登录事件。</li>
</ul>
</li>
<li>
<p><strong>用 Vuex</strong>:</p>
<ul>
<li>只在<strong>当前标签页内</strong>,多个 Vue 组件共享&nbsp;<code>store.state.user</code>。</li>
<li>标签页 A 的 Vuex&nbsp;<strong>无法直接影响</strong>标签页 B 的 Vuex。</li>
<li>如果你打开两个标签页,它们有<strong>两个完全独立的 Vuex 实例</strong>。</li>
</ul>
</li>
</ul>
<blockquote>
<p>✅ 所以:<strong>Vuex 管“页面内”,BroadcastChannel 管“页面间”</strong> 。</p>
</blockquote>
<hr>
<h4 data-id="heading-76">🤝 它们可以结合使用!</h4>
<p>实际项目中,<strong>两者常配合使用</strong>:</p>
</div>
<br>
<div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 在 Vuex 的 action 中监听 BroadcastChannel
const channel = new BroadcastChannel('auth-channel');

const store = new Vuex.Store({
state: { user: null },
mutations: {
    SET_USER(state, user) {
      state.user = user;
    }
},
actions: {
    login({ commit }, user) {
      commit('SET_USER', user);
      // 登录后广播给其他标签页
      channel.postMessage({ type: 'LOGIN', user });
    }
}
});

// 监听其他标签页的登录/登出
channel.onmessage = (e) =&gt; {
if (e.data.type === 'LOGIN') {
    store.commit('SET_USER', e.data.user); // 更新当前页状态
} else if (e.data.type === 'LOGOUT') {
    store.commit('SET_USER', null);
}
};</pre>
</div>
<div>
<div>
<p>这样:</p>
<ul>
<li>页面内:Vuex 管理状态,组件自动响应。</li>
<li>页面间:BroadcastChannel 同步关键事件。</li>
</ul>
<hr>
<h4 data-id="heading-77">❓那有没有“跨标签页的 Vuex”?</h4>
<p>有!社区有一些库尝试结合两者,例如:</p>
<ul>
<li><code>vuex-shared-mutations</code>:通过&nbsp;<code>localStorage</code>&nbsp;或&nbsp;<code>BroadcastChannel</code>&nbsp;同步 Vuex 的 mutations。</li>
<li>自定义方案:监听&nbsp;<code>storage</code>&nbsp;事件或&nbsp;<code>BroadcastChannel</code>,触发本地 store 更新。</li>
</ul>
<p>但核心思想不变:<strong>跨标签页通信靠 BroadcastChannel(或 storage),状态管理靠 Vuex</strong>。</p>
<hr>
<h4 data-id="heading-78">✅ 总结</h4>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260105105751569-2144810886.png" alt="ScreenShot_2026-01-05_105747_818" loading="lazy"></p>
<p>&nbsp;</p>
<div>
<div>
<h2 data-id="heading-79">7. PerformanceObserver</h2>
<p><code>PerformanceObserver</code> 是一个强大的 Web API,用于<strong>异步监听性能相关的事件和指标</strong>,而无需轮询 <code>performance.getEntries()</code>。它是现代 Web 性能监控(如 Core Web Vitals)的核心工具。</p>
<hr>
<h3 data-id="heading-80">🎯 核心作用</h3>
<p>监听浏览器自动记录的 <strong>Performance Timeline(性能时间线)</strong> 中的新条目,例如:</p>
<ul>
<li>资源加载(<code>resource</code>)</li>
<li>导航 timing(<code>navigation</code>)</li>
<li>长任务(<code>longtask</code>)</li>
<li>元素曝光(<code>element</code>,实验性)</li>
<li><strong>最重要:</strong> &nbsp;<strong>CLS、LCP、FCP、INP 等 Web Vitals 指标</strong></li>
</ul>
<hr>
<h3 data-id="heading-81">🧩 基本用法</h3>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const observer = new PerformanceObserver((list) =&gt; {
for (const entry of list.getEntries()) {
    console.log(entry.name, entry.entryType, entry.startTime, entry.duration);
}
});

// 开始监听特定类型的性能条目
observer.observe({ entryTypes: ['resource', 'navigation', 'paint'] });</pre>
</div>
<blockquote>
<p>⚠️ 必须指定&nbsp;<code>entryTypes</code>(或&nbsp;<code>type</code>),否则不会触发回调。</p>
</blockquote>
<hr>
<h3 data-id="heading-82">🔍 常见&nbsp;<code>entryTypes</code>&nbsp;及用途</h3>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260105105845735-1167097605.png" alt="ScreenShot_2026-01-05_105840_602" loading="lazy"></p>
<blockquote>
<p>✅&nbsp;LCP、CLS、INP 等现代指标必须通过&nbsp;<code>PerformanceObserver</code>&nbsp;获取,无法通过&nbsp;<code>getEntries()</code>&nbsp;静态读取。</p>
</blockquote>
<hr>
<h3 data-id="heading-83">✅ 实战示例</h3>
<h4 data-id="heading-84">1. 监听 LCP(最大内容绘制)</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">let lcpReported = false;

new PerformanceObserver((entryList) =&gt; {
const lcpEntry = entryList.getEntries().at(-1); // 取最后一个(最准确)
if (!lcpReported) {
    console.log('LCP:', lcpEntry.startTime); // 单位:毫秒
    // 上报到分析平台
    sendToAnalytics({ metric: 'LCP', value: lcpEntry.startTime });
    lcpReported = true;
}
}).observe({ type: 'largest-contentful-paint', buffered: true });</pre>
</div>
<blockquote>
<p><code>buffered: true</code>&nbsp;表示获取已发生但未被观察到的历史条目(对 LCP/CLS 必须加!)。</p>
</blockquote>
<hr>
<h4 data-id="heading-85">2. 监听 CLS(累积布局偏移)</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">let clsValue = 0;

new PerformanceObserver((entryList) =&gt; {
for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) { // 忽略用户交互后的偏移
      clsValue += entry.value;
    }
}
console.log('当前 CLS:', clsValue);
}).observe({ type: 'layout-shift', buffered: true });</pre>
</div>
<h4 data-id="heading-86">3. 监控慢资源加载</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">new PerformanceObserver((list) =&gt; {
for (const resource of list.getEntries()) {
    if (resource.duration &gt; 2000) {
      console.warn('慢资源:', resource.name, resource.duration + 'ms');
      // 上报性能问题
    }
}
}).observe({ entryTypes: ['resource'] });</pre>
</div>
<h4 data-id="heading-87">4. 捕获长任务(卡顿原因)</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">new PerformanceObserver((list) =&gt; {
for (const task of list.getEntries()) {
    if (task.duration &gt; 100) {
      console.log('长任务:', task.duration + 'ms', task.attribution);
    }
}
}).observe({ entryTypes: ['longtask'] });
</pre>
</div>
<p>需要先注册长任务支持(部分浏览器需 polyfill):</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">if (PerformanceObserver.supportedEntryTypes.includes('longtask')) {
// 启用观察
}</pre>
</div>
<div>
<div>
<h3 data-id="heading-88">🌐 浏览器兼容性</h3>
<ul>
<li>✅ Chrome / Edge:全面支持(包括 Web Vitals)</li>
<li>✅ Firefox:支持基础类型(<code>resource</code>,&nbsp;<code>navigation</code>),Web Vitals 支持较弱</li>
<li>✅ Safari 15+:支持 LCP、CLS、FCP 等核心指标</li>
<li>❌ IE:不支持</li>
</ul>
<blockquote>
<p>推荐使用 Google 的 <code>web-vitals</code> 库 跨浏览器采集 Core Web Vitals。</p>
</blockquote>
<hr>
<h3 data-id="heading-89">📦 与&nbsp;<code>performance.getEntries()</code>&nbsp;对比</h3>
</div>
<div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260105110016880-1391118649.png" alt="ScreenShot_2026-01-05_110013_030" loading="lazy"></p>
<div>
<div>
<blockquote>
<p>✅ <strong>现代性能监控应优先使用 <code>PerformanceObserver</code></strong>。</p>
</blockquote>
<hr>
<h3 data-id="heading-90">🚀 最佳实践</h3>
<ol>
<li><strong>尽早注册</strong>:在&nbsp;<code>&lt;head&gt;</code>&nbsp;中或页面顶部初始化,避免漏掉早期指标。</li>
<li><strong>使用&nbsp;<code>buffered: true</code></strong>:确保捕获 FCP、LCP、CLS 等可能在监听前已发生的指标。</li>
<li><strong>避免内存泄漏</strong>:通常不需要&nbsp;<code>disconnect()</code>,因为性能条目是一次性的。</li>
<li><strong>结合 RUM(真实用户监控)</strong> :将数据上报到分析平台(如 GA4、Sentry、自建服务)。</li>
</ol><hr>
<h3 data-id="heading-91">🛠️ 工具推荐</h3>
<ul>
<li><code>web-vitals</code>&nbsp;npm 包:Google 官方封装,一行代码获取 Web Vitals。</li>
</ul>
</div>
<div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">import { getLCP, getCLS, getFCP } from 'web-vitals';
getLCP(console.log);</pre>
</div>
<p>React(使用 Hook)&nbsp;和&nbsp;Vue 3(使用 Composition API)</p>
<h3 data-id="heading-92">✅ 共同前提</h3>
<p>我们使用 Google 官方的&nbsp;<code>web-vitals</code>&nbsp;库,它已封装好&nbsp;<code>PerformanceObserver</code>&nbsp;的兼容逻辑。</p>
</div>
</div>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">npm install web-vitals</pre>
</div>
<h3 data-id="heading-93">🟦 React 版本:<code>useWebVitals</code></h3>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// hooks/useWebVitals.ts
import { useEffect } from 'react';
import { getCLS, getFCP, getLCP, getFID, getINP } from 'web-vitals';

type WebVitalsMetric = {
id: string;
name: string;
value: number;
delta: number;
entries: PerformanceEntry[];
attribution: Record&lt;string, unknown&gt;;
};

type WebVitalsOptions = {
onReport?: (metric: WebVitalsMetric) =&gt; void;
reportAll?: boolean; // 是否上报所有指标(默认只上报一次)
};

export const useWebVitals = ({
onReport,
reportAll = false
}: WebVitalsOptions = {}) =&gt; {
useEffect(() =&gt; {
    // 定义上报函数
    const report = (metric: WebVitalsMetric) =&gt; {
      onReport?.(metric);
      if (process.env.NODE_ENV === 'development') {
      console.log('Web Vitals:', metric);
      }
    };

    // 启动监听(Web Vitals 内部使用 PerformanceObserver)
    getCLS(report, reportAll);
    getFCP(report, reportAll);
    getLCP(report, reportAll);
    getFID(report); // FID 只触发一次
    getINP(report, reportAll); // INP 替代 FID(未来标准)

    // 注意:web-vitals 的指标是自动管理生命周期的,无需 cleanup
}, );
};</pre>
</div>
<h4 data-id="heading-94">📌 使用示例</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// App.tsx
import { useWebVitals } from './hooks/useWebVitals';

function App() {
useWebVitals({
    onReport: (metric) =&gt; {
      // 上报到分析平台(如 GA4、Sentry、自建 API)
      fetch('/api/performance', {
      method: 'POST',
      body: JSON.stringify(metric),
      headers: { 'Content-Type': 'application/json' }
      });
    }
});

return &lt;div&gt;你的应用&lt;/div&gt;;
}</pre>
</div>
<blockquote>
<p>✅&nbsp;优点:自动处理浏览器兼容性、只上报有效指标、支持开发环境日志。</p>
</blockquote>
<hr>
<h3 data-id="heading-95">🟩 Vue 3 版本:<code>useWebVitals</code></h3>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// composables/useWebVitals.ts
import { onMounted } from 'vue';
import { getCLS, getFCP, getLCP, getFID, getINP } from 'web-vitals';

type WebVitalsMetric = {
id: string;
name: string;
value: number;
delta: number;
entries: PerformanceEntry[];
attribution: Record&lt;string, unknown&gt;;
};

export function useWebVitals(
onReport?: (metric: WebVitalsMetric) =&gt; void,
reportAll = false
) {
onMounted(() =&gt; {
    const report = (metric: WebVitalsMetric) =&gt; {
      onReport?.(metric);
      if (import.meta.env.DEV) {
      console.log('Web Vitals:', metric);
      }
    };

    getCLS(report, reportAll);
    getFCP(report, reportAll);
    getLCP(report, reportAll);
    getFID(report);
    getINP(report, reportAll);
});
}</pre>
</div>
<h4 data-id="heading-96">📌 使用示例</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;!-- App.vue --&gt;
&lt;script setup&gt;
import { useWebVitals } from './composables/useWebVitals';

useWebVitals((metric) =&gt; {
fetch('/api/performance', {
    method: 'POST',
    body: JSON.stringify(metric),
    headers: { 'Content-Type': 'application/json' }
});
});
&lt;/script&gt;

&lt;template&gt;
&lt;div&gt;你的应用&lt;/div&gt;
&lt;/template&gt;</pre>
</div>
<h3 data-id="heading-97">🧩 高级:监控慢资源加载(自定义 PerformanceObserver)</h3>
<p>如果你还想监控 JS/CSS/图片等资源加载性能,可以额外封装一个 Hook:</p>
<h4 data-id="heading-98">React:&nbsp;<code>useResourcePerformance</code></h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// hooks/useResourcePerformance.ts
import { useEffect } from 'react';

export const useResourcePerformance = (onSlowResource: (entry: PerformanceResourceTiming) =&gt; void) =&gt; {
useEffect(() =&gt; {
    if (!PerformanceObserver.supportedEntryTypes.includes('resource')) return;

    const observer = new PerformanceObserver((list) =&gt; {
      for (const entry of list.getEntries() as PerformanceResourceTiming[]) {
      if (entry.duration &gt; 2000) {
          onSlowResource(entry);
      }
      }
    });

    observer.observe({ entryTypes: ['resource'] });

    return () =&gt; {
      observer.disconnect();
    };
}, );
};</pre>
</div>
<div>
<div>
<p>ue 版本类似,用 <code>onMounted</code> + <code>onUnmounted</code> 管理生命周期。</p>
<hr>
<h3 data-id="heading-99">📊 上报建议</h3>
<ul>
<li><strong>LCP、FCP、CLS</strong>:每个页面会话上报一次(<code>reportAll: false</code>)。</li>
<li><strong>INP/FID</strong>:用户每次交互可能触发,可采样上报。</li>
<li><strong>慢资源</strong>:可聚合后批量上报,避免频繁请求。</li>
</ul>
<hr>
<h3 data-id="heading-100">🚀 部署提示</h3>
<ul>
<li>在&nbsp;<strong>生产环境</strong>&nbsp;使用,开发环境仅用于调试。</li>
<li>避免阻塞主渲染逻辑(<code>web-vitals</code>&nbsp;是异步非阻塞的)。</li>
<li>配合&nbsp;Google Analytics 4 的 Web Vitals 自动采集&nbsp;更省事。</li>
</ul>
<h2 data-id="heading-101">8. requestIdleCallback</h2>
<p><code>requestIdleCallback</code> 是一个浏览器提供的 API,用于<strong>在浏览器主线程空闲时执行低优先级任务</strong>,避免影响关键操作(如用户输入、动画、布局等),从而提升页面流畅性和响应性。</p>
<blockquote>
<p>💡 它是实现“<strong>协作式调度(Cooperative Scheduling)</strong> ”的关键工具,React 16+ 的 Fiber 架构就受其启发(尽管 React 最终未直接使用它)。</p>
</blockquote>
<hr>
<h3 data-id="heading-102">🧩 基本用法</h3>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">function doLowPriorityWork(deadline) {
// deadline.timeRemaining():返回当前空闲时段还剩多少毫秒(通常 &lt; 50ms)
// deadline.didTimeout:是否因超时而强制执行(配合 timeout 使用)

while (deadline.timeRemaining() &gt; 0 || deadline.didTimeout) {
    if (hasWork()) {
      performUnitOfWork();
    } else {
      break; // 没有更多工作,退出
    }
}

// 如果还有剩余任务,继续调度
if (hasMoreWork()) {
    requestIdleCallback(doLowPriorityWork);
}
}

// 启动任务
requestIdleCallback(doLowPriorityWork, { timeout: 2000 });</pre>
</div>
<div>
<div>
<h3 data-id="heading-103">⚙️ 参数说明</h3>
<h4 data-id="heading-104">1. 回调函数参数:<code>deadline</code></h4>
<ul>
<li><code>deadline.timeRemaining()</code>:返回一个<strong>估算值</strong>(单位:毫秒),表示当前帧剩余的空闲时间(通常 ≤ 50ms)。</li>
<li><code>deadline.didTimeout</code>:如果设置了&nbsp;<code>timeout</code>&nbsp;且超时,则为&nbsp;<code>true</code>,此时应尽快完成任务。</li>
</ul>
<h4 data-id="heading-105">2. 可选配置对象</h4>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">{
timeout: 2000 // 最大等待时间(毫秒)。超时后即使没有空闲也会执行回调。
}</pre>
</div>
<blockquote>
<p>⚠️&nbsp;<code>timeout</code>&nbsp;会降低优先级优势,仅用于“最终必须执行”的兜底场景。</p>
</blockquote>
<hr>
<h3 data-id="heading-106">✅ 典型应用场景</h3>
<h4 data-id="heading-107">1.&nbsp;非关键数据预加载</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">requestIdleCallback(() =&gt; {
// 预加载下一页数据、图片、代码分割 chunk
import('./NextPageComponent');
});</pre>
</div>
<h4 data-id="heading-108">2.&nbsp;埋点/日志批量上报</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">let logs = [];

function sendLogs() {
if (logs.length &gt; 0) {
    navigator.sendBeacon('/log', JSON.stringify(logs));
    logs = [];
}
}

function addLog(event) {
logs.push(event);
requestIdleCallback(sendLogs, { timeout: 5000 });
}</pre>
</div>
<h4 data-id="heading-109">3.&nbsp;大型列表虚拟滚动的缓存计算</h4>
<p>在用户停止滚动后,利用空闲时间预计算可视区域外的 item 尺寸。</p>
<h4 data-id="heading-110">4.&nbsp;分析用户行为(非实时)</h4>
<p>如统计停留时长、点击热力图聚合等。</p>
<hr>
<h3 data-id="heading-111">🌐 浏览器兼容性(截至 2025 年)</h3>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260105110307523-1387738920.png" alt="ScreenShot_2026-01-05_110302_089" loading="lazy"></p>
<div>
<div>
<blockquote>
<p>🔥 <strong>现实:仅 Chrome/Edge 支持,Firefox 和 Safari 永远不会支持!</strong></p>
</blockquote>
<p>可通过 caniuse.com/requestidle… 查看。</p>
<hr>
<h3 data-id="heading-112">🔄 降级方案(Polyfill / 替代方案)</h3>
<p>由于兼容性差,<strong>生产环境必须提供 fallback</strong>。</p>
<h4 data-id="heading-113">方案 1:使用&nbsp;<code>setTimeout</code>&nbsp;模拟(简单但不精确)</h4>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const requestIdleCallback =
window.requestIdleCallback ||
function (callback) {
    const start = Date.now();
    return setTimeout(() =&gt; {
      callback({
      didTimeout: false,
      timeRemaining: () =&gt; Math.max(0, 50 - (Date.now() - start))
      });
    }, 1);
};

const cancelIdleCallback =
window.cancelIdleCallback ||
function (id) {
    clearTimeout(id);
};</pre>
</div>
<div>
<div>
<h4 data-id="heading-114">方案 2:使用&nbsp;<code>requestAnimationFrame</code>&nbsp;+ 时间切片(更接近原生行为)</h4>
<p>适用于需要精细控制的任务调度(如 React Fiber 的思路)。</p>
<h4 data-id="heading-115">方案 3:直接使用&nbsp;<code>setTimeout(fn, 0)</code>&nbsp;或&nbsp;<code>queueMicrotask</code></h4>
<p>适用于非关键但需异步执行的任务,但无法利用“空闲时间”。</p>
<hr>
<h3 data-id="heading-116">⚠️ 注意事项</h3>
<ol>
<li><strong>不要执行高优先级任务</strong>:如用户输入响应、动画更新。</li>
<li><strong>避免长时间运行</strong>:即使&nbsp;<code>timeRemaining()</code>&nbsp;返回较大值,也应分片处理。</li>
<li><strong>不要依赖精确时间</strong>:<code>timeRemaining()</code>&nbsp;是估算值,可能突然变为 0。</li>
<li><strong>移动端效果有限</strong>:低端设备空闲时间极少,可能长期不触发。</li>
</ol><hr>
<h3 data-id="heading-117">🆚 与&nbsp;<code>requestAnimationFrame</code>&nbsp;对比</h3>
</div>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260105110424283-895234158.png" alt="ScreenShot_2026-01-05_110419_984" loading="lazy"></p>
<div>
<div>
<blockquote>
<p>✅ 两者互补:<code>rAF</code> 保证流畅动画,<code>rIC</code> 避免阻塞动画。</p>
</blockquote>
<hr>
<h3 data-id="heading-118">📦 在现代框架中的使用</h3>
<ul>
<li><strong>React</strong>:内部调度器受&nbsp;<code>rIC</code>&nbsp;启发,但使用自定义实现(因兼容性问题)。</li>
<li><strong>Vue / Svelte</strong>:一般不直接使用,但可用于自定义性能优化逻辑。</li>
<li><strong>推荐</strong>:在业务代码中谨慎使用,并做好降级。</li>
</ul>
<hr>
<h3 data-id="heading-119">✅ 最佳实践模板</h3>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">function scheduleIdleWork(workFn, timeout = 2000) {
if ('requestIdleCallback' in window) {
    return requestIdleCallback((deadline) =&gt; {
      if (deadline.timeRemaining() &gt; 0 || deadline.didTimeout) {
      workFn();
      }
    }, { timeout });
} else {
    // fallback: 稍后执行(不阻塞当前任务)
    return setTimeout(workFn, 0);
}
}

// 使用
const id = scheduleIdleWork(() =&gt; {
console.log('在空闲时执行');
});

// 取消(如组件卸载时)
// cancelIdleCallback(id) 或 clearTimeout(id)</pre>
</div>
<div>
<div>
<h3 data-id="heading-120">🔚 总结</h3>
<ul>
<li>
<p><strong>作用</strong>:在浏览器空闲时执行低优先级任务,提升用户体验。</p>
</li>
<li>
<p><strong>现状</strong>:<strong>仅 Chrome/Edge 支持</strong>,Firefox/Safari 已放弃。</p>
</li>
<li>
<p><strong>建议</strong>:</p>
<ul>
<li>可用于<strong>非关键优化</strong>(如预加载、日志上报)。</li>
<li><strong>必须提供降级方案</strong>。</li>
<li>不要用于核心功能。</li>
</ul>
</li>
</ul>
<h2 data-id="heading-121">9.AbortController</h2>
<p><code>AbortController</code> 是 Web 平台提供的一个标准接口,用于<strong>中止(取消)一个或多个异步操作</strong>,比如 <code>fetch()</code> 请求、定时器、自定义任务等。它提供了一种统一、可组合的方式来处理取消逻辑,避免内存泄漏或无效操作。</p>
<hr>
<h3 data-id="heading-122">🧠 核心概念</h3>
<ul>
<li><strong><code>AbortController</code></strong>:控制器对象,用于触发中止。</li>
<li><strong><code>AbortSignal</code></strong>:信号对象,与控制器关联,传递“是否已中止”的状态,并可监听&nbsp;<code>abort</code>&nbsp;事件。</li>
</ul>
<hr>
<h3 data-id="heading-123">✅ 基本用法</h3>
<h4 data-id="heading-124">1. 创建控制器和信号</h4>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const controller = new AbortController();
const signal = controller.signal; // 只读的 AbortSignal</pre>
</div>
<h4 data-id="heading-125">2. 监听中止信号(在异步操作中)</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 示例:自定义异步任务
function myAsyncTask(signal) {
return new Promise((resolve, reject) =&gt; {
    // 检查是否已经中止
    if (signal.aborted) {
      reject(new DOMException('操作已中止', 'AbortError'));
      return;
    }

    // 监听中止事件
    signal.addEventListener('abort', () =&gt; {
      reject(new DOMException('操作已中止', 'AbortError'));
    });

    // 模拟异步操作
    const timer = setTimeout(() =&gt; {
      resolve('任务完成');
    }, 3000);

    // 可选:在中止时清理资源
    signal.addEventListener('abort', () =&gt; {
      clearTimeout(timer);
    });
});
}</pre>
</div>
<h4 data-id="heading-126">3. 触发中止</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">myAsyncTask(controller.signal)
.then(console.log)
.catch(e =&gt; {
    if (e.name === 'AbortError') {
      console.log('任务被用户取消');
    } else {
      console.error('其他错误', e);
    }
});

// 1 秒后取消
setTimeout(() =&gt; {
controller.abort(); // 触发 abort 事件,signal.aborted 变为 true
}, 1000);</pre>
</div>
<h3 data-id="heading-127">🌐 实际应用场景</h3>
<h4 data-id="heading-128">1.&nbsp;取消&nbsp;<code>fetch</code>&nbsp;请求(最常见)</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const controller = new AbortController();

fetch('/api/data', { signal: controller.signal })
.then(res =&gt; res.json())
.then(data =&gt; console.log(data))
.catch(err =&gt; {
    if (err.name === 'AbortError') {
      console.log('请求被取消');
    } else {
      console.error('网络错误', err);
    }
});

// 取消请求
controller.abort();</pre>
</div>
<blockquote>
<p>✅ 所有现代浏览器都支持&nbsp;<code>fetch</code>&nbsp;的&nbsp;<code>signal</code>&nbsp;选项。</p>
</blockquote>
<hr>
<h4 data-id="heading-129">2.&nbsp;取消多个操作(一对多)</h4>
<p>一个&nbsp;<code>AbortController</code>&nbsp;可以控制多个异步任务:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const controller = new AbortController();

fetch('/api/1', { signal: controller.signal });
fetch('/api/2', { signal: controller.signal });
myAsyncTask(controller.signal);

// 一键取消所有
controller.abort();</pre>
</div>
<h4 data-id="heading-130">3.&nbsp;与&nbsp;<code>setTimeout</code>&nbsp;/&nbsp;<code>setInterval</code>&nbsp;结合</h4>
<p>虽然&nbsp;<code>setTimeout</code>&nbsp;本身不支持&nbsp;<code>signal</code>,但可以手动集成:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">function delay(ms, signal) {
return new Promise((resolve, reject) =&gt; {
    if (signal?.aborted) {
      reject(new DOMException('已中止', 'AbortError'));
      return;
    }

    const id = setTimeout(resolve, ms);
    signal?.addEventListener('abort', () =&gt; {
      clearTimeout(id);
      reject(new DOMException('已中止', 'AbortError'));
    });
});
}

// 使用
const ctrl = new AbortController();
delay(5000, ctrl.signal).catch(console.error);
ctrl.abort(); // 立即取消</pre>
</div>
<div>
<div>
<h3 data-id="heading-131">🔁 与&nbsp;<code>TaskController</code>(来自&nbsp;<code>scheduler.postTask</code>)的关系</h3>
<ul>
<li><code>TaskController</code>&nbsp;是&nbsp;<code>AbortController</code>&nbsp;的<strong>子类</strong>,专为调度任务设计。</li>
<li>它额外支持&nbsp;<code>priority</code>&nbsp;设置,并返回&nbsp;<code>TaskSignal</code>(继承自&nbsp;<code>AbortSignal</code>)。</li>
<li>因此,<code>AbortController</code>&nbsp;是更通用的取消机制,而&nbsp;<code>TaskController</code>&nbsp;是其在任务调度场景下的扩展。</li>
</ul>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// TaskController 用法(实验性)
const taskCtrl = new TaskController({ priority: 'background' });
scheduler.postTask(myTask, { signal: taskCtrl.signal });

// 也可以直接 abort()
taskCtrl.abort();</pre>
</div>
<div>
<div>
<h3 data-id="heading-132">⚠️ 注意事项</h3>
<ul>
<li><code>abort()</code>&nbsp;<strong>只能调用一次</strong>,多次调用无副作用。</li>
<li>中止后,<code>signal.aborted</code>&nbsp;永远为&nbsp;<code>true</code>。</li>
<li>被中止的操作<strong>不会自动停止</strong>,你需要在代码中<strong>主动监听并清理资源</strong>(如清除定时器、关闭流等)。</li>
<li>不要重复使用同一个&nbsp;<code>AbortController</code>&nbsp;实例处理不相关的任务,建议按逻辑分组使用。</li>
</ul>
<h3 data-id="heading-133">在React中的应用</h3>
<p>在 React 中,<code>AbortController</code> 是处理<strong>组件卸载后仍可能完成的异步操作</strong>(如 <code>fetch</code> 请求、定时器、动画等)的关键工具。它的主要目的是 <strong>避免“内存泄漏”或“状态更新已卸载组件”</strong> 的警告(例如经典的 <code>Can't perform a React state update on an unmounted component</code>)。</p>
<h3 data-id="heading-134">✅ 典型使用场景</h3>
<h4 data-id="heading-135">1.&nbsp;<strong>取消数据请求(最常见)</strong></h4>
<p>当组件在请求完成前被卸载(如用户快速切换路由),应取消请求。</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">import { useEffect, useState } from 'react';

function UserProfile({ userId }) {
const = useState(null);
const = useState(true);

useEffect(() =&gt; {
    const controller = new AbortController(); // 创建控制器

    const fetchUser = async () =&gt; {
      try {
      const res = await fetch(`/api/users/${userId}`, {
          signal: controller.signal // 传入 signal
      });
      const data = await res.json();
      setUser(data);
      } catch (err) {
      if (err.name !== 'AbortError') {
          console.error('请求失败:', err);
      }
      } finally {
      setLoading(false);
      }
    };

    fetchUser();

    // 清理函数:组件卸载时中止请求
    return () =&gt; {
      controller.abort();
    };
}, );

if (loading) return &lt;div&gt;加载中...&lt;/div&gt;;
return &lt;div&gt;用户名:{user?.name}&lt;/div&gt;;
}</pre>
</div>
<blockquote>
<p>✅ 这样即使组件卸载,也不会尝试调用&nbsp;<code>setUser</code>,避免警告。</p>
</blockquote>
<hr>
<h4 data-id="heading-136">2.&nbsp;取消多个并行请求</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">useEffect(() =&gt; {
const controller = new AbortController();

Promise.all([
    fetch('/api/posts', { signal: controller.signal }),
    fetch('/api/comments', { signal: controller.signal })
])
.then(/* ... */)
.catch(err =&gt; {
    if (err.name !== 'AbortError') {
      // 处理真实错误
    }
});

return () =&gt; controller.abort();
}, []);</pre>
</div>
<h4 data-id="heading-137">3.&nbsp;结合自定义 Hook 封装</h4>
<p>可以创建一个可复用的&nbsp;<code>useAbortableFetch</code>:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// hooks/useAbortableFetch.js
import { useEffect, useState } from 'react';

export function useAbortableFetch(url) {
const = useState(null);
const = useState(true);
const = useState(null);

useEffect(() =&gt; {
    const controller = new AbortController();

    const fetchData = async () =&gt; {
      try {
      const res = await fetch(url, { signal: controller.signal });
      if (!res.ok) throw new Error('请求失败');
      const json = await res.json();
      setData(json);
      } catch (err) {
      if (err.name !== 'AbortError') {
          setError(err);
      }
      } finally {
      setLoading(false);
      }
    };

    fetchData();

    return () =&gt; controller.abort();
}, );

return { data, loading, error };
}</pre>
</div>
<p>使用:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">function App() {
const { data, loading } = useAbortableFetch('/api/data');
// ...
}</pre>
</div>
<h4 data-id="heading-138">4.&nbsp;取消定时器或动画</h4>
<p>虽然&nbsp;<code>setTimeout</code>&nbsp;不原生支持&nbsp;<code>signal</code>,但可以手动集成:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">useEffect(() =&gt; {
const controller = new AbortController();

const timer = setTimeout(() =&gt; {
    if (!controller.signal.aborted) {
      setData('更新了!');
    }
}, 3000);

return () =&gt; {
    controller.abort(); // 标记为中止
    clearTimeout(timer); // 清理定时器
};
}, []);</pre>
</div>
<p>或者封装一个支持&nbsp;<code>signal</code>&nbsp;的&nbsp;<code>delay</code>&nbsp;工具函数(见前文)。</p>
<hr>
<h4 data-id="heading-139">5.&nbsp;与 React Router(v6)结合</h4>
<p>在路由切换时自动取消请求:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 不需要额外操作!只要在 useEffect 中正确使用 AbortController,
// 路由切换导致组件卸载时,清理函数会自动执行。</pre>
</div>
<div>
<div>
<h3 data-id="heading-140">⚠️ 注意事项</h3>
<ol>
<li><strong>不要忽略 <code>AbortError</code></strong><br>
在 <code>.catch()</code> 中要判断是否是 <code>AbortError</code>,避免把“正常取消”当作错误处理。</li>
<li><strong>每个 effect 使用独立的 controller</strong><br>
避免多个 effect 共用同一个 <code>AbortController</code>,除非你明确需要批量取消。</li>
<li><strong>不适用于同步操作</strong><br>
<code>AbortController</code> 只对异步、可中断的操作有效。</li>
<li><strong>React 18 严格模式下的双重调用</strong><br>
在开发模式下,React 18 的严格模式会故意 mount → unmount → remount 组件,此时 <code>AbortController</code> 能确保第一次请求被正确取消,是<strong>正常行为</strong>,不是 bug。</li>


</ol><hr>
<h3 data-id="heading-141">🔄 替代方案(现代 React)</h3>
<ul>
<li><strong>React Query / SWR</strong>:这些数据获取库<strong>内部已集成取消逻辑</strong>,无需手动管理&nbsp;<code>AbortController</code>。</li>
<li><strong>useEffect cleanup</strong>:仍是处理取消的核心机制,<code>AbortController</code>&nbsp;是其实现细节之一。</li>


</ul>
<hr>
<h3 data-id="heading-142">✅ 总结</h3>

</div>

</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202601/2149129-20260105110741901-1374244519.png" alt="ScreenShot_2026-01-05_110736_075" loading="lazy"></p>
<p>&nbsp;</p>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19441854
頁: [1]
查看完整版本: 神级JS API,谁用谁好用