源码分享 | 从0到1开发一个浏览器插件
0、前言
本文以文本采集插件为例,基于WXT框架,讲解三大核心组件(后台脚本、内容脚本、弹出页)的分工与通信机制,实现了从项目搭建到功能落地的全过程。
1、相关概念
1.1 什么是浏览器插件
浏览器插件(或扩展)是小型的软件组件,用于扩展浏览器功能,通过Web技术(HTML, CSS, JS)实现,让浏览体验更个性化和强大。

1.2 浏览器插件可以做什么
能帮你屏蔽广告、翻译网页、管理密码、美化界面、提高效率(如截图、笔记、抢票)

2、组件说明
2.1 概述
浏览器扩展的架构独特且多面,它并非集中式结构,而是由分布式元素网络构成。
包括后台脚本、内容脚本、弹出页和选项页面以及页面。这些元素协同工作,管理跨多个窗口和标签的复杂浏览器页面基础设施。
下面简化插件的开发内容,分为background(后台脚本)、content(内容脚本)、popup(弹出页)
2.1 组件分布

2.2 组件职能
在Chrome插件开发中,background、content和popup是核心组成部分,各自承担不同职责并协同工作。
后台脚本是扩展的大脑和指挥中心,权限最高,负责全局监听和协调。
内容脚本是扩展深入网页内部的“手”,能直接修改页面内容。
弹出页是扩展的临时控制面板,为用户提供快速交互的界面。
从操作对象、生命周期、操作权限来看后台脚本 、内容脚本、弹出页的区别。

那如果在内容脚本,要操作高权限的api,那就让内容区脚本去找后台脚本,由后台脚本来实现。

接下来就引出了后面组件的通信。
2.3 组件通信
先说结论:后台脚本、内容脚本、弹出页能互相通信,3个组件存在6种通信路径,不用记,用的时候查。
https://juejin.cn/post/6844903985711677453#heading-12

3、开发框架
3.1 主流框架对比
浏览器扩展开发领域正在快速进化。本文将从 GitHub 人气、上手体验、云服务支持、MVVM 框架兼容性、工程化能力和社区生态六大维度,完整呈现三大框架的差异 , 并分析各自更适合的场景。
https://segmentfault.com/a/1190000046364405

3.2 WXT简介
WXT是一个免费的开源浏览器插件开发框架,它致力于为开发者带来最好的开发体验和最快的开发速度,学习它可以为你的插件搭建一个坚实的基础,并为你节省大量的基础建设时间。
https://wxt.dev/

4、需求
4.1 content上实现的功能
划线、获取当前url,保存的浏览器内存

4.2 popup实现的功能
用列表展示记录内容,点击详情,跳转到原页面

5、实现
5.1 初始化项目
npm i -D wxt
5.2 content

export default defineContentScript({matches: ["<all_urls>"],main() {let actionButton: HTMLButtonElement | null = null;const STORAGE_KEY = "local:saved_texts";const contentObject = {text: "",url: "",};document.addEventListener("mouseup", (e: MouseEvent) => {const selection = window.getSelection();const _text = selection?.toString().trim() as string;const _currentURL = window.location.href;contentObject.text = _text;contentObject.url = _currentURL;if (!_text || (actionButton && actionButton.contains(e.target as Node))) {if (!_text) removeButton();return;}createButton(e.pageX, e.pageY, contentObject);});function createButton(x: number, y: number, contentObject: any) {removeButton();actionButton = document.createElement("button");actionButton.innerText = "➕ 收藏文本";// 精美样式Object.assign(actionButton.style, {position: "absolute",left: `${x + 10}px`,top: `${y + 10}px`,zIndex: "2147483647",padding: "8px 16px",backgroundColor: "#6366f1",color: "white",border: "none",borderRadius: "8px",cursor: "pointer",boxShadow: "0 4px 12px rgba(99, 102, 241, 0.3)",fontSize: "13px",fontWeight: "bold",transition: "all 0.2s",});actionButton.onclick = async (ev) => {ev.stopPropagation();// 1. 读取旧数据const current = (await storage.getItem<string[]>(STORAGE_KEY)) || [];// 2. 存入新数据await storage.setItem(STORAGE_KEY, [contentObject, ...current]);actionButton!.innerText = "✅ 已添加";actionButton!.style.backgroundColor = "#10b981";setTimeout(removeButton, 800);};document.body.appendChild(actionButton);}function removeButton() {actionButton?.remove();actionButton = null;}document.addEventListener("mousedown", (e) => {if (actionButton && !actionButton.contains(e.target as Node))removeButton();});},});
5.3 popup

import { useState, useEffect } from "react";import "./App.css";const STORAGE_KEY = "local:saved_texts";function App() {const [list, setList] = useState<any[]>([]);// 1. 初始化加载数据useEffect(() => {const loadData = async () => {const data = await storage.getItem<string[]>(STORAGE_KEY);setList(data || []);};loadData();// 2. 核心:监听存储变化 (当 content script 写入新数据时,这里会自动触发)const unwatch = storage.watch<string[]>(STORAGE_KEY, (newList) => {setList(newList || []);});// 组件卸载时取消监听return () => unwatch();}, []);const handleClear = async () => {await storage.setItem(STORAGE_KEY, []);};// Function to truncate text at 200 charactersconst truncateText = (text: string, maxLength: number = 200) => {return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;};return (<divclassName="container"><headerclassName="header"><h1>📖 采集列表</h1><buttononClick={handleClear}className="clear-btn">清空</button></header><mainclassName="list-container">{list.length === 0 ? (<divclassName="empty">暂无数据,请在网页选中文本</div>) : (<ulclassName="item-list">{list.map((item, index) => (<likey={index}className="list-item"onClick={() => {//@ts-ignorechrome.tabs.create({ url: item.url });}}>{truncateText(item.text)}</li>))}</ul>)}</main></div>);}export default App;
5.4 打包

5.5 源码
代码已共享
https://gitee.com/jlk1912/wxtSample
6、使用
6.1 引入插件

6.2 功能使用


7、总结
1、在popup和content组件中虽然可以获取浏览器对象,但是操作权限有限
2、浏览器插件是一个前端组件,插件前端通过网络请求与后端通信,实现更强大的功能。
3、chrome浏览器与edge浏览器的内核都是chrome内核,所以插件兼容
8、参考
生命周期:https://blog.csdn.net/heeheeai/article/details/142622784
手把手教学开发浏览器插件:https://zhuanlan.zhihu.com/p/16590557449
组件通讯:https://juejin.cn/post/6844903985711677453#heading-12
控制区域:https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface
夜雨聆风
