Chrome扩展程序是轻量级的工具,它们能够提升并个性化你的浏览体验——无论是帮助你管理密码、翻译网页,还是为日常使用的网站添加新的功能。

已有数百万开发者将他们的扩展程序发布到了Chrome应用商店中,而开发这样的扩展程序其实比你想象的要简单得多。

通过这本手册,你将学习如何使用TypeScript、React以及Plasmo这个现代框架从零开始制作一个可发布的Chrome扩展程序。Plasmo会处理那些重复性的设置和配置工作,这样你就可以专注于编写核心功能代码,而无需花费时间处理繁琐的底层细节。

在学习过程中,你还将接触到那些真正用于构建扩展程序的API:如何查询标签页、创建标签页组,以及如何在扩展程序的不同组件之间传递信息。

学习结束后,你将拥有可运行的代码,也会对扩展程序的结构有清晰的了解,并且掌握将自己的创意发布到Chrome应用商店所需的一切技能。

目录

什么是Plasmo?

Plasmo是一个用于开发浏览器扩展程序的开源框架。你可以把它看作是专门为Chrome扩展程序设计的“Create React App”或“Next.js”。

如果没有Plasmo,开发Chrome扩展程序就需要手动编写`manifest.json`文件、配置构建工具,并自行设置TypeScript和React的相关参数。而Plasmo可以完成所有这些工作。

只需一条命令,就能创建一个已经配置好了TypeScript和React的可用项目。Plasmo会自动读取你的`package.json`文件,并生成Chrome所需的`manifest.json`文件,因此你根本不需要直接编辑它。

此外,在开发过程中,对源代码所做的任何修改都会自动触发扩展程序的重新构建,并在Chrome浏览器中立即生效。同时,Plasmo还提供了类型安全功能,包括针对Chrome自身API的类型检测。

Plasmo并不会隐藏Chrome扩展程序开发的相关概念——你仍然需要直接使用`chrometabs`、`chrome.runtime`等Chrome API。但它省去了那些繁琐的开发准备工作,让你能够立即开始编写代码。

你将构建什么

在这个教程中,你将从零开始制作一个标签页分组器Chrome扩展程序。

这个扩展程序会根据网页的域名自动对浏览器中的标签页进行分类整理。

标签页分组器的动画演示:根据域名将打开的标签页分成不同颜色的组

使用案例示例

假设你打开了20个标签页:其中5个来自GitHub,4个来自YouTube,3个来自Stack Overflow,另外8个来自其他网站。

只需点击一下,标签页分组器就会自动为每个网站创建对应的颜色组,这样你就能轻松地找到并管理这些标签页了。

你将学到什么

完成这个教程后,你将在三个方面获得实践经验。

首先,是Chrome扩展程序基础:了解扩展程序在底层是如何工作的,扩展程序的构成要素(manifest文件、后台脚本、弹出窗口等),以及如何在开发过程中在Chrome中加载和测试扩展程序。

其次,是Chrome API:具体来说包括用于管理浏览器标签页的`chrome.tabs`,用于创建和自定义标签页组的`chrome.tabGroups`,以及用于在扩展程序的不同部分之间传递信息的`chrome.runtime`。

第三,是现代Web开发工具:使用TypeScript编写类型安全的JavaScript代码,利用React构建弹出窗口界面,同时借助Plasmo框架将所有这些技术整合在一起。

先决条件

你不需要在这些领域成为专家,但如果你对基本的JavaScript或TypeScript有一定了解,并且对HTML和CSS也有一定的认识,那么你的学习过程会更加顺利。

虽然熟悉React会有帮助,但并不是必需的。我们将会制作的弹出窗口组件非常简单,即使你是新手也能轻松掌握。

在软件方面,你需要安装Node.js 18版本或更高版本(点击此处下载)、Google Chrome浏览器、一个代码编辑器(推荐使用VS Code),以及pnpm作为包管理工具。

验证你的环境配置

打开终端,运行以下命令来确认所有软件都已经安装完毕:

node --version
# 应该输出v18.0.0或更高版本

npm --version
# 应该输出9.0.0或更高版本

寻求帮助

如果遇到困难,可以查看代码库中的完整代码,查阅Chrome扩展程序的官方文档,或者在社区论坛中寻求帮助。

准备开始了吗?

在下一节中,你将设置开发环境,并创建你的第一个Chrome扩展程序项目。

让我们开始吧!

项目设置

在本节中,您将使用 Plasmo 来搭建您的 Chrome 扩展程序项目框架,然后根据 Tab Grouper 的需求对其进行定制。

无需手动创建文件,Plasmo 会自动生成包含所有必要配置的起始项目模板,您可以先查看这些内容,再根据实际需要对其进行修改。

Plasmo 官方推荐使用 pnpm,因为它能够加快安装速度并更高效地利用磁盘空间。请检查您是否已经安装了 pnpm:

pnpm --version

如果看到了版本号,可以直接跳到步骤 2。

运行 pnpm --version 后终端显示的 pnpm 版本号

如果看到“命令未找到”的提示,请使用以下命令进行安装:

npm install -g pnpm

步骤 2:创建您的扩展程序项目

运行以下命令来创建一个新的 Plasmo 项目:

pnpm create plasmo tab-grouper

您会看到如下提示:

🟣 正在创建新的 Plasmo 扩展程序
📁 项目名称:tab-grouper
? 扩展程序描述:(请为您的扩展程序填写一个合适的描述)
? 作者姓名:(请输入您的名字)

Plasmo 会自动完成项目的框架搭建并安装依赖项。系统可能会要求您输入描述和作者姓名,您可以根据自己的喜好进行填写。

终端显示 Plasmo 正在创建名为 tab-grouper 的新项目并安装依赖项

步骤 3:进入您的项目目录

cd tab-grouper

步骤 4:查看已生成的内容

列出 Plasmo 生成的文件列表:

