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 = ` + + +
+ + + +