浏览器插件调试与排错手册

本文是"浏览器插件开发"系列的 Debug 篇,聚焦如何对每个上下文(context)打开开发者工具(DevTools)、如何读懂 chrome://extensions 的错误面板、高频错误的准确成因与解决方案,以及一套可落地的系统化排查流程。架构与 API 细节见系列其他篇目(文末"参见")。 MV3 插件由若干彼此隔离的 JavaScript 执行环境(execution context)组成,每个环境有独立的全局对象、独立的 DevTools 入口、独立的 console。调试第一步永远是确认报错发生在哪个上下文,因为同名 API 在不同上下文里

分享

[!info] 文档信息

本文是"浏览器插件开发"系列的 Debug 篇,聚焦如何对每个上下文(context)打开开发者工具(DevTools)、如何读懂 chrome://extensions 的错误面板、高频错误的准确成因与解决方案,以及一套可落地的系统化排查流程。架构与 API 细节见系列其他篇目(文末"参见")。


一、插件的多上下文调试模型

MV3 插件由若干彼此隔离的 JavaScript 执行环境(execution context)组成,每个环境有独立的全局对象、独立的 DevTools 入口、独立的 console。调试第一步永远是确认报错发生在哪个上下文,因为同名 API 在不同上下文里的可用性与行为并不一致。

