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

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

想知道那些"黑科技"是如何拦截点击、统计 PV/UV、精确计算页面停留时长的吗?

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

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

学习完本文,你将收获什么?

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

  1. 现代浏览器事件模型 :掌握 loadbeforeunloadvisibilitychangepagehide 的适用场景与避坑指南。

  2. SPA 路由监听的核心技巧 :深入理解如何劫持 history.pushState / replaceState 并结合 popstate 实现无缝的单页应用路由监控。

  3. 高可靠数据上报 :掌握 Navigator.sendBeacon 的使用场景,确保在页面卸载时也能稳定上报数据。

  4. 工程化实践:学习如何设计一个解耦的 SDK(入口、采集、存储、上报分离),并了解从开发到 NPM 发布的全流程

预备知识:核心概念解析

在开始设计系统之前,我们需要先理清两个最基础也是最重要的指标:

💡 PV 和 UV 到底有啥区别?

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

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

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

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

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

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


系统架构与功能设计

1. 监控目标

SDK 旨在捕获以下核心数据:

* 点击统计(Click) :用户点了哪个按钮?(比如"购买"按钮被点了多少次)。

* 页面浏览量(PV) :页面访问频次。

* 唯一用户访问量(UV) :基于用户 ID 的去重访问量。

* 页面停留时长(Page Dwell Time) :用户在页面上看了多久?(是秒关,还是看了很久?)

2. 核心思路

* PV 监控 :监听 window.load 事件,记录加载时间与用户 ID,上报 behavior: 'pv'

* UV 监控 :服务端去重是标准做法,但客户端可通过 localStorage 标记(isUVRecorded)减少无效上报。

* 点击监控 :利用事件委托 ,监听全局 click 事件,通过 data-track-click 属性识别埋点元素。

* 停留时长 :结合 beforeunloadvisibilitychange 事件,在用户离开或切换标签页时计算并上报时长。对于 SPA,还需要在路由切换时(劫持 history 等API)上报前一个页面的停留时长。 *

3. 目录结构

采用以下模块化设计,职责清晰,核心实现代码都在src下

bash 复制代码
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

核心代码实现

主入口 (index.ts)

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

ts 复制代码
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 是总指挥,它负责启动所有的监控任务:

ts 复制代码
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);
};

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

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

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

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

SPA和MPA区别:

  • MPA (Multi-Page Application) :传统多页应用(如京东首页跳转详情页),每次跳转都会重新加载 HTML,触发 window.load

  • SPA (Single-Page Application) :现代单页应用(如 Vue/React 项目),页面跳转不刷新浏览器,只是 JS 换了内容,不会 触发 window.load

(2) PV 与来源 (Referrer)

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

