Skip to content

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.* vs chrome.*)。

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 防样式污染
2AI 辅助阅读 / 翻译插件(类 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 功能设计

  • 目标用户:研究者、学生、产品经理等需要在网页阅读并沉淀笔记的用户。
  • 核心功能
    1. 选中文本后弹出微型工具条,点击「高亮」即可将选区标黄并附加笔记。
    2. 刷新页面后,基于 URL + 文本指纹自动恢复高亮。
    3. Popup 展示当前页面所有高亮列表,支持跳转与删除。
    4. Options 页以卡片形式展示所有网页的笔记,支持搜索与导出 JSON。
    5. Background 负责跨标签页同步(基于 chrome.storage.onChanged)与导出/导入。
  • 交互流程
    • Content Script 监听 mouseup → 显示工具条 → 用户确认 → 写入 storage → 标注 DOM。
    • Popup / Options 通过 useStorage 读写同一份数据,天然同步。

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.tsxoptions.tsxbackground.tscontents/*.tsx 均为 Plasmo 的约定文件名,无需在 manifest.json 中手动注册。

3.3 React 组件组织思路

  • 展示型组件放在 components/,被 Popup、Options 与 Content Script 共用。
  • 业务逻辑(存储/DOM 操作/序列化)下沉到 lib/,保持组件纯粹。
  • 跨端状态统一走 @plasmohq/storageuseStorage Hook,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 install

2)本地开发(带 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 Store

5)调试建议

  • 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-publisher GitHub 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 阅读器等各类扩展。