当前位置: 首页 > news >正文

第八章:终极合体 —— 实现智能一键分组

第八章:终极合体 —— 实现智能一键分组

本章目标:整合 chrome.tabschrome.storage API,实现一个能够读取用户配置(白名单),并智能地将标签页按域名分组的强大功能。同时,我们将学习 chrome.tabGroups API 的使用。


引子:从“无脑执行”到“带脑思考”

想象一个机器人管家。

  • 初级阶段:你对它说“把所有书都放到书架上”,它会勤勤恳恳地把桌上所有的书,包括你正在读的、做满了笔记的那本,一股脑地塞进书架。它执行了命令,但结果可能让你很恼火。

  • 高级阶段:你对它下达同样的指令。但这次,它的大脑里有一个你之前告诉它的规则:“主人正在读的书,不要动。” 于是,它会智能地跳过那本你正在读的书,只整理其他的。这,才是真正的“智能”。

我们之前的设想(在第六章末尾的伪代码)就处于“初级阶段”。它能按域名分组,但它会把所有标签页都分组,可能会打乱用户精心安排的布局。

而今天,我们将要把它升级到“高级阶段”。我们的“一键分组”功能,在执行前,会先去“大脑”(chrome.storage)里查询一下用户设置的“白名单”,然后“思考”一下,最终做出一个更符合用户心意的决策。

这个过程,将完美地展现一个优秀浏览器扩展是如何将用户交互 (Popup) -> 后台逻辑 (Background) -> 数据存储 (Storage) -> 浏览器控制 (Tabs/TabGroups) 这几个核心环节无缝串联起来的。

这不仅是技术的整合,更是我们开发思想的一次升华。


8.1 新的武器库:chrome.tabGroups API 概览

在实现分组功能之前,我们必须先认识一下专门用来管理标签页分组的 API:chrome.tabGroups。它和 chrome.tabs 是兄弟关系,但专注于“分组”这个维度。

记住,使用它需要在 manifest.json"permissions" 中添加 "tabGroups"。但好消息是,如果你已经有了 "tabs" 权限,那么你将自动获得 "tabGroups" 的权限,无需额外添加。

核心方法
  • chrome.tabGroups.update(groupId, updateProperties, callback)

    • 这是最重要的一个!当你用 chrome.tabs.group() 创建了一个分组后,它只是一个默认的、没有名字的、颜色随机的分组。你需要用这个方法来给它“装扮”一下。
    • groupId: 你要更新的分组的 ID。
    • updateProperties (对象):
      • title: string: 设置分组的标题。这会显示在标签栏上。
      • color: string: 设置分组的颜色。可选值有 grey, blue, red, yellow, green, pink, purple, cyan
      • collapsed: boolean: 设置分组是展开还是折叠状态。
  • chrome.tabGroups.query(queryInfo, callback)

    • tabs.query 类似,用于查询符合条件的分组。
    • queryInfo (对象):
      • collapsed: boolean: 是否是折叠的。
      • color: string: 特定颜色的分组。
      • title: string: 特定标题的分组。
      • windowId: number: 特定窗口中的分组。
  • chrome.tabGroups.move(groupId, moveProperties, callback)

    • 移动一个分组到窗口中的新位置。

TabGroup 对象是什么样的?
每个 TabGroup 对象包含了分组的信息:

{id: 456, // 分组的唯一数字 IDcollapsed: false, // 是否折叠color: "blue", // 颜色title: "Google Related", // 标题windowId: 1 // 所属窗口 ID
}

现在,我们的武器库已经更新完毕。让我们开始这场终极的整合之战。


8.2 项目实战:构建智能分组的逻辑流

我们的目标是:当用户点击 Popup 上的“一键分组”按钮时,触发一个完整的、智能的后台处理流程。

数据流拆解:

  1. (Popup) 用户点击按钮,向 Background 发送 "GROUP_TABS_SMART" 消息。
  2. (Background) 接收到消息,进入主处理函数。
  3. (Background) 第一步:获取用户配置。调用 chrome.storage.sync.get(),异步获取 whitelistedSites 白名单。
  4. (Background) 第二步:获取标签页信息。调用 chrome.tabs.query(),异步获取当前窗口所有标签页。
  5. (Background) 第三步:智能分类。等待前两步都完成后,开始遍历所有标签页。
    • 对于每个标签页,提取其域名。
    • 检查该域名是否在白名单中。
    • 如果不在白名单中,则将其 tab.id 按域名归类到一个准备分组的对象中。
  6. (Background) 第四步:执行分组。遍历分类好的对象。
    • 对于每个域名,如果其下有多个标签页,则调用 chrome.tabs.group() 将它们创建为一个分组。
    • group 的回调中,拿到 groupId,然后立即调用 chrome.tabGroups.update(),用域名作为标题来更新这个分组。
  7. (Background) (可选) 向 Popup 回复一条消息,告知任务完成。
  8. (Popup) 收到完成的消息后,可以刷新一下列表,或者给用户一个成功的提示。