ls -la

您应该会看到类似以下的输出:

tab-grouper/
├── .git/                 # Git 仓库(已初始化)
├ ├── .github/              # GitHub Actions 工作流程相关文件
├ ├── assets/
│   └── icon.png          # Plasmo 的默认图标文件
├ ├── node_modules/         # 依赖项(已安装完成)
├ ├── package.json          # 项目配置文件
├ ├── popup.tsx             # 默认的弹出窗口组件文件
├ ├── .prettierrc.cjs       # 代码格式化规则文件
├ ├── .gitignore            # Git 忽略规则文件
├ ├── README.md             | 默认的阅读说明文件
└── tsconfig.json         | TypeScript 配置文件

需要了解的关键文件包括:

  • assets/icon.png:Chrome浏览器所需的扩展程序图标文件。

  • package.json:列出了扩展程序所依赖的库和脚本,同时也是配置扩展程序元数据的地方。

  • popup.tsx:点击扩展程序图标时出现的用户界面代码。

  • tsconfig.json:其中包含了已经正确配置好的TypeScript相关设置。

步骤5:测试默认扩展程序

在对其进行自定义修改之前,请确保所有功能都能正常运行。

你可以通过启动开发服务器来验证这一点:

pnpm dev

你应该会看到如下输出:

🟣 Plasmo v0.90.5
🔴 浏览器扩展程序框架
🔵 INFO   | 正在启动扩展程序开发服务器...
🔵 INFO   | 正在为目标环境“chrome-mv3”构建代码...
🔵 INFO   | 从以下路径加载了环境变量:[]
🟢 DONE   | 扩展程序重新打包完成,用时1842毫秒!🚀

查看扩展程序文件:
📦 build/chrome-mv3-dev

你的扩展程序现在已经准备就绪。请保持这个终端窗口处于打开状态。

Plasmo会自动检测文件的变化并重新构建代码。

步骤6:在Chrome中加载扩展程序

现在,请将扩展程序加载到Chrome浏览器中进行测试:

  1. 打开Google Chrome浏览器

  2. 进入chrome://extensions/页面

  3. 启用开发者模式(位于右上角)

  4. 点击“加载解压后的文件”

  5. 导航到你的项目文件夹

  6. 选择build/chrome-mv3-dev文件夹

  7. 点击“选择文件夹”按钮

动画演示如何通过Chrome浏览器的开发者模式加载解压后的扩展程序文件

现在你的扩展程序应该已经出现在列表中了。

步骤7:测试默认弹出窗口

  1. 点击Chrome浏览器工具栏中的拼图图标

  2. 找到“tab-grouper”选项并将其固定到工具栏上

  3. 再次点击扩展程序图标

你会看到一个显示“欢迎使用Plasmo!”信息的默认弹出窗口。

Chrome浏览器工具栏中显示的Plasmo默认弹出窗口

扩展程序已经可以正常使用了,现在你可以对其进行自定义修改了。

步骤8:更新扩展程序信息

在编辑器中打开package.json文件。这个文件保存了关于你的扩展程序的元数据,包括名称、版本号、描述、依赖库以及用于构建和运行扩展程序的脚本等信息。

在文件顶部附近找到以下这几行代码:

{
  "name": "tab-grouper",
  "displayName": "tab-grouper",
  "version": "0.0.0",
  "description": "一个基本的Plasmo扩展程序。",

将它们修改为:

{
  "name": "tab-grouper",
  "displayName": "Tab Grouper",
  "version": "1.0.0",
  "description": "一个简单的Chrome扩展程序——可按域名对标签页进行分组。",

保存文件。

步骤9:添加所需的权限(非常重要!)

这是一个至关重要的步骤。如果没有这些权限,你的扩展程序将会出现错误,例如:

TypeError: 无法读取未定义对象的属性(在尝试访问‘query’时出现此错误)

Chrome扩展程序必须明确说明自己打算使用哪些浏览器API。在package.json文件中,找到"manifest"部分。

它的格式如下:

"manifest": {
  "host_permissions": [
    "https://*/*"
  ]
}

将其修改为:

"manifest": {
  "permissions": [
    "tabs",
    "tabGroups"
  ]
}

保存文件。tabs权限允许你读取标签页的相关信息(这是使用chrome.tabs.query()函数所必需的),而tabGroups权限则使你能够创建和管理标签组(使用chrome.tabGroups.update()函数时需要这些权限)。

如何为自己的扩展程序选择合适的权限:

Chrome扩展程序权限参考列出了所有可用的权限以及它们各自能实现的功能。

每个API的文档页面也会说明它需要哪些权限。例如,chrometabs API的文档明确指出了需要"tabs"权限。

如果你正在使用Plasmo框架,Manifest配置文档会说明如何通过package.json文件来添加权限。

一般来说:如果在使用Chrome API时遇到了undefined类型的错误,首先应该检查是否缺少某些权限。

步骤10:验证热重载功能是否正常

当你保存对扩展程序的修改后,Plasmo会自动重新加载该扩展程序。

查看运行着pnpm dev命令的终端窗口。在保存了package.json文件之后,你应该会看到类似以下的提示信息:

🔄 正在重新加载扩展程序...
✅ 0.8秒后即可完成重载

现在你的项目已经准备就绪:Chrome浏览器中已加载了可正常使用的扩展程序,开发服务器也启用了热重载功能,而且所有必要的权限也都已经配置好了。

在继续后续步骤时,保持开发服务器处于运行状态,并让扩展程序保持加载状态。这样你的修改就会自动得到应用。

章节总结

在本节中,您安装了pnpm,使用pnpm create plasmo创建了一个新的扩展程序,了解了生成的项目结构,启动了开发服务器,在Chrome中加载了这个扩展程序,并更新了其元数据和权限设置。

下一步:您需要编写用于处理标签页分组逻辑的背景脚本。

了解背景脚本

背景脚本是您的扩展程序的核心部分。它在后台持续运行,其中包含了实现核心功能的代码。

在这个例子中,这段代码负责根据域名对标签页进行分组。

什么是背景脚本?

即使弹出窗口被关闭,背景脚本也会继续运行。

它可以监听诸如标签页打开、关闭或更新等浏览器事件,执行那些不需要用户直接干预的任务,并通过传递消息与其他扩展程序组件进行通信。

可以把背景脚本看作是您的扩展程序的“后端部分”,而弹出窗口则只是与之交互的用户界面。

步骤1:创建background.ts文件

Plasmo的模板生成机制默认不会创建背景脚本,因此您需要手动创建这个文件。在项目根目录下创建一个名为background.ts的新文件(该文件的层级应与popup.tsx相同):

export {}

// 背景脚本——在后台运行,负责处理标签页分组逻辑

console.log("标签页分组器背景脚本已加载!");

// 监听来自弹出窗口的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === "GROUP_TABS") {
    groupTabsByDomain()
    sendResponse({ success: true })
  }
  return true
})

