绿城绿化 發表於 2026-4-13 10:54:00

JS-前端埋点神器 navigator.sendBeacon 全指南

<h1 data-id="heading-0">🧑‍💻 写在开头</h1>
<p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<div>
<div>
<h3 data-id="heading-0">前言</h3>
<p>在前端开发中,埋点系统是必不可少的一环。我们经常需要在用户<strong>关闭页面</strong>、<strong>刷新</strong>或<strong>跳转路由</strong>时,向服务器发送最后一条统计数据(比如用户停留时长、页面跳出率)。</p>
<p>但这看似简单的需求,在实现时却危机四伏:请求发不出去?页面跳转卡顿?今天我们就来聊聊这个问题的终极解决方案 —— <code>navigator.sendBeacon</code>。</p>
<h2 data-id="heading-1">一、 痛点与传统方案的挣扎</h2>
<h3 data-id="heading-2">场景还原</h3>
<p>当用户点击关闭按钮时,浏览器会触发生命周期事件(<code>unload</code> 或 <code>visibilitychange</code>)。如果我们直接使用普通的异步 AJAX (<code>xhr</code> 或 <code>fetch</code>) 发送请求,浏览器通常会忽略它,因为页面都要销毁了,浏览器不想处理未完成的请求。</p>
<h3 data-id="heading-3">传统方案:同步 XHR</h3>
<p>为了保证数据能发出去,以前的做法是将请求改为<strong>同步(Synchronous)</strong> 。</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const syncReport = (url, { data = {}, headers = {} } = {}) =&gt; {
const xhr = new XMLHttpRequest();
// 第三个参数 false 表示同步请求
xhr.open('POST', url, false);
xhr.withCredentials = true;
Object.keys(headers).forEach((key) =&gt; {
    xhr.setRequestHeader(key, headers);
});
xhr.send(JSON.stringify(data));
};</pre>
</div>
<div>
<div>
<h3 data-id="heading-4">致命缺陷</h3>
<ol>
<li><strong>用户体验极差</strong>:同步请求会阻塞主线程。这意味着只有请求发送完成,页面才能关闭或跳转。在弱网环境下,用户会感觉页面“卡死”了。</li>
<li><strong>浏览器废弃</strong>:现代浏览器(如 Chrome)已经明确表示将在页面卸载期间禁用同步 XHR,这种方法迟早失效。</li>
</ol><hr>
<h2 data-id="heading-5">二、 救世主:navigator.sendBeacon</h2>
<h3 data-id="heading-6">1. 概念</h3>
<p><code>navigator.sendBeacon()</code> 是专门为“页面卸载时发送数据”而设计的 Web API。 它的核心能力是:<strong>将数据放入浏览器的发送队列,即使页面已经关闭,浏览器也会在后台默默完成发送。</strong></p>
<h3 data-id="heading-7">2. 核心优势</h3>
<ul>
<li><strong>可靠性高</strong>:不受页面生命周期影响,确保数据不丢失。</li>
<li><strong>非阻塞</strong>:完全异步执行,不会阻塞页面关闭或跳转,用户体验丝滑。</li>
<li><strong>低优先级</strong>:浏览器会择机发送(通常是网络空闲时),不争抢关键资源。</li>
</ul>
<h3 data-id="heading-8">3. API 语法</h3>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const result = navigator.sendBeacon(url, data);</pre>
</div>
<div>
<div>
<ul>
<li>
<p><strong><code>url</code></strong>:请求地址。</p>
</li>
<li>
<p><strong><code>data</code></strong>:要发送的数据,支持 <code>ArrayBuffer</code>、<code>ArrayBufferView</code>、<code>Blob</code>、<code>DOMString</code>、<code>FormData</code> 或 <code>URLSearchParams</code>。</p>
</li>
<li>
<p><strong><code>result</code></strong>(返回值):布尔值 (<code>true</code> / <code>false</code>)。</p>
<ul>
<li><code>true</code>:表示数据成功加入传输队列(注意:<strong>不代表服务器接收成功</strong>)。</li>
<li><code>false</code>:表示队列已满,无法加入。</li>
</ul>
</li>
</ul>
<hr>
<h2 data-id="heading-9">三、 实战:三种常见发送姿势</h2>
<h3 data-id="heading-10">1. 发送普通字符串</h3>
<p>默认 <code>Content-Type</code> 为 <code>text/plain</code>。</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const reportData = (url, data) =&gt; {
// data 可能会被转为字符串 "",建议先 stringify
navigator.sendBeacon(url, JSON.stringify(data));
};</pre>
</div>
<h3 data-id="heading-11">2. 发送 JSON 数据(推荐)</h3>
<p>如果你希望后端接收到的&nbsp;<code>Content-Type</code>&nbsp;是&nbsp;<code>application/json</code>&nbsp;或者&nbsp;<code>application/x-www-form-urlencoded</code>,需要使用&nbsp;<code>Blob</code>&nbsp;来手动指定。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const reportData = (url, data) =&gt; {
// ✅ 正确写法:Blob 的第二个参数才是 options
const blob = new Blob(, {
    type: 'application/json; charset=UTF-8' // 或者 application/x-www-form-urlencoded
});
navigator.sendBeacon(url, blob);
};</pre>
</div>
<h3 data-id="heading-12">3. 发送 FormData</h3>
<p>适用于需要上传文件或模拟表单提交的场景。浏览器会自动设置&nbsp;<code>Content-Type</code>&nbsp;为&nbsp;<code>multipart/form-data</code>。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const reportData = (url, data) =&gt; {
const formData = new FormData();
Object.keys(data).forEach((key) =&gt; {
    let value = data;
    // FormData 的 value 只能是字符串或 Blob
    if (typeof value !== 'string' &amp;&amp; !(value instanceof Blob)) {
      value = JSON.stringify(value);
    }
    formData.append(key, value);
});
navigator.sendBeacon(url, formData);
};</pre>
</div>
<div>
<h2 data-id="heading-13">四、跨域场景的“万能钥匙” —— 1px 像素图片</h2>
<p>在某些场景下使用<code>sendBeacon</code> 会有跨域问题,而使用1px像素图片这种方式则利用了浏览器<strong>允许跨域加载资源</strong>(如图片、脚本)的特性,绕过了复杂的 CORS 配置</p>
<h3 data-id="heading-14">1. 核心原理</h3>
<p>通过动态创建 <code>Image</code> 对象,将埋点数据通过 <code>URL Query</code> 的形式挂载在图片请求的地址后面。服务端在接收到请求后,记录日志并返回一个 1x1 像素的透明图片。</p>
<h3 data-id="heading-15">2. 代码实现</h3>
<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;">/**
* 跨域埋点发送:1px 像素图片方案
* @param {string} url - 接口地址
* @param {Object} data - 埋点数据
*/
const reportByImg = (url, data) =&gt; {
// 1. 构造查询参数字符串
const params = Object.keys(data)
    .map(key =&gt; `${encodeURIComponent(key)}=${encodeURIComponent(data)}`)
    .join('&amp;');

// 2. 创建图片实例
const img = new Image();

// 3. 监听回调(可选,用于监控发送是否成功)
img.onload = () =&gt; console.log('埋点发送成功');
img.onerror = (err) =&gt; console.error('埋点发送失败', err);

// 4. 添加时间戳用于确保每次请求都被视为独立的新资源,避免被一些代理服务器或浏览器机制意外缓存
const connector = url.includes('?') ? '&amp;' : '?';
img.src = `${url}${connector}${params}&amp;_t=${Date.now()}`;
};</pre>
</div>
<h3 data-id="heading-16">3. 方案对比:为什么不用其他方式?</h3>
</div>
</div>
</div>
</div>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202604/2149129-20260413105313106-1498628595.png" alt="ScreenShot_2026-04-13_105031_839" loading="lazy"></p>
<div>
<div>
<h3 data-id="heading-17">4.注意事项</h3>
<ul>
<li><strong>URL 长度限制</strong>:由于数据是带在 URL 上的,浏览器对 URL 长度有限制(通常为 2KB-8KB)。如果数据量巨大,请拆分发送或改用 <code>sendBeacon</code>。</li>
<li><strong>GIF 是首选</strong>:服务端推荐返回 <strong>GIF</strong> 格式。对比 PNG 和 JPG,GIF 的透明像素块在文件头开销上是最小的(仅 43 字节)。</li>
<li><strong>内存释放</strong>:在一些极端高频埋点场景下,建议在 <code>onload</code> 之后执行 <code>img = null</code> 彻底释放内存。</li>
</ul>
<h2 data-id="heading-18">五、 避坑指南(面试考点)</h2>
<ol>
<li><strong>请求类型固定</strong>:<code>sendBeacon</code> 只能发送 <strong>POST</strong> 请求。</li>
<li><strong>无法读取响应</strong>:这是一个“射后不理”的 API,你无法获取服务器返回的数据(状态码、Response Body 等)。</li>
<li><strong>数据大小限制</strong>:虽然标准没有明确规定,但浏览器对队列总大小有限制(通常在 64KB 左右),不适合发送大数据。</li>
<li><strong>Cookie 携带</strong>:<code>sendBeacon</code> 默认会携带同域的 Cookie。</li>
</ol><hr>
<h2 data-id="heading-19">六、 面试模拟题</h2>
<h3 data-id="heading-20">Q1:<code>sendBeacon</code> 和 <code>ajax</code> (XHR/Fetch) 有什么根本区别?</h3>
<p><strong>参考回答:</strong></p>
<ul>
<li><strong>生命周期</strong>:Ajax 请求属于页面上下文,页面关闭时请求会被取消(除非同步);<code>sendBeacon</code> 属于浏览器上下文,页面关闭后依然存活。</li>
<li><strong>交互体验</strong>:页面卸载时,同步 Ajax 会阻塞跳转;<code>sendBeacon</code> 是异步非阻塞的。</li>
<li><strong>功能限制</strong>:<code>sendBeacon</code> 只能 POST,无法自定义 headers(除了 Content-Type),且无法读取响应。</li>
</ul>
<h3 data-id="heading-21">Q2:如果浏览器不支持 <code>sendBeacon</code> 怎么办?</h3>
<p><strong>参考回答:</strong> 需要做降级处理。</p>
<ol>
<li>检测 <code>navigator.sendBeacon</code> 是否存在。</li>
<li>如果不存在,降级为 <strong>同步 XHR</strong> 请求(虽然体验差,但得保数据)。</li>
<li>或者使用 <code>&lt;img&gt;</code> 标签发送 GET 请求(仅限数据量极小且不需要响应的场景)。</li>
</ol>
<h3 data-id="heading-22">Q3:<code>sendBeacon</code> 返回 <code>true</code> 代表数据一定发送成功了吗?</h3>
<p><strong>参考回答:</strong> 不一定。返回 <code>true</code> 仅代表浏览器<strong>成功将数据加入了发送队列</strong>。如果网络断开、或者浏览器崩溃,数据依然可能发送失败。但相比于普通 Ajax,它的成功率已经高出了几个数量级。</p>
</div>
<div>
<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><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19858310
頁: [1]
查看完整版本: JS-前端埋点神器 navigator.sendBeacon 全指南