ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零构建前端埋点SDK:TypeScript+Rollup实战指南

从零构建前端埋点SDK:TypeScript+Rollup实战指南 1. 项目概述为什么我们要从零打造一个前端埋点SDK如果你是一名前端开发者无论是刚入行还是已经摸爬滚打几年大概率都接触过“数据埋点”这个任务。产品经理跑过来问“这个按钮的点击率是多少” 运营同学想知道“这个新功能页面的用户停留时长如何” 在没有埋点数据之前我们往往只能两手一摊。而市面上虽然有成体系的第三方数据平台但要么收费昂贵要么数据采集逻辑是个黑盒定制化需求难以满足更别提在数据安全日益重要的今天将用户行为数据全盘托付给外部服务所带来的隐忧。于是自己动手开发一个轻量、可控、可扩展的前端埋点 SDK并将其发布到 npm 上供团队或社区使用就成了一个极具价值且能充分体现工程师能力的项目。这不仅仅是封装几个addEventListener那么简单它涉及到数据采集的准确性、传输的可靠性、对业务的无侵入性、以及 SDK 自身的可维护性和可发布性。从零开始意味着你需要考虑监控什么、如何监控、数据怎么组织、如何发送、遇到网络问题怎么办、如何让使用者用起来最简单最后如何将它打包成一个标准的 npm 包。这个过程是对前端工程化能力的一次全面检验。你会用到模块打包工具如 Rollup、TypeScript 提升开发体验、设计合理的 API 和配置项、处理各种边界情况和错误、编写完整的测试用例最终通过 npm 发布流程让你的代码能被任何人通过一句npm install轻松使用。接下来我将带你完整走一遍这个旅程分享从设计思路到发布上线的每一个关键步骤和踩过的坑。2. 核心设计思路与架构选型在动手写第一行代码之前明确设计目标和架构是避免后期返工的关键。一个埋点 SDK 的核心使命是无感、准确、可靠地收集用户行为数据。2.1 核心需求拆解无侵入性采集SDK 应尽可能少地影响宿主页面的性能和逻辑。通常采用脚本异步加载、事件代理等方式。数据模型定义需要规范采集的数据格式。一个通用的事件模型通常包含event_id: 事件唯一标识如click_button_submit。event_type: 事件类型如click,pv(页面浏览),custom。properties: 事件属性一个键值对对象用于携带额外信息如按钮文字、商品ID、页面URL等。timestamp: 事件发生的时间戳。user_id/device_id: 用户或设备标识用于关联用户行为序列。传输策略如何将数据发送到后端服务器需要考虑即时发送使用sendBeacon或fetch。sendBeacon在页面卸载时更可靠但无法自定义请求头和获取响应。批量发送将短时间内的多个事件合并为一个请求减少 HTTP 请求数节省服务器资源。失败重试与队列网络失败或服务器错误时数据不能丢失需要本地暂存如使用localStorage并在适当时机重试。灵活的配置与API提供初始化配置如上报地址、应用ID、采样率等和手动上报APItrack,pageview等。性能与异常监控SDK 自身不能成为性能瓶颈同时要能容错避免因 SDK 报错导致主页面功能异常。2.2 技术栈与工具选型基于以上需求我们选择以下技术栈语言TypeScript。对于 SDK 这类需要明确接口和类型的库项目TypeScript 能极大提升开发体验和代码质量为使用者提供良好的类型提示。打包工具Rollup。与 Webpack 相比Rollup 更擅长打包库文件能生成更小、更干净的捆绑包并且对 Tree-shaking摇树优化支持得更好非常适合 SDK 开发。开发环境Node.js 环境使用npm或yarn管理依赖。测试Jest 或 Vitest 进行单元测试。代码规范ESLint Prettier 保证代码风格统一。这个选型组合是目前前端库开发的“黄金搭档”能很好地平衡开发效率、输出质量和社区生态。3. 项目初始化与核心模块实现让我们开始动手。首先创建一个项目目录并初始化。mkdir xiaoman-tracker-sdk cd xiaoman-tracker-sdk npm init -y修改生成的package.json设置入口文件、类型定义文件并添加脚本和依赖。{ name: xiaoman-tracker-sdk, version: 0.1.0, description: A lightweight front-end tracking SDK., main: dist/index.cjs.js, module: dist/index.esm.js, unpkg: dist/index.umd.js, types: dist/index.d.ts, scripts: { dev: rollup -c -w, build: rollup -c, test: vitest run, lint: eslint src --ext .ts, format: prettier --write \src/**/*.ts\ }, devDependencies: { rollup/plugin-commonjs: ^25.0.7, rollup/plugin-node-resolve: ^15.2.3, rollup/plugin-terser: ^0.4.4, rollup/plugin-typescript: ^11.1.6, typescript-eslint/eslint-plugin: ^6.7.0, typescript-eslint/parser: ^6.7.0, eslint: ^8.49.0, prettier: ^3.0.3, rollup: ^3.29.4, tslib: ^2.6.2, typescript: ^5.2.2, vitest: ^0.34.6 }, files: [dist] }注意main,module,unpkg和types字段它们分别定义了 CommonJS、ES Module、UMD 格式的入口和 TypeScript 类型定义这是发布一个高质量 npm 库的标配。3.1 核心类型与配置定义在src/types.ts中我们先定义核心的数据结构和配置接口。// 事件基础接口 export interface BaseEvent { event_id: string; // 事件唯一标识 event_type: string; // 事件类型如 click, pv, custom properties?: Recordstring, any; // 事件属性 timestamp?: number; // 时间戳SDK可自动生成 } // 用户上下文信息 export interface UserContext { user_id?: string; device_id?: string; session_id?: string; page_url?: string; user_agent?: string; // ... 其他需要收集的上下文信息 } // SDK 初始化配置 export interface TrackerConfig { endpoint: string; // 数据上报服务器地址 appId: string; // 应用标识 autoTrack?: { // 是否自动追踪页面浏览 pageView?: boolean; // 是否自动追踪点击事件 click?: boolean; // 需要自动追踪点击事件的选择器默认追踪所有带有 data-track 属性的元素 clickSelector?: string; }; batch?: { // 是否开启批量上报 enable: boolean; // 批量上报的最大事件数 maxSize: number; // 批量上报的最大等待时间毫秒 maxWait: number; }; // 采样率0-1之间1表示100%上报 sampling?: number; // 是否在控制台打印调试信息 debug?: boolean; }3.2 实现核心 Tracker 类在src/core/tracker.ts中我们实现 SDK 的核心类。这个类负责管理配置、收集事件、处理队列和发送数据。import { BaseEvent, TrackerConfig, UserContext } from ../types; import { generateDeviceId, getPageInfo } from ../utils; export class Tracker { private config: TrackerConfig; private queue: BaseEvent[] []; private userContext: UserContext {}; private batchTimer: any null; constructor(config: TrackerConfig) { // 合并默认配置 this.config { autoTrack: { pageView: true, click: true, clickSelector: [data-track] }, batch: { enable: true, maxSize: 10, maxWait: 5000 }, sampling: 1, debug: false, ...config, }; // 初始化用户上下文设备ID、会话ID等 this.initUserContext(); // 初始化自动追踪 this.initAutoTrack(); // 初始化批量上报定时器 this.initBatchTimer(); if (this.config.debug) { console.log([Tracker SDK] 初始化完成, this.config); } } private initUserContext(): void { this.userContext { device_id: generateDeviceId(), // 生成一个持久化的设备ID session_id: this.generateSessionId(), page_url: getPageInfo().url, user_agent: navigator.userAgent, }; } private initAutoTrack(): void { if (this.config.autoTrack?.pageView) { this.trackPageView(); } if (this.config.autoTrack?.click) { this.bindClickEvent(); } } // 手动上报事件 - 核心API public track(eventId: string, eventType: string, properties?: Recordstring, any): void { // 采样率判断 if (Math.random() (this.config.sampling || 1)) { return; } const event: BaseEvent { event_id: eventId, event_type: eventType, properties, timestamp: Date.now(), }; // 添加上下文信息到事件属性中 const enrichedEvent this.enrichEvent(event); this.addToQueue(enrichedEvent); } // 上报页面浏览事件 public trackPageView(properties?: Recordstring, any): void { const pageInfo getPageInfo(); this.track(pv_${pageInfo.path}, pageview, { ...properties, page_title: pageInfo.title, page_url: pageInfo.url, referrer: document.referrer, }); } // 私有方法丰富事件数据 private enrichEvent(event: BaseEvent): BaseEvent { return { ...event, properties: { ...this.userContext, ...event.properties, }, }; } // 私有方法将事件加入队列并触发发送逻辑 private addToQueue(event: BaseEvent): void { this.queue.push(event); if (this.config.debug) { console.log([Tracker SDK] 事件入队:, event); } // 批量上报逻辑 if (this.config.batch?.enable) { if (this.queue.length this.config.batch.maxSize) { this.flushQueue(); } } else { // 非批量模式立即发送单个事件 this.sendEvents([event]); this.queue []; } } // 私有方法发送队列中的所有事件 private flushQueue(): void { if (this.queue.length 0) return; const eventsToSend [...this.queue]; this.queue []; // 清空当前队列 this.sendEvents(eventsToSend); } // 私有方法实际发送HTTP请求 private sendEvents(events: BaseEvent[]): void { const payload { app_id: this.config.appId, events, }; // 优先使用 sendBeacon在页面卸载时更可靠 if (navigator.sendBeacon) { const blob new Blob([JSON.stringify(payload)], { type: application/json }); const success navigator.sendBeacon(this.config.endpoint, blob); if (!success this.config.debug) { console.warn([Tracker SDK] sendBeacon 发送失败尝试使用 fetch); this.sendByFetch(payload); } } else { // 降级方案使用 fetch this.sendByFetch(payload); } } private sendByFetch(payload: any): void { fetch(this.config.endpoint, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload), keepalive: true, // 允许在页面卸载后继续请求 }).catch((err) { console.error([Tracker SDK] 数据上报失败:, err); // 重要发送失败将数据存回队列这里简化处理实际应存到 localStorage 并实现重试机制 this.queue.unshift(...payload.events); }); } // 初始化批量上报定时器 private initBatchTimer(): void { if (this.config.batch?.enable) { this.batchTimer setInterval(() { if (this.queue.length 0) { this.flushQueue(); } }, this.config.batch.maxWait); } } // 绑定自动点击追踪 private bindClickEvent(): void { document.addEventListener(click, (e) { const target e.target as HTMLElement; // 通过事件冒泡找到符合选择器的元素 const trackElement target.closest(this.config.autoTrack!.clickSelector!); if (trackElement) { const eventId trackElement.getAttribute(data-track-id) || trackElement.id || unknown_click; const properties: Recordstring, any {}; // 可以收集元素上的自定义属性如>// 生成一个相对稳定的设备ID存储在 localStorage export function generateDeviceId(): string { const STORAGE_KEY _xiaoman_device_id; let deviceId localStorage.getItem(STORAGE_KEY); if (!deviceId) { deviceId device_${Date.now()}_${Math.random().toString(36).substr(2, 9)}; try { localStorage.setItem(STORAGE_KEY, deviceId); } catch (e) { // localStorage 可能被禁用降级方案使用 sessionStorage 或仅用随机数 console.warn([Tracker SDK] localStorage 不可用设备ID将在本次会话中有效); deviceId session_device_${Math.random().toString(36).substr(2, 9)}; } } return deviceId; } // 获取页面基本信息 export function getPageInfo(): { url: string; title: string; path: string } { return { url: window.location.href, title: document.title, path: window.location.pathname, }; } // 节流函数用于可能的高频事件这里未在核心类中使用但可作为工具提供 export function throttleT extends (...args: any[]) any(fn: T, delay: number): T { let lastCall 0; return function (...args: any[]) { const now Date.now(); if (now - lastCall delay) { lastCall now; return fn(...args); } } as T; }3.4 创建主入口文件在src/index.ts中我们暴露 SDK 的主要 API。import { Tracker } from ./core/tracker; import { TrackerConfig } from ./types; // 导出一个创建跟踪器实例的函数这是最常见的用法 export function initTracker(config: TrackerConfig): Tracker { // 做一些必要的环境检查比如是否在浏览器环境 if (typeof window undefined) { console.warn([Tracker SDK] 当前非浏览器环境SDK 将不会初始化。); // 可以返回一个模拟对象避免在使用时报错 return {} as Tracker; } return new Tracker(config); } // 也可以直接导出 Tracker 类供高级用户使用 export { Tracker }; export type { TrackerConfig, BaseEvent } from ./types; // 默认导出一个立即执行函数IIFE风格的安装方式适用于通过script标签引入 const globalObj window as any; if (!globalObj.__XIAOMAN_TRACKER_SDK__) { globalObj.__XIAOMAN_TRACKER_SDK__ { initTracker }; }4. 使用 Rollup 进行工程化打包SDK 代码写好了我们需要将它打包成适合不同环境ES Module, CommonJS, UMD的格式。创建rollup.config.js。import resolve from rollup/plugin-node-resolve; import commonjs from rollup/plugin-commonjs; import typescript from rollup/plugin-typescript; import terser from rollup/plugin-terser; import pkg from ./package.json assert { type: json }; export default { input: src/index.ts, // 入口文件 output: [ { file: pkg.main, // dist/index.cjs.js format: cjs, sourcemap: true, }, { file: pkg.module, // dist/index.esm.js format: esm, sourcemap: true, }, { file: pkg.unpkg, // dist/index.umd.js format: umd, name: XiaomanTracker, // UMD 模式下的全局变量名 sourcemap: true, plugins: [terser()], // 对 UMD 包进行压缩 }, ], plugins: [ resolve(), // 解析 node_modules 中的模块 commonjs(), // 将 CommonJS 模块转换为 ES6 typescript({ tsconfig: ./tsconfig.json }), // 编译 TypeScript ], // 指出哪些模块应该被视为外部依赖不打包进库 external: [...Object.keys(pkg.peerDependencies || {})], };对应的tsconfig.json配置如下{ compilerOptions: { target: ES2015, module: ESNext, lib: [DOM, ES2015], declaration: true, declarationDir: ./dist, outDir: ./dist, strict: true, moduleResolution: node, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true }, include: [src/**/*], exclude: [node_modules, dist, **/*.test.ts] }现在运行npm run buildRollup 就会在dist目录下生成我们需要的三种格式的打包文件以及对应的.d.ts类型声明文件。注意在package.json中我们通过files字段指定了只有dist目录会被发布到 npm。确保src源码目录不会被上传以保护知识产权并减少包体积。5. 编写测试用例与质量保障一个可靠的 SDK 必须有测试覆盖。我们使用 Vitest一个更快的测试框架来写单元测试。在src/core/tracker.test.ts中import { describe, it, expect, vi, beforeEach, afterEach } from vitest; import { Tracker } from ./tracker; // 模拟全局对象 const mockNavigator { sendBeacon: vi.fn(), userAgent: test }; const mockWindow { location: { href: http://test.com, pathname: / }, document: { title: Test } }; (global as any).navigator mockNavigator; (global as any).window mockWindow; describe(Tracker, () { let tracker: Tracker; beforeEach(() { vi.clearAllMocks(); // 每次测试前创建一个新的 Tracker 实例 tracker new Tracker({ endpoint: https://api.example.com/track, appId: test-app, debug: false, // 测试时关闭 debug 日志 batch: { enable: false }, // 关闭批量方便测试单次发送 }); }); it(应该正确初始化并生成设备ID, () { // 这里可以模拟 localStorage const localStorageMock { getItem: vi.fn(), setItem: vi.fn(), }; (global as any).localStorage localStorageMock; // 重新初始化 tracker new Tracker({ endpoint: test, appId: test }); expect(localStorageMock.setItem).toHaveBeenCalled(); }); it(track 方法应能正确添加事件到队列, () { const spyAddToQueue vi.spyOn(tracker as any, addToQueue); tracker.track(test_event, custom, { foo: bar }); expect(spyAddToQueue).toHaveBeenCalledWith( expect.objectContaining({ event_id: test_event, event_type: custom, properties: { foo: bar }, }) ); }); it(当采样率小于1时部分事件应被丢弃, () { const config { endpoint: test, appId: test, sampling: 0.5 }; const mockTracker new Tracker(config); const spyAddToQueue vi.spyOn(mockTracker as any, addToQueue); // 由于随机性这个测试可能不稳定。更可靠的方法是模拟 Math.random const mockMath Object.create(global.Math); mockMath.random () 0.8; // 大于0.5事件应被丢弃 global.Math mockMath; mockTracker.track(e1, t1); expect(spyAddToQueue).not.toHaveBeenCalled(); }); it(当 sendBeacon 可用时应使用 sendBeacon 发送数据, () { mockNavigator.sendBeacon.mockReturnValue(true); tracker.track(beacon_event, test); // 由于 batch.enablefalse会立即发送 expect(mockNavigator.sendBeacon).toHaveBeenCalledWith( https://api.example.com/track, expect.any(Blob) ); }); it(当 sendBeacon 失败时应降级使用 fetch, () { mockNavigator.sendBeacon.mockReturnValue(false); global.fetch vi.fn(); // 模拟 fetch tracker.track(fetch_event, test); expect(mockNavigator.sendBeacon).toHaveBeenCalled(); expect(global.fetch).toHaveBeenCalled(); // 因为 sendBeacon 返回 false所以会调用 fetch }); });运行npm test来执行测试。良好的测试覆盖率是代码信心的来源尤其是在 SDK 这种基础工具中。6. 本地调试、联调与发布前准备在发布到 npm 之前我们需要在真实项目中本地测试 SDK。6.1 使用 npm link 进行本地调试在 SDK 项目根目录运行npm link。这会在全局 node_modules 中创建一个符号链接指向你的项目。在另一个测试项目比如一个 Vue 或 React 项目中运行npm link xiaoman-tracker-sdk。这样测试项目就会使用你本地开发的 SDK 包。在测试项目中引入并使用 SDK// 在测试项目的入口文件如 main.js 或 index.js import { initTracker } from xiaoman-tracker-sdk; const tracker initTracker({ endpoint: https://your-backend.com/track, appId: your-app-id, debug: true, // 开发时开启调试 }); // 手动触发一个事件 tracker.track(user_login, custom, { method: password });在页面上添加自动追踪的属性button>{ name: xiaoman-tracker-sdk, version: 0.1.0, description: A lightweight, configurable front-end tracking SDK for user behavior analysis., keywords: [tracking, analytics, sdk, frontend, monitoring], author: Your Name, license: MIT, repository: { type: git, url: https://github.com/your-username/xiaoman-tracker-sdk.git }, homepage: https://github.com/your-username/xiaoman-tracker-sdk#readme, bugs: { url: https://github.com/your-username/xiaoman-tracker-sdk/issues }, engines: { node: 14 }, peerDependencies: { // 可以声明对某些库的 peer 依赖比如如果用了 Vue 的插件系统 } }同时在项目根目录创建README.md编写清晰的使用文档、API 说明、配置项详解和开发指南。6.3 版本管理与发布流程版本号遵循语义化版本规范SemVer。主版本号.次版本号.修订号。初始开发版可以用0.1.0。不兼容的 API 更改升级主版本号向下兼容的功能性新增升级次版本号向下兼容的问题修复升级修订号。登录 npm在终端运行npm login输入你的 npm 账号、密码和邮箱。发布运行npm publish。如果是第一次发布包名可用如果包名已存在你需要换一个名字。--access public参数对于 scoped package如yourname/package是必须的。更新版本修改代码后使用npm version patch小修复、npm version minor新功能或npm version major不兼容更新来更新package.json中的版本号并创建一个 git tag然后再运行npm publish。7. 高级功能扩展与优化思路一个基础的 SDK 上线后可以根据实际需求不断迭代。以下是一些高级功能和优化方向7.1 数据持久化与重试机制上面的示例中发送失败的数据只是简单放回内存队列页面刷新后数据就丢失了。一个生产级的 SDK 应该使用localStorage或IndexedDB进行持久化存储并实现指数退避的重试机制。// 简化的持久化队列类 class PersistentQueue { private STORAGE_KEY _tracker_queue; private maxRetries 3; add(event: BaseEvent): void { const queue this.getQueue(); queue.push({ ...event, retries: 0 }); this.saveQueue(queue); } getEventsToSend(maxSize: number): BaseEvent[] { const queue this.getQueue(); const toSend queue.slice(0, maxSize); const remaining queue.slice(maxSize); this.saveQueue(remaining); return toSend; } markAsFailed(events: BaseEvent[]): void { const queue this.getQueue(); events.forEach((event: any) { if (event.retries this.maxRetries) { event.retries; queue.unshift(event); // 放回队列头部优先重试 } else { // 超过重试次数丢弃或记录日志 console.error([Tracker SDK] 事件上报最终失败已丢弃:, event); } }); this.saveQueue(queue); } private getQueue(): any[] { try { const data localStorage.getItem(this.STORAGE_KEY); return data ? JSON.parse(data) : []; } catch { return []; } } private saveQueue(queue: any[]): void { try { localStorage.setItem(this.STORAGE_KEY, JSON.stringify(queue)); } catch (e) { console.warn([Tracker SDK] 无法保存数据到 localStorage, e); } } }7.2 性能指标自动采集除了用户行为前端性能数据也至关重要。可以扩展 SDK自动采集FP(First Paint),FCP(First Contentful Paint),LCP(Largest Contentful Paint),CLS(Cumulative Layout Shift) 等 Web Vitals 指标。import { onCLS, onFCP, onLCP } from web-vitals; private initWebVitalsTracking(): void { if (typeof onCLS ! undefined) { onCLS((metric) { this.track(web_vital_cls, performance, { value: metric.value }); }); } // ... 类似地监听 FCP, LCP, FID 等 }7.3 错误边界与异常监控监听全局的error和unhandledrejection事件自动上报 JavaScript 错误和未处理的 Promise 拒绝帮助开发者发现线上问题。private initErrorTracking(): void { window.addEventListener(error, (event) { this.track(js_error, error, { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, error: event.error?.stack, }); }); window.addEventListener(unhandledrejection, (event) { this.track(promise_rejection, error, { reason: event.reason?.toString(), }); }); }7.4 插件化架构为了保持核心轻量可以将一些非核心功能如性能监控、错误收集、用户行为录屏等设计成插件。SDK 核心提供一个插件注册机制。interface TrackerPlugin { install(tracker: Tracker): void; } class Tracker { private plugins: TrackerPlugin[] []; use(plugin: TrackerPlugin): void { plugin.install(this); this.plugins.push(plugin); } } // 定义一个错误监控插件 class ErrorMonitorPlugin implements TrackerPlugin { install(tracker: Tracker) { window.addEventListener(error, (e) { tracker.track(plugin_js_error, error, { msg: e.message }); }); } } // 使用 const tracker new Tracker(config); tracker.use(new ErrorMonitorPlugin());8. 常见问题、排查技巧与避坑指南在实际开发和集成过程中你肯定会遇到各种问题。这里记录一些典型的坑和解决方案。8.1 数据上报丢失或重复问题页面关闭时使用fetch或XMLHttpRequest发送的请求可能被浏览器取消导致数据丢失。解决在pagehide或beforeunload事件中优先使用navigator.sendBeacon()。它专为在页面生命周期末尾发送少量数据设计即使页面关闭浏览器也会保证请求发出。我们的 SDK 中已经做了这个兼容。问题快速触发多个事件导致重复上报或顺序错乱。解决实现一个稳健的队列机制。我们的批量队列是一个基础方案。更复杂的场景可以考虑使用“发送中队列”和“待发送队列”分离确保同一批数据不会因为网络慢而被重复发送。8.2 单页应用 (SPA) 路由切换追踪问题在 Vue Router 或 React Router 构建的单页应用中页面切换不会触发传统的pageview。解决SDK 需要提供手动调用trackPageView的 API并建议使用者在自己的路由守卫中调用。或者可以开发针对 Vue/React 的专用插件自动监听路由变化。// 在 Vue Router 中 router.afterEach((to, from) { tracker.trackPageView({ from: from.fullPath, to: to.fullPath }); });8.3 广告拦截器 (Ad Blockers) 的影响问题一些广告拦截器会屏蔽包含track,analytics,beacon等关键词的请求 URL 或脚本。解决端点路径避免在上报地址中使用明显的关键词如/track,/collect。可以使用更隐蔽或业务相关的路径如/api/logs。脚本名打包后的 JS 文件命名也避免使用tracker.js可以用主项目相关的名字。功能降级在 SDK 初始化时可以尝试发送一个探测请求如果被拦截则优雅降级比如只收集数据但不发送或在控制台给出警告避免脚本报错影响主应用。8.4 跨域 (CORS) 问题问题如果 SDK 部署在www.a.com而上报服务器是api.b.com浏览器会因为同源策略而阻止fetch请求。解决后端服务器必须正确配置 CORS 响应头例如Access-Control-Allow-Origin: *或指定允许的域名。对于简单的上报场景也可以考虑使用img标签的src发起 GET 请求但能携带的数据量和类型受限。8.5 类型声明文件 (.d.ts) 生成不全问题使用npm install安装你的包后在 TypeScript 项目中导入时VS Code 没有类型提示。解决确保tsconfig.json中设置了declaration: true和declarationDir: ./dist。并且package.json中的types字段正确指向了生成的.d.ts文件如types: dist/index.d.ts。发布前务必检查dist目录下是否存在类型声明文件。8.6 包体积过大问题打包后的 UMD 文件有好几百 KB。解决使用 Rollup 的 Tree-shaking 能力确保库是 ES Module 格式导出。将一些大型依赖如web-vitals设置为peerDependencies或optionalDependencies让使用者按需安装。使用rollup/plugin-terser进行代码压缩。检查打包产物看是否有未使用的代码或过大的 polyfill 被引入。开发一个前端埋点 SDK 是一个系统工程它要求开发者不仅熟悉前端 API 和浏览器特性还要具备良好的软件设计、错误处理和工程化思维。从设计、编码、测试、打包到发布每一步都充满细节。当你看到自己开发的 SDK 通过npm install被成千上万的项目使用时那种成就感是无与伦比的。希望这篇详尽的指南能帮你避开我当年踩过的那些坑顺利打造出属于你自己的、稳定可靠的数据采集利器。
返回列表