文件开头的export {}是Plasmo要求必须添加的,这样才能将这个文件视为一个模块。如果没有这行代码,可能会出现全局变量声明冲突的错误。

console.log这条语句可以帮助您确认脚本是否已正确加载(您可以在扩展程序的DevTools控制台中看到这条日志)。chrome.runtime.onMessage则用于设置监听器,使背景脚本能够接收来自弹出窗口的指令。

当背景脚本收到"GROUP_TABS"类型的消息时,就会调用groupTabsByDomain函数来执行分组操作。

关于这种消息传递机制的更多信息,您可以参考Chrome扩展程序文档

步骤2:实现标签页分组逻辑

现在,在消息监听器下方添加主要的分组逻辑代码:

async function groupTabsByDomain() {
  try {
    // 第一步:获取当前窗口中的所有标签页
    const tabs = await chrome.tabs.query({ currentWindow: true })

    // 第二步:使用Map按域名对标签页进行分类
    const domainGroups = new Map()

    // 第三步:遍历每个标签页并按域名将其分组
    tabs.forEach(tab => {
      // 跳过没有URL的标签页
      if (!tab.url) return

      // 从URL中提取域名
      const domain = getDomainFromUrl(tab.url)

      // 跳过无效的域名(例如chrome:// pages)
      if (!domain) return

      // 将标签页添加到对应的域名组中
      if (!domainGroups.has(domain)) {
        domainGroups.set(domain, [])
      }
      domainGroups.get(domain)!.push(tab)
    })

    // 第四步:为每个域名创建标签页组(只有当该域名下有2个以上的标签页时才执行此步骤)
    for (const [domain, domainTabs] of domainGroups) {
      // 跳过只有一个标签页的域名
      if (domain Tabs.length < 2) continue

      // 获取所有标签页的ID
      const tabIds = domainTabs
        .map(t => t.id!)
        .filter(id => id !== undefined)

      if (tabIds.length === 0) continue

      // 创建标签页组
      const groupId = await chrome.tabs.group({ tabIds })

      // 为标签页组设置标题和颜色
      await chrome.tabGroups.updategroupId, {
        title: domain,
        color: getColorForDomain(domain) // 为标签页组随机分配颜色
      })
    }

    console.log(`成功地为${domainGroups.size}个域名分组完成了标签页`)
  } catch (error) {
    console.error("在分组标签页时出现错误:", error)
  }
}

该函数首先会查询当前窗口中的所有标签页,然后遍历这些标签页,构建一个以域名作为键的Map

当所有标签页都被分到相应的域名组中后,程序会遍历这个映射表,对于那些拥有两个或更多标签页的域名,就会调用chrome.tabs.group()函数来将这些标签页组合成一个组,随后立即为这个组设置标题和颜色。

只有单个标签页的域名会被忽略——因为对这样的域名进行分组并没有意义。

步骤3:提取域名辅助功能

添加一个辅助函数,用于从URL中提取主机名:

function getDomainFromUrl(url: string): string | null {
  try {
    const urlObj = new URL(url)

    // 跳过Chrome的内部页面(如chrome://、chrome-extension://)
    if (urlObj.protocol === "chrome:" || urlObj.protocol === "chrome-extension:") {
      return null
    }

    // 去掉“www.”前缀,返回主机名
    return urlObj.hostname.replace(/^www\./, "")
  } catch {
    // 如果URL无效,返回null
    return null
  }
}

new URL(url)能为我们提供一个结构化的对象,从而无需手动解析URL字符串。

通过检查协议地址,可以过滤掉Chrome的内部页面,比如chrome://extensionschrome://settings——这些页面是扩展程序无法访问的。

.replace/^www\./, "")这一操作确保了www.github.comgithub.com会被视为同一个域名,而不会被分到不同的组中。

整个过程都被包裹在try-catch语句中,这样遇到格式错误的URL时,程序就会直接返回null并忽略它。

在实际应用中:https://www.github.com/user/repo会被处理成github.comhttps://youtube.com/watch?v=123会被处理成youtube.com,而chrome://extensions则会返回null

步骤4:颜色分配辅助功能

添加一个函数,用于为每个域名确定性地分配一种颜色:

function getColorForDomain(domain: string): chrome.tabGroups.ColorEnum {
  // Chrome支持的颜色种类
  const colors: chrome.tabGroups.ColorEnum[] = [
    "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"
  ]

  // 根据域名生成一个哈希值
  let hash = 0
  for (let i = 0; i < domain.length; i++) {
    hash = domain.charCodeAt(i) + ((hash << 5) - hash)
  }

  // 根据哈希值选择对应的颜色
  return colors[Math.abs(hash) % colors.length]
}

Chrome支持为标签组分配八种不同的颜色。这个函数并不会随机选择颜色,而是将域名转换成一个数字,然后利用取模运算来从颜色数组中选取一个固定的索引,从而确保相同的域名总是会被分配到相同的颜色组中。

