浏览器插件技术-wxt

一、技术选型

维度 最终选择 核心理由
扩展框架 WXT 基于 Vite,热更新快;对 Vue 3 中立友好;维护活跃,不像 Plasmo 已进入维护模式
Manifest 版本 V3 Chrome 强制推行,V2 已彻底淘汰,没有选择余地
前端框架 Vue 3 你的技术栈偏好,WXT 官方支持
UI 组件库 Element Plus 静态 CSS 分发,Shadow DOM 适配成本比 Ant Design Vue 低;维护状态更健康
抓取方式 Content Script 读 DOM + Main World Hook(如需) MV3 下阻塞式 webRequest 已不可用,页面内 Hook 是主流替代方案
测试 Vitest(单元)+ Playwright(E2E) WXT 官方对两者均有一等支持

二、整体架构

复制代码
博客页面(你的网站)
      │
      │ 派发 Custom DOM Event 或 window.postMessage
      ▼
Content Script(注入到博客页面)
      │
      │ runtime.sendMessage
      ▼
Background Service Worker(WXT 后台)
      │
      ├──► CSDN API(发布)
      ├──► 掘金 API(发布)
      └──► chrome.storage(凭证/状态)

核心思路:博客页面负责"选中文章",WXT 插件负责"同步到其他平台"。网页不直接调用各平台 API,所有发布逻辑收敛在扩展后台,凭证不暴露给网页。

三、网页与扩展通信的两种方案

方案一:externally_connectable

WXT 配置

复制代码
// wxt.config.ts
import { defineConfig } from 'wxt';

export default defineConfig({
  modules: ['@wxt-dev/module-vue'],
  manifest: {
    externally_connectable: {
      matches: [
        'http://localhost:5173/*',
        'http://127.0.0.1:5173/*',
      ],
    },
    permissions: ['storage'],
  },
});

扩展后台监听

复制代码
// entrypoints/background.ts
export default defineBackground(() => {
  browser.runtime.onMessageExternal.addListener(
    (message, sender, sendResponse) => {
      const allowedOrigins = [
        'http://localhost:5173',
        'http://127.0.0.1:5173',
      ];
      if (!sender.origin || !allowedOrigins.includes(sender.origin)) {
        sendResponse({ success: false, error: '未授权的来源' });
        return true;
      }

      if (message.action === 'sync-article') {
        console.log('收到文章:', message.article.title);
        browser.storage.local.set({
          lastSyncedArticle: message.article,
        });
        sendResponse({ success: true, message: '已接收' });
      }
      return true;
    }
  );
});
复制代码

博客页面发送

复制代码
// 你的博客网站代码
const EXTENSION_ID = '你的扩展ID'; // 从 chrome://extensions 获取

function syncArticle(article) {
  if (!window.chrome?.runtime?.sendMessage) {
    alert('请先安装同步插件');
    return;
  }

  chrome.runtime.sendMessage(
    EXTENSION_ID,
    { action: 'sync-article', article },
    (response) => {
      if (chrome.runtime.lastError) {
        console.error('通信失败:', chrome.runtime.lastError.message);
        return;
      }
      console.log('同步结果:', response);
    }
  );
}

// 用户点击"同步"按钮时调用
syncArticle({
  title: '我的文章标题',
  content: '# 正文内容...',
  tags: ['技术', '前端'],
});
复制代码

特点 :网页直接调用扩展 API,代码简洁,但需要知道扩展 ID,且 Firefox 不支持。


方案二:桥接方式(DOM 事件)

WXT 配置

复制代码
// wxt.config.ts
import { defineConfig } from 'wxt';

export default defineConfig({
  modules: ['@wxt-dev/module-vue'],
  manifest: {
    host_permissions: [
      'http://localhost:5173/*',
      'http://127.0.0.1:5173/*',
    ],
    permissions: ['storage'],
  },
});
复制代码

内容脚本注入博客页面

复制代码
// entrypoints/content/index.ts
export default defineContentScript({
  matches: [
    'http://localhost:5173/*',
    'http://127.0.0.1:5173/*',
  ],
  main() {
    document.addEventListener('BLOG_SYNC_ARTICLE', (event: Event) => {
      const customEvent = event as CustomEvent;
      const article = customEvent.detail;

      if (!article?.title || !article?.content) {
        console.warn('文章数据格式不正确');
        return;
      }

      browser.runtime.sendMessage({
        action: 'sync-article',
        article,
      });
    });
  },
});
复制代码

