Vite 8 版 Chrome 插件全家桶,popup/options/sidepanel 一次集齐

之前开了一个系列记录 chrome 插件的开发过程,讲了怎么用 vite + vue3 把模板工程搭起来,顺带踩了 contentScript 样式隔离的坑,感兴趣的jym可以去翻一下 📌

这阵子模板仓库又迭代了一波,主要更新了三个点:

  • 🗂️ 支持 sidepanel + options 多页面
  • 📨 统一的类型化消息通信
  • 🚀 依赖全面升级到 vite 8

所以这篇就把这三个更新点盘一遍,老规矩单篇两千字左右 🚲,看不完就点个赞。

更新总览 ✨

更新点 解决了什么 核心文件
多页面支持 popup / options / sidepanel 一个模板全搞定 src/manifest.jsonvite.config.ts
类型化消息通信 taskId 写错、参数不对,编译期就能拦住 src/shared/message.ts
Vite 8 升级 依赖大版本刷新 + 工程化体验重构 vite*.config.tseslint.config.mjs

多页面:popup / options / sidepanel 🗂️

很多插件模板只带一个 popup,真要上 options 或侧边栏,得自己改 manifest、加打包入口、写复制脚本,坑不少。这个模板把四种页面全部内置了,src/manifest.json 里一眼可见:

json 复制代码
{
  "action": { "default_popup": "/popup/index.html" },
  "options_ui": { "page": "/options/index.html", "open_in_tab": true },
  "side_panel": { "default_path": "/sidepanel/index.html" },
  "permissions": ["tabs", "contextMenus", "activeTab", "scripting", "storage", "sidePanel"]
}

四种页面各自的使用场景:

页面 入口配置 适合放什么
popup action.default_popup 点图标弹窗,轻量的快捷开关
options options_uiopen_in_tab: true 独立标签页打开的完整设置表单
sidepanel side_panel.default_path 常驻型工具,类似 Claude 的侧拉层
contentScript content_scripts 注入到页面里的 UI(shadow DOM 隔离)

三张页面共享同一份 Element Plus 自动导入和合并后的 style.css,样式能力完全一致。

sidepanel和options都可以通过在popup里进行打开

侧边栏的一个坑 🕳️

chrome.sidePanel.open() 需要真实的 windowId,但千万别直接传 chrome.windows.WINDOW_ID_CURRENT(固定值 -2)------close() 不会把它解析成真实窗口 id,会静默失败,没有任何报错,很难排查。

模板把解析逻辑封装到了 src/shared/sidePanel.ts

javascript 复制代码
export const getCurrentWindowId = async () => {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
  return tab?.windowId
}

popup 里打开 / 关闭侧边栏只需两行:

csharp 复制代码
await chrome.sidePanel.open({ windowId })   // 打开
await chrome.sidePanel.close({ windowId })  // 关闭

类型化消息通信 📨

插件开发里最让人头大的就是消息通信:chrome.runtime.sendMessage 传的是裸对象,taskId 写错、参数形状不对,运行时才知道,调试全靠 console.log

模板的核心设计是把所有消息契约集中到一处src/shared/message.ts),用 TaskMap 声明式定义每个 taskId 的入参和出参:

css 复制代码
export interface TaskMap {
  /** contentScript 通过 background 读写 IndexedDB 缓存 */
  'get-value-bg': { params: { keyName: string }; response: { result: any } }
  'set-value-bg': { params: { keyName: string; value: any }; response: { result: any } }
  'del-value-bg': { params: { keyName: string }; response: { result: any } }
  /** popup / options 通过 background 读写 chrome.storage.sync 设置 */
  'get-setting': { params: { key: string }; response: { result: any } }
  'set-setting': { params: { key: string; value: any }; response: { result: any } }
  /** background 通过 contextMenus 点击向当前标签页 contentScript 推送 */
  'ping-content': { params: void; response: { url: string; title: string; injectedAt: number } }
}

新增一个消息 = 往 TaskMap 加一行,发起端和接收端的类型同时被推导出来,编译期就能拦住拼错 taskId 或传错参数。三个方向一致的工具函数:

csharp 复制代码
// contentScript / popup / options -> background
sendMessage('get-setting', { key: 'nickname' })

// background -> 指定标签页的 contentScript
sendMessageToTab(tabId, 'ping-content', undefined)

// 注册处理器;callback 返回 Promise 时自动保持通道开启
onMessage('get-setting', async (params) => ({ result: data[params.key] }))

又一个坑:异步 sendResponse 🕳️

MV3 里最容易踩的坑------异步 sendResponse 必须 return true 保持通道开启 ,否则回调里的 Promise 还没 resolve,消息通道就关了,接收端拿到 undefined 一脸懵。这个细节 onMessage 已经封装好了,你不用再记。

围绕这套通信,background 里挂了三组现成的示例处理器:

模块 taskId 背后存储
src/background/db.ts get-value-bg / set-value-bg / del-value-bg IndexedDB(idb 封装,适合大体积缓存)
src/background/settings.ts get-setting / set-setting chrome.storage.sync(跨设备同步的偏好设置)
src/background/contextMenu.ts ping-content 右键菜单 + 图标角标示例