上下文(context) 全局对象 典型职责 DevTools 入口
Service Worker(后台,背景脚本) self(ServiceWorkerGlobalScope,无 window/document 事件中枢、长任务、网络拦截 chrome://extensions → 插件卡片 → "Service Worker" 链接
Popup(弹窗) window 操作面板 UI 右键插件图标弹窗 → 检查
Options(选项页) window 设置页 页面内右键 → 检查
Side Panel(侧边栏) window 常驻侧栏 UI 侧栏内右键 → 检查
Content Script(内容脚本) 与宿主页共享 window(隔离世界 isolated world) 操作宿主 DOM 宿主网页 DevTools → Sources → Content scripts
DevTools 页面 window 扩展 DevTools 面板 对 DevTools 窗口再开一层 DevTools
Offscreen Document(离屏文档) window(不可见) DOM/剪贴板/音频等 SW 缺失能力 chrome://extensions → "检查视图" 列出的离屏页

1.1 Service Worker

  1. 打开 chrome://extensions,确认右上角 开发者模式(Developer mode) 已开启。
  2. 找到目标插件卡片,点击蓝色的 "Service Worker" 链接(无激活时显示 "Service worker (inactive)",点击即唤醒并打开 DevTools)。
  3. 在打开的 DevTools 中查看 Console 日志、在 Sources 下断点、在 Network 抓 SW 内 fetch
  4. 手动唤醒/终止:另一种入口是 chrome-extension://<扩展ID>/manifest.json → 检查 → Application 面板 → Service Workers 窗格,里面有 Start / Stop / skipWaiting 按钮,可手动启停以验证休眠后行为。
  5. 关键事实:打开 SW 的 DevTools 会让 SW 一直保持存活。要验证"SW 被终止后插件仍正常",必须先关闭这个 DevTools,或在 Service Workers 窗格点 Stop 后再触发事件。

1.2 Popup

  • 在浏览器工具栏右键插件图标 → 弹出弹窗后右键 → 检查(Inspect),即可对 popup 开 DevTools。注意弹窗一旦失焦就会关闭,所以要在弹窗上直接右键。
  • 也可独立打开:地址栏访问 chrome-extension://<扩展ID>/src/popup/index.html(路径以构建产物为准),此时它作为普通标签页运行,便于稳定调试,但 chrome.action 相关 API 行为与真实弹窗略有差异。
  • 调试 popup 打开瞬间的网络请求:在 DevTools Network 面板内点刷新,可重载 popup 而不关闭 DevTools。

1.3 Options 与 Side Panel

  • 二者都是普通 HTML 页面,直接在页面内右键 → 检查即可。
  • Options 若以独立标签页打开(open_in_tab: true)或嵌入在 chrome://extensions 详情内(嵌入式 options),调试方式相同。

1.4 Content Script

  1. 打开宿主网页(注入目标页面),按 F12 打开该页面的 DevTools。
  2. 内容脚本运行在隔离世界,其源码位于 Sources 面板 → 左侧 Content scripts 分组下,按断点调试与普通脚本一致。
  3. Console 默认在宿主页主世界(top)执行。要在内容脚本的隔离世界里执行表达式或读取其变量,点击 Console 左上角的上下文下拉框(默认 top),切换到 <你的插件名> 对应的扩展上下文。
  4. 内容脚本的 console.log 出现在宿主页的 Console,不在 chrome://extensions;只有 runtime error、console.warnconsole.error 才会被记录到插件的错误面板。

1.5 DevTools 页面

扩展自定义的 DevTools 面板(devtools_page)本身运行在 DevTools 窗口里。调试方法是对 DevTools 再开一层 DevTools:让 DevTools 窗口获得焦点后按 Ctrl+Shift+I(或撤离停靠后右键检查),即可调试 devtools_page 脚本。


二、chrome://extensions 开发者模式功能

功能 位置 用途
开发者模式开关 页面右上角 Toggle 启用"加载已解压""打包"等开发功能
加载已解压的扩展程序 左上角按钮 加载本地构建产物目录(dist/
重新加载(Reload) 卡片上的 ↻ 图标 改完 manifest/SW 后重载,使变更生效
错误(Errors) 卡片上的红色 "错误" 按钮 查看 SW、popup 等记录的 runtime 错误堆栈;红色表示有未清理错误
全部清除(Clear all) 错误面板内 清空历史错误;旧错误不会自动消失,需手动清
检查视图(Inspect views) 卡片"检查视图"区 列出当前所有活动上下文(SW、离屏页等),点击即开对应 DevTools
详细信息(Details) 卡片按钮 查看权限、站点访问、源文件

[!warning] 重载范围
改动 manifest.json、Service Worker、声明式规则文件 后必须点 ↻ 重载。仅改内容脚本/popup 的源码时,构建工具的 HMR 通常能热更新,但内容脚本的逻辑变更往往仍需重载插件并刷新宿主页才能彻底生效。


三、错误速查表

错误文本以 Chrome 138+ 控制台/错误面板实际输出为准。下表"原因"列描述根因,"解决"列给可执行动作。

错误信息 出现位置 原因 解决
Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist. SW/popup 发消息时 目标上下文没有 onMessage 监听器:内容脚本未注入该页、SW 未唤醒、发给了不存在的 tab,或时序上接收端尚未就绪 确认目标页已注入内容脚本(chrome://extensions 看 host 权限);发送前用 chrome.scripting.executeScript 兜底注入;用安全包装捕获 lastError(见 4.2);对 chrome://、Web Store 等受限页不发消息
The message port closed before a response was received. sendMessage 回调 onMessage 监听器要异步 sendResponse 却没 return true 保持通道;或监听器内抛错、Promise reject;或接收端在响应前卸载 异步响应的监听器必须 return true(或返回 Promise);确保所有分支都调用 sendResponse;用 try/catch 包裹监听器逻辑
Extension context invalidated. 内容脚本 插件被重载/更新后,旧内容脚本仍残留在已打开的页面里(orphaned),其 chrome.runtime 已失效,再调用任何扩展 API 即报错 调用前先检测 chrome.runtime?.id 是否存在(见 4.1);失效时移除监听器/定时器并提示刷新页面;开发期重载后刷新宿主标签页
Manifest version 2 is deprecated, and support will be removed in 2024. See https://developer.chrome.com/.../mv2-sunset / 拒绝加载 MV2 加载插件时 manifest 仍为 "manifest_version": 2;Chrome 138 已停止对 MV2 的支持 迁移到 MV3:manifest_version: 3background.service_workeractionhost_permissions 等(见迁移指南)
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'" popup/options/扩展页 MV3 扩展页 CSP 默认禁止内联 <script>…</script>、内联事件属性(onclick=)、eval 把脚本拆到外部 .js 文件并用 <script src> 引入;事件用 addEventListener 绑定;Vue 模板正常,但勿用内联 on* HTML 属性
Service worker registration failed. Status code: 15 加载/重载插件 状态码 15 = 脚本求值阶段抛错:SW 顶层代码访问了 window/document、调用了不存在的 API、import 路径错误、或顶层同步异常 点 "Service Worker" 链接看真实堆栈;移除 DOM 依赖;顶层 import 用静态 import(type module)或 importScripts;用 try/catch 包裹定位
Service worker registration failed. Status code: 3 加载/重载插件 状态码 3 = 找不到 SW 脚本文件background.service_worker 路径与构建产物不符 核对 manifest 中路径相对扩展根目录正确;确认构建确实产出了该文件;CRXJS 下让插件自动生成路径
Cannot access contents of url "https://...". Extension manifest must request permission to access this host. 内容脚本注入/scripting/tabs 未在 host_permissions 或内容脚本 matches 中声明该域;或用户未在"站点访问"中授权 在 manifest 添加对应 host_permissions/matches;动态注入前确保有 activeTab 或显式 host 权限;引导用户授予站点访问
Unchecked runtime.lastError: <任意消息> 任意异步 API 回调 上一个 chrome.* 异步调用失败但回调里没有读取 chrome.runtime.lastError,Chrome 自动告警 在回调首行检查 if (chrome.runtime.lastError) {…};或改用 Promise 形式 await 并 try/catch
net::ERR_BLOCKED_BY_RESPONSE / 资源 404 / Denying load of chrome-extension://…/foo.png. Resources must be listed in the web_accessible_resources manifest key. 宿主页加载扩展资源 内容脚本/宿主页引用了扩展内文件(图片、字体、注入脚本),但未在 web_accessible_resources 声明 在 manifest 的 web_accessible_resources 中以 { resources: [...], matches: [...] } 声明这些文件
Uncaught ReferenceError: importScripts is not defined / Cannot use import statement outside a module SW SW 用了 importScripts 但被打包成 ESM;或用了 ESM import 但 manifest 未声明 "type": "module" 二选一保持一致:要么 background: { service_worker: "sw.js", type: "module" } 配合静态 ESM import;要么用经典脚本 + importScripts()。MV3 SW 不支持顶层动态 import() 拉远程模块
Failed to fetch dynamically imported module / 动态 import 失败 SW SW 内 import() 动态导入在某些时机(休眠后)不可靠,或导入了未打包进扩展的资源 将依赖静态导入到 SW 顶层由打包器内联;必需的代码分割放到 offscreen 文档执行
[vite] failed to connect to websocket / WebSocket connection to 'ws://localhost:5173' failed / HMR 不生效 宿主页/popup 控制台 CRXJS/Vite HMR 的 WS 端口被占用、server.port 与实际不符、HTTPS 站点拦截 ws://、或扩展页 CSP 未放行 dev server 固定 server.portserver.strictPort: true;确认无其他进程占端口;HTTPS 宿主页用 server.hmr 配置或改在 http 页面调试;CRXJS 会注入所需 CSP,升级到匹配 Vite 版本的 CRXJS
Error: Could not load manifest. / Manifest is not valid JSON 加载插件 manifest 语法错误、字段类型错误、引用了不存在的图标 用 JSON 校验;核对 iconsaction.default_icon 路径存在
This request has been blocked; the content must be served over HTTPS. (mixed content) 内容脚本/popup 在 HTTPS 上下文请求了 http:// 资源 全部改用 https://;SW 内 fetch 同理
Permission '<api>' is unknown or URL pattern is malformed. 加载插件 permissions 写了 MV3 不存在的权限名,或 host 模式写在 permissions 而非 host_permissions 校对权限名拼写;host 模式移到 host_permissions
Cannot read properties of undefined (reading 'onClicked')chrome.* 为 undefined SW 用了该上下文不可用的 API(如 SW 里访问 chrome.action.onClicked 需先声明 action),或权限缺失导致命名空间未注入 声明对应 manifest 字段/权限;用前判空;查 API 的可用上下文
Refused to load the script '<远程URL>' because it violates ... script-src 扩展页 MV3 禁止扩展页加载远程代码 远程逻辑改为数据/配置下发,代码全部打包进扩展
Storage quota exceeded / QUOTA_BYTES_PER_ITEM quota exceeded 任意上下文 chrome.storage.sync 单项超 8KB 或总量超限 大数据用 chrome.storage.local(默认更大,可申请 unlimitedStorage);拆分/压缩数据
Tabs cannot be edited right now (user may be dragging a tab). tabs API 用户正在拖拽标签时操作 tabs 捕获错误后重试;避免在拖拽期间批量操作
Unchecked runtime.lastError: No tab with id: <n>. tabs/scripting 目标 tab 已关闭或 id 失效 操作前 chrome.tabs.get 校验存在;捕获错误忽略已关闭的 tab

四、诊断代码片段

4.1 检测内容脚本上下文是否失效

内容脚本被插件重载后会变成"孤儿",此时访问 chrome.runtime.id 会抛错或为 undefined。在每次调用扩展 API 前判活。

// content-script.ts —— 上下文有效性探测

/** 判断当前内容脚本是否仍连接到有效的扩展上下文 */
export function isExtensionContextValid(): boolean {
  try {
    // chrome.runtime 为惰性初始化,用可选链避免读取时抛错
    return Boolean(chrome.runtime?.id)
  } catch {
    // 访问已失效的 runtime 会直接抛 "Extension context invalidated."
    return false
  }
}

/** 失效时自我清理:移除监听器与定时器,避免后续报错刷屏 */
function teardownOnInvalidContext(cleanup: () => void): void {
  const timer = setInterval(() => {
    if (!isExtensionContextValid()) {
      clearInterval(timer)
      cleanup() // 解绑 DOM 事件、断开 MutationObserver、停止定时任务
      console.warn('[content] 扩展上下文已失效,请刷新页面以重新注入')
    }
  }, 2000)
}

4.2 安全的 sendMessage 包装

统一捕获 runtime.lastError,把"接收端不存在""端口关闭"等转成可控的 null 返回,避免 Unchecked 告警刷屏。

// messaging.ts —— 安全消息发送

export interface SafeResult<T> {
  ok: boolean
  data?: T
  error?: string
}

/** 向 SW/其他上下文发消息,永不抛出未捕获错误 */
export function safeSendMessage<T = unknown>(
  message: unknown,
): Promise<SafeResult<T>> {
  return new Promise((resolve) => {
    try {
      chrome.runtime.sendMessage(message, (response: T) => {
        const err = chrome.runtime.lastError // 必须在回调同步阶段读取
        if (err) {
          resolve({ ok: false, error: err.message })
          return
        }
        resolve({ ok: true, data: response })
      })
    } catch (e) {
      // 上下文失效时 sendMessage 本身会同步抛错
      resolve({ ok: false, error: (e as Error).message })
    }
  })
}

/** 向指定 tab 的内容脚本发消息,附带兜底注入 */
export async function sendToTab<T = unknown>(
  tabId: number,
  message: unknown,
): Promise<SafeResult<T>> {
  return new Promise((resolve) => {
    chrome.tabs.sendMessage(tabId, message, (response: T) => {
      const err = chrome.runtime.lastError
      resolve(err ? { ok: false, error: err.message } : { ok: true, data: response })
    })
  })
}

接收端正确写法(异步响应必须 return true):

// service-worker.ts
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg?.type === 'FETCH_USER') {
    // 异步分支:返回 true 让消息通道保持开启,直到 sendResponse 被调用
    fetchUser(msg.id)
      .then((user) => sendResponse({ ok: true, user }))
      .catch((e) => sendResponse({ ok: false, error: String(e) })) // 所有分支都要回应
    return true // 关键:缺它会触发 "message port closed before a response"
  }
  // 同步分支无需 return true
})

