From b065b22f097b56a9d88d852235d1b0699416212e Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] refactor the repl tool to support multipage apps and better dev --- README.md | 14 +++- package.json | 8 +- rollup.config.js | 35 +++++++-- scripts/repl.ts | 160 +++++++++++++++++++++++++++++++++++++++ test.d.ts | 6 +- test/integration.test.ts | 16 +--- test/repl.ts | 106 -------------------------- 7 files changed, 215 insertions(+), 130 deletions(-) create mode 100644 scripts/repl.ts delete mode 100644 test/repl.ts diff --git a/README.md b/README.md index 32ef324e..e394c1d4 100644 --- a/README.md +++ b/README.md @@ -70,4 +70,16 @@ Once you want to finish the recording, enter 'y' to start replay: 此时可以在页面中进行交互,待所需录制操作完成后,在命令行输入 y,测试工具就会将刚刚的操作进行回放,用于验证录制是否成功。 -_注意,当页面发生跳转时,rrweb 将停止录制,但仍可以回放跳转前的操作。_ +回放时命令行中将出现以下提示信息: + +``` +Enter 'y' to persistently store these recorded events: +``` + +此时可以再次在命令行中输入 y,测试工具将把已录制的内容存入一个静态 HTML 文件中,并提示存放位置: + +``` +Saved at PATH_TO_YOUR_REPO/temp/replay_2018_11_23T07_53_30.html +``` + +该文件默认使用最新 bundle 的 rrweb 代码,所以我们可以在修改代码后运行 `npm run bundle:browser`,之后刷新静态文件就可以查看最新代码对回放的影响并进行调试。 diff --git a/package.json b/package.json index 90915db7..1dc77585 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,10 @@ "version": "0.6.1", "description": "record and replay the web", "scripts": { - "test": "TS_NODE_CACHE=false TS_NODE_FILES=true mocha -r ts-node/register test/**/*.test.ts", - "repl": "TS_NODE_CACHE=false TS_NODE_FILES=true ts-node test/repl.ts", - "bundle": "rollup --config", - "bundle:watch": "rollup --config -w" + "test": "npm run bundle:browser && TS_NODE_CACHE=false TS_NODE_FILES=true mocha -r ts-node/register test/**/*.test.ts", + "repl": "npm run bundle:browser && TS_NODE_CACHE=false TS_NODE_FILES=true ts-node scripts/repl.ts", + "bundle:browser": "BROWSER_ONLY=true rollup --config", + "bundle": "rollup --config" }, "repository": { "type": "git", diff --git a/rollup.config.js b/rollup.config.js index ecda6dc0..2d4e504d 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -14,7 +14,7 @@ function toMinPath(path) { return path.replace(/\.js$/, '.min.js'); } -export default [ +let configs = [ // browser(record only) { input: './src/record/index.ts', @@ -22,7 +22,7 @@ export default [ output: [ { name: 'record', - format: 'umd', + format: 'iife', file: toRecordPath(pkg.unpkg), }, ], @@ -33,7 +33,7 @@ export default [ output: [ { name: 'record', - format: 'umd', + format: 'iife', file: toMinPath(toRecordPath(pkg.unpkg)), }, ], @@ -84,7 +84,7 @@ export default [ output: [ { name: 'rrweb', - format: 'umd', + format: 'iife', file: pkg.unpkg, }, ], @@ -104,7 +104,7 @@ export default [ output: [ { name: 'rrweb', - format: 'umd', + format: 'iife', file: toMinPath(pkg.unpkg), }, ], @@ -164,3 +164,28 @@ export default [ ], }, ]; + +if (process.env.BROWSER_ONLY) { + configs = { + input: './src/index.ts', + plugins: [ + typescript(), + resolve(), + postcss({ + extract: true, + minimize: true, + sourceMap: 'inline', + }), + terser(), + ], + output: [ + { + name: 'rrweb', + format: 'iife', + file: toMinPath(pkg.unpkg), + }, + ], + }; +} + +export default configs; diff --git a/scripts/repl.ts b/scripts/repl.ts new file mode 100644 index 00000000..9fb73c98 --- /dev/null +++ b/scripts/repl.ts @@ -0,0 +1,160 @@ +/* tslint:disable: no-console */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as EventEmitter from 'events'; +import * as readline from 'readline'; +import * as puppeteer from 'puppeteer'; +import { eventWithTime } from '../src/types'; + +const emitter = new EventEmitter(); + +function getCode(): string { + const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js'); + return fs.readFileSync(bundlePath, 'utf8'); +} + +(async () => { + const code = getCode(); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + let events: eventWithTime[] = []; + + rl.question( + 'Enter the url you want to record, e.g https://react-redux.realworld.io: ', + async url => { + console.log(`Going to open ${url}...`); + await record(url); + console.log('Ready to record. You can do any interaction on the page.'); + rl.question( + `Once you want to finish the recording, enter 'y' to start replay: `, + async answer => { + if (answer.toLowerCase() === 'y') { + emitter.emit('done'); + } + }, + ); + rl.write('y'); + }, + ); + + emitter.once('done', async () => { + rl.question( + `Enter 'y' to persistently store these recorded events: `, + async answer => { + if (answer.toLowerCase() === 'y') { + saveEvents(); + } + }, + ); + }); + + async function record(url: string) { + const browser = await puppeteer.launch({ + headless: false, + defaultViewport: { + width: 1600, + height: 900, + }, + args: ['--start-maximized'], + }); + const page = await browser.newPage(); + await page.goto(url, { + waitUntil: 'domcontentloaded', + }); + + await page.exposeFunction('_replLog', (event: eventWithTime) => { + events.push(event); + }); + await page.evaluate(`;${code} + window.__IS_RECORDING__ = true + rrweb.record({ + emit: event => window._replLog(event) + }); + `); + page.on('framenavigated', async () => { + const isRecording = await page.evaluate('window.__IS_RECORDING__'); + if (!isRecording) { + await page.evaluate(`;${code} + window.__IS_RECORDING__ = true + rrweb.record({ + emit: event => window._replLog(event) + }); + `); + } + }); + + emitter.once('done', async () => { + await browser.close(); + console.log(`Recorded ${events.length} events`); + replay(); + }); + } + + async function replay() { + const browser = await puppeteer.launch({ + headless: false, + defaultViewport: { + width: 1600, + height: 900, + }, + }); + const page = await browser.newPage(); + await page.goto('about:blank'); + await page.addStyleTag({ + path: path.resolve(__dirname, '../dist/rrweb.min.css'), + }); + await page.evaluate(`${code} + const events = ${JSON.stringify(events)}; + const replayer = new rrweb.Replayer(events); + replayer.play(); + `); + } + + const tempFolder = path.join(__dirname, '../temp'); + function saveEvents() { + if (!fs.existsSync(tempFolder)) { + fs.mkdirSync(tempFolder); + } + const time = new Date() + .toISOString() + .replace(/[-|:]/g, '_') + .replace(/\..+/, ''); + const fileName = `replay_${time}.html`; + const content = ` + + + + + + + Record @${time} + + + + + + + + `; + const savePath = path.resolve(tempFolder, fileName); + fs.writeFileSync(savePath, content); + console.log(`Saved at ${savePath}`); + } + + process + .on('uncaughtException', error => { + console.error(error); + }) + .on('unhandledRejection', error => { + console.error(error); + }); +})(); diff --git a/test.d.ts b/test.d.ts index 01d48cba..0c1dca73 100644 --- a/test.d.ts +++ b/test.d.ts @@ -8,8 +8,12 @@ declare module 'rollup-plugin-node-resolve' { export = resolve; } +declare module 'rollup-plugin-terser' { + export function terser(options?: any): any; +} + declare module 'rollup-plugin-postcss' { - function postcss(options: any): any; + function postcss(options?: any): any; export = postcss; } diff --git a/test/integration.test.ts b/test/integration.test.ts index 9f63f5a4..b84e818a 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -2,9 +2,6 @@ import * as fs from 'fs'; import * as path from 'path'; import * as puppeteer from 'puppeteer'; import { assert } from 'chai'; -import * as rollup from 'rollup'; -import typescript = require('rollup-plugin-typescript'); -import resolve = require('rollup-plugin-node-resolve'); import { SnapshotState, toMatchSnapshot } from 'jest-snapshot'; import { EventType, IncrementalSource, eventWithTime } from '../src/types'; import { NodeType } from 'rrweb-snapshot'; @@ -94,7 +91,7 @@ describe('record integration tests', () => { ${this.code} window.Date.now = () => new Date(Date.UTC(2018, 10, 15, 8)).valueOf(); window.snapshots = []; - record({ + rrweb.record({ emit: event => { console.log(event); window.snapshots.push(event); @@ -116,15 +113,8 @@ describe('record integration tests', () => { args: ['--no-sandbox'], }); - const bundle = await rollup.rollup({ - input: path.resolve(__dirname, '../src/record/index.ts'), - plugins: [typescript(), resolve()], - }); - const { code } = await bundle.generate({ - name: 'record', - format: 'iife', - }); - this.code = code; + const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js'); + this.code = fs.readFileSync(bundlePath, 'utf8'); }); after(async () => { diff --git a/test/repl.ts b/test/repl.ts deleted file mode 100644 index 3110e733..00000000 --- a/test/repl.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* tslint:disable: no-console */ - -import * as fs from 'fs'; -import * as path from 'path'; -import * as EventEmitter from 'events'; -import * as readline from 'readline'; -import * as rollup from 'rollup'; -import typescript = require('rollup-plugin-typescript'); -import resolve = require('rollup-plugin-node-resolve'); -import postcss = require('rollup-plugin-postcss'); -import * as puppeteer from 'puppeteer'; -import { eventWithTime } from '../src/types'; - -const emitter = new EventEmitter(); - -async function getCode(): Promise { - const bundle = await rollup.rollup({ - input: path.resolve(__dirname, '../src/index.ts'), - plugins: [typescript(), resolve(), postcss({ extract: false })], - }); - const { code } = await bundle.generate({ - name: 'rrweb', - format: 'iife', - }); - return code; -} - -(async () => { - const code = await getCode(); - const browser = await puppeteer.launch({ - headless: false, - defaultViewport: { - width: 1600, - height: 900, - }, - }); - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - let events: eventWithTime[] = []; - - rl.question( - 'Enter the url you want to record, e.g https://react-redux.realworld.io: ', - async url => { - console.log(`Going to open ${url}...`); - await record(url); - console.log('Ready to record. You can do any interaction on the page.'); - rl.question( - `Once you want to finish the recording, enter 'y' to start replay: `, - async answer => { - if (answer.toLowerCase() === 'y') { - emitter.emit('done'); - } - }, - ); - }, - ); - - async function record(url: string) { - const page = await browser.newPage(); - await page.goto(url, { - waitUntil: 'domcontentloaded', - }); - await page.exposeFunction('log', (event: eventWithTime) => { - events.push(event); - }); - await page.evaluate(`${code} - rrweb.record({ - emit: event => window.log(event) - }); - `); - emitter.once('done', async () => { - await page.close(); - console.log(`Recorded ${events.length} events`); - replay(); - }); - } - - async function replay() { - const style = fs.readFileSync( - path.resolve(__dirname, '../src/replay/styles/style.css'), - 'utf8', - ); - const page = await browser.newPage(); - await page.goto('about:blank'); - await page.addStyleTag({ - content: style, - }); - await page.evaluate(`${code} - const events = ${JSON.stringify(events)}; - const replayer = new rrweb.Replayer(events); - replayer.play(); - `); - } - - process - .on('uncaughtException', error => { - console.error(error); - }) - .on('unhandledRejection', error => { - console.error(error); - }); -})();