Some checks failed
Tests / Tests (push) Has been cancelled
ESLint Check / ESLint Check and Report Upload (push) Has been cancelled
Prettier Check / Format Check (push) Has been cancelled
Prettier Check / Format Code (push) Has been cancelled
ESLint Check / Build Base for Bundle Size Comparison (push) Has been cancelled
- Add export functionality to SessionList and Player pages - Add new utility modules: dataOperations, format, path, settings - Update manifest with export and download permissions - Enhance storage utility with new data operations - Add various test scripts and documentation files
19 KiB
19 KiB
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
// 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
// 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
// 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
// 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 `
<!DOCTYPE html>
<html>
<head>
<title>rrweb 回放</title>
<script src="https://unpkg.com/rrweb@latest/dist/rrweb.umd.cjs"></script>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
#replayer { width: 100%; height: 600px; border: 1px solid #ccc; }
</style>
</head>
<body>
<h1>rrweb 录制回放</h1>
<div id="replayer"></div>
<script>
const replayer = new rrweb.Replayer(${JSON.stringify(events)});
replayer.play();
</script>
</body>
</html>`;
}
}
3. UI 组件设计
Popup Main Component
<!-- popup/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>rrweb 录制插件</title>
<script src="popup.js" type="module"></script>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="app">
<!-- 状态栏 -->
<div class="status-bar" id="statusBar">
<div class="status-indicator" id="statusIndicator"></div>
<span id="statusText">停止录制</span>
<span id="statsText"></span>
</div>
<!-- 预览区域 -->
<div class="preview-section" id="previewSection">
<div class="preview-container">
<iframe id="previewFrame" sandbox="allow-same-origin"></iframe>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button id="recordBtn" class="btn btn-primary">
<span class="btn-icon">🎬</span>
<span class="btn-text">开始录制</span>
</button>
<button id="playBtn" class="btn btn-secondary" disabled>
<span class="btn-icon">▶</span>
<span class="btn-text">播放</span>
</button>
<button id="exportBtn" class="btn btn-secondary" disabled>
<span class="btn-icon">📤</span>
<span class="btn-text">导出</span>
</button>
<button id="deleteBtn" class="btn btn-danger" disabled>
<span class="btn-icon">🗑️</span>
<span class="btn-text">删除</span>
</button>
</div>
<!-- 底部导航 -->
<div class="nav-bar">
<button class="nav-item active" data-page="main">
<span class="nav-icon">📹</span>
<span class="nav-text">主页</span>
</button>
<button class="nav-item" data-page="history">
<span class="nav-icon">📄</span>
<span class="nav-text">历史</span>
</button>
<button class="nav-item" data-page="settings">
<span class="nav-icon">⚙️</span>
<span class="nav-text">设置</span>
</button>
<button class="nav-item" data-page="help">
<span class="nav-icon">❓</span>
<span class="nav-text">帮助</span>
</button>
</div>
</div>
</body>
</html>
CSS 样式
/* popup.css */
@import 'https://cdn.tailwindcss.com';
:root {
--primary-color: #3B82F6;
--success-color: #10B981;
--danger-color: #EF4444;
}
.app {
width: 400px;
height: 600px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.status-bar {
display: flex;
align-items: center;
padding: 12px;
background: #F3F4F6;
border-bottom: 1px solid #E5E7EB;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
background: #9CA3AF;
}
.status-indicator.recording {
background: #EF4444;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
border: none;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-primary:hover {
background: #2563EB;
transform: translateY(-1px);
}
.btn-secondary {
background: #6B7280;
color: white;
}
.btn-secondary:hover {
background: #4B5563;
}
.btn-danger {
background: var(--danger-color);
color: white;
}
.btn-danger:hover {
background: #DC2626;
}
.nav-bar {
display: flex;
border-top: 1px solid #E5E7EB;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px;
background: white;
border: none;
cursor: pointer;
transition: background 0.2s;
}
.nav-item.active {
background: #F9FAFB;
color: var(--primary-color);
}
.nav-item:hover {
background: #F3F4F6;
}
📊 开发时间估算
| 阶段 | 主要任务 | 预估时间 | 优先级 |
|---|---|---|---|
| 阶段一 | 基础录制功能 | 1-2周 | 🏆 高 |
| 录制核心逻辑 | 3天 | ||
| 基础 UI | 2天 | ||
| 快捷键 | 1天 | ||
| 文件保存 | 2天 | ||
| 阶段二 | UI 增强和回放 | 2-3周 | 🥈 中 |
| UI 设计优化 | 4天 | ||
| 本地回放 | 5天 | ||
| 状态管理 | 3天 | ||
| 设置页面 | 3天 | ||
| 阶段三 | 数据管理和导出 | 1-2周 | 🥉 中 |
| 自定义路径 | 3天 | ||
| 多格式导出 | 4天 | ||
| 历史管理 | 3天 | ||
| 数据清理 | 2天 |
🎯 功能特性清单
基础录制功能
- 开始/停止录制
- 实时状态显示
- 事件计数
- 录制时长
- 快捷键支持
UI 增强功能
- 现代化界面设计
- 实时预览窗口
- 录制控制按钮
- 状态指示器
- 响应式布局
回放功能
- 本地回放
- 播放/暂停控制
- 进度条
- 速度调节
数据管理
- 自定义保存路径
- 文件命名规则
- 录制历史
- 搜索功能
- 批量操作
导出功能
- JSON 格式导出
- HTML 回放页面
- ZIP 压缩包
- 批量导出
- 云端同步(后续版本)
隐私保护
- 敏感信息遮罩
- 自定义屏蔽规则
- 网站白名单
- 数据加密(后续版本)
🛠️ 技术栈
| 组件 | 技术选择 | 理由 |
|---|---|---|
| UI Framework | Tailwind CSS + Alpine.js | 快速开发,美观 |
| State Management | Zustand/Redux | 轻量级状态管理 |
| File System | File System API | 原生文件访问 |
| Storage | IndexedDB + Chrome Storage | 大数据存储 |
| Icons | Lucide Icons | 现代化图标库 |
| Charts | Chart.js | 数据可视化 |
🚀 实施计划
第一步:评估现有扩展
- 测试当前 rrweb web-extension 功能
- 确认录制质量是否满足需求
- 分析需要改进的地方
第二步:阶段一实施
- 实现基础录制功能
- 创建简单的 popup UI
- 添加文件保存功能
- 实现快捷键支持
第三步:阶段二实施
- UI 设计优化
- 实现本地回放
- 添加设置页面
- 完善状态管理
第四步:阶段三实施
- 实现自定义保存路径
- 开发多格式导出
- 创建历史管理界面
- 添加数据清理功能
📝 注意事项
-
浏览器兼容性
- 支持 Chrome 88+
- 支持 Firefox 75+
- 避免使用不兼容的 API
-
性能考虑
- 大文件处理优化
- 内存管理
- 录制性能监控
-
用户隐私
- 明确的录制提示
- 敏感信息保护
- 数据安全存储
-
用户体验
- 流畅的界面交互
- 清晰的状态反馈
- 易用的操作流程
🎉 总结
本计划提供了 rrweb 浏览器插件开发的完整路线图,从基础录制功能到高级的数据管理和导出功能。通过分阶段实施,可以确保每个阶段都有明确的交付成果,同时保持开发的灵活性。
主要特点:
- ✅ 功能完整,覆盖录制、回放、管理、导出
- ✅ 现代化 UI 设计,用户体验良好
- ✅ 技术方案成熟,可扩展性强
- ✅ 分阶段实施,风险可控
预期成果: 一个功能完善、界面美观、易于使用的浏览器录制插件。