从零开发Edge/Chrome标签页扩展:Manifest V3与核心API实战
简介:这份资源是一套自制 Edge、Chrome 标签页扩展插件,面向具备一定前端基础、希望入门浏览器扩展开发的开发者与学习者。内容围绕 Chromium 内核扩展体系展开,涵盖 manifest.json 配置、background 后台脚本、content scripts 注入、popup 弹窗页面以及图标资源等核心模块,并涉及 jQuery 简化 DOM 操作、CSS 样式设计、chrome.tabs 与 chrome.storage 等 API 的使用,同时兼顾权限声明、安全实践、打包发布、调试技巧与跨浏览器兼容性等工程要点。压缩包为 zip 格式,整体约 14.64MB,文件类型以 JavaScript、HTML、CSS 及 JSON 配置为主,分别承担逻辑处理、界面结构、样式渲染与扩展声明等职责。目前已有 1027 人学习下载,适合想通过完整示例理解扩展目录结构、对照源码梳理开发流程、并在此基础上二次定制个性化插件的读者参考。
1. 从「标签页太乱」到自制 Edge、Chrome 标签页扩展插件
每天开几十个标签页,找东西全靠肉眼扫,Chrome 标签页分组怎么隐藏、Edge 设置启动打开新的标签页却还是恢复上次会话,这些破事堆在一起,逼得人想自己动手。自制 Edge、Chrome 标签页扩展插件,本质就是用浏览器提供的扩展 API,去读写、整理、增强标签页行为,把「手动点」变成「自动管」。它适合两类人:一类是被标签页淹没、想给自己做个顺手工具的开发者;另一类是刚学完前端、想找个真实项目练手的同学。Chrome 和 Edge 现在都基于 Chromium,扩展体系基本互通,一份代码两边都能装,这也是它值得投入的原因。下面按「先跑通最小插件 → 再吃透标签页 API → 最后处理权限和踩坑」的顺序讲,能直接抄。
2. 最小可运行插件:manifest、popup 与加载流程
2.1 先搞清楚 Manifest V3 的结构
现在新建扩展,默认走 Manifest V3(MV3)。它和老的 V2 最大区别是:后台脚本从常驻的 background page 换成了按需唤醒的 service worker,权限声明更细,远程代码基本被禁。对标签页插件来说,核心就三块:manifest.json声明权限和入口,popup 或 side panel 做界面,service worker 或 popup 里的脚本调chrome.tabs系列 API。
一个能跑的最小目录长这样:
tab-tool/ ├── manifest.json ├── popup.html ├── popup.js └── icons/ └── icon128.pngmanifest.json里必须声明tabs权限才能读取标签页的 url、title 等敏感字段;如果只是操作当前标签页,activeTab就够,权限越小审核越顺。
2.2 写一个能列出所有标签页的 popup
先上manifest.json:
{ "manifest_version": 3, "name": "Tab Tool", "version": "1.0.0", "description": "列出并管理当前窗口的标签页", "permissions": ["tabs"], "action": { "default_popup": "popup.html", "default_icon": { "128": "icons/icon128.png" } }, "icons": { "128": "icons/icon128.png" } }逻辑说明:manifest_version固定 3;permissions里放tabs,这是后面能拿到tab.url的前提;action.default_popup指定点扩展图标后弹出的页面。参数上,version每次改动建议递增,否则浏览器可能不刷新缓存。
再写popup.html和popup.js:
<!DOCTYPE html> <html> <head><meta charset="utf-8"><style> body { width: 320px; font: 13px/1.5 sans-serif; margin: 0; padding: 8px; } li { display: flex; justify-content: space-between; padding: 4px 0; } button { cursor: pointer; } </style></head> <body> <ul id="list"></ul> <script src="popup.js"></script> </body> </html>// 查询当前窗口所有标签页并渲染 async function renderTabs() { // currentWindow: true 只取当前窗口,避免把别的窗口也列出来 const tabs = await chrome.tabs.query({ currentWindow: true }); const ul = document.getElementById('list'); ul.innerHTML = ''; for (const tab of tabs) { const li = document.createElement('li'); const span = document.createElement('span'); // tab.title 可能为空,兜底显示 url span.textContent = tab.title || tab.url || '(无标题)'; const btn = document.createElement('button'); btn.textContent = '关闭'; btn.onclick = async () => { await chrome.tabs.remove(tab.id); // 按 id 关闭 renderTabs(); // 关完重渲染 }; li.append(span, btn); ul.appendChild(li); } } renderTabs();逻辑说明:chrome.tabs.query返回 Promise(MV3 支持 Promise 写法),currentWindow: true是关键参数,不加会把所有窗口的标签页都拉进来。chrome.tabs.remove(tab.id)按 id 关闭,关完必须重新查询,否则列表和实际状态对不上。参数上,query还支持active、pinned、url等过滤条件,后面做「只关重复页」就靠它。
2.3 在 Edge 和 Chrome 里加载调试
打开chrome://extensions/或edge://extensions/,右上角开启「开发者模式」,点「加载已解压的扩展程序」,选中tab-tool目录。改完代码后回到扩展页点刷新图标即可,popup 不用重装。调试 popup 时右键扩展图标选「审查弹出内容」,能直接开 DevTools;service worker 的日志在扩展页点「Service Worker」链接查看。这一步是后面所有功能的地基,先确认列表能正常显示再往下走。
3. 标签页核心 API:查询、分组、批量操作怎么落地
3.1 tabs.query 的过滤参数与常见组合
chrome.tabs.query是整个插件的入口,参数组合决定了你能拿到什么。常用的几组:
| 参数 | 作用 | 典型场景 |
|---|---|---|
currentWindow | 限定当前窗口 | 只管理本窗口 |
active | 只取激活标签 | 获取当前页 url |
pinned | 只取固定标签 | 批量取消固定 |
url | 按 url 模式匹配 | 找同域名重复页 |
groupId | 按分组 id 取 | 操作某个标签组 |
比如找当前窗口里所有同域名的重复标签:
// 找出当前窗口内 url 完全相同的标签,保留第一个 async function findDuplicates() { const tabs = await chrome.tabs.query({ currentWindow: true }); const seen = new Map(); const dupIds = []; for (const tab of tabs) { if (!tab.url) continue; // 扩展页等可能没有 url if (seen.has(tab.url)) { dupIds.push(tab.id); } else { seen.set(tab.url, tab.id); } } return dupIds; }逻辑说明:用 Map 记录第一次出现的 url,再次出现就归为重复。参数上注意tab.url在没tabs权限时是空的,所以权限必须给到位。tab.id是后续关闭、移动、分组的唯一凭据,务必保存。
3.2 用 tabGroups 做自动分组
Chromium 从 89 起支持标签组,扩展侧对应chrome.tabs.group和chrome.tabGroups。自动按域名分组的最小实现:
// 按域名把当前窗口标签分组,同域名归一组 async function groupByDomain() { const tabs = await chrome.tabs.query({ currentWindow: true }); const byDomain = {}; for (const tab of tabs) { if (!tab.url || !tab.url.startsWith('http')) continue; const domain = new URL(tab.url).hostname; (byDomain[domain] ||= []).push(tab.id); } for (const [domain, ids] of Object.entries(byDomain)) { if (ids.length < 2) continue; // 单个标签不分组 const groupId = await chrome.tabs.group({ tabIds: ids }); await chrome.tabGroups.update(groupId, { title: domain, color: 'blue' }); } }逻辑说明:先按 hostname 聚合,再对数量大于 1 的组调chrome.tabs.group,它返回 groupId,接着用chrome.tabGroups.update设置标题和颜色。参数上color只接受固定枚举(grey/blue/red/yellow/green/pink/purple/cyan/orange),传别的会报错。注意chrome.tabs.group需要tabGroups权限,别漏声明。
3.3 批量关闭、固定与移动
批量操作的核心是「先 query 出 id 列表,再循环调 API」。关一批:
// 关闭所有非固定、非激活的标签页 async function closeInactive() { const tabs = await chrome.tabs.query({ currentWindow: true, pinned: false, active: false }); const ids = tabs.map(t => t.id); if (ids.length) await chrome.tabs.remove(ids); // remove 支持传数组 }逻辑说明:chrome.tabs.remove既能传单个 id 也能传数组,批量关一次调用即可,比循环快。参数上pinned: false保证固定标签不被误关,active: false保住当前页。移动标签用chrome.tabs.move(id, { index }),固定用chrome.tabs.update(id, { pinned: true })。这些 API 都返回 Promise,记得await,否则连续操作会因状态未更新而错乱。
4. 权限、跨浏览器兼容与界面选型
4.1 权限最小化:tabs、activeTab、host_permissions 的区别
很多人一上来就把tabs、<all_urls>全勾上,结果 Edge 加载时警告一堆,Chrome 商店审核也慢。三者的边界:
activeTab:用户点击扩展图标后,临时获得当前标签页的访问权,最轻量,适合「只处理当前页」。tabs:能读取所有标签页的 url、title、favIconUrl,做标签管理基本绕不开。host_permissions:访问页面 DOM 内容用,比如注入脚本改页面。只做标签页管理不需要它。
结论:纯标签页管理给tabs就够;要读页面内容再加具体域名,别用<all_urls>一把梭。
4.2 Chrome 与 Edge 的差异点
两者同源 Chromium,chrome.*API 在 Edge 里同样可用,Edge 也认browser.*。实际差异主要在:Edge 有自己的edge://extensions/管理页和商店,离线安装包分发方式不同;部分 Edge 独有 API(如侧边栏)在 Chrome 里没有。写代码时用特性检测而不是硬判断浏览器:
// 优先用 Promise 风格,兼容 chrome 与 browser 命名空间 const api = globalThis.browser ?? globalThis.chrome; const tabs = await api.tabs.query({ currentWindow: true });逻辑说明:Firefox 用browser,Chromium 系用chrome,这行做统一入口。参数上不用管版本号,MV3 下两者都支持 Promise。这样一份代码在 Edge、Chrome 都能跑,省得维护两套。
4.3 popup、side panel 还是 options 页
界面选型看使用频率:popup 适合「点一下就用完」的轻操作,缺点是失焦即关,不能做长任务;side panel(Chrome 114+、Edge 支持)常驻侧边,适合标签页列表这种要一直看的场景;options 页放设置项。标签管理插件我一般主界面用 side panel,设置放 options,popup 只留一个快捷入口。注意 side panel 需要在 manifest 里声明side_panel字段并调chrome.sidePanel.setPanelBehavior,不是所有旧版本都支持,做兼容时留好降级。
5. 避坑与排查:加载失败、API 不生效、状态错乱
5.1 改了代码没生效
现象:改完popup.js刷新扩展,界面还是旧的。原因:popup 页面被缓存,或 service worker 没重启。解决:在扩展页点刷新后,关掉已打开的 popup 重新点开;service worker 在扩展页点「Service Worker」链接进 DevTools 后按 Ctrl+R 强制重启。macOS 上 Chrome 强制刷新用 Cmd+Shift+R。
5.2 tabs.query 拿不到 url
现象:tab.url是undefined。原因:manifest 没声明tabs权限,或该标签是chrome://、扩展页等受限页面。解决:补上"permissions": ["tabs"];对受限页面做判断跳过,别硬读。这类页面本来也不允许扩展操作,属于预期行为。
5.3 批量操作后列表和实际对不上
现象:关了几个标签,列表还显示旧的。原因:chrome.tabs.remove是异步的,没await就重渲染,拿到的是旧状态。解决:所有 tabs API 调用都await,操作完再重新query一次。更稳的做法是监听chrome.tabs.onRemoved、onUpdated事件,状态变了自动刷新界面,而不是手动重渲染。
5.4 分组颜色或标题报错
现象:tabGroups.update抛错。原因:color传了枚举外的值,或 groupId 已失效(组被解散)。解决:颜色只用官方枚举;操作前先chrome.tabGroups.get(groupId)确认存在,捕获异常做兜底,别让一个失败中断整批操作。
5.5 Edge 开发者模式下的加载警告
现象:Edge 加载时提示权限过多或清单有问题。原因:权限声明过宽,或 manifest 字段拼写错误。解决:按第 4 章做权限最小化;用edge://extensions/里的「错误」按钮看具体报错行,逐条对照 manifest 规范改。Edge 对 MV3 的校验和 Chrome 基本一致,Chrome 能过的一般 Edge 也能过。
6. 进阶:用事件驱动替代轮询,把插件做稳
前面所有操作都是「用户点了才查」,真正好用的标签页插件得会「自己感知变化」。核心是监听chrome.tabs的事件,而不是定时轮询——轮询既费电又容易和实际状态错位。常用事件有onCreated、onRemoved、onUpdated、onActivated、onMoved,以及chrome.tabGroups.onUpdated。
一个自动维护「重复标签」提示的实现:
// 监听标签创建与更新,实时统计重复 url 数量 const urlCount = new Map(); async function refreshCount() { const tabs = await chrome.tabs.query({ currentWindow: true }); urlCount.clear(); for (const tab of tabs) { if (!tab.url) continue; urlCount.set(tab.url, (urlCount.get(tab.url) || 0) + 1); } const dup = [...urlCount.values()].filter(n => n > 1).length; // 用 badge 显示重复组数量 await chrome.action.setBadgeText({ text: dup ? String(dup) : '' }); await chrome.action.setBadgeBackgroundColor({ color: '#e00' }); } chrome.tabs.onCreated.addListener(refreshCount); chrome.tabs.onRemoved.addListener(refreshCount); chrome.tabs.onUpdated.addListener((id, info) => { if (info.url) refreshCount(); // 只有 url 变了才重算 }); refreshCount();逻辑说明:用 Map 统计每个 url 出现次数,重复组数写进扩展图标的 badge。参数上onUpdated的changeInfo.url只在地址变化时存在,用它做过滤能避免每次加载状态变化都重算。setBadgeText传空字符串即清空角标。这套事件驱动比setInterval干净得多,也不会在后台空转。
验证方法:开几个相同网址的标签,看 badge 数字是否实时变化;关掉一个,数字应立刻减。如果不动,先查 service worker 是否被浏览器休眠——MV3 的 service worker 空闲会被挂起,事件会重新唤醒它,但全局变量会丢,所以urlCount这种状态要么每次重算(如上),要么存进chrome.storage。这是 MV3 最容易翻车的地方,我早期就栽在「以为全局变量一直在」上,后来全部改成事件触发时重新 query,稳了。
再补一个实用技巧:把常用操作绑到chrome.commands快捷键,比如 Ctrl+Shift+D 一键去重,比点图标快。manifest 里声明commands,service worker 里监听chrome.commands.onCommand分发即可。做插件这事,别一上来追求功能全,先把「查询—操作—事件刷新」这条链路跑顺,再往上叠分组、去重、快捷键。我自己现在用的就是这套骨架,从最初只能列标签,到现在自动分组加去重提示,迭代了七八个版本,每次都是先保证状态同步不出错,再加新功能。希望帮到你。
本文还有配套的精品资源,点击获取