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
715 lines
19 KiB
Markdown
715 lines
19 KiB
Markdown
# 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 `
|
||
<!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
|
||
```html
|
||
<!-- 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 样式
|
||
```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 设计优化
|
||
- [ ] 实现本地回放
|
||
- [ ] 添加设置页面
|
||
- [ ] 完善状态管理
|
||
|
||
### 第四步:阶段三实施
|
||
- [ ] 实现自定义保存路径
|
||
- [ ] 开发多格式导出
|
||
- [ ] 创建历史管理界面
|
||
- [ ] 添加数据清理功能
|
||
|
||
---
|
||
|
||
## 📝 注意事项
|
||
|
||
1. **浏览器兼容性**
|
||
- 支持 Chrome 88+
|
||
- 支持 Firefox 75+
|
||
- 避免使用不兼容的 API
|
||
|
||
2. **性能考虑**
|
||
- 大文件处理优化
|
||
- 内存管理
|
||
- 录制性能监控
|
||
|
||
3. **用户隐私**
|
||
- 明确的录制提示
|
||
- 敏感信息保护
|
||
- 数据安全存储
|
||
|
||
4. **用户体验**
|
||
- 流畅的界面交互
|
||
- 清晰的状态反馈
|
||
- 易用的操作流程
|
||
|
||
---
|
||
|
||
## 🎉 总结
|
||
|
||
本计划提供了 rrweb 浏览器插件开发的完整路线图,从基础录制功能到高级的数据管理和导出功能。通过分阶段实施,可以确保每个阶段都有明确的交付成果,同时保持开发的灵活性。
|
||
|
||
**主要特点:**
|
||
- ✅ 功能完整,覆盖录制、回放、管理、导出
|
||
- ✅ 现代化 UI 设计,用户体验良好
|
||
- ✅ 技术方案成熟,可扩展性强
|
||
- ✅ 分阶段实施,风险可控
|
||
|
||
**预期成果:**
|
||
一个功能完善、界面美观、易于使用的浏览器录制插件。 |