4.3 统一 logger 封装(按上下文打标签 + 生产关闭)

// logger.ts —— 跨上下文统一日志

type Level = 'debug' | 'info' | 'warn' | 'error'

// 构建期由 Vite 注入;生产构建时为 false,日志自动消失(配合 tree-shaking)
const ENABLED = import.meta.env.DEV

/** 运行时探测当前上下文,给每条日志打标签 */
function detectContext(): string {
  if (typeof window === 'undefined') return 'SW' // service worker 无 window
  if (location.protocol === 'chrome-extension:') {
    if (location.pathname.includes('popup')) return 'POPUP'
    if (location.pathname.includes('options')) return 'OPTIONS'
    if (location.pathname.includes('sidepanel')) return 'PANEL'
    return 'EXT-PAGE'
  }
  return 'CONTENT' // 注入到宿主页
}

const TAG = detectContext()

function emit(level: Level, ...args: unknown[]): void {
  if (!ENABLED && level === 'debug') return
  const prefix = `%c[${TAG}]`
  const style = level === 'error' ? 'color:#e55' : 'color:#39c'
  console[level](prefix, style, ...args)
}

export const logger = {
  debug: (...a: unknown[]) => emit('debug', ...a),
  info: (...a: unknown[]) => emit('info', ...a),
  warn: (...a: unknown[]) => emit('warn', ...a),
  error: (...a: unknown[]) => emit('error', ...a),
}

