Chrome Plugin 浏览器插件开发(拓展程序)
项目地址:BrowserExtension By Github
Hello World
初始化一个项目:添加manifest.json文件,再添加一个popup.html文件,这个是插件的入口文件,也就是点击插件图标的时候,就会展示到这个文件
{
"manifest_version": 3,
"name": "modify-plugin",
"version": "1.0.0",
"description": "modify chrome plugin",
"author": "modify",
"action": {
"default_icon": {
"16": "plugin.png",
"32": "plugin.png",
"48": "plugin.png",
"64": "plugin.png",
"128": "plugin.png"
},
"default_popup": "popup.html"
},
"icons": {
"16": "plugin.png",
"32": "plugin.png",
"48": "plugin.png",
"64": "plugin.png",
"128": "plugin.png"
}
}在chrome当中,打开chrome://extensions/,打开开发者模式,加载未打包的扩展程序,选择项目目录,就会自动加载
模块间通信
Content Scripts
content script 是运行在网页上下文(DOM)中的 js 脚本,由浏览器扩展注入,能访问网页的 DOM 结构,但和网页自身的脚本、扩展的后台脚本(background)有严格的隔离
特点:
- 能读写当前网页的 DOM,修改样式、文本、节点等
- 不能访问网页自身的全局变量 / 函数(如网页里定义的 window.xxx)
- 不能直接访问扩展的
chrome.api全部方法(仅能访问 chrome.runtime、chrome.storage 等少数 API) - 可通过
chrome.runtime.sendMessage与扩展的后台脚本通信
manifest.json 配置
{
"content_scripts": [
{
"matches": [
"https://*.example.com/*"
],
"exclude_matches": [
"https://example.com/admin/*"
],
"include_globs": [
"*example.com/*/test*"
],
"exclude_globs": [
"*example.com/*/test/old*"
],
"js": [
"content.js",
"utils.js"
],
"css": [
"content.css"
],
"run_at": "document_end",
"all_frames": false,
"match_about_blank": false,
"world": "ISOLATED",
"match_as_incognito": false
}
]
}| 参数 | 取值 | 说明 |
|---|---|---|
| matches | string | 匹配的URL(支持通配符),<all_urls>表示所有url |
| exclude_matches | string | 排除的URL |
| include_globs | string | 更灵活的匹配(通配符更强) |
| exclude_globs | string | 排除的glob规则 |
| js | string | 注入的JS文件(按顺序执行) |
| css | string | 注入的CSS文件(按顺序执行、先于JS加载) |
| run_at | document_start、document_end、document_idle(默认) | 注入时机 |
| all_frames | boolean | 是否注入到网页的iframe中(默认false,仅主框架) |
| match_about_blank | boolean | 是否注入到about:blank页面(需matches匹配) |
| world | ISOLATED、MAIN | 运行上下文 ISOLATED(隔离,默认) MAIN(网页主上下文,可直接访问网页全局变量,但风险高) |
| match_as_incognito | boolean | 是否在无痕模式中注入(需扩展开启无痕权限) |
核心能力
DOM操作
可完全操控网页DOM,包括添加、删除、修改节点、属性、文本内容,其中监听DOM变化可以使用MutationObserver
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
console.log("DOM变化:", mutation.addedNodes); // 监听新增节点(如广告)
});
});
observer.observe(document.body, {childList: true, subtree: true});如果是vue、react这种动态渲染的页面,不能直接在content.js里面使用document去选择元素,是取不到的,需要监听body的dom变化
const leftTerminal = document.querySelector('#leftTerminal');
console.log('在vue框架,直接取leftTerminal:', leftTerminal);
// 目标元素选择器
const targetSelector = '#leftTerminal';
// 监听DOM变化的函数
function waitForElement(selector, callback) {
// 先检查元素是否已存在
const element = document.querySelector(selector);
if (element) {
callback(element);
return;
}
// 创建DOM观察者
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
// 检查新增的节点中是否包含目标元素
const addedNodes = Array.from(mutation.addedNodes);
for (const node of addedNodes) {
// 节点是元素,且自身/子节点包含目标元素
if (node.nodeType === 1) {
const target = node.matches(selector) ? node : node.querySelector(selector);
if (target) {
observer.disconnect(); // 找到元素后停止监听
callback(target);
return;
}
}
}
});
});
// 配置监听规则:监听body下所有子节点变化
observer.observe(document.body, {
childList: true, // 监听子节点添加/删除
subtree: true, // 监听所有后代节点
attributes: false,
characterData: false
});
}
// 调用函数,等待元素出现后绑定事件
waitForElement(targetSelector, (my_node) => {
console.log('找到目标元素:', my_node);
my_node.addEventListener('click', function (e) {
e.preventDefault();
console.log('我被点了');
});
});还可以在content script里面获取localStorage和httpOnly=false的cookie
样式操作
- 静态注入:通过
manifest.json的css字段注入(优先级高于网页样式) - 动态注入:在
js中创建<style>标签注入
访问网页全局变量
通过配置"world": "MAIN",即可访问网页全局变量
通信机制
在manifest.json中配置background字段,用来指定后台脚本
{
"background": {
"service_worker": "background/background.js"
}
}与后台通信
chrome.runtime.sendMessage({
type: 'TO_BACKGROUND',
data: {
pageUrl: window.location.href,
message: '你好,我是content script TO_BACKGROUND!'
}
}).then(res => {
console.log('TO_BACKGROUND后台回复的消息', res);
}).catch(err => {
console.log('TO_BACKGROUND出错了', err);
});chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// 打印收到的消息和发送者信息
console.log("收到content script的消息:", message);
console.log("发送者信息:", sender); // sender包含标签页、frame等信息
// 验证消息类型(可选,用于区分不同业务的消息)
if (message.type === "TO_BACKGROUND") {
// 构造回复数据
const reply = {
status: "success",
message: "你好,我是service worker!已收到你的消息",
receivedData: message.data // 回显收到的数据
};
// 发送回复(注意:如果是异步操作,需要返回true保持端口开放)
sendResponse(reply);
}
// 🌟 重要:如果 sendResponse 在异步操作中调用,必须返回 true
// setTimeout(() => {
// sendResponse({ status: "success", message: "异步回复" });
// }, 1000);
// return true;
});注:代码提示,因为编辑器不认识chrome这个全局对象,安装
npm i -D @types/chrome
与popup通信
和与background通信一样,但是popup必须是点开状态
const head = document.querySelector('.head_wrapper');
console.log('head:', head);
head.addEventListener('click', function (e) {
e.preventDefault();
console.log('我被点了');
chrome.runtime.sendMessage({
type: 'TO_POPUP',
data: {
pageUrl: window.location.href,
message: '你好,我是content script TO_POPUP!',
time: new Date().toLocaleString()
}
}).then(res => {
console.log('TO_POPUP后台回复的消息', res);
}).catch(err => {
console.log('TO_POPUP出错了', err);
});
});chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('popup 收到消息:', message);
// 判断消息类型,处理不同逻辑
if (message.type === 'TO_POPUP') {
// 模拟处理逻辑(比如读取插件存储、调用API等)
const replyContent = `已收到你的消息「${message.data.message}」,时间:${message.data.time}`;
// 向Content script发送响应(同步回复)
sendResponse({
success: true, reply: replyContent
});
}
});chrome storage
storage有三钟,sync,local,session。只要用 chrome.storage,必须先在 manifest.json 中声明 storage 权限
{
"permissions": [
"storage"
]
}区别
| 特性维度 | chrome.storage.sync | chrome.storage.local | chrome.storage.session |
|---|---|---|---|
| 存储位置 | 云端 + 本地(同步到 Chrome 账号) | 仅本地设备 | 内存(临时),插件进程结束即销毁 |
| 数据持久化 | 永久(除非手动删除 / 同步失败) | 永久(除非手动删除 / 清除浏览器数据) | 临时(插件关闭 / 浏览器重启后丢失) |
| 存储大小限制 | 约 100KB(总数据),单条 8KB | 约 5MB(可通过扩展权限提升) | 约 10MB(内存级,无严格硬限制) |
| 同步特性 | 多设备同步(登录同一 Chrome 账号) | 仅当前设备,不同步 | 仅当前浏览器会话,不同步 |
| 适用场景 | 跨设备共享的轻量配置(如主题偏好、账号设置) | 单设备的大量数据(如本地缓存、历史记录) | 临时状态(如弹窗临时数据、会话级缓存) |
| 读写性能 | 较慢(需云端同步) | 较快(纯本地) | 极快(内存操作) |
读写操作【get、set、remove】
const storageChange = async () => {
await chrome.storage.local.set({'name': 'modify', 'age': 18});
const {name} = await chrome.storage.local.get('name');
console.log('name:', name);
// await chrome.storage.sync.clear();
await chrome.storage.local.remove('name');
await chrome.storage.local.remove(['name', 'age']);
};
chrome.storage.onChanged.addListener((changes, areaName) => {
console.log(`存储区域${areaName}发生变化:`, changes);
});
storageChange();Popup
popup就是弹出层的页面,一般是用于插件的配置
popup.html只能引用扩展包内的本地文件,不能用远程地址,默认不能内嵌脚本,内联事件- 网络请求需要在
manifest.json中配置host_permissions权限 - 生命周期短,只有点击扩展图标时,popup 才会加载,点击其他地方,popup 会立即销毁,所以不要在 popup 中写长时任务(比如定时器、轮询),销毁后会直接停止,这类逻辑要放到service worker中
- popup 只能操作自己的 DOM,无法直接操作当前网页的 DOM(需要通过 content script 实现)
options页面
在manifest.json中配置,右键插件图标,点击选项就可以进到options页面了
{
"options_ui": {
"page": "popup/options.html",
"open_in_tab": true
}
}单击跳转
点击浏览器插件图标跳转options页面,这个时候不能配置default_popup,在background.js中监听插件图标的点击事件
chrome.action.onClicked.addListener(() => {
// 打开插件的options页面
chrome.runtime.openOptionsPage().catch(async (error) => {
// 异常处理:如果openOptionsPage失败,手动拼接URL打开
console.error('打开选项页面失败:', error);
const optionsUrl = chrome.runtime.getURL('popup/options.html');
await chrome.tabs.create({url: optionsUrl});
});
});openOptionsPage() 是打开插件配置的options页面,也可以用chrome.runtime.getURL获取插件本地html的路径,然后用chrome.tabs.create去打开
通信机制
与content script通信
点击popup的时候,再去执行content script
document.addEventListener('DOMContentLoaded', () => {
const sendBtn = document.getElementById('btn-content');
sendBtn.addEventListener('click', async () => {
try {
// 1. 获取当前激活的标签页(需要tabs权限)
const [activeTab] = await chrome.tabs.query({
active: true,
currentWindow: true
});
if (!activeTab.id) {
console.warn("无法获取当前标签页ID")
return;
}
// 2. 向content script发送消息
const response = await chrome.tabs.sendMessage(activeTab.id, {
type: 'POPUP_MESSAGE',
data: '这是来自Popup的消息',
timestamp: new Date().getTime()
});
// 3. 接收content script的响应并提示
console.log(`Content Script响应:${response.message}`)
} catch (error) {
// 常见错误:当前页面没有注入content script
console.error(`Content Script响应:${error.message}`)
}
});
});chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('收到来自Popup的消息:', message);
// 判断消息类型并处理
if (message.type === 'POPUP_MESSAGE') {
// 模拟处理逻辑
const replyMessage = `已收到消息:${message.data},当前页面标题是「${document.title}」`;
// 向Popup发送响应(sendResponse必须同步调用,异步需返回true)
sendResponse({
success: true,
message: replyMessage
});
}
// 如果需要异步处理,需返回true(比如调用API后再回复)
// return true;
});与service worker通信
popup这边直接通过chrome.runtime.sendMessage发送消息,而service那边和前面content与后台通信 一样,加一个else if判断即可
const sendBtn2 = document.getElementById('btn-service');
sendBtn2.addEventListener('click', async () => {
try {
// 2. 向content script发送消息
const response = await chrome.runtime.sendMessage( {
type: 'POPUP_MESSAGE',
data: {
message: '你好,Service Worker!',
time: new Date().toLocaleTimeString()
}
});
// 3. 接收content script的响应并提示
console.log("Service worker 响应", response)
} catch (error) {
// 常见错误:当前页面没有注入content script
console.error("Service worker 响应失败", error)
}
});if (message.type === 'POPUP_MESSAGE') {
const replyContent = `已收到你的消息「${message.data.message}」,时间:${message.data.time}`;
// 向popup发送响应(同步回复)
sendResponse({
success: true,
reply: replyContent
});
}Service Worker
特点
- 它是一个无界面、轻量级的后台脚本(对应 background.js),独立于 popup、网页标签页运行
- 它没有 DOM 访问权限(不能用 document/window),也不能操作页面元素
- 它是事件驱动的:只有触发特定事件(如扩展安装、消息接收、定时器)时才会激活,闲置一段时间(约 30 秒)后会自动休眠,事件触发时又会唤醒
核心功能(能做什么)
- 监听 / 响应扩展生命周期事件(安装、更新、启动)
- 处理扩展内组件通信(popup ↔ background ↔ content script)
- 执行后台任务(定时器、网络请求、数据存储)
- 监听 / 操作浏览器资源(标签页、书签、下载、通知)
- 管理扩展权限和状态
生命周期
| 方法 / 事件 | 触发时机 |
|---|---|
| install | Service Worker 首次安装 / 更新时触发(仅一次),可用于缓存静态资源 |
| activate | Service Worker 安装完成后激活触发,可用于清理旧缓存、初始化状态 |
| fetch | 扩展内发起网络请求时触发(可拦截、修改请求 / 响应) |
| message | 接收来自内容脚本 / 弹窗页面的消息时触发 |
| runtime.onStartup | 浏览器启动时触发(若扩展已启用) |
| runtime.onInstalled | 扩展首次安装、更新或浏览器更新后首次加载时触发(比 install 更通用) |
| runtime.onMessage | 等同于 message,是 Manifest V3 更推荐的消息监听方式 |
| self.clients.claim() | 激活后调用,让 Service Worker 立即控制所有已打开的扩展相关页面 |
chrome.runtime.onInstalled.addListener((details) => {
console.log('插件已安装或更新:', details);
});
chrome.runtime.onStartup.addListener(() => {
console.log('插件已启动');
});通信机制
与popup通信
service worker向 popup 发消息时,仅当 popup 处于打开状态才能收到- 如果 popup 已关闭,消息会丢失(可通过chrome.storage暂存消息,popup 打开时读取)
service worker使用chrome.runtime.sendMessage发消息,popup用chrome.runtime.onMessage.addListener接收消息
与content script通信
service worker和content script的通信依赖标签页 ID(因为 content script 绑定到具体标签页)service worker用chrome.tabs.sendMessage(tabId, message)(指定标签页发送)
右键菜单
首先必须声明右键菜单权限,在manifest.json中添加"permissions"权限
contextMenus:必须声明右键菜单权限activeTab:可选,用于操作当前标签页storage:可选,用于保存菜单状态
{
"permissions": [
"storage",
"contextMenus",
"activeTab"
]
}在onInstalled生命周期当中先清除所有菜单,再创建菜单,通过chrome.contextMenus.onClicked用来监听菜单点击事件
chrome.runtime.onInstalled.addListener(async (details) => {
await chrome.contextMenus.removeAll();
chrome.contextMenus.create({
id: 'menu_base', title: '🔧 我的插件功能', contexts: ['all']
});
chrome.contextMenus.create({
id: 'menu_print', title: '📝 打印当前页面URL', parentId: 'menu_base', contexts: ['all']
});
chrome.contextMenus.create({
id: 'menu_selected', title: '🔍 搜索选中的文本:%s', contexts: ['selection']
});
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
console.log('右键菜单被点击:', {info, tab});
const {menuItemId} = info;
const {title, url} = tab;
if (menuItemId === 'menu_print') {
console.log('打印当前页面URL:', url, title);
}
});
});定时任务
添加权限
{
"permissions": [
"alarms"
]
}使用
chrome.runtime.onInstalled.addListener(async () => {
await chrome.alarms.clearAll();
await chrome.alarms.create('myAlarm1', {
delayInMinutes: 5 / 60 //单位为分钟,5秒后执行
});
await chrome.alarms.create('myAlarm2', {
when: Date.now() + 5000 // 指定时间戳,5秒后执行
});
await chrome.alarms.create('myAlarm3', {
periodInMinutes: 1 // 每1分钟执行一次
});
const allAlarms = await chrome.alarms.getAll();
console.log('所有定时任务:', allAlarms);
const alarm1 = await chrome.alarms.get('myAlarm1');
console.log('定时任务1:', alarm1);
});
chrome.alarms.onAlarm.addListener(async (alarm) => {
console.log('定时任务执行:', alarm);
});plasmo
Plasmo 是专为现代浏览器扩展开发设计的框架,能极大简化 manifest v3 扩展的开发流程,解决原生开发中的配置繁琐、打包复杂等问题
- 零配置开箱即用:内置
Webpack/Vite打包、ts支持、热重载,无需手动配置manifest.json - 简化
Service Worker开发:统一的背景脚本(Background)开发体验,无需关注Service Worker生命周期细节 - 原生支持
React/Vue:无缝集成前端框架,开发弹窗、选项页更高效 - 自动处理权限 / 清单:通过代码注释或配置自动生成
manifest.json,无需手动维护 - 跨浏览器兼容:一键适配 Chrome、Edge、Firefox 等主流浏览器
项目初始化
默认初始化是React项目,可以通过--with-vue参数初始化为Vue项目
pnpm create plasmo plasmo-plugin
pnpm create plasmo plasmo-plugin --with-vue