impl #309 observe font face set changes

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent 6c6f9c14da
commit 8b198b338e
7 changed files with 121 additions and 4 deletions

View File

@@ -58,6 +58,7 @@
"typescript": "^3.9.5" "typescript": "^3.9.5"
}, },
"dependencies": { "dependencies": {
"@types/css-font-loading-module": "0.0.4",
"@xstate/fsm": "^1.4.0", "@xstate/fsm": "^1.4.0",
"mitt": "^1.1.3", "mitt": "^1.1.3",
"pako": "^1.0.11", "pako": "^1.0.11",

View File

@@ -95,7 +95,8 @@ function getCode(): string {
window.__IS_RECORDING__ = true window.__IS_RECORDING__ = true
rrweb.record({ rrweb.record({
emit: event => window._replLog(event), emit: event => window._replLog(event),
recordCanvas: true recordCanvas: true,
collectFonts: true
}); });
`); `);
page.on('framenavigated', async () => { page.on('framenavigated', async () => {
@@ -105,7 +106,8 @@ function getCode(): string {
window.__IS_RECORDING__ = true window.__IS_RECORDING__ = true
rrweb.record({ rrweb.record({
emit: event => window._replLog(event), emit: event => window._replLog(event),
recordCanvas: true recordCanvas: true,
collectFonts: true
}); });
`); `);
} }

View File

@@ -42,6 +42,7 @@ function record<T = eventWithTime>(
sampling = {}, sampling = {},
mousemoveWait, mousemoveWait,
recordCanvas = false, recordCanvas = false,
collectFonts = false,
} = options; } = options;
// runtime checks for user options // runtime checks for user options
if (!emit) { if (!emit) {
@@ -256,12 +257,23 @@ function record<T = eventWithTime>(
}, },
}), }),
), ),
fontCb: (p) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Font,
...p,
},
}),
),
blockClass, blockClass,
ignoreClass, ignoreClass,
maskInputOptions, maskInputOptions,
inlineStylesheet, inlineStylesheet,
sampling, sampling,
recordCanvas, recordCanvas,
collectFonts,
}, },
hooks, hooks,
), ),

View File

@@ -1,4 +1,5 @@
import { INode, MaskInputOptions } from 'rrweb-snapshot'; import { INode, MaskInputOptions } from 'rrweb-snapshot';
import { FontFaceDescriptors, FontFaceSet } from 'css-font-loading-module';
import { import {
mirror, mirror,
throttle, throttle,
@@ -32,6 +33,8 @@ import {
MediaInteractions, MediaInteractions,
SamplingStrategy, SamplingStrategy,
canvasMutationCallback, canvasMutationCallback,
fontCallback,
fontParam,
} from '../types'; } from '../types';
import MutationBuffer from './mutation'; import MutationBuffer from './mutation';
@@ -432,6 +435,56 @@ function initCanvasMutationObserver(
}; };
} }
function initFontObserver(cb: fontCallback): listenerHandler {
const handlers: listenerHandler[] = [];
const fontMap = new WeakMap<FontFace, fontParam>();
const originalFontFace = FontFace;
// tslint:disable-next-line: no-any
(window as any).FontFace = function FontFace(
family: string,
source: string | ArrayBufferView,
descriptors?: FontFaceDescriptors,
) {
const fontFace = new originalFontFace(family, source, descriptors);
fontMap.set(fontFace, {
family,
buffer: typeof source !== 'string',
descriptors,
fontSource:
typeof source === 'string'
? source
: // tslint:disable-next-line: no-any
JSON.stringify(Array.from(new Uint8Array(source as any))),
});
return fontFace;
};
const restoreHandler = patch(document.fonts, 'add', function (original) {
return function (this: FontFaceSet, fontFace: FontFace) {
setTimeout(() => {
const p = fontMap.get(fontFace);
if (p) {
cb(p);
fontMap.delete(fontFace);
}
}, 0);
return original.apply(this, [fontFace]);
};
});
handlers.push(() => {
// tslint:disable-next-line: no-any
(window as any).FonFace = originalFontFace;
});
handlers.push(restoreHandler);
return () => {
handlers.forEach((h) => h());
};
}
function mergeHooks(o: observerParam, hooks: hooksParam) { function mergeHooks(o: observerParam, hooks: hooksParam) {
const { const {
mutationCb, mutationCb,
@@ -443,6 +496,7 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
mediaInteractionCb, mediaInteractionCb,
styleSheetRuleCb, styleSheetRuleCb,
canvasMutationCb, canvasMutationCb,
fontCb,
} = o; } = o;
o.mutationCb = (...p: Arguments<mutationCallBack>) => { o.mutationCb = (...p: Arguments<mutationCallBack>) => {
if (hooks.mutation) { if (hooks.mutation) {
@@ -498,6 +552,12 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
} }
canvasMutationCb(...p); canvasMutationCb(...p);
}; };
o.fontCb = (...p: Arguments<fontCallback>) => {
if (hooks.font) {
hooks.font(...p);
}
fontCb(...p);
};
} }
export default function initObservers( export default function initObservers(
@@ -539,6 +599,7 @@ export default function initObservers(
const canvasMutationObserver = o.recordCanvas const canvasMutationObserver = o.recordCanvas
? initCanvasMutationObserver(o.canvasMutationCb, o.blockClass) ? initCanvasMutationObserver(o.canvasMutationCb, o.blockClass)
: () => {}; : () => {};
const fontObserver = o.collectFonts ? initFontObserver(o.fontCb) : () => {};
return () => { return () => {
mutationObserver.disconnect(); mutationObserver.disconnect();
@@ -550,5 +611,6 @@ export default function initObservers(
mediaInteractionHandler(); mediaInteractionHandler();
styleSheetObserver(); styleSheetObserver();
canvasMutationObserver(); canvasMutationObserver();
fontObserver();
}; };
} }

View File

@@ -813,6 +813,22 @@ export class Replayer {
} catch (error) { } catch (error) {
this.warnCanvasMutationFailed(d, d.id, error); this.warnCanvasMutationFailed(d, d.id, error);
} }
break;
}
case IncrementalSource.Font: {
try {
const fontFace = new FontFace(
d.family,
d.buffer ? new Uint8Array(JSON.parse(d.fontSource)) : d.fontSource,
d.descriptors,
);
this.iframe.contentDocument?.fonts.add(fontFace);
} catch (error) {
if (this.config.showWarning) {
console.warn(error);
}
}
break;
} }
default: default:
} }

