前言:在上一篇中,我们搭建了 SDK 的骨架。今天,我们要给它注入灵魂——让它能够感知环境,并将数据可靠地发送给服务器。
1. 挑战:复杂的浏览器环境
发送 HTTP 请求看似简单,但在 SDK 开发中却暗藏玄机:
- 页面关闭时:用户关闭页面的一瞬间,普通的
fetch请求会被浏览器无情截断。 - 兼容性:有的老旧浏览器不支持
fetch,甚至不支持sendBeacon。 - 数据格式:后端可能要求 JSON,也可能要求 URL 参数。
如何优雅地解决这些问题?答案是:策略模式。
2. 策略模式 (Strategy Pattern)
我们定义一个统一的接口 SenderStrategy,然后实现不同的发送策略。上层逻辑(Tracker)不需要关心具体是用什么发的,只需要调用 send()。
2.1 定义接口
export interface SenderStrategy {
send(events: TrackerEvent[]): Promise<void>;
}
2.2 策略一:FetchSender (主力军)
这是我们最常用的方式。现代浏览器支持 keepalive 属性,允许请求在页面卸载后继续存活。
src/core/Sender.ts
// src/core/Sender.ts
export class FetchSender implements SenderStrategy {
async send(events: TrackerEvent[]): Promise<void> {
await fetch(this.endpoint, {
method: 'POST',
body: JSON.stringify(events),
keepalive: true // 🔥 关键:防止页面关闭时请求被杀
});
}
}
2.3 策略二:BeaconSender (断后)
navigator.sendBeacon 是专门为"埋点"设计的 API。它的特点是:可靠性极高,即使页面关闭也能发送成功。
export class BeaconSender implements SenderStrategy {
async send(events: TrackerEvent[]): Promise<void> {
// 🔥 坑点:必须用 Blob 包装,否则 Content-Type 默认是 text/plain
const blob = new Blob([JSON.stringify(events)], {
type: 'application/json',
});
navigator.sendBeacon(this.endpoint, blob);
}
}
2.4 策略三:ImageSender (兜底)
对于不支持上述 API 的古董浏览器,我们祭出上古神器:1x1 像素 GIF。
export class ImageSender implements SenderStrategy {
async send(events: TrackerEvent[]): Promise<void> {
const img = new Image();
// 数据只能放在 URL 参数里,注意长度限制
img.src = `${this.endpoint}?data=${JSON.stringify(events)}`;
}
}
3. 智能化环境采集 (Context)
如果每次埋点都要业务方手动传 userAgent、url,那这个 SDK 就太难用了。
我们封装一个 getContext() 工具,自动采集环境信息。
// src/utils/Context.ts
export function getContext() {
return {
userAgent: navigator.userAgent,
screen: `${window.screen.width}x${window.screen.height}`,
url: window.location.href,
timestamp: Date.now()
};
}
4. 全链路集成与验证
最后,我们在 Tracker 初始化时,根据浏览器支持情况选择合适的 Sender。
// src/core/Tracker.ts
this.sender = new FetchSender(config.endpoint); // 默认策略
// track 方法中
const context = getContext();
const finalEvent = { ...event, context };
this.sender.send([finalEvent]);
验证时刻
为了验证全链路,我们用 Node.js + Express 写了一个简单的接收端:
// server/index.js
import express from 'express';
const app = express();
app.post('/track', (req, res) => {
console.log('收到埋点:', req.body);
res.sendStatus(200);
});
app.listen(3000);
运行 Demo,点击按钮,看着服务器终端打印出数据的那一刻,闭环完成了。
5. 总结
通过这两篇文章,我们实现了一个麻雀虽小、五脏俱全的埋点 SDK。
- 架构:TypeScript + Vite + 单例/外观模式。
- 网络:策略模式 (Fetch/Beacon/Image) + 环境自动采集。
当然,这只是开始。在生产环境中,我们还需要考虑:
- 消息队列:削峰填谷,合并发送。
- 离线存储:断网重试 (IndexedDB)。
这些进阶话题,我们将在后续文章中继续探讨。