欢迎光临
我们一直在努力

Chrome插件Manifest V3完全开发指南:从架构到实战发布

引言:为什么现在要学Chrome插件开发?

Chrome浏览器插件(Chrome Extensions)是提升浏览器功能、定制Web体验的利器。2024年Google全面强制迁移至Manifest V3,废弃了MV2的background page机制,转而使用Service Worker。这意味着如果你还在用MV2的写法,你的插件已经在Chrome Web Store上无法更新了。本文将从零开始,手把手教你构建一个符合Manifest V3标准的现代化Chrome插件,涵盖核心API、消息通信、存储方案、Side Panel以及实际发布流程。

一、Manifest V3架构概览

Manifest V3(简称MV3)是Chrome插件的最新架构规范,相比MV2有几个根本性变化:

  • Service Worker替代Background Page:不再有持久运行的后台页面,取而代之的是事件驱动的Service Worker,空闲时会被回收
  • 移除远程代码执行:所有代码必须打包进插件,不允许从外部加载和执行JavaScript
  • declarativeNetRequest替代webRequest:网络请求拦截改为声明式API,减少性能开销
  • 新的权限模型:权限粒度更细,支持运行时权限请求

一个标准的MV3插件目录结构如下:

my-extension/
├── manifest.json          # 插件配置清单(必需)
├── background.js          # Service Worker入口
├── content.js             # 注入到网页的内容脚本
├── popup.html             # 点击插件图标弹出的面板
├── popup.js               # popup的逻辑
├── popup.css              # popup的样式
├── sidepanel.html         # 侧边栏面板(MV3新增)
├── sidepanel.js           # 侧边栏逻辑
├── options.html           # 设置页面
├── icons/
│   ├── icon16.png
│   ├── icon48.png
│   └── icon128.png
└── _locales/              # 国际化资源
    ├── zh_CN/
    │   └── messages.json
    └── en/
        └── messages.json

二、manifest.json深度解析

manifest.json是Chrome插件的核心配置文件,每个字段都有明确的作用。下面是一个完整的MV3 manifest示例:

{
  "manifest_version": 3,
  "name": "__MSG_extName__",
  "version": "1.0.0",
  "description": "__MSG_extDesc__",
  "default_locale": "zh_CN",
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "permissions": [
    "storage",
    "activeTab",
    "scripting",
    "sidePanel",
    "contextMenus",
    "notifications",
    "alarms"
  ],
  "host_permissions": [
    "https://*.example.com/*"
  ],
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png"
    },
    "default_title": "点击打开面板"
  },
  "content_scripts": [
    {
      "matches": ["https://*.example.com/*"],
      "js": ["content.js"],
      "css": ["content.css"],
      "run_at": "document_idle"
    }
  ],
  "side_panel": {
    "default_path": "sidepanel.html"
  },
  "options_page": "options.html",
  "minimum_chrome_version": "116",
  "web_accessible_resources": [
    {
      "resources": ["images/*", "fonts/*"],
      "matches": [""]
    }
  ]
}

关键字段说明:

  • manifest_version:必须是3,这是MV3的标识
  • permissions:声明插件需要的Chrome API权限,每个权限在安装时会向用户展示
  • host_permissions:控制插件可以访问哪些网站的数据,MV3中从permissions中分离出来
  • background.service_worker:指定Service Worker的入口文件,注意MV3只支持一个入口
  • content_scripts:配置自动注入到匹配网页中的脚本和样式
  • type: “module”:允许在Service Worker中使用ES Module语法import/export

三、Service Worker:插件的大脑

MV3的Service Worker是插件的事件处理中心。与Web Service Worker类似,它不常驻内存,而是在收到事件时被唤起,处理完成后自动休眠。这意味着你需要特别注意状态管理——不能把可变状态存在Service Worker的全局变量中。

// background.js — Service Worker入口
// 使用chrome.alarms代替setTimeout,因为SW休眠后setTimeout会失效

// 插件安装时的初始化
chrome.runtime.onInstalled.addListener((details) => {
  console.log('插件已安装,原因:', details.reason);

  // 创建右键菜单
  chrome.contextMenus.create({
    id: 'search-selection',
    title: '使用自定义搜索: "%s"',
    contexts: ['selection']
  });

  // 设置默认配置
  chrome.storage.local.set({
    settings: {
      theme: 'light',
      enableNotifications: true,
      apiEndpoint: 'https://api.example.com'
    }
  });

  // 创建定时任务
  chrome.alarms.create('sync-data', { periodInMinutes: 30 });
});