这个流程使用了多个异步 API,我们需要妥善地处理它们之间的依赖关系。async/await 将是我们的最佳伙伴。

第一步:改造 Background (实现核心处理函数)

打开 scripts/background.js。我们将要大展身手,编写整个项目的“皇冠上的明珠”。

// scripts/background.jschrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {// ... 其他 if 分支 ...// --- 智能分组的逻辑分支 ---if (message.action === "GROUP_TABS_SMART") {console.log("Background: 收到智能分组请求...");try {// Step 1: 获取用户配置 (白名单)const storageData = await chrome.storage.sync.get({ whitelistedSites: [] });const whitelist = new Set(storageData.whitelistedSites); // 使用 Set 以获得 O(1) 的查询效率console.log("Background: 获取到白名单:", whitelist);// Step 2: 获取当前窗口的所有标签页const tabs = await chrome.tabs.query({ currentWindow: true });console.log("Background: 获取到", tabs.length, "个标签页");// Step 3: 智能分类const tabsToGroup = {}; // key 是域名, value 是 tabId 数组for (const tab of tabs) {// 确保 tab 有 URL 且不是 chrome:// 等内部页面if (tab.url && !tab.url.startsWith('chrome://')) {try {const domain = new URL(tab.url).hostname;// 如果域名不在白名单中,则加入待分组列表if (!whitelist.has(domain)) {if (!tabsToGroup[domain]) {tabsToGroup[domain] = [];}tabsToGroup[domain].push(tab.id);}} catch (error) {console.warn(`无法解析 URL: ${tab.url}`, error);}}}console.log("Background: 待分组的标签页:", tabsToGroup);// Step 4: 执行分组let groupsCreated = 0;for (const domain in tabsToGroup) {const tabIds = tabsToGroup[domain];// 只对拥有2个或以上标签页的域名进行分组,更有意义if (tabIds.length >= 2) {const groupId = await chrome.tabs.group({ tabIds: tabIds });await chrome.tabGroups.update(groupId, { title: domain });groupsCreated++;}}console.log("Background: 智能分组完成,创建了", groupsCreated, "个分组。");sendResponse({ status: "success", groupsCreated: groupsCreated });} catch (error) {console.error("Background: 智能分组过程中发生错误:", error);sendResponse({ status: "error", message: error.message });}// 因为整个函数是 async 的,并且我们正确使用了 await,// sendResponse 会在所有异步操作完成后执行。// 所以我们不需要显式 return true。async 函数隐式返回 Promise。// 但为了兼容所有情况和保持良好习惯,在复杂的异步消息处理中,// return true 仍然是处理非 async 回调时的金科玉律。// 在这里,因为整个监听器回调是 async 的,所以可以省略。}// 为了让其他非 async 的分支正常工作,我们最好在末尾判断// (虽然此例中其他分支都是同步的,但这是个好习惯)// 或者将所有分支都改写为 async// return true; 
});