4.4 storage 调试:监听 onChanged 打印 diff

// storage-debug.ts —— 任意上下文均可监听
import { logger } from './logger'

chrome.storage.onChanged.addListener((changes, areaName) => {
  for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
    logger.debug(`storage[${areaName}] ${key}`, { old: oldValue, new: newValue })
  }
})

/** 一次性 dump 某个区域全部内容(在任意上下文 console 直接调用) */
export async function dumpStorage(area: 'local' | 'sync' = 'local') {
  const all = await chrome.storage[area].get(null) // null = 取全部
  console.table(all)
  return all
}

4.5 declarativeNetRequest 规则匹配调试

getMatchedRulesonRuleMatchedDebug 仅对已解压扩展且声明 declarativeNetRequestFeedback 权限时可用。getMatchedRules 有配额(10 分钟内最多 20 次,用户手势触发不计入)。

// dnr-debug.ts —— manifest 需含 "declarativeNetRequestFeedback" 权限
import { logger } from './logger'

// 仅未打包扩展可用:每命中一条规则就回调,适合开发期观察
if (chrome.declarativeNetRequest.onRuleMatchedDebug) {
  chrome.declarativeNetRequest.onRuleMatchedDebug.addListener((info) => {
    logger.debug('DNR matched', {
      rule: info.rule, // { rulesetId, ruleId }
      url: info.request.url,
      type: info.request.type,
    })
  })
}

