阅读视图

发现新文章,点击刷新页面。

WebTab等插件出事后:不到100行代码,带你做一个干净透明的新标签页

新闻截图.png

前段时间我写过《别再无脑装插件了,你的浏览器可能在偷家》juejin.cn/post/755347… ,提醒大家:浏览器插件的权限远比想象大。

结果没过多久,就看到有些标签页插件被曝偷偷上传数据的新闻。

女朋友问我:“那我们还敢装吗?

我说:“别慌,要不我们自己做一个?

实话讲,浏览器扩展不是小玩具。它能读写你的本地存储、拦截网络请求、看你的标签页和历史,甚至通过特权接口拿到一些你以为安全的东西;一旦失守,隐私就是敞开的大门。与其盲装、把钥匙交给别人,不如把门锁装回自己手里,做一个干净透明的浏览器标签页,知根知底。

做一个新标签页其实不难:一份 manifest,几行 HTML/CSS,再加一丢丢 JS,就能把主页改成你喜欢的样子。权限只留必要的、能不发网请求就不发、数据都放在本地;代码简单到你一眼能审、每一步都看得懂。

如果你读过那篇“别再无脑装插件”的提醒,这就是它的行动版。而且写标签页这事跟写网站页面没啥区别:本质就是“一个普通网页 + 一份清单”。

这篇文章用不到100行代码,带你做一个干净透明的新标签页,做完你会发现:新标签页并不复杂,更不必神秘——最重要的是,你清楚每一行代码在做什么,安心又可控。

先看效果

标签页演示.gif

项目结构

newtab
├── icons              # 插件的"图标"
├── index.html         # 标签页页面
├── manifest.json      # 插件身份证
├── newtab.js          # 标签页页面脚本
└── style.css          # 标签页页面样式

快速开始

从github仓库拉代码,本地安装

5分钟搞定安装:下载仓库代码 → 加载扩展 → 开始使用!

🚀 浏览项目的完整代码可以点击这里 github.com/Teernage/ne…,如果对你有帮助欢迎Star。

一步步来

  1. 创建文件夹 newtab

  2. 创建manifest.json文件

这个文件是插件的身份证,告诉浏览器你的插件是谁、能干啥。

{
  "manifest_version": 3,
  "name": "极简新标签页",
  "version": "1.0.0",
  "icons": {
    "16": "icons/16.png",
    "32": "icons/32.png",
    "48": "icons/48.png",
    "128": "icons/128.png"
  },
  "chrome_url_overrides": {
    "newtab": "index.html"
  }
}    

关键点解读:

字段 说明
manifest_version: 3 使用最新的 Manifest V3 扩展规范
name 插件名称
version 插件版本号
icons 图标集
chrome_url_overrides 覆盖新标签页入口