其中 contextMenu.ts 是个完整的「background → contentScript」闭环:右键点菜单 → 计数写入 storage.sync → 更新图标角标 → 用 sendMessageToTabping-content 推给当前页面的 contentScript 并打印响应。新手照抄就能理解整个通信链路。

Vite 8 升级 🚀

这次更新把基础依赖从老版本一口气拉到了当前主版本:

依赖 升级前 升级后
vite 3.x 8.x
vue 3.2 3.5
element-plus 2.2 2.14
typescript 4.8 6.x
eslint 旧版 10(flat config)

同时做了一轮工程化重构:

1. 去掉停更插件,改内联插件

移除了停止维护的 vite-plugin-eslint / vite-plugin-replace,ESLint 改走官方 flat configeslint.config.mjs),新增 pnpm lint / pnpm typecheck 脚本,配合 husky + lint-staged 在提交前卡代码质量。

2. 全链路 ESM

package.json 设了 "type": "module"scripts/monitor.js 和热更新脚本全部迁移为 ESM,Vite 配置用 import.meta.dirname 定位路径、用 .ts 后缀直接导入配置模块(Vite 8 原生配置加载器的要求,别手痒删掉)。

3. 修了 contentScript 样式 404 的老大难 🕳️

contentScript 跑在页面里,样式文件不能内联,只能通过 chrome.runtime.getURL() 外链加载,还必须在 web_accessible_resources 里声明。之前构建产物一旦没有 CSS 就生成不了 style.css,页面就 404。

修复方式是在 vite.content.config.ts 里加了一个内联插件 vite-content-script-css:打包收尾时保证 contentScript/style.css 永远被输出 (没有 CSS 就生成空文件),顺手把 :root 重写成 :host,让样式正确作用到 shadow DOM 上------也就是第一篇里那个 :root 变量坑的自动化版:

bash 复制代码
if (styleCss) {
  styleCss.source = styleCss.source.replace(/:root(?=\s*{)/g, ':host')
} else {
  this.emitFile({ type: 'asset', fileName: 'style.css', source: '' })
}

4. Chrome API 统一 Promise 风格

所有 Chrome API 调用改为 Promise 风格,不再用回调地狱;manifest 的 background 增加 "type": "module",contentScript 用 IIFE 格式打包避免全局污染。

5. 双构建配置 + 可靠的热更新

两个 Vite 配置并行跑:vite.config.ts 产出 background / popup / options / sidepanel(ES 格式),vite.content.config.ts 单独产出 contentScript(IIFE 格式,子目录输出)。开发时 WebSocket 监听在 8801 端口:background 变了自动 chrome.runtime.reload(),contentScript 变了只重注入当前活动标签页,不用手动刷新页面。

体验对比 📊

场景 传统模板 本模板
新增一个 taskId 手写两端消息代码 + 靠记忆对齐类型 TaskMap 加一行,两端自动推导
想加 options / sidepanel 手改 manifest + 打包配置 + 拷贝脚本 入口声明式注册,模板自动处理
异步 sendResponse 容易忘 return true 导致通道提前关闭 onMessage 封装好了
contentScript 样式 构建缺文件就 404 内联插件保证 style.css 永远存在
依赖升级 大版本不兼容警告满天飞 模板已踩完坑,直接开箱即用

快速上手 🚀

bash 复制代码
pnpm i
pnpm dev    # 产物输出到 local/,chrome://extensions 加载 unpacked 即可热更新
pnpm build  # 产物输出到 extension/,可直接发布
pnpm test   # vitest,含消息通信、IndexedDB 封装的单元测试

代码层面需要关注的入口就几个:src/shared/message.ts(消息契约)、src/manifest.json(权限与入口)、vite.config.ts + vite.content.config.ts(打包入口)。

结语

这次更新的核心思想是**「把插件工程化的脏活累活提前封装好」**:多页面开箱即用、消息通信类型安全、构建与热更新不再有玄学 bug。你只需要专注于插件本身的业务逻辑。

模板仓库也会跟着这个系列持续迭代,如果你正要开始一个新的 Chrome 插件项目,或者想把手头那个还在手写消息通信的老项目迁移过来,这个模板值得一试:

github.com/Tinsson/vit...

相关推荐
玉鸯1 小时前
让 Agent 面向用户:AG-UI 协议构建 Agent 前端
前端·python·agent
程序员黑豆2 小时前
鸿蒙应用开发 @Extend 装饰器使用教程
前端·harmonyos
杨先生哦2 小时前
【2026热端攻防系列 10/12】前端凭据安全深度攻防:Cookie/Storage劫持、会话固定、凭据泄露与浏览器最新加固方案
前端·笔记·安全·web安全
莫石2 小时前
坦克打无人机模拟(three-tile 地形)
前端
hunterandroid2 小时前
[鸿蒙从零到一] HarmonyOS 任务调度与并发模型实战:taskpool、Worker 与可取消任务
前端
颜进强2 小时前
前端看后端 14:什么是 CORS?
前端·后端
sugar__salt2 小时前
Vue3 自定义指令与插槽(Slot)技术详解
前端·javascript·vue.js·前端框架·vue
Csvn3 小时前
🎯 Flex 布局的 `min-width: auto` 陷阱:为什么内容总是撑破容器?
前端
果然_3 小时前
纯前端图片压缩怎么做?不用上传服务器,浏览器里跑完所有逻辑
前端