Vite Vue3 Chrome 浏览器插件开发完整教程
📚 教程概述
本教程将带你从零开始构建一个功能完整的 Chrome 浏览器插件,使用现代化的技术栈:Vue3 + Vite + Chrome Extension API。
🎯 学习目标
- 掌握 Chrome 插件开发基础
- 学会使用 Vue3 构建插件界面
- 理解 Vite 构建工具配置
- 掌握模块化架构设计
- 学会调试和部署插件
🛠️ 技术栈
- 前端框架: Vue3 + Vue Router
- 构建工具: Vite
- 开发语言: JavaScript (ES6+)
- 样式: CSS3
- API: Chrome Extension APIs
🚀 第一章:项目初始化
1.1 创建项目结构
# 创建项目目录
$ mkdir vue3-chrome-extension
$ cd vue3-chrome-extension
# 初始化 npm 项目
$ npm init -y
$ pnpm init1.2 安装依赖
# 安装 Vue3 和相关依赖
$ pnpm add vue vue-router
# 安装开发依赖
$ pnpm add -D vite @vitejs/plugin-vue1.3 项目目录结构
vue3-chrome-extension/
├── src/ # 源代码目录
│ ├── views/ # Vue 页面组件
│ │ ├── Home.vue # 首页
│ │ ├── Tools.vue # 工具页
│ │ ├── Settings.vue # 设置页
│ │ └── About.vue # 关于页
│ ├── router/ # 路由配置
│ │ └── index.js # 路由定义
│ ├── content/ # Content Script 模块
│ │ ├── index.js # 主入口
│ │ ├── messageHandler.js # 消息处理
│ │ ├── linkManager.js # 链接管理
│ │ ├── styleManager.js # 样式管理
│ │ ├── analysisTools.js # 分析工具
│ │ └── storageManager.js # 存储管理
│ ├── App.vue # 主应用组件
│ ├── popup.js # 弹窗入口
│ └── background.js # 后台脚本
├── dist/ # 构建输出目录
├── manifest.json # 插件配置文件
├── popup.html # 弹窗 HTML
├── package.json # 项目配置
└── vite.config.js # Vite 配置📋 第二章:核心配置文件
2.1 manifest.json - 插件配置文件
{
"manifest_version": 3,
"name": "Vue3 Chrome Extension",
"version": "1.0.0",
"description": "基于Vue3和Vite构建的Chrome插件",
// 权限配置 - 定义插件需要的权限
"permissions": [
"activeTab", // 访问当前活动标签页
"storage", // 使用 Chrome 存储 API
"contextMenus" // 创建右键菜单
],
// 插件图标和弹窗配置
"action": {
"default_popup": "popup.html",
"default_title": "Vue3 Chrome Extension"
},
// Content Script 配置 - 注入到网页的脚本
"content_scripts": [
{
"matches": ["<all_urls>"], // 匹配所有网址
"js": ["content.js"] // 注入的脚本文件
}
],
// 后台脚本配置 - Service Worker
"background": {
"service_worker": "background.js"
},
// 网页可访问的资源
"web_accessible_resources": [
{
"resources": ["assets/*"],
"matches": ["<all_urls>"]
}
]
}配置说明:
- manifest_version: 3: 使用最新的 Manifest V3 规范
- permissions: 定义插件所需权限,遵循最小权限原则
- action: 配置插件图标点击行为
- content_scripts: 配置注入到网页的脚本
- background: 配置后台服务工作者
2.2 vite.config.js - 构建配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import fs from 'fs'
import path from 'path'
// 自定义插件:复制 manifest.json 到构建目录
function copyManifest() {
return {
name: 'copy-manifest',
// 在构建完成后执行
closeBundle() {
const manifestSrc = path.resolve(__dirname, 'manifest.json')
const manifestDest = path.resolve(__dirname, 'dist/manifest.json')
// 复制文件
fs.copyFileSync(manifestSrc, manifestDest)
console.log('✓ manifest.json 已复制到 dist 目录')
}
}
}
export default defineConfig({
// Vue 插件配置
plugins: [
vue(), // Vue3 支持
copyManifest() // 自定义复制插件
],
// 构建配置
build: {
// Rollup 构建选项
rollupOptions: {
// 多入口配置 - 为不同的脚本定义入口点
input: {
popup: 'popup.html', // 弹窗页面
background: 'src/background.js', // 后台脚本
content: 'src/content/index.js' // 内容脚本
},
// 输出配置
output: {
// 入口文件命名格式
entryFileNames: '[name].js',
// 代码分割文件命名格式
chunkFileNames: '[name].js',
// 资源文件命名格式
assetFileNames: '[name].[ext]'
}
},
// 输出目录
outDir: 'dist'
}
})配置原理解析:
多入口配置: Chrome 插件需要多个独立的 JavaScript 文件
- popup: 弹窗界面入口
- background: 后台服务工作者
- content: 内容脚本入口
- 输出文件命名: 确保生成的文件名与 manifest.json 中的配置一致
- 自定义插件: 自动复制 manifest.json 到构建目录,简化构建流程
2.3 package.json - 项目配置
{
"name": "vue3-chrome-extension",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.0",
"vue-router": "^4.2.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.0",
"vite": "^4.5.0"
}
}🎨 第三章:Vue3 界面开发 3.1 popup.html - 弹窗页面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue3 Chrome Extension</title>
<style>
/* 弹窗基础样式 */
body {
margin: 0;
padding: 0;
width: 350px; /* 固定宽度 */
height: 500px; /* 固定高度 */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 防止内容溢出 */
#app {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<!-- Vue 应用挂载点 -->
<div id="app"></div>
<!-- 引入 Vue 应用脚本 -->
<script type="module" src="/src/popup.js"></script>
</body>
</html>3.2 src/popup.js - Vue 应用入口
import { createApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import App from './App.vue'
import router from './router/index.js'
// 创建 Vue 应用实例
const app = createApp(App)
// 使用路由 - 注意:Chrome 插件中必须使用 Hash 模式
app.use(router)
// 挂载应用到 DOM
app.mount('#app')
// 调试信息
console.log('Vue3 Chrome Extension Popup 已启动')关键点说明:
使用 createWebHashHistory 而不是 createWebHistory Chrome 插件环境不支持 HTML5 History API Hash 模式确保路由在插件环境中正常工作 3.3 src/App.vue - 主应用组件
<template>
<div class="app">
<!-- 导航栏 -->
<nav class="nav">
<router-link
v-for="route in routes"
:key="route.path"
:to="route.path"
class="nav-item"
:class="{ active: $route.path === route.path }"
>
<span class="nav-icon">{{ route.icon }}</span>
<span class="nav-text">{{ route.name }}</span>
</router-link>
</nav>
<!-- 页面内容区域 -->
<main class="content">
<!-- Vue Router 视图 -->
<router-view />
</main>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
// 路由配置数据
routes: [
{ path: '/', name: '首页', icon: '🏠' },
{ path: '/tools', name: '工具', icon: '🔧' },
{ path: '/settings', name: '设置', icon: '⚙️' },
{ path: '/about', name: '关于', icon: 'ℹ️' }
]
}
}
}
</script>
<style scoped>
/* 应用容器 */
.app {
display: flex;
flex-direction: column;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* 导航栏样式 */
.nav {
display: flex;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding: 8px;
}
/* 导航项样式 */
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 4px;
text-decoration: none;
color: rgba(255, 255, 255, 0.8);
border-radius: 8px;
transition: all 0.3s ease;
font-size: 12px;
}
/* 导航项悬停效果 */
.nav-item:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
transform: translateY(-2px);
}
/* 激活状态 */
.nav-item.active {
background: rgba(255, 255, 255, 0.2);
color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 图标样式 */
.nav-icon {
font-size: 16px;
margin-bottom: 4px;
}
/* 内容区域 */
.content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
/* 滚动条样式 */
.content::-webkit-scrollbar {
width: 6px;
}
.content::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
.content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style>3.4 路由配置 - src/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
// 导入页面组件
import Home from '../views/Home.vue'
import Tools from '../views/Tools.vue'
import Settings from '../views/Settings.vue'
import About from '../views/About.vue'
// 路由配置
const routes = [
{
path: '/',
name: 'Home',
component: Home,
meta: { title: '首页' }
},
{
path: '/tools',
name: 'Tools',
component: Tools,
meta: { title: '工具' }
},
{
path: '/settings',
name: 'Settings',
component: Settings,
meta: { title: '设置' }
},
{
path: '/about',
name: 'About',
component: About,
meta: { title: '关于' }
}
]
// 创建路由实例
const router = createRouter({
// 使用 Hash 模式 - Chrome 插件必须使用此模式
history: createWebHashHistory(),
routes
})
// 路由守卫 - 设置页面标题
router.beforeEach((to, from, next) => {
// 更新文档标题
if (to.meta.title) {
document.title = `${to.meta.title} - Vue3 Chrome Extension`
}
next()
})
export default router路由配置要点:
Hash 模式: Chrome 插件环境限制,必须使用 Hash 路由 元信息: 使用 meta 字段存储页面标题等信息 路由守卫: 实现页面切换时的逻辑处理 🔧 第四章:页面组件开发 4.1 首页组件 - src/views/Home.vue
<template>
<div class="home">
<!-- 页面标题 -->
<h2 class="title">🏠 插件首页</h2>
<!-- 当前页面信息卡片 -->
<div class="card">
<h3>📄 当前页面信息</h3>
<div class="info-item">
<strong>标题:</strong>
<span>{{ pageInfo.title || '获取中...' }}</span>
</div>
<div class="info-item">
<strong>URL:</strong>
<span class="url">{{ pageInfo.url || '获取中...' }}</span>
</div>
<button @click="refreshPageInfo" class="btn btn-primary">
🔄 刷新信息
</button>
</div>
<!-- 快速操作区域 -->
<div class="card">
<h3>⚡ 快速操作</h3>
<div class="quick-actions">
<button
@click="highlightLinks"
class="btn btn-success"
:disabled="loading.highlight"
>
{{ loading.highlight ? '处理中...' : '🔗 高亮链接' }}
</button>
<button
@click="removeHighlight"
class="btn btn-warning"
:disabled="loading.remove"
>
{{ loading.remove ? '处理中...' : '🚫 移除高亮' }}
</button>
<button
@click="getPageStats"
class="btn btn-info"
:disabled="loading.stats"
>
{{ loading.stats ? '分析中...' : '📊 页面统计' }}
</button>
</div>
</div>
<!-- 页面统计信息 -->
<div v-if="pageStats" class="card">
<h3>📊 页面统计</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">链接数量</span>
<span class="stat-value">{{ pageStats.linkCount }}</span>
</div>
<div class="stat-item">
<span class="stat-label">图片数量</span>
<span class="stat-value">{{ pageStats.imageCount }}</span>
</div>
<div class="stat-item">
<span class="stat-label">文本长度</span>
<span class="stat-value">{{ pageStats.textLength }}</span>
</div>
<div class="stat-item">
<span class="stat-label">标题数量</span>
<span class="stat-value">{{ pageStats.headingCount }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Home',
data() {
return {
// 页面信息
pageInfo: {
title: '',
url: ''
},
// 页面统计数据
pageStats: null,
// 加载状态
loading: {
highlight: false,
remove: false,
stats: false
}
}
},
// 组件挂载时获取页面信息
mounted() {
this.getPageInfo()
},
methods: {
// 获取当前页面信息
async getPageInfo() {
try {
// 向 background script 发送消息
const response = await chrome.runtime.sendMessage({
action: 'getTabInfo'
})
if (response) {
this.pageInfo = response
}
} catch (error) {
console.error('获取页面信息失败:', error)
}
},
// 刷新页面信息
refreshPageInfo() {
this.getPageInfo()
},
// 高亮页面链接
async highlightLinks() {
this.loading.highlight = true
try {
// 获取当前活动标签页
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
})
// 向 content script 发送消息
await chrome.tabs.sendMessage(tab.id, {
action: 'highlightLinks'
})
console.log('链接高亮完成')
} catch (error) {
console.error('高亮链接失败:', error)
} finally {
this.loading.highlight = false
}
},
// 移除高亮
async removeHighlight() {
this.loading.remove = true
try {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
})
await chrome.tabs.sendMessage(tab.id, {
action: 'removeHighlight'
})
console.log('高亮移除完成')
} catch (error) {
console.error('移除高亮失败:', error)
} finally {
this.loading.remove = false
}
},
// 获取页面统计信息
async getPageStats() {
this.loading.stats = true
try {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
})
const response = await chrome.tabs.sendMessage(tab.id, {
action: 'getPageStats'
})
if (response) {
this.pageStats = response
}
} catch (error) {
console.error('获取页面统计失败:', error)
} finally {
this.loading.stats = false
}
}
}
}
</script>
<style scoped>
/* 首页容器 */
.home {
color: white;
}
/* 页面标题 */
.title {
text-align: center;
margin-bottom: 20px;
font-size: 24px;
font-weight: 600;
}
/* 卡片样式 */
.card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.card h3 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
}
/* 信息项样式 */
.info-item {
margin-bottom: 8px;
font-size: 14px;
}
.info-item strong {
color: rgba(255, 255, 255, 0.9);
}
.url {
word-break: break-all;
font-family: monospace;
font-size: 12px;
}
/* 按钮样式 */
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: all 0.3s ease;
margin: 4px;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* 按钮颜色变体 */
.btn-primary {
background: #007bff;
color: white;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-warning {
background: #ffc107;
color: #212529;
}
.btn-info {
background: #17a2b8;
color: white;
}
/* 快速操作区域 */
.quick-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
/* 统计信息网格 */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
}
.stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 4px;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: #ffc107;
}
</style>4.2 工具页面 - src/views/Tools.vue
<template>
<div class="tools">
<h2 class="title">🔧 页面工具</h2>
<!-- 样式工具 -->
<div class="card">
<h3>🎨 页面美化</h3>
<div class="tool-grid">
<button @click="toggleDarkMode" class="tool-btn">
🌙 暗黑模式
</button>
<button @click="toggleReadingMode" class="tool-btn">
📖 阅读模式
</button>
<button @click="increaseFontSize" class="tool-btn">
🔍 增大字体
</button>
<button @click="resetStyles" class="tool-btn">
🔄 重置样式
</button>
</div>
</div>
<!-- 分析工具 -->
<div class="card">
<h3>📊 页面分析</h3>
<div class="tool-grid">
<button @click="analyzeImages" class="tool-btn">
🖼️ 图片分析
</button>
<button @click="analyzePerformance" class="tool-btn">
⚡ 性能分析
</button>
<button @click="extractLinks" class="tool-btn">
🔗 提取链接
</button>
<button @click="checkAccessibility" class="tool-btn">
♿ 可访问性
</button>
</div>
</div>
<!-- 结果显示区域 -->
<div v-if="analysisResult" class="card">
<h3>📋 分析结果</h3>
<div class="result-content">
<pre>{{ JSON.stringify(analysisResult, null, 2) }}</pre>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Tools',
data() {
return {
analysisResult: null
}
},
methods: {
// 发送消息到 content script
async sendMessage(action, data = {}) {
try {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
})
const response = await chrome.tabs.sendMessage(tab.id, {
action,
...data
})
return response
} catch (error) {
console.error(`执行 ${action} 失败:`, error)
return null
}
},
// 切换暗黑模式
async toggleDarkMode() {
await this.sendMessage('toggleDarkMode')
},
// 切换阅读模式
async toggleReadingMode() {
await this.sendMessage('toggleReadingMode')
},
// 增大字体
async increaseFontSize() {
await this.sendMessage('increaseFontSize')
},
// 重置样式
async resetStyles() {
await this.sendMessage('resetStyles')
},
// 分析图片
async analyzeImages() {
const result = await this.sendMessage('analyzeImages')
this.analysisResult = result
},
// 性能分析
async analyzePerformance() {
const result = await this.sendMessage('analyzePerformance')
this.analysisResult = result
},
// 提取链接
async extractLinks() {
const result = await this.sendMessage('extractLinks')
this.analysisResult = result
},
// 检查可访问性
async checkAccessibility() {
const result = await this.sendMessage('analyzeAccessibility')
this.analysisResult = result
}
}
}
</script>
<style scoped>
.tools {
color: white;
}
.title {
text-align: center;
margin-bottom: 20px;
font-size: 24px;
font-weight: 600;
}
.card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.card h3 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
}
.tool-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.tool-btn {
padding: 12px 8px;
border: none;
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
color: white;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.tool-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.result-content {
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
padding: 12px;
max-height: 200px;
overflow-y: auto;
}
.result-content pre {
margin: 0;
font-size: 11px;
color: #e0e0e0;
white-space: pre-wrap;
word-break: break-word;
}
</style>🔄 第五章:Content Script 模块化架构 5.1 架构设计理念 Content Script 是插件与网页交互的核心,我们采用模块化设计:
src/content/
├── index.js # 主入口 - 模块协调器
├── messageHandler.js # 消息处理 - 统一消息路由
├── linkManager.js # 链接管理 - 链接相关功能
├── styleManager.js # 样式管理 - 页面美化功能
├── analysisTools.js # 分析工具 - 页面分析功能
└── storageManager.js # 存储管理 - 数据持久化设计原则:
单一职责: 每个模块负责特定功能领域 松耦合: 模块间通过消息机制通信 可扩展: 新功能可以独立模块开发 可维护: 清晰的代码结构和注释 5.2 主入口文件 - src/content/index.js
/**
* Content Script 主入口文件
* 负责初始化各个功能模块并协调它们之间的工作
*/
// 导入各功能模块
import MessageHandler from './messageHandler.js'
import LinkManager from './linkManager.js'
import StyleManager from './styleManager.js'
import AnalysisTools from './analysisTools.js'
import StorageManager from './storageManager.js'
/**
* Content Script 主类
* 作为各个模块的协调器和入口点
*/
class ContentScript {
constructor() {
// 初始化各功能模块
this.messageHandler = new MessageHandler()
this.linkManager = new LinkManager()
this.styleManager = new StyleManager()
this.analysisTools = new AnalysisTools()
this.storageManager = new StorageManager()
// 初始化完成标志
this.initialized = false
}
/**
* 初始化 Content Script
* 设置消息处理器和各模块的交互
*/
async init() {
try {
console.log('🚀 Content Script 开始初始化...')
// 1. 初始化存储管理器(优先级最高)
await this.storageManager.init()
// 2. 初始化样式管理器
await this.styleManager.init()
// 3. 初始化链接管理器
await this.linkManager.init()
// 4. 初始化分析工具
await this.analysisTools.init()
// 5. 设置消息处理器
this.setupMessageHandlers()
// 6. 检查自动高亮设置
await this.checkAutoHighlight()
this.initialized = true
console.log('✅ Content Script 初始化完成')
} catch (error) {
console.error('❌ Content Script 初始化失败:', error)
}
}
/**
* 设置消息处理器
* 注册各种消息的处理函数
*/
setupMessageHandlers() {
// 链接相关操作
this.messageHandler.register('highlightLinks', () => {
return this.linkManager.highlightAllLinks()
})
this.messageHandler.register('removeHighlight', () => {
return this.linkManager.removeAllHighlight()
})
this.messageHandler.register('extractLinks', () => {
return this.linkManager.extractAllLinks()
})
// 页面统计
this.messageHandler.register('getPageStats', () => {
return this.analysisTools.getPageStats()
})
// 样式管理操作
this.messageHandler.register('toggleDarkMode', () => {
return this.styleManager.toggleDarkMode()
})
this.messageHandler.register('toggleReadingMode', () => {
return this.styleManager.toggleReadingMode()
})
this.messageHandler.register('increaseFontSize', () => {
return this.styleManager.increaseFontSize()
})
this.messageHandler.register('resetStyles', () => {
return this.styleManager.resetAllStyles()
})
// 分析工具
this.messageHandler.register('analyzeImages', () => {
return this.analysisTools.analyzeImages()
})
this.messageHandler.register('analyzePerformance', () => {
return this.analysisTools.analyzePerformance()
})
this.messageHandler.register('analyzeAccessibility', () => {
return this.analysisTools.analyzeAccessibility()
})
console.log('📝 消息处理器注册完成')
}
/**
* 检查自动高亮设置
* 如果启用了自动高亮,则自动执行链接高亮
*/
async checkAutoHighlight() {
try {
const settings = await this.storageManager.getSettings()
if (settings.autoHighlight) {
console.log('🔗 自动高亮已启用,开始高亮链接')
await this.linkManager.highlightAllLinks()
}
} catch (error) {
console.error('检查自动高亮设置失败:', error)
}
}
/**
* 获取模块实例
* 提供外部访问各模块的接口
*/
getModule(moduleName) {
const modules = {
messageHandler: this.messageHandler,
linkManager: this.linkManager,
styleManager: this.styleManager,
analysisTools: this.analysisTools,
storageManager: this.storageManager
}
return modules[moduleName] || null
}
}
// 创建并初始化 Content Script 实例
const contentScript = new ContentScript()
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
contentScript.init()
})
} else {
// 如果页面已经加载完成,直接初始化
contentScript.init()
}
// 将实例暴露到全局,便于调试
window.contentScript = contentScript
// 导出实例(用于模块化环境)
export default contentScript5.3 消息处理器 - src/content/messageHandler.js
/**
* 消息处理器模块
* 统一管理来自 popup 和 background 的消息
* 提供消息路由和处理机制
*/
class MessageHandler {
constructor() {
// 消息处理器映射表
this.handlers = new Map()
// 消息处理统计
this.stats = {
received: 0,
processed: 0,
errors: 0
}
// 初始化消息监听
this.setupMessageListener()
}
/**
* 设置消息监听器
* 监听来自 popup 和 background 的消息
*/
setupMessageListener() {
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
// 更新接收统计
this.stats.received++
console.log('📨 收到消息:', request)
// 异步处理消息
this.handleMessage(request, sender)
.then(result => {
// 发送响应
sendResponse({
success: true,
data: result,
timestamp: Date.now()
})
// 更新处理统计
this.stats.processed++
})
.catch(error => {
console.error('❌ 消息处理失败:', error)
// 发送错误响应
sendResponse({
success: false,
error: error.message,
timestamp: Date.now()
})
// 更新错误统计
this.stats.errors++
})
// 返回 true 表示异步响应
return true
})
console.log('👂 消息监听器已设置')
}
/**
* 处理消息
* @param {Object} request - 消息请求对象
* @param {Object} sender - 消息发送者信息
* @returns {Promise} 处理结果
*/
async handleMessage(request, sender) {
const { action, ...params } = request
// 检查是否有对应的处理器
if (!this.handlers.has(action)) {
throw new Error(`未知的操作: ${action}`)
}
// 获取处理器函数
const handler = this.handlers.get(action)
try {
// 执行处理器
const result = await handler(params, sender)
console.log(`✅ 操作 ${action} 执行成功:`, result)
return result
} catch (error) {
console.error(`❌ 操作 ${action} 执行失败:`, error)
throw error
}
}
/**
* 注册消息处理器
* @param {string} action - 操作名称
* @param {Function} handler - 处理器函数
*/
register(action, handler) {
if (typeof handler !== 'function') {
throw new Error('处理器必须是一个函数')
}
this.handlers.set(action, handler)
console.log(`📝 注册消息处理器: ${action}`)
}
/**
* 移除消息处理器
* @param {string} action - 操作名称
*/
unregister(action) {
if (this.handlers.has(action)) {
this.handlers.delete(action)
console.log(`🗑️ 移除消息处理器: ${action}`)
}
}
/**
* 获取已注册的处理器列表
* @returns {Array} 处理器名称列表
*/
getRegisteredHandlers() {
return Array.from(this.handlers.keys())
}
/**
* 获取消息处理统计信息
* @returns {Object} 统计信息
*/
getStats() {
return {
...this.stats,
successRate: this.stats.received > 0
? ((this.stats.processed / this.stats.received) * 100).toFixed(2) + '%'
: '0%',
errorRate: this.stats.received > 0
? ((this.stats.errors / this.stats.received) * 100).toFixed(2) + '%'
: '0%'
}
}
/**
* 重置统计信息
*/
resetStats() {
this.stats = {
received: 0,
processed: 0,
errors: 0
}
console.log('📊 消息处理统计已重置')
}
}
export default MessageHandler5.4 链接管理器 - src/content/linkManager.js
/**
* 链接管理器模块
* 负责页面链接的高亮、提取、分析等功能
*/
class LinkManager {
constructor() {
// 高亮样式 ID
this.styleId = 'vue3-extension-link-highlight'
// 高亮状态
this.isHighlighted = false
// 链接缓存
this.linkCache = new Map()
// 配置选项
this.options = {
highlightColor: '#ffeb3b',
highlightOpacity: 0.3,
animationDuration: '0.3s'
}
}
/**
* 初始化链接管理器
*/
async init() {
console.log('🔗 链接管理器初始化中...')
// 注入高亮样式
this.injectHighlightStyles()
// 缓存页面链接
this.cachePageLinks()
console.log('✅ 链接管理器初始化完成')
}
/**
* 注入链接高亮样式
*/
injectHighlightStyles() {
// 检查是否已经注入
if (document.getElementById(this.styleId)) {
return
}
const style = document.createElement('style')
style.id = this.styleId
style.textContent = `
/* Vue3 Extension 链接高亮样式 */
.vue3-ext-highlighted-link {
background-color: ${this.options.highlightColor} !important;
opacity: ${this.options.highlightOpacity} !important;
transition: all ${this.options.animationDuration} ease !important;
border-radius: 3px !important;
padding: 2px 4px !important;
margin: 0 1px !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
}
.vue3-ext-highlighted-link:hover {
opacity: 0.6 !important;
transform: scale(1.02) !important;
}
/* 不同类型链接的颜色区分 */
.vue3-ext-highlighted-link.external {
background-color: #f44336 !important;
}
.vue3-ext-highlighted-link.internal {
background-color: #4caf50 !important;
}
.vue3-ext-highlighted-link.anchor {
background-color: #2196f3 !important;
}
`
document.head.appendChild(style)
console.log('🎨 链接高亮样式已注入')
}
/**
* 缓存页面链接
*/
cachePageLinks() {
const links = document.querySelectorAll('a[href]')
this.linkCache.clear()
links.forEach((link, index) => {
const href = link.getAttribute('href')
const text = link.textContent.trim()
const type = this.getLinkType(href)
this.linkCache.set(index, {
element: link,
href,
text,
type,
isValid: this.isValidLink(href)
})
})
console.log(`📋 已缓存 ${this.linkCache.size} 个链接`)
}
/**
* 判断链接类型
* @param {string} href - 链接地址
* @returns {string} 链接类型
*/
getLinkType(href) {
if (!href) return 'invalid'
if (href.startsWith('#')) {
return 'anchor' // 锚点链接
}
if (href.startsWith('http') || href.startsWith('//')) {
const currentDomain = window.location.hostname
const linkDomain = new URL(href, window.location.origin).hostname
return linkDomain === currentDomain ? 'internal' : 'external'
}
if (href.startsWith('/') || href.startsWith('./') || href.startsWith('../')) {
return 'internal' // 相对链接
}
return 'other'
}
/**
* 检查链接是否有效
* @param {string} href - 链接地址
* @returns {boolean} 是否有效
*/
isValidLink(href) {
if (!href || href === '#') return false
// 排除 JavaScript 链接
if (href.startsWith('javascript:')) return false
// 排除邮件链接(可选)
if (href.startsWith('mailto:')) return false
return true
}
/**
* 高亮所有链接
* @returns {Object} 高亮结果
*/
highlightAllLinks() {
try {
// 如果已经高亮,先移除
if (this.isHighlighted) {
this.removeAllHighlight()
}
let highlightedCount = 0
this.linkCache.forEach((linkInfo) => {
const { element, type, isValid } = linkInfo
if (isValid && element) {
// 添加高亮类
element.classList.add('vue3-ext-highlighted-link', type)
highlightedCount++
}
})
this.isHighlighted = true
const result = {
success: true,
highlightedCount,
totalLinks: this.linkCache.size,
timestamp: Date.now()
}
console.log('🔆 链接高亮完成:', result)
return result
} catch (error) {
console.error('❌ 链接高亮失败:', error)
throw error
}
}
/**
* 移除所有高亮
* @returns {Object} 移除结果
*/
removeAllHighlight() {
try {
let removedCount = 0
// 移除所有高亮类
const highlightedLinks = document.querySelectorAll('.vue3-ext-highlighted-link')
highlightedLinks.forEach(link => {
link.classList.remove(
'vue3-ext-highlighted-link',
'external',
'internal',
'anchor'
)
removedCount++
})
this.isHighlighted = false
const result = {
success: true,
removedCount,
timestamp: Date.now()
}
console.log('🚫 链接高亮已移除:', result)
return result
} catch (error) {
console.error('❌ 移除链接高亮失败:', error)
throw error
}
}
/**
* 提取所有链接
* @returns {Object} 链接提取结果
*/
extractAllLinks() {
try {
const links = []
const stats = {
total: 0,
internal: 0,
external: 0,
anchor: 0,
invalid: 0
}
this.linkCache.forEach((linkInfo) => {
const { href, text, type, isValid } = linkInfo
if (isValid) {
links.push({
href,
text: text.substring(0, 100), // 限制文本长度
type
})
}
stats.total++
stats[type]++
})
const result = {
success: true,
links,
stats,
timestamp: Date.now()
}
console.log('🔗 链接提取完成:', result)
return result
} catch (error) {
console.error('❌ 链接提取失败:', error)
throw error
}
}
/**
* 获取链接统计信息
* @returns {Object} 统计信息
*/
getLinkStats() {
const stats = {
total: this.linkCache.size,
internal: 0,
external: 0,
anchor: 0,
invalid: 0,
valid: 0
}
this.linkCache.forEach((linkInfo) => {
const { type, isValid } = linkInfo
stats[type]++
if (isValid) stats.valid++
})
return stats
}
/**
* 检查链接有效性(异步)
* @param {string} url - 要检查的链接
* @returns {Promise<boolean>} 是否有效
*/
async checkLinkValidity(url) {
try {
const response = await fetch(url, {
method: 'HEAD',
mode: 'no-cors'
})
return response.ok
} catch (error) {
console.warn('链接检查失败:', url, error)
return false
}
}
/**
* 更新高亮颜色
* @param {string} color - 新的高亮颜色
*/
updateHighlightColor(color) {
this.options.highlightColor = color
// 重新注入样式
const existingStyle = document.getElementById(this.styleId)
if (existingStyle) {
existingStyle.remove()
}
this.injectHighlightStyles()
// 如果当前有高亮,重新应用
if (this.isHighlighted) {
this.removeAllHighlight()
this.highlightAllLinks()
}
}
}
export default LinkManager⚙️ 第六章:后台脚本开发 6.1 background.js - Service Worker
/**
* Background Service Worker
* Chrome Extension 的后台脚本,运行在独立的上下文中
* 负责处理插件的生命周期事件、权限管理、消息路由等
*/
// 插件安装时的初始化处理
chrome.runtime.onInstalled.addListener((details) => {
console.log('🚀 Vue3 Chrome Extension 已安装', details)
// 根据安装原因执行不同的初始化逻辑
switch (details.reason) {
case 'install':
console.log('📦 首次安装插件')
handleFirstInstall()
break
case 'update':
console.log('🔄 插件已更新')
handleUpdate(details.previousVersion)
break
case 'chrome_update':
console.log('🌐 Chrome 浏览器已更新')
break
}
})
/**
* 首次安装处理函数
* 设置默认配置和创建右键菜单
*/
function handleFirstInstall() {
// 设置默认配置到 Chrome 存储
chrome.storage.sync.set({
autoHighlight: false, // 自动高亮链接
highlightColor: '#ffeb3b', // 高亮颜色
darkMode: false, // 暗黑模式
readingMode: false, // 阅读模式
fontSize: 100, // 字体大小百分比
version: '1.0.0' // 插件版本
})
// 创建右键菜单项
createContextMenus()
console.log('✅ 默认配置已设置')
}
/**
* 插件更新处理函数
* @param {string} previousVersion - 之前的版本号
*/
function handleUpdate(previousVersion) {
console.log(`📈 从版本 ${previousVersion} 更新到当前版本`)
// 这里可以添加版本迁移逻辑
// 例如:数据格式变更、新功能默认设置等
}
/**
* 创建右键菜单
*/
function createContextMenus() {
// 主菜单项
chrome.contextMenus.create({
id: 'vue3-extension-main',
title: 'Vue3 插件工具',
contexts: ['page', 'selection', 'link']
})
// 子菜单项 - 链接高亮
chrome.contextMenus.create({
id: 'highlightLinks',
parentId: 'vue3-extension-main',
title: '🔗 高亮页面所有链接',
contexts: ['page']
})
// 子菜单项 - 暗黑模式
chrome.contextMenus.create({
id: 'toggleDarkMode',
parentId: 'vue3-extension-main',
title: '🌙 切换暗黑模式',
contexts: ['page']
})
// 子菜单项 - 阅读模式
chrome.contextMenus.create({
id: 'toggleReadingMode',
parentId: 'vue3-extension-main',
title: '📖 切换阅读模式',
contexts: ['page']
})
console.log('📋 右键菜单已创建')
}
// 处理右键菜单点击事件
chrome.contextMenus.onClicked.addListener((info, tab) => {
console.log('🖱️ 右键菜单被点击:', info.menuItemId)
// 根据菜单项 ID 执行相应操作
switch (info.menuItemId) {
case 'highlightLinks':
sendMessageToContentScript(tab.id, { action: 'highlightLinks' })
break
case 'toggleDarkMode':
sendMessageToContentScript(tab.id, { action: 'toggleDarkMode' })
break
case 'toggleReadingMode':
sendMessageToContentScript(tab.id, { action: 'toggleReadingMode' })
break
default:
console.log('未知的菜单项:', info.menuItemId)
}
})
/**
* 向 Content Script 发送消息的辅助函数
* @param {number} tabId - 标签页 ID
* @param {Object} message - 要发送的消息
*/
async function sendMessageToContentScript(tabId, message) {
try {
const response = await chrome.tabs.sendMessage(tabId, message)
console.log('✅ 消息发送成功:', response)
} catch (error) {
console.error('❌ 消息发送失败:', error)
// 如果 Content Script 未加载,可以尝试注入
if (error.message.includes('Could not establish connection')) {
console.log('🔄 尝试重新注入 Content Script')
await injectContentScript(tabId)
}
}
}
/**
* 注入 Content Script(紧急情况下使用)
* @param {number} tabId - 标签页 ID
*/
async function injectContentScript(tabId) {
try {
await chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
})
console.log('✅ Content Script 注入成功')
} catch (error) {
console.error('❌ Content Script 注入失败:', error)
}
}
// 处理来自 Content Script 或 Popup 的消息
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('📨 Background 收到消息:', request)
// 异步处理消息
handleBackgroundMessage(request, sender)
.then(result => {
sendResponse({
success: true,
data: result,
timestamp: Date.now()
})
})
.catch(error => {
console.error('❌ Background 消息处理失败:', error)
sendResponse({
success: false,
error: error.message,
timestamp: Date.now()
})
})
// 返回 true 表示异步响应
return true
})
/**
* 处理后台消息的主函数
* @param {Object} request - 请求对象
* @param {Object} sender - 发送者信息
* @returns {Promise} 处理结果
*/
async function handleBackgroundMessage(request, sender) {
const { action, ...params } = request
switch (action) {
case 'getTabInfo':
return await getTabInfo()
case 'openOptionsPage':
return await openOptionsPage()
case 'updateBadge':
return await updateBadge(params)
case 'getStorageData':
return await getStorageData(params.keys)
case 'setStorageData':
return await setStorageData(params.data)
default:
throw new Error(`未知的后台操作: ${action}`)
}
}
/**
* 获取当前标签页信息
* @returns {Promise<Object>} 标签页信息
*/
async function getTabInfo() {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
})
if (!tab) {
throw new Error('无法获取当前标签页')
}
return {
id: tab.id,
title: tab.title,
url: tab.url,
favIconUrl: tab.favIconUrl,
status: tab.status
}
}
/**
* 打开选项页面
* @returns {Promise<boolean>} 是否成功打开
*/
async function openOptionsPage() {
try {
await chrome.runtime.openOptionsPage()
return true
} catch (error) {
console.error('打开选项页面失败:', error)
return false
}
}
/**
* 更新插件图标徽章
* @param {Object} params - 徽章参数
* @returns {Promise<boolean>} 是否成功更新
*/
async function updateBadge({ text, color, tabId }) {
try {
if (text !== undefined) {
await chrome.action.setBadgeText({
text: String(text),
tabId
})
}
if (color) {
await chrome.action.setBadgeBackgroundColor({
color,
tabId
})
}
return true
} catch (error) {
console.error('更新徽章失败:', error)
return false
}
}
/**
* 获取存储数据
* @param {Array|string} keys - 要获取的键
* @returns {Promise<Object>} 存储数据
*/
async function getStorageData(keys) {
return new Promise((resolve, reject) => {
chrome.storage.sync.get(keys, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message))
} else {
resolve(result)
}
})
})
}
/**
* 设置存储数据
* @param {Object} data - 要存储的数据
* @returns {Promise<boolean>} 是否成功设置
*/
async function setStorageData(data) {
return new Promise((resolve, reject) => {
chrome.storage.sync.set(data, () => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message))
} else {
resolve(true)
}
})
})
}
// 监听标签页更新事件
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
// 只在页面完全加载后处理
if (changeInfo.status === 'complete' && tab.url) {
console.log('📄 页面加载完成:', tab.url)
try {
// 检查自动高亮设置
const settings = await getStorageData(['autoHighlight'])
if (settings.autoHighlight) {
console.log('🔗 自动高亮已启用,准备高亮链接')
// 延迟执行,确保 Content Script 已加载
setTimeout(async () => {
await sendMessageToContentScript(tabId, {
action: 'highlightLinks'
})
}, 1000)
}
// 重置徽章
await updateBadge({ text: '', tabId })
} catch (error) {
console.error('处理页面更新事件失败:', error)
}
}
})
// 监听存储变化事件
chrome.storage.onChanged.addListener((changes, namespace) => {
console.log('💾 存储发生变化:', changes, namespace)
// 处理特定设置的变化
Object.keys(changes).forEach(key => {
const { oldValue, newValue } = changes[key]
switch (key) {
case 'autoHighlight':
console.log(`🔗 自动高亮设置变更: ${oldValue} -> ${newValue}`)
break
case 'highlightColor':
console.log(`🎨 高亮颜色变更: ${oldValue} -> ${newValue}`)
break
default:
console.log(`⚙️ 设置 ${key} 变更: ${oldValue} -> ${newValue}`)
}
})
})
// 监听插件启动事件
chrome.runtime.onStartup.addListener(() => {
console.log('🚀 Chrome 启动,插件后台脚本已加载')
})
// 监听插件挂起事件
chrome.runtime.onSuspend.addListener(() => {
console.log('😴 插件后台脚本即将挂起')
// 在挂起前清理资源
// 例如:关闭数据库连接、保存状态等
})
console.log('✅ Vue3 Chrome Extension Background Script 已启动')Background Script 关键特性:
生命周期管理: 处理安装、更新、启动等事件 权限管理: 管理插件权限和 API 访问 消息路由: 在 popup 和 content script 之间传递消息 存储管理: 统一管理插件配置和数据 右键菜单: 提供便捷的页面操作入口 自动化功能: 根据设置自动执行某些操作 🔧 第七章:构建和部署 7.1 构建流程详解
# 开发模式 - 实时预览
npm run dev
# 生产构建 - 生成插件文件
npm run build
# 预览构建结果
npm run preview构建过程分析:
Vite 解析配置: 读取 vite.config.js 配置 多入口处理: 分别构建 popup、background、content 脚本 Vue 组件编译: 将 .vue 文件编译为 JavaScript 代码优化: 压缩、tree-shaking、代码分割 资源处理: 处理 CSS、图片等静态资源 文件输出: 生成到 dist 目录 自动复制: 复制 manifest.json 到输出目录 7.2 部署到 Chrome 商店 步骤 1: 准备发布文件
# 构建生产版本
npm run build
# 检查 dist 目录内容
ls -la dist/
# 应该包含:
# - manifest.json
# - popup.html
# - popup.js
# - popup.css
# - background.js
# - content.js步骤 2: 创建发布包
# 进入构建目录
cd dist
# 创建 ZIP 包
zip -r ../vue3-chrome-extension.zip .
# 返回项目根目录
cd ..步骤 3: 上传到 Chrome 开发者控制台
访问 Chrome 开发者控制台 点击「新增项目」 上传 vue3-chrome-extension.zip 填写插件信息: 名称和描述 图标和截图 分类和标签 隐私政策 提交审核 7.3 本地测试和调试 加载未打包的扩展程序:
打开 Chrome 浏览器 访问 chrome://extensions/ 开启「开发者模式」 点击「加载已解压的扩展程序」 选择项目的 dist 目录 调试技巧:
// 在 Content Script 中调试
console.log('Content Script 调试信息')
debugger // 设置断点
// 在 Background Script 中调试
// 右键插件图标 -> 检查弹出内容 -> 背景页
console.log('Background Script 调试信息')
// 在 Popup 中调试
// 右键插件图标 -> 检查弹出内容
console.log('Popup 调试信息')📊 第八章:性能优化和最佳实践 8.1 性能优化策略
- 代码分割和懒加载
// 在 Vue Router 中使用懒加载
const routes = [
{
path: '/tools',
name: 'Tools',
// 懒加载组件
component: () => import('../views/Tools.vue')
}
]- Content Script 优化
// 使用防抖函数避免频繁操作
function debounce(func, wait) {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
// 优化 DOM 查询
class OptimizedLinkManager {
constructor() {
this.linkCache = new WeakMap() // 使用 WeakMap 避免内存泄漏
this.observer = null
}
// 使用 MutationObserver 监听 DOM 变化
setupDOMObserver() {
this.observer = new MutationObserver(
debounce(this.handleDOMChanges.bind(this), 300)
)
this.observer.observe(document.body, {
childList: true,
subtree: true
})
}
handleDOMChanges(mutations) {
// 只处理新增的链接
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
const links = node.querySelectorAll('a[href]')
this.cacheNewLinks(links)
}
})
})
}
}- 内存管理
// 清理事件监听器
class ComponentWithCleanup {
constructor() {
this.eventListeners = []
}
addEventListener(element, event, handler) {
element.addEventListener(event, handler)
this.eventListeners.push({ element, event, handler })
}
cleanup() {
// 移除所有事件监听器
this.eventListeners.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler)
})
this.eventListeners = []
}
}8.2 安全最佳实践
- 内容安全策略 (CSP)
// manifest.json 中添加 CSP
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}- 权限最小化原则
// 只请求必要的权限
{
"permissions": [
"activeTab", // 而不是 "tabs"
"storage" // 而不是 "unlimitedStorage"
]
}- 输入验证和清理
// 清理用户输入
function sanitizeInput(input) {
return input
.replace(/[<>"'&]/g, '') // 移除危险字符
.trim()
.substring(0, 1000) // 限制长度
}
// 验证 URL
function isValidURL(url) {
try {
new URL(url)
return true
} catch {
return false
}
}8.3 用户体验优化
- 加载状态管理
<template>
<div class="tool-button">
<button
@click="executeAction"
:disabled="loading"
class="btn"
>
<span v-if="loading" class="spinner"></span>
{{ loading ? '处理中...' : '执行操作' }}
</button>
</div>
</template>
<style>
.spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>- 错误处理和用户反馈
// 全局错误处理
class ErrorHandler {
static showNotification(message, type = 'info') {
// 创建通知元素
const notification = document.createElement('div')
notification.className = `notification notification-${type}`
notification.textContent = message
// 添加到页面
document.body.appendChild(notification)
// 自动移除
setTimeout(() => {
notification.remove()
}, 3000)
}
static handleError(error, context = '') {
console.error(`错误 [${context}]:`, error)
// 显示用户友好的错误信息
const userMessage = this.getUserFriendlyMessage(error)
this.showNotification(userMessage, 'error')
}
static getUserFriendlyMessage(error) {
const errorMessages = {
'NetworkError': '网络连接失败,请检查网络设置',
'PermissionError': '权限不足,请检查插件权限设置',
'TimeoutError': '操作超时,请稍后重试'
}
return errorMessages[error.name] || '操作失败,请稍后重试'
}
}🎯 第九章:实战案例和扩展 9.1 案例 1:智能链接分析器 这个案例展示如何扩展链接管理器,添加智能分析功能:
// 扩展的链接分析器
class SmartLinkAnalyzer extends LinkManager {
constructor() {
super()
this.analysisCache = new Map()
this.patterns = {
social: /(?:facebook|twitter|instagram|linkedin|youtube)\.com/i,
shopping: /(?:amazon|ebay|taobao|tmall)\.com/i,
news: /(?:cnn|bbc|reuters|xinhua)\.com/i,
tech: /(?:github|stackoverflow|medium)\.com/i
}
}
/**
* 智能分析链接类型和安全性
*/
async analyzeLinksIntelligently() {
const analysis = {
categories: {},
security: {
safe: 0,
suspicious: 0,
unknown: 0
},
recommendations: []
}
for (const [index, linkInfo] of this.linkCache.entries()) {
const { href, text } = linkInfo
// 分类分析
const category = this.categorizeLink(href)
analysis.categories[category] = (analysis.categories[category] || 0) + 1
// 安全性分析
const securityLevel = await this.analyzeLinkSecurity(href)
analysis.security[securityLevel]++
// 缓存分析结果
this.analysisCache.set(index, {
category,
securityLevel,
timestamp: Date.now()
})
}
// 生成建议
analysis.recommendations = this.generateRecommendations(analysis)
return analysis
}
/**
* 链接分类
*/
categorizeLink(href) {
for (const [category, pattern] of Object.entries(this.patterns)) {
if (pattern.test(href)) {
return category
}
}
return 'other'
}
/**
* 安全性分析
*/
async analyzeLinkSecurity(href) {
try {
// 检查是否为 HTTPS
if (!href.startsWith('https://')) {
return 'suspicious'
}
// 检查域名长度(过长可能是钓鱼网站)
const domain = new URL(href).hostname
if (domain.length > 50) {
return 'suspicious'
}
// 检查是否包含可疑字符
if (/[\u4e00-\u9fa5]/.test(domain)) { // 包含中文字符的域名
return 'suspicious'
}
return 'safe'
} catch {
return 'unknown'
}
}
/**
* 生成安全建议
*/
generateRecommendations(analysis) {
const recommendations = []
if (analysis.security.suspicious > 0) {
recommendations.push({
type: 'warning',
message: `发现 ${analysis.security.suspicious} 个可疑链接,建议谨慎点击`
})
}
if (analysis.categories.shopping > 5) {
recommendations.push({
type: 'info',
message: '页面包含大量购物链接,建议开启广告拦截器'
})
}
return recommendations
}
}9.2 案例 2:页面性能监控器
// 页面性能监控器
class PerformanceMonitor {
constructor() {
this.metrics = {}
this.observers = []
}
/**
* 开始性能监控
*/
startMonitoring() {
this.collectNavigationMetrics()
this.setupPerformanceObserver()
this.monitorResourceLoading()
this.trackUserInteractions()
}
/**
* 收集导航性能指标
*/
collectNavigationMetrics() {
const navigation = performance.getEntriesByType('navigation')[0]
if (navigation) {
this.metrics.navigation = {
// DNS 查询时间
dnsLookup: navigation.domainLookupEnd - navigation.domainLookupStart,
// TCP 连接时间
tcpConnect: navigation.connectEnd - navigation.connectStart,
// 请求响应时间
requestResponse: navigation.responseEnd - navigation.requestStart,
// DOM 解析时间
domParsing: navigation.domContentLoadedEventEnd - navigation.responseEnd,
// 页面完全加载时间
pageLoad: navigation.loadEventEnd - navigation.navigationStart
}
}
}
/**
* 设置性能观察器
*/
setupPerformanceObserver() {
// 监控 LCP (Largest Contentful Paint)
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
this.metrics.lcp = lastEntry.startTime
})
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] })
this.observers.push(lcpObserver)
// 监控 FID (First Input Delay)
const fidObserver = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
this.metrics.fid = entry.processingStart - entry.startTime
})
})
fidObserver.observe({ entryTypes: ['first-input'] })
this.observers.push(fidObserver)
// 监控 CLS (Cumulative Layout Shift)
let clsValue = 0
const clsObserver = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
if (!entry.hadRecentInput) {
clsValue += entry.value
}
})
this.metrics.cls = clsValue
})
clsObserver.observe({ entryTypes: ['layout-shift'] })
this.observers.push(clsObserver)
}
/**
* 监控资源加载
*/
monitorResourceLoading() {
const resources = performance.getEntriesByType('resource')
this.metrics.resources = {
total: resources.length,
images: resources.filter(r => r.initiatorType === 'img').length,
scripts: resources.filter(r => r.initiatorType === 'script').length,
stylesheets: resources.filter(r => r.initiatorType === 'link').length,
slowResources: resources.filter(r => r.duration > 1000).length
}
}
/**
* 跟踪用户交互
*/
trackUserInteractions() {
let interactionCount = 0
const trackInteraction = () => {
interactionCount++
}
['click', 'scroll', 'keydown'].forEach(event => {
document.addEventListener(event, trackInteraction, { passive: true })
})
// 每 5 秒更新一次交互统计
setInterval(() => {
this.metrics.interactions = {
total: interactionCount,
rate: interactionCount / (Date.now() - performance.timing.navigationStart) * 1000
}
}, 5000)
}
/**
* 生成性能报告
*/
generateReport() {
const report = {
timestamp: Date.now(),
url: window.location.href,
metrics: this.metrics,
recommendations: this.generateRecommendations()
}
return report
}
/**
* 生成性能优化建议
*/
generateRecommendations() {
const recommendations = []
// LCP 建议
if (this.metrics.lcp > 2500) {
recommendations.push({
type: 'warning',
metric: 'LCP',
message: 'Largest Contentful Paint 过慢,建议优化图片和关键资源加载'
})
}
// FID 建议
if (this.metrics.fid > 100) {
recommendations.push({
type: 'warning',
metric: 'FID',
message: 'First Input Delay 过长,建议减少 JavaScript 执行时间'
})
}
// CLS 建议
if (this.metrics.cls > 0.1) {
recommendations.push({
type: 'warning',
metric: 'CLS',
message: 'Cumulative Layout Shift 过大,建议为图片和广告预留空间'
})
}
return recommendations
}
/**
* 清理监控器
*/
cleanup() {
this.observers.forEach(observer => observer.disconnect())
this.observers = []
}
}🔍 第十章:调试和故障排除 10.1 常见问题和解决方案 问题 1: Content Script 无法注入
// 解决方案:检查 manifest.json 配置
{
"content_scripts": [
{
"matches": ["<all_urls>"], // 确保匹配模式正确
"js": ["content.js"],
"run_at": "document_end" // 确保在 DOM 加载后运行
}
]
}
// 在 background.js 中添加错误处理
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete') {
chrome.tabs.sendMessage(tabId, { action: 'ping' })
.catch(error => {
console.log('Content script 未加载,尝试注入:', error)
// 手动注入 content script
chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
})
})
}
})问题 2: 消息传递失败
// 解决方案:添加重试机制
class ReliableMessaging {
static async sendMessage(tabId, message, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await chrome.tabs.sendMessage(tabId, message)
return response
} catch (error) {
console.warn(`消息发送失败,重试 ${i + 1}/${maxRetries}:`, error)
if (i === maxRetries - 1) {
throw error
}
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
}
}问题 3: 样式冲突
/* 解决方案:使用更具体的选择器和 !important */
.vue3-extension-container {
all: initial !important; /* 重置所有样式 */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
}
.vue3-extension-container * {
box-sizing: border-box !important;
}
/* 使用 CSS 自定义属性避免冲突 */
:root {
--vue3-ext-primary-color: #007bff;
--vue3-ext-secondary-color: #6c757d;
}10.2 调试工具和技巧
- Chrome DevTools 扩展调试
// 在 content script 中添加调试信息 class DebugHelper { static log(message, data = null) { if (process.env.NODE_ENV === 'development') { console.group(🔧 [Vue3 Extension Debug] ${message}) if (data) { console.log('数据:', data) } console.trace('调用栈') console.groupEnd() } }
static performance(label, fn) { const start = performance.now() const result = fn() const end = performance.now()
this.log(`性能测试: ${label}`, {
duration: `${(end - start).toFixed(2)}ms`,
result
})
return result
} }
// 使用示例 DebugHelper.log('开始高亮链接') const result = DebugHelper.performance('链接高亮', () => { return linkManager.highlightAllLinks() }) DebugHelper.log('高亮完成', result) 一键获取完整项目代码 javascript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 2. 错误监控和上报
// 全局错误监控 class ErrorMonitor { constructor() { this.errors = [] this.setupErrorHandlers() }
setupErrorHandlers() { // 监听未捕获的错误 window.addEventListener('error', (event) => { this.recordError({ type: 'javascript', message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, stack: event.error?.stack }) })
// 监听 Promise 拒绝
window.addEventListener('unhandledrejection', (event) => {
this.recordError({
type: 'promise',
message: event.reason?.message || 'Promise rejected',
stack: event.reason?.stack
})
})
}
recordError(error) { const errorRecord = { ...error, timestamp: Date.now(), url: window.location.href, userAgent: navigator.userAgent }
this.errors.push(errorRecord)
// 限制错误记录数量
if (this.errors.length > 100) {
this.errors = this.errors.slice(-50)
}
// 发送错误报告(可选)
this.reportError(errorRecord)
}
async reportError(error) { try { // 发送到错误收集服务 await fetch('https://your-error-service.com/api/errors', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(error) }) } catch (reportError) { console.warn('错误上报失败:', reportError) } }
getErrorReport() { return { totalErrors: this.errors.length, recentErrors: this.errors.slice(-10), errorTypes: this.groupErrorsByType() } }
groupErrorsByType() { const groups = {} this.errors.forEach(error => { const key = error.type || 'unknown' groups[key] = (groups[key] || 0) + 1 }) return groups } }
// 初始化错误监控 const errorMonitor = new ErrorMonitor() 一键获取完整项目代码 javascript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
———————————————— 版权声明:本文为CSDN博主「2401_86326952」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/2401_86326952/article/details/149606329