/** 把扩展拦截命中数显示为 action 徽章,直观看到规则是否生效 */
chrome.declarativeNetRequest.setExtensionActionOptions({
  displayActionCountAsBadgeText: true,
})

/** 主动查询最近匹配的规则 */
export async function queryMatched(tabId?: number) {
  const { rulesMatchedInfo } =
    await chrome.declarativeNetRequest.getMatchedRules(tabId ? { tabId } : {})
  console.table(rulesMatchedInfo.map((m) => ({
    ruleId: m.rule.ruleId,
    ruleset: m.rule.rulesetId,
    ts: new Date(m.timeStamp).toISOString(),
  })))
}

[!tip] 收集错误
规则文件(rule_resources JSON)语法/字段错误不会进 SW console,而是显示在 chrome://extensions 卡片的 "错误" 面板("Rule with id N specifies an invalid …")。声明式规则报错优先看那里。

4.6 给消息加 trace 与 sender 检查

// service-worker.ts
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  // sender 用于安全校验:拒绝来源不明的消息
  logger.debug('recv', {
    type: msg?.type,
    fromTab: sender.tab?.id,        // 来自内容脚本时有值
    fromUrl: sender.url,            // 发送方页面 URL
    fromExtId: sender.id,           // 应等于 chrome.runtime.id
    frameId: sender.frameId,        // 0 = 主框架
  })
  if (sender.id !== chrome.runtime.id) {
    return // 非本扩展消息,忽略
  }
  // ……业务分支
})

五、网络调试

  1. 抓 SW 的 fetch:在 SW 的 DevTools Network 面板查看后台发起的请求。注意 SW 休眠会清空面板,调试时让 DevTools 保持打开以维持存活。
  2. 抓内容脚本/宿主页请求:在宿主页 DevTools Network 查看,内容脚本发起的 fetch 以宿主页 origin 出现。
  3. 声明式拦截调试:用 5.4.5 的 onRuleMatchedDebug + 徽章计数确认规则是否命中;用 getMatchedRules 回溯历史命中。
  4. 被拦截的扩展资源:宿主页 console 出现 Denying load of chrome-extension://… 时,去补 web_accessible_resources
  5. CORS:MV3 下跨域请求由 SW 发起最稳妥(SW 拥有 host_permissions 声明域的扩展级 CORS 豁免);内容脚本里的跨域 fetch 受宿主页 CORS 约束,应改为转发给 SW 代发。

