强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 扩展的完整知识体系。祝你的扩展开发之旅顺利!