View File

@@ -5,6 +5,7 @@ import {
MaskInputOptions, MaskInputOptions,
} from 'rrweb-snapshot'; } from 'rrweb-snapshot';
import { PackFn, UnpackFn } from './packer/base'; import { PackFn, UnpackFn } from './packer/base';
import { FontFaceDescriptors } from 'css-font-loading-module';
export enum EventType { export enum EventType {
DomContentLoaded, DomContentLoaded,
@@ -71,6 +72,7 @@ export enum IncrementalSource {
MediaInteraction, MediaInteraction,
StyleSheetRule, StyleSheetRule,
CanvasMutation, CanvasMutation,
Font,
} }
export type mutationData = { export type mutationData = {
@@ -111,6 +113,10 @@ export type canvasMutationData = {
source: IncrementalSource.CanvasMutation; source: IncrementalSource.CanvasMutation;
} & canvasMutationParam; } & canvasMutationParam;
export type fontData = {
source: IncrementalSource.Font;
} & fontParam;
export type incrementalData = export type incrementalData =
| mutationData | mutationData
| mousemoveData | mousemoveData
@@ -120,7 +126,8 @@ export type incrementalData =
| inputData | inputData
| mediaInteractionData | mediaInteractionData
| styleSheetRuleData | styleSheetRuleData
| canvasMutationData; | canvasMutationData
| fontData;
export type event = export type event =
| domContentLoadedEvent | domContentLoadedEvent
@@ -172,6 +179,7 @@ export type recordOptions<T> = {
packFn?: PackFn; packFn?: PackFn;
sampling?: SamplingStrategy; sampling?: SamplingStrategy;
recordCanvas?: boolean; recordCanvas?: boolean;
collectFonts?: boolean;
// departed, please use sampling options // departed, please use sampling options
mousemoveWait?: number; mousemoveWait?: number;
}; };
@@ -190,8 +198,10 @@ export type observerParam = {
inlineStylesheet: boolean; inlineStylesheet: boolean;
styleSheetRuleCb: styleSheetRuleCallback; styleSheetRuleCb: styleSheetRuleCallback;
canvasMutationCb: canvasMutationCallback; canvasMutationCb: canvasMutationCallback;
fontCb: fontCallback;
sampling: SamplingStrategy; sampling: SamplingStrategy;
recordCanvas: boolean; recordCanvas: boolean;
collectFonts: boolean;
}; };
export type hooksParam = { export type hooksParam = {
@@ -204,6 +214,7 @@ export type hooksParam = {
mediaInteaction?: mediaInteractionCallback; mediaInteaction?: mediaInteractionCallback;
styleSheetRule?: styleSheetRuleCallback; styleSheetRule?: styleSheetRuleCallback;
canvasMutation?: canvasMutationCallback; canvasMutation?: canvasMutationCallback;
font?: fontCallback;
}; };
// https://dom.spec.whatwg.org/#interface-mutationrecord // https://dom.spec.whatwg.org/#interface-mutationrecord
@@ -328,6 +339,15 @@ export type canvasMutationParam = {
setter?: true; setter?: true;
}; };
export type fontParam = {
family: string;
fontSource: string;
buffer: boolean;
descriptors?: FontFaceDescriptors;
};
export type fontCallback = (p: fontParam) => void;
export type viewportResizeDimention = { export type viewportResizeDimention = {
width: number; width: number;
height: number; height: number;

View File

@@ -14,5 +14,9 @@
}, },
"compileOnSave": true, "compileOnSave": true,
"exclude": ["test"], "exclude": ["test"],
"include": ["src", "test.d.ts"] "include": [
"src",
"test.d.ts",
"node_modules/@types/css-font-loading-module/index.d.ts"
]
} }