最关键的是 chrome_url_overrides :它会把 Chrome 的内置页面(最常见是 chrome://newtab )换成你插件包里的 HTML 页面。只需在manifest文件中把 newtab 指向 index.html ,用户每次打开新标签页看到的就是你的 index.html 内容,这就是插件自定义新标签页的核心原理。

  1. 创建index.html文件

index.html就是你要自定义的标签页

🔗 文件源码链接

  1. 创建manifest.json文件

🔗 文件源码链接

  1. newtab.js文件

作用:时间更新、搜索行为、图标数据驱动渲染

🔗 文件源码链接

  1. style.css文件

作用:标签页的样式

🔗 文件源码链接

  1. 创建文件夹 icons,文件夹中的图标在下面的仓库中下载

作用:扩展展示用图标集

🔗 源码链接

一键安装

  1. 打开Chrome浏览器
  2. 地址栏输入:chrome://extensions/
  3. 打开右上角"开发者模式"
  4. 点击"加载已解压的扩展程序"
  5. 选择newtab文件夹
  6. 搞定!扩展安装完成!

操作演示图:

标签页插件安装演示.gif

以上只是一个入门级的新标签页demo,你可以按喜好继续扩展成自己的版本。如果觉得纯原生 JS 在维护性上不够友好,推荐使用我的Vue 的插件脚手架juejin.cn/post/754380… :内置基于 Vue 的新标签页模板、侧边栏模板、Popup 弹窗模板等,用 Vue 开发更顺手、代码组织更清晰。若想实现类似 Infinity 新标签页的图标列表拖拽效果,也可以参考我的拖拽指令一个Vue自定义指令搞定丝滑拖拽列表,告别复杂组件封装 juejin.cn/post/751133…

总结

  • 核心原理:用一份 manifest.json 把 chrome://newtab 指向你的 index.html ,本质就是“一个普通网页 + 一份清单”,不需要额外权限即可接管新标签页。
  • 目录职责清晰: manifest.json 负责接管入口, index.html 提供骨架, style.css 管视觉, newtab.js 管交互与数据, icons/ 放扩展图标。
  • 安全与隐私:尽量使用本地资源、最小权限、不做不必要的网络请求;每一行代码看得懂、改得动,才能放心使用。
  • 可扩展建议:如果需要更强的可维护性与组件化,使用基于 Vue 的插件脚手架;若要实现类似 Infinity 的图标拖拽,参考拖拽指令即可快速增强交互。

如果觉得对您有帮助,欢迎点赞 👍 收藏 ⭐ 关注 🔔 支持一下!

往期实战推荐:

【错误监控】别只做工具人了!手把手带你写一个前端错误监控 SDK

你是否一直对前端错误监控系统的底层原理充满好奇?

想知道那些“黑科技”是如何拦截报错、上报数据的吗?

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

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

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

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

  1. 浏览器底层原理:事件冒泡/捕获机制,以及 onerrorunhandledrejection 等 API 的工作细节。
  2. AOP 面向切面编程:学会如何通过劫持(Hook)原生方法(如 XMLHttpRequestfetch)来实现无感监控。
  3. 高可靠数据上报:掌握 Navigator.sendBeacon 的使用场景,确保在页面卸载时也能稳定上报数据。
  4. 工程化实践:从架构设计到 NPM 发布,体验完整的 SDK 开发全流程。

1. 架构设计

别被“监控系统”这四个字吓到了。拆解下来,核心逻辑就三步:监听 -> 收集 -> 上报

在开始编码之前,我们先梳理一下 SDK 的整体架构。我们需要监控 JS 运行时错误网络请求错误 以及 资源加载错误,并将这些数据统一格式化后上报到服务端。

graph TD
    A[应用启动] --> B{SDK 初始化}
    B --> C[JS 错误监控]
    B --> D[网络请求监控]
    B --> E[资源加载监控]

    C -->|捕获 onerror/unhandledrejection| F[错误处理中心]
    D -->|劫持 XHR/Fetch| F
    E -->|捕获 error 事件| F

    F --> G[数据组装]
    G --> H[上报模块]
    H -->|Navigator.sendBeacon / Fetch| I[服务端 API]
    I --> J[数据库/日志系统]

项目结构

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

error-monitor/
├── dist/                # 打包产物
├── src/                 # 源码目录
│   ├── index.ts         # 入口文件
│   ├── errorHandler.ts  # JS 错误捕获
│   ├── networkMonitor.ts# 网络请求监控
│   ├── resourceMonitor.ts# 资源加载监控
│   ├── sender.ts        # 上报逻辑
│   └── utils.ts         # 工具函数
├── test/                # 测试靶场
│   ├── server.js        # 本地测试服务
│   └── index.html       # 错误触发页面
├── package.json         # 项目配置
├── rollup.config.js     # Rollup 打包配置
├── tsconfig.json        # TypeScript 配置
└── README.md

错误监控源码在 src目录下 ,最终使用rollup对代码进行打包,dist是打包产物 ; test目录下是对打包产物的测试:能否拦截 JS/请求/资源错误,能否稳妥上报。现在就从 0 到 1 开干,做个mini版的错误监控 SDK

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

2. 核心实现详解

2.1 SDK 初始化入口 (index.ts)

SDK 的入口主要负责接收配置(如上报地址、项目名称)并启动各个监控模块。

// src/index.ts
import { monitorJavaScriptErrors } from './errorHandler';
import { monitorNetworkErrors } from './networkMonitor';
import { monitorResourceErrors } from './resourceMonitor';

interface ErrorMonitorConfig {
  reportUrl: string; // 上报接口地址
  projectName: string; // 项目标识
  environment: string; // 环境 (dev/prod)
}

export const initErrorMonitor = (config: ErrorMonitorConfig) => {
  const { reportUrl, projectName, environment } = config;

  // 启动三大监控模块
  monitorJavaScriptErrors(reportUrl, projectName, environment);
  monitorNetworkErrors(reportUrl, projectName, environment);
  monitorResourceErrors(reportUrl, projectName, environment);
};

2.2 全局异常捕获 (errorHandler.ts)

这是错误监控的核心部分。JavaScript 的错误主要分为两类,我们需要分别处理:

  1. 常规运行时错误:代码逻辑错误(如 undefined is not a function)。这部分由老牌的 window.onerror 事件负责,它能提供详细的行号、列号和堆栈信息。
  2. Promise 异常:随着 async/await 的普及,未被 catch 的 Promise 错误越来越常见。这部分错误 不会 触发 onerror,需要通过监听 unhandledrejection 事件来捕获。

双管齐下,才能确保代码逻辑错误不被遗漏。

// src/errorHandler.ts
import { sendErrorData } from './sender';

export const monitorJavaScriptErrors = (
  reportUrl: string,
  projectName: string,
  environment: string
) => {
  // 1. 捕获 JS 运行时错误
  const originalOnError = window.onerror;
  window.onerror = (message, source, lineno, colno, error) => {
    const errorInfo = {
      type: 'JavaScript Error',
      message,
      source,
      lineno,
      colno,
      stack: error ? error.stack : null,
      projectName,
      environment,
      timestamp: new Date().toISOString(),
    };
    sendErrorData(errorInfo, reportUrl);

    // 关键点:如果原来有 onerror 处理函数,继续执行它,避免覆盖用户逻辑
    // 这样做是为了不破坏宿主环境(例如用户自己写的或其他 SDK)已有的错误处理逻辑
    if (originalOnError) {
      return originalOnError(message, source, lineno, colno, error);
    }
  };

  // 2. 捕获未处理的 Promise Rejection
  const originalOnUnhandledRejection = window.onunhandledrejection;
  window.onunhandledrejection = (event) => {
    const errorInfo = {
      type: 'Unhandled Promise Rejection',
      message: event.reason?.message || event.reason,
      stack: event.reason?.stack,
      projectName,
      environment,
      timestamp: new Date().toISOString(),
    };
    sendErrorData(errorInfo, reportUrl);

    // 关键点:执行原有的 Promise 错误处理逻辑
    // 这样做是为了不破坏宿主环境(例如用户自己写的或其他 SDK)已有的错误处理逻辑
    if (originalOnUnhandledRejection) {
      return originalOnUnhandledRejection.call(window, event);
    }
  };
};

2.3 网络请求监控 (networkMonitor.ts)

接口监控是监控的难点,因为浏览器并没有提供一个全局的 onNetworkError 事件。

怎么办?

我们需要使用 AOP(面向切面编程) 的思想,重写浏览器原生的 XMLHttpRequestfetch方法。

简单来说,就是把原生的方法“包”一层:在请求发出前/响应返回后,插入我们的监控代码,然后再执行原有的逻辑。这样业务代码完全无感知,而我们却能拿到所有的请求细节。

// src/networkMonitor.ts
export const monitorNetworkErrors = (
  reportUrl: string,
  projectName: string,
  environment: string
) => {
  // 1. 劫持 XMLHttpRequest
  const originalXhrOpen = XMLHttpRequest.prototype.open;
  XMLHttpRequest.prototype.open = function (
    method: string,
    url: string | URL,
    ...args: any[]
  ) {
    // 关键点:排除上报接口自身的请求,防止死循环
    const urlStr = typeof url === 'string' ? url : String(url);
    if (urlStr.includes(reportUrl)) {
      return originalXhrOpen.apply(this, [method, url, ...args] as any);
    }

    // 监听 error 事件
    this.addEventListener('error', () => {
      sendErrorData(
        {
          type: 'Network Error',
          message: `Request Failed: ${method} ${url}`,
          projectName,
          environment,
        },
        reportUrl
      );
    });
    return originalXhrOpen.apply(this, [method, url, ...args] as any);
  };

  // 2. 劫持 Fetch
  const originalFetch = window.fetch;
  window.fetch = async (input, init) => {
    // 关键点:排除上报接口自身的请求,防止死循环
    const urlStr = (input instanceof Request) ? input.url : String(input);
    if (urlStr.includes(reportUrl)) {
      return originalFetch(input, init);
    }

    try {
      const response = await originalFetch(input, init);
      if (!response.ok) {
        sendErrorData(
          {
            type: 'Fetch Error',
            message: `HTTP ${response.status}: ${response.statusText}`,
            url: input instanceof Request ? input.url : input,
            projectName,
            environment,
          },
          reportUrl
        );
      }
      return response;
    } catch (error) {
      // 网络故障等无法发出请求的情况
      sendErrorData(
        {
          type: 'Fetch Error',
          message: `Fetch Failed: ${input}`,
          projectName,
          environment,
        },
        reportUrl
      );
      throw error;
    }
  };
};

2.4 资源加载监控 (resourceMonitor.ts)

使用 window.onerror 检测不到资源的错误,因为 资源加载失败(如 img/script src 404)产生的 error 事件是不会冒泡的

window.onerror 依靠事件冒泡来捕获错误,所以它对资源错误无能为力。

但是 window.addEventListener('error', handler, true)捕获阶段 可以将资源加载的错误“拦截”下来。

所以资源加载监控这里,我们使用 window.addEventListener('error', () => {}, true) 来进行监控。

// src/resourceMonitor.ts
export const monitorResourceErrors = (
  reportUrl: string,
  projectName: string,
  environment: string
) => {
  // 注意:useCapture 设置为 true,在捕获阶段处理
  window.addEventListener(
    'error',
    (event) => {
      const target = event.target as HTMLElement;
      // 过滤掉 window 自身的 error,只处理资源元素的 error
      if (target && (target.tagName === 'IMG' || target.tagName === 'SCRIPT')) {
        sendErrorData(
          {
            type: 'Resource Load Error',
            message: `Failed to load ${target.tagName}: ${
              target.getAttribute('src') || target.getAttribute('href')
            }`,
            projectName,
            environment,
          },
          reportUrl
        );
      }
    },
    true // 捕获阶段
  );
};

2.5 数据上报 (sender.ts)

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

痛点:页面卸载时的“遗言”发不出去

用户遇到 Bug 的第一反应往往是关闭页面。如果我们使用普通的 fetchXHR 上报:

  1. 异步请求可能会被取消:页面关闭时,浏览器通常会 cancel 掉所有未完成的请求。
  2. 同步请求会阻塞跳转:虽然能强行发出去,但会卡住页面切换,严重影响体验。

救星:Navigator.sendBeacon

sendBeacon 是专门为此场景设计的 API。它有三大优势:

  1. 可靠:即使页面卸载,浏览器也会在后台保证数据发送成功。
  2. 异步:完全不阻塞页面关闭或跳转。
  3. 高效:传输少量数据时性能极佳。

因此,我们的上报策略是:优先 sendBeacon,不支持则降级为 fetch

// src/sender.ts
export const sendErrorData = (errorData: Record<string, any>, url: string) => {
  // 补充浏览器信息(UserAgent 等)
  const dataToSend = {
    ...errorData,
    userAgent: navigator.userAgent,
    // 还可以添加更多环境信息,如屏幕分辨率、当前 URL 等
  };

  // 优先使用 sendBeacon (异步,不阻塞,页面卸载时仍有效)
  if (navigator.sendBeacon) {
    const blob = new Blob([JSON.stringify(dataToSend)], {
      type: 'application/json',
    });
    navigator.sendBeacon(url, blob);
  } else {
    // 降级使用 fetch
    fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(dataToSend),
    }).catch(console.error);
  }
};

