浏览器扩展
浏览器扩展(Browser Extension)是一种可以修改和增强浏览器功能的小型软件程序。它们使用 Web 技术(HTML、CSS、JavaScript)开发,可以在用户浏览网页时提供额外功能。
典型应用场景
| 类型 | 示例 | 技术要点 |
|---|---|---|
| 内容增强 | 广告拦截器(AdBlock)、语法检查(Grammarly) | Content Scripts、DOM 操作 |
| 生产力工具 | 密码管理器、笔记工具、截图工具 | Storage API、Context Menu |
| 数据获取 | 价格追踪、天气查询、新闻订阅 | Fetch API、定时任务 |
| 页面修改 | 暗黑模式、字体替换、样式调整 | CSS 注入、动态 DOM |
| 开发工具 | React DevTools、Vue DevTools | DevTools API、消息传递 |
| 标签管理 | OneTab、Session Buddy | Tabs API、Bookmarks API |
扩展的优势
- ✅ 直接访问浏览器 API:比网页应用有更强的能力
- ✅ 无需服务器:可以完全离线运行
- ✅ 跨页面工作:可以在所有标签页中生效
- ✅ 持久化存储:使用浏览器提供的存储 API
- ✅ 用户基数大:Chrome Web Store 和 Firefox Add-ons 有庞大用户群
核心概念
1. Manifest 文件:扩展的身份证
Manifest 是扩展的核心配置文件,定义了扩展的所有信息、权限和行为。
基础结构:
json
{
"manifest_version": 3,
"name": "我的扩展",
"version": "1.0.0",
"description": "这是一个示例扩展",
"icons": {
"16": "icon-16.png",
"48": "icon-48.png",
"128": "icon-128.png"
},
"permissions": ["storage", "activeTab"],
"host_permissions": ["https://*/*"],
"action": {
"default_popup": "popup.html",
"default_icon": "icon-32.png",
"default_title": "点击打开"
}
}
Manifest主要有v2和v3两种,现在以 v3 为主
Manifest V2 vs V3 主要差异:
| 特性 | Manifest V2 | Manifest V3 |
|---|---|---|
| 后台脚本 | background.page / background.scripts |
background.service_worker |
| 网络请求 | webRequest API(同步阻塞) |
declarativeNetRequest(声明式) |
| 代码执行 | executeScript |
scripting.executeScript |
| 远程代码 | 允许 | 禁止(必须打包在扩展内) |
| 安全性 | 一般 | 更强(CSP 更严格) |
2. 五大核心组件详解
组件架构图

