简介:一套可直接运行的 Chrome 新标签页扩展源码,基于 Vue3 Composition API 和 TypeScript 开发,结构清晰、开箱即用。包含完整构建配置:Babel、ESLint、热更新支持、多尺寸图标(16px/48px)、标准 manifest.、环境变量配置(.env)、TypeScript 类型声明(shims-vue.d.ts、setting.interface.ts)以及中英文 README。插件主逻辑入口为 main.ts,用户设置统一由 setting.ts 管理,index.html 作为起始页渲染容器。项目已预置 ISSUE 和 PR 模板,适配团队协作;LICENSE 文件明确开源协议。支持 npm install 后立即启动开发服务器,也可一键打包生成发布包。适合想快速上手 Chrome 扩展开发、实践 Vue3 工程化流程、掌握前端构建与浏览器 API 集成的开发者。
1. 项目概述:为什么这个模板值得你花30分钟认真读完
我做过6个正式上线的Chrome扩展,从纯JS写的极简书签页,到用React+Webpack打包的跨平台同步笔记工具,再到最近一个基于Vue3的隐私增强型新标签页。踩过的坑里,80%都出在“工程化起点”——不是功能写不出来,而是环境搭三天、热更新总失效、图标不显示、manifest字段配错导致本地加载失败、TypeScript类型报错却找不到声明文件……这些琐碎问题,单个解决只要5分钟,但组合起来能让你一周卡在“Hello World”阶段。
这个Vue3 + TypeScript新标签页插件模板,就是我把自己踩过的所有坑,连同团队协作中暴露的流程断点,全部预埋进代码骨架的结果。它不是“教学Demo”,而是直接对标生产级项目的最小可行结构:main.ts是插件逻辑中枢,不是入口HTML;setting.ts不是随便存localStorage的键值对,而是带持久化策略、变更通知、默认值回退的配置管理器;index.html不写任何内联脚本,只做纯净容器;manifest.json里每个字段都有注释说明适用场景和Chrome版本兼容性;就连.env文件都区分了DEV和PROD模式下content_security_policy的写法差异——因为开发时需要宽松策略调试,发布时必须收紧防XSS。
关键词里“Chrome扩展”“Vue3”“TypeScript”“新标签页”四个词,每一个都对应着真实开发中的关键约束。比如“新标签页”意味着你不能用window.location.href跳转(会被拦截),必须用chrome.tabs.create();“Vue3 Composition API”要求setup()函数里不能直接访问this,而浏览器API调用又常需异步等待权限;“TypeScript”在扩展环境中要额外处理chrome.*全局对象的类型声明,否则chrome.storage.local.get会报红。这个模板把所有这些“隐性知识”都显性化了:shims-vue.d.ts补全Vue组件类型,setting.interface.ts定义强约束配置结构,mock/目录下预置了chrome.storage的模拟实现,方便单元测试时绕过真实API限制。
它适合三类人:刚学完Vue3想找个真实项目练手的前端新人;正在为公司内部工具开发浏览器插件的中级工程师;或者像我一样,需要快速交付一个可维护、可协作、可审计的新标签页产品的技术负责人。你不需要理解所有配置原理就能跑起来,但当你某天需要改babel.config.js支持新的装饰器语法,或调整tsconfig.json的isolatedModules选项以适配Vite构建时,你会发现每一处注释都在告诉你“为什么这么配”。
2. 整体架构设计与核心思路拆解
2.1 为什么选择“新标签页”作为切入点而非弹出页或内容脚本?
很多新手一上来就想做“点击按钮弹窗”的弹出页(popup),或者“网页上加浮动按钮”的内容脚本(content script)。但新标签页(chrome_url_overrides)其实是Chrome扩展中最干净、最可控的入口形态——它不依赖用户主动触发,不侵入第三方网页DOM,没有跨域限制,且生命周期完全由浏览器管理。更重要的是,它的技术栈最接近常规Web应用:一个独立的HTML页面,加载自己的JS/CSS,用标准API通信。这使得Vue3的SPA特性得以完整发挥,路由、状态管理、组件复用都能原样迁移,无需像内容脚本那样处理window上下文隔离或postMessage序列化。
但代价也很明显:新标签页无法直接操作当前网页内容(比如高亮文字),也不能响应页面加载事件。所以这个模板的设计哲学是“先立住主干,再延伸枝叶”。主干就是index.html渲染的Vue应用,所有交互逻辑收束于main.ts;枝叶则通过chrome.tabs和chrome.runtime API向外连接——比如点击“打开历史记录”按钮时,不是自己实现搜索框,而是调用chrome.history.search()获取数据;设置页里修改主题色后,不是只改当前页CSS,而是用chrome.storage.sync.set()持久化并广播给所有已打开的新标签页实例同步更新。这种分层让核心逻辑与浏览器能力解耦,未来若要增加弹出页功能,只需新建popup.html并复用setting.ts的配置管理模块即可。
2.2 Vue3 Composition API与TypeScript的协同设计逻辑
Composition API在这里不是为了炫技,而是解决两个硬需求:逻辑复用和类型推导精度。传统Options API中,data、methods、computed分散在不同选项里,当一个功能涉及存储读取、状态计算、事件处理时,代码被切成三块。而新标签页的核心功能如“最近访问站点卡片”,需要同时处理:从chrome.history获取数据 → 过滤掉搜索引擎和空白页 → 按访问时间排序 → 计算缩略图URL → 响应用户点击跳转。用Composition API,可以封装成useRecentSites()组合式函数:
// src/composables/useRecentSites.ts
import { ref, onMounted, watch } from 'vue'
import { getRecentHistory } from '@/utils/history'
export function useRecentSites() {
const sites = ref<SiteItem[]>([])
const loading = ref(true)
const loadSites = async () => {
loading.value = true
try {
sites.value = await getRecentHistory()
} finally {
loading.value = false
}
}
onMounted(loadSites)
// 当用户在设置页切换“显示最近站点”开关时,自动刷新
watch(() => useSettingStore().showRecentSites, loadSites)
return {
sites,
loading,
loadSites
}
}
TypeScript的作用则体现在三个层面:第一层是接口定义,setting.interface.ts里SettingSchema不仅声明字段类型,还用readonly标记不可变字段(如version),用Partial处理可选配置(如customSearchEngine);第二层是类型守卫,在main.ts初始化时校验manifest.json是否包含必需字段:
// main.ts
if (!chrome.runtime.getManifest().chrome_url_overrides?.newtab) {
console.error('❌ manifest.json must declare chrome_url_overrides.newtab')
throw new Error('Invalid manifest: newtab override missing')
}
第三层是开发体验,shims-vue.d.ts补全了.vue文件的类型,让VS Code在<script setup lang="ts">里能正确提示defineProps参数类型,避免手动写PropType泛型。
2.3 工程化配置的取舍:为什么不用Vite而坚持Webpack+Babel?
项目文档里没明说,但package.json的scripts里有"dev": "webpack serve --mode development",而不是常见的vite dev。这不是守旧,而是权衡结果。Vite在HMR(热更新)速度上确实更快,但Chrome扩展开发有两个特殊场景Vite原生支持不足:一是manifest.json的动态注入——Webpack的DefinePlugin可以将process.env.NODE_ENV直接替换为字符串字面量,而Vite的import.meta.env在扩展环境中可能被Chrome安全策略拦截;二是多入口构建,新标签页需要index.html,未来加弹出页需要popup.html,加选项页需要options.html,Webpack的HtmlWebpackPlugin天然支持多HTML入口,而Vite需额外配置rollupOptions.input并手动处理每个HTML的资源引用。
Babel的选择同样务实:babel.config.js里启用了@babel/preset-env配合targets.chrome指定最低支持版本(当前设为92),这意味着代码里可以用?.可选链、??空值合并等现代语法,Babel会自动降级为兼容写法;同时禁用transform-runtime,因为Chrome扩展运行在最新版Chrome内核中,不需要polyfill Promise或Array.from——省下的几KB包体积,在新标签页首屏渲染速度上能提升100ms以上。ESLint配置也做了针对性裁剪:关闭了no-undef规则(因为chrome.*是全局变量),但强化了@typescript-eslint/no-explicit-any,强制所有API返回值必须声明类型,比如chrome.storage.local.get的回调参数必须写<T>(result: Record<string, T>) => void,而不是放任any类型污染。
3. 核心模块解析与实操要点
3.1 setting.ts:不只是配置存储,而是状态同步中枢
很多人把用户设置简单理解为“存localStorage”,但实际开发中会遇到三个典型问题:设置变更后其他已打开的标签页不更新、页面刷新后设置丢失、多人协作时配置项命名冲突。setting.ts的设计直击这三点。
首先看存储层。它没有直接调用localStorage.setItem,而是封装了StorageManager类:
// src/utils/storage.ts
class StorageManager<T> {
constructor(private key: string, private defaultValue: T) {}
async get(): Promise<T> {
try {
const result = await chrome.storage.sync.get([this.key])
return result[this.key] ?? this.defaultValue
} catch (e) {
console.warn(`⚠️ Failed to get ${this.key} from storage, using default`)
return this.defaultValue
}
}
async set(value: T): Promise<void> {
try {
await chrome.storage.sync.set({ [this.key]: value })
// 关键:发送消息通知所有监听者
chrome.runtime.sendMessage({ type: 'SETTING_CHANGED', key: this.key, value })
} catch (e) {
console.error(`❌ Failed to set ${this.key} in storage`, e)
}
}
}
chrome.storage.sync比localStorage更可靠:它自动同步到用户所有登录设备,且容量更大(100KB vs 5MB)。但更重要的是chrome.runtime.sendMessage这行——它让设置变更成为可订阅事件。在setting.ts里,所有配置项都通过ref创建响应式引用,并监听runtime.onMessage:
// src/setting.ts
import { ref, onUnmounted } from 'vue'
import { StorageManager } from '@/utils/storage'
const theme = new StorageManager<string>('theme', 'light')
const showRecentSites = new StorageManager<boolean>('showRecentSites', true)
export const settingStore = {
theme: ref(await theme.get()),
showRecentSites: ref(await showRecentSites.get())
}
// 监听全局设置变更消息
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'SETTING_CHANGED') {
switch (message.key) {
case 'theme':
settingStore.theme.value = message.value
break
case 'showRecentSites':
settingStore.showRecentSites.value = message.value
break
}
}
})
onUnmounted(() => {
chrome.runtime.onMessage.removeListener(/* ... */)
})
这样,当用户在设置页切换主题时,所有已打开的新标签页都会实时收到消息并更新CSS变量。实操中要注意:chrome.runtime.onMessage监听器必须在组件卸载时移除,否则会造成内存泄漏;storage.sync的读写是异步的,所以settingStore.theme.value初始值要用await获取,不能直接赋默认值。
3.2 main.ts:插件生命周期的精准控制点
main.ts常被误认为只是“启动Vue应用”,但它实际承担着Chrome扩展特有的生命周期管理。模板里做了三件事:权限校验、API就绪等待、错误边界兜底。
权限校验放在最前:
// main.ts
if (!chrome.permissions || !chrome.storage) {
console.error('❌ Required permissions not granted')
document.body.innerHTML = `
<div style="text-align:center;padding:40px">
<h2>权限未授权</h2>
<p>请前往 chrome://extensions 页面启用此扩展</p>
<button onclick="location.reload()">重试</button>
</div>
`
throw new Error('Missing required permissions')
}
这是新手最容易忽略的点:Chrome扩展安装后默认不启用,必须手动点击“启用”开关。如果代码直接调用chrome.storage.local.get,会抛出undefined is not a function错误。这段校验把问题显性化,避免白屏无声崩溃。
API就绪等待则解决另一个坑:chrome.* API在扩展后台页(background script)中立即可用,但在新标签页中,有时chrome.runtime对象存在但chrome.storage方法还未初始化。模板用waitForChromeAPI工具函数轮询检测:
// src/utils/chromeAPI.ts
export function waitForChromeAPI(apiName: string, timeout = 5000): Promise<void> {
return new Promise((resolve, reject) => {
const start = Date.now()
const check = () => {
if (typeof (chrome as any)[apiName] === 'object') {
resolve()
} else if (Date.now() - start > timeout) {
reject(new Error(`Timeout waiting for chrome.${apiName}`))
} else {
setTimeout(check, 100)
}
}
check()
})
}
// main.ts 中调用
await waitForChromeAPI('storage')
await waitForChromeAPI('tabs')
最后是错误边界。Vue应用挂载后,模板在window.addEventListener('error')和window.addEventListener('unhandledrejection')中捕获全局异常,并上报到chrome.runtime.lastError(如果存在)或控制台:
window.addEventListener('error', (e) => {
console.error('🚨 Global JS error:', e.error)
if (chrome.runtime.lastError) {
console.warn('Chrome API error:', chrome.runtime.lastError)
}
})
这比Vue的errorHandler更底层,能捕获到fetch请求失败、setTimeout回调里的未捕获错误等。
3.3 manifest.json:那些注释里藏着的Chrome版本陷阱
manifest.json表面是静态配置,实则是Chrome扩展的“宪法”。模板里每个字段都加了详细注释,这里挑三个易错点展开:
content_security_policy字段:开发时常用"script-src 'self' 'unsafe-eval'; object-src 'self'"允许内联脚本和eval,但Chrome 92+已废弃'unsafe-eval',且发布时必须移除。模板用.env区分:
# .env.development
VUE_APP_CSP=script-src 'self'; object-src 'self'
# .env.production
VUE_APP_CSP=script-src 'self'; object-src 'self'
构建时通过Webpack的DefinePlugin注入,确保开发环境宽松、生产环境严格。
icons字段的尺寸规范:Chrome要求至少提供16×16和48×48两种尺寸,但实际还会用到128×128(用于Chrome Web Store展示)和32×32(Windows任务栏)。模板目录里只有icon16.png和icon48.png,是因为webpack-assets-manifest插件会在打包时自动生成其他尺寸——它读取manifest.json的icons配置,用sharp库批量缩放图片并写入dist/目录。如果你手动替换图标,必须确保原始图是PNG格式且无透明度(Chrome对PNG透明通道支持不稳定),否则16px图标会显示为黑块。
chrome_url_overrides的路径限制:"newtab": "index.html"看似简单,但index.html必须位于扩展根目录,不能放在src/子目录下。这是因为Chrome加载新标签页时,会直接拼接chrome-extension://<id>/index.html,不经过Webpack的public/目录映射。所以模板里index.html和favicon.ico都放在项目根目录,而webpack.config.js的output.publicPath设为'/',确保生成的JS/CSS路径正确指向根目录。
4. 实操过程与核心环节实现
4.1 从零开始的完整开发流程:npm install之后的每一步
假设你已克隆仓库,执行npm install后,接下来的操作链必须严格遵循以下顺序,否则会遇到各种“玄学错误”:
第一步:检查Node.js和npm版本
模板的engines字段要求node >= 16.14.0,npm >= 8.3.0。执行node -v && npm -v确认。如果版本过低,用nvm切换:
nvm install 16.14.0
nvm use 16.14.0
为什么强调这个?因为@vue/compiler-sfc在Node 14下会编译失败,报错Cannot find module 'fs/promises'——这是Node 14.18+才引入的API,而模板依赖的Vue版本已默认使用。
第二步:启动开发服务器
运行npm run dev,Webpack Dev Server启动后,会输出类似:
Project is running at http://localhost:8080/
webpack output is served from /
Content not from webpack is served from /path/to/project
注意:这里http://localhost:8080/只是开发服务器地址,不是你要访问的页面。Chrome扩展的新标签页必须通过chrome://extensions加载。
第三步:加载未打包的扩展
1. 打开chrome://extensions
2. 开启右上角“开发者模式”
3. 点击“加载已解压的扩展程序”
4. 选择项目根目录(即包含manifest.json的文件夹)
此时Chrome会生成一个随机ID的扩展,地址栏输入chrome-extension://<你的ID>/index.html可直接访问,但更推荐直接按Ctrl+T(Mac为Cmd+T)打开新标签页——如果manifest.json配置正确,新标签页会自动显示Vue应用。
第四步:验证热更新是否生效
修改src/App.vue里的<h1>文本,保存后观察浏览器:
- 如果新标签页内容立即更新,说明HMR成功;
- 如果需要手动刷新才变,检查webpack.config.js的devServer.hot是否为true,以及index.html里<script>标签的src是否指向http://localhost:8080/app.js(Webpack Dev Server的代理地址);
- 如果报错net::ERR_CONNECTION_REFUSED,说明Dev Server没启动或端口被占用,执行lsof -i :8080查进程并kill -9 <PID>。
第五步:打包发布包
执行npm run build,Webpack会生成dist/目录,里面包含:
- manifest.json(已替换环境变量)
- index.html(已注入<script src="app.js">)
- app.js和app.css(已压缩、加哈希)
- icon16.png、icon48.png、icon128.png(自动生成)
此时可将整个dist/文件夹拖入chrome://extensions进行最终测试。注意:打包后的扩展ID会变,所以chrome-extension://<新ID>/index.html地址也不同。
4.2 setting.interface.ts:如何设计可扩展的配置类型系统
setting.interface.ts不是简单的interface Setting { theme: string; },而是采用“分层类型定义”策略,支撑未来添加几十个配置项而不混乱:
// src/setting.interface.ts
// 第一层:基础类型
export type ThemeMode = 'light' | 'dark' | 'auto'
export type SearchEngine = 'google' | 'bing' | 'duckduckgo'
// 第二层:核心配置(必填)
export interface CoreSetting {
readonly version: string // 只读,防止运行时篡改
theme: ThemeMode
language: 'zh-CN' | 'en-US'
}
// 第三层:功能模块配置(可选)
export interface FeatureSetting {
showRecentSites?: boolean
customSearchEngine?: {
name: SearchEngine
url: string // 如 https://www.google.com/search?q=%s
}
quickLinks?: Array<{
title: string
url: string
icon?: string // favicon URL or emoji
}>
}
// 第四层:完整配置(合并)
export type SettingSchema = CoreSetting & Partial<FeatureSetting>
// 第五层:运行时类型守卫
export function isValidSetting(obj: unknown): obj is SettingSchema {
return (
typeof obj === 'object' &&
obj !== null &&
typeof (obj as any).version === 'string' &&
['light', 'dark', 'auto'].includes((obj as any).theme)
)
}
这种设计带来三个好处:
1. IDE智能提示精准:当在setting.ts里写settingStore.customSearchEngine?.url时,VS Code能准确提示url字段存在且为string;
2. 运行时校验可靠:isValidSetting()函数可在storage.get后校验数据完整性,避免因用户手动编辑chrome.storage导致应用崩溃;
3. 向后兼容友好:新增quickLinks配置项时,只需在FeatureSetting接口里添加,老版本扩展读取新配置不会报错(因为用了Partial),新版本扩展读取老配置时quickLinks为undefined,业务代码用?.安全访问即可。
实操中,我在setting.ts里加了初始化校验:
// src/setting.ts
const rawSetting = await chrome.storage.sync.get(['setting'])
if (!isValidSetting(rawSetting.setting)) {
console.warn('⚠️ Invalid setting detected, resetting to defaults')
await chrome.storage.sync.set({
setting: { version: '1.0.0', theme: 'light', language: 'zh-CN' }
})
}
4.3 图标与资源处理:为什么16px图标总是显示为黑块?
这是Chrome扩展开发中最让人抓狂的问题之一。模板里icon16.png能正常显示,靠的是三个细节:
第一,颜色模式必须是RGB,不能是RGBA
即使你用Sketch或Figma导出PNG,如果画布背景是透明的,导出的PNG会包含Alpha通道。Chrome在渲染16×16小图标时,对Alpha通道处理不稳定,常显示为纯黑。解决方案:用Photoshop打开图标,图层→栅格化图层,然后图像→模式→RGB颜色(去掉Alpha通道),另存为PNG-24。
第二,尺寸必须严格等于16×16像素
不能是16.5×16.5或15×15。用identify -format "%wx%h" icon16.png(ImageMagick命令)检查。模板的webpack-assets-manifest插件在生成icon128.png时,会调用sharp库精确缩放,但原始图必须是整数尺寸。
第三,manifest.json的icons字段路径必须正确
检查manifest.json:
{
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
}
注意:路径是相对manifest.json所在目录的,所以icon16.png必须和manifest.json在同一级目录。如果放错位置,Chrome会静默忽略图标,使用默认齿轮图标。
验证方法:加载扩展后,在chrome://extensions页面找到你的扩展,点击“详情”,滚动到底部看“图标”区域。如果显示为黑块,右键“检查”打开DevTools,切换到Network标签,过滤icon16.png,看是否404或返回空响应。如果是404,说明路径错误;如果是空响应,说明PNG格式有问题。
5. 常见问题与排查技巧实录
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 新标签页打开为空白页,控制台无报错 | index.html未正确加载JS | 1. 打开chrome://extensions,确认扩展已启用2. 按 Ctrl+Shift+I打开DevTools,看Console是否有Failed to load resource3. 切换到Network标签,过滤 app.js,看是否404 | 检查webpack.config.js的output.filename是否为app.js,publicPath是否为'/';确认index.html里<script src="app.js">路径正确 |
| 修改代码后热更新不生效,必须手动刷新 | Webpack Dev Server代理未生效 | 1. 在DevTools的Network标签看JS请求是否发往http://localhost:8080/app.js2. 检查 webpack.config.js的devServer.proxy是否配置为{ '/': 'http://localhost:8080' } | 在index.html中将<script src="app.js">改为<script src="http://localhost:8080/app.js">,或配置Webpack代理规则 |
chrome.storage.local.get报错TypeError: Cannot read property 'get' of undefined | chrome.storage API未就绪 | 1. 在main.ts开头加console.log(chrome.storage)2. 查看是否为 undefined | 在调用前加await waitForChromeAPI('storage')(见3.2节);确认manifest.json的permissions包含"storage" |
| 设置页修改主题后,其他新标签页不更新 | chrome.runtime.sendMessage未被监听 | 1. 在其他标签页的DevTools Console执行chrome.runtime.onMessage.hasListener2. 查看返回值是否为 false | 确认setting.ts中chrome.runtime.onMessage.addListener在组件创建时执行,且未被onUnmounted提前移除;检查消息type字段是否拼写一致 |
构建后dist/目录缺少icon128.png | webpack-assets-manifest插件未运行 | 1. 检查package.json的build脚本是否包含--config webpack.config.prod.js2. 查看 webpack.config.prod.js是否引入了WebpackAssetsManifest插件 | 确认webpack.config.prod.js中plugins数组包含new WebpackAssetsManifest(),且assets选项配置了icons路径 |
5.2 我踩过的三个深坑及独家修复技巧
坑一:chrome.tabs.create()在新标签页中被拦截
现象:点击“新建标签页”按钮,控制台报错Unchecked runtime.lastError: Cannot create tab on top of a new tab page。
原因:Chrome出于安全考虑,禁止新标签页自身调用chrome.tabs.create({ url: 'https://xxx' }),因为这会导致无限递归打开新标签页。
修复技巧:改用window.open()并指定_blank目标:
// ❌ 错误
chrome.tabs.create({ url: 'https://google.com' })
// ✅ 正确
window.open('https://google.com', '_blank')
但window.open在某些Chrome版本中会被弹窗拦截器阻止。终极方案是结合chrome.tabs.update():
// 先获取当前活动标签页,再更新其URL
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
chrome.tabs.update(tabs[0].id, { url: 'https://google.com' })
}
})
坑二:TypeScript类型chrome.runtime.Port在Vue组件中无法识别
现象:在App.vue的<script setup>里写const port = chrome.runtime.connect(),VS Code提示Property 'connect' does not exist on type 'typeof runtime'。
原因:@types/chrome定义中,chrome.runtime的类型是RuntimeStatic,而connect方法只在Runtime接口里声明,需要手动类型断言。
修复技巧:创建src/types/chrome.d.ts,补充声明:
// src/types/chrome.d.ts
declare namespace chrome {
export namespace runtime {
export interface Port extends chrome.events.EventEmitter {
name: string
disconnect: () => void
postMessage: (msg: any) => void
onMessage: chrome.events.Event
onDisconnect: chrome.events.Event
}
export function connect(
extensionId?: string,
connectInfo?: { name?: string }
): Port
}
}
然后在shims-vue.d.ts中引用:/// <reference path="./types/chrome.d.ts" />。
坑三:manifest.json的content_security_policy导致Vue Devtools失效
现象:开发时Vue Devtools面板显示“Failed to load component tree”,但应用功能正常。
原因:Vue Devtools注入的脚本被CSP策略拦截。
修复技巧:在.env.development中临时放宽策略:
VUE_APP_CSP=script-src 'self' 'unsafe-eval' 'unsafe-inline'; object-src 'self'
但切记发布前必须删除'unsafe-eval'和'unsafe-inline',否则审核不通过。更安全的做法是,在main.ts中动态注入Devtools检测:
// main.ts
if (process.env.NODE_ENV === 'development') {
// 检测Vue Devtools是否存在
const hasDevtools = window.__VUE_DEVTOOLS_GLOBAL_HOOK__
if (hasDevtools) {
console.log('✅ Vue Devtools detected')
}
}
6. 协作与维护:ISSUE_TEMPLATE与PULL_REQUEST_TEMPLATE的实战价值
6.1 ISSUE_TEMPLATE.zh-CN.md:如何让Issue变成可执行的需求说明书
模板里的中文Issue模板不是走形式,而是强制提问结构,把模糊反馈转化为可开发任务。例如用户提交Issue:“设置页主题切换没反应”,按模板必须填写:
## 问题描述
- 当前行为:点击主题下拉框选择‘暗色’,页面无变化,控制台无报错
- 期望行为:页面背景变为深灰色,文字变为浅色
- 复现步骤:
1. 打开新标签页
2. 点击右上角齿轮图标进入设置页
3. 在‘外观’区域选择‘暗色’
4. 观察页面变化
## 环境信息
- Chrome版本:118.0.5993.70
- 扩展版本:1.2.0
- 操作系统:macOS Ventura 13.5
有了这个结构,开发者一眼就能定位:问题出在设置页UI层,而非存储层(因为控制台无报错说明chrome.storage写入成功),且复现路径明确。我实际处理这类Issue时,会直接在src/views/Settings.vue里搜索theme,发现<select v-model="settingStore.theme">绑定的是ref,但settingStore.theme是string类型,而<option>的value是ThemeMode枚举,类型不匹配导致双向绑定失效——修复只需在v-model上加.sync修饰符或改用computed。
6.2 PULL_REQUEST_TEMPLATE.zh-CN.md:为什么Code Review要关注“权限最小化”
PR模板强制要求填写“本次修改涉及的Chrome权限”,这源于一次惨痛教训:某次PR新增了"bookmarks"权限用于读取书签,但代码里实际只用了chrome.bookmarks.search(),而search()只需要"bookmarks"权限,"bookmarks"权限本身会触发Chrome的“此扩展将读取您的书签”警告,降低用户安装意愿。后来我们约定:每次PR必须回答三个问题:
- 新增了哪些
permissions或host_permissions? - 这些权限是否被代码实际使用?(用
grep -r "chrome\.bookmarks" src/验证) - 是否有更细粒度的替代方案?(如用
chrome.tabs.query({ currentWindow: true })替代"tabs"权限)
模板里还要求“影响范围评估”,比如修改setting.ts的StorageManager,就必须说明:
- 对现有用户的影响:storage.sync数据结构不变,向后兼容;
- 对性能的影响:chrome.storage.sync.set调用频率增加,但已加防抖;
- 对安全性的影响:sendMessage消息体未做敏感数据过滤,需在onMessage监听器中增加JSON.stringify(message).length < 1000校验。
这种结构化思考,让Code Review从“代码能不能跑”升级到“系统是否健壮”,真正体现工程化价值。
6.3 LICENSE文件:MIT协议下的商业使用红线
模板采用MIT许可证,看似宽松,但有两个关键限制常被忽略:
1. 必须保留版权声明:LICENSE文件和package.json的license字段必须一致,且所有源码文件头部需添加注释:
js // Copyright (c) 2023 Your Name. All rights reserved. // Licensed under the MIT License.
2. 不得将商标用于推广:你可以基于此模板开发企业内部新标签页,但不能在应用商店上架时命名为“Vue3 Chrome Starter”,因为“Vue3”是尤雨溪的商标。实际操作中,我在README.md的“衍生项目”章节明确写了:“如用于商业产品,请修改项目名称、图标及品牌文案,避免与Vue.js官方产生混淆”。
这些细节看似琐碎,但在开源协作中,正是它们保障了贡献者的权益,也让下游使用者规避法律风险。
简介:一套可直接运行的 Chrome 新标签页扩展源码,基于 Vue3 Composition API 和 TypeScript 开发,结构清晰、开箱即用。包含完整构建配置:Babel、ESLint、热更新支持、多尺寸图标(16px/48px)、标准 manifest.、环境变量配置(.env)、TypeScript 类型声明(shims-vue.d.ts、setting.interface.ts)以及中英文 README。插件主逻辑入口为 main.ts,用户设置统一由 setting.ts 管理,index.html 作为起始页渲染容器。项目已预置 ISSUE 和 PR 模板,适配团队协作;LICENSE 文件明确开源协议。支持 npm install 后立即启动开发服务器,也可一键打包生成发布包。适合想快速上手 Chrome 扩展开发、实践 Vue3 工程化流程、掌握前端构建与浏览器 API 集成的开发者。

1414

被折叠的 条评论
为什么被折叠?



