feat(app): add rrweb recording and replay functionality
- Integrate rrweb library for session recording capabilities - Add RecordeReplayPage component with start/stop recording buttons - Implement useRecorder hook to manage recording state and events - Create bridge service for extension communication - Add storage service with Chrome extension and web compatibility - Implement downloadHtml utility for replay file generation - Update manifest permissions for activeTab, scripting, and tabs - Add Chrome extension type definitions as development dependencies
This commit is contained in:
Generated
+173
-6
@@ -8,6 +8,7 @@
|
||||
"name": "chrome-extension-router-demo",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@rrweb/all": "^2.0.0-alpha.18",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
@@ -21,7 +22,14 @@
|
||||
"react-router-dom": "^6.30.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"remark-gfm": "^1.0.0",
|
||||
"rrweb": "^2.0.0-alpha.4",
|
||||
"rrweb-player": "^1.0.0-alpha.4",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.1.36",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/react-dom": "^19.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
@@ -3066,6 +3074,39 @@
|
||||
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rrweb/all": {
|
||||
"version": "2.0.0-alpha.18",
|
||||
"resolved": "https://registry.npmmirror.com/@rrweb/all/-/all-2.0.0-alpha.18.tgz",
|
||||
"integrity": "sha512-QJ1WxHfAdkDadu79/zrZt+yzco6ckPOXEVRMW/RIv+8BIFh3q/j89y9rPbg5BpmH662V5J0GB99f7BKIM+RMJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rrweb/packer": "^2.0.0-alpha.18",
|
||||
"@rrweb/types": "^2.0.0-alpha.18",
|
||||
"rrweb": "^2.0.0-alpha.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@rrweb/packer": {
|
||||
"version": "2.0.0-alpha.18",
|
||||
"resolved": "https://registry.npmmirror.com/@rrweb/packer/-/packer-2.0.0-alpha.18.tgz",
|
||||
"integrity": "sha512-rEXltE/gnflEv/NatVNPIp4/6EtVlPLSwzIbd4WPSMtQGvwZe5OfAAk/Y88HkbAwb1nHfdA3PtFGn2Epik4inQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rrweb/types": "^2.0.0-alpha.18",
|
||||
"fflate": "^0.4.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@rrweb/types": {
|
||||
"version": "2.0.0-alpha.18",
|
||||
"resolved": "https://registry.npmmirror.com/@rrweb/types/-/types-2.0.0-alpha.18.tgz",
|
||||
"integrity": "sha512-iMH3amHthJZ9x3gGmBPmdfim7wLGygC2GciIkw2A6SO8giSn8PHYtRT8OKNH4V+k3SZ6RSnYHcTQxBA7pSWZ3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rrweb/utils": {
|
||||
"version": "2.0.0-alpha.18",
|
||||
"resolved": "https://registry.npmmirror.com/@rrweb/utils/-/utils-2.0.0-alpha.18.tgz",
|
||||
"integrity": "sha512-qV8azQYo9RuwW4NGRtOiQfTBdHNL1B0Q//uRLMbCSjbaKqJYd88Js17Bdskj65a0Vgp2dwTLPIZ0gK47dfjfaA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
@@ -3449,6 +3490,12 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/svelte": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmmirror.com/@tsconfig/svelte/-/svelte-1.0.13.tgz",
|
||||
"integrity": "sha512-5lYJP45Xllo4yE/RUBccBT32eBlRDbqN8r1/MIvQbKxW3aFqaYPCNgm8D5V20X4ShHcwvYWNlKg3liDh1MlBoA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/aria-query": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
@@ -3515,6 +3562,17 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chrome": {
|
||||
"version": "0.1.36",
|
||||
"resolved": "https://registry.npmmirror.com/@types/chrome/-/chrome-0.1.36.tgz",
|
||||
"integrity": "sha512-BvHbuyGttYXnGt5Gpwa4769KIinKHY1iLjlAPrrMBS2GI9m/XNMPtdsq0NgQalyuUdxvlMN/0OyGw0shFVIoUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/filesystem": "*",
|
||||
"@types/har-format": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz",
|
||||
@@ -3534,6 +3592,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/css-font-loading-module": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz",
|
||||
"integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.56.12",
|
||||
"resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-8.56.12.tgz",
|
||||
@@ -3596,6 +3660,23 @@
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/filesystem": {
|
||||
"version": "0.0.36",
|
||||
"resolved": "https://registry.npmmirror.com/@types/filesystem/-/filesystem-0.0.36.tgz",
|
||||
"integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/filewriter": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/filewriter": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmmirror.com/@types/filewriter/-/filewriter-0.0.33.tgz",
|
||||
"integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmmirror.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||
@@ -3605,6 +3686,13 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/har-format": {
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmmirror.com/@types/har-format/-/har-format-1.2.16.tgz",
|
||||
"integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "2.3.10",
|
||||
"resolved": "https://registry.npmmirror.com/@types/hast/-/hast-2.3.10.tgz",
|
||||
@@ -3735,15 +3823,24 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"version": "19.2.9",
|
||||
"resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.9.tgz",
|
||||
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.17.1.tgz",
|
||||
@@ -4236,6 +4333,12 @@
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@xstate/fsm": {
|
||||
"version": "1.6.5",
|
||||
"resolved": "https://registry.npmmirror.com/@xstate/fsm/-/fsm-1.6.5.tgz",
|
||||
"integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||
@@ -5127,6 +5230,15 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.7",
|
||||
"resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz",
|
||||
@@ -6390,8 +6502,7 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
@@ -8039,6 +8150,12 @@
|
||||
"bser": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.4.8",
|
||||
"resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.4.8.tgz",
|
||||
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
@@ -11842,6 +11959,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
@@ -14844,6 +14967,50 @@
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rrdom": {
|
||||
"version": "2.0.0-alpha.18",
|
||||
"resolved": "https://registry.npmmirror.com/rrdom/-/rrdom-2.0.0-alpha.18.tgz",
|
||||
"integrity": "sha512-fSFzFFxbqAViITyYVA4Z0o5G6p1nEqEr/N8vdgSKie9Rn0FJxDSNJgjV0yiCIzcDs0QR+hpvgFhpbdZ6JIr5Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rrweb-snapshot": "^2.0.0-alpha.18"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb": {
|
||||
"version": "2.0.0-alpha.18",
|
||||
"resolved": "https://registry.npmmirror.com/rrweb/-/rrweb-2.0.0-alpha.18.tgz",
|
||||
"integrity": "sha512-1mjZcB+LVoGSx1+i9E2ZdAP90fS3MghYVix2wvGlZvrgRuLCbTCCOZMztFCkKpgp7/EeCdYM4nIHJkKX5J1Nmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rrweb/types": "^2.0.0-alpha.18",
|
||||
"@rrweb/utils": "^2.0.0-alpha.18",
|
||||
"@types/css-font-loading-module": "0.0.7",
|
||||
"@xstate/fsm": "^1.4.0",
|
||||
"base64-arraybuffer": "^1.0.1",
|
||||
"mitt": "^3.0.0",
|
||||
"rrdom": "^2.0.0-alpha.18",
|
||||
"rrweb-snapshot": "^2.0.0-alpha.18"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb-player": {
|
||||
"version": "1.0.0-alpha.4",
|
||||
"resolved": "https://registry.npmmirror.com/rrweb-player/-/rrweb-player-1.0.0-alpha.4.tgz",
|
||||
"integrity": "sha512-Wlmn9GZ5Fdqa37vd3TzsYdLl/JWEvXNUrLCrYpnOwEgmY409HwVIvvA5aIo7k582LoKgdRCsB87N+f0oWAR0Kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tsconfig/svelte": "^1.0.0",
|
||||
"rrweb": "^2.0.0-alpha.4"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb-snapshot": {
|
||||
"version": "2.0.0-alpha.18",
|
||||
"resolved": "https://registry.npmmirror.com/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.18.tgz",
|
||||
"integrity": "sha512-hBHZL/NfgQX6wO1D9mpwqFu1NJPpim+moIcKhFEjVTZVRUfCln+LOugRc4teVTCISYHN8Cw5e2iNTWCSm+SkoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss": "^8.4.38"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@rrweb/all": "^2.0.0-alpha.18",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
@@ -16,6 +17,8 @@
|
||||
"react-router-dom": "^6.30.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"remark-gfm": "^1.0.0",
|
||||
"rrweb": "^2.0.0-alpha.4",
|
||||
"rrweb-player": "^1.0.0-alpha.4",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -41,5 +44,10 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.1.36",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/react-dom": "^19.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,11 +1,14 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "React Router Chrome Extension",
|
||||
"name": "Testing Tools",
|
||||
"version": "1.0",
|
||||
"description": "A Chrome Extension with React Router: User List + Actions Pages",
|
||||
"description": "测试工具",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"clipboardWrite"
|
||||
"clipboardWrite",
|
||||
"activeTab",
|
||||
"scripting",
|
||||
"tabs"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
|
||||
@@ -2,6 +2,7 @@ import {HashRouter as Router, Routes, Route, NavLink} from 'react-router-dom';
|
||||
import {useState, useEffect} from 'react';
|
||||
import TimestampPage from './pages/TimestampPage';
|
||||
// import ElectronicWoodenFishPage from "./pages/ElectronicWoodenFishPage";
|
||||
import RecordeReplayPage from './pages/RecordeReplayPage';
|
||||
import './App.css';
|
||||
|
||||
// 导航项配置数组,便于后续添加
|
||||
@@ -9,6 +10,7 @@ import './App.css';
|
||||
const navItems = [
|
||||
{path: '/', label: '时间戳', element: <TimestampPage/>},
|
||||
// {path: '/dzmy', label: '电子木鱼', element: <ElectronicWoodenFishPage/>},
|
||||
{path: '/recorde-replay', label: '录制与回放', element: <RecordeReplayPage/>},
|
||||
];
|
||||
|
||||
function App() {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import {useRef, useState} from "react";
|
||||
import {record} from "rrweb";
|
||||
|
||||
export const useRecorder = () => {
|
||||
// 存储录制事件
|
||||
const eventRef = useRef([]);
|
||||
// 存储停止录制函数
|
||||
const stopFnRef = useRef();
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
|
||||
const startRecord = async () => {
|
||||
if (isRecording) return;
|
||||
|
||||
setIsRecording(true);
|
||||
|
||||
// 启动录制
|
||||
stopFnRef.current = record({
|
||||
emit(event) {
|
||||
// 存储数据
|
||||
eventRef.current.push(event);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const stopRecord = () => {
|
||||
if (!isRecording) return;
|
||||
|
||||
setIsRecording(false);
|
||||
|
||||
// 停止录制
|
||||
if (stopFnRef.current) {
|
||||
stopFnRef.current();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
startRecord,
|
||||
stopRecord,
|
||||
isRecording,
|
||||
getEvents: () => eventRef.current
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
// import {sendRecordCommand} from '../services/bridge'
|
||||
// import {storage} from "../services/storage";
|
||||
import {useRecorder} from "../hooks/useRecorder";
|
||||
import {downloadHtml} from "../utils/recordUtils";
|
||||
|
||||
const RecordeReplayPage = () => {
|
||||
// const [isRecording, setIsRecording] = useState(false);
|
||||
|
||||
const {startRecord, stopRecord, getEvents, isRecording} = useRecorder();
|
||||
|
||||
const handleStart = async () => {
|
||||
try {
|
||||
// await sendRecordCommand("START_RECORD");
|
||||
if (!isRecording) {
|
||||
await startRecord();
|
||||
} else {
|
||||
stopRecord();
|
||||
}
|
||||
// await storage.set({isRecording: true, recordingStartTime: Date.now()});
|
||||
// console.log('events', events);
|
||||
} catch (e) {
|
||||
console.error("Error starting recording:", e);
|
||||
}
|
||||
}
|
||||
|
||||
const handleStop = async () => {
|
||||
try {
|
||||
// await sendRecordCommand("STOP_RECORD");
|
||||
// const events =useRecorder().getEvents();
|
||||
// console.log('events', events);
|
||||
// setIsRecording(false);
|
||||
// await storage.remove('isRecording');
|
||||
stopRecord();
|
||||
downloadHtml(getEvents());
|
||||
// console.log('events', getEvents());
|
||||
} catch (e) {
|
||||
console.error("Error stopping recording:", e)
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{width: "300px", padding: "16px"}}>
|
||||
<h3>RRWeb Recorder</h3>
|
||||
|
||||
{/* 3. 选择标签页逻辑:默认为当前页,如果需要跨页,需先列出 chrome.tabs.query */}
|
||||
|
||||
<div style={{display: "flex", gap: "10px", marginBottom: "20px"}}>
|
||||
{!isRecording ? (
|
||||
<button onClick={handleStart} className={"action-btn"}>
|
||||
开始录制
|
||||
</button>
|
||||
) : (
|
||||
<button className={"action-btn stop-btn"}
|
||||
onClick={handleStop}>停止录制</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecordeReplayPage;
|
||||
@@ -0,0 +1,56 @@
|
||||
// src/services/bridge.ts
|
||||
import {record} from 'rrweb';
|
||||
import {isExtension} from '../utils/env';
|
||||
|
||||
let stopFn = null;
|
||||
let events = [];
|
||||
|
||||
/**
|
||||
* 发送指令给录制器(Content Script 或 当前页面逻辑)
|
||||
* @param action 'START_RECORD' | 'STOP_RECORD'
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
export const sendRecordCommand = async (action) => {
|
||||
if (isExtension()) {
|
||||
// === 插件环境 ===
|
||||
try {
|
||||
const [tab] = await chrome.tabs.query({active: true, currentWindow: true});
|
||||
if (tab?.id) {
|
||||
// 使用 Promise 包装 sendMessage 以便统一 async/await 风格
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.sendMessage(tab.id, {action}, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(chrome.runtime.lastError);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Extension communication error:", e);
|
||||
}
|
||||
} else {
|
||||
// === 网页环境 (Web Demo / Localhost 调试) ===
|
||||
console.log(`[Web Mode] Simulation: Sending ${action}`);
|
||||
|
||||
if (action === 'START_RECORD') {
|
||||
stopFn = record({
|
||||
emit(event) {
|
||||
if (events.length > 1000) {
|
||||
console.log('events length > 1000');
|
||||
stopFn();
|
||||
}
|
||||
events.push(event);
|
||||
}
|
||||
});
|
||||
} else if (action === 'STOP_RECORD') {
|
||||
if (stopFn != null) {
|
||||
stopFn();
|
||||
}
|
||||
console.log('[Web Mode] Simulation: Stopping recorder');
|
||||
console.log('[Web Mode] Simulation: Events:', events);
|
||||
console.log('[Web Mode] Simulation: Events:', events.length);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
// src/services/storage.ts
|
||||
import {isExtension} from '../utils/env';
|
||||
|
||||
export const storage = {
|
||||
set: async (items) => {
|
||||
if (isExtension()) {
|
||||
return chrome.storage.local.set(items);
|
||||
} else {
|
||||
// Web 兼容:写入 localStorage
|
||||
Object.keys(items).forEach(key => {
|
||||
localStorage.setItem(key, JSON.stringify(items[key]));
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
|
||||
get: async (keys) => {
|
||||
if (isExtension()) {
|
||||
return chrome.storage.local.get(keys);
|
||||
} else {
|
||||
// Web 兼容:读取 localStorage
|
||||
const result = {};
|
||||
const keyList = Array.isArray(keys) ? keys : [keys];
|
||||
keyList.forEach(key => {
|
||||
const val = localStorage.getItem(key);
|
||||
if (val) result[key] = JSON.parse(val);
|
||||
});
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
},
|
||||
|
||||
remove: async (key) => {
|
||||
if (isExtension()) {
|
||||
return chrome.storage.local.remove(key);
|
||||
} else {
|
||||
localStorage.removeItem(key);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 判断当前是否处于 Chrome 插件环境
|
||||
* @returns {false|chrome.storage.LocalStorageArea}
|
||||
*/
|
||||
export const isExtension = () => {
|
||||
return typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// import rrwebPlayer from 'rrweb-player';
|
||||
|
||||
export const downloadHtml = (events) => {
|
||||
if (events.length === 0) return;
|
||||
|
||||
// 1. 构建 HTML 模板字符串
|
||||
// 我们将 events 数据直接注入到 <script> 标签中
|
||||
const htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RRWeb 录像回放</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/style.css" />
|
||||
</head>
|
||||
<body style="margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background: #f0f2f5;">
|
||||
|
||||
<div id="player"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/index.js"></script>
|
||||
|
||||
<script>
|
||||
/* 注入录制的数据 */
|
||||
const events = ${JSON.stringify(events)};
|
||||
|
||||
/* 初始化播放器 */
|
||||
new rrwebPlayer({
|
||||
target: document.getElementById('player'),
|
||||
props: {
|
||||
events: events,
|
||||
width: 1024, // 可以根据需要调整
|
||||
height: 576,
|
||||
autoPlay: true,
|
||||
showController: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// 2. 创建 Blob 并下载
|
||||
const blob = new Blob([htmlContent], {type: 'text/html'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `replay-${Date.now()}.html`; // 保存为 .html 文件
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
Reference in New Issue
Block a user