Chrome 扩展开发完全指南 / 第 18 章:最佳实践(Best Practices)
第 18 章:最佳实践(Best Practices)
经过前 17 章的学习,你已经掌握了 Chrome 扩展开发的全部核心知识。本章将总结性能优化、代码规范、架构模式和调试技巧等最佳实践,帮助你构建高质量、可维护的扩展。
18.1 性能优化
18.1.1 Service Worker 优化
// ❌ 不推荐 — Service Worker 启动时执行大量操作
chrome.runtime.onInstalled.addListener(() => {
// 同步执行多个初始化任务
initDatabase();
syncRemoteData();
createAllMenus();
setupAllAlarms();
});
// ✅ 推荐 — 延迟加载,按需执行
chrome.runtime.onInstalled.addListener(async () => {
// 仅执行必需的初始化
await createEssentialMenus();
// 其他任务延迟执行
chrome.alarms.create('initDeferred', { delayInMinutes: 0.1 });
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'initDeferred') {
initDatabase();
setupAllAlarms();
}
});
18.1.2 存储优化
// ❌ 不推荐 — 频繁小量写入
for (const item of items) {
await chrome.storage.local.set({ [item.key]: item.value });
}
// ✅ 推荐 — 批量写入
const batch = {};
for (const item of items) {
batch[item.key] = item.value;
}
await chrome.storage.local.set(batch);
// ✅ 使用 debounce 避免频繁写入
function debounce(fn, ms = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
const saveSettings = debounce(async (settings) => {
await chrome.storage.sync.set({ settings });
}, 500);
// 监听输入变化
document.querySelectorAll('.setting-input').forEach(input => {
input.addEventListener('input', () => {
saveSettings(collectSettings());
});
});
18.1.3 Content Script 优化
// ❌ 不推荐 — 注入所有页面
{
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"]
}]
}
// ✅ 推荐 — 精确匹配
{
"content_scripts": [
{
"matches": ["*://github.com/*"],
"js": ["content/github.js"],
"run_at": "document_idle"
},
{
"matches": ["*://docs.google.com/*"],
"js": ["content/google-docs.js"],
"run_at": "document_idle"
}
]
}
// ✅ 按需注入 — 仅在需要时注入
// manifest.json
{
"permissions": ["activeTab", "scripting"],
"action": {
"default_popup": "popup.html"
}
}
// service-worker.js
chrome.action.onClicked.addListener(async (tab) => {
// 仅在用户点击时注入
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content.js']
});
});
18.1.4 DOM 操作优化
// ❌ 不推荐 — 逐个操作 DOM
for (const item of items) {
const li = document.createElement('li');
li.textContent = item.name;
list.appendChild(li); // 每次都触发重排
}
// ✅ 推荐 — 使用 DocumentFragment
const fragment = document.createDocumentFragment();
for (const item of items) {
const li = document.createElement('li');
li.textContent = item.name;
fragment.appendChild(li);
}
list.appendChild(fragment); // 一次性插入
// ✅ 使用虚拟列表处理大量数据
class VirtualList {
constructor(container, items, itemHeight = 40) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleCount = Math.ceil(container.clientHeight / itemHeight) + 2;
this.init();
}
init() {
this.container.style.overflow = 'auto';
this.container.style.position = 'relative';
this.spacer = document.createElement('div');
this.spacer.style.height = `${this.items.length * this.itemHeight}px`;
this.container.appendChild(this.spacer);
this.renderArea = document.createElement('div');
this.renderArea.style.position = 'absolute';
this.renderArea.style.width = '100%';
this.container.appendChild(this.renderArea);
this.container.addEventListener('scroll', () => this.render());
this.render();
}
render() {
const scrollTop = this.container.scrollTop;
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = Math.min(startIndex + this.visibleCount, this.items.length);
this.renderArea.style.top = `${startIndex * this.itemHeight}px`;
const fragment = document.createDocumentFragment();
for (let i = startIndex; i < endIndex; i++) {
const item = document.createElement('div');
item.style.height = `${this.itemHeight}px`;
item.textContent = this.items[i];
fragment.appendChild(item);
}
this.renderArea.innerHTML = '';
this.renderArea.appendChild(fragment);
}
}
18.2 代码规范
18.2.1 项目结构规范
my-extension/
├── src/
│ ├── background/ # Service Worker
│ │ ├── index.ts # 入口
│ │ ├── handlers/ # 事件处理器
│ │ ├── services/ # 业务服务
│ │ └── utils.ts # 工具函数
│ ├── content/ # Content Scripts
│ │ ├── injectors/ # 各站点注入器
│ │ ├── ui/ # DOM UI 组件
│ │ └── index.ts
│ ├── popup/ # Popup 页面
│ │ ├── components/
│ │ ├── App.ts
│ │ └── index.html
│ ├── options/ # Options 页面
│ ├── sidepanel/ # Side Panel
│ ├── shared/ # 共享代码
│ │ ├── constants.ts
│ │ ├── types.ts
│ │ ├── storage.ts
│ │ ├── messaging.ts
│ │ └── utils.ts
│ └── manifest.json
├── public/ # 静态资源
│ ├── icons/
│ └── _locales/
├── tests/
│ ├── unit/
│ └── e2e/
├── scripts/
│ └── build.mjs
├── package.json
├── tsconfig.json
├── vite.config.ts
└── .eslintrc.json
18.2.2 命名规范
// 文件命名
// - 组件: PascalCase (UserProfile.ts)
// - 工具: camelCase (dateUtils.ts)
// - 常量: UPPER_SNAKE (API_ENDPOINTS.ts)
// - 类型: PascalCase (types/User.ts)
// 变量命名
const MAX_RETRY_COUNT = 3; // 常量
let currentPageNumber = 1; // 变量
const userSettings = {}; // 对象
const isInitialized = false; // 布尔
// 函数命名
function getUserData() {} // 获取数据
function isValidEmail(email) {} // 验证函数
function handleTabUpdate() {} // 事件处理
function calculateTotalPrice() {} // 计算函数
// 类命名
class StorageManager {} // 管理类
class MessageRouter {} // 路由类
class UIComponent {} // 组件类
// Chrome API 类型
type ChromeMessage = {
type: string;
payload?: unknown;
};
type StorageData = {
settings: UserSettings;
cache: Map<string, CacheEntry>;
};
18.2.3 ESLint 配置
// .eslintrc.json
{
"env": {
"browser": true,
"es2022": true,
"webextensions": true
},
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-console": ["warn", { "allow": ["warn", "error"] }],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_"
}],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "warn"
},
"overrides": [
{
"files": ["src/content/**/*.ts"],
"rules": {
"no-restricted-properties": ["error", {
"object": "document",
"property": "innerHTML",
"message": "Use textContent or DOM API for security"
}]
}
}
]
}
18.3 架构模式
18.3.1 消息总线模式
// shared/message-bus.ts
type MessageHandler<T = unknown> = (
data: T,
sender: chrome.runtime.MessageSender
) => Promise<unknown> | unknown;
class MessageBus {
private handlers = new Map<string, MessageHandler>();
constructor() {
chrome.runtime.onMessage.addListener(
(message, sender, sendResponse) => {
this.dispatch(message, sender, sendResponse);
return true;
}
);
}
on<T>(type: string, handler: MessageHandler<T>) {
this.handlers.set(type, handler as MessageHandler);
}
private async dispatch(
message: { type: string; data?: unknown },
sender: chrome.runtime.MessageSender,
sendResponse: (response: unknown) => void
) {
const handler = this.handlers.get(message.type);
if (!handler) {
sendResponse({ error: `Unknown message type: ${message.type}` });
return;
}
try {
const result = await handler(message.data, sender);
sendResponse({ success: true, data: result });
} catch (error) {
sendResponse({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
async send<T>(type: string, data?: T): Promise<unknown> {
return chrome.runtime.sendMessage({ type, data });
}
async sendToTab<T>(
tabId: number,
type: string,
data?: T
): Promise<unknown> {
return chrome.tabs.sendMessage(tabId, { type, data });
}
}
export const messageBus = new MessageBus();
// background/handlers.ts
import { messageBus } from '../shared/message-bus';
messageBus.on('GET_TAB_INFO', async (data, sender) => {
const tab = await chrome.tabs.get(sender.tab?.id!);
return { id: tab.id, url: tab.url, title: tab.title };
});
messageBus.on('SAVE_DATA', async (data: { key: string; value: unknown }) => {
await chrome.storage.local.set({ [data.key]: data.value });
return { saved: true };
});
// content/index.ts
import { messageBus } from '../shared/message-bus';
// 发送消息
const tabInfo = await messageBus.send('GET_TAB_INFO');
console.log(tabInfo);
// 监听消息
messageBus.on('UPDATE_DOM', (data: { selector: string; html: string }) => {
const el = document.querySelector(data.selector);
if (el) el.textContent = data.html; // 使用 textContent 而非 innerHTML
});
18.3.2 仓储模式
// shared/repository.ts
interface Storable {
id: string;
updatedAt: number;
}
class Repository<T extends Storable> {
constructor(
private storageKey: string,
private area: 'local' | 'sync' | 'session' = 'local'
) {}
private get storage() {
return chrome.storage[this.area];
}
async getAll(): Promise<T[]> {
const result = await this.storage.get(this.storageKey);
return (result[this.storageKey] || []) as T[];
}
async getById(id: string): Promise<T | undefined> {
const items = await this.getAll();
return items.find(item => item.id === id);
}
async save(item: T): Promise<void> {
const items = await this.getAll();
const index = items.findIndex(i => i.id === item.id);
if (index >= 0) {
items[index] = { ...item, updatedAt: Date.now() };
} else {
items.push({ ...item, updatedAt: Date.now() });
}
await this.storage.set({ [this.storageKey]: items });
}
async delete(id: string): Promise<void> {
const items = await this.getAll();
const filtered = items.filter(item => item.id !== id);
await this.storage.set({ [this.storageKey]: filtered });
}
async query(predicate: (item: T) => boolean): Promise<T[]> {
const items = await this.getAll();
return items.filter(predicate);
}
onChange(callback: (items: T[]) => void) {
chrome.storage.onChanged.addListener((changes, area) => {
if (area === this.area && changes[this.storageKey]) {
callback(changes[this.storageKey].newValue || []);
}
});
}
}
// 使用
interface Note extends Storable {
title: string;
content: string;
tags: string[];
}
const noteRepo = new Repository<Note>('notes');
await noteRepo.save({
id: '1',
title: '笔记 1',
content: '内容...',
tags: ['work'],
updatedAt: Date.now()
});
const workNotes = await noteRepo.query(note =>
note.tags.includes('work')
);
18.4 调试技巧
18.4.1 Service Worker 调试
// 在 Service Worker 中添加调试日志
const DEBUG = true;
function debugLog(...args) {
if (DEBUG) {
console.log(`[SW ${new Date().toISOString()}]`, ...args);
}
}
// 监听 Service Worker 生命周期
chrome.runtime.onInstalled.addListener((details) => {
debugLog('onInstalled:', details.reason);
});
chrome.runtime.onStartup.addListener(() => {
debugLog('onStartup');
});
// 追踪 Service Worker 重启
let restartCount = 0;
debugLog('SW started, restart count:', ++restartCount);
18.4.2 Content Script 调试
// content/debug.js
// 在页面上显示调试信息
function showDebugPanel(info) {
if (!DEBUG) return;
const panel = document.createElement('div');
panel.id = 'ext-debug-panel';
panel.style.cssText = `
position: fixed;
bottom: 10px;
right: 10px;
background: rgba(0,0,0,0.85);
color: #0f0;
font-family: monospace;
font-size: 12px;
padding: 12px;
border-radius: 6px;
z-index: 2147483647;
max-width: 300px;
max-height: 200px;
overflow: auto;
`;
panel.textContent = JSON.stringify(info, null, 2);
document.body.appendChild(panel);
}
// 追踪消息通信
const originalSendMessage = chrome.runtime.sendMessage.bind(chrome.runtime);
chrome.runtime.sendMessage = async function (...args) {
debugLog('Sending:', args[0]);
const response = await originalSendMessage(...args);
debugLog('Response:', response);
return response;
};
18.4.3 网络请求调试
// 监控所有 fetch 请求
const originalFetch = globalThis.fetch;
globalThis.fetch = async function (...args) {
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
console.log(`[Fetch] ${url}`);
const start = Date.now();
try {
const response = await originalFetch.apply(this, args);
console.log(
`[Fetch] ${url} - ${response.status} (${Date.now() - start}ms)`
);
return response;
} catch (error) {
console.error(`[Fetch] ${url} - ERROR:`, error);
throw error;
}
};
18.4.4 存储调试
// 存储内容查看工具
async function dumpStorage(area = 'local') {
const data = await chrome.storage[area].get(null);
console.table(
Object.entries(data).map(([key, value]) => ({
key,
type: typeof value,
size: JSON.stringify(value).length,
preview: JSON.stringify(value).substring(0, 100)
}))
);
}
// 存储变更日志
chrome.storage.onChanged.addListener((changes, area) => {
console.group(`Storage changed (${area})`);
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
console.log(`${key}:`, oldValue, '→', newValue);
}
console.groupEnd();
});
18.5 错误处理
// shared/error-handler.ts
class ExtensionError extends Error {
constructor(
message: string,
public code: string,
public context?: Record<string, unknown>
) {
super(message);
this.name = 'ExtensionError';
}
}
class ErrorHandler {
static handle(error: unknown, context?: string) {
const err = error instanceof Error ? error : new Error(String(error));
console.error(`[${context || 'Extension'}]`, err.message);
// 上报错误(生产环境)
if (!DEBUG) {
this.reportError(err, context);
}
}
static async reportError(error: Error, context?: string) {
try {
await fetch('https://api.example.com/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: error.message,
stack: error.stack,
context,
version: chrome.runtime.getManifest().version,
timestamp: Date.now()
})
});
} catch {
// 静默失败
}
}
// 包装异步函数,自动处理错误
static wrap<T extends (...args: unknown[]) => Promise<unknown>>(
fn: T,
context?: string
): T {
return (async (...args: unknown[]) => {
try {
return await fn(...args);
} catch (error) {
this.handle(error, context);
throw error;
}
}) as T;
}
}
// 使用示例
const safeFetch = ErrorHandler.wrap(
async (url: string) => {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
'API'
);
18.6 扩展测试
18.6.1 单元测试
// tests/unit/storage.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock chrome.storage
const mockStorage = new Map();
const chromeMock = {
storage: {
local: {
get: vi.fn(async (keys) => {
if (typeof keys === 'string') {
return { [keys]: mockStorage.get(keys) };
}
return Object.fromEntries(
(keys as string[]).map(k => [k, mockStorage.get(k)])
);
}),
set: vi.fn(async (items) => {
Object.entries(items).forEach(([k, v]) => mockStorage.set(k, v));
}),
remove: vi.fn(async (keys) => {
(Array.isArray(keys) ? keys : [keys]).forEach(
k => mockStorage.delete(k)
);
})
}
}
};
global.chrome = chromeMock as any;
describe('StorageManager', () => {
beforeEach(() => {
mockStorage.clear();
vi.clearAllMocks();
});
it('should save and retrieve data', async () => {
await chrome.storage.local.set({ test: 'value' });
const result = await chrome.storage.local.get('test');
expect(result.test).toBe('value');
});
it('should handle multiple keys', async () => {
await chrome.storage.local.set({ a: 1, b: 2 });
const result = await chrome.storage.local.get(['a', 'b']);
expect(result).toEqual({ a: 1, b: 2 });
});
});
18.7 开发工作流总结
完整开发工作流:
1. 需求分析
├── 确定功能范围
├── 识别所需权限
└── 设计 UI 原型
2. 项目搭建
├── 选择技术栈 (Vite + TypeScript)
├── 配置 manifest.json
└── 搭建目录结构
3. 开发实现
├── Service Worker (后台逻辑)
├── Content Script (页面交互)
├── Popup / Options / Side Panel (UI)
└── 共享模块 (工具、类型、常量)
4. 测试验证
├── 单元测试 (Vitest)
├── 集成测试 (Puppeteer)
└── 手动测试 (Chrome DevTools)
5. 代码审查
├── ESLint 检查
├── TypeScript 类型检查
└── 安全检查
6. 构建打包
├── npm run build
├── ZIP 打包
└── 版本号更新
7. 发布上线
├── 上传 Chrome Web Store
├── 填写商店信息
├── 提交审核
└── 监控反馈
8. 维护迭代
├── 收集用户反馈
├── 修复 Bug
├── 功能迭代
└── 版本更新
18.8 扩展阅读
🎉 恭喜! 你已经完成了 Chrome 扩展开发完全指南的全部 18 章内容。现在你具备了从零开始构建、测试、发布和维护 Chrome 扩展的完整知识体系。祝你的扩展开发之旅顺利!