让我们来深度剖析这段精彩的代码:

  1. async/await 的威力:我们把 onMessage 的回调函数声明为了 async 函数。这使得我们可以用 await 来等待异步操作(如 storage.get, tabs.query)完成,让代码看起来像同步执行一样,极大地增强了可读性,避免了“回调地狱”。
  2. new Set(storageData.whitelistedSites): 这是一个性能优化的细节。将数组转换成 Set(集合)后,使用 whitelist.has(domain) 来检查一个元素是否存在的时间复杂度是 O(1)(常量时间),远快于数组的 includes() 方法的 O(n)(线性时间)。当白名单很长时,这个优化会非常显著。
  3. URL 解析与错误处理:我们使用 new URL(tab.url).hostname 来安全地提取域名。并用 try...catch 包裹它,因为有些 URL(比如无效的 javascript: 链接)可能会导致 new URL() 构造函数失败。我们还过滤掉了 chrome:// 开头的内部页面。
  4. 分类逻辑:我们创建了一个 tabsToGroup 对象,它的键是域名,值是对应域名的 tabId 数组。这是一个典型的数据规整(Data Wrangling)过程。
  5. 有意义的分组:我们添加了一个判断 if (tabIds.length >= 2),只对那些至少有两个标签页的域名进行分组。为一个孤零零的标签页创建分组是没有意义的。
  6. 串行分组操作await chrome.tabs.group(...)await chrome.tabGroups.update(...) 确保了我们对每个域名的处理是原子性的——先创建分组,再立刻更新它的标题。
  7. 统一的错误处理:我们用一个大的 try...catch 块包裹了整个逻辑。任何一步的 await 如果失败(抛出异常),都会被 catch 块捕获,然后我们可以向 Popup 发送一个包含错误信息的回应,而不是让扩展默默地崩溃。
  8. 关于 return true; 的说明:当 onMessage 的监听器本身是 async 函数时,它会自动返回一个 Promise。Chrome 的消息系统能够识别这一点,并会等待这个 Promise 完成(resolve 或 reject),所以理论上可以不写 return true;。但如果你的监听器中混合了 async 和非 async 的分支,保持 return true; 的习惯仍然是最稳妥的。
第二步:改造 Popup (触发智能分组)

现在,我们需要让我们的“一键分组”按钮名副其实。它不再是刷新列表,而是要发送 GROUP_TABS_SMART 指令。

打开 popup.html<script> 部分。

// popup.html 内的 <script>
document.addEventListener('DOMContentLoaded', function() {const groupTabsBtn = document.getElementById('groupTabsBtn');// ... 其他代码 ...// --- 为“一键分组”按钮绑定真正的功能 ---groupTabsBtn.addEventListener('click', () => {// 给用户一个即时反馈groupTabsBtn.textContent = '正在分组...';groupTabsBtn.disabled = true;chrome.runtime.sendMessage({ action: "GROUP_TABS_SMART" }, (response) => {if (chrome.runtime.lastError) {console.error("智能分组失败:", chrome.runtime.lastError.message);// 可以在此给用户一个错误提示} else if (response && response.status === "success") {console.log("分组成功,创建了", response.groupsCreated, "个分组。");// 分组后,最好刷新一下列表,以反映新的状态fetchAndRenderTabs();} else {console.error("分组失败,后台返回:", response.message);}// 恢复按钮状态groupTabsBtn.textContent = '一键分组当前窗口的标签页';groupTabsBtn.disabled = false;});});// ... 其他代码 ...
});

代码解读:

  1. 我们找到了“一键分组”按钮(groupTabsBtn)的点击事件监听器。
  2. 在发送消息前,我们立即修改了按钮的文本并将其禁用 (disabled = true)。这是一个非常重要的用户体验优化,它能防止用户在处理过程中反复点击,并明确地告诉用户“系统正在工作中”。
  3. 我们向后台发送了新的“暗号”:GROUP_TABS_SMART
  4. 在收到后台的回复后,我们检查状态。如果成功,我们调用 fetchAndRenderTabs()刷新 Popup 中的列表。虽然分组是直接在浏览器标签栏上体现的,但刷新列表可以让用户看到标签顺序的变化,保持界面与实际状态同步。
  5. 无论成功还是失败,最后我们都恢复按钮的原始状态,以便用户可以进行下一次操作。
第三步:(可选) 添加一个打开选项页的链接

为了让用户能方便地找到我们的白名单设置,我们可以在 Popup 中添加一个链接,直接跳转到选项页。

popup.html<footer> 部分添加一个链接:

<!-- popup.html -->
<footer class="footer"><a href="options.html" target="_blank" id="options-link">设置</a><p>Made with ❤️ by [你的名字]</p>
</footer>

再加点 CSS 让它好看些:

/* popup.html 内的 <style> */
.footer {/* ... */display: flex;justify-content: space-between;align-items: center;
}
#options-link {color: #4A90E2;text-decoration: none;
}
#options-link:hover {text-decoration: underline;
}
第四步:终极部署与验证

我们已经完成了所有的编码工作。现在,是时候验收我们的最终成果了。

  1. 保存所有修改过的文件 (background.js, popup.html)。
  2. chrome://extensions 页面,刷新扩展。
  3. 准备测试环境
    • 设置白名单:点击 Popup 右下角的“设置”链接(或者从扩展管理页进入),打开选项页。在白名单文本框中输入一些你不想分组的网站,比如 github.com,然后保存。
    • 创建“标签页地狱”:打开多个不同网站的标签页。确保其中包含:
      • 多个 google.com 的页面。
      • 多个 developer.mozilla.org 的页面。
      • 几个你已加入白名单的 github.com 页面。
      • 一些单独的、不成对的页面。
  4. 点击我们工具栏上的扩展图标,打开 Popup。
  5. 深呼吸,然后点击那个绿色的“一键分组当前窗口的标签页”按钮!