// 处理右键菜单点击
chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === 'search-selection') {
    const query = encodeURIComponent(info.selectionText);
    chrome.tabs.create({
      url: `https://www.google.com/search?q=${query}`
    });
  }
});

// 处理定时任务
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'sync-data') {
    syncRemoteData();
  }
});

// 处理来自content script和popup的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  switch (message.type) {
    case 'GET_DATA':
      handleGetData(message.payload).then(sendResponse);
      return true; // 保持消息通道开放,等待异步响应

    case 'SAVE_TAB_INFO':
      saveTabInfo(sender.tab, message.payload);
      sendResponse({ success: true });
      break;

    case 'OPEN_SIDE_PANEL':
      chrome.sidePanel.open({ tabId: sender.tab.id });
      sendResponse({ success: true });
      break;
  }
});

// 监听标签页更新事件
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete' && tab.url) {
    if (tab.url.includes('github.com')) {
      chrome.tabs.sendMessage(tabId, {
        type: 'PAGE_READY',
        url: tab.url
      }).catch(() => {});
    }
  }
});

async function handleGetData(payload) {
  const data = await chrome.storage.local.get(payload.keys);
  return { success: true, data };
}

async function saveTabInfo(tab, info) {
  const { tabHistory = [] } = await chrome.storage.local.get('tabHistory');
  tabHistory.push({
    url: tab.url,
    title: tab.title,
    info,
    timestamp: Date.now()
  });
  if (tabHistory.length > 100) tabHistory.splice(0, tabHistory.length - 100);
  await chrome.storage.local.set({ tabHistory });
}

async function syncRemoteData() {
  try {
    const { settings } = await chrome.storage.local.get('settings');
    const response = await fetch(`${settings.apiEndpoint}/sync`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' }
    });
    const data = await response.json();
    await chrome.storage.local.set({ syncedData: data });
  } catch (error) {
    console.error('同步失败:', error);
  }
}

3.1 Service Worker的生命周期陷阱

Service Worker在空闲约30秒后会被Chrome终止。以下是你必须注意的几个陷阱:

  • 不要用全局变量存储状态:SW被杀死后全局变量会丢失,使用chrome.storage代替
  • 不要用setTimeout/setInterval:SW休眠后这些定时器会失效,使用chrome.alarms代替
  • 长时间操作需要保活:可以使用chrome.runtime.Port或者定期发送消息来延长SW的生命周期
  • 异步操作要小心:使用async/await确保操作完成后再返回

四、Content Script:网页中的代理人

Content Script运行在网页的上下文中,可以读取和修改DOM,但它运行在独立的JavaScript沙箱中,无法直接访问网页的全局变量。通过Isolated World机制,你的脚本与网页脚本互不干扰。

// content.js — 注入到目标网页的内容脚本

// 检查是否已经注入过(防止重复注入)
if (!window.__myExtensionInjected) {
  window.__myExtensionInjected = true;
  initContentScript();
}

function initContentScript() {
  console.log('[MyExtension] Content script已加载');

  // 1. 读取页面信息
  const pageInfo = {
    title: document.title,
    url: window.location.href,
    meta: getMetaTags(),
    headings: getHeadings()
  };

  // 2. 向Service Worker发送页面信息
  chrome.runtime.sendMessage({
    type: 'SAVE_TAB_INFO',
    payload: pageInfo
  });

  // 3. 注入自定义UI元素
  injectFloatingButton();

  // 4. 监听来自Service Worker的消息
  chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
    if (msg.type === 'PAGE_READY') {
      console.log('[MyExtension] 收到页面就绪通知:', msg.url);
      sendResponse({ received: true });
    }
    if (msg.type === 'HIGHLIGHT_TEXT') {
      highlightMatches(msg.payload.keyword);
      sendResponse({ success: true });
    }
  });

  // 5. 使用MutationObserver监听DOM变化
  const observer = new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      if (mutation.type === 'childList') {
        mutation.addedNodes.forEach(node => {
          if (node.nodeType === Node.ELEMENT_NODE) {
            processNewElement(node);
          }
        });
      }
    }
  });

  observer.observe(document.body, {
    childList: true,
    subtree: true
  });
}

