一、技术选型
| 维度 | 最终选择 | 核心理由 |
|---|---|---|
| 扩展框架 | 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 事件,耦合度最低。
四、关键避坑点
-
扩展 ID 不是项目名 :它是 Chrome 基于密钥生成的独立字符串。开发时路径一变 ID 就变,建议尽早用
manifest.key固定。 -
Shadow DOM 样式隔离 :Element Plus 的
:rootCSS 变量不会自动进入 Shadow DOM,需要替换为:host或使用cssInjectionMode: 'ui'。 -
MV3 网络拦截限制:无法实时修改请求,爬虫/同步逻辑要转向"页面内 Hook"或"声明式规则"思路。
-
凭证安全 :CSDN/掘金的登录凭证应存在扩展的
storage中,由后台 Service Worker 调用 API,不要暴露给网页。 -
测试策略:纯函数提取逻辑用 Vitest 快速验证;完整流程用 Playwright 的持久化上下文跑通。
五、项目结构参考
blog-sync-extension/ ├── entrypoints/ │ ├── content/ # 注入博客页面,监听事件并转发 │ ├── background.ts # 接收文章,调用各平台 API │ └── popup/ # 配置凭证、查看同步状态 ├── components/ # Vue 组件(自动导入) ├── composables/ # 组合式函数 ├── utils/ # 消息/存储封装 ├── wxt.config.ts # manifest、权限、externally_connectable └── package.json