架构设计
本文档介绍 AI-SideChat 的整体架构设计和技术实现。
技术栈
核心技术
- 原生 JavaScript:无框架依赖,轻量高效
- Manifest V3:最新的浏览器扩展标准
- Vite 5.0:现代化构建工具
- @crxjs/vite-plugin:Chrome 扩展开发插件
浏览器 API
| API | 用途 |
|---|---|
chrome.storage.local | 本地数据存储 |
chrome.runtime | 消息传递、资源访问 |
chrome.tabs | 标签页管理和跳转 |
chrome.sidePanel | 侧边栏面板(可选) |
Web API
| API | 用途 |
|---|---|
MutationObserver | DOM 变化监听 |
IntersectionObserver | 元素可见性检测 |
sessionStorage | 跨页状态保持 |
Shadow DOM | 样式隔离 |
架构概览
┌─────────────────────────────────────────┐
│ Content Scripts │
│ ┌────────────┐ ┌──────────────┐ │
│ │ content.js │─────▶│ drawer.js │ │
│ └────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────────┐ │
│ │ chrome.storage.local │ │
│ └────────────────────────────────┘ │
│ ▲ │
└──────────────────┼──────────────────────┘
│
┌─────────┴──────────┐
│ │
┌────────▼────────┐ ┌───────▼────────┐
│ background.js │ │ sidepanel.js │
│ (Service │ │ (Optional) │
│ Worker) │ │ │
└─────────────────┘ └────────────────┘模块设计
content.js - 内容脚本
职责:
- 监听页面 DOM 变化
- 注入收藏按钮
- 处理收藏/取消收藏操作
- 执行跳转定位逻辑
- 高亮显示目标元素
核心函数:
javascript
// 初始化
function init() {
detectPlatform()
observeDOM()
syncFavorites()
checkPendingJump()
}
// DOM 监听
function observeDOM() {
const observer = new MutationObserver(() => {
addCollectButtons()
})
observer.observe(document.body, {
childList: true,
subtree: true
})
}
// 收藏操作
function collectItem(bubble) {
const data = extractData(bubble)
saveFavorite(data)
updateButton(bubble, true)
}
// 跳转定位
function performJump(questionHash, bubbleIndex) {
const strategies = [
strategyA_IndexWithHash,
strategyB_HashSearch,
strategyC_IndexFallback
]
for (const strategy of strategies) {
if (strategy(questionHash, bubbleIndex)) {
return true
}
}
}drawer.js - 悬浮坞
职责:
- 渲染悬浮坞入口按钮
- 显示/隐藏抽屉面板
- 渲染收藏列表
- 处理搜索和筛选
- 显示预览窗口
核心函数:
javascript
// 创建悬浮坞
function createDrawer() {
const container = document.createElement('div')
container.attachShadow({ mode: 'open' })
injectStyles()
renderDrawerButton()
renderDrawerPanel()
}
// 渲染列表
function renderList(items) {
const list = items.map(item => {
return createItemElement(item)
})
listContainer.innerHTML = ''
list.forEach(el => listContainer.appendChild(el))
}
// 搜索筛选
function filterItems(keyword, tags, site) {
return allItems.filter(item => {
const matchKeyword = item.question.includes(keyword) ||
item.answer.includes(keyword)
const matchTags = tags.every(tag => item.tags.includes(tag))
const matchSite = !site || item.site === site
return matchKeyword && matchTags && matchSite
})
}
// 显示预览
function showPreview(item, position) {
const preview = createPreviewElement(item)
positionPreview(preview, position)
document.body.appendChild(preview)
}background.js - 后台脚本
职责:
- 处理跨标签页消息
- 管理侧边栏状态
- 执行标签页操作
核心函数:
javascript
// 消息监听
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
switch (message.type) {
case 'OPEN_TAB':
openTab(message.url)
break
case 'JUMP_TO_CONVERSATION':
jumpToConversation(message.data)
break
}
})
// 标签页跳转
async function jumpToConversation(data) {
const tab = await findOrCreateTab(data.url)
chrome.tabs.update(tab.id, { active: true })
// 通知 content script 执行定位
chrome.tabs.sendMessage(tab.id, {
type: 'PERFORM_JUMP',
data: data
})
}sidepanel.js - 侧边栏(可选)
职责:
- 提供独立的侧边栏界面
- 与抽屉面板功能相同
- 更好的多任务体验
数据流
收藏流程
用户点击收藏按钮
↓
content.js 提取数据
↓
chrome.storage.local 保存
↓
触发 onChanged 事件
↓
所有 content.js 实例更新 UI
↓
drawer.js 刷新列表跳转流程
用户点击收藏条目
↓
drawer.js 发送消息
↓
background.js 接收
↓
打开/切换到目标标签页
↓
content.js 接收跳转指令
↓
执行定位策略
↓
高亮显示目标存储设计
数据结构
javascript
{
"aiClipData": [
{
id: number, // 唯一 ID
questionHash: string, // 问题哈希
bubbleIndex: number, // 索引位置
contentHash: string, // 回答哈希
site: string, // 平台名称
siteColor: string, // 主题色
convTitle: string, // 对话标题
question: string, // 问题文本
answer: string, // 回答文本
answerHtml: string, // 回答 HTML
url: string, // 对话 URL
tags: string[], // 标签数组
timestamp: number // 时间戳
}
]
}存储操作
javascript
// 读取
async function getFavorites() {
const result = await chrome.storage.local.get('aiClipData')
return result.aiClipData || []
}
// 保存
async function saveFavorite(data) {
const favorites = await getFavorites()
favorites.push(data)
await chrome.storage.local.set({ aiClipData: favorites })
}
// 删除
async function removeFavorite(id) {
const favorites = await getFavorites()
const filtered = favorites.filter(item => item.id !== id)
await chrome.storage.local.set({ aiClipData: filtered })
}
// 更新
async function updateFavorite(id, updates) {
const favorites = await getFavorites()
const index = favorites.findIndex(item => item.id === id)
if (index !== -1) {
favorites[index] = { ...favorites[index], ...updates }
await chrome.storage.local.set({ aiClipData: favorites })
}
}性能优化
1. 防抖和节流
javascript
// 搜索防抖
function debounce(func, wait) {
let timeout
return function(...args) {
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(this, args), wait)
}
}
// DOM 监听节流
function throttle(func, limit) {
let inThrottle
return function(...args) {
if (!inThrottle) {
func.apply(this, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}2. 事件委托
javascript
// 不要为每个按钮单独绑定事件
// ❌ 不好的做法
buttons.forEach(btn => {
btn.addEventListener('click', handler)
})
// ✅ 使用事件委托
document.addEventListener('click', (e) => {
if (e.target.matches('.collect-btn')) {
handleCollect(e.target)
}
})3. 虚拟滚动
javascript
// 长列表优化(规划中)
class VirtualList {
constructor(items, itemHeight) {
this.items = items
this.itemHeight = itemHeight
this.visibleCount = Math.ceil(window.innerHeight / itemHeight)
}
getVisibleItems(scrollTop) {
const startIndex = Math.floor(scrollTop / this.itemHeight)
return this.items.slice(startIndex, startIndex + this.visibleCount)
}
}安全性
XSS 防护
javascript
// 清理用户输入
function sanitizeHTML(html) {
const temp = document.createElement('div')
temp.textContent = html
return temp.innerHTML
}
// 使用 textContent 而非 innerHTML
element.textContent = userInputCSP 兼容
manifest.json 中的 CSP 配置:
json
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}权限最小化
只请求必需的权限:
json
{
"permissions": ["storage", "tabs"],
"host_permissions": [
"https://gemini.google.com/*",
"https://chatgpt.com/*"
]
}错误处理
全局错误捕获
javascript
window.addEventListener('error', (e) => {
console.error('[AI-SideChat Error]', e.error)
// 可选:上报错误
})
window.addEventListener('unhandledrejection', (e) => {
console.error('[AI-SideChat Promise Rejection]', e.reason)
})优雅降级
javascript
async function safeOperation() {
try {
await riskyOperation()
} catch (error) {
console.error('Operation failed:', error)
// 降级方案
fallbackOperation()
}
}测试策略
单元测试
javascript
// 使用 Jest 测试工具函数
describe('simpleHash', () => {
test('should return same hash for same input', () => {
const input = 'test string'
expect(simpleHash(input)).toBe(simpleHash(input))
})
test('should return different hash for different input', () => {
expect(simpleHash('a')).not.toBe(simpleHash('b'))
})
})集成测试
手动测试清单:
- [ ] 收藏按钮正确显示
- [ ] 收藏/取消收藏功能正常
- [ ] 跳转定位准确
- [ ] 预览窗口显示正确
- [ ] 标签编辑保存成功
- [ ] 搜索筛选结果正确
构建配置
vite.config.js
javascript
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
import manifest from './manifest.json'
export default defineConfig({
plugins: [crx({ manifest })],
build: {
rollupOptions: {
input: {
popup: 'popup.html',
sidepanel: 'sidepanel.html'
}
}
}
})