# rrweb 浏览器插件开发计划 ## 📋 项目概述 基于 rrweb 开发一个功能完善的浏览器插件,支持录制用户操作、本地回放、多格式导出等功能。 --- ## 🎯 开发阶段规划 ### 阶段一:基础录制功能(1-2周) #### 核心功能清单 - ✅ **录制开始/停止** - ✅ **基本数据收集**(事件、时间戳) - ✅ **简单 popup UI**(状态显示、控制按钮) - ✅ **文件保存到默认路径**(自动 JSON 导出) - ✅ **快捷键支持**(Ctrl+Shift+R 开始/停止) #### UI 设计(简化版) ``` ┌─────────────────────────────────────┐ │ 📹 rrweb 录制插件 │ ├─────────────────────────────────────┤ │ 状态: [❌] 停止录制 │ │ │ │ [🎬 开始录制] [⚙️ 设置] │ │ │ │ 快捷键: Ctrl+Shift+R │ │ 帮助 📄 │ └─────────────────────────────────────┘ ``` #### 技术实现 - Background Service 管理录制状态 - Content Script 注入录制脚本 - 基础的数据收集和存储 - 简单的 popup 界面 --- ### 阶段二:UI 增强和回放(2-3周) #### 功能增强 - 🎨 **现代化 UI 设计** - 使用 Tailwind CSS - 添加图标和动画 - 响应式布局 - 🎬 **本地回放功能** - 在 popup 中预览录制 - 基本播放控制(播放/暂停/进度条) - 📊 **录制状态显示** - 实时事件计数 - 录制时长显示 - 文件大小显示 - 🔧 **设置页面** - 基础配置选项 - 隐私控制设置 #### UI 设计(增强版) ``` ┌─────────────────────────────────────┐ │ 📹 rrweb 录制插件 │ ├─────────────────────────────────────┤ │ ● 正在录制 [00:15:23] │ │ 事件数: 1,234 | 文件: 2.3MB │ │ │ │ ┌─────────────────────────────┐ │ │ │ [🎬 实时预览] │ │ │ │ 👆 点击、输入、滚动等操作 │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ [▶ 播放录制] [⏸ 暂停] │ │ │ │ [📤 导出] [🗑️ 删除] │ │ │ └─────────────────────────────┘ │ │ │ │ 设置 ⚙️ 历史 📄 快捷键 ⌨️ │ └─────────────────────────────────────┘ ``` #### 技术实现 - 现代化 UI 组件设计 - 录制数据实时预览 - 基础回放功能实现 - 设置页面配置管理 --- ### 阶段三:数据管理和导出(1-2周) #### 核心功能 - 💾 **自定义保存路径** - 用户选择文件夹 - 支持自定义文件名格式 - 路径持久化存储 - 📤 **多格式导出** - JSON 原始数据 - HTML 回放页面 - ZIP 压缩包 - 📄 **录制历史管理** - 录制列表显示 - 详情预览 - 批量操作 - 🗑️ **数据清理** - 手动删除 - 自动清理选项 #### 历史管理界面 ``` ┌─────────────────────────────────────┐ 📄 录制历史 │ ├─────────────────────────────────────┤ │ 🔍 搜索: [_________________] │ │ │ │ ┌─────────────────────────────┐ │ │ │ 🎬 product-demo.web.app │ │ │ │ ● 02:35 | 342事件 | 1.2MB │ │ │ │ 2024-01-15 14:30 │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ [▶ 播放] [📤 导出] [🗑️] │ │ │ │ │ └─────────────────────────┘ │ │ │ │ │ │ │ │ 🎬 login-test.com │ │ │ │ ● 01:15 | 156事件 | 0.8MB │ │ │ │ 2024-01-15 10:20 │ │ │ └─────────────────────────────┘ │ │ │ │ [←] [1/5] [→] 清空全部 🗑️ │ └─────────────────────────────────────┘ ``` #### 技术实现 - 文件路径管理系统 - 多格式导出工具 - 录制历史数据管理 - 用户配置持久化 --- ## 🛠️ 技术架构设计 ### 1. 项目结构 ``` web-extension/ ├── manifest.json # 扩展配置 ├── popup/ # 弹出窗口 │ ├── index.html # popup HTML │ ├── popup.css # 样式文件 │ ├── popup.js # popup 脚本 │ └── components/ # UI 组件 │ ├── StatusIndicator.js │ ├── RecordingPreview.js │ └── ControlButtons.js ├── options/ # 设置页面 │ ├── index.html │ ├── options.css │ ├── options.js │ └── settings.html ├── background/ # 后台服务 │ ├── service-worker.js │ └── recording-manager.js ├── content/ # 内容脚本 │ ├── inject.js # 注入脚本 │ └── enhanced-recorder.js # 增强录制器 ├── utils/ # 工具函数 │ ├── file-manager.js │ ├── export-manager.js │ └── shortcut-manager.js ├── assets/ # 资源文件 │ ├── icons/ │ └── styles/ └── lib/ # 第三方库 └── rrweb-dist/ ``` ### 2. 核心模块实现 #### Background Service Worker ```javascript // background/service-worker.js class RecordingManager { constructor() { this.recordings = new Map(); this.currentRecording = null; } async startRecording(tabId) { try { const recorder = new EnhancedRecorder({ tabId, onData: (events) => this.saveEvents(tabId, events), options: await this.getRecordingOptions() }); this.currentRecording = { tabId, recorder, startTime: Date.now(), events: [], status: 'recording' }; await this.saveToStorage(); // 更新 popup 状态 chrome.runtime.sendMessage({ type: 'RECORDING_STARTED', data: { tabId } }); } catch (error) { console.error('Start recording failed:', error); } } async stopRecording(tabId) { if (!this.currentRecording) return; const recording = this.currentRecording; recording.recorder.stop(); recording.status = 'stopped'; recording.endTime = Date.now(); // 保存文件 await this.saveRecordingFile(recording); this.currentRecording = null; await this.saveToStorage(); chrome.runtime.sendMessage({ type: 'RECORDING_STOPPED', data: { tabId } }); } } ``` #### Enhanced Recorder ```javascript // content/enhanced-recorder.js class EnhancedRecorder { constructor({ tabId, onData, options }) { this.tabId = tabId; this.onData = onData; this.options = options; this.events = []; this.recorder = null; this.init(); } init() { this.recorder = record({ emit: (event) => { this.events.push(event); this.onData(this.events); }, recordCrossOriginIframes: true, blockClass: 'rr-block', maskTextClass: 'rr-mask', ...this.options }); } stop() { if (this.recorder) { this.recorder(); this.recorder = null; } } } ``` #### File Manager ```javascript // utils/file-manager.js class FileManager { constructor() { this.settings = this.loadSettings(); } async saveRecording(data) { const filename = this.generateFilename(); const filepath = await this.getFilePath(filename); // 转换为 JSON const json = JSON.stringify({ version: '1.0', timestamp: data.timestamp, events: data.events, metadata: { duration: data.duration, eventCount: data.events.length, url: data.url } }, null, 2); // 保存文件 await this.writeFile(filepath, json); return { filename, filepath, size: json.length }; } generateFilename() { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); return `rrweb-${timestamp}.json`; } async getFilePath(filename) { const savePath = this.settings.savePath || this.getDefaultPath(); const fullPath = `${savePath}/${filename}`; // 确保目录存在 await this.ensureDirectory(savePath); return fullPath; } } ``` #### Export Manager ```javascript // utils/export-manager.js class ExportManager { async exportJSON(events, filename) { const data = this.prepareEventData(events); const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); return this.downloadFile(blob, `${filename}.json`); } async exportHTML(events, filename) { const html = this.generateReplayHTML(events); const blob = new Blob([html], { type: 'text/html' }); return this.downloadFile(blob, `${filename}.html`); } async exportZIP(events, filename) { const JSZip = await import('jszip'); const zip = new JSZip(); // 添加 JSON 数据 const data = this.prepareEventData(events); zip.file('recording.json', JSON.stringify(data, null, 2)); // 添加回放 HTML zip.file('replay.html', this.generateReplayHTML(events)); // 添加 README zip.file('README.md', this.generateReadme(events)); const content = await zip.generateAsync({ type: 'blob' }); return this.downloadFile(content, `${filename}.zip`); } generateReplayHTML(events) { return `