乐于分享
好东西不私藏

从零开发浏览器插件:我的第一个 Chrome 扩展实战笔记

从零开发浏览器插件:我的第一个 Chrome 扩展实战笔记

前言

最近我在 AI 的帮助下完成了一个浏览器插件的开发——TextNova,一个选中网页文字就能调用 AI 进行翻译、解释、总结的工具。作为一个浏览器插件开发的新手,整个开发过程让我对 Chrome 扩展的架构和运作机制有了深入的理解。

今天把这段经历整理成文,希望给同样想入门浏览器插件开发的同学一些参考。

已关注

关注

重播 分享


一、项目简介

TextNova 的功能很简单:在任意网页选中文字后,会弹出一个悬浮按钮,点击后调用 AI API,结果以弹窗形式显示在页面上。

核心特点:

  • 零依赖:纯原生 JavaScript,没有使用 React/Vue 等框架
  • 流式输出:支持 AI 响应的流式显示
  • 自定义菜单:用户可以添加自己的 AI 处理模板
  • 多 API 支持:兼容智谱 AI 和其他 OpenAI 格式接口

二、浏览器插件的核心架构

Chrome 扩展采用三层分离架构,这是理解插件开发的第一把钥匙。你可以将它想象成一个高效协作的团队,各司其职:

┌─────────────────────────────────────────────────────┐│                    用户网页                          ││           (content.js 注入到这里运行)                ││             负责与页面内容和用户交互                 │└─────────────────────────────────────────────────────┘                         ▲                         │ chrome.runtime.sendMessage                          / chrome.tabs.sendMessage                         ▼┌─────────────────────────────────────────────────────┐│              background.js (Service Worker)          ││         处理 API 调用、管理菜单、协调消息              ││                 插件的“幕后总指挥”                   │└─────────────────────────────────────────────────────┘                         ▲                         │ chrome.storage.sync                         ▼┌─────────────────────────────────────────────────────┐│               sidepanel.html/js (侧边栏)              ││              配置界面、测试入口                        ││                 用户的“操作控制台”                   │└─────────────────────────────────────────────────────┘

各层的职责

层级
文件
职责
运行环境
Background
background.js
API 调用、消息路由、右键菜单管理
独立的后台线程 (Service Worker),非持久化,按需唤醒,更省资源。
Content
content.js
页面内的 UI 交互、用户操作检测
注入到每个用户网页的独立沙箱环境
UI Pages
sidepanel.html
设置界面、测试入口
独立的扩展页面,拥有完整的 DOM 和 JavaScript 环境

三、Manifest V3 配置文件详解

manifest.json 是插件的”身份证”,定义了插件的基本信息和权限:

