Эта статья от товарищей по команде@ 小 信Обмен знаниями, я надеюсь поделиться и обсудить с вами.
Стремитесь накопить кремния на тысячу миль и имейте смелость исследовать красоту жизни.
фон проблемы
Что такое фронтальная запись и воспроизведение?
Как следует из названия, он предназначен для записи различных операций пользователя на веб-странице и поддерживает операции воспроизведения в любое время.
Зачем тебе это?
Когда дело доходит до необходимости, я должен сказать классический сценарий: как правило, интерфейс выполняет нештатный мониторинг и отчеты об ошибках, и будет использовать доступ собственной разработки или стороннего.SDK
форма для сбора и отчета о взаимодействиях с веб-сайтом во времяJavaScript
Сообщение об ошибке и другие связанные данные, то есть скрытая точка.
В традиционной схеме заглубленных точек, согласноSourceMap
Он может найти конкретный файл кода ошибки, а также информацию о строке и столбце. В основном он может локализовать большинство проблем сцены, но в некоторых случаях трудно воспроизвести ошибку, большая часть которой является одной из мантр программиста при тестировании фарса (здесь я не сообщал об ошибке, есть ли проблема с твой компьютер).
Было бы неплохо, если бы мы могли зафиксировать процесс операции, который пошел не так, чтобы мы могли легко воспроизвести сцену и сохранить улики, как если бы мы вырыли себе яму.
Как добиться?
Может ли фронтенд записывать видео? Моей первой реакцией был вопрос, затем я был волнойGoogle
, обнаружил, что действительно существует допустимое решение.
существуетGoogle
Раньше я думал сделать скриншот окна просмотра, установив таймер, и скриншот доступенcanvas2html
способ достижения, но этот способ, несомненно, вызовет проблемы с производительностью, сразу же отвергается.
Вот что я "знаю"Google
Если есть какие-либо проблемы, пожалуйста, поправьте меня.
Новые идеи
Веб-страница по сути являетсяDOM
Он существует в виде узлов и отображается браузером. Можем ли мы поставитьDOM
Сохраните его каким-нибудь образом и продолжайте записывать в разные моменты времени.DOM
статус данных. восстановить данные наDOM
Отрисовывает ли узел и завершает ли воспроизведение?
Запись операции
пройти черезdocument.documentElement.cloneNode()
клонировать вDOM
В настоящее время данные не могут быть напрямую переданы на серверную часть через интерфейс, и требуется некоторая предварительная обработка форматирования для их преобразования в формат данных, удобный для передачи и хранения. Самый простой способ — сериализовать, то есть преобразовать вJSON
Формат данных.
// 序列化后
let docJSON = {
"type": "Document",
"childNodes": [
{
"type": "Element",
"tagName": "html",
"attributes": {},
"childNodes": [
{
"type": "Element",
"tagName": "head",
"attributes": {},
"childNodes": []
}
]
}
]
}
иметь полныйDOM
После данных также необходимоDOM
Следите за изменениями, записывайте каждое изменениеDOM
Информация об узле. Доступны для мониторинга данныхMutationObserver
, который представляет собой монитор, который можетDOM
изменениеAPI
.
const observer = new MutationObserver(mutationsList => {
console.log(mutationsList); // 发生变化的数据
});
// 以上述配置开始观察目标节点
observer.observe(document, {});
за исключениемDOM
В дополнение к мониторингу изменений существует также мониторинг событий.Взаимодействие между пользователями и веб-страницами в основном осуществляется через устройства ввода, такие как мышь и клавиатура. И за этими взаимодействиями стоятJavaScript
прослушиватель событий. Мониторинг событий можно осуществлять путем привязки системных событий, которые также необходимо записывать, например движение мыши:
// 鼠标移动
document.addEventListener('mousemove', e => {
// 伪代码 获取鼠标移动的信息并记录下来
positions.push({
x: clientX,
y: clientY,
timeOffset: Date.now() - timeBaseline,
});
});
addEventListener может связывать несколько идентичных событий, не влияя на привязку событий разработчика.
операция воспроизведения
Данные уже есть, а дальше идет воспроизведение, что по сутиJSON
восстановление данных вDOM
Узел визуализируется. Затем вы можете восстановить данные снимка.«Король рта», восстановление данных не так просто!
среда рендеринга
Во-первых, чтобы обеспечить изоляцию кода во время воспроизведения, требуется среда песочницы.iframe
Этикетки могут это сделать, иiframe
при условииsandbox
Свойства настраиваемой песочницы. Роль среды «песочницы» заключается в том, чтобы сохранить код безопасным и нетронутым.
<iframe sandbox srcdoc></iframe>
sanbox
Атрибуты могут быть помещены в песочницу,Нажмите, чтобы просмотреть документацию
srcdoc
может быть непосредственно установлен как сегментhtml
код
восстановление данных
Реорганизация моментальных снимков в основномDOM
Реорганизация узлов, немного похожая на виртуальнуюDOM
Процесс преобразования в реальный узел документа, но моментальные снимки событийного типа не нуждаются в реорганизации.
таймер
С данными и контекстом также необходимы таймеры. Непрерывный рендеринг через таймерDOM
, что по сути является эффектом воспроизведения видео,requestAnimationFrame
является наиболее подходящим.
Механизм выполнения requestAnimationFrame выполняется перед очередной перерисовкой (перерисовкой) браузера, частота выполнения зависит от частоты обновления браузера, что больше подходит для создания анимационных эффектов.
На данный момент есть общее представление, и до посадки еще далеко. Благодаря открытому исходному коду мы можем зайти на Github, чтобы посмотреть, есть ли подходящий руль для копирования (для справки), там просто готовый фреймворк"рвеб", возможно, пожелают взглянуть вместе.
структура rrweb
rrweb
Является интерфейсной средой записи и воспроизведения. полное имяrecord and replay the web
, как следует из названия, вы можете записывать и воспроизводитьweb
Основным принципом работы в интерфейсе является описанная выше схема.
состав rrweb
rrweb
Содержит три части:
-
rrweb-snapshot
основная обработкаDOM
Структурная сериализация и реорганизация; -
rrweb
Основная функция — запись и воспроизведение; -
rrweb-player
Пространство пользовательского интерфейса видеоплеера
использование rrweb
Установка npm обычная, импорт/требование не является большой проблемой
записывать
пройти черезrrweb.record
способ записи страницы,emit
Обратный вызов может получать записанные данные.
// 1.录制
let events = []; // 记录快照
rrweb.record({
emit(event) {
// 将 event 存入 events 数组中
events.push(event);
},
});
воспроизведение
пройти черезrrweb.Replayer
Видео можно воспроизвести, а записанные данные нужно передать.
// 2.回放
const replayer = new rrweb.Replayer(events);
replayer.play();
Нажмите, чтобы просмотреть официальный эффект случая
исходный код rrweb
В соответствии с идеями, упомянутыми выше, некоторые из ключевых кодов будут проанализированы далее.Конечно, это только некоторый анализ, основанный на моем личном понимании.На самом деле,rrweb
Исходный код намного больше.
Основная часть разделена на три блока:record
(запись),replay
воспроизведение,snapshot
снимок.
Записывать
существуетDOM
После завершения загрузкиrecord
сделает полныйDOM
Сериализация, мы называем это полным снимком, полный снимок записывает всеHTML
структура данных.
существуетrecord.ts
Найдите определение функции ввода с ключа вinit
, функция входа будет вdocument
Вызывается, когда загрузка завершена или (интерактивно, сделано)takeFullSnapshot
а такжеobserve(document)
функция.
if (
document.readyState === 'interactive' ||
document.readyState === 'complete'
) {
init();
} else {
//...
on('load',() => { init(); },),
}
const init = () => {
takeFullSnapshot(); // 生成全量快照
handlers.push(observe(document)); //监听器
};
document.readyState
Содержит три состояния:
- Интерактивный
interactive
;- Загрузка
loading
;- Заканчивать
complete
takeFullSnapshot
Из буквального значения видно, что его роль заключается в создании «полного» снимка, т. е.document
Сериализуйте полные данные, назовите их«Полный снимок».
Все операции, связанные с сериализацией, выполняются с использованиемsnapshot
Заканчивать,snapshot
принять одинdom
переданный объект и объект конфигурацииdocument
Сериализуйте всю страницу, чтобы получить данные завершенного снимка.
// 生成全量快照
takeFullSnapshot = (isCheckout = false) => {
//...
const [node, idNodeMap] = snapshot(document, {
//...一些配置项
});
//...
}
idNodeMap — это
id
заkey
,DOM
объектvalue
изkey-value
объект ключ-значение
observe(document)
это инициализация некоторых слушателей, а также всегоdocument
Объект передается в прошлое для мониторинга путем вызоваinitObservers
для инициализации некоторых слушателей.
const observe = (doc: Document) => {
return initObservers(//...)
}
существуетobserver.ts
можно найти в файлеinitObservers
Определение функции, которая инициализирует 11 прослушивателей, которые можно разделить наDOM
тип /Event
тип события /Media
Три типа СМИ:
export function initObservers(
// dom
const mutationObserver = initMutationObserver();
const mousemoveHandler = initMoveObserver();
const mouseInteractionHandler = initMouseInteractionObserver();
const scrollHandler = initScrollObserver();
const viewportResizeHandler = initViewportResizeObserver();
// ...
)
-
DOM
Меняйте слушателей, в основномDOM
Изменения (дополнения, удаления и изменения), изменения стиля, ядро черезMutationObserver
реализоватьlet mutationObserverCtor = window.MutationObserver; const observer = new mutationObserverCtor( // 处理变化的数据 mutationBuffer.processMutations.bind(mutationBuffer), ); observer.observe(doc, {}); return observer;
-
Монитор взаимодействия - перемещение мышью
initMoveObserver
Например
// 鼠标移动记录
function initMoveObserver() {
const updatePosition = throttle<MouseEvent | TouchEvent>(
(evt) => {
positions.push({
x: clientX,
y: clientY,
});
});
const handlers = [
on('mousemove', updatePosition, doc),
on('touchmove', updatePosition, doc),
];
}
- прослушиватель типа носителя, с
canvas
/video
/audio
,отvideo
Например, по существу запись состояний воспроизведения и паузы,mediaInteractionCb
будетplay
/pause
Статус перезванивается.
function initMediaInteractionObserver(): listenerHandler {
mediaInteractionCb({
type: type === 'play' ? MediaInteractions.Play : MediaInteractions.Pause,
id: mirror.getId(target as INode),
});
}
Снимок
snapshot
Функции, отвечающие за сериализацию и повторную сборку, в основном черезserializeNodeWithId
иметь дело сDOM
сериализовать иrebuildWithSN
обработка функцийDOM
реорганизация.
serializeNodeWithId
Функция отвечает за сериализацию и в основном делает три вещи:
- перечислить
serializeNode
СериализацияNode
; - пройти через
genId()
Создайте уникальный идентификатор и привяжите его кNode
середина; - Рекурсивная реализация сериализует дочерние узлы и, наконец, возвращает
ID
Объект
// 序列化一个带有ID的DOM
export function serializeNodeWithId(n) {
// 1. 序列化 核心函数 serializeNode
const _serializedNode = serializeNode(n);
// 2. 生成唯一ID
let id = genId();
// 绑定ID
const serializedNode = Object.assign(_serializedNode, { id });
// 3.子节点序列化-递归
for (const childN of Array.from(n.childNodes)) {
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
if (serializedChildNode) {
serializedNode.childNodes.push(serializedChildNode);
}
}
}
serializeNodeWithId
Ядро проходит черезserializeNode
СериализацияDOM
и выполнить специальную обработку для разных узлов.
Обработка атрибутов узла:
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
attributes[name] = transformAttribute(doc, tagName, name, value);
}
справиться с охватомcss
стиль, черезgetCssRulesString
Получите определенный код стиля и сохраните его вattributes
середина.
const cssText = getCssRulesString(stylesheet as CSSStyleSheet);
if (cssText) {
attributes._cssText = absoluteToStylesheet(
cssText,
stylesheet!.href!,
);
}
иметь дело сform
Form, логика состоит в том, чтобы сохранить выбранное состояние и выполнить некоторую обработку безопасности, например, заменить содержимое поля пароля на*
.
if (
attributes.type !== 'radio' &&
attributes.type !== 'checkbox' &&
// ...
) {
attributes.value = maskInputOptions[tagName]
? '*'.repeat(value.length)
: value;
} else if (n.checked) {
attributes.checked = n.checked;
}
canvas
состояние сохраняется черезtoDataURL
спастиcanvas
данные:
attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL();
rebuildответственный за восстановлениеDOM
:
- пройти через
buildNodeWithSN
реорганизация функцийNode
- Рекурсивный вызов Реорганизация дочерних узлов
export function buildNodeWithSN(n) {
// DOM 重组核心函数 buildNode
let node = buildNode(n, { doc, hackCss });
// 子节点重建并且appendChild
for (const childN of n.childNodes) {
const childNode = buildNodeWithSN(childN);
if (afterAppend) {
afterAppend(childNode);
}
}
}
Повтор
Раздел воспроизведения находится вreplay.ts
файла, сначала создайте среду песочницы, затем или перестройтеdocument
Полный снимок, после прохожденияrequestAnimationFrame
Имитирует таймер для воспроизведения добавочных снимков.
replay
Конструктор получает два параметра, данные снимкаevents
и элементы конфигурацииconfig
export class Replayer {
constructor(events, config) {
// 1.创建沙箱环境
this.setupDom();
// 2.定时器
const timer = new Timer();
// 3.播放服务
this.service = new createPlayerService(events, timer);
this.service.start();
}
}
Три основных шага в конструкторе — это создание среды песочницы, таймера, а также инициализация и запуск проигрывателя. Зависимости создания игрокаevents
иtimer
, в основном все еще используяtimer
добиться воспроизведения.
среда песочницы
Первый вreplay.ts
можно найти в конструктореthis.setupDom
вызов,setupDom
Ядро проходит черезiframe
для создания среды песочницы.
private setupDom() {
// 创建iframe
this.iframe = document.createElement('iframe');
this.iframe.style.display = 'none';
this.iframe.setAttribute('sandbox', attributes.join(' '));
}
игровой сервис
Также вreplay.ts
В конструкторе вызовитеcreatePlayerService
функция для создания сервера игрока, функция находится в каталоге того же уровняmachine.ts
Основная идея, определенная в , состоит в том, чтобы дать таймеруtimer
Добавьте действия со снимками, которые необходимо выполнитьactions
, звонюtimer.start()
Запустите воспроизведение снимка.
export function createPlayerService() {
//...
play(ctx) {
// 获取每个 event 执行的 doAction 函数
for (const event of needEvents) {
//..
const castFn = getCastFn(event);
actions.push({
doAction: () => {
castFn();
}
})
//..
}
// 添加到定时器队列中
timer.addActions(actions);
// 启动定时器播放 视频
timer.start();
},
//...
}
Play Services использует стороннюю библиотеку
@xstate/fsm
Конечный автомат для управления различными состояниями (воспроизведение, пауза, прямой эфир)
таймерtimer.ts
Также в каталоге того же уровня ядро находится черезrequestAnimationFrame
Реализована функция таймера, и снимок воспроизводится, а снимок для воспроизведения хранится в виде очередиactions
, затем вstart
рекурсивный вызовaction.doAction
Для достижения моментального восстановления соответствующего узла времени.
export class Timer {
// 添加队列
public addActions(actions: actionWithDelay[]) {
this.actions = this.actions.concat(actions);
}
// 播放队列
public start() {
function check() {
// ...
// 循环调用actions中的doAction 也就是 castFn 函数
while (actions.length) {
const action = actions[0];
actions.shift();
// doAction 会对快照进行回放动作,针对不同快照会执行不同动作
action.doAction();
}
if (actions.length > 0 || self.liveMode) {
self.raf = requestAnimationFrame(check);
}
}
this.raf = requestAnimationFrame(check);
}
}
doAction
Различные действия выполняются с разными типами снимков в Play Services.doAction
в конце концов позвонитgetCastFn
функция, чтобы сделать некоторыеcase
:
private getCastFn(event: eventWithTime, isSync = false) {
switch (event.type) {
case EventType.DomContentLoaded: //dom 加载解析完成
case EventType.FullSnapshot: // 全量快照
case EventType.IncrementalSnapshot: //增量
castFn = () => {
this.applyIncremental(event, isSync);
}
}
}
applyIncremental
Функция будет обрабатывать разные инкрементные снимки по-разному, в том числеDOM
Инкременты, взаимодействие с мышью, прокрутка страницы и т. д.DOM
добавочный снимокcase
Например, в конечном итоге перейдет кapplyMutation
середина:
private applyIncremental(){
switch (d.source) {
case IncrementalSource.Mutation: {
this.applyMutation(d, isSync); // DOM变化
break;
}
case IncrementalSource.MouseMove: //鼠标移动
case IncrementalSource.MouseInteraction: //鼠标点击事件
//...
}
applyMutation
окончательное исполнениеDOM
где происходит операция восстановления, в том числеDOM
Этапы добавления, удаления и изменения:
private applyMutation(d: mutationData, useVirtualParent: boolean) {
d.removes.forEach((mutation) => {
//.. 移除dom
});
const appendNode = (mutation: addedNodeMutation) => {
// 添加dom到具体节点下
};
d.adds.forEach((mutation) => {
// 添加
appendNode(mutation);
});
d.texts.forEach((mutation) => {
//...文本处理
});
d.attributes.forEach((mutation) => {
//...属性处理
});
}
Выше приведен ключевой код реализации процесса воспроизведения,rrweb
Он не только делает это, но также включает в себя сжатие данных, обработку мобильного терминала, вопросы конфиденциальности и др. Если вам интересно, вы можете просмотреть исходный код самостоятельно.
Наконец
Эту идею записи и воспроизведения действительно стоит изучить, прочтитеrrweb
Процесс исходного кода также приносит много пользы, и некоторые варианты использования структур данных в исходном коде, такие как двусвязные списки, очереди, деревья и т. д., также заслуживают внимания.
Выше приведено все содержание этого обмена, я надеюсь, что это поможет вам ^_^
Если вам это нравится, не забудьте пошевелить пальцами, лайк, избранное, и следуйте трем последовательным волнам, чтобы забрать.
о нас
Мы являемся передовой командой Vantop Science and Technology, библиотеки компонентов для левой руки, библиотеки инструментов для правой руки и различных технологий, которые быстро растут.
Человек может бежать быстрее, чем группа людей может пробежать далеко. Добро пожаловать в нашу команду, и год Быка мчится вперед.