diff --git a/README.md b/README.md
index 05ebcd4f..2cd3cecb 100644
--- a/README.md
+++ b/README.md
@@ -134,7 +134,7 @@ const elementList: [HTMLElement, string][] = [
用户在网页中移动鼠标会产生很多`mouseMove`事件,通过 `const {x,y} = event.target` 获取到了轨迹的坐标与时间戳
假如我在页面上用鼠标划过一个💖的轨迹,可能会得到下图这样的坐标点
-
+
但是对于录屏这个业务场景来说,大部分场合我们并不要求100%还原精确的鼠标轨迹,我门只会关心两种情况:
```
@@ -144,7 +144,7 @@ const elementList: [HTMLElement, string][] = [
那么通过这个两个策略对鼠标轨迹进行精简后,画一个💖大约只需要6个点,通过样条曲线来模拟鼠标的虚拟轨迹,当 t = 0.2 的时候,就可以得到一个下图这样带着弧度的轨迹了
-
+
// TODO DETAIL
@@ -154,7 +154,7 @@ const elementList: [HTMLElement, string][] = [
这里需要注意的地方是当页面切换的时候我们需要重置热力图,如果是单页应用,通过 `History` 的 `popstate` 与 `hashchange` 可以监听页面的变化
-
+
##### 对于用户隐私的脱敏
diff --git a/heart1.png b/assets/heart1.png
similarity index 100%
rename from heart1.png
rename to assets/heart1.png
diff --git a/heart2.png b/assets/heart2.png
similarity index 100%
rename from heart2.png
rename to assets/heart2.png
diff --git a/heatmap.png b/assets/heatmap.png
similarity index 100%
rename from heatmap.png
rename to assets/heatmap.png
diff --git a/assets/template.html b/assets/template.html
new file mode 100644
index 00000000..6dab556a
--- /dev/null
+++ b/assets/template.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Web Replay
+
+
+
+
+
+
+
+
+
diff --git a/examples/test.html b/examples/test.html
index a473abd4..19e5efec 100644
--- a/examples/test.html
+++ b/examples/test.html
@@ -5,8 +5,6 @@
WebReplay
-
-
`, 'text/html').head.firstChild as HTMLElement
return style
}
-
- renderHTML() {
- const html = convertVNode(this.vNode, null)
- if (html) {
- this.convertHTMLElement = html as HTMLElement
- }
- }
}
diff --git a/packages/player/src/index.ts b/packages/player/src/index.ts
index f954228d..05673d30 100644
--- a/packages/player/src/index.ts
+++ b/packages/player/src/index.ts
@@ -6,12 +6,6 @@ export async function replay() {
const indexDB = await DBPromise
const { width, height, vNode, data } = await indexDB.getData()
- document.documentElement.innerHTML = ''
-
- Array.from(listenerStore.entries()).forEach(([name, handle]) => {
- document.removeEventListener(name, handle)
- })
-
const contain = new Container({
vNode,
width,
diff --git a/packages/record/src/record.ts b/packages/record/src/record.ts
index f030919f..348b57bd 100644
--- a/packages/record/src/record.ts
+++ b/packages/record/src/record.ts
@@ -1,12 +1,20 @@
-import { snapshot, SnapshotData } from '@WebReplay/snapshot'
+import { snapshots, SnapshotData } from '@WebReplay/snapshot'
import { RecordOptions } from './types'
+import { listenerStore } from '@WebReplay/utils'
+
+const ctrl = {
+ uninstall: () => {
+ Array.from(listenerStore.values()).forEach(un => un())
+ }
+}
export const record = ({ emitter }: RecordOptions = {}) => {
recordAll(emitter)
+ return ctrl
}
function recordAll(emitter?: (e: SnapshotData) => void) {
- const recordTasks: Function[] = [...Object.values(snapshot)]
+ const recordTasks: Function[] = [...Object.values(snapshots)]
recordTasks.forEach(task => {
task(emitter)
diff --git a/packages/snapshot/src/snapshot.ts b/packages/snapshot/src/snapshot.ts
index 65db7878..9b9fdd79 100644
--- a/packages/snapshot/src/snapshot.ts
+++ b/packages/snapshot/src/snapshot.ts
@@ -62,8 +62,12 @@ function mouseObserve(emit: SnapshotEvent) {
const listenerHandle = throttle(evt, 100, {
trailing: true
})
- listenerStore.set(name, listenerHandle)
+
document.addEventListener(name, listenerHandle)
+
+ listenerStore.add(() => {
+ document.removeEventListener(name, listenerHandle)
+ })
}
function mouseClick() {
@@ -82,7 +86,9 @@ function mouseObserve(emit: SnapshotEvent) {
const name = 'click'
const listenerHandle = throttle(evt, 250)
- listenerStore.set(name, listenerHandle)
+ listenerStore.add(() => {
+ document.removeEventListener(name, listenerHandle)
+ })
document.addEventListener(name, listenerHandle)
}
@@ -119,7 +125,6 @@ function DOMObserve(emit: SnapshotEvent) {
break
case 'characterData':
const parent = target.parentNode!
-
joinData({
parentId: nodeStore.getNodeId(parent),
value: target.nodeValue,
@@ -193,49 +198,77 @@ function DOMObserve(emit: SnapshotEvent) {
childList: true,
subtree: true
})
+
+ listenerStore.add(() => {
+ observer.disconnect()
+ })
}
function formElementObserve(emit: SnapshotEvent) {
- const els = nodeStore.getAllInputs()
+ listenInputs(emit)
+ kidnapInputs(emit) // for sys write in input
+}
- listenInput(emit) // for sys write in input
+function listenInputs(emit: SnapshotEvent) {
+ const eventTypes = ['input', 'change', 'focus', 'blur']
- els.forEach(el => {
- el.addEventListener('input', (e: InputEvent) => {
- emit({
- type: SnapshotType.FORM_EL_UPDATE,
- data: {
- type: FormElementEvent.INPUT,
- id: nodeStore.getNodeId(e.target as Node)!,
- value: (e.target as HTMLInputElement).value
- },
- time: Date.now().toString()
- })
- })
- el.addEventListener('focus', (e: InputEvent) => {
- emit({
- type: SnapshotType.FORM_EL_UPDATE,
- data: {
- type: FormElementEvent.FOCUS,
- id: nodeStore.getNodeId(e.target as Node)!
- },
- time: Date.now().toString()
- })
+ eventTypes
+ .map(type => {
+ return (fn: (e: InputEvent) => void) => {
+ document.addEventListener(type, fn, { passive: true, capture: true, once: false })
+ }
})
- el.addEventListener('blur', (e: InputEvent) => {
- emit({
- type: SnapshotType.FORM_EL_UPDATE,
- data: {
- type: FormElementEvent.BLUR,
- id: nodeStore.getNodeId(e.target as Node)!
- },
- time: Date.now().toString()
- })
+ .forEach(handle => handle(handleFn))
+
+ listenerStore.add(() => {
+ eventTypes.forEach(type => {
+ document.removeEventListener(type, handleFn)
})
})
+
+ function handleFn(e: InputEvent) {
+ const eventType = e.type
+
+ switch (eventType) {
+ case 'input':
+ case 'change':
+ emit({
+ type: SnapshotType.FORM_EL_UPDATE,
+ data: {
+ type: FormElementEvent.INPUT,
+ id: nodeStore.getNodeId(e.target as Node)!,
+ value: (e.target as HTMLInputElement).value
+ },
+ time: Date.now().toString()
+ })
+ break
+ case 'focus':
+ emit({
+ type: SnapshotType.FORM_EL_UPDATE,
+ data: {
+ type: FormElementEvent.FOCUS,
+ id: nodeStore.getNodeId(e.target as Node)!
+ },
+ time: Date.now().toString()
+ })
+ break
+ case 'blur':
+ emit({
+ type: SnapshotType.FORM_EL_UPDATE,
+ data: {
+ type: FormElementEvent.BLUR,
+ id: nodeStore.getNodeId(e.target as Node)!
+ },
+ time: Date.now().toString()
+ })
+ break
+ default:
+ break
+ }
+ }
}
-function listenInput(emit: SnapshotEvent) {
+function kidnapInputs(emit: SnapshotEvent) {
const elementList: [HTMLElement, string][] = [
[HTMLInputElement.prototype, 'value'],
[HTMLInputElement.prototype, 'checked'],
@@ -243,21 +276,25 @@ function listenInput(emit: SnapshotEvent) {
[HTMLTextAreaElement.prototype, 'value']
]
- elementList.forEach(item => {
- const [target, key] = item
- const original = Object.getOwnPropertyDescriptor(target, key)
- Object.defineProperty(target, key, {
- set: function(value: string | boolean) {
- setTimeout(() => {
- handleEvent.call(this, key, value)
- })
- if (original && original.set) {
- original.set.call(this, value)
+ const handles = elementList.map(item => {
+ return () => {
+ const [target, key] = item
+ const original = Object.getOwnPropertyDescriptor(target, key)
+ Object.defineProperty(target, key, {
+ set: function(value: string | boolean) {
+ setTimeout(() => {
+ handleEvent.call(this, key, value)
+ })
+ if (original && original.set) {
+ original.set.call(this, value)
+ }
}
- }
- })
+ })
+ }
})
+ handles.concat([]).forEach(handle => handle())
+
function handleEvent(this: HTMLElement, key: string, value: string) {
emit({
type: SnapshotType.FORM_EL_UPDATE,
@@ -272,7 +309,7 @@ function listenInput(emit: SnapshotEvent) {
}
}
-export const snapshot = {
+export const snapshots = {
windowSnapshot,
DOMSnapshot,
mouseObserve,
diff --git a/packages/snapshot/src/types.ts b/packages/snapshot/src/types.ts
index 8cb3922b..c242e6c7 100644
--- a/packages/snapshot/src/types.ts
+++ b/packages/snapshot/src/types.ts
@@ -11,6 +11,7 @@ export enum SnapshotType {
export enum FormElementEvent {
'ATTR' = 'ATTR',
'INPUT' = 'INPUT',
+ 'CHANGE' = 'CHANGE',
'FOCUS' = 'FOCUS',
'BLUR' = 'BLUR'
}
diff --git a/packages/utils/src/store/data.ts b/packages/utils/src/store/data.ts
index e1d24541..df4c948f 100644
--- a/packages/utils/src/store/data.ts
+++ b/packages/utils/src/store/data.ts
@@ -19,7 +19,7 @@ export class IndexDBOperator {
request.onsuccess = e => {
this.db = request.result
- this.clear()
+ // this.clear()
callback(this.db)
}
diff --git a/packages/utils/src/store/listener.ts b/packages/utils/src/store/listener.ts
index 6c88d9a4..c3caf2de 100644
--- a/packages/utils/src/store/listener.ts
+++ b/packages/utils/src/store/listener.ts
@@ -1 +1 @@
-export const listenerStore = new Map()
+export const listenerStore = new Set()
diff --git a/packages/utils/src/store/node.ts b/packages/utils/src/store/node.ts
index 1acba604..f18a95ff 100644
--- a/packages/utils/src/store/node.ts
+++ b/packages/utils/src/store/node.ts
@@ -24,12 +24,6 @@ class NodeStore {
return this.idMap.get(node)
}
- public getAllInputs() { // TODO IMPROVE
- return [...this.nodeMap.values()].filter((node: Element) =>
- ['INPUT', 'SELECT', 'TEXTAREA'].includes(node.tagName)
- )
- }
-
public updateNode(id: number, node: Node) {
this.idMap.set(node, id)
this.nodeMap.set(id, node)
diff --git a/packages/virtual-dom/src/deserialize.ts b/packages/virtual-dom/src/deserialize.ts
index fee331c5..d85e338e 100644
--- a/packages/virtual-dom/src/deserialize.ts
+++ b/packages/virtual-dom/src/deserialize.ts
@@ -20,7 +20,6 @@ export function convertVNode(vNode: VNode | string | null, node: Element | null)
function travel(vNode: VNode, node: Element): void {
const nodeChildren: Element[] = []
const vNodeChildren = vNode.children.slice()
- node.setAttribute('vid', vNode.id.toString())
vNodeChildren.forEach(vChild => {
let child = nodeChildren.pop() as Element | null
child = convertVNode(vChild, child)
diff --git a/rollup.config.dev.js b/rollup.config.dev.js
index f19e1b8b..631f429c 100644
--- a/rollup.config.dev.js
+++ b/rollup.config.dev.js
@@ -10,19 +10,21 @@ import { string } from 'rollup-plugin-string'
export default {
input: 'index.ts',
output: {
+ name: 'wr',
+ format: 'esm',
file: 'dist/web-replay.js',
- format: 'umd',
sourcemap: true
},
plugins: [
ts(),
- node({
- jsnext: true
- }),
+ node(),
sourcemaps(),
html({
- // template: () => fs.readFileSync('examples/test.html')
- template: () => fs.readFileSync('examples/todo.html')
+ template: () => fs.readFileSync('examples/test.html')
+ }),
+ html({
+ fileName: 'replay.html',
+ template: () => fs.readFileSync('assets/template.html')
}),
string({
include: ['**/*.html', '**/*.css'],
diff --git a/rollup.config.prod.js b/rollup.config.prod.js
index 2acd31dc..e102707f 100644
--- a/rollup.config.prod.js
+++ b/rollup.config.prod.js
@@ -21,9 +21,12 @@ export default {
}),
sourcemaps(),
html({
- // template: () => fs.readFileSync('examples/test.html')
template: () => fs.readFileSync('examples/todo.html')
}),
+ html({
+ fileName: 'replay.html',
+ template: () => fs.readFileSync('assets/template.html')
+ }),
string({
include: ['**/*.html', '**/*.css'],
exclude: ['**/index.html', '**/index.css']