{"manifest_version"3,"name""TextNova","version""1.0.0","description""选中文字,AI帮你翻译、总结、解释","action": {"default_icon": {"16""assets/icon16.png","48""assets/icon48.png","128""assets/icon128.png"    }  },"permissions": ["sidePanel",      // 侧边栏 API"storage",        // 存储 API"activeTab",      // 活动标签页"tabs",           // 标签页管理"scripting",      // 脚本注入"contextMenus"// 右键菜单  ],"host_permissions": [ // 声明需要访问的外部 API 域名,遵循最小权限原则"https://open.bigmodel.cn/*""https://*.openai.com/*"  ],"background": {"service_worker""background.js","type""module"  },"content_scripts": [{"matches": ["https://*/*"],"js": ["content.js"],"css": ["content.css"]  }]}

关键配置说明

  • manifest_version:必须是 3,表示使用 Manifest V3 标准。
  • permissions:定义插件需要使用的 Chrome API 权限。遵循最小权限原则,只申请必需的权限,增强用户信任和安全性。
  • host_permissions:声明插件需要访问的特定域名。这对于需要跨域请求的 API 调用至关重要。
  • background.service_worker:指定后台 Service Worker 脚本。Service Worker 是非持久的,按需唤醒,有效节省资源。
  • content_scripts.matches:控制 content script 注入到哪些页面。此处配置为"https://*/*"表示注入到所有HTTPS 页面。

四、开发 SOP:从零到上线的完整流程

步骤 1:创建基础文件结构

一个清晰的项目结构是高效开发的基础:

TextNova/├── manifest.json        # 插件的配置文件,必不可少├── background.js        # 后台 Service Worker 脚本,处理核心逻辑├── content.js           # 内容脚本,与网页进行交互├── content.css          # 内容脚本的样式文件├── sidepanel.html       # 侧边栏的 HTML 页面└── sidepanel.js         # 侧边栏的 JavaScript 逻辑└── icons/               # 存放插件图标的目录    ├── icon16.png    ├── icon48.png    └── icon128.png

步骤 2:加载插件进行调试

将你的代码加载到 Chrome 中进行调试是开发的第一步:

  1. 打开 Chrome,访问 chrome://extensions/
  2. 开启右上角的「开发者模式」
  3. 点击「加载已解压的扩展程序」
  4. 选择项目根目录
加载扩展程序

步骤 3:开发调试循环

在开发过程中,你将频繁地修改代码并测试。遵循这个循环可以提高效率:

# 修改了 background.js 或 manifest.json 后:1. 去 chrome://extensions/ 页面,点击你的插件卡片上的「刷新」图标。# 修改了 content.js 或 content.css 后:2. 回到你正在测试的网页,按 F5 刷新页面(**非常重要!**)。3. 重新测试你的功能。

踩坑提醒:修改 content.js 或 content.css 后,必须刷新所有已打开的、注入了该内容脚本的网页,否则你可能会遇到「Extension context invalidated」错误。这是因为旧的 content script 仍在运行,但它与 background Service Worker 的连接已断开,导致通信失效。


五、核心技术点与代码解析

5.1 消息通信机制

Chrome 扩展的各个部分(background、content script、sidepanel 等)运行在独立的上下文中,它们之间不能直接访问彼此的变量或函数,必须通过消息通信。主要有两种方式:

(1)一次性请求:chrome.runtime.sendMessage

适用于短生命周期、一次性的消息传递,通常用于 content script 向 background 发送请求。

// content.js 发送请求:将选中的文本发送给后台进行 AI 处理chrome.runtime.sendMessage(  { type'AI_REQUEST'menuId'translate'text'Hello' },  (response) => { // 接收来自 background 的响应if (response && response.content) {console.log(response.content);// 在页面上显示 AI 处理结果    }  });// background.js 监听并响应:处理来自 content script 的 AI 请求chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {if (message.type === 'AI_REQUEST') {// 异步调用 AI 服务    callAI(message.menuId, message.text).then(result => {      sendResponse(result); // 将 AI 处理结果发送回 content script    }).catch(error => {console.error('AI call failed:', error);      sendResponse({ error: error.message });    });returntrue// 必须返回 true,表示 sendResponse 将会异步调用  }});

(2)标签页通信:chrome.tabs.sendMessage

适用于 background 或其他扩展页面向指定标签页的 content script 发送消息。

// background.js 向指定标签页发送消息:将 AI 结果推送到当前页面显示chrome.tabs.sendMessage(tabId, {type'SHOW_POPUP'// 消息类型result: { content'翻译结果...' } // 消息内容}, response => {if (chrome.runtime.lastError) {console.warn("Error sending message to tab:", chrome.runtime.lastError.message);  }});

5.2 悬浮按钮的智能定位

当用户选中文字后,TextNova 的悬浮按钮需要显示在合适的位置,既要靠近选区,又不能超出屏幕边界,保证良好的可见性。

// 监听鼠标松开事件,处理文本选择后的浮窗显示document.addEventListener('mouseup'async (e) => {// 检查扩展是否有效(未禁用或卸载)if (!checkExtensionValid()) {if (state.floatButton) state.floatButton.style.display = 'none';return;  }// 获取用户选中的文本const selection = window.getSelection();const text = selection.toString().trim();// 只在选中文本长度为 1-5000 字符时显示浮窗if (text.length > 0 && text.length < 5000) {// 创建或获取浮窗按钮    state.floatButton = await createFloatButton();    state.floatButton.style.display = 'flex';// 检查是否需要重新定位(非手动拖动过的浮窗才自动定位)if (needsPositioning(state.floatButton)) {// 获取选中文本的位置信息const range = selection.getRangeAt(0);const rect = range.getBoundingClientRect();const buttonWidth = 130;const buttonHeight = 50;// 默认显示在选中文本下方 8px 处let top = window.scrollY + rect.bottom + 8;let left = window.scrollX + rect.left;// 如果浮窗超出右侧边界,向左调整if (left + buttonWidth > window.innerWidth) {        left = window.innerWidth - buttonWidth - 20;      }// 如果浮窗超出底部边界,尝试显示在选中文本上方if (top + buttonHeight > window.innerHeight + window.scrollY) {const alternativeTop = window.scrollY + rect.top - buttonHeight - 8;if (alternativeTop >= 0) {          top = alternativeTop;        }      }// 设置浮窗位置,至少距离边缘 10px      state.floatButton.style.top = `${Math.max(10, top)}px`;      state.floatButton.style.left = `${Math.max(10, left)}px`;    }  } else {// 未选中文本或文本过长,隐藏浮窗if (state.floatButton) {      state.floatButton.style.display = 'none';    }  }});

5.3 流式响应实现

为了提供更流畅的用户体验,TextNova 支持 AI 响应的流式显示,即结果会像打字一样逐字呈现,而不是等待全部结果返回。这通常通过处理 ReadableStream 和服务器发送事件 (SSE) 实现。

// background.jsconst response = await fetch(url, { ... });const reader = response.body.getReader();const decoder = new TextDecoder('utf-8');while (true) {const { done, value } = await reader.read();if (done) break;const chunk = decoder.decode(value, { streamtrue });// 解析 SSE 格式数据const lines = chunk.split('\n');for (const line of lines) {if (line.startsWith('data: ')) {const data = JSON.parse(line.slice(6));const content = data.choices[0].delta.content;// 实时发送到前端      chrome.tabs.sendMessage(tabId, {type'AI_STREAM_CHUNK',        content      });    }  }}

六、踩坑记录与解决方案

在开发过程中,遇到问题是常态。以下是我在开发 TextNova 时遇到的一些典型“坑”,以及它们的解决方案。

坑 1:Extension context invalidated

现象:当你在 chrome://extensions/ 页面刷新或重新加载插件后,如果之前打开的网页中存在运行中的 content script,当你尝试与其通信或操作时,会报错 Extension context invalidated

原因:旧的 content script 实例仍然存在于网页中,但它与 background Service Worker 的连接已经断开(因为 Service Worker 被重新加载了)。它所依赖的扩展上下文已不再有效。

解决:在 content script 中,每次尝试与 background 通信前,先检查扩展上下文是否仍然有效。如果失效,则提示用户刷新页面。

// 检测扩展是否有效functioncheckExtensionValid({try {return !!(chrome.runtime && chrome.runtime.id);  } catch (e) {returnfalse;  }}// 使用前检查if (!checkExtensionValid()) {  showPopup('error'null'', {error'扩展已更新,请刷新页面 (F5)'  });return;}

坑 2:异步消息处理未返回 true 导致 sendResponse 失效

现象:在 background.js 的 onMessage 监听器中,执行了异步操作(如 fetch 请求、setTimeout 等),并在异步操作完成后调用 sendResponse,但 content script 却收不到响应。

原因:如果 onMessage 监听器没有返回 true,Chrome 扩展系统会认为消息处理是同步的,并在监听器函数执行完毕后立即关闭消息通道。这样,异步操作完成后再调用的 sendResponse 将无法发送数据。

解决:当你的消息处理器中包含异步操作,并且你需要在异步操作完成后调用 sendResponse 时,必须在监听器函数中返回 true。这会告诉 Chrome 扩展系统,消息通道需要保持打开,等待异步响应。

// background.js:确保异步响应能被正确接收chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {// 异步处理 AI 请求if (message.type === 'AI_REQUEST') {    someAsyncOperation(message.text).then(result => {      sendResponse(result); // 在异步操作完成后发送响应    }).catch(error => {console.error("Async operation failed:", error);      sendResponse({ error: error.message });    });returntrue// !!! 必须返回 true,表示 sendResponse 将被异步调用 !!!  }// 其他同步消息处理,不需要返回 true// return false; // 或不返回任何值,默认即为 false});

坑 3:Storage API 数据不同步

现象:在 sidepanel.js 中修改了 chrome.storage.sync 中的设置后,background.js 立刻去读取,却发现读到的不是最新值。

原因chrome.storage.sync.set 和 chrome.storage.sync.get 都是异步操作,它们不会立即使数据同步完成。在 set 操作返回后,数据可能还没有完全写入或同步。

解决:始终使用 await 或 .then() 来确保存储操作的完成,再进行读取。在不同上下文之间,可以通过监听 chrome.storage.onChanged 事件来实时同步数据变化,这是更健壮的方式。

// sidepanel.js:写入数据时等待完成asyncfunctionsaveSettings(key, value{await chrome.storage.sync.set({ [key]: value });console.log(`Settings for ${key} saved.`);}// background.js:读取数据时等待完成asyncfunctiongetSetting(key{const result = await chrome.storage.sync.get([key]);return result[key];}// 更健壮的数据同步方式:监听 storage 变化chrome.storage.onChanged.addListener((changes, namespace) => {if (namespace === 'sync' && changes.myKey) {console.log('My key has changed:', changes.myKey.newValue);// 在这里更新 background 中的内部状态  }});

七、给初学者的建议

学习路径

  1. 先理解 Manifest V3 架构:这是基石。搞清楚 background、content script、UI Pages 各自的角色、运行环境以及它们之间的通信方式,能让你对插件的整体运作有一个清晰的认识。
  2. 从简单功能开始:不要一开始就想做一个大而全的插件。可以先实现一个简单的功能,例如一个右键菜单,点击后在控制台打印信息,逐步深入到 content script 交互、sidepanel 页面等。
  3. 善用 Chrome DevTools
    • background Service Worker 调试:访问 chrome://extensions 页面,找到你的插件卡片,点击「Service Worker」旁边的链接,即可打开独立的开发者工具窗口进行调试。
    • content script 调试:直接在注入了 content script 的网页上打开开发者工具,然后在「Sources」面板中找到你的 content script 文件进行断点调试。

开发技巧

  • 使用本地存储chrome.storage.sync 是一个非常有用的 API,它不仅能存储用户配置,还能在用户登录同一 Chrome 账号的不同设备间自动同步这些数据。对于不敏感的配置数据,它是绝佳的选择。
  • 错误处理要充分:在进行 API 调用、消息通信以及任何可能失败的操作时,都要使用 try-catch 进行错误捕获和处理。友好的错误提示能极大提升用户体验。
  • 添加调试日志:在开发阶段,多加 console.log 和 console.error 可以帮助你快速定位问题。但在发布前,记得清理或禁用这些日志,保持代码的整洁和性能。

推荐资源

  • Chrome 扩展官方文档:https://developer.chrome.com/docs/extensions/
  • Manifest V3 迁移指南:https://developer.chrome.com/docs/extensions/mv3/intro/

结语

浏览器插件开发是一个非常实用的技能,TextNova 这个项目让我深入理解了 Chrome 扩展的运作机制。整个过程最大的收获是:不要害怕报错,每个错误都是理解系统运作方式的机会

如果你也想开发自己的浏览器插件,我的建议是:从解决自己的小需求开始,比如我最初只是想方便地翻译网页上的英文,一步步迭代就有了 TextNova。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 从零开发浏览器插件:我的第一个 Chrome 扩展实战笔记

评论 抢沙发

3 + 1 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