💡 知识扩展:经典的 1x1 GIF 打点

你可能听说过用 new Image().src = 'http://api.com/report?data=...' 这种方式上报。这在统计 PV/UV 时非常流行,因为它兼容性极好且天然跨域。

但在错误监控场景下,通常不推荐作为主力方案。 核心原因正是数据量

  1. URL 长度限制:GIF 打点本质是 GET 请求,数据都挂在 URL 上。浏览器对 URL 长度有限制(通常 2KB~8KB)。
  2. 堆栈过长:一个完整的报错堆栈(Stack Trace)动辄几千字符,很容易就被浏览器截断,导致我们看不到关键的报错信息。

所以,对于体积较大的错误数据,走 POST 通道的 sendBeaconfetch 是更稳妥的选择。

2.6 进阶优化:采样与缓冲,别把服务器搞崩了

如果线上出现大规模故障,成千上万的用户同时上报错误,可能会瞬间把监控服务器打挂(DDoS 既视感)。

这时候我们需要引入两个机制:

  1. 采样 (Sampling)

    • 大白话:不要每个错误都报。比如只允许 20% 的运气不好的用户上报,剩下的忽略。这样既能发现问题,又能节省 80% 的流量。
    • 实现if (Math.random() > 0.2) return;
  2. 缓冲 (Buffering)

    • 大白话:不要出一条错就发一个请求,太浪费资源。先把错误攒在数组里,凑够 10 条或者每隔 5 秒统一发一车。
    • 注意:记得在页面卸载(关闭)时,把车上剩下的货强制发出去,别丢了。

