Практика инструментирования внешнего кода на основе перехвата цепочки прототипов

функциональное программирование внешний фреймворк

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

Концепции фундамента приборостроения

Основную идею интерфейсной инструментации можно выразить в этом вопросе:Предположим, есть функция, которая широко используется в бизнесе. Можем ли мы внедрить часть нашей пользовательской логики до и после ее выполнения, не изменяя бизнес-код, который ее вызывает, или исходный код функции?

В качестве более конкретного примера, если бизнес-логика имеет многоconsole.logКод журнала, можем ли мы сообщить об этом содержимом журнала через сетевые запросы, не изменяя эти коды? Простая идея такова:

  1. Инкапсулируйте функцию, которая «сначала выполняет пользовательскую логику, а затем выполняет исходный метод журнала».
  2. будет роднымconsole.logзаменена этой функцией.

Если мы хотим, чтобы наше решение было общим, нетрудно обобщить операцию первого шага на функцию более высокого порядка:

function withHookBefore (originalFn, hookFn) {
  return function () {
    hookFn.apply(this, arguments)
    return originalFn.apply(this, arguments)
  }
}

Поэтому наш инструментальный код очень лаконичен. Просто нужно выглядеть так:

console.log = withHookBefore(console.log, (...data) => myAjax(data))

оригинальныйconsole.logбудет продолжаться после вставленной нами логики. Рассмотрим следующий вопрос: Можем ли мы заблокировать извнеconsole.logреализация? С функциями высшего порядка это тоже проще простого:

function withHookBefore (originalFn, hookFn) {
  return function () {
    if (hookFn.apply(this, arguments) === false) {
      return
    }
    return originalFn.apply(this, arguments)
  }
}

пока функция ловушки возвращаетfalse, то исходная функция выполняться не будет. Например, следующее дает классную операцию по обновлению консоли:

console.log = withHookBefore(console.log, () => false)

Это основной принцип «кражи неба» в браузере.

Инструментарий DOM API

Простая подстановка функций недостаточна для выполнения некоторых более сложных операций. Ниже рассмотрим более интересный сценарий:Как зафиксировать все пользовательские события в браузере?

Конечно вы можетеdocument.bodyДобавьте различные прослушиватели событий, чтобы выполнить это требование. Но проблема в настоящее время заключается в том, что после использования дочернего элементаe.stopPropagation()Событие не может всплывать, и узел верхнего уровня не может получить событие. Собираемся ли мы перебрать все элементы в DOM и взломать их обработчики событий? Вместо обхода методом грубой силы мы можем создать суету в цепочке прототипов.

Для элемента DOM используйтеaddEventListenerДобавление к нему обратных вызовов событий — совершенно нормальная операция. Этот метод на самом деле находится в общедоступной цепочке прототипов, и мы можем захватить его с помощью предыдущей высокоуровневой функции инструментирования:

EventTarget.prototype.addEventListener = withHookBefore(
  EventTarget.prototype.addEventListener,
  myHookFn // 自定义的钩子函数
)

Но этого недостаточно. Потому что таким образом фактически добавляемый параметр слушателя не изменяется. Итак, можем ли мы перехватить параметр слушателя? На данный момент нам действительно нужна функция более высокого порядка, подобная этой:

  1. Передайте параметры исходной функции в пользовательский хук и верните ряд новых параметров.
  2. Вызов исходной функции с новыми параметрами после магической модификации.

Эта функция выглядит так:

function hookArgs (originalFn, argsGetter) {
  return function () {
    var _args = argsGetter.apply(this, arguments)
    // 在此魔改 arguments
    for (var i = 0; i < _args.length; i++) arguments[i] = _args[i]
    return originalFn.apply(this, arguments)
  }
}

Комбинируя эту функцию более высокого порядка с существующейwithHookBefore, мы можем разработать полную схему угона:

  • использоватьhookArgsзаменить входящийaddEventListenerкаждого параметра.
  • Среди замененных параметров второй параметр является реальнымlistenerПерезвоните. Замените этот обратный вызов наwithHookBeforeИндивидуальная версия.
  • в нашемlistenerВ добавленном хуке выполните наш собственный код сбора событий.

Основная логическая структура этой схемы примерно следующая:

EventTarget.prototype.addEventListener = hookArgs(
  EventTarget.prototype.addEventListener,
  function (type, listener, options) {
    const hookedListener = withHookBefore(listener, e => myEvents.push(e))
    return [type, hookedListener, options]
  }
)

Просто убедитесь, что приведенный выше код включен во всеaddEventListenerПеред выполнением фактического бизнес-кода мы можем выйти за пределы всплытия событий и собрать все интересующие нас пользовательские события :)

Инструментарий интерфейсных фреймворков

После того, как мы поймем принцип инструментирования DOM API, API фронтенд-фреймворка можно будет сделать по-кошачьи. Например, можем ли мы собрать или даже настроить всеthis.$emitКак насчет информации? Этого также можно легко добиться с помощью перехвата цепочки прототипов:

import Vue from 'vue'

Vue.prototype.$emit = withHookBefore(Vue.prototype.$emit, (name, payload) => {
  // 在此发挥你的黑魔法
  console.log('emitting', name, payload)
})

Конечно, для фреймворка, который уже инкапсулировал полный набор API-интерфейсов, его настройка таким образом, скорее всего, нарушит лучшие практики. Но когда вам нужно разработать базовые библиотеки или инструменты разработчика, я считаю, что эта технология найдет свое место. Приведу несколько примеров:

  • Основываясь на инструментарии console.log, мы можем обеспечить межэкранный сбор журналов (например, просмотр журналов операций других устройств в режиме реального времени на вашем компьютере).
  • На основе инструментария DOM API мы можем реализовать ненавязчивое встраивание бизнеса, а также запись и воспроизведение поведения пользователей.
  • Основываясь на инструментарии обработчиков жизненного цикла компонентов, мы можем добиться более точного и безболезненного сбора и анализа производительности.
  • ...

Суммировать

На данный момент мы представили основные концепции и методы инструментирования. Если вам интересно, хорошая новость заключается в том, что мы упаковали часто используемые инструментальные функции высшего порядка в готовую базовую библиотеку NPM.runtime-hooks, который включает следующие инструментальные функции:

  • withHookBefore- добавить хуки перед функциями
  • withHookAfter- добавить после хуков к функциям
  • hookArgs- Параметры функции магического изменения
  • hookOutput- Магическое значение возвращаемого значения функции изменения

Добро пожаловать вGitHubПопробуйте наш проект с открытым исходным кодом и приглашаем всех обратить внимание на эту колонку интерфейса :)

P.S. Команда фронтенда на нашей базе Сямынь активно набирает людей, ищет резюме xuebi наgaoding.comах~