这样一来,github.com在不同会话中始终会使用相同的颜色,而不同的域名则很可能会被分配到不同的颜色组中。

完整的background.ts文件

你的完整background.ts文件应该如下所示:

export {}

console.log("标签页分组器后台脚本已加载!")

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === "GROUP_TABS") {
    groupTabsByDomain()
    sendResponse({ success: true })
  }
  return true
})

async function group TabsByDomain() {
  try {
    const tabs = await chrome.tabs.query({ currentWindow: true })
    const domainGroups = new Map()

    tabs.forEach(tab => {
      if (!tab.url) return
      const domain = getDomainFromUrl(tab.url)
      if (!domain) return

      if (!domainGroups.has(domain)) {
        domainGroups.set(domain, [])
      }
      domainGroups.get(domain)!.push(tab)
    })

    for (const [domain, domainTabs] of domainGroups) {
      if (domainTabs.length < 2) continue

      const tabIds = domainTabs
        .map(t => t.id!)
        .filter(id => id !== undefined)

      if (tabIds.length === 0) continue

      const groupId = await chrome.tabs.group({ tabIds })

      await chrome.tabGroups.updategroupId, {
        title: domain,
        color: getColorForDomain(domain)
      })
    }

    console.log(`成功将${domainGroups.size}个域名进行了分组`)
  } catch (error) {
    console.error("在分组标签页时出现错误:", error)
  }
}

function getDomainFromUrl(url: string): string | null {
  try {
    const urlObj = new URL(url)
    if (urlObj.protocol === "chrome:" || urlObj.protocol === "chrome-extension:") {
      return null
    }
    return urlObj.hostname.replace(/^www\./, "")
  } catch {
    return null
  }
}

function getColorForDomain(domain: string): chrome.tabGroups.ColorEnum {
  const colors: chrome.tabGroups.ColorEnum[] = [
    "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"
  ]

  let hash = 0
  for (let i = 0; i < domain.length; i++) {
    hash = domain.charCodeAt(i) + ((hash << 3) - hash)
  }

  return colors[Math.abs(hash) % colors.length]
}

测试后台脚本

如果你的开发服务器还没有在前一节中启动,请现在运行它:

pnpm dev

为了验证后台脚本是否已正确加载,请打开chrome://extensions,找到“Tab Grouper Tutorial”,然后点击"service worker"链接。

此时会打开DevTools控制台,你会看到“标签页分组器后台脚本已加载!”的提示信息,这说明一切设置都已经完成。

构建弹出窗口界面

弹出窗口是指当用户在Chrome工具栏中点击你的扩展程序图标时出现的那个小窗口。

这个窗口可以用来显示信息、提供操作按钮,以及展示设置选项。

在这一节中,你将构建一个基于React的弹出窗口,该窗口能够实时显示标签页的相关统计信息,并触发后台脚本中的分组逻辑。

步骤1:替换popup.tsx文件

当你运行pnpm create plasmo时,系统会生成一个默认的popup.tsx文件,该文件仅用于显示欢迎信息。

打开这个文件,将其所有内容替换为以下代码框架:

import { useState, useEffect } from "react"

function IndexPopup() {
  const [tabCount, setTabCount] = useState(0)
  const [groupCount, setGroupCount] = useState(0)
  const [isGrouping, setIsGrouping] = useState(false)

  return (
    

标签页分组器

) } export default IndexPopup

保存文件后,页面会自动重新加载。

这三个状态变量分别用于记录当前打开的标签页数量、现有的分组数量,以及是否正在执行分组操作。

通过设置这个状态变量,我们可以禁用“分组标签页”按钮,并在分组操作进行时显示加载提示,从而防止用户同时触发多次分组操作。

步骤2:加载统计信息

现在需要在弹出窗口打开时加载标签页数量和分组数量的统计信息。将以下代码添加到IndexPopup函数中,放在状态变量声明之后:

// 在弹出窗口打开时加载统计信息
useEffect(() => {
  loadStats()
}, []

async function loadStats() {
  const tabs = await chrome.tabs.query({ currentWindow: true })
  const groups = await chrome.tabGroups.query({
    windowId: chrome.windows.WINDOW_ID_CURRENT
  })

  setTabCount(tabs.length)
  setGroupCount(groups.length)
}

带有空依赖数组[]useEffect会在组件首次挂载时执行一次,也就是说,每次弹出窗口打开时都会调用这个函数。

该函数会通过Chrome API查询当前窗口的标签页和分组信息,然后更新相应的状态变量。

步骤3:触发标签页分组操作

需要添加一个处理程序,在用户点击“分组标签页”按钮时向后台脚本发送消息:

async function handleGroupTabs() {
  setIsGrouping(true)

  // 向后台脚本发送消息
  await chrome.runtime.sendMessage({ type: "GROUP_TABS" })

  // 更新统计信息
  await loadStats()
  setIsGrouping(false)
}

chrome.runtime sendMessage会将类型为"GROUP TABS"的消息发送到我们在background.ts中设置的监听器。

后台脚本处理完消息后,我们会立即重新加载统计信息,以便分组数量能够及时更新,然后再重新启用“分组标签页”按钮。

步骤4:构建用户界面

将原来的占位符return语句替换为这个完整且带有样式的代码版本:

return (


{/* 页眉 */}

🗂️ 标签分组器

按域名来整理您的标签页

{/* 统计信息 */}

{tabCount}
打开的标签页

{groupCount}
标签分组

{/* 分组按钮 */}

{/* 页脚 */}


💡 提示: 此功能会根据网页的域名来对所有标签页进行分组。

)

用户界面由四个部分组成:顶部显示扩展程序的名称及简短描述,中间是一个统计信息框,其中并排显示当前选中的标签页数量和分组数量,主要的操作按钮在处理数据时会变为灰色,其文字也会变为“分组中…”,底部还有一个提示框。

本教程为简化说明使用了内联样式。在实际的开发环境中,你可能会选择使用CSS模块、Tailwind或styled-components等工具来设计界面。

完整的popup.tsx文件

你的完整popup.tsx文件应该如下所示:

import { useState, useEffect } from "react"

function IndexPopup() {
  const [tabCount, setTabCount] = useState(0)
  const [groupCount, setGroupCount] = useState(0)
  const [isGrouping, setIsGrouping] = useState(false)

  useEffect(() => {
    loadStats()
  }, [])

  async function loadStats() {
    const tabs = await chrome.tabs.query({ currentWindow: true })
    const groups = await chrome.tabGroups.query({
      windowId: chrome.windows.WINDOW_ID_CURRENT
    })

    setTabCount(tabs.length)
    setGroupCount(groups.length)
  }

  async function handleGroupTabs() {
    setIsGrouping(true)
    await chrome.runtime.sendMessage({ type: "GROUP_TABS" })
    await loadStats()
    setIsGrouping(false)
  }

  return (
    

标签页分组
) } export default IndexPopup

测试你的扩展程序

现在你已经完成了后台脚本和弹出界面的开发,是时候验证这些组件在Chrome浏览器中能否正常协同工作了。

步骤1:确保开发服务器正在运行

如果之前没有通过命令 `pnpm dev` 启动开发服务器,请现在执行该操作:

pnpm run dev # 或 pnpm dev

Plasmo会将扩展程序编译到 `build/chrome-mv3-dev` 目录中,并持续监控该目录中的变化。

步骤2:在Chrome中加载扩展程序

如果你还没有加载这个扩展程序,请打开 `chrome://extensions/`,启用开发者模式,然后点击“加载解压后的文件”选项,并选择 `build/chrome-mv3-dev` 文件夹。

加载完成后,你应该会看到名为“Tab Grouper Tutorial”、版本号为“1.0.0”的扩展程序被列在列表中,其状态也会显示为“已启用”。

步骤3:将扩展程序固定到工具栏上

点击Chrome工具栏上的图标,找到“Tab Grouper Tutorial”,然后点击固定按钮,这样该扩展程序就会一直显示在工具栏上了。

此时,这个扩展程序的图标将会直接出现在你的工具栏中。

步骤4:测试扩展程序

测试1:打开多个标签页

打开属于不同域名的几个标签页,这样就有内容可以进行分组了:

  1. https://github.com/topics, https://github.com/trending, https://github.com/explore

  2. https://www.youtube.com/https://www.youtube.com/trending

  3. https://stackoverflow.com/questionshttps://stackoverflow.com/tags

至少要打开7个标签页。

测试2:对标签页进行分组

点击“Tab Grouper”扩展程序的图标,弹出的窗口会显示你当前打开的标签页数量(至少为7个)以及分组数量(通常为0个)。

点击“按域名对标签页进行分组”,你会看到所有的标签页被分成了不同颜色的组。

测试3:验证分组结果

点击按钮后,GitHub相关的标签页会被归为一组,并显示“github.com”这样的标签以及统一的颜色;YouTube相关的标签页也会以相同的方式被分组。

再次点击扩展程序的图标,此时分组数量应该会显示为2,而标签页的数量保持不变。

步骤5:调试扩展程序

如果遇到任何问题,Chrome的开发者工具会是你的得力帮手。

要查看后台脚本的运行情况,请打开 `chrome://extensions/`,找到你的扩展程序,然后点击“服务工作者”链接。

开发者工具的控制台会显示相关信息,其中“Tab Grouper背景脚本已加载!”这样的消息以及任何错误信息都会以红色显示出来。

要检查弹出窗口,右键点击扩展程序图标,然后选择“检查弹出窗口”。这样就会专门为该弹出窗口打开DevTools工具——可以在控制台标签页中查看是否有任何错误。

如果点击按钮后没有任何反应,请检查后台脚本控制台中的错误信息,确认至少有两个来自同一域的标签页,并验证消息是否真的被发送了(可以在弹出窗口的控制台中查找是否有sendMessage操作失败的情况)。

,请再次检查是否在package.json文件中添加了tabstabGroups权限,并在保存后重新加载扩展程序。

,这是正常现象——扩展程序确实不能与Chrome的内部页面进行交互,因此代码中会故意跳过这些部分。

步骤6:热重载

Plasmo的一个显著优点就是支持热重载功能,这意味着你可以在不手动重启应用程序的情况下,立即更新正在运行的程序中的代码。

打开popup.tsx文件,将标题处的表情符号从🗂️改为📁,然后保存文件。

扩展程序会自动重新加载。

点击图标后,你会立即看到更新后的表情符号。

热重载功能非常实用,因为它能让你实时看到代码修改的效果,从而大大加快开发速度。

如果你以后想让扩展程序与其他教程示例及截图保持一致,也可以随时将表情符号改回原来的样子。

步骤7:测试边缘情况

为了确保扩展程序能够正确处理各种特殊情况,有必要进行一些测试。

如果你关闭了除一个标签页之外的所有标签页,然后点击“分组标签页”,应该不会发生任何变化。因为扩展程序要求至少有两个来自同一域的标签页才能形成一组。另外,打开chrome://extensionschrome://settings后尝试进行分组操作,也应该不会有结果,因为这些页面会被系统自动过滤掉。

如果你有一个来自reddit.com的标签页和一个来自freecodecamp.org的标签页,并且每个域只出现一次,那么就不会有任何组被创建出来。

步骤8:生成生产版本

当你准备分享自己的扩展程序时,可以运行以下命令:

pnpm run build

这样就会在build/chrome-mv3-prod目录下生成一个经过优化、压缩过的生产版本文件。这个版本的JavaScript代码已经被最小化,且不包含仅用于开发环境的代码,因此文件大小也会更小。

要验证这个生产版本是否正常工作,请进入chrome://extensions/页面,删除开发版本,然后点击“加载解压包”,并选择build/chrome-mv3-prod。在正式发布之前,请务必进行彻底的测试。

这个扩展程序体积很小(小于100 KB),只有在你点击按钮时才会运行,在空闲状态下也不会有任何后台进程在运行。

后续步骤与扩展程序开发建议

恭喜你成功制作出了自己的第一个Chrome扩展程序!

你现在拥有这样一个实用工具:它能够一键按域名对标签页进行分类,还能显示打开的标签页及分类组的实时统计信息。该工具采用了现代技术框架,如TypeScript、React和Plasmo,并遵循了Chrome扩展程序的最佳开发实践。

这个扩展程序为后续的开发奠定了坚实的基础。以下是一些关于如何进一步改进它的想法。

1. 自动分类功能

你完全可以不必点击按钮,而是让新打开的标签页自动被分类。你可以在`background.ts`文件中监听`chrome.tabs.onCreated`事件,在页面URL加载完成后再稍等片刻再调用`groupTabsByDomain()`函数:

// 在background.ts文件中
chrometabs.onCreated.addListener(async (tab) => {
  // 等待页面URL加载完毕
  setTimeout(() => {
    groupTabsByDomain()
  }, 2000)
})

这种实现方式涉及到事件监听、异步处理以及合理选择执行时机——这是了解如何让后台脚本更加主动工作的绝佳练习。

2. 键盘快捷键

你还可以通过添加键盘快捷键来直接触发分类功能,而无需打开弹出窗口。只需在`package.json`文件中的`manifest`部分添加一个`commands`字段即可:

"manifest": {
  "commands": {
    "group-tabs": {
      "suggested_key": {
        "default": "Ctrl+Shift+G",
        "mac": "Command+Shift+G"
      },
      "description": "按域名对标签页进行分类"
    }
  }
}

然后你可以在`background.ts`文件中监听这个快捷键命令:

chromecommands.onCommand.addListener((command) => {
  if (command === "group-tabs") {
    groupTabsByDomain()
  }
})

3. 基于类别的分类

你不仅可以按域名对标签页进行分类,还可以根据类别来分组。例如,可以将GitHub、Stack Overflow和npm归到“开发”这一类别中:

const categories = {
  social: ["facebook.com", "twitter.com", "instagram.com"],
  shopping: ["amazon.com", "ebay.com", "etsy.com"],
  dev: ["github.com", "stackoverflow.com", "npmjs.com"]
}

function getCategoryForDomain(domain: string): string {
  for (const [category, domains] of Object.entries(categories)) {
    if (domains.includes(domain)) {
      return category
    }
  }
  return "other"
}

4. 设置页面

使用Plasmo,你只需创建一个`options.tsx`文件,就能轻松添加设置页面。

在设置页面上,用户可以切换是否启用自动分类功能,选择是按域名还是按类别来分类标签页,或者自定义类别映射关系。

这同时也是了解Chrome存储API以及如何持久化保存用户偏好设置的一个很好的入门方式。

function OptionsPage() {
  return (
    

标签页分类器设置

) }

5. 标签页使用时长追踪

你可以追踪每个标签页的创建时间,从而找出那些已经一周或更长时间未被使用的标签页,这样有助于保持标签页管理的整洁性:

// 追踪标签页的创建时间
const tabCreationTimes = new Map〈number, number〉();

chrome.tabs.onCreated.addListener((tab) => {
  if (tab.id) {
    tabCreationTimes.set(tab.id, Date.now())
  }
});

// 查找旧的标签页(例如,创建时间超过7天的)
function getOldTabs(): chrometabs.Tab[] {
  const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000)
  return tabs.filter(tab => {
    const created = tabCreationTimes.get(tab.id!)
    return created && created < sevenDaysAgo
  })
}

