浏览器插件技术-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
相关推荐
szp200513 小时前
Cache API 会丢掉 URL 里的 #:我的 9 个模型分块一直写在同一条缓存上
javascript·浏览器
SoulRoar.2 天前
Armbian 上为虚拟桌面安装 Chromium 浏览器
浏览器·armbian·虚拟显示器
Summer-Bright3 天前
浏览器成了AI应用的新入口:智能体接管标签页之后,三本账谁买单
人工智能·chatgpt·agent·浏览器·智能体
ShineWinsu4 天前
对于CSS:弹性布局(Flexbox)的解析
前端·css·面试·浏览器·web·网页·弹性布局
Amos_Web5 天前
我做了一个 Chrome 插件,专治“这篇文章我明明看过,但重点去哪了?”
前端·浏览器
Ikalus19886 天前
我给自己写的工具,连着骗了我七次
chrome·浏览器
ShineWinsu8 天前
对于HTML:HTML常见标签的解析—上
前端·面试·html·浏览器·标签·图片·超链接
Frag0ut15 天前
Chrome 浏览器安全指南:常见风险、隐私设置与防护建议
chrome·浏览器·隐私保护·安全防护·账号安全·安全设置·扩展权限
Frag0ut16 天前
Chrome四大版本获取及共存指南:Stable/Beta/Dev/Canary
前端·chrome·浏览器·dev·beta·canary·共存版
Frag0ut17 天前
新兴浏览器内核能和 Chrome 有一战之力吗?Servo、Ladybird、WebKit 与 Gecko 的技术格局分析
chrome·浏览器·webkit·blink·gecko·servo·ladybird