chrome.tabs 实战:标签页查询、创建与分组

标签页是扩展最容易感知的浏览器对象:查询、创建、移动、静音、分组,全在 chrome.tabs 一个命名空间里。而且它有个反直觉的好消息——多数方法不需要任何权限。这一篇把高频操作和权限边界一次讲全。

权限的真相

先纠正一个广泛流传的误解:使用 chrome.tabs API 大部分功能不需要权限。tabs.create、tabs.update、tabs.move、tabs.reload 这些随便调。需要权限的只有两类事:

  • tabs 权限:解锁读取 Tab 对象的四个敏感属性 url、pendingUrl、title、favIconUrl;
  • host_permissions 或 activeTab:对特定站点做同样的事,并允许注入脚本、截屏(captureVisibleTab)。

所以先想清楚:只是开关标签页,零权限;要按 URL 过滤标签页,就需要上面的授权之一。

查询:query 的条件语法

// 当前窗口的活动标签页
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

// 所有窗口的活动标签页(多窗口场景)
const actives = await chrome.tabs.query({ active: true, lastFocusedWindow: true });

// 按 URL 模式找(需要 tabs 或 host 权限才能读到 url)
const baike = await chrome.tabs.query({ url: "*://*.baidu.com/*" });

query 条件是 AND 组合:active、currentWindow、url、pinned、audible、muted 等字段同时生效。返回数组,解构取第一个时注意空数组的可能。注意 currentWindow 与 lastFocusedWindow 的差别:前者是「调用者所在窗口」,后者是「最近聚焦的窗口」,popup 里通常用 currentWindow,后台里通常用 lastFocusedWindow。

创建与更新

// 打开扩展自己的引导页(onInstalled 里做,无需任何权限)
chrome.runtime.onInstalled.addListener(({ reason }) => {
  if (reason === "install") {
    chrome.tabs.create({ url: "onboarding.html" });
  }
});

// 更新当前页到新地址
await chrome.tabs.update(tab.id, { url: "https://example.com/new" });

// 静音某个标签页
const t = await chrome.tabs.get(tabId);
await chrome.tabs.update(tabId, { muted: !t.mutedInfo.muted });

update 能改 url、muted、pinned、active 等。move 可把标签页挪位置,挪动时若用户正在拖拽标签,Chrome 会抛错「Tabs cannot be edited right now」,官方示例的处理是捕获后延迟 50 毫秒重试。

分组:tabs.group 与 TabGroups API

标签页分组(Chrome 88+)的入口是 tabs.group / tabs.ungroup,视觉控制靠 chrome.tabGroups:

// 把两个标签页组成一个组
const groupId = await chrome.tabs.group({
  tabIds: [tabA.id, tabB.id],
  createProperties: { windowId: tabA.windowId }
});

// 设置组的标题和颜色(需要 "tabGroups" 权限)
await chrome.tabGroups.update(groupId, { title: "工作区", color: "blue" });

// 监听组变化
chrome.tabGroups.onUpdated.addListener((group) => {
  console.log("组更新", group.id, group.title);
});

颜色是固定枚举:grey、blue、red、yellow、green、pink、purple、cyan、orange。做「一键整理」类扩展时,常用 query 找出同类标签再 group,这正是第五章实战项目「标签页管理器」的核心套路。

事件:让扩展对标签页活动做出反应

事件 触发时机
tabs.onCreated 新标签页创建
tabs.onUpdated 标签页 URL、状态、标题等变化
tabs.onRemoved 标签页关闭
tabs.onActivated 切换活动标签页

onUpdated 事件会在一个页面加载过程中触发多次(loading、title 变化、favicon 就绪……),处理时按 changeInfo 字段过滤,别把每个回调都当「页面加载完成」。

实战提醒

按 URL 过滤时模式串写错会静默返回空数组,排查方法是把条件放宽成 {} 逐步加严。URL 模式与正则的关系见 正则实战。标签页数据要持久化的话,结合 《chrome.storage 实战》 篇的方案落库。官方文档:https://developer.chrome.com/docs/extensions/reference/api/tabs

一个标签页整理器的雏形

把本篇 API 组合成一个实用函数:找出当前窗口所有已静音或长期不活跃的标签页并折叠成组——「一键收纳」类扩展的核心逻辑:

async function tidyUp() {
  const tabs = await chrome.tabs.query({ currentWindow: true });
  const byHost = new Map();
  for (const t of tabs) {
    if (!t.url) continue; // 无 tabs 权限时 url 不可读,需授权
    const host = new URL(t.url).hostname;
    if (!byHost.has(host)) byHost.set(host, []);
    byHost.get(host).push(t.id);
  }
  const colors = ["blue", "green", "yellow", "pink", "purple", "orange"];
  let i = 0;
  for (const [host, ids] of byHost) {
    if (ids.length < 2) continue; // 单页不成组
    const gid = await chrome.tabs.group({
      tabIds: ids,
      createProperties: { windowId: tabs[0].windowId }
    });
    await chrome.tabGroups.update(gid, { title: host, color: colors[i++ % colors.length] });
  }
}

注意其中 if (!t.url) continue:没有 tabs 权限或 host 权限时,Tab 对象的 url 是 undefined,按域名分组的功能直接失效。功能设计与权限申请是绑定的,这是「权限模型」篇反复强调的主题在实操中的样子。

事件处理的两个防坑

事件驱动的标签页功能有两个经典坑。其一,onUpdated 高频触发:一个页面从加载到完成可能回调五六次,处理函数里先检查 changeInfo.status === "complete" 或按需过滤字段,否则同一段逻辑会被执行多次。其二,onRemoved 的时机:tabId 已失效,回调里再用它去 get 会报错;需要「关闭前保存内容」的逻辑要在关闭前完成(监听 onUpdated 缓存状态),而不是指望在 onRemoved 里抢救。

多窗口场景别忘了窗口维度:query 加 windowId 或 currentWindow,onActivated 回调里区分 windowId,跨窗口的「活动标签」是每个窗口各一个。做「全局唯一活动标签」假设的代码在双显示器用户那里必然出错。

结合存储做会话恢复

标签页管理器的高级形态是「保存会话、随时恢复」:把 query 结果的 url 列表写进 storage.sync(数据小,跨设备恢复),恢复时循环 tabs.create。完整代码在第五章实战项目 《标签页管理器》 一篇。URL 模式匹配与 host 解析的正则技巧见 正则实战。官方文档:https://developer.chrome.com/docs/extensions/reference/api/tabs

本章由 WorkBuddy 原创整理,API 行为依据 Chrome 扩展官方文档(Manifest V3)。