typescript 复制代码
const trackMpaPageView = (projectName: string, reportUrl: string) => {
  window.addEventListener('load', () => {
    // ......
  });
};

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

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

  • MPA:每次跳转都是一次全新的页面加载,浏览器会自动处理一切。

  • SPA:跳转只是 JS 修改了 URL 和 DOM,浏览器不会自动触发加载事件。

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

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

  1. 代码跳转 (如 router.push 或者 router.replace
  • 现象 :JS 里调用了 history.pushState 或 history.replaceState

  • 坑点 :浏览器这时候是装死 的,它悄悄改了 URL,但完全不通知任何人(不触发事件)。

  • 对策 :我们得把pushState和replaceState这两个原生方法劫持(重写) 了,在它干活之前,先插播一段我们的上报逻辑。

  1. 浏览器后退/前进
  • 现象:用户点了浏览器左上角的回退或者前进箭头。

  • 坑点 :这回不触发 pushState 了,而是触发 popstate事件了。

  • 对策 :老老实实监听 popstate 事件。

  1. Hash 模式 (#)
  • 现象 :URL 里的 # 变了。

  • 对策 :监听 hashchange 事件。

关键技巧:Referrer 接力

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

ts 复制代码
const trackSpaBehavior = (projectName: string, reportUrl: string) => {
  const handleRouteChange = () => {
    // 1. 结算上一页:上报前一个页面的停留时间
    reportDwellTime(projectName, reportUrl);

    // 2. 更新状态:保存当前 URL,作为下一次跳转的 referrer
    pageLoadTime = Date.now();
    lastPageUrl = window.location.href;

    // 3. 记录新页面:上报 PV,并将 lastPageUrl 作为 referrer 上报
    const userId = getUserID();
    const pv = incrementPV();
    sendBehaviorData({
      behavior: 'pv',
      userId,
      projectName,
      timestamp: new Date().toISOString(),
      pageUrl: window.location.href,
      referrer: lastPageUrl, // SPA 内部跳转,来源是上一个页面
      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) 停留时长防抖

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

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

ts 复制代码
// 记录上一次上报停留时间的时间戳
let lastDwellReportedForLoadTime: number | null = null;

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

(5) 点击监控 (事件委托)

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

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

ts 复制代码
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 元素上添加属性即可自动采集。

html 复制代码
<button data-track-click="buy_button">购买</button>

3. 数据存储 (storage.ts)

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

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

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

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

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

核心逻辑拆解:

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

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

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

ts 复制代码
/**
 * @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 也变了,计数器自动归零,重新开始。

ts 复制代码
 /* 当天 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'。每次初始化时检查一下,如果存的日期是今天,说明"已阅",不用再报。

ts 复制代码
 /* 当前版本:存在即认为已记录 */
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

    1. 它是专门为"页面卸载上报"设计的。
    2. 特点:浏览器会在后台默默把数据发完,不阻塞页面关闭,也不会被杀掉。
  2. 次选 fetch + keepalive

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

    2. 特点:告诉浏览器"这个请求很重要,页面关了也请帮我发完"。

3. 代码实现

ts 复制代码
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 不仅仅是依赖管理,它还定义了你的包如何被外部使用。配置不当会导致用户引入报错或无法获得代码提示。

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 的用户非常友好。

json 复制代码
{
  "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> 标签引入,会挂载全局变量。
javascript 复制代码
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,确保没有被占用。如果不幸重名,改个独特的名字,比如 error-monitor-sdk-vip

4.2 终端操作三步走

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

第一步:登录 NPM

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

第二步:打包代码

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

bash 复制代码
npm run build

第三步:正式发布

bash 复制代码
npm publish --access public
  • --access public 参数用于确保发布的包是公开的(特别是当包名带 @ 前缀时)。
  • 看到 + behavior-monitor-sdk@1.0.0 字样,恭喜你,发布成功!

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

5. 如何使用

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

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

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

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

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

typescript 复制代码
// 请将 initUserBehaviorMonitor 替换为你实际发布的包名
import { initUserBehaviorMonitor } from 'behavior-monitor-sdk';

initErrorMonitor({
  projectName: 'MyMallProject', // 项目名称
  reportUrl: 'https://api.yourserver.com/v1/report' // 上报接口地址
});

方式 2:CDN 直接引入

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

html 复制代码
<!-- 请将 behavior-monitor-sdk 替换为你实际发布的包名,x.x.x 替换为具体版本号 -->
<script src="https://unpkg.com/behavior-monitor-sdk@x.x.x/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 属性,值为该按钮的业务标识:

html 复制代码
<!-- 比如:监控购买按钮的点击 -->
<button data-track-click="buy_now_btn">立即购买</button>

<!-- 比如:监控轮播图点击 -->
<div data-track-click="banner_ad_01">...</div>

6. 总结与展望

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

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

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

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

  1. 数据可视化:搭建一个后端服务和看板,将上报的数据绘制成精美的图表。
  2. 性能监控 :结合 Performance API,监控首屏加载时间 (FCP)、最大内容绘制 (LCP) 等性能指标。
  3. 错误监控 :监听 errorunhandledrejection 事件,捕获 JS 报错和接口异常。

技术的深度往往隐藏在这些看似平常的"轮子"之中。希望本文能成为你探索前端工程化与底层原理的一把钥匙。

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

至于上面那些进阶玩法,我们下篇文章接着聊,带你一步步把这个系统打磨得更完美。

相关推荐
king王一帅2 小时前
AI 时代真正流式解析+渲染双重优化的 Incremark
前端·ai编程·markdown
Glommer2 小时前
AST 反混淆处理示例(二)
javascript·爬虫
aesthetician3 小时前
用铜钟听歌,发 SCI !
前端·人工智能·音频
Mike_jia3 小时前
LogWhisperer 全解析:打造你的Linux服务器AI日志分析中枢
前端
网安Ruler3 小时前
崭新出厂,自研CipherForge小工具,攻破 D-Link M30 固件加密
前端·网络·python
daxiang120922053 小时前
记一次前端请求报错:Content-Length can‘t be present with Transfer-Encoding,+Cursor使用教训
前端·cursor
武清伯MVP3 小时前
深入了解Canvas:HTML5时代的绘图利器(二)
前端·html5·canvas
float_六七3 小时前
Spring AOP表达式速查手册
前端·javascript·spring
PineappleCoder3 小时前
没 CDN = 用户等半天?四大核心机制:就近、分流、提速、容错全搞定
前端·性能优化