扩展后台接收

复制代码
// entrypoints/background.ts
export default defineBackground(() => {
  browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.action === 'sync-article') {
      console.log('收到文章:', message.article.title);
      browser.storage.local.set({
        lastSyncedArticle: message.article,
      });
      sendResponse({ success: true, message: '已接收' });
    }
    return true;
  });
});
复制代码

博客页面派发事件

复制代码
// 你的博客网站代码 ------ 不依赖任何 chrome.* API
function syncArticle(article) {
  const event = new CustomEvent('BLOG_SYNC_ARTICLE', {
    detail: article,
  });
  document.dispatchEvent(event);
}

// 用户点击"同步"按钮时调用
syncArticle({
  title: '我的文章标题',
  content: '# 正文内容...',
  tags: ['技术', '前端'],
});
复制代码

特点 :网页代码完全不依赖 chrome.* API,Firefox 也能用,但需要扩展注入内容脚本到你的博客页面。


两种方案对比

externally_connectable 桥接方式(DOM 事件)
Firefox 支持 ❌ 不支持 ✅ 支持
需要扩展 ID 需要 不需要
网页代码依赖 chrome.runtime.sendMessage 纯 DOM 事件,无扩展 API
前提条件 扩展已安装且域名在 matches 中 扩展的内容脚本已注入博客页面
安全性 由 matches 限制域名 内容脚本需自行校验数据格式
响应机制 支持回调,可拿到扩展返回值 单向事件,如需响应可再派发反向事件

建议 :如果你的博客希望覆盖 Firefox 用户,或者不想在网页代码里硬编码扩展 ID,桥接方式是更稳妥的选择。它把"扩展相关"的逻辑完全收敛在内容脚本里,网页端只负责派发一个标准 DOM 事件,耦合度最低。

四、关键避坑点

  1. 扩展 ID 不是项目名 :它是 Chrome 基于密钥生成的独立字符串。开发时路径一变 ID 就变,建议尽早用 manifest.key 固定。

  2. Shadow DOM 样式隔离 :Element Plus 的 :root CSS 变量不会自动进入 Shadow DOM,需要替换为 :host 或使用 cssInjectionMode: 'ui'。

  3. MV3 网络拦截限制:无法实时修改请求,爬虫/同步逻辑要转向"页面内 Hook"或"声明式规则"思路。

  4. 凭证安全 :CSDN/掘金的登录凭证应存在扩展的 storage 中,由后台 Service Worker 调用 API,不要暴露给网页。

  5. 测试策略:纯函数提取逻辑用 Vitest 快速验证;完整流程用 Playwright 的持久化上下文跑通。

五、项目结构参考

复制代码
blog-sync-extension/
├── entrypoints/
│   ├── content/          # 注入博客页面,监听事件并转发
│   ├── background.ts     # 接收文章,调用各平台 API
│   └── popup/            # 配置凭证、查看同步状态
├── components/           # Vue 组件(自动导入)
├── composables/          # 组合式函数
├── utils/                # 消息/存储封装
├── wxt.config.ts         # manifest、权限、externally_connectable
└── package.json
相关推荐
Frag0ut1 天前
Chrome打开某网站突然卡顿,是中了“挖矿病毒”吗?
chrome·网络安全·浏览器·chromium·挖矿病毒·技术科普
追光者491 天前
把 10 个小工具做成"单文件网页":不装、不联网、不上传,我踩过的 7 个坑
浏览器
竹林8182 天前
把神经网络塞进一个浏览器标签页:端侧视觉 AI 的工程真相
前端·浏览器
竹林81813 天前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
嘉琪coder18 天前
我做了一个 Chrome 扩展,把 YouTube 播放列表批量变成 AI 可读的本地 Markdown
chrome·开源·浏览器
若丶相见20 天前
Codex、Claude Code、WorkBuddy + Tabbit CLI:让 AI 操控浏览器发文章
人工智能·浏览器
围炉聊科技21 天前
Playwright Test Agents 三件套实测 ——智能体基建系列
浏览器·ai编程·测试
竹林81821 天前
现代浏览器插件早已不是小脚本:从 MV3 架构、跨进程通信到端侧 AI 的工程化实战
浏览器
贝锐25 天前
贝锐洋葱头浏览器技术解析:企业多账号管理与安全隔离实践
浏览器