Первый опыт фронтальной системы записи и воспроизведения

внешний интерфейс
Первый опыт фронтальной системы записи и воспроизведения

Эта статья от товарищей по команде@ 小 信Обмен знаниями, я надеюсь поделиться и обсудить с вами.

Стремитесь накопить кремния на тысячу миль и имейте смелость исследовать красоту жизни.

фон проблемы

Что такое фронтальная запись и воспроизведение?

Как следует из названия, он предназначен для записи различных операций пользователя на веб-странице и поддерживает операции воспроизведения в любое время.

Зачем тебе это?

Когда дело доходит до необходимости, я должен сказать классический сценарий: как правило, интерфейс выполняет нештатный мониторинг и отчеты об ошибках, и будет использовать доступ собственной разработки или стороннего.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Исходный код намного больше.

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Содержит три состояния:

  1. Интерактивныйinteractive;
  2. Загрузкаloading;
  3. Заканчивать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!,
    );
}

иметь дело сformForm, логика состоит в том, чтобы сохранить выбранное состояние и выполнить некоторую обработку безопасности, например, заменить содержимое поля пароля на*.

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, библиотеки компонентов для левой руки, библиотеки инструментов для правой руки и различных технологий, которые быстро растут.

Человек может бежать быстрее, чем группа людей может пробежать далеко. Добро пожаловать в нашу команду, и год Быка мчится вперед.

Справочная статья