六、Source Map 与 Vite 构建产物调试

  1. 开启 source map:开发期 vite 默认提供 inline source map;生产排错时在 build.sourcemap: true(或 'hidden')下构建,使 chrome://extensions 错误面板的堆栈能映射回 Vue/TS 源码。
  2. 定位真实文件:DevTools Sources 左侧若只看到打包后的 assets/xxx.js,确认 source map 已加载(文件名旁有源映射标记),否则断点会落在压缩代码上。
  3. CRXJS 产物结构:CRXJS 会改写 manifest 路径并为 SW 生成 loader,调试 SW 时入口可能是生成的 service-worker-loader.js,真实逻辑在它 import 的模块里。
  4. Vue 组件调试:popup/options 是标准 SPA,可正常用 Vue DevTools 扩展;但 Vue DevTools 无法挂到另一个扩展的 popup,需用独立标签页方式(1.2)打开后再连。
  5. HMR 与内容脚本:内容脚本逻辑改动经常需要重载插件 + 刷新宿主页;样式/Vue 模板类改动通常可 HMR 热更。HMR 失败先查 6 节与错误速查表的 Vite WS 行。

七、复现与隔离

  1. 最小复现(minimal reproduction):把插件裁剪到只剩触发 bug 的最小 manifest + 一个 SW + 一段脚本,排除无关代码干扰。
  2. 关掉其他插件:其他扩展可能注入冲突的内容脚本或抢占快捷键/DNR 规则。在 chrome://extensions 临时禁用全部其他插件复测。
  3. 全新 profile:用 chrome --user-data-dir=<临时目录> 启动一个干净 profile,排除缓存、已损坏的 storage、企业策略的影响。
  4. 无痕/访客模式:确认问题是否与已有登录态/Cookie 相关(需在插件详情勾选"在无痕模式下启用")。
  5. 多 Chrome 通道:在 Stable / Beta / Canary 对比,区分是自身 bug 还是浏览器回归。

八、最佳实践(调试相关)

  1. 每个上下文统一接入 logger 并打上下文标签。 跨上下文 bug 最难的是分不清日志来自哪里。用 4.3 的 detectContext() 自动给 [SW]/[CONTENT]/[POPUP] 前缀,一眼定位。

    import { logger } from './logger'
    logger.info('init done') // 输出 [POPUP] init done
    
  2. 所有 chrome.* 异步调用都走 Promise + try/catch,杜绝裸回调。 既能用 await 写顺序逻辑,又强制处理 lastError,从根上消灭 "Unchecked runtime.lastError"。

    try {
      const tab = await chrome.tabs.get(tabId)
      await chrome.tabs.sendMessage(tab.id!, { type: 'PING' })
    } catch (e) {
      logger.warn('tab 不可达', e) // 已关闭/无内容脚本时安全降级
    }
    
  3. 内容脚本每次跨边界调用前判活,并在失效时自卸载。 开发期插件频繁重载,孤儿脚本会狂刷 "Extension context invalidated"。用 4.1 的 isExtensionContextValid() 守门。

    if (!isExtensionContextValid()) return // 直接跳过,避免抛错
    chrome.runtime.sendMessage({ type: 'SYNC' })
    
  4. 异步 onMessage 监听器永远显式 return true(或返回 Promise),且每个分支都回应。 这是 "message port closed" 的唯一根治法。统一封装一个 handler 注册器以免遗漏。

    chrome.runtime.onMessage.addListener((msg, _s, sendResponse) => {
      handle(msg).then(sendResponse).catch((e) => sendResponse({ error: String(e) }))
      return true // 保持通道开启
    })
    
  5. 生产构建用编译期常量关闭 debug 日志,而非运行时 if。 import.meta.env.DEV 在 Vite 生产构建为 false,配合 tree-shaking 让 debug 日志被整段删除,既不泄露内部信息也不占体积。

    if (import.meta.env.DEV) logger.debug('internal state', state)
    
  6. 验证 SW 休眠行为时主动 Stop,而非等它自然休眠。Application → Service WorkersStop,再触发事件,能在几秒内复现"冷启动丢失内存态"类 bug,而不必等 30 秒空闲超时。

  7. 声明式规则与 manifest 错误先看错误面板,再看 console。 这两类错误不进 SW console,只进 chrome://extensions 的"错误"面板;养成改完 manifest/规则先点红色"错误"按钮的习惯。


