稀土掘金技术社区

【用户行为监控】别只做工具人了!手把手带你写一个前端埋点统计 SDK

❝

你是否一直对前端「用户行为监控」系统的底层原理充满好奇?

想知道那些“黑科技”是如何「拦截点击」、统计 「PV」(页面浏览量)与 「UV」(独立访客数)、精确计算「页面停留时长」的吗?

与其只做工具的使用者,不如深入底层,探寻其背后的实现机制。

本文将从原理角度切入,手把手带你设计并实现一个轻量级、功能完备的用户行为监控 SDK

❞

读完这篇,你将收获什么?

通过手写这个 SDK,你不仅能获得一个可用的监控工具,更能深入掌握以下核心知识点:

  1. 「浏览器事件模型」:load、beforeunload、visibilitychange、pagehide... 到底该用哪个?怎么避坑?一次讲清楚!

  2. 「SPA 路由劫持大法」:单页应用路由跳转不刷新页面,怎么监控?带你手写 history.pushState 拦截器,配合 popstate 玩转路由监听。

  3. 「数据上报的“稳”字诀」:页面都要关了,数据还没发出去咋办?Navigator.sendBeacon 救命,稳稳地把最后一条数据送达服务端。

  4. 「SDK 架构设计」:从 0 到 1 设计一个高内聚、低耦合的 SDK(入口、采集、存储、上报分离),顺便把 NPM 发布流程也走一遍。

预备知识:别被名词吓唬住

在动手写代码之前,咱们先统一一下“黑话”,搞清楚两个最基础的指标:「PV」 和 「UV」。

「PV 和 UV 到底有啥区别?」

  • 「PV (Page View - 页面浏览量)」 :简单说就是页面被打开了多少次。\

    「举个例子」:小明打开了你的网站首页(PV+1),手抖刷新了一下(PV+1),又点进去看详情页(PV+1)。小明一个人就贡献了 3 个 PV。

    「核心意义」:衡量网站被访问的频次,看流量大小(看热闹)。

  • 「UV (Unique Visitor - 独立访客数)」 :简单说就是有多少个不同的人来过。

    「举个例子」:还是小明,他今天疯狂刷新了你的网站 100 次(PV=100),但他还是小明这一个人,所以 UV 只算 1。

    「核心意义」:衡量网站被多少真实用户使用了(看人头)。

「系统架构与功能设计」

「别急着写代码,先看图!」 👇下面这张流程图清晰地展示了数据在 SDK 内部是如何流转的:

image.png

如图所示,整个流程主要分为三步走:

  1. 「存储校验(Storage)」:初始化时,先检查 localStorage,判断是新客还是回头客(UV 校验)。
  2. 「核心采集(Collection)」:启动监听器,分别捕获 PV、点击和停留时长。
  3. 「数据上报(Reporting)」:所有数据最终汇聚到上报模块,发送给服务端。

「搞懂了数据是怎么“流”的之后,接下来我们得明确一下源头到底要“抓”什么」。

「1. 咱们到底要抓哪些数据?」

简单来说,这个 SDK 就是你安插在页面里的「侦察兵」,主要负责收集这 4 类情报:

  • 「点击 (Click)」:用户到底最爱戳哪个按钮?(比如那个“立即购买”到底有没有人点?)
  • 「PV (流量)」:页面今天被加载了多少次?(看热闹的人多不多?)
  • 「UV (访客)」:到底有多少「活人」来过?(去重后的真实用户数)
  • 「停留时长 (Dwell Time)」:用户进来是「秒关」,还是津津有味地「看了半天」?

「2. 核心战术(怎么抓?)」