3. 工程化构建配置

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

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

3.1 package 配置 (package.json)

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

{
  "name": "error-monitor-sdk",
  "version": "1.0.0",
  "description": "A lightweight front-end error 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": ["error-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: 'ErrorMonitor', // <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

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 参数用于确保发布的包是公开的(特别是当包名带 @ 前缀时)。
  • 看到 + error-monitor-sdk@1.0.0 字样,恭喜你,发布成功!

7141076e-3f21-4ab4-91d3-5f5918624c9b.png

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

5. 如何使用

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

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

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

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

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

// 请将 error-monitor-sdk 替换为你实际发布的包名
import { initErrorMonitor } from 'error-monitor-sdk';

initErrorMonitor({
  reportUrl: 'http://localhost:3000/error-report',
  projectName: 'MyAwesomeProject',
  environment: 'production',
});

方式 2:NPM + CommonJS

适用于 Node.js 环境或旧版打包工具。

# 请将 error-monitor-sdk 替换为你实际发布的包名
npm install error-monitor-sdk
// 请将 error-monitor-sdk 替换为你实际发布的包名
const { initErrorMonitor } = require('error-monitor-sdk');

initErrorMonitor({
  reportUrl: 'http://localhost:3000/error-report',
  projectName: 'MyAwesomeProject',
  environment: 'production',
});

方式 3:CDN 直接引入

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

<!-- 请将 error-monitor-sdk 替换为你实际发布的包名,x.x.x 替换为具体版本号 -->
<script src="https://unpkg.com/error-monitor-sdk@x.x.x/dist/index.umd.js"></script>

<script>
  // UMD 版本会将 SDK 挂载到 window.ErrorMonitor
  ErrorMonitor.initErrorMonitor({
    reportUrl: 'http://localhost:3000/error-report',
    projectName: 'MyAwesomeProject',
    environment: 'production',
  });
</script>

6. 总结与展望

到这里,我们这个“麻雀虽小,五脏俱全”的错误监控 SDK 就算是跑起来了。

回头看看,几百行代码没白写,实打实搞定了三件事:

  1. 啥都能抓:JS 报错、Promise 挂了、接口 500、图片 404,一个都跑不掉,统统收入囊中。
  2. 死活都能报:用了 Navigator.sendBeacon,哪怕用户秒关页面,最后那条“遗言”也能顽强地发给服务器。
  3. 拿来就能用:打包好了三种格式,还送了个“靶场”页面,点点按钮就能看效果,主打一个省心。

不过说实话,这离真正的“企业级”监控还有点距离。

想在生产环境(特别是高流量业务)扛大旗,还得把下面这些坑填了:

  • 别盲猜 Bug:线上代码都是压缩的,得搞定 Sourcemap 还原,不然对着 a.b is not a function 只有哭的份。
  • 页面白了没:有时候没报错但页面一片白,这种“假死”得靠 白屏检测 来发现。
  • 到底快不快:光不报错不够,还得看 性能指标 (FCP/LCP),监控页面加载速度。
  • 用户干了啥:复现 Bug 全靠猜?不行,得把用户出事前的点击、路由跳转全记下来,来个 行为回溯(案发现场还原)。
  • 别把服务器搞崩:报错太多得限流、去重,引入 采样率,不然监控服务先挂了就尴尬了。

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

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

造轮子不是为了重复造,而是为了亲手拆开看看里面的齿轮是怎么转的,这才是学习的本质。

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

不仅免费,还开源?这个 AI Mock 神器我必须曝光它

前两周我做了一个零侵入的接口 Mock 插件。还写了篇掘金文章记录了一下:juejin.cn/post/757098…

此前用 Popup 弹窗做联调,窗小、易误关、操作繁琐;

于是我重构为 Sidebar 常驻侧栏:规则随时可见,刷新也不丢。

  • 换成 Sidebar 侧边栏:钉在页面右边,想开就开,数据都存着,再也不怕手抖关掉。即使刷新页面,规则也不会丢,重新打开就能继续用

  • 接入 AI 自动生成:描述你想要的数据,或者直接贴接口的 TypeScript 类型,AI 秒出结构化数据。手写?不存在的

  • 支持延时和状态码:可以模拟慢接口(延时 3 秒)、接口报错(返回 500)等真实场景

现在用起来爽多了,前端开发不用干等后端,自己就能把页面跑起来。

下文附安装与使用指南,欢迎试用与反馈

如果你现在还在用 Popup 弹窗做 Mock,或者还在一个字一个字敲 JSON, 真心建议试试 Sidebar + AI 这套,用完你就知道什么叫"真香"。

演示效果

mock插件演示.gif

插件功能预览

  • 拦截能力:覆盖 fetch 与 XMLHttpRequest ,规则实时生效,零侵入
  • 匹配规则:支持模糊匹配和完全匹配、正则表达式
  • 响应控制:支持按 HTTP 方法过滤(GET/POST/PUT/DELETE)
  • 异常模拟:支持模拟200/400/401/403/500等状态码
  • AI 生成:输入描述或贴 TS 类型,秒出结构化数据,支持模板与一键覆写
  • 持续优化中,欢迎反馈与建议
  • 建设中: 调试与管理:导入导出、规则分组、Network 面板标识等;性能与匹配体验持续优化,欢迎反馈

什么时候你会需要它?

  • 后端还在写接口:你不用干等,Mock 一下先把前端页面撸出来
  • 测试加载状态:设置延时 2 秒,看看你的 Loading 动画、骨架屏是不是真的在转
  • 测试异常处理:一键切换 200/400/401/403/500,看看你的错误提示是否友好
  • 测试防重复提交:延时 3 秒 + 返回失败,看看用户会不会疯狂点击
  • 给老板/客户演示:不用搭环境、不用连服务器,打开浏览器就能演示

注:支持拦截ajax和fetch请求


🚀 快速开始

获取代码

打开代码仓库,按以下方操作下载插件包。安装包为 zip,请先解压。

下载与解压

  • 在仓库的 Releases 或打包入口下载插件包(zip)。
  • 将 zip 解压到任意目录,得到 AI-Mock-v1.0.0 文件夹。

参考示意图:

下载安装包.gif

一键安装

  1. 打开Chrome浏览器 3.. 地址栏输入:chrome://extensions/
  2. 打开右上角"开发者模式"
  3. 点击"加载已解压的扩展程序"
  4. 选择刚刚解压的AI-Mock-v1.0.0文件夹
  5. 搞定!扩展安装完成!

参考示意图:

安装ai-mock.gif

gitee 仓库链接的同理,在仓库页点击“发行版(Release)”, 进入最新版本页面,下载插件包即可

AI 前置准备

  • 使用 AI 生成前需配置 DeepSeek API Key。为便于体验,提供一个限额试用 Key;额度用尽或失效后,请前往 DeepSeek 官网申请个人 Key 并在侧栏替换
  • 试用 Key(仅用于体验,随时可能失效): sk-9fa67c84581d4f67b61039ff8b199baa

示例:

配置key.gif

DeepSeek 官网申请key示例:

设计与实现

1. Sidebar 侧边栏 vs popup弹窗

特性 Popup Sidebar
显示空间 小(~400x600px) 大(可调整宽度)
操作便捷性 点击外部就关闭 固定显示,不怕误触
与页面交互 互斥 可同时显示
数据持久化 关闭即丢失 刷新页面也保留

2. AI 生成 Mock 数据

这块就两件事:

  • 你给一个 URL,我把它转成正则,用来拦截这个接口
  • 你给数据类型或者一句话描述,AI 直接把 Mock 数据“现做现给”

1. 根据描述输出 Mock 数据

  • 只要一句话就够:
    • 输入:用户列表,包含姓名、年龄、邮箱,来 10 条

效果图:
根据描述输出 Mock 数据.gif

2. 根据类型输出 Mock 数据

  • 或者直接贴 数据的类型:
interface User {
  id: number;
  name: string;
  age: number;
  email: string;
}

效果图:

根据类型输出 Mock 数据.gif

3. 根据输入的地址生成正则表达式

目的:把带占位符的路径,转成更稳的正则,方便在开发态拦截请求

常用示例:

  • 输入: /api/users/:id
  • 正则: ^/api/users/[^/]+/?$
  • 可拦截: /api/users/1 、 /api/users/abc

效果图:

ai生成正则表达式.gif

修改 Mock 数据

  • 这块很直观,给你三种编辑入口,改完就是生效:

    • 规则编辑里改:打开“编辑规则”,在 Mock 数据 (JSON) 文本框里直接改,支持“格式化/验证/复制/清空”,点“保存”后立即应用
  • 放大弹窗改:点击“放大”,全屏 JSON 编辑器更适合改大结构或深层嵌套,改完“验证”一下再“保存”

  • 卡片内联改:在规则卡片下方的“Mock 响应”树视图,鼠标双击某个值就能直接编辑,编辑完进行回车保存。这个适合改小字段(比如把 data.hits.total 从 1 改成 20 )

效果图:

json数据修改方式.gif

同步规则

  • 三个地方改的都是同一份数据,任意入口保存后,其他入口会显示最新值
  • 修改仅作用于“当前这条规则”,不会影响其他规则或接口

推荐使用场景

  • 小改动(单字段/少量数值):用“卡片内联”最快
  • 中等改动(几个字段/一层结构):在“编辑规则”里改,配合“格式化/验证”
  • 大改动(数组长度、层级结构、批量替换):用“放大弹窗”编辑 JSON,更清晰、更不容易漏

操作建议

  • 先“验证”再“保存”:能快速提示 JSON 语法、字段类型不匹配等问题
  • 用“格式化”提升可读性,避免缩进错乱
  • 需要回到初始或重置时,用“清空”或点击“生成”重新出一版数据
  • 要分享或备份当前数据,用“复制”一键拷贝

4. 延时请求 - 测试加载状态

设置延时 3 秒,模拟慢接口:

  • 测试 Loading 动画是否正常
  • 测试骨架屏效果
  • 测试用户会不会重复点击
  • 测试超时逻辑

演示效果图:

延迟效果.gif

4. 状态码 - 测试异常场景

一键切换不同状态码:

  • 200:测试成功流程
  • 400:测试参数校验
  • 401:测试登录跳转
  • 403:测试权限提示
  • 500:测试错误兜底

效果图:

状态码修改.gif


核心实现

1. Manifest V3:从 Popup 到 Sidebar

配置文件:

{
  "manifest_version": 3,
  "name": "AI Mock",
  "version": "2.0.0",
  "side_panel": {
    "default_path": "sidepanel.html"
  },
  "permissions": [
    "sidePanel",
    "storage",
    "scripting"
  ],
  "host_permissions": [
    "<all_urls>"
  ]
}

打开侧边栏:

// background.ts
chrome.action.onClicked.addListener(async (tab) => {
  await chrome.sidePanel.open({ windowId: tab.windowId });
});

关键区别:

  • Popup:窗口小、点击外部就关闭、关闭即销毁
  • Sidebar:空间大、可固定显示、数据持久化

2. 拦截 fetch / XHR(支持延时和状态码)

劫持实现原理参考我上篇文章:【前端效率工具】再也不用 APIfox 联调!零侵入 Mock,全程不改代码、不开代理juejin.cn/post/757098…

核心代码:

injected.ts

const originalFetch = window.fetch;

window.fetch = async function(input, init) {
  const url = typeof input === 'string' ? input : input.url;
  const method = init?.method || 'GET';
  
  // 查找匹配的规则
  const rule = await findMatchedRule(url, method);
  
  if (rule) { 
    //  延时处理
    if (rule.delay > 0) {
      await new Promise(resolve => setTimeout(resolve, rule.delay));
    }
    
    // 返回指定状态码
    return new Response(
      JSON.stringify(rule.data),
      {
        status: rule.statusCode || 200,
        statusText: getStatusText(rule.statusCode),
        headers: { 'Content-Type': 'application/json' }
      }
    );
  }
  
  // 不拦截,调用原始 fetch
  return originalFetch.apply(this, arguments as any);
};

// 状态码文本映射
function getStatusText(code: number): string {
  const statusTexts: Record<number, string> = {
    200: 'OK',
    400: 'Bad Request',
    401: 'Unauthorized',
    403: 'Forbidden',
    404: 'Not Found',
    500: 'Internal Server Error',
    502: 'Bad Gateway',
    503: 'Service Unavailable',
    504: 'Gateway Timeout'
  };
  return statusTexts[code] || 'Unknown';
}

匹配规则:

async function findMatchedRule(url: string, method: string) {
  // 从 storage 获取规则
  const { rules } = await chrome.storage.local.get('rules');
  
  return rules.find((rule: Rule) => {
    if (!rule.enabled) return false;
    
    // 方法匹配
    if (rule.method !== 'ALL' && rule.method !== method) {
      return false;
    }
    
    // URL 匹配
    if (rule.matchMode === 'contains') {
      return url.includes(rule.url);
    } else if (rule.matchMode === 'exact') {
      return url === rule.url;
    } else if (rule.matchMode === 'regex') {
      return new RegExp(rule.url).test(url);
    }
    
    return false;
  });
}

支持的匹配模式:

  • 包含匹配/api/user 可以匹配 /api/user/list/api/user/detail
  • 完整匹配:必须完全一致
  • 正则匹配/api/user/\d+ 可以匹配 /api/user/123

3. AI 生成 Mock 数据

Prompt 设计:

const buildPrompt = (userInput: string) => {
  return `
你是一个专业的 Mock 数据生成助手。

用户需求:${userInput}

请生成符合以下规范的 JSON 数据:
1. 必须是有效的 JSON 格式
2. 包含常见的响应结构(code, msg, data)
3. 数据要真实合理(中文姓名、真实邮箱格式等)
4. 如果是列表,生成 2-3 条示例数据

只返回 JSON,不要有其他说明文字。
  `.trim();
};

生成逻辑:

const generateMockData = async () => {
  if (!aiPrompt.value) {
    ElMessage.warning('请输入数据描述');
    return;
  }

  generating.value = true;

  try {
    const response = await fetch('YOUR_AI_API_ENDPOINT', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        prompt: buildPrompt(aiPrompt.value),
        model: 'gpt-3.5-turbo'
      })
    });

    const data = await response.json();
    
    // 解析 AI 返回的 JSON
    const mockData = JSON.parse(data.choices[0].message.content);
    
    // 填充到编辑器
    form.data = JSON.stringify(mockData, null, 2);
    
    ElMessage.success('✨ AI 生成成功');
  } catch (error) {
    ElMessage.error('生成失败:' + error.message);
  } finally {
    generating.value = false;
  }
};