见证魔法的时刻!

你应该能观察到以下一系列流畅的动作:

  • 浏览器顶部的标签栏发生了翻天覆地的变化!
    • 所有 google.com 的页面被收纳进了一个名为 “google.com” 的彩色分组里。
    • 所有 developer.mozilla.org 的页面也被收纳进了它们自己的分组。
    • 那几个 github.com 的页面,以及其他单个的页面,安然无恙地留在了原地,没有被分组!
  • Popup 上的按钮先是显示“正在分组…”,完成后又恢复原状。
  • 分组完成后,Popup 里的标签页列表自动刷新,反映了最新的顺序。
  • 打开 Background 的开发者工具,你可以看到整个智能分组流程的详细日志,从读取白名单到最终创建了多少个分组,一目了然。

我们做到了!我们创造了一个真正智能、可配置、并且极其实用的生产力工具!


本章总结与展望

在这一章,我们完成了一次酣畅淋漓的“技术大阅兵”,将前面所有章节学到的知识融会贯通:

  1. 整合了核心 API:我们将 chrome.tabs 的查询和分组能力,与 chrome.storage 的数据持久化能力完美结合。
  2. 实现了智能逻辑:我们的扩展不再是机械执行,而是能够根据用户配置(白名单)进行“思考”,做出更符合用户预期的决策。
  3. 掌握了 tabGroups API:我们学会了如何创建分组后,利用 update 方法为其命名和上色。
  4. 优化了开发流程:我们全面拥抱 async/await,编写出了清晰、健壮、易于维护的异步代码。
  5. 提升了用户体验:通过禁用按钮、状态反馈、刷新UI等细节,我们让整个交互过程更加流畅和人性化。

我们的“智能标签页管家”的核心功能已经全部完成。它已经是一个可以打包发布、能为用户带来真实价值的作品了。

在接下来的最后几章,我们将把重心从“功能实现”转向“工程化和产品化”。我们将学习:

  • 如何调试我们的扩展? (Popup, Background, Content Script 的调试技巧汇总)
  • 如何让我们的扩展更快、更强? (性能优化的基本原则)
  • 如何将我们的心血结晶打包,并上架到 Chrome 网上应用店? (打包、注册开发者账号、提交审核的全流程)
http://www.dtcms.com/a/322928.html

相关文章:

  • 【Python 工具人快餐 · 第 1 份】
  • 【代码随想录|232.用栈实现队列、225.用队列实现栈、20.有效的括号、1047.删除字符串中的所有相邻重复项】
  • 第05章 排序与分页
  • 模板方法模式:优雅封装算法骨架
  • Python-UV-portry项目管理流程
  • redis8.0.3部署于mac
  • C++ 中的智能指针
  • Python 继承和多态
  • ElaWidgetTools qt5+vs2019编译
  • 1.JavaScript 介绍
  • 基于STM32的智能电表设计与实现
  • 计算机组成原理2-4-1:浮点数的表示
  • Linux 安装 JDK 8u291 教程(jdk-8u291-linux-x64.tar.gz 解压配置详细步骤)​
  • 【c++】探秘Loop机制:C++中优雅的双向数据交互模式
  • 低速CAN 高速CAN是否兼容?
  • 功能测试详解
  • 【面试题】cookie和session 的区别
  • Ubuntu下Nginx的部署后端项目(Java为例),配置Nginx代理
  • 自编教材实操课程学习笔记
  • 商品、股指及ETF期权五档盘口Tick级与分钟级历史行情数据多维解析
  • dify离线插件安装
  • Spring Boot Starter 自动化配置原理深度剖析
  • 【工具变量】地市人力资本水平数据集(2003-2023年)
  • 聊聊经常用的微服务
  • Java 枚举解析:从基础到进阶的知识点与注意事项
  • 【完整源码+数据集+部署教程】植物生长阶段检测系统源码和数据集:改进yolo11-rmt
  • gRPC for C++ 实战全流程 —— 从零搭建到同步/异步服务
  • vw和vh:CSS中的视口相对单位
  • Linux下管道的实现
  • 第十四节 代理模式