function getMetaTags() {
  const metas = {};
  document.querySelectorAll('meta[name], meta[property]').forEach(el => {
    const key = el.getAttribute('name') || el.getAttribute('property');
    metas[key] = el.getAttribute('content');
  });
  return metas;
}

function getHeadings() {
  return Array.from(document.querySelectorAll('h1, h2, h3')).map(h => ({
    level: h.tagName,
    text: h.textContent.trim()
  }));
}

function injectFloatingButton() {
  const btn = document.createElement('div');
  btn.id = 'my-ext-floating-btn';
  btn.innerHTML = '🔧';
  btn.style.cssText = `
    position: fixed; bottom: 20px; right: 20px; z-index: 2147483647;
    width: 48px; height: 48px; border-radius: 50%;
    background: #4285f4; color: white; font-size: 24px;
    display: flex; align-items: center; justify-content: center;
    cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,0.3);
    transition: transform 0.2s;
  `;
  btn.addEventListener('mouseenter', () => btn.style.transform = 'scale(1.1)');
  btn.addEventListener('mouseleave', () => btn.style.transform = 'scale(1)');
  btn.addEventListener('click', () => {
    chrome.runtime.sendMessage({ type: 'OPEN_SIDE_PANEL' });
  });
  document.body.appendChild(btn);
}

function highlightMatches(keyword) {
  document.querySelectorAll('.my-ext-highlight').forEach(el => {
    el.replaceWith(el.textContent);
  });

  if (!keyword) return;

  const walker = document.createTreeWalker(
    document.body,
    NodeFilter.SHOW_TEXT,
    null,
    false
  );

  const textNodes = [];
  while (walker.nextNode()) textNodes.push(walker.currentNode);

  textNodes.forEach(node => {
    const idx = node.textContent.toLowerCase().indexOf(keyword.toLowerCase());
    if (idx !== -1) {
      const range = document.createRange();
      range.setStart(node, idx);
      range.setEnd(node, idx + keyword.length);
      const span = document.createElement('span');
      span.className = 'my-ext-highlight';
      span.style.cssText = 'background: #ffeb3b; padding: 1px 2px; border-radius: 2px;';
      range.surroundContents(span);
    }
  });
}

function processNewElement(element) {
  // 对新增元素执行自定义逻辑
}

4.1 Content Script与网页通信

Content Script不能直接访问网页的JavaScript变量,但可以通过window.postMessage进行通信。这在需要与嵌入的iframe或者网页自定义脚本交互时非常有用:

// 在content.js中向网页发送消息
window.postMessage({ type: 'FROM_EXTENSION', data: 'hello' }, '*');

// 在content.js中监听网页的消息
window.addEventListener('message', (event) => {
  // 安全检查:只处理来自同源的消息
  if (event.source !== window) return;
  if (event.data.type && event.data.type === 'FROM_PAGE') {
    console.log('收到来自网页的消息:', event.data.payload);
    // 转发给Service Worker
    chrome.runtime.sendMessage({
      type: 'PAGE_MESSAGE',
      payload: event.data.payload
    });
  }
});

五、Chrome Storage API:数据持久化方案

MV3提供了三种存储方案,各有适用场景:

API 存储位置 容量 同步 适用场景
chrome.storage.local 本地磁盘 10MB(可申请unlimitedStorage) 大量本地数据缓存
chrome.storage.sync Google账号 100KB(单项8KB) 跨设备同步 用户设置、偏好
chrome.storage.session 内存 10MB 否(SW重启后丢失) 临时状态数据
// storage.js — 统一的存储管理模块

class ExtensionStorage {
  // 保存数据
  static async set(key, value, area = 'local') {
    const storage = chrome.storage[area];
    return new Promise((resolve, reject) => {
      storage.set({ [key]: value }, () => {
        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message));
        } else {
          resolve();
        }
      });
    });
  }

  // 读取数据
  static async get(key, area = 'local') {
    const storage = chrome.storage[area];
    return new Promise((resolve, reject) => {
      storage.get(key, (result) => {
        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message));
        } else {
          resolve(typeof key === 'string' ? result[key] : result);
        }
      });
    });
  }

  // 删除数据
  static async remove(key, area = 'local') {
    const storage = chrome.storage[area];
    return new Promise((resolve) => {
      storage.remove(key, resolve);
    });
  }

  // 监听存储变化
  static onChanged(callback) {
    chrome.storage.onChanged.addListener((changes, areaName) => {
      const formatted = {};
      for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
        formatted[key] = { oldValue, newValue };
      }
      callback(formatted, areaName);
    });
  }

  // 批量操作
  static async batchSet(obj, area = 'local') {
    const storage = chrome.storage[area];
    return new Promise((resolve) => storage.set(obj, resolve));
  }
}