6. 在组内进行搜索

弹出窗口中的搜索栏允许用户按标题筛选他们打开的标签页,从而轻松跳转到特定的标签页:

const [searchQuery, setSearchQuery] = useState("")

const filteredTabs = tabs.filter(tab =>
  tab.title?.toLowerCase().includes(searchQuery.toLowerCase())
)

7. 导出/导入标签页组

用户可以将当前的标签页组保存为JSON文件,之后再恢复这些设置。这对于在重启后保持工作状态非常有用:

// 导出
async function exportGroups() {
  const groups = await chrome.tabGroups.query{}
  const data = JSON.stringify(groups)
  const blob = new Blob([data], { type: 'application/json' })
  const url = URL.createObjectURL(blob)
  chrome.downloads.download({ url, filename: 'tab-groups.json' })
}

// 导入
async function importGroups(file: File) {
  const text = await file.text()
  const groups = JSON.parse(text)
  // 恢复标签页组...
}

8. 标签页组统计信息面板

一个扩展的弹出窗口可以显示浏览分析数据、今天打开的总标签页数、访问次数最多的域名等信息:

function Statistics() {
  const [stats, setStats] = useState({
    totalTabs: 0,
    totalGroups: 0,
    mostUsedDomain: "",
    tabsToday: 0
  })

  return (
    

浏览统计信息

今天打开的总标签页数:{statstabsToday}</p>

访问次数最多的域名:{stats.mostUsedDomain}</p>

) }

学习资源

如果你想深入了解相关内容,Chrome扩展程序官方文档非常有用,其中详细介绍了所有的API接口。

上的Chrome扩展程序示例仓库提供了许多实用的案例供学习参考。对于与Plasmo相关的问题,Plasmo官方文档示例代码库是最佳的学习资源,而且Plasmo社区在Plasmo Discord频道也非常活跃。

React文档TypeScript文档都非常值得添加书签,作为参考资料使用;而React TypeScript速查表在您对某些类型模式不太确定时也会派上用场。

