第六章:玩转浏览器 ------ chrome.tabs
API 精讲与实战
本章目标:深入学习 chrome.tabs
API 的核心方法,如 query
, create
, update
, group
,并利用它们实现"智能标签页管家"的第一个核心功能:一键查询并展示所有标签页。
从"标签页地狱"到"标签页天堂"**
让我们面对现实:我们都是"标签页囤积症"患者。
在研究一个新课题、对比几件商品,或者仅仅是漫无目的地网上冲浪时,我们的浏览器顶部很快就会变成这样:
[Goo...][GitH...][知乎 -...][Bili...][淘宝...][Noti...][沉浸...][MDN...][Juej...][From...][...][...]
几十个标签页挤在一起,连标题都看不清,像一排挤得密不透风的沙丁鱼罐头。我们称之为**"标签页地狱" (Tab Hell)**。在这种状态下,找到你真正需要的那个页面,简直像是在大海捞针。CPU 占用率飙升,电脑风扇狂转,你的注意力和生产力也被无情地吞噬。
而我们"智能标签页管家"的终极使命,就是要把我们从这个地狱中解救出来,带领我们进入一个有序、清晰、可控的**"标签页天堂"**。
要做到这一点,我们必须先学会如何与浏览器中的"标签页"这个实体进行对话。chrome.tabs
API 就是我们的"通用语"。通过它,我们可以:
- 查询 (Query):像人口普查一样,精确地找出所有(或符合特定条件的)标签页。
- 创建 (Create):像女娲造人一样,随时创造一个新的标签页。
- 更新 (Update) :给指定的标签页"搬家"(改变 URL)或"改名"(虽然不能直接改名,但可以执行脚本改变
document.title
)。 - 分组 (Group):这是 MV3 时代的一个明星功能,像整理书架一样,把相关的标签页归类到一起,并给它们贴上彩色的标签。
- 移动 (Move) 、移除 (Remove)... 等等。
今天,我们将从最基础也是最重要的 query
方法开始,一步步实现我们 Popup 界面上的核心功能:点击按钮,列出当前窗口所有打开的标签页。
6.1 核心 API 精讲:chrome.tabs
的四大天王
在深入项目之前,我们先快速了解一下 chrome.tabs
API 中最常用的几个方法。记住,所有这些方法都需要在 manifest.json
中声明 "tabs"
权限,并且它们都是异步的,通常以回调函数或 Promise 的形式返回结果。
1. chrome.tabs.query(queryInfo, callback)
------ 人口普查员
这是你最常打交道的"伙计"。它能帮你筛选出符合你各种苛刻条件的标签页。
queryInfo
(对象): 一个包含查询条件的对象。你可以把它想象成一张筛选表。active: boolean
: 是否是当前窗口的活动标签页。通常一个窗口只有一个。currentWindow: boolean
: 是否在当前 (即代码执行时所在的那个)窗口。设为true
可以避免查到其他显示器上最小化的窗口里的标签页。url: string | string[]
: 匹配 URL。支持 匹配模式,比如"*://*.google.com/*"
。title: string
: 匹配标题。windowId: number
: 属于哪个窗口。- ...还有很多其他条件。
- 如果传入一个空对象
{}
,则表示"给我所有的标签页!"
callback(tabs: Tab[])
: 查询完成后执行的回调函数。tabs
: 一个数组,包含了所有符合条件的Tab
对象。如果一个都没找到,它就是一个空数组。
Tab
对象是什么样的?
每个 Tab
对象都包含了关于一个标签页的详尽信息:
javascript
{
id: 123, // 标签页的唯一数字 ID,非常重要!
index: 0, // 在窗口中的位置(从0开始)
windowId: 1, // 所属窗口的 ID
url: "https://www.google.com/", // 当前 URL
title: "Google", // 当前标题
favIconUrl: "https://www.google.com/favicon.ico", // 网站的小图标 URL
active: true, // 是否是活动标签页
audible: false, // 是否正在播放声音
...
}
2. chrome.tabs.create(createProperties, callback)
------ 创世神
用于创建一个新的标签页。
createProperties
(对象): 新标签页的属性。url: string
: 新标签页要打开的 URL。active: boolean
(默认为true
): 创建后是否立即激活它。index: number
: 想把它插入到哪个位置。windowId: number
: 想在哪个窗口创建。
3. chrome.tabs.update(tabId, updateProperties, callback)
------ 改造者
用于修改一个已存在的标签页。
tabId
(可选的数字): 你要修改的那个标签页的 ID。如果省略,则默认修改当前活动标签页。updateProperties
(对象): 你想修改的属性。url: string
: 将标签页导航到新的 URL。active: boolean
: 激活或取消激活这个标签页。highlighted: boolean
: 高亮(选中)这个标签页。muted: boolean
: 静音或取消静音。
4. chrome.tabs.group(options, callback)
------ 整理大师
MV3 的明星功能,用于将标签页分组。
options
(对象): 分组选项。tabIds: number | number[]
: 你想编入一组的一个或多个标签页的 ID。createProperties
(可选对象): 如果要创建新分组,可以在这里定义分组的属性。windowId: number
: 在哪个窗口创建分组。
callback(groupId: number)
: 操作完成后,会返回这个新创建或更新的分组的 ID。
一旦分组创建,你还可以使用 chrome.tabGroups.update(groupId, updateProperties)
来给分组改名、改颜色等。
理论知识已经足够了。现在,让我们把这些强大的能力应用到我们的项目中去!
6.2 项目实战:实现核心功能
我们的目标非常明确:改造 Popup,让它在打开时,或者点击某个按钮时,能够查询并展示当前窗口的所有标签页。
这个功能逻辑比较重,涉及到权限 API 调用,所以它不应该在 Popup 的 JS 里直接执行。还记得我们上一章学到的"关注点分离"吗?
正确的数据流 应该是:
Popup (UI) -> Background (逻辑/API调用) -> Popup (渲染结果)
- Popup 向 Background 发送一个"请给我标签页列表"的请求。
- Background 收到请求后,调用
chrome.tabs.query()
获取数据。 - Background 将获取到的
Tab
对象数组回复给 Popup。 - Popup 收到数组后,动态地创建
<li>
元素,将列表渲染到界面上。
第一步:改造 Background (成为数据提供方)
打开我们的"指挥中心" scripts/background.js
。我们需要在 onMessage
监听器里添加一个新的分支,来处理获取标签页的请求。
javascript
// scripts/background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// ... 其他 if/else if 分支 ...
// --- 新增逻辑分支 ---
if (message.action === "GET_TABS_FOR_CURRENT_WINDOW") {
console.log("Background: 收到获取当前窗口标签页的请求...");
// 使用 chrome.tabs.query 获取标签页
// { currentWindow: true } 是一个非常常用的过滤器,
// 它确保我们只获取当前用户正在交互的这个窗口里的标签页。
chrome.tabs.query({ currentWindow: true }, (tabs) => {
console.log("Background: 查询完毕,找到", tabs.length, "个标签页。");
// 将查询结果通过 sendResponse 回复给请求方 (Popup)
sendResponse({ status: "success", tabs: tabs });
});
// 同样,因为 tabs.query 是异步的,所以必须 return true
return true;
}
});
代码解读:
- 我们定义了一个新的"暗号"
GET_TABS_FOR_CURRENT_WINDOW
。 - 在对应的逻辑块里,我们调用
chrome.tabs.query({ currentWindow: true }, ...)
。这个查询条件非常实用,它帮我们过滤掉了其他最小化或在其他显示器上的窗口。 - 在
query
的回调函数中,我们拿到了tabs
数组(一个包含了多个Tab
对象的数组)。 - 我们通过
sendResponse
将这个数组包装在一个对象里回复出去。包装一下(比如加上status
字段)是个好习惯,方便未来扩展和错误处理。 - 不要忘记
return true;
!这是异步消息通信的生命线。
第二步:改造 Popup (成为数据请求和渲染方)
现在,轮到我们的"驾驶舱" popup.html
了。我们将要实现:
- 当 Popup 打开时,自动发送请求获取标签页列表。
- 将获取到的列表动态地渲染到我们之前预留的
<ul id="tab-list">
中。
打开 popup.html
并定位到 <script>
部分。
javascript
// popup.html 内的 <script>
document.addEventListener('DOMContentLoaded', function() {
// ... 原有的 getElementById 和事件监听器 ...
// --- 核心功能实现 ---
// 函数:用于渲染标签页列表
function renderTabList(tabs) {
const tabListUl = document.getElementById('tab-list');
// 先清空旧的列表,防止重复渲染
tabListUl.innerHTML = '';
if (!tabs || tabs.length === 0) {
tabListUl.innerHTML = '<li>没有找到标签页。</li>';
return;
}
tabs.forEach(tab => {
const listItem = document.createElement('li');
listItem.className = 'tab-item'; // 添加 class 方便统一样式
// 使用模板字符串构建列表项的 HTML 内容
// 我们会显示网站小图标、标题,并把 URL 放在一个看不见的地方备用
listItem.innerHTML = `
<img src="${tab.favIconUrl || 'images/default_icon.png'}" alt="favicon" class="favicon">
<span class="tab-title">${tab.title}</span>
<span class="tab-url" style="display:none;">${tab.url}</span>
`;
// 为每个列表项添加点击事件,点击后可以切换到该标签页
listItem.addEventListener('click', () => {
chrome.tabs.update(tab.id, { active: true });
// 如果想切换到标签页所在窗口并激活
// chrome.windows.update(tab.windowId, { focused: true });
});
tabListUl.appendChild(listItem);
});
}
// 函数:用于向后台请求标签页数据
function fetchAndRenderTabs() {
console.log("Popup: 正在向后台请求标签页列表...");
chrome.runtime.sendMessage({ action: "GET_TABS_FOR_CURRENT_WINDOW" }, (response) => {
if (chrome.runtime.lastError) {
console.error("Popup: 请求失败:", chrome.runtime.lastError.message);
renderTabList([]); // 显示空状态
} else if (response && response.status === "success") {
console.log("Popup: 成功获取到标签页列表:", response.tabs);
renderTabList(response.tabs);
} else {
console.error("Popup: 后台返回了未知响应:", response);
renderTabList([]);
}
});
}
// 当 Popup 打开时,立即执行一次获取和渲染
fetchAndRenderTabs();
// 你也可以把 fetchAndRenderTabs() 绑定到某个刷新按钮的点击事件上
const groupTabsBtn = document.getElementById('groupTabsBtn'); // 借用一下这个按钮
groupTabsBtn.addEventListener('click', () => {
// 原来的模拟交互可以注释或删掉了
// tabListUl.prepend(newListItem);
// 实际功能:重新获取并渲染列表
fetchAndRenderTabs();
// 我们会在后面章节给这个按钮加上真正的"分组"功能
});
// ... 其他按钮的逻辑 ...
});
哇哦!这段代码变复杂了。让我们把它拆成几块来消化:
-
renderTabList(tabs)
函数:- 这是一个专门负责"渲染"的函数。它的职责单一而明确:接收一个
tabs
数组,然后把它变成 HTML 列表。 tabListUl.innerHTML = '';
: 这是一个关键步骤。每次渲染前,我们先清空列表,防止用户多次点击刷新按钮导致列表内容无限叠加。- 我们遍历
tabs
数组,为每一个tab
对象创建一个<li>
元素。 listItem.innerHTML = ...
: 我们使用了模板字符串,这让拼接 HTML 变得非常方便。我们展示了favIconUrl
(如果不存在,则显示一个默认图标)、title
。- 交互增强 :我们为每个
<li>
添加了点击事件。当用户点击列表中的某一项时,我们调用chrome.tabs.update(tab.id, { active: true })
,这会立即将浏览器切换到那个被点击的标签页。这极大地提升了我们工具的实用性!
- 这是一个专门负责"渲染"的函数。它的职责单一而明确:接收一个
-
fetchAndRenderTabs()
函数:- 这个函数负责"获取数据"。它向后台发送我们约定好的
GET_TABS_FOR_CURRENT_WINDOW
消息。 - 在收到后台的回复后,它会检查状态,如果成功,就调用
renderTabList()
函数,把拿到的tabs
数组交给它去渲染。
- 这个函数负责"获取数据"。它向后台发送我们约定好的
-
执行时机:
- 我们在
DOMContentLoaded
事件的最后,直接调用了一次fetchAndRenderTabs()
。这意味着,只要用户一打开 Popup,它就会立即开始加载和显示标签页列表,提供了非常流畅的体验。 - 我还把这个函数绑定到了我们之前创建的"一键分组"按钮上,现在它临时扮演了"刷新列表"的角色。
- 我们在
第三步:添加一点 CSS 美化列表
我们的列表虽然出来了,但可能还不好看。打开 popup.html
,在 <style>
标签里,添加一些针对新元素的样式。
css
/* popup.html 内的 <style> */
/* ... 原有的样式 ... */
#tab-list {
list-style: none;
max-height: 320px; /* 增加一点最大高度 */
overflow-y: auto;
margin-top: 16px;
border: 1px solid #ddd;
border-radius: 4px;
}
.tab-item {
display: flex;
align-items: center;
padding: 8px 10px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.2s ease;
}
.tab-item:last-child {
border-bottom: none;
}
.tab-item:hover {
background-color: #f5f5f5;
}
.favicon {
width: 16px;
height: 16px;
margin-right: 10px;
flex-shrink: 0; /* 防止图标被压缩 */
}
.tab-title {
white-space: nowrap; /* 防止标题换行 */
overflow: hidden;
text-overflow: ellipsis; /* 标题过长时显示 ... */
flex-grow: 1;
font-size: 14px;
}
这些 CSS 样式会让我们的列表看起来更像一个专业的应用程序界面:对齐的图标、过长时会用省略号截断的标题、以及鼠标悬停时的反馈效果。
第四步:最终部署与验证
- 确保你已经保存了所有修改过的文件 (
background.js
,popup.html
)。 - 回到
chrome://extensions
页面,狠狠地点击刷新按钮。 - 打开几个不同的网页,让你的当前窗口里有 5-10 个标签页。
- 点击我们工具栏上的扩展图标!
见证奇迹的时刻,第三次!
你应该能看到:
- Popup 弹出的瞬间,列表区域可能短暂地显示空状态,但很快(取决于你有多少标签页),一个包含了当前窗口所有标签页的、带图标的、格式精美的列表就出现了!
- 列表中的每一项都显示了正确的网站小图标和页面标题。
- 将鼠标悬停在某一项上,它会有背景色变化。
- 点击列表中的任意一项,浏览器会立刻切换到对应的标签页。
- 点击顶部的"一键分组"按钮(我们现在的刷新按钮),列表会重新加载。
我们成功了!我们已经实现了"智能标签页管家"的第一个,也是最核心的功能! 我们的扩展不再是一个玩具,它已经变成了一个有用的工具。
6.3 进阶功能预告:一键分组(group
)
我们已经用 query
和 update
实现了核心功能。现在,让我们来为下一章的"明星功能"------一键分组,做好铺垫。
目标:当用户点击那个绿色的"一键分组当前窗口的标签页"按钮时,我们不再是刷新列表,而是真正地将当前窗口的所有标签页(除了它自己)按域名进行分组。
这个逻辑会稍微复杂一些,我们将它放在下一章详细实现,但现在我们可以先在 background.js
中构思一下这个功能的伪代码,让你对即将到来的挑战有一个初步的认识。
javascript
// background.js 中未来会添加的逻辑
// onMessage ...
else if (message.action === "GROUP_TABS_BY_DOMAIN") {
// 1. 查询当前窗口的所有标签页
chrome.tabs.query({ currentWindow: true }, (tabs) => {
const tabsByDomain = {}; // 创建一个对象来按域名分类
// 2. 遍历所有标签页,按域名归类
for (const tab of tabs) {
const domain = new URL(tab.url).hostname;
if (!tabsByDomain[domain]) {
tabsByDomain[domain] = [];
}
tabsByDomain[domain].push(tab.id);
}
// 3. 遍历分类好的对象,为每个域名下的标签页创建分组
for (const domain in tabsByDomain) {
const tabIds = tabsByDomain[domain];
if (tabIds.length > 1) { // 只对超过1个标签页的域名进行分组
chrome.tabs.group({ tabIds: tabIds }, (groupId) => {
// 4. (可选) 给新创建的分组命名
chrome.tabGroups.update(groupId, { title: domain });
});
}
}
});
return true;
}
这个逻辑清晰地展示了如何组合使用 query
, group
, 和 tabGroups.update
来实现一个非常强大的自动化功能。
本章总结与展望
在这一章,我们从理论到实践,全面征服了 chrome.tabs
API:
- 精通了核心方法 :我们详细学习了
query
,create
,update
,group
等关键 API 的用法和参数。 - 构建了核心功能:我们遵循"关注点分离"的原则,通过 Popup 和 Background 的消息通信,成功实现了查询并动态渲染当前窗口所有标签页列表的功能。
- 增强了用户体验:我们不仅展示了列表,还通过 CSS 美化和添加点击切换功能,让我们的工具变得真正好用。
我们的"智能标签页管家"已经从一个概念,变成了一个可以日常使用的、有价值的工具。你现在已经拥有了管理"标签页地狱"的初步能力。
在下一章,我们将要学习如何让数据**"留下来"。目前,我们的扩展是无记忆的,一关一开,什么都不记得。我们将深入学习 chrome.storage
API,为我们的扩展添加持久化数据存储**的能力。我们将实现:
- 让用户可以自定义设置,比如设置一个不希望被分组的网站"白名单"。
- 将这些设置安全地保存下来,即使用户关闭了浏览器再打开,设置依然有效。