// 使用示例
async function demo() {
  await ExtensionStorage.set('theme', 'dark', 'sync');
  await ExtensionStorage.set('fontSize', 14, 'sync');

  await ExtensionStorage.set('cachedPages', {
    'example.com': { html: '...', timestamp: Date.now() }
  });

  await ExtensionStorage.set('currentTask', { step: 2 }, 'session');

  ExtensionStorage.onChanged((changes, area) => {
    console.log(`[${area}] 数据变化:`, changes);
  });

  const theme = await ExtensionStorage.get('theme', 'sync');
  console.log('当前主题:', theme);
}

六、Side Panel:MV3的杀手级特性

Side Panel(侧边栏)是Chrome 114引入的MV3专属功能,允许你在浏览器右侧显示一个持久化的面板。与Popup不同,Side Panel不会因为点击其他区域而关闭,非常适合做笔记工具、AI助手、实时数据分析等场景。

<!-- sidepanel.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, 'Segoe UI', sans-serif;
      background: #f8f9fa; color: #333; padding: 16px;
    }
    .header {
      display: flex; align-items: center; gap: 8px;
      margin-bottom: 16px; padding-bottom: 12px;
      border-bottom: 1px solid #e0e0e0;
    }
    .header h1 { font-size: 18px; font-weight: 600; }
    .tabs { display: flex; gap: 4px; margin-bottom: 12px; }
    .tab {
      padding: 6px 16px; border: 1px solid #ddd;
      border-radius: 16px; cursor: pointer; font-size: 13px;
      background: white; transition: all 0.2s;
    }
    .tab.active { background: #4285f4; color: white; border-color: #4285f4; }
    .card {
      background: white; border-radius: 8px; padding: 12px;
      margin-bottom: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
    }
    .card h3 { font-size: 14px; margin-bottom: 6px; }
    .card p { font-size: 13px; color: #666; line-height: 1.5; }
    #content { min-height: 200px; }
    .note-input {
      width: 100%; min-height: 80px; padding: 10px;
      border: 1px solid #ddd; border-radius: 8px;
      font-size: 13px; resize: vertical; margin-bottom: 8px;
    }
    .btn {
      padding: 8px 16px; border: none; border-radius: 6px;
      background: #4285f4; color: white; cursor: pointer;
      font-size: 13px;
    }
    .btn:hover { background: #3367d6; }
  </style>
</head>
<body>
  <div class="header">
    <span>🔧</span>
    <h1>我的助手</h1>
  </div>
  <div class="tabs">
    <div class="tab active" data-tab="info">页面信息</div>
    <div class="tab" data-tab="notes">笔记</div>
    <div class="tab" data-tab="settings">设置</div>
  </div>
  <div id="content"></div>
  <script src="sidepanel.js"></script>
</body>
</html>
// sidepanel.js
const tabs = document.querySelectorAll('.tab');
const content = document.getElementById('content');

tabs.forEach(tab => {
  tab.addEventListener('click', () => {
    tabs.forEach(t => t.classList.remove('active'));
    tab.classList.add('active');
    renderTab(tab.dataset.tab);
  });
});

async function renderTab(tabName) {
  switch (tabName) {
    case 'info': await renderPageInfo(); break;
    case 'notes': await renderNotes(); break;
    case 'settings': await renderSettings(); break;
  }
}

async function renderPageInfo() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  const { tabHistory = [] } = await chrome.storage.local.get('tabHistory');
  const currentHistory = tabHistory.filter(h => h.url === tab.url);

  content.innerHTML = `
    

当前页面

标题: ${tab.title} URL: ${tab.url}

访问历史 (${currentHistory.length}次)

${currentHistory.slice(-5).map(h => ` ${new Date(h.timestamp).toLocaleString('zh-CN')} `).join('')}
`; } async function renderNotes() { const { notes = [] } = await chrome.storage.local.get('notes'); content.innerHTML = `
${notes.slice().reverse().map(n => `
${n.text}

${new Date(n.time).toLocaleString('zh-CN')}

`).join('')}
`; document.getElementById('saveNote').addEventListener('click', async () => { const text = document.getElementById('noteText').value.trim(); if (!text) return; notes.push({ text, time: Date.now() }); await chrome.storage.local.set({ notes }); renderNotes(); }); } async function renderSettings() { const { settings } = await chrome.storage.local.get('settings'); content.innerHTML = `

插件设置

主题: ${settings?.theme || 'light'} 通知: ${settings?.enableNotifications ? '开启' : '关闭'}
`; } renderTab('info');

七、Popup面板开发

Popup是用户点击插件图标时弹出的小窗口。它与Side Panel的关键区别是:Popup会因失去焦点而关闭,适合做快速操作面板。

<!-- popup.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <style>
    body { width: 350px; padding: 16px; font-family: sans-serif; }
    .search-box {
      width: 100%; padding: 10px; border: 2px solid #e0e0e0;
      border-radius: 8px; font-size: 14px; outline: none;
      transition: border-color 0.2s;
    }
    .search-box:focus { border-color: #4285f4; }
    .actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; }
    .action-btn {
      padding: 10px; border: 1px solid #e0e0e0; border-radius: 8px;
      background: white; cursor: pointer; text-align: center;
      transition: all 0.2s;
    }
    .action-btn:hover { background: #f0f4ff; border-color: #4285f4; }
    .action-btn .icon { font-size: 20px; }
    .action-btn .label { font-size: 12px; margin-top: 4px; color: #666; }
    #result { margin-top: 12px; }
  </style>
</head>
<body>
  <input class="search-box" id="search" placeholder="搜索当前页面...">
  <div class="actions">
    <div class="action-btn" id="btnInfo">
      <div class="icon">📊</div>
      <div class="label">页面信息</div>
    </div>
    <div class="action-btn" id="btnSidePanel">
      <div class="icon">📌</div>
      <div class="label">打开侧栏</div>
    </div>
    <div class="action-btn" id="btnScreenshot">
      <div class="icon">📸</div>
      <div class="label">截图</div>
    </div>
    <div class="action-btn" id="btnSettings">
      <div class="icon">⚙️</div>
      <div class="label">设置</div>
    </div>
  </div>
  <div id="result"></div>
  <script src="popup.js"></script>
</body>
</html>
// popup.js
document.getElementById('search').addEventListener('input', async (e) => {
  const keyword = e.target.value.trim();
  if (!keyword) return;

  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  chrome.tabs.sendMessage(tab.id, {
    type: 'HIGHLIGHT_TEXT',
    payload: { keyword }
  });
});

document.getElementById('btnSidePanel').addEventListener('click', async () => {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  await chrome.sidePanel.open({ tabId: tab.id });
  window.close();
});

document.getElementById('btnScreenshot').addEventListener('click', async () => {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  const dataUrl = await chrome.tabs.captureVisibleTab(null, { format: 'png' });
  chrome.downloads.download({
    url: dataUrl,
    filename: `screenshot-${Date.now()}.png`
  });
  window.close();
});

八、调试技巧与常见问题

Chrome插件的调试比普通Web开发复杂一些,因为你需要同时调试多个上下文:

8.1 调试不同组件

  • Service Worker:在 chrome://extensions/ 找到你的插件,点击”Service Worker”链接,打开DevTools
  • Popup:右键点击插件图标 → “检查弹出内容”
  • Content Script:在网页的DevTools中,Sources面板左侧会出现”Content scripts”分类
  • Side Panel:在Side Panel打开时,右键 → “检查”

8.2 常见报错与解决

错误信息 原因 解决方案
Unchecked runtime.lastError: Could not establish connection Content Script未加载就发送消息 发送前检查tab是否完成加载,或使用try-catch
The message port closed before a response was received 消息处理函数没有返回true 异步消息处理需要return true保持通道开放
Service worker registration failed Service Worker代码有语法错误 检查background.js的语法,查看Service Worker DevTools的Console
Permission ‘xxx’ is not recognized manifest.json中权限名拼写错误 检查Chrome官方文档的权限列表
Content Security Policy violation 在页面中执行了内联脚本 MV3禁止内联脚本,所有JS必须是独立文件

8.3 热重载开发

开发时每次修改代码都需要手动点击刷新按钮重新加载插件,效率很低。推荐使用web-ext工具实现自动重载:

# 安装web-ext
npm install -g web-ext

# 启动开发模式(自动重载 + 自动打开Chrome)
web-ext run --source-dir ./my-extension

# 指定Chrome路径
web-ext run --source-dir ./my-extension --chromium-binary /usr/bin/google-chrome

# 只重载不打开新窗口
web-ext run --source-dir ./my-extension --no-input --reload

九、打包发布到Chrome Web Store

开发完成后,需要将插件发布到Chrome Web Store供用户安装:

# 1. 打包为zip(排除不需要的文件)
cd my-extension
zip -r ../my-extension-v1.0.0.zip . -x ".git/*" "node_modules/*" "*.md" "tests/*"

# 2. 使用web-ext打包(推荐,会自动验证)
web-ext build --source-dir ./my-extension --artifacts-dir ./dist

# 3. 验证包内容
web-ext lint --source-dir ./my-extension

发布流程:

  1. 访问 Chrome Web Store Developer Dashboard
  2. 支付一次性注册费 $5(个人开发者账号)
  3. 点击”New Item”上传zip文件
  4. 填写插件描述、截图(至少1张1280×800或640×400)、隐私政策
  5. 设置可见性(Public/Unlisted)
  6. 提交审核(通常需要几小时到3天)

审核常见被拒原因:权限请求过多且未说明用途、隐私政策缺失、描述与实际功能不符、使用了远程代码执行。建议只申请最小必要权限,并在描述中清楚说明每个权限的使用场景。

十、实战:构建一个网页摘要AI助手

最后,我们用一个实际项目串联所有知识。这个插件可以提取当前网页的核心内容,并通过API发送给AI进行摘要:

// content_script_extractor.js — 提取页面核心内容
function extractArticleContent() {
  let article = document.querySelector('article');
  if (!article) {
    article = document.querySelector(
      '[role="main"], .post-content, .article-body, .entry-content, #content'
    );
  }
  if (!article) article = document.body;

  const text = article.innerText
    .replace(/\n{3,}/g, '\n\n')
    .trim();

  return {
    title: document.title,
    url: window.location.href,
    text: text.substring(0, 8000),
    wordCount: text.split(/\s+/).length,
    extractedAt: new Date().toISOString()
  };
}

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === 'EXTRACT_CONTENT') {
    const content = extractArticleContent();
    sendResponse(content);
  }
  return true;
});
// background.js中增加摘要处理逻辑
async function summarizePage(tabId) {
  // 1. 从content script提取内容
  const pageContent = await chrome.tabs.sendMessage(tabId, {
    type: 'EXTRACT_CONTENT'
  });

  // 2. 调用AI API生成摘要
  const { settings } = await chrome.storage.local.get('settings');
  const response = await fetch(`${settings.apiEndpoint}/summarize`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${settings.apiKey}`
    },
    body: JSON.stringify({
      text: pageContent.text,
      title: pageContent.title,
      language: 'zh-CN'
    })
  });

  const { summary, keywords } = await response.json();

  // 3. 存储摘要结果
  const { summaries = {} } = await chrome.storage.local.get('summaries');
  summaries[pageContent.url] = {
    title: pageContent.title,
    summary,
    keywords,
    createdAt: Date.now()
  };
  await chrome.storage.local.set({ summaries });

  // 4. 发送通知
  chrome.notifications.create({
    type: 'basic',
    iconUrl: 'icons/icon128.png',
    title: '摘要生成完成',
    message: `已为「${pageContent.title}」生成摘要,点击侧栏查看`
  });

  return summary;
}

总结

Chrome插件Manifest V3的开发模式与传统Web开发有显著不同,核心要点回顾:

  • Service Worker是事件驱动的,不要依赖全局状态,使用chrome.storage持久化数据
  • Content Script运行在隔离沙箱中,通过消息API与Service Worker通信
  • Side Panel提供了比Popup更好的持久化面板体验,适合复杂交互场景
  • 使用web-ext工具链进行开发和打包,提高开发效率
  • 遵循最小权限原则,只申请必要的API权限

Chrome插件开发的门槛不高,但要做一个好用、安全、高性能的插件,需要深入理解MV3的架构设计和各种边界情况。希望本文能帮助你快速上手并避开常见的坑。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Chrome插件Manifest V3完全开发指南:从架构到实战发布
分享到: 更多 (0)