对于社区支持方面,Stack Overflow上的chrome-extension标签会得到密切关注,而Reddit上的r/chromeextensions也是一个非常适合提问的地方。

将扩展程序部署到Chrome应用商店

现在您已经构建并测试了自己的扩展程序,接下来就来看看如何将其发布出来并与全世界分享吧。

所需准备的材料

在开始发布之前,您需要一个已完成测试的扩展程序、一个Google账户、5美元的一次性开发者注册费,以及一些用于应用商店发布的资源文件,比如图标、截图和文字描述等。

这5美元的费用是一次性支付的(并非每年都需要缴纳),Google通过这笔费用来验证开发者的身份,并减少垃圾信息的出现。该费用适用于无限次的应用程序提交,且会通过Google Payments立即完成支付流程。

步骤1:创建生产版本

如果您之前还没有为扩展程序创建生产版本,请现在执行以下操作:

cd tab-grouper-tutorial
npm run build

这样就会在build/chrome-mv3-prod/目录下生成一个优化后的版本。生产版本的扩展程序会压缩JavaScript和CSS文件,从而减小文件大小;同时也会删除仅用于开发环境的代码及控制台日志,并对资源文件进行优化,以加快加载速度。

在上传之前,请先将build/chrome-mv3-prod/目录下的文件解压成可使用的扩展程序格式,然后再测试所有功能,确保在构建过程中没有出现任何问题。

步骤2:准备应用商店所需的资源文件

扩展程序图标

您需要准备三种尺寸的图标:128×128像素大小的图标用于应用商店的主页展示(这是必需的);48×48像素大小的图标用于扩展程序管理页面;16×16像素大小的图标则用作网站的favicon。

所有图标都应为PNG格式,且背景应为透明色。设计时请保持简洁性,确保即使在小尺寸下也能清晰可见。请注意,在16×16像素的图标中不要添加任何文字。

Figma是一款免费工具,非常适合用来制作这些图标;CanvaGIMP也同样适用。

截图

请上传1到5张尺寸为1280×800或640×400像素的截图(文件格式应为PNG或JPEG)。

上传的截图应该展示扩展程序的实际使用效果,而不是设计稿。包含统计数据、标签分组功能以及前后对比效果的截图会更有帮助。

在截图中添加注释可以帮助用户更好地理解这些图示所表达的内容。

宣传图片(可选)

<如果你希望自己的扩展程序能在商店中得到展示,你也可以上传一个小尺寸的图片(440×280像素)、大尺寸的图片(920×680像素),以及一张用于宣传的图片(1400×560像素)。只有当谷歌决定推广你的扩展程序时,这些图片才会被需要。>

演示视频(可选)

一个时长为30至60秒的YouTube短视频,展示该扩展功能的实际使用效果,能够显著提高转化率。你可以在店铺列表中添加这个视频链接。

步骤3:编写店铺列表

扩展程序名称(最多45个字符):要简洁明了且具有描述性。“Tab Grouper——按域名整理标签页”这个名称就很合适。避免使用过多关键词或过多的标点符号。

简介(最多132个字符):这部分内容会显示在搜索结果中。首先说明该扩展程序的功能:“自动按域名整理浏览器标签页,一键分组功能能让你的工作空间保持整洁有序。”

详细说明(最多16,000个字符):首先介绍扩展程序的功能,清晰列出其各项特性,解释使用方法,说明隐私政策,并提供联系方式。以下是一个可供参考的模板:

## Tab Grouper是什么?

Tab Grouper会根据标签页所属网站的域名自动将它们整理分类。再也不用在数十个标签页中费力寻找所需页面了——所有标签页都会被整齐地分类存放。

  1. 功能 - ✅ 一键分组标签页 - ✅ 按域名自动设置颜色编码 - ✅ 提供实时统计信息 - ✅ 适用于所有网站 - ✅ 体积小且运行速度快

使用方法 1. 点击工具栏中的Tab Grouper图标 2. 选择“按域名分组标签页” 3> 标签页会立即被整理好。

为何需要这个扩展程序 如果你经常同时打开很多标签页,寻找所需的页面会浪费大量时间。Tab Grouper会自动将标签页按域名分类,使导航变得快速便捷。

隐私政策 该扩展程序不会收集任何个人数据。它仅会在本地访问标签页信息以完成分组操作,没有任何数据会被发送到外部服务器。

支持与反馈 如果发现漏洞或有任何建议,请通过support@example.com与我们联系。

分类:为Tab Grouper选择“生产力工具”类别。如果你以后想为店铺列表添加其他语言版本,也可以随时进行设置。

步骤4:注册成为Chrome Web商店开发者

访问Chrome Web商店开发者控制台,使用你的Google账户登录,同意开发者协议,然后支付5美元的注册费用。几分钟内你的账户就会激活。

步骤5:提交你的扩展程序

在开发者控制台中,点击“新建项目”并上传你的扩展程序文件。你可以手动将build/chrome-mv3-prod/文件夹压缩成ZIP文件,或者使用Plasmo的工具来打包:


# 方案1:手动压缩
cd build/chrome-mv3-prod
zip -r ../../tab-grouper.zip .

# 方案2:使用Plasmo的包管理命令
cd tab-grouper-tutorial
npm run package

上传扩展程序后,需要填写商店列表表单中的所有四个部分:产品详情(名称、简介、描述、分类、语言)、图形资源(图标和截图)、隐私政策(详见下文),以及分发设置(可见范围、地区、定价信息)。

单一用途说明

Chrome要求每个扩展程序都必须有明确且唯一的用途。以“Tab Grouper”为例:“该扩展程序会根据标签页的域名对它们进行分类,从而帮助用户高效地管理多个打开的标签页。”

权限说明

对于你声明的每一项权限,都需要给出合理的解释。以tabs权限为例:“需要这一权限才能读取标签页的URL和标题,以便根据域名对它们进行分类。”而对于tabGroups权限,则是因为“需要它来创建和管理标签页分组。”

隐私政策

尽管“Tab Grouper”不会收集用户的个人信息,但Chrome仍要求提供隐私政策。你可以在GitHub Pages或个人网站上发布隐私政策,并添加相应的链接。以下是一个基本的模板:

# Tab Grouper隐私政策

## 数据收集
“Tab Grouper”不会收集、存储或传输任何用户的个人信息。
## 权限说明
- **tabs**: 仅用于读取标签页的URL,以便进行分类。
- **tabGroups**: 仅用于创建和管理标签页分组。
## 数据处理方式
所有的标签页分类操作都在用户的浏览器中本地完成,不会有任何数据被发送到外部服务器。
## 联系方式
如有疑问,请联系:your-email@example.com
最后更新时间:[当前日期]

步骤6:提交审核

在点击“提交”之前,请先检查以下内容是否无误:

  • 生产版本已经过全面测试。

  • 所有用于商店展示的资源(图标及至少一张截图)都已上传。

  • 描述内容清晰准确。

  • 所申请的权限都是合理的。

  • 隐私政策链接已经添加。

  • 扩展程序的名称具有描述性。

准备就绪后,点击“提交审核”,确认相关信息,然后点击“发布”。此时你的扩展程序就会进入审核流程。

步骤7:审核过程

对于简单的扩展程序,Google通常会在1到3个工作日内完成审核;而对于复杂的扩展程序或首次提交的扩展程序,审核时间可能会延长至一周。审核人员会检查该扩展程序是否按描述正常工作、所申请的权限是否合理、其中是否包含恶意代码,以及其列表信息是否符合Chrome Web Store的规定。

你可以在开发者控制台中查看自己的审核状态:待审核 → 审核中 → 已批准或被拒绝。如果被拒绝,Google会通过电子邮件告知你具体原因及重新提交的要求。

最常见的被拒绝的原因包括:权限申请的理由不够充分、描述存在误导性、未提供隐私政策文件,或者请求的权限超出了实际需要。请在收到拒绝通知后,针对这些问题进行相应的修改,然后重新提交申请。

步骤8:审批通过后

一旦获得批准,您的扩展程序就会在https://chrome.google.com/webstore/detail/[extension-id]上正式上线。您可以通过社交媒体分享该链接,或在博客中发表文章、在Reddit的r/chrome或r/chromeextensions板块发帖,或者将扩展程序提交到Product Hunt平台来促进安装量。

开发者控制台会提供实时的分析数据——包括总安装次数和每周的安装数量、用户评价与评分、展示次数以及卸载次数。请定期查看这些数据,尤其是在刚开始的那一周。对于用户的评价(尤其是负面的评价)要及时回复,对正面反馈要表示感谢,并利用用户报告的漏洞来确定未来更新的优先级。

步骤9:发布更新

当您修复了漏洞或添加了新功能后,请在package.json文件中修改版本号(遵循语义版本命名规范:修复漏洞用“patch”版本号,添加新功能用“minor”版本号,进行重大改动时使用“major”版本号),然后运行npm run build命令,最后通过开发者控制台的Package选项卡上传新的软件包。与初次提交相比,更新通常会更快得到审核,往往在24小时内就能得到回复。

步骤10:长期管理您的扩展程序

Chrome Web Store提供了内置的分析功能,但如果您需要更详细的数据,也可以添加Google Analytics进行分析。

在提供用户支持方面,在扩展程序的描述中提供一个电子邮件地址,或者创建一个GitHub问题页面,都是很好的选择。随着您不断添加新功能,请及时更新描述内容,并维护一份变更日志,这样用户就能清楚地了解哪些功能发生了变化以及变化的时间。积极回应用户的疑问和评价,有助于建立一批会向他人推荐您的扩展程序的忠实用户群体。

常见发布问题的解决方法

上传时出现“软件包无效”的错误:请确保您压缩的是build/chrome-mv3-prod/文件夹内的文件内容,而不是整个文件夹本身,并且要确认生成的manifest.json文件是有效的JSON格式。

审核被拒绝:权限设置不合理:在“权限说明”字段中,请明确说明每个功能需要哪些权限,以及如果没有这些权限的话会导致什么问题。

审核被拒绝:单一功能的描述不够清晰:请重新撰写关于该功能的描述,明确指出它的核心作用是什么。

上线后安装率很低:糟糕的截图往往是导致这一问题的原因——大多数用户首先看到的就是这些截图。因此,请确保截图能够清楚地展示您的扩展程序是如何解决实际问题的。即使只收集到少量的早期用户评价,也会对新访问者产生很大的影响。

其他分发方式

对于大多数公开发布的扩展程序来说,Chrome Web Store确实是最佳的选择。但如果您正在开发一个内部使用的工具,那么未上架的扩展程序也是一个不错的选择——这种扩展程序只能通过直接链接访问,无法在搜索引擎中找到。

<如果你需要将这个扩展程序仅限特定 Google Workspace 组织中的用户使用,那么有一个名为 Private 的扩展程序选项可供选择。虽然也可以自行托管或通过其他方式安装该扩展程序,但用户需要手动启用开发者模式,因此这种方法只适合技术背景较强的用户。

恭喜你!

<你已经成功将一个空文件夹发展成了一个可以在 Google Web Store 上发布的 Chrome 扩展程序。在这个过程中,你了解了扩展程序的结构、后台脚本与弹出窗口之间的通信机制、Chrome 的标签页 API 的工作原理,同时也掌握了从开始到结束的整个发布流程。

<相比任何具体的 API 或配置细节,最重要的是你建立起了一种关于扩展程序工作原理的理解模式,这种理解模式会直接帮助你开发后续的任何扩展程序。

<继续努力吧,不断学习,持续推出新的扩展程序吧!

Comments are closed.