- Background Script (后台脚本): 扩展的核心,持续运行在后台,处理事件和协调其他组件
- Popup (弹窗UI): 用户点击扩展图标时显示的界面
- Content Script (内容脚本): 注入到网页中的脚本,可以访问和修改页面DOM
- Options Page (选项页面): 扩展的设置页面,用于配置选项
- Web Page DOM: 实际网页的文档对象模型,Content Script可以直接操作
组件对比表
| 组件 | 运行环境 | 生命周期 | 能访问 DOM | 能使用 Chrome API | 通信方式 |
|---|---|---|---|---|---|
| Background | 独立上下文 | 长期/事件触发 | ❌ | ✅ 全部 | runtime.sendMessage |
| Content Script | 网页上下文 | 随页面加载 | ✅ 页面 DOM | ⚠️ 部分 | runtime.sendMessage |
| Popup | 独立窗口 | 弹窗打开时 | ✅ 自身 DOM | ✅ 全部 | 直接调用或消息 |
| Options | 独立页面 | 设置页打开时 | ✅ 自身 DOM | ✅ 全部 | 直接调用或消息 |
| DevTools | DevTools 面板 | DevTools 打开时 | ⚠️ 通过 inspectedWindow | ✅ 部分 | runtime.connect |
3. 权限系统
权限类型
基本权限(permissions):
json
{
"permissions": [
"storage", // 使用 chrome.storage API
"activeTab", // 访问当前活动标签页
"tabs", // 访问标签页信息(URL、标题等)
"notifications", // 显示系统通知
"alarms", // 定时任务
"contextMenus", // 右键菜单
"cookies", // 访问 Cookie
"downloads", // 管理下载
"bookmarks", // 管理书签
"history" // 访问历史记录
]
}
主机权限(host_permissions):
json
{
"host_permissions": [
"https://api.example.com/*", // 特定域名
"https://*/*", // 所有 HTTPS 网站
"<all_urls>" // 所有网站(需谨慎)
]
}
权限最小化原则
⚠️ 重要:只请求必要的权限,过多权限会:
- 降低用户信任度
- 增加审核难度
- 影响商店排名
示例:
json
// ❌ 不好:请求了不必要的权限
{
"permissions": ["tabs", "history", "bookmarks", "<all_urls>"]
}
// ✅ 好:只请求实际使用的权限
{
"permissions": ["storage", "activeTab"]
}
项目架构与技术栈
技术选型
笔者推荐直接使用现成的开发模板 github.com/JohnBra/vit...
以 vite-web-extension 模板开发的每日金价小例子
1. 前置要求
- Node.js: >= 16.x(推荐 18.x 或 20.x)
- 包管理器: npm / yarn / pnpm(推荐 pnpm)
- 浏览器: Chrome 或 Firefox 最新版
检查版本:
bash
node --version # v20.x.x
npm --version # 10.x.x
2. 项目初始化
bash
# 克隆项目或使用模板
git clone https://github.com/JohnBra/vite-web-extension
# 安装依赖
pnpm install
# 或使用 npm
npm install
3. 开发模式启动
bash
# Chrome 开发模式(默认)
pnpm dev
# 或
pnpm dev:chrome
# Firefox 开发模式
pnpm dev:firefox
开发模式特性:
- ✅ 自动监听文件变化
- ✅ 热重载(nodemon 自动重新构建)
- ✅ Source Map 支持
- ✅ 开发环境专用图标
- ✅ 详细的错误日志
4. 加载扩展到浏览器
Chrome / Edge
-
打开扩展管理页面:
- 地址栏输入
chrome://extensions - 或通过菜单:更多工具 → 扩展程序
- 地址栏输入
-
开启开发者模式(右上角开关)
-
点击 "加载已解压的扩展程序"
-
选择项目的
dist_chrome文件夹 -
扩展加载成功,可以看到开发版图标
常见问题:
- ⚠️ 如果修改了 Manifest,需要点击刷新按钮
- ⚠️ Content Script 修改后,需要刷新目标网页
- ⚠️ Popup 和 Options 页面可以直接刷新
Firefox
-
打开调试页面:
- 地址栏输入
about:debugging#/runtime/this-firefox - 或通过菜单:附加组件和主题 → 管理扩展 → 调试附加组件
- 地址栏输入
-
点击 "临时载入附加组件"
-
选择
dist_firefox文件夹中的manifest.json文件 -
扩展加载成功
注意:
- ⚠️ Firefox 的临时扩展在浏览器关闭后会被卸载
- ⚠️ 每次重启浏览器需要重新加载
5. 验证安装
加载扩展后,验证以下功能:
typescript
// 1. 打开 Popup
// 点击扩展图标,应该看到黄金价格查询界面
// 2. 查看 Background Script 日志
// 在扩展管理页面,点击"service worker"或"背景页"
// 控制台应该显示:'background script loaded'
// 3. 查看 Content Script
// 访问任意网站,打开开发者工具
// 应该看到页面左下角有黄色提示:"content script loaded"
// 4. 打开 Options 页面
// 右键点击扩展图标 → 选项
// 应该显示设置页面
Manifest 配置详解
完整配置文件
本项目的 manifest.json:
json
{
"manifest_version": 3,
"name": "",
"description": "",
"version": "1.0.0",
"icons": {
"16": "icon-16.png",
"32": "icon-32.png",
"48": "icon-48.png",
"128": "icon-128.png"
},
"action": {
"default_popup": "src/pages/popup/index.html",
"default_icon": {
"32": "icon-32.png"
},
"default_title": "查看黄金价格"
},
"background": {
"service_worker": "src/pages/background/index.ts",
"type": "module"
},
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
"js": ["src/pages/content/index.tsx"],
"css": ["contentStyle.css"],
"run_at": "document_idle"
}
],
"permissions": [
"storage",
"activeTab"
],
"host_permissions": [
"https://chatbot-be-omega.vercel.app/*"
],
"options_ui": {
"page": "src/pages/options/index.html",
"open_in_tab": true
},
"chrome_url_overrides": {
"newtab": "src/pages/newtab/index.html"
},
"devtools_page": "src/pages/devtools/index.html",
"web_accessible_resources": [
{
"resources": [
"contentStyle.css",
"icon-128.png",
"icon-32.png"
],
"matches": ["<all_urls>"]
}
]
}
字段详解
基础信息
json
{
"manifest_version": 3, // 必须是 3(新版)
"name": "扩展名称", // 最多 45 个字符
"version": "1.0.0", // 语义化版本号
"description": "扩展描述", // 最多 132 个字符
"author": "作者名" // 可选
}
图标配置
json
{
"icons": {
"16": "icon-16.png", // 扩展页面的 favicon
"48": "icon-48.png", // 扩展管理页面
"128": "icon-128.png" // 安装和 Chrome Web Store
}
}
图标设计建议:
- 使用简洁、可识别的设计
- PNG 格式,透明背景
- 不同尺寸保持一致性
- 避免文字(太小看不清)
Action(工具栏图标)
json
{
"action": {
"default_popup": "popup.html", // 点击图标打开的页面
"default_icon": {
"16": "icon-16.png",
"32": "icon-32.png"
},
"default_title": "鼠标悬停提示文字"
}
}
动态修改:
typescript
// 修改图标
chrome.action.setIcon({ path: "new-icon.png" });
// 修改标题
chrome.action.setTitle({ title: "新标题" });
// 修改徽章
chrome.action.setBadgeText({ text: "99+" });
chrome.action.setBadgeBackgroundColor({ color: "#FF0000" });
Content Scripts 配置
json
{
"content_scripts": [
{
"matches": [
"https://www.example.com/*", // 匹配模式
"https://*.google.com/*"
],
"js": ["content.js"], // JavaScript 文件
"css": ["content.css"], // CSS 文件
"run_at": "document_idle", // 注入时机
"all_frames": false, // 是否注入到 iframe
"match_about_blank": false // 是否匹配 about:blank
}
]
}
run_at 选项:
document_start: DOM 构建之前(最早)document_end: DOM 构建完成后document_idle: 页面空闲时(默认,推荐)
Web Accessible Resources
json
{
"web_accessible_resources": [
{
"resources": ["images/*", "styles.css"],
"matches": ["https://example.com/*"]
}
]
}
用途: 允许网页访问扩展内的资源文件
示例:
typescript
// 在 Content Script 中使用扩展资源
const imageUrl = chrome.runtime.getURL('images/logo.png');
const img = document.createElement('img');
img.src = imageUrl;
document.body.appendChild(img);
核心组件开发实践
1. Background Script(后台脚本)
Background Script 是扩展的"大脑",负责处理事件、管理状态、协调各组件。
Manifest V3 的变化
在 Manifest V3 中,Background Page 被 Service Worker 取代:
| 特性 | Background Page (V2) | Service Worker (V3) |
|---|---|---|
| 生命周期 | 持续运行 | 事件驱动,空闲时休眠 |
| DOM 访问 | 有 DOM | 无 DOM |
| 定时器 | setTimeout/setInterval | chrome.alarms API |
| 存储 | 变量持久化 | 需要使用 chrome.storage |
基础示例
typescript
// src/pages/background/index.ts
// 监听扩展安装或更新
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
console.log('扩展首次安装');
// 初始化默认设置
chrome.storage.local.set({
settings: {
refreshInterval: 30,
notifications: true
}
});
} else if (details.reason === 'update') {
console.log('扩展已更新到', chrome.runtime.getManifest().version);
}
});
// 监听来自其他组件的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('收到消息:', message);
console.log('来自:', sender.tab ? `标签页 ${sender.tab.id}` : '扩展页面');
if (message.action === 'getGoldPrice') {
// 异步处理
fetchGoldPrice().then(data => {
sendResponse({ success: true, data });
}).catch(error => {
sendResponse({ success: false, error: error.message });
});
// 返回 true 表示异步响应
return true;
}
if (message.action === 'openOptionsPage') {
chrome.runtime.openOptionsPage();
}
});
// 监听标签页更新
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url) {
console.log('页面加载完成:', tab.url);
// 可以在这里注入 Content Script 或执行其他操作
}
});
// 使用 Alarms API 实现定时任务
chrome.alarms.create('fetchGoldPrice', {
periodInMinutes: 30 // 每 30 分钟执行一次
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'fetchGoldPrice') {
console.log('定时获取黄金价格');
fetchGoldPrice();
}
});
// 数据获取函数
async function fetchGoldPrice() {
try {
const response = await fetch('https://api.example.com/gold-price');
const data = await response.json();
// 保存到 storage
await chrome.storage.local.set({ goldPrice: data });
// 更新徽章
chrome.action.setBadgeText({
text: data.price.toString()
});
return data;
} catch (error) {
console.error('获取数据失败:', error);
throw error;
}
}
高级用法:长连接
typescript
// Background Script
chrome.runtime.onConnect.addListener((port) => {
console.log('建立连接:', port.name);
port.onMessage.addListener((message) => {
console.log('收到消息:', message);
// 处理消息
port.postMessage({ response: 'processed' });
});
port.onDisconnect.addListener(() => {
console.log('连接断开');
});
});
// Popup 或 Content Script
const port = chrome.runtime.connect({ name: 'my-channel' });
port.postMessage({ action: 'subscribe' });
port.onMessage.addListener((message) => {
console.log('收到推送:', message);
});
2. Content Script(内容脚本)
Content Script 运行在网页上下文中,可以读取和修改页面 DOM,但有独立的 JavaScript 运行环境。
特性与限制
可以做:
- ✅ 访问和修改页面 DOM
- ✅ 监听页面事件
- ✅ 使用部分 Chrome API(runtime、storage、i18n 等)
- ✅ 与 Background Script 通信
不能做:
- ❌ 访问页面的 JavaScript 变量和函数
- ❌ 使用大部分 Chrome API(tabs、windows、bookmarks 等)
- ❌ 跨域请求(需要通过 Background)
React 集成示例
typescript
// src/pages/content/index.tsx
import { createRoot } from 'react-dom/client';
import React, { useState, useEffect } from 'react';
import './style.css';
// 创建容器
const container = document.createElement('div');
container.id = 'gold-price-extension-root';
document.body.appendChild(container);
// React 组件
function GoldPriceWidget() {
const [price, setPrice] = useState<number | null>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
// 从 storage 获取价格
chrome.storage.local.get(['goldPrice'], (result) => {
if (result.goldPrice) {
setPrice(result.goldPrice.price);
}
});
// 监听 storage 变化
chrome.storage.onChanged.addListener((changes) => {
if (changes.goldPrice) {
setPrice(changes.goldPrice.newValue.price);
}
});
}, []);
if (!visible) {
return (
<button
className="gold-price-toggle"
onClick={() => setVisible(true)}
>
💰
</button>
);
}
return (
<div className="gold-price-widget">
// 。。。
</div>
);
}
// 渲染
const root = createRoot(container);
root.render(<GoldPriceWidget />);
// 监听页面 DOM 变化
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
// 检测特定元素并进行处理
if (mutation.type === 'childList') {
const targetElement = document.querySelector('.target-class');
if (targetElement) {
// 在目标元素上添加功能
addCustomFeature(targetElement);
}
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// 自定义功能
function addCustomFeature(element: Element) {
const button = document.createElement('button');
button.textContent = '查看金价';
button.addEventListener('click', () => {
chrome.runtime.sendMessage({ action: 'openPopup' });
});
element.appendChild(button);
}
CSS 隔离
css
/* src/pages/content/style.css */
/* 使用唯一的类名前缀,避免与页面样式冲突 */
.gold-price-extension-root {
all: initial; /* 重置所有样式 */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.gold-price-widget {
position: fixed;
bottom: 20px;
right: 20px;
width: 300px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 999999; /* 确保在最上层 */
padding: 16px;
}
/* 使用 CSS-in-JS 也是不错的选择 */
与页面脚本通信
Content Script 和页面脚本运行在不同的上下文中,需要通过 window.postMessage 通信:
typescript
// Content Script → 页面
window.postMessage({
type: 'FROM_EXTENSION',
action: 'getData'
}, '*');
// 页面 → Content Script
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (event.data.type === 'FROM_PAGE') {
console.log('来自页面的消息:', event.data);
// 转发到 Background
chrome.runtime.sendMessage(event.data);
}
});
3. Popup(弹窗页面)
Popup 是用户与扩展交互的主要界面,点击工具栏图标时弹出。
特点
- ⏱️ 临时性:关闭后页面和状态会被销毁
- 📏 尺寸限制:最小 25x25px,最大 800x600px
- ⚡ 快速加载:应该在 1 秒内完成初始化
完整示例
typescript
// src/pages/popup/Popup.tsx
import React, { useEffect, useState } from "react";
import { GoldPriceData } from "./types";
import { fetchGoldPriceData, refreshGoldPriceData } from "./services/goldPriceService";
import { JewelryCard } from "./components/JewelryCard";
import { BankCard } from "./components/BankCard";
import { LoadingSpinner } from "./components/LoadingSpinner";
export default function Popup() {
const [data, setData] = useState<GoldPriceData | null>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [activeTab, setActiveTab] = useState<"jewelry" | "bank">("jewelry");
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
const goldData = await fetchGoldPriceData();
setData(goldData);
} catch (error) {
console.error("加载数据失败:", error);
} finally {
setLoading(false);
}
};
const handleRefresh = async () => {
try {
setRefreshing(true);
const goldData = await refreshGoldPriceData();
setData(goldData);
} catch (error) {
console.error("刷新数据失败:", error);
} finally {
setRefreshing(false);
}
};
if (loading) {
return (
<div className="w-[400px] h-[600px] flex items-center justify-center">
<LoadingSpinner />
</div>
);
}
return (
<div className="w-[400px] h-[600px] bg-gradient-to-br from-amber-50 via-yellow-50 to-orange-50 flex flex-col">
{/* 头部 */}
<header className="bg-white/60 backdrop-blur-md shadow-sm px-4 py-3 flex items-center justify-between">
<h1 className="text-base font-semibold">每日金价</h1>
<button
onClick={handleRefresh}
disabled={refreshing}
className="p-1.5 rounded-full hover:bg-amber-100/50 transition-colors"
>
<RefreshIcon spinning={refreshing} />
</button>
</header>
{/* 标签切换 */}
<div className="flex border-b border-amber-100">
<button
onClick={() => setActiveTab("jewelry")}
className={`flex-1 py-2.5 text-sm ${
activeTab === "jewelry" ? "text-amber-600" : "text-gray-500"
}`}
>
金饰品
</button>
<button
onClick={() => setActiveTab("bank")}
className={`flex-1 py-2.5 text-sm ${
activeTab === "bank" ? "text-amber-600" : "text-gray-500"
}`}
>
银行金条
</button>
</div>
{/* 内容区域 */}
<div className="flex-1 overflow-y-auto p-3">
{activeTab === "jewelry" ? (
<div className="grid grid-cols-2 gap-2">
{data?.jewelryBrands.map((brand) => (
<JewelryCard key={brand.id} brand={brand} />
))}
</div>
) : (
<div className="flex flex-col gap-2">
{data?.bankGoldBars.map((bank) => (
<BankCard key={bank.id} bank={bank} />
))}
</div>
)}
</div>
{/* 底部信息 */}
<footer className="px-4 py-2 text-center">
<p className="text-xs text-gray-500">更新时间: {data?.lastUpdate}</p>
</footer>
</div>
);
}
Popup 设计原则:
- 🎯 单一目标:每个 Popup 应该有明确的用途
- ⚡ 快速响应:优先显示缓存数据,后台刷新
- 📱 简洁 UI:空间有限,避免复杂交互
- 💾 状态管理 :使用
chrome.storage持久化重要状态
组件间通信机制
浏览器扩展的各个组件之间需要频繁通信,Chrome 提供了多种通信方式。
1. 一次性消息传递
适用场景: 简单的请求-响应模式
Content Script → Background
typescript
// Content Script
chrome.runtime.sendMessage(
{ action: 'getPrice', item: 'gold' },
(response) => {
if (response.success) {
console.log('价格:', response.data);
}
}
);
// Background Script
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'getPrice') {
fetchPrice(request.item).then(price => {
sendResponse({ success: true, data: price });
}).catch(error => {
sendResponse({ success: false, error: error.message });
});
// 必须返回 true 表示异步响应
return true;
}
});
Popup → Background
typescript
// Popup
const response = await chrome.runtime.sendMessage({
action: 'getData'
});
// 或者使用 callback
chrome.runtime.sendMessage(
{ action: 'getData' },
(response) => {
console.log(response);
}
);
Background → Content Script
typescript
// Background: 向特定标签页发送消息
chrome.tabs.sendMessage(
tabId,
{ action: 'updatePrice', price: 520 },
(response) => {
console.log('Content Script 响应:', response);
}
);
// Background: 向所有标签页广播
chrome.tabs.query({}, (tabs) => {
tabs.forEach(tab => {
if (tab.id) {
chrome.tabs.sendMessage(tab.id, { action: 'refresh' });
}
});
});
// Content Script: 监听消息
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'updatePrice') {
updatePriceDisplay(request.price);
sendResponse({ updated: true });
}
});
2. 长连接(Long-lived Connections)
适用场景: 需要持续通信、实时推送数据
typescript
// Content Script: 建立连接
const port = chrome.runtime.connect({ name: 'price-updates' });
// 发送消息
port.postMessage({ action: 'subscribe', symbol: 'GOLD' });
// 接收消息
port.onMessage.addListener((message) => {
console.log('收到更新:', message);
updateUI(message.price);
});
// 监听断开
port.onDisconnect.addListener(() => {
console.log('连接已断开');
});
// Background Script: 监听连接
const connections = new Map();
chrome.runtime.onConnect.addListener((port) => {
console.log('新连接:', port.name);
connections.set(port.name, port);
port.onMessage.addListener((message) => {
if (message.action === 'subscribe') {
// 开始推送数据
startPriceUpdates(port, message.symbol);
}
});
port.onDisconnect.addListener(() => {
connections.delete(port.name);
});
});
function startPriceUpdates(port, symbol) {
const intervalId = setInterval(() => {
fetchPrice(symbol).then(price => {
try {
port.postMessage({ price, symbol, timestamp: Date.now() });
} catch (e) {
// 端口已断开
clearInterval(intervalId);
}
});
}, 5000);
}
3. Storage 变化监听
适用场景: 多个组件共享状态
typescript
// 任意组件:设置数据
chrome.storage.local.set({ goldPrice: 520 });
// 任意组件:监听变化
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName === 'local' && changes.goldPrice) {
console.log('价格更新:');
console.log(' 旧值:', changes.goldPrice.oldValue);
console.log(' 新值:', changes.goldPrice.newValue);
// 更新 UI
updatePriceDisplay(changes.goldPrice.newValue);
}
});
4. 消息传递最佳实践
typescript
// 定义消息类型(TypeScript)
interface Message {
action: string;
data?: any;
}
interface Response {
success: boolean;
data?: any;
error?: string;
}
// 封装消息发送
async function sendMessage(message: Message): Promise<Response> {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(message, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(response);
}
});
});
}
// 使用
try {
const response = await sendMessage({
action: 'getPrice',
data: { symbol: 'GOLD' }
});
if (response.success) {
console.log(response.data);
}
} catch (error) {
console.error('通信失败:', error);
}
数据存储方案
1. chrome.storage API
Chrome Extension 提供了专门的存储 API,比 localStorage 更强大。
存储类型对比
| 存储类型 | 容量 | 同步 | 用途 |
|---|---|---|---|
storage.local |
10 MB | ❌ | 本地数据缓存 |
storage.sync |
100 KB | ✅ | 用户设置(跨设备同步) |
storage.session |
10 MB | ❌ | 临时会话数据(MV3) |
基础用法
typescript
// 保存数据
await chrome.storage.local.set({
user: { name: 'John', age: 30 },
settings: { theme: 'dark' },
prices: [520, 530, 525]
});
// 读取数据
const result = await chrome.storage.local.get(['user', 'settings']);
console.log(result.user); // { name: 'John', age: 30 }
console.log(result.settings); // { theme: 'dark' }
// 读取所有数据
const allData = await chrome.storage.local.get(null);
// 删除数据
await chrome.storage.local.remove(['user']);
// 清空所有数据
await chrome.storage.local.clear();
// 获取存储使用量
const bytes = await chrome.storage.local.getBytesInUse(['prices']);
console.log(`使用了 ${bytes} 字节`);
封装 Storage 工具类
typescript
// utils/storage.ts
class StorageManager {
async get<T>(key: string): Promise<T | null> {
const result = await chrome.storage.local.get(key);
return result[key] ?? null;
}
async set<T>(key: string, value: T): Promise<void> {
await chrome.storage.local.set({ [key]: value });
}
async remove(key: string): Promise<void> {
await chrome.storage.local.remove(key);
}
async getMultiple<T extends Record<string, any>>(
keys: string[]
): Promise<Partial<T>> {
return await chrome.storage.local.get(keys);
}
async setMultiple(items: Record<string, any>): Promise<void> {
await chrome.storage.local.set(items);
}
onChange(callback: (changes: any, areaName: string) => void): void {
chrome.storage.onChanged.addListener(callback);
}
}
export const storage = new StorageManager();
// 使用
import { storage } from './utils/storage';
// 保存
await storage.set('goldPrice', { price: 520, timestamp: Date.now() });
// 读取
const goldPrice = await storage.get<{ price: number; timestamp: number }>('goldPrice');
// 监听变化
storage.onChange((changes, areaName) => {
if (changes.goldPrice) {
console.log('价格更新:', changes.goldPrice.newValue);
}
});
2. IndexedDB(大量数据)
对于需要存储大量结构化数据的场景,可以使用 IndexedDB。
typescript
// utils/indexedDB.ts
class GoldPriceDB {
private dbName = 'GoldPriceExtension';
private version = 1;
private db: IDBDatabase | null = null;
async init(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
// 创建对象存储
if (!db.objectStoreNames.contains('prices')) {
const store = db.createObjectStore('prices', {
keyPath: 'id',
autoIncrement: true
});
// 创建索引
store.createIndex('timestamp', 'timestamp', { unique: false });
store.createIndex('brand', 'brand', { unique: false });
}
};
});
}
async addPrice(price: any): Promise<number> {
const transaction = this.db!.transaction(['prices'], 'readwrite');
const store = transaction.objectStore('prices');
return new Promise((resolve, reject) => {
const request = store.add(price);
request.onsuccess = () => resolve(request.result as number);
request.onerror = () => reject(request.error);
});
}
async getPrices(limit = 100): Promise<any[]> {
const transaction = this.db!.transaction(['prices'], 'readonly');
const store = transaction.objectStore('prices');
return new Promise((resolve, reject) => {
const request = store.getAll(undefined, limit);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getPricesByBrand(brand: string): Promise<any[]> {
const transaction = this.db!.transaction(['prices'], 'readonly');
const store = transaction.objectStore('prices');
const index = store.index('brand');
return new Promise((resolve, reject) => {
const request = index.getAll(brand);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
export const goldPriceDB = new GoldPriceDB();
// 使用
await goldPriceDB.init();
await goldPriceDB.addPrice({
brand: '周大福',
price: 520,
timestamp: Date.now()
});
const prices = await goldPriceDB.getPricesByBrand('周大福');
实战案例:黄金价格查询扩展
让我们深入分析本项目的完整实现。
1. 项目架构
数据流向:
API → Service Layer → Storage → UI Components
↓ ↓ ↓ ↓
外部数据 业务逻辑 缓存层 用户界面
2. 类型定义系统
typescript
// src/pages/popup/types.ts
/** 金饰品品牌价格 */
export interface JewelryBrand {
id: string;
name: string; // 品牌名称
price: number; // 价格(元/克)
change: number; // 涨跌幅度(元)
updateTime: string; // 更新时间
}
/** 银行金条价格 */
export interface BankGoldBar {
id: string;
bankName: string; // 银行名称
buyPrice: number; // 买入价(元/克)
sellPrice: number; // 卖出价(元/克)
change: number; // 涨跌幅度(元)
updateTime: string; // 更新时间
}
/** 黄金价格数据 */
export interface GoldPriceData {
jewelryBrands: JewelryBrand[];
bankGoldBars: BankGoldBar[];
lastUpdate: string;
}
/** API 响应类型 */
export interface BrandPriceData {
brand: string;
goldPrice: number;
platinumPrice: number | string;
}
export interface PriceResponse {
status: string;
timestamp: string;
data: BrandPriceData[];
}
3. Service Layer实现
typescript
// src/pages/popup/services/goldPriceService.ts
const API_URL = "https://chatbot-be-omega.vercel.app/api/goldPrice";
// 定义品牌分类
const JEWELRY_BRANDS = [
"周大福", "老凤祥", "周六福", "周生生",
"六福珠宝", "老庙", "金至尊", "菜百"
// ... 更多品牌
];
const BANK_BRANDS = ["中国黄金", "高赛尔"];
/**
* 从 API 获取黄金价格数据
*/
export const fetchGoldPriceFromAPI = async (): Promise<PriceResponse> => {
try {
const response = await fetch(API_URL, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
throw new Error(`API 请求失败: ${response.status}`);
}
const data: PriceResponse = await response.json();
if (data.status !== "ok") {
throw new Error("API 返回状态异常");
}
return data;
} catch (error) {
console.error("获取黄金价格失败:", error);
throw error;
}
};
/**
* 将 API 数据转换为应用格式
*/
const transformAPIData = (apiResponse: PriceResponse): GoldPriceData => {
// 转换金饰品数据
const jewelryBrands: JewelryBrand[] = apiResponse.data
.filter(item => JEWELRY_BRANDS.includes(item.brand))
.map((item, index) => ({
id: `jewelry-${index}`,
name: item.brand,
price: item.goldPrice,
change: 0,
updateTime: new Date(apiResponse.timestamp).toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}),
}));
// 转换银行金条数据
const bankGoldBars: BankGoldBar[] = apiResponse.data
.filter(item => BANK_BRANDS.includes(item.brand))
.map((item, index) => ({
id: `bank-${index}`,
bankName: item.brand,
buyPrice: item.goldPrice,
sellPrice: typeof item.platinumPrice === "number"
? item.platinumPrice
: item.goldPrice,
change: 0,
updateTime: new Date(apiResponse.timestamp).toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}),
}));
const lastUpdate = new Date(apiResponse.timestamp).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
return { jewelryBrands, bankGoldBars, lastUpdate };
};
/**
* 获取黄金价格数据(对外接口)
* 带缓存机制
*/
export const fetchGoldPriceData = async (): Promise<GoldPriceData> => {
try {
// 先尝试从缓存读取
const cached = await chrome.storage.local.get(['goldPriceData', 'cacheTime']);
const now = Date.now();
const cacheAge = now - (cached.cacheTime || 0);
// 缓存有效期:5 分钟
if (cached.goldPriceData && cacheAge < 5 * 60 * 1000) {
console.log('使用缓存数据');
return cached.goldPriceData;
}
// 缓存过期,重新获取
console.log('获取新数据');
const apiResponse = await fetchGoldPriceFromAPI();
const data = transformAPIData(apiResponse);
// 保存到缓存
await chrome.storage.local.set({
goldPriceData: data,
cacheTime: now
});
return data;
} catch (error) {
console.error("获取并转换黄金价格数据失败:", error);
// 如果有缓存数据,即使过期也返回
const cached = await chrome.storage.local.get(['goldPriceData']);
if (cached.goldPriceData) {
console.log('API 失败,使用过期缓存');
return cached.goldPriceData;
}
throw error;
}
};
/**
* 强制刷新数据
*/
export const refreshGoldPriceData = async (): Promise<GoldPriceData> => {
// 清除缓存时间,强制重新获取
await chrome.storage.local.remove(['cacheTime']);
return fetchGoldPriceData();
};
4. UI 组件实现
typescript
// src/pages/popup/components/JewelryCard.tsx
import React from "react";
import { JewelryBrand } from "../types";
interface JewelryCardProps {
brand: JewelryBrand;
}
export const JewelryCard: React.FC<JewelryCardProps> = ({ brand }) => {
const isPositive = brand.change >= 0;
return (
<div className="bg-white/80 backdrop-blur-sm rounded-lg p-3 shadow-sm hover:shadow-md transition-shadow">
{/* 品牌名和涨跌 */}
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-800">{brand.name}</h3>
{brand.change !== 0 && (
<span
className={`text-xs px-2 py-0.5 rounded-full ${
isPositive
? "bg-red-50 text-red-600"
: "bg-green-50 text-green-600"
}`}
>
{isPositive ? "+" : ""}{brand.change}
</span>
)}
</div>
{/* 价格显示 */}
<div className="flex items-baseline">
<span className="text-xl font-semibold text-amber-600">
¥{brand.price}
</span>
<span className="text-xs text-gray-500 ml-1">/克</span>
</div>
{/* 更新时间 */}
<div className="mt-1 text-xs text-gray-400">
{brand.updateTime}
</div>
</div>
);
};
typescript
// src/pages/popup/components/BankCard.tsx
import React from "react";
import { BankGoldBar } from "../types";
interface BankCardProps {
bank: BankGoldBar;
}
export const BankCard: React.FC<BankCardProps> = ({ bank }) => {
const isPositive = bank.change >= 0;
return (
<div className="bg-white/80 backdrop-blur-sm rounded-lg p-3 shadow-sm hover:shadow-md transition-shadow">
{/* 银行名和涨跌 */}
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-800">{bank.bankName}</h3>
{bank.change !== 0 && (
<span
className={`text-xs px-2 py-0.5 rounded-full ${
isPositive
? "bg-red-50 text-red-600"
: "bg-green-50 text-green-600"
}`}
>
{isPositive ? "+" : ""}{bank.change}
</span>
)}
</div>
{/* 买入/卖出价格 */}
<div className="flex justify-between items-center">
<div className="flex flex-col">
<span className="text-xs text-gray-500 mb-1">买入</span>
<span className="text-base font-semibold text-blue-600">
¥{bank.buyPrice}
</span>
</div>
<div className="h-6 w-px bg-gray-200"></div>
<div className="flex flex-col items-end">
<span className="text-xs text-gray-500 mb-1">卖出</span>
<span className="text-base font-semibold text-purple-600">
¥{bank.sellPrice}
</span>
</div>
</div>
{/* 更新时间 */}
<div className="mt-2 text-xs text-gray-400 text-center">
{bank.updateTime}
</div>
</div>
);
};
5. 关键技术点
错误处理与降级策略
typescript
// 多层次的错误处理
async function fetchWithRetry(url: string, retries = 3): Promise<Response> {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (response.ok) return response;
if (i === retries - 1) throw new Error(`请求失败: ${response.status}`);
} catch (error) {
if (i === retries - 1) throw error;
// 指数退避
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
}
throw new Error('Max retries reached');
}
// 使用
try {
const response = await fetchWithRetry(API_URL);
const data = await response.json();
} catch (error) {
// 降级到缓存数据
const cached = await getCachedData();
if (cached) return cached;
// 显示错误提示
showErrorNotification(error);
}
性能优化
typescript
// 1. 防抖刷新
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout;
return function(...args: Parameters<T>) {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
const debouncedRefresh = debounce(refreshGoldPriceData, 1000);
// 2. 数据预加载
chrome.runtime.onInstalled.addListener(() => {
// 安装时预先获取数据
fetchGoldPriceData().catch(console.error);
});
// 3. 后台定时更新
chrome.alarms.create('updatePrices', {
periodInMinutes: 30
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'updatePrices') {
fetchGoldPriceData().catch(console.error);
}
});
跨浏览器兼容性处理
1. 使用 webextension-polyfill
typescript
// 安装
npm install webextension-polyfill
npm install --save-dev @types/webextension-polyfill
// 使用
import Browser from 'webextension-polyfill';
// 统一的 Promise 风格 API
const data = await Browser.storage.local.get('key');
const tabs = await Browser.tabs.query({ active: true });
// 自动处理回调转 Promise
Browser.runtime.sendMessage({ action: 'test' })
.then(response => console.log(response))
.catch(error => console.error(error));
2. Vite 配置差异
typescript
// vite.config.chrome.ts
export default defineConfig({
plugins: [
crx({
manifest: {
...baseManifest,
background: {
service_worker: 'src/pages/background/index.ts', // Chrome: Service Worker
type: 'module'
},
},
browser: 'chrome',
})
],
});
// vite.config.firefox.ts
export default defineConfig({
plugins: [
crx({
manifest: {
...baseManifest,
background: {
scripts: ['src/pages/background/index.ts'] // Firefox: Scripts Array
},
},
browser: 'firefox',
})
],
});
3. 浏览器特性检测
typescript
// utils/browser.ts
export const getBrowserInfo = () => {
const userAgent = navigator.userAgent;
if (userAgent.includes('Edg/')) {
return { name: 'Edge', isChromium: true };
} else if (userAgent.includes('Chrome')) {
return { name: 'Chrome', isChromium: true };
} else if (userAgent.includes('Firefox')) {
return { name: 'Firefox', isChromium: false };
} else if (userAgent.includes('Safari')) {
return { name: 'Safari', isChromium: false };
}
return { name: 'Unknown', isChromium: false };
};
// 根据浏览器调整行为
export const isSidePanel Supported = () => {
const { name } = getBrowserInfo();
return name === 'Chrome' || name === 'Edge';
};
// 条件性使用 API
if ('sidePanel' in chrome) {
chrome.sidePanel.setOptions({ path: 'panel.html' });
}
调试技巧与最佳实践
1. 调试工具
Background Script 调试
bash
# Chrome
1. 打开 chrome://extensions
2. 找到你的扩展
3. 点击 "service worker" 或 "背景页"
4. 打开 DevTools 控制台
# Firefox
1. 打开 about:debugging
2. 点击 "检查"
3. 打开浏览器控制台
Content Script 调试
bash
# 直接在网页中调试
1. 打开目标网页
2. 按 F12 打开开发者工具
3. 在 Console 中可以看到 Content Script 的日志
4. 在 Sources/Debugger 标签可以找到注入的脚本
Popup 调试
bash
# Chrome
1. 右键点击扩展图标
2. 选择 "检查弹出内容"
3. DevTools 会附加到 Popup
# Firefox
1. 右键点击扩展图标
2. 选择 "调试此扩展"
2. 日志系统
typescript
// utils/logger.ts
enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3
}
class Logger {
private level: LogLevel = LogLevel.INFO;
private prefix: string;
constructor(prefix: string) {
this.prefix = `[${prefix}]`;
}
setLevel(level: LogLevel) {
this.level = level;
}
debug(...args: any[]) {
if (this.level <= LogLevel.DEBUG) {
console.log(this.prefix, '[DEBUG]', ...args);
}
}
info(...args: any[]) {
if (this.level <= LogLevel.INFO) {
console.info(this.prefix, '[INFO]', ...args);
}
}
warn(...args: any[]) {
if (this.level <= LogLevel.WARN) {
console.warn(this.prefix, '[WARN]', ...args);
}
}
error(...args: any[]) {
if (this.level <= LogLevel.ERROR) {
console.error(this.prefix, '[ERROR]', ...args);
}
}
}
export const logger = new Logger('GoldPrice');
// 使用
logger.info('获取价格数据');
logger.error('API 请求失败', error);
3. 性能监控
typescript
// 性能计时
const startTime = performance.now();
await fetchGoldPriceData();
const endTime = performance.now();
console.log(`数据获取耗时: ${(endTime - startTime).toFixed(2)}ms`);
// 使用 Performance API
performance.mark('fetch-start');
await fetchData();
performance.mark('fetch-end');
performance.measure('fetch-duration', 'fetch-start', 'fetch-end');
const measures = performance.getEntriesByName('fetch-duration');
console.log(`数据获取耗时: ${measures[0].duration}ms`);
性能优化策略
1. Content Script 优化
typescript
// ❌ 不好:阻塞主线程
document.querySelectorAll('.item').forEach(item => {
// 大量 DOM 操作
item.innerHTML = generateComplexHTML();
});
// ✅ 好:批量操作,使用 DocumentFragment
const fragment = document.createDocumentFragment();
document.querySelectorAll('.item').forEach(item => {
const newElement = document.createElement('div');
newElement.innerHTML = generateComplexHTML();
fragment.appendChild(newElement);
});
document.body.appendChild(fragment);
// ✅ 更好:使用 IntersectionObserver 延迟加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
enhanceElement(entry.target);
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('.item').forEach(item => {
observer.observe(item);
});
2. 资源优化
typescript
// 代码分割
const loadHeavyFeature = async () => {
const module = await import('./heavy-feature');
module.initialize();
};
// 图片懒加载
const imageUrl = chrome.runtime.getURL('images/large-image.png');
const img = new Image();
img.loading = 'lazy';
img.src = imageUrl;
// 压缩数据
import pako from 'pako';
// 保存时压缩
const compressed = pako.deflate(JSON.stringify(largeData));
await chrome.storage.local.set({ data: compressed });
// 读取时解压
const { data } = await chrome.storage.local.get('data');
const decompressed = JSON.parse(pako.inflate(data, { to: 'string' }));
3. 内存管理
typescript
// 及时清理监听器
let listener: ((changes: any) => void) | null = null;
function startMonitoring() {
listener = (changes) => {
console.log('Storage changed:', changes);
};
chrome.storage.onChanged.addListener(listener);
}
function stopMonitoring() {
if (listener) {
chrome.storage.onChanged.removeListener(listener);
listener = null;
}
}
// 限制缓存大小
async function addToCache(key: string, data: any) {
const cache = await chrome.storage.local.get('cache') || {};
const cacheKeys = Object.keys(cache);
// 最多保留 100 条
if (cacheKeys.length >= 100) {
const oldest = cacheKeys[0];
delete cache[oldest];
}
cache[key] = data;
await chrome.storage.local.set({ cache });
}
打包构建与发布
1. 生产构建
bash
# 构建 Chrome 版本
npm run build:chrome
# 构建 Firefox 版本
npm run build:firefox
# 输出目录
# dist_chrome/ # Chrome 扩展文件
# dist_firefox/ # Firefox 扩展文件
2. 打包为 ZIP
bash
# 手动打包
cd dist_chrome
zip -r ../extension-chrome.zip *
cd ../dist_firefox
zip -r ../extension-firefox.zip *
# 或使用脚本
npm install --save-dev archiver
// scripts/zip.js
const archiver = require('archiver');
const fs = require('fs');
function zipDirectory(source, out) {
const archive = archiver('zip', { zlib: { level: 9 } });
const stream = fs.createWriteStream(out);
return new Promise((resolve, reject) => {
archive
.directory(source, false)
.on('error', reject)
.pipe(stream);
stream.on('close', resolve);
archive.finalize();
});
}
zipDirectory('./dist_chrome', './extension-chrome.zip');
3. 发布到 Chrome Web Store
准备工作
-
注册开发者账号
- 访问 Chrome Web Store Developer Dashboard
- 支付 $5 一次性注册费
-
准备资料
- 扩展 ZIP 文件
- 图标(128x128px)
- 截图(至少 1 张,1280x800 或 640x400)
- 推广图片(可选,440x280px)
- 详细描述(英文,最多 132 字符简介 + 详细说明)
发布流程
bash
1. 登录 Developer Dashboard
2. 点击 "New Item"
3. 上传 ZIP 文件
4. 填写商店信息:
- 名称和描述
- 类别和语言
- 截图和图标
- 隐私政策(如果使用了用户数据)
5. 定价和分发:
- 免费或付费
- 发布地区
6. 提交审核
审核时间: 通常 1-3 天
4. 发布到 Firefox Add-ons
准备工作
-
注册账号(免费)
-
签名要求
- Firefox 要求所有扩展必须签名
- 可以选择自动签名(不上架)或完整审核(上架)
发布流程
bash
1. 登录 Developer Hub
2. Submit a New Add-on
3. 选择分发方式:
- On this site (完整审核,公开发布)
- On your own (自动签名,自行分发)
4. 上传 ZIP 文件
5. 填写信息:
- 名称、描述、分类
- 版本说明
- 许可证
- 隐私政策
6. 提交审核
审核时间: 人工审核通常 1-5 天
5. GitHub Actions 自动化
yaml
# .github/workflows/release.yml
name: Build and Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm install
- name: Build Chrome
run: npm run build:chrome
- name: Build Firefox
run: npm run build:firefox
- name: Create ZIP files
run: |
cd dist_chrome && zip -r ../extension-chrome.zip * && cd ..
cd dist_firefox && zip -r ../extension-firefox.zip * && cd ..
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
extension-chrome.zip
extension-firefox.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
常见问题与解决方案
1. Manifest 错误
问题: Manifest version 3 is not supported
解决:
- 确保 Chrome 版本 >= 88
- Firefox 需要特殊配置支持 MV3
问题: Permission 'tabs' is not recognized
解决:
json
// ❌ 错误
{
"permissions": ["tabs", "https://*/*"]
}
// ✅ 正确
{
"permissions": ["tabs"],
"host_permissions": ["https://*/*"]
}
2. Content Script 问题
问题: Content Script 无法访问页面变量
解决:
typescript
// ❌ 不能直接访问
console.log(window.pageVariable);
// ✅ 通过 postMessage 通信
window.postMessage({ type: 'GET_DATA' }, '*');
window.addEventListener('message', (e) => {
if (e.data.type === 'DATA_RESPONSE') {
console.log(e.data.value);
}
});
// 页面脚本
window.addEventListener('message', (e) => {
if (e.data.type === 'GET_DATA') {
window.postMessage({
type: 'DATA_RESPONSE',
value: window.pageVariable
}, '*');
}
});
3. CORS 问题
问题: Fetch API cannot load due to CORS policy
解决:
json
// manifest.json 添加 host_permissions
{
"host_permissions": [
"https://api.example.com/*"
]
}
typescript
// 或在 Background Script 中代理请求
// Content Script
chrome.runtime.sendMessage({
action: 'fetch',
url: 'https://api.example.com/data'
}, (response) => {
console.log(response);
});
// Background Script
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'fetch') {
fetch(request.url)
.then(r => r.json())
.then(data => sendResponse(data))
.catch(error => sendResponse({ error: error.message }));
return true; // 异步响应
}
});
4. Storage 配额问题
问题: QUOTA_BYTES quota exceeded
解决:
typescript
// 检查存储使用量
const bytes = await chrome.storage.local.getBytesInUse();
console.log(`已使用: ${bytes} / ${chrome.storage.local.QUOTA_BYTES}`);
// 定期清理旧数据
async function cleanup() {
const all = await chrome.storage.local.get(null);
const keys = Object.keys(all);
// 删除超过 7 天的数据
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
const toRemove = keys.filter(key => {
const item = all[key];
return item.timestamp && item.timestamp < sevenDaysAgo;
});
if (toRemove.length > 0) {
await chrome.storage.local.remove(toRemove);
console.log(`清理了 ${toRemove.length} 条过期数据`);
}
}
5. Service Worker 生命周期
问题: Background Service Worker 意外休眠
解决:
typescript
// ❌ 不要依赖全局变量
let cache = {}; // Service Worker 休眠后会丢失
// ✅ 使用 chrome.storage
async function getCache() {
const result = await chrome.storage.local.get('cache');
return result.cache || {};
}
async function setCache(cache) {
await chrome.storage.local.set({ cache });
}
// ❌ 不要使用 setTimeout
setTimeout(() => {
doSomething();
}, 60000); // 可能不会执行
// ✅ 使用 chrome.alarms
chrome.alarms.create('task', { delayInMinutes: 1 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'task') {
doSomething();
}
});
参考资料
官方文档
工具和库
- @crxjs/vite-plugin - Vite 插件
- webextension-polyfill - 浏览器兼容层
- Chrome Types - TypeScript 类型定义