4. 规则数据结构

interface Rule {
  id: string;
  remark: string;              // 规则名称
  url: string;                 // URL 匹配
  matchMode: 'contains' | 'exact' | 'regex';
  method: 'ALL' | 'GET' | 'POST' | 'PUT' | 'DELETE';
  statusCode: number;          // 🆕 状态码(默认 200)
  delay: number;               // 🆕 延时(毫秒,默认 0)
  data: Record<string, any>;   // Mock 数据
  enabled: boolean;            // 是否启用
  createdAt: number;           // 创建时间
}

存储和读取:

// 保存规则
const saveRules = async () => {
  await chrome.storage.local.set({ rules: rules.value });
};

// 加载规则
const loadRules = async () => {
  const result = await chrome.storage.local.get('rules');
  rules.value = result.rules || [];
};

// 监听变化
chrome.storage.onChanged.addListener((changes, areaName) => {
  if (areaName === 'local' && changes.rules) {
    rules.value = changes.rules.newValue;
  }
});

总结

这次重构主要做了四件事:

  1. Sidebar 替代 Popup:更大的空间、更好的体验
  2. AI 自动生成:一句话描述即可生成 Mock 数据
  3. 延时请求:测试 Loading、骨架屏、超时逻辑
  4. 自定义状态码:测试各种异常场景

这四个功能组合起来,基本覆盖了前端开发中 90% 的 Mock 需求。

如果你也在等后端接口,或者被 Popup 小窗口折磨过, 真的可以试试这个插件,保证你用了就回不去了。

❌