如何使用 Plasmo 开发 Chrome 扩展 [完整手册]
![如何使用 Plasmo 开发 Chrome 扩展 [完整手册]](/api/img-proxy?url=https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F64ef9ca6a3a26476fe998b69%2F43f51cde-41c8-46ac-9305-6b4ad5adc1ac.gif)
TL;DR · AI 摘要
Plasmo 可自动化配置 Chrome 扩展项目,支持 TypeScript 与 React,本教程教你从零构建并发布标签分组扩展。
核心要点
- Plasmo 自动生成 manifest.json 并集成热重载,支持 TypeScript 和 React 开箱即用。
- 通过 chrome.tabs 和 chrome.tabGroups API 实现自动按域名分组标签页功能。
- 项目可通过 plasmo build 构建并上传至 Chrome Web Store 发布。
结构提纲
按章节快速跳转。
Plasmo 是一个现代浏览器扩展开发框架,能自动生成 manifest.json 并集成 TypeScript、React 和热重载功能。
开发一个可将浏览器标签按网站域名自动分组并着色的 Chrome 扩展。
- ·学习内容
掌握 Chrome 扩展的基本结构、核心 API 使用以及现代前端工具链集成方法。
需要 Node.js 18+、pnpm、Chrome 浏览器和代码编辑器即可开始开发。
运行 'npx create-plasmo' 命令即可生成带 TypeScript 和 React 支持的完整扩展项目结构。
使用 plasmo build 生成生产包,解压后可提交到 Chrome Web Store 完成发布。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- Plasmo 开发 Chrome 扩展
- 核心优势
- 自动生成 manifest
- TypeScript + React 支持
- 热重载开发体验
- 关键技术
- chrome.tabs API
- chrome.tabGroups API
- chrome.runtime 消息通信
- 开发流程
- npx create-plasmo 初始化
- 本地调试与实时更新
- plasmo build 构建发布包
金句 / Highlights
值得收藏与分享的关键句。
Plasmo 能处理所有配置工作,一条命令即可搭建好 TypeScript 与 React 已就绪的可运行项目。
只需单击一次,Tab Grouper 扩展就会为每个网站自动创建彩色分组,便于查找和管理标签页。
Plasmo 不会隐藏 Chrome 扩展的概念,你仍可直接使用 chrome.tabs、chrome.runtime 等原生 API。
标题:如何使用 Plasmo 开发 Chrome 扩展 [完整手册]
来源链接:https://www.freecodecamp.org/news/how-to-develop-chrome-extensions-using-plasmo-handbook/
发布时间:2026-05-11T20:11:25.776Z
Markdown 内容:
![图片 1:如何使用 Plasmo 开发 Chrome 扩展 [完整手册]](https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/e0d0bca4-a2e8-495a-9c1c-4f0b9ef52630.png) Chrome 扩展是轻量级工具,可以增强并个性化你的浏览体验,无论是管理密码、翻译网页,还是为你每天使用的网站添加全新功能。
已有数百万开发者向 Chrome 网上应用店发布扩展,而构建一个扩展的门槛比你想象中更低。
在本手册中,你将从零开始,使用 TypeScript、React 和 Plasmo 框架开发并发布一个 Chrome 扩展。Plasmo 是一个现代框架,能自动处理重复性的配置和初始化工作,让你专注于功能开发,而非样板代码。
在此过程中,你会接触到真实可用的 Chrome 扩展 API,这些 API 正是生产级扩展的核心:查询标签页、创建标签组,以及在扩展的不同部分之间传递消息。
最终,你将拥有一套可运行的代码,理解扩展的整体结构,并掌握将自己创意发布到 Chrome 网上应用店所需的全部知识。
目录
什么是 Plasmo?
Plasmo 是一个用于构建浏览器扩展的开源框架。你可以将其视为 Create React App 或 Next.js 的“扩展版”——专为 Chrome 扩展打造。
如果没有 Plasmo,你需要手动编写 manifest.json 文件,自行配置构建工具、TypeScript 和 React。而 Plasmo 能帮你完成所有这些工作。
只需一条命令,就能生成一个已配置好 TypeScript 和 React 的项目脚手架。它会读取你的 package.json 并自动生成 Chrome 所需的 manifest.json,你无需直接编辑该文件。
此外,在开发过程中,源文件的任何更改都会自动重新构建并在 Chrome 中实时刷新。开箱即用的类型安全支持,包括对 Chrome 自身 API 的类型定义。
Plasmo 并不会隐藏 Chrome 扩展的核心概念。你仍然可以直接使用 chrome.tabs、chrome.runtime 等 Chrome API。它只是帮你省去了繁琐的初始化步骤,让你能够立即投入开发。
你将构建什么
在本教程中,你将从零开始构建一个名为 Tab Grouper 的 Chrome 扩展。
这个扩展会根据标签页的网站域名,自动将你的浏览器标签分组整理。

示例场景
假设你打开了 20 个标签页:5 个来自 GitHub,4 个来自 YouTube,3 个来自 Stack Overflow,其余 8 个来自其他网站。
只需点击一次,Tab Grouper 扩展就会为每个网站自动创建彩色标签组,让你更轻松地查找和管理标签页。
你将学到什么
完成本教程后,你将在三个方面获得实战经验。
第一,Chrome 扩展基础:了解扩展的底层工作原理、扩展的组成部分(清单文件、后台脚本、弹出窗口),以及如何在开发期间加载和测试扩展。
第二,Chrome API:特别是使用 chrome.tabs 管理浏览器标签页,使用 chrome.tabGroups 创建和自定义标签组,以及使用 chrome.runtime 在扩展的不同部分之间传递消息。
第三,现代 Web 开发工具链:使用 TypeScript 实现类型安全的 JavaScript,使用 React 构建弹出界面,以及使用 Plasmo 框架整合整个项目。
前提条件
你不需要成为以下任一技术的专家,但如果你熟悉基本的 JavaScript 或 TypeScript,并对 HTML 和 CSS 有大致了解,学习过程会更加顺畅。
对 React 有一定了解会有帮助,但不是必需的。我们将要构建的弹窗组件足够简单,即使你是 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 自动生成一个包含所有必要配置的初始项目,再探索其结构并根据需求进行修改。
第一步:安装 pnpm(推荐)
Plasmo 官方推荐使用 pnpm,以实现更快的安装速度和更优的磁盘空间利用。请先检查是否已安装:
pnpm --version如果看到版本号,跳转到第二步即可。

如果提示“命令未找到”,请使用以下命令安装:
npm install -g pnpm第二步:创建你的扩展项目
运行以下命令创建一个新的 Plasmo 项目:
pnpm create plasmo tab-grouper🟣 创建一个新的 Plasmo 扩展
📁 项目名称:tab-grouper
? 扩展描述:(为你的扩展提供一个不错的描述)
? 作者姓名:(你的名字)Plasmo 将会自动搭建项目并安装依赖项。你可能会被提示输入描述和作者姓名。
根据你的喜好填写即可。

第 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:列出依赖项和脚本,并用于配置扩展清单(manifest)。
- popup.tsx:点击扩展图标时显示的用户界面。
- tsconfig.json:包含已正确配置的 TypeScript 设置。
第 5 步:测试默认扩展
在你进行自定义之前,请确保一切正常工作。
你可以通过启动开发服务器来实现:
pnpm dev你应该会看到类似以下的输出:
🟣 Plasmo v0.90.5
🔴 浏览器扩展框架
🔵 INFO | 启动扩展开发服务器...
🔵 INFO | 正在构建目标:chrome-mv3
🔵 INFO | 从 [] 加载环境变量
🟢 DONE | 扩展已在 1842ms 内重新打包!🚀
查看扩展:
📦 build/chrome-mv3-dev你的扩展已准备就绪。请保持此终端窗口打开。
Plasmo 会监听文件更改并自动重建。
第 6 步:在 Chrome 中加载扩展
现在将扩展加载到 Chrome 中进行测试:
- 打开 Google Chrome
- 访问
chrome://extensions/
- 启用 开发者模式(右上角的开关)
- 点击 “加载已解压的扩展程序”
- 导航到你的项目文件夹
- 选择
build/chrome-mv3-dev文件夹
- 点击“选择文件夹”

你的扩展现在应该出现在列表中。
- 点击 Chrome 工具栏中的拼图块图标
- 找到 "tab-grouper" 并固定它
- 点击扩展图标
你会看到一个默认弹窗,上面写着“Welcome to Plasmo!”(欢迎使用 Plasmo!)

扩展已正常运行。现在你可以开始自定义了。
第 8 步:更新扩展信息
在编辑器中打开 package.json。该文件存储有关项目的元数据,包括名称、版本、描述、依赖项以及用于构建和运行扩展的脚本。
找到顶部附近的这几行:
{
"name": "tab-grouper",
"displayName": "tab-grouper",
"version": "0.0.0",
"description": "A basic Plasmo extension.",将其修改为:
{
"name": "tab-grouper",
"displayName": "Tab Grouper",
"version": "1.0.0",
"description": "A simple Chrome extension - group tabs by domain",保存文件。
第 9 步:添加所需权限(关键步骤!)
这是关键一步。 如果没有权限,你的扩展将会报错,例如:
TypeError: Cannot read properties of undefined (reading 'query')Chrome 扩展必须声明它们打算使用的浏览器 API。在 package.json 中找到 "manifest" 部分。
它看起来像这样:
"manifest": {
"host_permissions": [
"https://*/*"
]
}将其替换为:
"manifest": {
"permissions": [
"tabs",
"tabGroups"
]
}保存文件。tabs 权限允许你读取标签页信息(调用 chrome.tabs.query() 所需),而 tabGroups 权限允许你创建和管理标签组(调用 chrome.tabGroups.update() 所需)。
为自己的扩展查找正确的权限:
Chrome 扩展权限参考文档 列出了所有可用权限及其功能。
每个 API 的文档页面也会列出所需的权限,例如 chrome.tabs API 页面 指明需要 "tabs" 权限。
如果你使用的是 Plasmo,可以查阅 清单配置文档,了解如何通过 package.json 添加权限。
一般规则是:当你调用 Chrome API 时遇到 undefined 错误,首先应检查是否缺少权限。
第 10 步:验证热重载是否生效
Plasmo 在你保存更改后会自动重新加载扩展。
查看运行 pnpm dev 的终端。在保存 package.json 后,你应该会看到类似以下内容:
🔄 正在重新加载扩展...
✅ 准备就绪,耗时 0.8s你的项目现已准备就绪:一个在 Chrome 中加载的可运行扩展、一个启用热重载的开发服务器,以及已配置的必要权限。
在接下来的章节中,请保持开发服务器运行并保留已加载的扩展。你的更改将自动重新加载。
本节小结
在本节中,你安装了 pnpm,使用 pnpm create plasmo 脚手架创建了一个新扩展,探索了生成的项目结构,启动了开发服务器,将扩展加载到 Chrome 浏览器中,并更新了扩展的元数据和权限。
下一步: 你将创建用于处理标签页分组逻辑的后台脚本。
理解后台脚本
后台脚本是你的扩展的核心。它在后台持续运行,并包含核心逻辑。
在本例中,该代码负责按域名对标签页进行分组。
什么是后台脚本?
后台脚本会持续运行,即使弹出窗口已关闭也不会停止。
它可以监听浏览器事件(如标签页的打开、关闭或更新),执行无需用户直接交互的任务,并通过消息传递与其他扩展组件通信。
你可以将其视为扩展的“服务端”。而弹出窗口只是与之通信的用户界面。
步骤 1:创建 background.ts
Plasmo 的脚手架默认不会创建后台脚本,因此你需要从零开始创建此文件。在项目根目录下(与 popup.tsx 同级)创建一个名为 background.ts 的新文件:
export {}
// 后台脚本 - 在后台运行并处理标签页分组逻辑
console.log("Tab Grouper 后台脚本已加载!")
// 监听来自弹出窗口的消息
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" 消息时,它将调用分组函数。
你可以在 Chrome 扩展文档 中了解更多关于这种消息传递模式的信息。
步骤 2:实现标签页分组逻辑
现在,在消息监听器下方添加主要的分组函数:
async function groupTabsByDomain() {
try {
// 步骤 1:获取当前窗口中的所有标签页
const tabs = await chrome.tabs.query({ currentWindow: true })
// 步骤 2:创建一个 Map,按域名组织标签页
const domainGroups = new Map<string, chrome.tabs.Tab[]>()
// 步骤 3:遍历每个标签页并按域名分组
tabs.forEach(tab => {
// 跳过没有 URL 的标签页
if (!tab.url) return
// 从 URL 中提取域名
const domain = getDomainFromUrl(tab.url)
// 跳过无效域名(例如 chrome:// 页面)
if (!domain) return
// 将标签页添加到对应的域名分组中
if (!domainGroups.has(domain)) {
domainGroups.set(domain, [])
}
domainGroups.get(domain)!.push(tab)
})
// 步骤 4:为每个域名创建标签页组(仅当有 2 个以上标签页时)
for (const [domain, domainTabs] of domainGroups) {
// 跳过只有一个标签页的域名
if (domainTabs.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.update(groupId, {
color: getColorForDomain(domain) // 随机化的标签页组颜色
})
}
console.log(`成功分组 ${domainGroups.size} 个域名`)
} catch (error) {
console.error("分组标签页时出错:", error)
}
}该函数首先查询当前窗口中的所有标签页,然后遍历这些标签页,构建一个以域名作为键的 Map。
当每个标签页都被归入相应的域名分组后,函数遍历该 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://extensions 和 chrome://settings。
.replace(/^www\./, "") 确保 www.github.com 和 github.com 被视为同一域名,而不是两个独立的分组。
整个函数包裹在 try-catch 中,以便错误格式的 URL 直接返回 null 并被跳过。
实际效果:https://www.github.com/user/repo 变为 github.com,https://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("Tab Grouper 背景脚本已加载!")
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === "GROUP_TABS") { groupTabsByDomain() sendResponse({ success: true }) } return true })
async function groupTabsByDomain() { try { const tabs = await chrome.tabs.query({ currentWindow: true }) const domainGroups = new Map<string, chrome.tabs.Tab[]>()
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.update(groupId, {
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 << 5) - hash) }
return colors[Math.abs(hash) % colors.length] }
### 测试背景脚本
如果你的开发服务器尚未从前一节启动,请运行以下命令启动它:
pnpm dev
要验证背景脚本是否正确加载,请前往 `chrome://extensions`,找到“Tab Grouper Tutorial”,然后点击 **"service worker"** 链接。
DevTools 控制台将会打开,你应该能看到“Tab Grouper 背景脚本已加载!”的消息,确认一切已正确连接。
弹出窗口(popup)是当用户点击 Chrome 工具栏中的扩展图标时出现的小型窗口。
它可以显示信息、提供操作按钮以及展示设置选项。
在本节中,你将构建一个基于 React 的弹出窗口,用于显示实时的标签页统计信息,并触发背景脚本中的分组逻辑。
当你运行 `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 ( <div> <h2>Tab Grouper</h2> <button>分组标签页</button> </div> ) }
export default IndexPopup
保存文件后,扩展程序将自动重新加载。
这三个状态变量分别用于跟踪打开的标签页数量、现有分组数量,以及当前是否正在进行分组操作。
最后一个状态允许我们禁用按钮并显示加载状态,防止用户同时触发多次分组操作。
### 第二步:加载统计数据
现在添加逻辑,在弹出窗口打开时加载标签页和分组的计数。将以下代码添加到 `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` 会在组件首次挂载时执行一次。也就是说,每次弹出窗口打开时都会运行。
它调用 `loadStats`,向 Chrome 查询当前窗口的标签页和分组,然后使用计数更新状态变量。
### 第三步:触发标签页分组
添加处理函数,在用户点击按钮时向背景脚本发送消息:
async function handleGroupTabs() { setIsGrouping(true)
// 向背景脚本发送消息 await chrome.runtime.sendMessage({ type: "GROUP_TABS" })
// 刷新统计数据 await loadStats() setIsGrouping(false) }
`chrome.runtime.sendMessage` 将 `{ type: "GROUP_TABS" }` 消息发送给我们在 `background.ts` 中设置的监听器。
背景脚本完成后,我们刷新统计数据以立即更新分组计数,然后重新启用按钮。
### 第四步:构建 UI
将占位的 `return` 语句替换为以下完整且带样式的版本:
return ( <div style={{ width: 300, padding: 20, fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' }}> {/* 头部 */} <div style={{ marginBottom: 20 }}> <h2 style={{ margin: 0, fontSize: 20, fontWeight: 600 }}> 🗂️ 标签分组器 </h2> <p style={{ margin: "8px 0 0", fontSize: 13, color: "#666" }}> 按域名整理你的标签页 </p> </div>
{/* 统计信息 */} <div style={{ display: "flex", gap: 12, marginBottom: 20, padding: 12, background: "#f5f5f5", borderRadius: 8 }}> <div style={{ flex: 1 }}> <div style={{ fontSize: 24, fontWeight: 600, color: "#333" }}> {tabCount} </div> <div style={{ fontSize: 12, color: "#666" }}> 打开的标签页 </div> </div> <div style={{ flex: 1 }}> <div style={{ fontSize: 24, fontWeight: 600, color: "#0066ff" }}> {groupCount} </div> <div style={{ fontSize: 12, color: "#666" }}> 标签组 </div> </div> </div>
{/* 分组按钮 */} <button onClick={handleGroupTabs} disabled={isGrouping} style={{ width: "100%", padding: "12px 16px", fontSize: 14, fontWeight: 500, color: "white", background: isGrouping ? "#ccc" : "#0066ff", border: "none", borderRadius: 8, cursor: isGrouping ? "not-allowed" : "pointer", transition: "background 0.2s" }} > {isGrouping ? "正在分组..." : "🗂️ 按域名分组标签页"} </button>
{/* 底部提示 */} <div style={{ marginTop: 16, padding: 12, fontSize: 12, color: "#666", background: "#fff9e6", borderRadius: 6, border: "1px solid #ffe066" }}> 💡 <strong>提示:</strong> 此操作将按网站域名对当前窗口中的所有标签页进行分组。 </div> </div> )
该界面包含四个部分:带有扩展名称和简短描述的头部、并排显示实时标签页数量和分组数量的统计框、主操作按钮(工作进行时会变灰并显示“正在分组...”),以及底部的提示框。
本教程为简化起见使用了内联样式。在生产环境的扩展中,你可能会选择使用 CSS 模块、Tailwind 或 styled-components。
你的完整 `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 ( <div style={{ width: 300, padding: 20, fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' }}> <div style={{ marginBottom: 20 }}> <h2 style={{ margin: 0, fontSize: 20, fontWeight: 600 }}> 🗂️ 标签分组器 </h2> <p style={{ margin: "8px 0 0", fontSize: 13, color: "#666" }}> 按域名整理你的标签页 </p> </div>
<div style={{ display: "flex", gap: 12, marginBottom: 20, padding: 12, background: "#f5f5f5", borderRadius: 8 }}> <div style={{ flex: 1 }}> <div style={{ fontSize: 24, fontWeight: 600, color: "#333" }}> {tabCount} </div> <div style={{ fontSize: 12, color: "#666" }}> 打开的标签页 </div> </div> <div style={{ flex: 1 }}> <div style={{ fontSize: 24, fontWeight: 600, color: "#0066ff" }}> {groupCount} </div> <div style={{ fontSize: 12, color: "#666" }}> 标签组 </div> </div> </div>
<button onClick={handleGroupTabs} disabled={isGrouping} style={{ width: "100%", padding: "12px 16px", fontSize: 14, fontWeight: 500, color: "white", background: isGrouping ? "#ccc" : "#0066ff", border: "none", borderRadius: 8, cursor: isGrouping ? "not-allowed" : "pointer", transition: "background 0.2s" }} > {isGrouping ? "正在分组..." : "🗂️ 按域名分组标签页"} </button>
<div style={{ marginTop: 16, padding: 12, fontSize: 12, color: "#666", background: "#fff9e6", borderRadius: 6, border: "1px solid #ffe066" }}> 💡 <strong>提示:</strong> 此操作将按网站域名对当前窗口中的所有标签页进行分组。 </div> </div> ) }
export default IndexPopup
## 测试你的扩展
现在你已经构建好了后台脚本和弹出界面,是时候在 Chrome 中验证一切是否正常工作了。
### 第一步:确保开发服务器正在运行
如果之前的步骤中没有运行 `pnpm dev`,现在启动它:
pnpm run dev # 或 pnpm dev
Plasmo 将把扩展构建到 `build/chrome-mv3-dev` 目录并监听文件变化。
### 第二步:在 Chrome 中加载扩展
如果你还没有加载扩展,请前往 `chrome://extensions/`,启用 **开发者模式**,点击 **加载已解压的扩展程序**,然后选择 `build/chrome-mv3-dev` 文件夹。
加载后,你应该能在列表中看到名为“Tab Grouper Tutorial”、版本为“1.0.0”且状态为“已启用”的扩展。
### 第三步:固定扩展
点击 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/questions` 和 `https://stackoverflow.com/tags`
请确保至少打开 7 个标签页。
#### 测试 2:对标签页进行分组
点击 Tab Grouper 扩展图标。弹出窗口应显示你当前打开的标签页数量(7 个或更多)以及分组数量(可能为 0)。
点击 **"按域名分组标签页"**,观察你的标签页被自动组织成不同颜色的组。
#### 测试 3:验证分组结果
点击按钮后,GitHub 的标签页应被归入一个带有 "github.com" 标签和统一颜色的组,YouTube 的标签页也应类似处理。
再次点击扩展图标,此时分组数量应显示为 2,而标签页总数保持不变。
### 第 5 步:调试扩展程序
如果某些功能未正常工作,Chrome 的 DevTools 是你最好的帮手。
要检查后台脚本,请前往 `chrome://extensions/`,找到你的扩展程序,点击 **"service worker"** 链接。
DevTools 控制台将打开,你可以查找 “Tab Grouper background script loaded!” 消息,以及任何红色显示的错误信息。
要检查弹出窗口,右键点击扩展图标并选择 **"Inspect popup"**。这会为弹出窗口单独打开 DevTools —— 请查看 Console 标签页中的错误。
**如果你点击按钮后没有任何反应**,请检查后台脚本控制台中的错误,确认你至少有两个来自同一域名的标签页,并验证消息是否已正确发送(在弹出窗口控制台中查看是否有 `sendMessage` 失败的情况)。
**如果标签页没有被分组**,请仔细检查是否已在 `package.json` 中添加了 `tabs` 和 `tabGroups` 权限,并在保存后重新加载了扩展程序。
**如果你看到“Extension cannot access chrome://...”的提示**,这是预期行为 —— 扩展程序无法与 Chrome 内部页面交互,代码会主动跳过这些页面。
### 第 6 步:热重载
Plasmo 的一大优势是支持热重载(hot reloading),允许你在应用运行时即时更新代码,无需手动重启。
打开 `popup.tsx`,将标题的 emoji 从 🗂️ 改为 📁,然后保存。
扩展程序将自动重新加载。
点击图标,你会立即看到更新后的 emoji。
热重载的优势在于加快开发速度,让你能实时查看更改效果。
如果你想让扩展程序与本教程其余示例和截图保持一致,之后可以再把 emoji 改回去。
### 第 7 步:测试边界情况
值得测试几种特殊情况,以确保扩展程序能优雅地处理它们。
如果你关闭所有标签页只剩一个,然后点击“分组标签页”,不应有任何反应。扩展程序需要至少两个来自同一域名的标签页才能形成分组。打开 `chrome://extensions` 和 `chrome://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` 事件,并稍作延迟后调用 `groupTabsByDomain()`,以便等待页面 URL 加载完成:
// 在 background.ts 中 chrome.tabs.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` 中监听该命令:
chrome.commands.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 Storage API 和持久化保存用户偏好设置的良好入门实践。
function OptionsPage() { return ( <div> <h1>标签页分组设置</h1> <label> <input type="checkbox" /> 启用自动分组 </label> <label> <input type="checkbox" /> 按类别而非域名进行分组 </label> </div> ) }
### 5. 标签页使用时长追踪
你可以记录每个标签页的创建时间,并找出那些已闲置一周甚至更久的标签页,以此鼓励用户保持良好的标签页管理习惯:
// 记录标签页创建时间 const tabCreationTimes = new Map<number, number>()
chrome.tabs.onCreated.addListener((tab) => { if (tab.id) { tabCreationTimes.set(tab.id, Date.now()) } })
// 查找旧标签页(例如超过7天) function getOldTabs(): chrome.tabs.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 ( <div> <h3>浏览统计</h3> <p>今日总共打开标签页数:{stats.tabsToday}</p> <p>访问最多的域名:{stats.mostUsedDomain}</p> </div> ) }
## 学习资源
如果你想深入学习,[官方 Chrome 扩展文档](https://developer.chrome.com/docs/extensions/) 非常出色,详细介绍了每一个 API。
GitHub 上的 [Chrome 扩展示例仓库](https://github.com/GoogleChrome/chrome-extensions-samples) 提供了数十个真实可用的示例供你学习参考。对于 Plasmo 特有的问题,[Plasmo 官方文档](https://docs.plasmo.com/) 和 [示例仓库](https://github.com/PlasmoHQ/examples) 是最佳起点,同时社区成员活跃于 [Plasmo Discord](https://www.plasmo.com/community) 中。
[React 官方文档](https://react.dev/) 和 [TypeScript 官方文档](https://www.typescriptlang.org/docs/) 值得收藏作为参考资料,而当你对某些类型写法不确定时,[React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/) 是一个非常实用的工具。
如需社区支持,Stack Overflow 上的 `chrome-extension` 标签受到良好维护,Reddit 上的 r/chrome_extensions 社区也是一个提问交流的友好场所。
## 发布到 Chrome 网上应用店
现在你已经构建并测试好了你的扩展程序,接下来介绍如何发布它并与全世界分享。
### 所需准备
在发布之前,你需要准备好一个已完成并经过测试的扩展程序、一个 Google 账号、一次性支付 5 美元的开发者注册费用,以及一些商店所需的资源文件,例如图标、截图和文字描述。
这 5 美元是一次性费用(非年度收费),Google 用它来验证开发者身份并减少垃圾信息。该费用涵盖无限次提交扩展程序,通过 Google Payments 立即完成扣款。
### 第一步:生成生产版本构建
如果你之前没有做过,请为生产环境构建你的扩展程序:
cd tab-grouper-tutorial npm run build
这将在 `build/chrome-mv3-prod/` 目录下生成一个优化后的版本。生产构建会对 JavaScript 和 CSS 进行压缩以减小文件大小,移除仅用于开发的代码和控制台日志,并优化资源以实现更快加载。
上传前,请先将 `build/chrome-mv3-prod/` 作为未打包扩展程序加载进来,再次全面测试所有功能,确保构建过程中没有引入任何问题。
### 第二步:创建商店资源
#### 扩展图标
你需要提供三种尺寸的图标:**128×128 像素**(用于主商店列表,必填)、**48×48**(用于扩展管理页面)和 **16×16**(用作网站 favicon)。
所有图标都应为带透明背景的 PNG 文件。设计应简洁明了,在小尺寸下仍清晰可辨。避免在 16×16 图标中加入文字。
[Figma](https://figma.com/) 免费且非常适合此用途,[Canva](https://canva.com/) 或 [GIMP](https://gimp.org/) 也是不错的选择。
#### 截图
上传 1 到 5 张截图,尺寸为 1280×800 或 640×400 像素(PNG 或 JPEG 格式均可)。
尽量展示扩展的实际使用场景,而不是静态设计稿。例如弹出窗口中的统计数据、标签页正在被分组的过程、以及分组前后的对比状态都非常合适。
添加标注以突出关键功能,有助于用户理解他们看到的内容。
#### 宣传图片(可选)如果你想在商店中获得推荐展示,还可以上传一个小磁贴(440×280)、大磁贴(920×680)和横幅图片(1400×560)。这些仅在 Google 决定推广你的扩展程序时才需要。
#### 演示视频(可选)
一段简短的 YouTube 视频(30–60 秒),展示扩展程序的实际使用效果,可以显著提高转化率。在你的商店列表中添加该视频链接。
第 3 步:撰写商店列表内容
扩展名称(45 字符限制):清晰且具有描述性。“Tab Grouper - 按域名整理标签页”就是一个很好的例子。避免关键词堆砌或过度使用标点符号。
摘要(132 字符限制):这是在搜索结果中显示的内容。开头应说明扩展的功能:“自动按域名整理浏览器标签页。一键分组,保持工作区整洁高效。”
详细描述(16,000 字符限制):从扩展功能开始,清晰列出特性,说明使用方法,阐述隐私保护措施,并提供联系方式。以下是一个可自定义的模板:
## 什么是 Tab Grouper?
Tab Grouper 会根据网站域名自动整理你的浏览器标签页,将它们分组管理。再也不用在几十个标签页中翻找——一切井然有序。
## 功能特点
- ✅ 一键标签分组
- ✅ 按域名自动颜色编码
- ✅ 实时统计信息
- ✅ 兼容所有网站
- ✅ 轻量快速
## 如何使用
1. 点击工具栏中的 Tab Grouper 图标
2. 点击“按域名分组标签”
3. 标签页立即完成整理
## 为什么你需要它
如果你经常打开大量标签页,寻找正确的那一个会浪费宝贵时间。Tab Grouper 通过将标签页自动归入彩色分组,让导航变得快速而简单。
## 隐私保护
本扩展不收集任何个人数据。仅在本地访问标签信息以执行分组操作,不会向外部服务器发送任何数据。
## 支持
发现 Bug 或有建议?请联系我们:[email protected]类别:为 Tab Grouper 选择 效率工具。如果需要本地化列表,之后可添加其他语言。
第 4 步:注册成为 Chrome 网上应用店开发者
前往 Chrome 网上应用店开发者控制台,使用你的 Google 账户登录,接受开发者协议,并支付 5 美元注册费。你的账户将在几分钟内激活。
第 5 步:提交你的扩展程序
在开发者控制台中,点击 “新建项目” 并上传你的扩展程序。你可以手动压缩 build/chrome-mv3-prod/ 文件夹,也可以使用 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:“需要 tabs 权限以读取标签页的 URL 和标题,从而按域名进行分组。” 对于 tabGroups:“需要 tabGroups 权限以创建和管理标签组,实现组织功能。”
#### 隐私政策
尽管 Tab Grouper 不收集个人数据,Chrome 可能仍要求提供隐私政策。你可以将其托管在 GitHub Pages 或个人网站上并提供链接。以下是一个极简模板:
# Tab Grouper 隐私政策
## 数据收集
Tab Grouper 不收集、存储或传输任何个人数据。
## 权限说明
- **tabs**:仅用于读取标签页 URL 以实现分组
- **tabGroups**:仅用于创建和管理标签组
## 本地处理
所有标签分组操作均在你的浏览器本地完成。无任何数据发送至外部服务器。
## 联系方式
如有疑问,请联系:[email protected]
最后更新日期:[当前日期]第 6 步:提交审核
在点击提交前,请检查以下清单:
- 已充分测试生产版本
- 已上传所有商店资源(图标 + 至少一张截图)
- 描述清晰准确
- 所有权限均已说明
- 已链接隐私政策
- 扩展名称具有描述性
准备就绪后,点击 “提交审核”,确认信息无误,然后点击 “发布”。你的扩展将进入审核队列。
第 7 步:审核流程
对于简单的提交,Google 通常在 1–3 个工作日内完成审核;复杂扩展或首次提交可能需要长达一周时间。审核人员会检查扩展是否如描述般运行,权限是否合理,是否存在恶意代码,以及列表是否符合 Chrome 网上应用店政策。
你可以在开发者控制台跟踪状态:待审核 → 审核中 → 已批准或已拒绝。如果被拒,Google 会通过邮件告知具体原因及重新提交的指导。
最常见的拒绝原因包括:权限说明不足、描述误导、缺少隐私政策、请求了超出必要的权限。请根据拒绝邮件中的每一点进行修改,更新提交内容后重新提交。
第 8 步:审核通过后
一旦获批,你的扩展将在 https://chrome.google.com/webstore/detail/[extension-id] 上线。你可以将链接分享到社交媒体,撰写博客文章,在 Reddit(r/chrome, r/chrome_extensions)发帖,或提交至 Product Hunt,以吸引更多安装。
开发者仪表板会为你提供持续的分析数据——总安装量和每周安装量、评价与评分、展示次数以及卸载数量。请定期查看,尤其是在发布的第一周内。及时回复用户评论(特别是负面评论),感谢用户的积极反馈,并利用报告中的 bug 来优先安排后续更新。
第 9 步:发布更新
当你修复了 bug 或添加了新功能时,请在 package.json 中增加版本号(遵循 语义化版本控制 规范——bug 修复使用补丁版本 patch,新增功能使用次版本 minor,破坏性变更使用主版本 major),运行 npm run build,然后通过开发者仪表板的 Package 标签页上传新的包。更新通常比初次提交审核更快,通常在 24 小时内完成。
第 10 步:长期维护你的扩展程序
Chrome 网上应用店提供内置的分析功能,但如果你需要更详细的数据,也可以集成 Google Analytics。
对于用户支持,可以在描述中提供一个邮箱地址,或使用 GitHub 的 Issues 页面,两者都很有效。随着功能的增加,请保持描述内容的更新,并维护一份变更日志,让用户清楚知道每次更新的内容和时间。积极回应用户的问题和评论,有助于建立一批忠实用户,他们将乐于向他人推荐你的扩展。
常见发布问题排查
上传时提示“包无效”:请确保你压缩的是 build/chrome-mv3-prod/ 文件夹内的内容,而不是该文件夹本身,并验证生成的 manifest.json 是否为有效的 JSON 格式。
被拒:权限未说明清楚:在“权限说明”字段中,具体说明每一项权限对应的功能,以及缺少该权限会导致什么功能失效。
被拒:单一用途不明确:重写单一用途描述,聚焦于一个核心功能,并用清晰直白的语言表达。
发布后安装率低:问题常出在截图质量差——这是大多数用户首先看到的内容。确保截图能清晰展示扩展如何解决实际问题。在上线初期积累少量正面评价也会显著提升新访客的转化率。
其他分发方式
对于大多数公开扩展程序来说,Chrome 网上应用店是最佳选择。如果你正在开发内部工具,可以选择发布为 非列表型 扩展(仅可通过直接链接访问,无法被搜索到),这是一个不错的选择。
如果你希望将扩展限制为仅特定 Google Workspace 组织内的用户使用,则可以发布为 私有 扩展。你也可以选择自行托管并侧载安装,但这要求用户手动启用开发者模式,因此只适合技术能力较强的用户群体。
恭喜你!
你已经从一个空文件夹,成功打造出一款上线到 Web Store 的 Chrome 扩展程序。在此过程中,你学习了扩展程序的结构、后台脚本与弹窗之间的通信机制、Chrome 的标签页 API 工作原理,以及如何完整走完整个发布流程。
比起任何具体的 API 或配置细节,你最重要的收获是对扩展程序工作机制建立起的心智模型,这种理解将直接适用于你未来想要实现的任何扩展创意。
继续构建,持续学习,不断发布!
- * *
- * *
免费学习编程。freeCodeCamp 的开源课程已帮助超过 40,000 人成为开发者。立即开始