针对上面这些目标,我们逐个击破:

  1. 「抓 PV(页面浏览量)」:这事儿分两头堵。

    - 「MPA(传统页面)」:死守 window.load,加载完就报。

    - 「SPA(单页应用)」:路由变了页面不刷新咋办?劫持 

    history.pushState 和 replaceState,再监听 popstate,路由一变,PV 立马 +1。

    「注:SPA和MPA区别:」

    - 「MPA (Multi-Page Application)」:传统多页应用(如京东首页跳转详情页),每次跳转都会重新加载 HTML,触发 window.load。
    - 「SPA (Single-Page Application)」:现代单页应用(如 Vue/React 项目),页面跳转不刷新浏览器,只是 JS 换了内容,「不会」触发 window.load。
  • 「抓 UV(独立访客数)」:在浏览器本地 (localStorage) 盖个章。如果发现今天已经盖过章了,就不重复上报了。

  • 「抓用户点击」:利用 「事件委托」 技术,在最外层 (document) 装个「窃听器」。不管你点了里面的哪个元素,最终都会被我捕获。

  • 「抓页面停留时长」:这个也得「分头行动」。

    • 「通用招数」:监听 beforeunload(页面要关了)和 visibilitychange(页面隐藏了),一旦触发就计算时间。
    • 「SPA 特攻」:路由切换时(pushState/popstate),除了报 PV,还得把「上一页」的停留时间给结清了。

    「3. 目录结构」

    为了保持代码的模块化和可维护性,我们采用以下目录结构:

    behavior-monitor/├── dist/                # 打包产物├── src/                 # 源码目录│   ├── index.ts         # 入口文件│   ├── tracker.ts       # 行为采集逻辑(PV/Click/Dwell)│   ├── storage.ts       # 本地存储与 ID 管理 │   └── sender.ts        # 上报逻辑├── test/                # 测试靶场│   ├── server.js        # 本地测试服务│   └── index.html       # 行为触发页面├── package.json         # 项目配置├── rollup.config.js     # Rollup 打包配置├── tsconfig.json        # TypeScript 配置└── README.md

    行为监控源码在 src目录下 ,最终使用rollup对代码进行打包,dist是打包产物 ; test目录下是对打包产物的测试。现在就从 0 到 1 开干,做个mini版的用户行为监控 SDK

    🚀 「浏览项目的完整代码及示例可以点击这里 user-behavior-monitor https://github.com/Teernage/user-behavior-monitor ,如果对您有帮助欢迎Star。」

    「核心代码实现」

    「1.主入口 (index.ts)」

    入口文件负责对外暴露初始化方法,串联各个模块。在这里我们进行 UV 的初步检查。

    import { trackUserBehavior } from './tracker';import { getUserID, isUVRecorded, setUVRecorded } from './storage';import { sendBehaviorData } from './sender';
    export interface InitOptions {  projectName: string;  reportUrl: string;}
    export const initUserBehaviorMonitor = (options: InitOptions) => {  const { projectName, reportUrl } = options;  const userId = getUserID();
      // UV 逻辑:如果本地未记录,则上报 UV 并标记  if (!isUVRecorded()) {    sendBehaviorData({      behavior: 'uv',      userId,      projectName,      timestamp: new Date().toISOString()    }, reportUrl);    setUVRecorded();  }
      // 启动行为追踪  trackUserBehavior(projectName, reportUrl);};

    「2. 行为采集 (tracker.ts)」

    这也是 SDK 最核心的部分。为了方便理解,我们将功能拆分为四个具体的任务模块:

    「(1) 任务分配 (架构拆解)」

    trackUserBehavior 是总指挥,它负责启动所有的监控任务:

    export const trackUserBehavior = (projectName: string, reportUrl: string) => {  // 1. 点击监控:通过事件委托监听用户的点击操作  trackClicks(projectName, reportUrl);
      // 2. MPA(传统页面) PV 监控:监听页面首次加载  trackMpaPageView(projectName, reportUrl);
      // 3. 停留时长监控:在页面关闭或隐藏时,计算并上报时长  trackPageDwellTime(projectName, reportUrl);
      // 4. SPA 路由监控:专门处理单页应用的路由跳转  trackSpaBehavior(projectName, reportUrl);};

    这样拆分后,职责非常清晰:

    • trackClicks:  负责监控点击操作。

    • trackMpaPageView: 只管「首次打开」网页的那一次 PV。

    • trackSpaBehavior: 负责处理后续的「路由跳转」。

    • trackPageDwellTime: 兜底处理所有「非路由跳转」引起的页面关闭。

    「(2) PV 与来源 (Referrer)」

    对于传统的 MPA 网站,我们只需要监听 window.load 事件。不仅要记录“PV + 1”,还要记录 document.referrer,告诉服务端用户是从哪里跳过来的(比如从百度搜索进入)。

      const trackMpaPageView = (projectName: string, reportUrl: string) => {    window.addEventListener('load', () => {          // 获取用户id          const userId = getUserID();          // 增加 PV 计数 => (曝光+1)          const pv = incrementPV(); 
              // 发送 PV 数据到服务器          sendBehaviorData({            behavior: 'pv',            userId,            projectName,            timestamp: new Date().toISOString(),            pageUrl: window.location.href,            referrer: document.referrer || '', // 记录来源            pv,          }, reportUrl);          ...    });  };

    「(3) SPA 路由监听 (核心难点)」

    SPA(单页应用)的特点是「页面跳转不刷新」。

    • 「MPA」:每次跳转都是一次全新的页面加载,浏览器会自动处理一切。
    • 「SPA」:跳转只是 JS 修改了 URL 和 DOM,浏览器不会自动触发加载事件。

    所以,我们需要「主动监听」路由变化,并手动处理数据的上报与传递。

    在 SPA(如 Vue/React)中,路由跳转主要有三种方式,导致我们需要不同的监听手段:

    1. 「代码跳转」(如 router.push 或者 router.replace)

    • 「现象」:JS 里调用了 history.pushState 或 history.replaceState。
    • 「坑点」:浏览器这时候是「装死」的,它悄悄改了 URL,但「完全不通知」任何人(不触发事件)。
    • 「对策」:我们得把 pushState 和 replaceState 这两个原生方法「劫持(重写)」 了,在它干活之前,先插播一段我们的上报逻辑。
  • 「浏览器后退/前进」

    • 「现象」:用户点了浏览器左上角的回退或者前进箭头。
    • 「坑点」:这回不触发 pushState 了,而是触发 popstate 事件了。
    • 「对策」:老老实实监听 popstate 事件。
  • 「Hash 模式」 (#)

    • 「现象」:URL 里的 # 变了。
    • 「对策」:监听 hashchange 事件。

    「关键技巧:Referrer 接力」

    在 SPA 内部跳转时,浏览器不会更新 document.referrer。我们需要手动维护一个 lastPageUrl 变量,把“上一个页面的 URL”传给“下一个页面”,这样才能串联起完整的用户访问路径。

    const trackSpaBehavior = (projectName: string, reportUrl: string) => {  const handleRouteChange = () => {    // 1. 防抖校验:如果 URL 没变(比如 hashchange 和 popstate 同时触发),直接退出    if (window.location.href === lastPageUrl) return;
        // 2. 结算上一页:上报前一个页面的停留时间    reportDwellTime(projectName, reportUrl);
        // 3. 记录当前 URL 为 referrer (在更新 lastPageUrl 之前!)    const referrer = lastPageUrl;
        // 4. 更新状态:保存当前 URL,为下一次跳转做准备    pageLoadTime = Date.now();    lastPageUrl = window.location.href;
        // 5. 记录新页面:上报 PV    const userId = getUserID();    const pv = incrementPV();    sendBehaviorData(      {        behavior: 'pv',        userId,        projectName,        timestamp: new Date().toISOString(),        pageUrl: window.location.href,        referrer: referrer, // 这里的 referrer 是跳转前的页面 URL        pv,      },      reportUrl    );  };
      // 1. 监听 Hash 和浏览器后退/前进  window.addEventListener('hashchange', handleRouteChange);  window.addEventListener('popstate', handleRouteChange);
      // 2. 劫持 History API (解决 pushState/replaceState 不触发事件的问题)  const originalPush = history.pushState;  const originalReplace = history.replaceState;
      // 路由跳转,劫持 pushState  history.pushState = function (...args: Parameters<typeof history.pushState>) {    originalPush.apply(this, args);    handleRouteChange();  };
      // 路由跳转,劫持 replaceState  history.replaceState = function (...args: Parameters<typeof history.replaceState>) {    originalReplace.apply(this, args);    handleRouteChange();  };};

    「(4) 页面停留时长 (Dwell Time)」

    正如前面所说,抓取时长要「分头行动」:

    「第一步:通用招数(处理关闭/隐藏)」用户直接关闭页面或切换到后台时,触发结算:

    const trackPageDwellTime = (projectName: string, reportUrl: string) => {  // 1. 页面关闭/刷新时  window.addEventListener('beforeunload', () => {    reportDwellTime(projectName, reportUrl);  });
      // 2. 兼容移动端(部分移动端不触发 beforeunload,只触发 pagehide)  window.addEventListener('pagehide', () => {    reportDwellTime(projectName, reportUrl);  });
      // 3. 页面隐藏/切后台时  document.addEventListener('visibilitychange', () => {    if (document.visibilityState === 'hidden') {      reportDwellTime(projectName, reportUrl);    }  });};

    「第二步:SPA 特攻(处理路由切换)」

    这部分逻辑其实已经包含在上面的 「(3) SPA 路由监听」 里了。 在 handleRouteChange 函数中,我们在跳转前第一件事就是调用 reportDwellTime(),把上一页的时间结清。

    // 回顾一下 trackSpaBehavior 里的逻辑const handleRouteChange = () => {  // 1. 路由要变了?先结账!上报停留时长  reportDwellTime(projectName, reportUrl);   // ...};

    「(5) 停留时长防抖 (避免重复上报)」

    「痛点」:用户关闭页面时,浏览器可能会同时触发 beforeunload、pagehide 等多个事件。如果不处理,可能会导致同一段停留时间被「重复上报」。

    「解决办法」:引入一个标记变量 lastDwellReportedForLoadTime。只要当前时间段已经上报过一次,就直接跳过,不再重复处理。

    // 记录上一次上报停留时间的时间戳let lastDwellReportedForLoadTime: number | null = null;
    const reportDwellTime = (projectName: string, reportUrl: string) => {  // 防抖:如果当前加载时间段已经上报过,直接跳过  if (lastDwellReportedForLoadTime === pageLoadTime) return;
      // ... 计算并上报 ...
      // 标记已上报  lastDwellReportedForLoadTime = pageLoadTime;};

    「(6) 用户点击监控 (事件委托)」

    如果给页面上每个按钮都单独绑定事件,性能会很差。

    更高效的做法是利用「事件冒泡」:只在最外层的 document 上绑定一个监听器。不管用户点了哪个按钮,事件最终都会冒泡到 document,我们在这里统一拦截处理。

    const trackClicks = (projectName: string, reportUrl: string) => {  document.addEventListener('click', (event) => {    // 只关心那些带 data-track-click 属性的元素    const target = event.target as HTMLElement;    if (target && target.dataset.trackClick) {       // ... 上报 ...    }  });};

    「使用方式」:在 HTML 元素上添加属性即可自动采集。

    <buttondata-track-click="buy_button">购买</button>

    「3. 数据存储 (storage.ts)」

    这一层主要充当 SDK 的「记性」。它得清楚地记得:「这个用户是谁?今天来了几次?今天有没有报到过?」

    为了保证刷新页面也不会“失忆”,我们利用浏览器的 localStorage 来实现持久化存储。

    • 「唯一用户 ID」:用户首次访问时生成一个 UUID(唯一标识 ID),像发身份证一样存入 localStorage。

    • 「PV 日统计」:按「日期」作为 Key 来计数,每天重新开始。

    • 「UV 标记」:记录「每日 UV」,确保每天只上报一次。

    「核心逻辑拆解:」

    1. 「你是谁?(获取 UserID)」
    • 「逻辑」:先去 localStorage 翻翻有没有身份证(USER_ID)。

    • 「有」:直接用,说明是老熟人。

    • 「没有」:说明是新客,立刻给他印一张新身份证(生成 UUID),并存起来,下次来就认识了。

    /** * @description: 获取用户ID * @return {string} 用户ID */export const getUserID = (): string => {  let userId = localStorage.getItem(USER_ID_KEY);  if (!userId) {    // 给他发个新身份证    userId = generateUUID();    // 存起来,下次就认识了    localStorage.setItem(USER_ID_KEY, userId);  }  return userId;};
    /** * @description: 生成唯一标识符 * 简单来说,这就是用来生成一个独一无二的字符串 ID。 * 它通过随机替换模板中的字符来保证唯一性,就像给每个人发一个不重复的号码牌。 * @return {string} 唯一标识符 */ const generateUUID = (): string => {  return 'xxxx-xxxx-4xxx-yxxx-xxxx'.replace(/[xy]/g, (char) => {    const random = (Math.random() * 16) | 0;    const value = char === 'x' ? random : (random & 0x3) | 0x8;    return value.toString(16);  });};
    1. 「今天来了第几次?(PV 计数)」

    • 「逻辑」:不能只存一个总数,因为 PV 通常是按「天」统计的。

    • 「技巧」:在 Key 里带上日期,比如 pv_count_2023-10-01。

    • 这样到了第二天,日期变了,Key 也变了,计数器自动归零,重新开始。

     /* 当天 PV +1 */export const incrementPV = (): number => {  // 获取当天的日期  const today = new Date().toISOString().split('T')[0];  const pvData = localStorage.getItem(`${PV_COUNT_KEY}_${today}`);  const newPV = (pvData ? parseInt(pvData, 10) : 0) + 1;  localStorage.setItem(`${PV_COUNT_KEY}_${today}`, newPV.toString());  return newPV;};
    1. 「今天记过人头了吗?(UV 标记)」

    • 「逻辑」:UV 是按天去重的。如果今天已经上报过这个人的 UV 了,就别再发了,省流量。

    • 「实现」:在 localStorage 里存一个标记 uv_record_date = '2023-10-01'。每次初始化时检查一下,如果存的日期是今天,说明“已阅”,不用再报。

     /* 当前版本:存在即认为已记录 */export const isUVRecorded = (): boolean => {  const today = new Date().toISOString().split('T')[0];  return localStorage.getItem(UV_STORAGE_KEY) === today;};

    「4. 数据上报(sender.ts)」

    收集到数据后,如何发给后端?这看似简单,实则暗藏玄机。

    1. 核心痛点:页面关了,请求还没发完怎么办?

    用户看完网页直接关掉(或者刷新跳转),这时候浏览器会无情地杀掉当前页面进程里所有正在跑的异步请求(XHR/Fetch)。

    结果就是:「监控数据还没发出去,就死在半路上了。」

    2. 解决方案

    为了确保数据必达,我们采用一套组合拳:

    1. 「首选」「Navigator.sendBeacon」:

      它是专门为“页面卸载上报”设计的。

      「特点」:浏览器会在后台默默把数据发完,不阻塞页面关闭,也不会被杀掉。

    2. 「次选」「fetch」「+」「keepalive」:

      如果浏览器不支持 Beacon,或者你需要自定义 Header(Beacon 不支持自定义 Header),就用 「fetch」 并开启 「keepalive: true」。

      「特点」:告诉浏览器“这个请求很重要,页面关了也请帮我发完”。

    3. 代码实现

    export const sendBehaviorData = (data: Record<string, any>, url: string) => {  // 1. 包装数据:加上一些公共信息(比如 UserAgent,屏幕分辨率等)  const dataToSend = {    ...data,    userAgent: navigator.userAgent,    // screenWidth: window.screen.width, // 可选  };
      // 2. 优先使用 sendBeacon (最稳,且不阻塞)  // 注意:sendBeacon 不支持自定义 Content-Type,默认是 text/plain  // 这里用 Blob 强制指定为 application/json  if (navigator.sendBeacon) {    const blob = new Blob([JSON.stringify(dataToSend)], {      type: 'application/json',    });
        // sendBeacon 返回 true 表示进入队列成功    navigator.sendBeacon(url, blob);
          return;   }
      // 3. 降级方案:使用 fetch + keepalive  // 即使页面关闭,keepalive 也能保证请求发出  fetch(url, {    method: 'POST',    headers: { 'Content-Type': 'application/json' },    body: JSON.stringify(dataToSend),    keepalive: true, // <--- 关键参数!防止页面关闭时请求被杀  }).catch((err) => {    console.error('上报失败:', err);  });};

    3. 工程化构建配置

    既然是 SDK,最好的分发方式当然是发布到 NPM。这样其他项目只需要一行命令就能接入你的前端错误监控系统。

    这里我们选择 「Rollup」对代码进行打包,因为它比 Webpack 更适合打包库(Library),生成的代码更简洁。

    3.1 package 配置 (package.json)

    package.json 不仅仅是依赖管理,它还定义了你的包如何被外部使用。配置不当会导致用户引入报错或无法获得代码提示。

    {  "name": "behavior-monitor-sdk",  "version": "1.0.0",  "description": "A lightweight front-end behavior monitoring SDK",  "main": "dist/index.cjs.js", // CommonJS 入口  "module": "dist/index.esm.js", // ESM 入口  "browser": "dist/index.umd.js", // UMD 入口  "type": "module",  "scripts": {    "build": "rollup -c",    "dev": "rollup -c -w"  },  "keywords": ["behavior-monitor", "frontend", "sdk"],  "license": "MIT",  "files": ["dist"], // 发布时仅包含 dist 目录  "devDependencies": {    "rollup": "^4.9.0",    "@rollup/plugin-typescript": "^11.1.0",    "@rollup/plugin-terser": "^0.4.0", // 用于压缩代码    "typescript": "^5.3.0",    "tslib": "^2.6.0"  }}

    「💡 关键字段解读:」

    • 「name」: 包的“身份证号”。在 NPM 全球范围内必须唯一,发布前记得先去搜一下有没有重名。
    • 「入口文件“三剑客”」(决定了别人怎么引用你的包):
      • 「main」: 「CommonJS 入口」。给 Node.js 环境或老旧构建工具(如 Webpack 4)使用的。
      • 「module」: 「ESM 入口」。给现代构建工具(Vite, Webpack 5)使用的。支持 Tree Shaking(摇树优化),能减小体积。
      • 「browser」: 「UMD 入口」。给浏览器直接通过 <script> 标签引入使用的(如 CDN)。
    • 「files」: 「发布白名单」。指定 npm publish 时只上传哪些文件(这里我们只传编译后的 dist 目录)。源码、测试代码等不需要发上去,以减小包体积。

    3.2 TypeScript 配置 (tsconfig.json)

    我们需要配置 TypeScript 如何编译代码,并生成类型声明文件(.d.ts),这对使用 TS 的用户非常友好。

    {  "compilerOptions": {    "target": "es5", // 编译成 ES5,兼容旧浏览器    "module": "esnext", // 保留 ES 模块语法,交给 Rollup 处理    "declaration": true, // 生成 .d.ts 类型文件 (关键!)    "declarationDir": "./dist", // 类型文件输出目录    "strict": true, // 开启严格模式,代码更健壮    "moduleResolution": "node" // 按 Node 方式解析模块  },  "include": ["src/**/*"] // 编译 src 下的所有文件}

    3.3 Rollup 打包配置 (rollup.config.js)

    为了兼容各种使用场景,我们配置 Rollup 输出三种格式:

    1. 「ESM (.esm.js)」: 给现代构建工具(Vite, Webpack)使用,支持 Tree Shaking。
    2. 「CJS (.cjs.js)」: 给 Node.js 或旧版工具使用。
    3. 「UMD (.umd.js)」: 可以直接在浏览器通过 <script> 标签引入,会挂载全局变量。
    import typescript from '@rollup/plugin-typescript';import terser from '@rollup/plugin-terser';
    export default {  input: 'src/index.ts', // 入口文件  output: [    {      file: 'dist/index.cjs.js',      format: 'cjs',      sourcemap: true,    },    {      file: 'dist/index.esm.js',      format: 'es',      sourcemap: true,    },    {      file: 'dist/index.umd.js',      format: 'umd',      name: 'frontendBehaviorMonitor', // <script> 引入时的全局变量名',      sourcemap: true,      plugins: [terser()], // UMD 格式进行压缩体积    },  ],  plugins: [    typescript({      tsconfig: './tsconfig.json',      declaration: true,      declarationDir: 'dist',    }),  ],};

    4. 发布到 NPM (保姆级教程)

    4.1 准备工作

    1. 「注册账号」:去 npmjs.com 注册一个账号(记得验证邮箱,否则无法发布)。
    2. 「检查包名」:在 NPM 搜一下你的 package.json 里的 name,确保没有被占用。如果不幸重名,改个独特的名字,比如 behavior-monitor-sdk-vip。

    4.2 终端操作三步走

    打开终端(Terminal),在项目根目录下操作:

    「第一步:登录 NPM」

    npm login
    • 输入命令后按回车,浏览器会弹出登录页面。
    • 或者在终端根据提示输入用户名、密码和邮箱验证码。
    • 登录成功后会显示 Logged in as <your-username>.
    • 注意:如果你之前切换过淘宝源,发布时必须切回官方源:npm config set registry https://registry.npmjs.org/

    「第二步:打包代码」

    确保 dist 目录是最新的,不要发布空代码。

    npm run build

    「第三步:正式发布」

    npm publish --access public
    • --access public 参数用于确保发布的包是公开的(特别是当包名带 @ 前缀时)。
    • 看到 + [email protected] 字样,恭喜你,发布成功!

    现在,全世界的开发者都可以通过 npm install behavior-monitor-sdk 来使用你的作品了!

    5. 如何使用

    SDK 发布后,支持多种引入方式,适配各种开发场景。

    方式 1:NPM + ES Modules (推荐)

    适用于现代前端项目(Vue, React, Vite, Webpack 等)。

    # 请将 behavior-monitor-sdk 替换为你实际发布的包名npm install behavior-monitor-sdk

    在你的业务代码入口(如 main.ts 或 app.js)引入并初始化:

    // 请将 initUserBehaviorMonitor 替换为你实际发布的包名import { initUserBehaviorMonitor } from 'behavior-monitor-sdk';
    initUserBehaviorMonitor({  projectName: 'MyMallProject', // 项目名称  reportUrl: 'https://api.yourserver.com/v1/report' // 上报接口地址});

    方式 2:CDN 直接引入

    适用于不使用构建工具的传统项目或简单的 HTML 页面。

    <!-- 请将 behavior-monitor-sdk 替换为你实际发布的包名,x.x.x 替换为具体版本号 --><scriptsrc="https://unpkg.com/[email protected]/dist/index.umd.js"></script>
    <script>  // UMD 版本会将 SDK 挂载到 window.frontendBehaviorMonitor  window.frontendBehaviorMonitor.initUserBehaviorMonitor({    projectName: 'MyMallProject',    reportUrl: 'https://api.yourserver.com/v1/report',  });</script>

    方式 3:埋点使用说明 (关键)

    本 SDK 支持自动采集 PV、UV 和停留时长,但「点击事件」需要手动标记。

    在需要监控点击的元素上添加 data-track-click 属性,值为该按钮的业务标识:

    <!-- 比如:监控购买按钮的点击 --><buttondata-track-click="buy_now_btn">立即购买</button>
    <!-- 比如:监控轮播图点击 --><divdata-track-click="banner_ad_01">...</div>

    6. 总结与展望

    至此,我们已经亲手打造了一个麻雀虽小、五脏俱全的前端行为监控 SDK。

    回顾这段旅程,我们不仅实现了代码,更重要的是深入理解了浏览器的底层机制:

    • 「知其然」:学会了如何监听 PV、UV、点击和停留时长。
    • 「知其所以然」:理解了 history API 的劫持原理、sendBeacon 的可靠性优势以及事件委托的性能价值。

    当然,这只是一个起点。在企业级的生产环境中,你还可以继续扩展:

    1. 「数据可视化」:搭建一个后端服务和看板,将上报的数据绘制成精美的图表。

    2. 「性能监控」:结合 Performance API,监控首屏加载时间 (FCP)、最大内容绘制 (LCP) 等性能指标。

    3. 「错误监控」:监听 error 和 unhandledrejection 事件,捕获 JS 报错和接口异常。

      ❝

      🚀 「阅读」:关于错误监控的详细实现,请参考上一篇 【错误监控】别只做工具人了!手把手带你写一个前端错误监控 SDK,技术的深度往往隐藏在这些看似平常的“轮子”之中。希望本文能成为你探索前端工程化与底层原理的一把钥匙。

      ❞

    「贪多嚼不烂,这次我们先聚焦在最核心的“行为监控”闭环。」

    至于性能监控等其他进阶玩法,「我们下篇文章接着聊」,带你一步步把这个系统打磨得更完美。

    希望这篇文章能是你打造专属监控系统的起点。「Happy Coding!」

    图片
    点击"阅读原文"了解详情~