Plasmo 浏览器扩展开发框架:能力解析、典型案例与实战示例
一、Plasmo 框架核心能力与优势
Plasmo 是一个面向现代 Web 开发者的「Browser Extension Framework」,其定位类似于浏览器扩展领域的 Next.js —— 用约定式的项目结构 + 零配置的脚手架,让开发者专注业务,而不是和 manifest.json、WebExtension API、打包脚本搏斗。
1. 零配置脚手架与约定式结构
- 一键创建项目:通过
pnpm create plasmo即可生成标准项目骨架。 - 约定优于配置:文件名即路由/入口。例如
popup.tsx自动成为扩展的 Popup,options.tsx自动成为 Options 页面,background.ts自动成为 Service Worker,contents/*.tsx自动成为 Content Script。 - 自动生成 manifest:开发者不再手写
manifest.json,Plasmo 根据文件和package.json配置自动合成 MV3/MV2 清单。
2. 多浏览器多目标产物
- 一次源码,多端构建:支持 Chrome、Firefox、Edge、Brave、Opera 等主流浏览器。
- 通过
--target=chrome-mv3 | firefox-mv2 | edge-mv3等 target 参数输出不同浏览器对应的产物包。 - 自动处理各浏览器间 manifest 和 API 的差异(如 MV2/MV3、
browser.*vschrome.*)。
3. 原生 React / TypeScript / CSS 生态
- 开箱即用支持 React、TypeScript、JSX/TSX。
- 支持 Tailwind CSS、CSS Modules、Sass、PostCSS,可与现代前端工具链无缝对接。
- 内容脚本也可以写成 React 组件,直接在目标页面渲染 UI(可选 Shadow DOM 隔离)。
4. 完善的开发体验(HMR / Live Reload)
- 热模块替换(HMR):修改 Popup/Options/Content Script 代码后,扩展会自动重新加载,无需手动点「刷新扩展」。
- 内置 TypeScript 类型(含
chrome.*API 类型),智能提示友好。 - 与 Chrome DevTools、React DevTools 联动良好。
5. 内置常用能力封装
@plasmohq/storage:对chrome.storage的封装,支持 React Hook(useStorage)响应式读写。@plasmohq/messaging:类型安全的 Background ↔ Content ↔ Popup 消息通信,类似定义 API 路由。@plasmohq/redux-persist、@plasmohq/persist-context:状态持久化方案。- 对 i18n、内容脚本 CSS 注入、Shadow DOM 组件、远程代码加载等有官方方案。
6. 打包、发布与 CI
plasmo build输出生产包,按 target 生成build/chrome-mv3-prod等目录。plasmo package自动打成.zip。- 官方 Browser Platform Publisher(BPP)GitHub Action 支持一键发布到 Chrome Web Store、Firefox Add-ons、Edge Add-ons 等市场。
二、典型使用场景与案例
| # | 场景 | 解决的问题 | 为什么适合 Plasmo |
|---|---|---|---|
| 1 | 网页高亮 & 笔记工具(类 Liner / Weava) | 在任意网页划词高亮、做笔记并同步云端 | Content Script 用 React 写 UI,useStorage 直接同步笔记,Shadow DOM 防样式污染 |
| 2 | AI 辅助阅读 / 翻译插件(类 Immersive Translate、Monica) | 调用 LLM 为网页提供翻译、总结、问答 | Background Service Worker 管理 API Key 与请求,Messaging 在 Popup/Content 间流转数据 |
| 3 | 电商比价 / 历史价格助手(类 Honey、慢慢买) | 在商品页注入比价浮层与历史价格曲线 | Content Script 精准匹配域名注入,React 渲染图表组件 |
| 4 | 开发者工具类扩展(API 测试、Cookie 管理、JSON 格式化) | 提升研发效率 | Options 页做复杂配置,Popup 快速操作,TypeScript 保证类型安全 |
| 5 | 生产力 / 工作流(稍后读、书签增强、新标签页替换) | 重塑浏览器首页或信息收集流 | newtab.tsx 约定直接替换新标签页,React 组件化开发非常顺手 |
三、实战案例:基于 Plasmo 的「网页高亮与笔记」扩展
下面以一个实用且功能闭环的方向作为示范 —— WebMarker:在任意网页划词高亮并保存笔记,Popup 查看本页所有高亮,Options 页管理全部笔记。
3.1 功能设计
- 目标用户:研究者、学生、产品经理等需要在网页阅读并沉淀笔记的用户。
- 核心功能:
- 选中文本后弹出微型工具条,点击「高亮」即可将选区标黄并附加笔记。
- 刷新页面后,基于 URL + 文本指纹自动恢复高亮。
- Popup 展示当前页面所有高亮列表,支持跳转与删除。
- Options 页以卡片形式展示所有网页的笔记,支持搜索与导出 JSON。
- Background 负责跨标签页同步(基于
chrome.storage.onChanged)与导出/导入。
- 交互流程:
- Content Script 监听
mouseup→ 显示工具条 → 用户确认 → 写入 storage → 标注 DOM。 - Popup / Options 通过
useStorage读写同一份数据,天然同步。
- Content Script 监听
3.2 目录结构
web-marker/
├─ package.json
├─ tsconfig.json
├─ assets/
│ └─ icon.png
├─ background.ts # Service Worker(MV3)
├─ popup.tsx # 点击图标弹出的 Popup
├─ options.tsx # 扩展设置与全部笔记管理页
├─ newtab.tsx # (可选) 自定义新标签页
├─ contents/
│ ├─ highlighter.tsx # 注入网页的高亮 UI(React + Shadow DOM)
│ └─ highlighter.css # 高亮样式
├─ components/
│ ├─ HighlightToolbar.tsx # 选区工具条
│ ├─ NoteEditor.tsx # 笔记编辑弹层
│ └─ HighlightList.tsx # 高亮列表(Popup/Options 复用)
├─ lib/
│ ├─ storage.ts # 存储 schema 与封装
│ ├─ messaging.ts # 消息类型定义
│ └─ highlight.ts # DOM Range 序列化与恢复
└─ messages/
└─ export-notes.ts # @plasmohq/messaging 的 handler说明:
popup.tsx、options.tsx、background.ts、contents/*.tsx均为 Plasmo 的约定文件名,无需在manifest.json中手动注册。
3.3 React 组件组织思路
- 展示型组件放在
components/,被 Popup、Options 与 Content Script 共用。 - 业务逻辑(存储/DOM 操作/序列化)下沉到
lib/,保持组件纯粹。 - 跨端状态统一走
@plasmohq/storage的useStorageHook,Popup / Options / Content 三端数据实时一致。
3.4 关键代码示例
① 存储与类型定义(lib/storage.ts)
ts
import { Storage } from "@plasmohq/storage"
export interface Highlight {
id: string
url: string
text: string
note?: string
xpath: string // 选区定位
startOffset: number
endOffset: number
createdAt: number
}
export const storage = new Storage({ area: "local" })
export const HIGHLIGHTS_KEY = "highlights"
export async function addHighlight(h: Highlight) {
const list = (await storage.get<Highlight[]>(HIGHLIGHTS_KEY)) ?? []
await storage.set(HIGHLIGHTS_KEY, [...list, h])
}
export async function removeHighlight(id: string) {
const list = (await storage.get<Highlight[]>(HIGHLIGHTS_KEY)) ?? []
await storage.set(HIGHLIGHTS_KEY, list.filter((x) => x.id !== id))
}② Content Script(contents/highlighter.tsx)
Plasmo 支持用 TSX 写内容脚本,通过 config 指定匹配规则,并可将组件挂载到自带的 Shadow DOM 容器中,避免样式被目标网页污染。
tsx
import type { PlasmoCSConfig, PlasmoGetInlineAnchor } from "plasmo"
import { useEffect, useState } from "react"
import { useStorage } from "@plasmohq/storage/hook"
import HighlightToolbar from "~components/HighlightToolbar"
import { HIGHLIGHTS_KEY, addHighlight, type Highlight } from "~lib/storage"
import { serializeRange, restoreHighlights } from "~lib/highlight"
import cssText from "data-text:./highlighter.css"
export const config: PlasmoCSConfig = {
matches: ["<all_urls>"],
run_at: "document_idle"
}
// 将 CSS 注入 Shadow DOM
export const getStyle = () => {
const style = document.createElement("style")
style.textContent = cssText
return style
}
const Highlighter = () => {
const [toolbar, setToolbar] = useState<{ x: number; y: number } | null>(null)
const [highlights] = useStorage<Highlight[]>(HIGHLIGHTS_KEY, [])
// 1. 恢复已有高亮
useEffect(() => {
restoreHighlights(highlights.filter((h) => h.url === location.href))
}, [highlights])
// 2. 监听选区
useEffect(() => {
const onMouseUp = () => {
const sel = window.getSelection()
if (!sel || sel.isCollapsed || !sel.toString().trim()) {
setToolbar(null)
return
}
const rect = sel.getRangeAt(0).getBoundingClientRect()
setToolbar({ x: rect.left + window.scrollX, y: rect.bottom + window.scrollY + 6 })
}
document.addEventListener("mouseup", onMouseUp)
return () => document.removeEventListener("mouseup", onMouseUp)
}, [])
const handleHighlight = async (note?: string) => {
const sel = window.getSelection()
if (!sel || sel.isCollapsed) return
const range = sel.getRangeAt(0)
const meta = serializeRange(range)
const h: Highlight = {
id: crypto.randomUUID(),
url: location.href,
text: sel.toString(),
note,
...meta,
createdAt: Date.now()
}
await addHighlight(h)
sel.removeAllRanges()
setToolbar(null)
}
if (!toolbar) return null
return (
<HighlightToolbar
style={{ position: "absolute", top: toolbar.y, left: toolbar.x }}
onHighlight={handleHighlight}
/>
)
}
export default Highlighter
~components/...是 Plasmo 内置的路径别名,等价于src/components/...(或项目根)。
③ Popup 页面(popup.tsx)
tsx
import { useEffect, useState } from "react"
import { useStorage } from "@plasmohq/storage/hook"
import { HIGHLIGHTS_KEY, removeHighlight, type Highlight } from "~lib/storage"
function IndexPopup() {
const [highlights] = useStorage<Highlight[]>(HIGHLIGHTS_KEY, [])
const [currentUrl, setCurrentUrl] = useState("")
useEffect(() => {
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
setCurrentUrl(tab?.url ?? "")
})
}, [])
const pageHighlights = highlights.filter((h) => h.url === currentUrl)
return (
<div style={{ width: 320, padding: 12, fontFamily: "system-ui" }}>
<h3 style={{ margin: "0 0 8px" }}>本页高亮 ({pageHighlights.length})</h3>
{pageHighlights.length === 0 && <p>在网页上划词后点击「高亮」即可添加。</p>}
<ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
{pageHighlights.map((h) => (
<li key={h.id} style={{ padding: 8, borderBottom: "1px solid #eee" }}>
<div style={{ background: "#fff59d", padding: 4 }}>{h.text}</div>
{h.note && <small style={{ color: "#666" }}>📝 {h.note}</small>}
<button
onClick={() => removeHighlight(h.id)}
style={{ float: "right", fontSize: 12 }}>
删除
</button>
</li>
))}
</ul>
<a href="options.html" target="_blank" style={{ display: "block", marginTop: 12 }}>
打开管理页 →
</a>
</div>
)
}
export default IndexPopup④ Background Service Worker(background.ts)
Background 负责:扩展图标点击行为、快捷键、跨标签页广播、导出导入等全局逻辑。
ts
import { Storage } from "@plasmohq/storage"
import { HIGHLIGHTS_KEY, type Highlight } from "~lib/storage"
const storage = new Storage({ area: "local" })
// 1. 安装时初始化
chrome.runtime.onInstalled.addListener(async () => {
const exist = await storage.get<Highlight[]>(HIGHLIGHTS_KEY)
if (!exist) await storage.set(HIGHLIGHTS_KEY, [])
console.log("[WebMarker] installed.")
})
// 2. 注册右键菜单
chrome.contextMenus.create({
id: "webmarker-highlight",
title: "用 WebMarker 高亮选中文本",
contexts: ["selection"]
})
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "webmarker-highlight" && tab?.id) {
chrome.tabs.sendMessage(tab.id, { type: "HIGHLIGHT_FROM_MENU" })
}
})
// 3. 监听来自 Popup 的导出请求(使用 @plasmohq/messaging 也可以)
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg?.type === "EXPORT_NOTES") {
storage.get<Highlight[]>(HIGHLIGHTS_KEY).then((list) => {
sendResponse({ ok: true, data: list ?? [] })
})
return true // 异步响应
}
})⑤ package.json 中的 Plasmo 配置片段
json
{
"name": "web-marker",
"displayName": "WebMarker",
"version": "0.1.0",
"scripts": {
"dev": "plasmo dev",
"build": "plasmo build",
"package": "plasmo package"
},
"dependencies": {
"plasmo": "latest",
"react": "18.2.0",
"react-dom": "18.2.0",
"@plasmohq/storage": "latest",
"@plasmohq/messaging": "latest"
},
"manifest": {
"permissions": ["storage", "activeTab", "contextMenus", "scripting"],
"host_permissions": ["<all_urls>"]
}
}
manifest字段会被 Plasmo 合并进最终生成的manifest.json,你不需要手动维护整份清单。
3.5 开发、调试与打包流程
1)创建项目
bash
pnpm create plasmo web-marker --with-src
cd web-marker
pnpm install2)本地开发(带 HMR)
bash
pnpm dev
# 默认输出到 build/chrome-mv3-dev在 Chrome 中打开 chrome://extensions → 打开「开发者模式」→「加载已解压的扩展程序」→ 选择 build/chrome-mv3-dev。之后修改代码,扩展会自动刷新。
3)针对不同浏览器构建
bash
pnpm dev --target=firefox-mv2 # 开发 Firefox 版本
pnpm build --target=edge-mv3 # 打包 Edge 生产版
pnpm build --target=chrome-mv3 # 打包 Chrome 生产版4)打成上架用的 zip
bash
pnpm package
# 生成 build/chrome-mv3-prod.zip,可直接上传到 Chrome Web Store5)调试建议
- Popup:在弹窗上右键「检查」打开 DevTools。
- Background(Service Worker):
chrome://extensions中点击扩展的「检查视图:Service Worker」。 - Content Script:直接在所在页面 F12,Sources 面板可看到 Plasmo 注入的 content 脚本,可打断点。
- 搭配 React DevTools 扩展可以像调试普通 React 应用一样调试 Popup / Options / Content UI。
6)发布
- Chrome Web Store:在开发者后台上传
chrome-mv3-prod.zip,首次上架需支付一次性注册费并通过审核。 - Firefox Add-ons / Edge Add-ons:分别使用对应 target 产物上传。
- CI 自动发布:使用 Plasmo 官方的
browser-platform-publisherGitHub Action,可在 push tag 后自动同步发布到多个商店。
四、小结
- Plasmo 的价值在于把浏览器扩展的开发体验拉齐到现代前端水平:约定式结构、React/TS 原生支持、HMR、多浏览器构建、一键发布。
- 适用边界:偏 UI、偏业务逻辑的扩展(AI 助手、高亮笔记、比价工具、生产力工具)用 Plasmo 非常顺手;对极少数深度依赖特殊 manifest 细节或需要完全定制构建管线的场景,仍可通过自定义
plasmo配置或 parcel 插件进行扩展。 - 上文的 WebMarker 示例 已覆盖 Popup、Options、Content Script(React + Shadow DOM)、Background Service Worker、
@plasmohq/storage状态同步与 CLI 全流程,可直接作为你上手 Plasmo 的「脚手架级」参考项目,按需替换业务逻辑即可演进成翻译助手、比价插件、AI 阅读器等各类扩展。