九、常见陷阱

与第三节错误速查表互补,这里收录不一定报错但行为异常的坑。

  1. 现象:SW 里 setTimeout/setInterval 到点不触发,全局变量莫名归零。
    原因:MV3 SW 是事件驱动的,空闲约 30 秒即被终止,定时器与内存态随之销毁;事件再来时是全新实例。
    解决:跨唤醒的状态写 chrome.storage;定时任务用 chrome.alarms(最小周期 30 秒以上)替代 setInterval;不要依赖 SW 顶层的可变全局变量。

  2. 现象:打开 SW 的 DevTools 时一切正常,关掉后插件就出问题。
    原因:打开 SW DevTools 会强制 SW 保持存活,掩盖了"休眠后丢状态/监听器没在顶层注册"的 bug。
    解决:所有事件监听器(onMessageonInstalledonAlarm 等)必须在 SW 顶层同步注册,不能放在某个异步回调里;关掉 DevTools 后用真实场景回归测试。

  3. 现象console.log(obj) 在 DevTools 里展开看到的值,和实际逻辑用到的值对不上。
    原因:DevTools 对对象是惰性求值,展开时读取的是当时的最新状态,而非 log 那一刻的快照;异步代码里对象可能已被改写。
    解决:需要快照时打印深拷贝 console.log(structuredClone(obj))JSON.parse(JSON.stringify(obj)),避免被后续 mutation 误导。

  4. 现象:内容脚本里能 console.log 出宿主页变量,但 window.someLib 永远 undefined
    原因:内容脚本运行在隔离世界,与宿主页共享 DOM 但不共享 JS 全局,拿不到页面自己的 JS 变量/库。
    解决:需访问页面 JS 时,通过 web_accessible_resources 注入一段 <script> 到主世界(main world),或用 MV3 的 world: "MAIN" 内容脚本,再经 window.postMessage 与隔离世界通信。

  5. 现象:popup 里发起的请求/操作"莫名其妙中断",没有任何错误。
    原因:popup 失焦即销毁,其 DOM、JS、未完成的 fetch/Promise 全部连同上下文一起消失。
    解决:长任务交给 SW 执行,popup 只发起并通过 storage/消息读结果;调试时用独立标签页(1.2)方式打开 popup 以防自动关闭。


十、系统化排查 Checklist

按顺序执行,多数问题在前几步即可定位。

  1. 确认上下文:错误发生在 SW / popup / options / 内容脚本 / DevTools 页中的哪个?打开对应 DevTools。
  2. 读错误面板chrome://extensions → 目标插件 → 红色"错误"按钮,看完整堆栈与触发时间;记下后点"全部清除"再复现,确保看到的是最新错误。
  3. 确认插件是否最新:改过 manifest/SW/规则文件?点 ↻ 重载;改过内容脚本?重载 + 刷新宿主页。
  4. 核对权限:报"Cannot access""permission"类 → 检查 host_permissions / permissions / matches / 站点访问授权。
  5. 核对资源声明:报资源 blocked/Denying load → 检查 web_accessible_resources
  6. 检查 SW 存活与监听器位置:监听器是否在 SW 顶层同步注册?关掉 SW DevTools 后还正常吗?用 Application → Service Workers 的 Stop 测冷启动。
  7. 检查消息链路:发送端用安全包装(4.2)确认是 "receiving end does not exist"(接收端缺失/未注入)还是 "port closed"(接收端没 return true)。
  8. 检查 storage 一致性:用 dumpStorage()(4.4)确认数据确实写入;用 onChanged 看 diff 是否符合预期。
  9. 检查网络层:SW/宿主页 Network 面板看请求是否发出、状态码、CORS;DNR 用徽章计数与 getMatchedRules 确认拦截。
  10. 隔离环境复现:关其他插件、换全新 profile、做最小复现,区分自身 bug 与环境/冲突。
  11. 核对构建产物:source map 是否生效、CRXJS 生成的入口路径是否正确、HMR WS 是否连上。
  12. 跨版本/跨机验证:在 Canary 与干净机器复测,排除浏览器回归与本机环境。

参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog