Статьи

Прагматичное использование мартышек в JavaScript

Обезьяна в скафандре набирает код в терминале

Эта статья была рецензирована Морицем Крёгером и Томом Греко . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Вы когда-нибудь работали со сторонним кодом, который работал хорошо, за исключением одной мелочи, которая сводила вас с ума? Почему создатель забыл удалить эти надоедливые журналы консоли? Разве не было бы здорово, если бы этот вызов API мог сделать еще одну вещь? Если это так, то вы знаете, что может быть сложно (или невозможно) внедрить ваши изменения сопровождающим. Но как насчет изменения кода самостоятельно? Как вы можете это сделать, если у вас нет источников и вы не хотите размещать их самостоятельно? Добро пожаловать в путешествие в мир Monkey Patching на JavaScript!

В этой статье мы рассмотрим, что такое Monkey Patching, и разберем несколько разных примеров, используя их для изменения функциональности стороннего виджета в соответствии с нашими требованиями.

Что такое патч обезьян?

Monkey Patching (в дальнейшем называемый MP) — это метод переопределения, расширения или даже подавления поведения сегмента кода по умолчанию без изменения его исходного исходного кода. Это делается путем замены исходного поведения на фиксированную версию.

В этой статье будет использоваться существующий виджет поля обратной связи, который отображает простое всплывающее всплывающее окно, как показано на рисунке ниже, с формой обратной связи.

Виджет формы обратной связи, который будет исправлен

Исходный код был изменен, чтобы включить варианты использования, которые действуют как цели MP. Под целью я подразумеваю определенную часть функциональности, функции или, на самом низком уровне, метод, который мы собираемся исправить .

Еще одна модификация, которую я сделал, заключалась в том, чтобы удалить выражение немедленного вызова функции (IIFE), окружающее код. Это было сделано для того, чтобы сосредоточиться на технике МП.

Вы можете найти весь пример, в том числе патчи обезьян, обсуждаемые в этой статье, как Plunker .

Разве обезьяна не исправляет плохую практику?

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

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

Описанная здесь ситуация может быть неестественной, доведенной до крайности с помощью поддельного виджета, чтобы показать, какие у вас есть варианты. Вы, как читатель, должны решить, нравится ли вам то, что вы видите, или нет. Если ничего другого, после прочтения этого у вас будет лучшее понимание, чтобы спорить против MP.

Мишени для Patching Monkey

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

Закодированный цвет фона

Первым из них является метод toggleError который должен изменять цвет фона элемента на основе логического параметра.

 FeedbackBox.prototype.toggleError = function(obj, isError) { if(isError) { obj.css("background-color", "darkgrey"); } else { obj.css("background-color", ""); } } 

Как вы можете видеть, он устанавливает свойство background-color с помощью метода jQuery css . Это проблема, поскольку мы хотели бы, чтобы это было указано в правилах таблицы стилей.

Пеские консольные бревна

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

Перехват вызовов сервера рекламы

Виджет отличный, но у него странное поведение. Каждый раз, когда вы инициализируете скрипт, он будет отправлять запрос на странный рекламный сервер и отображать ненужные сообщения на нашей странице.

 FeedbackBox.prototype.init = function() { // call to an adserver we'd like to skip $.ajax('vendor/service.json', { method: 'GET' }).then(function(data) { console.log("FeedbackBox: AdServer contacted"); }); ... 

ПРИМЕЧАНИЕ . Демонстрационный код нацелен на файл JSON внутри Plunker для имитации исходящего Ajax-запроса, но я надеюсь, что вы поняли суть.

Перезапись метода

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

Место, где вы применяете MP, должно быть после загрузки и доступности оригинальной реализации. Как правило, вы должны стремиться применять свои изменения как можно ближе к цели, но имейте в виду, что реализация цели может со временем измениться. Что касается нашего примера, инициализация вместе с MP будет идти в файл main.js

Глядя на реализацию виджета, мы видим, что есть объект FeedbackBox, который служит корнем виджета. Позже функция toggleError была реализована в ее прототипе.

 function FeedbackBox(elem, options) { this.options = options; this.element = elem; this.isOpen = false; } FeedbackBox.prototype.toggleError = function(obj, isError) { ... } 

Поскольку JavaScript является динамическим языком и его объекты могут быть изменены во время выполнения, мы в конечном итоге просто заменим toggleError нашим пользовательским методом. Единственное, что нужно помнить, это сохранить подпись (имя и переданные аргументы) одинаковыми.

 FeedbackBox.prototype.toggleError = function(obj, isError) { if(isError) { obj.addClass("error"); } else { obj.removeClass("error"); } }; 

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

Дополнение метода

В предыдущем примере мы увидели, как переписать исходную реализацию, предоставив нашу собственную. С другой стороны, работа с консольными журналами должна, по сути, только отфильтровывать определенные вызовы и подавлять их. Ключом к успеху является проверка встраиваемого кода и попытка понять его рабочий процесс. Как правило, это делается путем запуска консоли разработчика в выбранном вами браузере и просмотра загруженных ресурсов, добавления точек останова и отладки частей целевого кода, чтобы понять, что он делает. На этот раз, однако, все, что вам нужно сделать, это открыть реализацию из примера Plunker под названием vendor / jquery.feedBackBox.js на другой вкладке.

Глядя на сообщения отладки, мы видим, что каждое из них начинается с FeedbackBox:. Таким образом, простой способ добиться того, чего мы хотим, это перехватить исходный вызов, проверить предоставленный текст для записи и вызвать оригинальный метод, только если он не содержит подсказки отладки.

Для этого сначала нужно сохранить исходный console.log в переменной для последующего использования. Затем мы снова переопределяем исходную с помощью нашей пользовательской реализации, которая сначала проверяет, имеет ли предоставленный text атрибута тип string и, если да, содержит ли он подстроку FeedbackBox: Если это так, мы просто ничего не будем делать, в противном случае мы выполним исходный консольный код, вызвав его метод apply .

Обратите внимание, что этот метод принимает контекст в качестве первого параметра, то есть объекта, для которого должен быть вызван метод, и переменную магических arguments . Последним является массив всех аргументов, первоначально переданных исходному вызову журнала консоли.

 var originalConsoleLog = console.log; console.log = function(text) { if (typeof text === "string" && text.indexOf("FeedbackBox:") === 0) { return; } originalConsoleLog.apply(console, arguments); } 

ПРИМЕЧАНИЕ. Вы можете удивиться, почему мы просто не перенаправили атрибут text . Ну, на самом деле console.log можно вызывать с бесконечными параметрами, которые в итоге будут объединены в один текстовый вывод. Таким образом, вместо того, чтобы определять их все, что может быть довольно сложно для бесконечных возможностей, мы просто отправляем все, что там происходит.

Перехват вызовов Ajax

И последнее, но не менее важное, давайте посмотрим, как мы можем решить проблему с Ad-сервером. Давайте снова посмотрим на функцию init виджета:

 $.ajax({ url: './vendor/ad-server.json', method: 'GET', success: function(data) { console.log(data); console.log("FeedbackBox: AdServer contacted"); } }); 

Первой идеей может быть открытие браузера и поиск того, как перезаписать плагин jQuery. В зависимости от того, насколько хороши ваши навыки поиска, вы можете или не можете найти подходящий ответ. Но давайте на минуту остановимся и подумаем о том, что на самом деле здесь происходит. Независимо от того, что делает jQuery со своим методом ajax , через некоторое время он в конечном итоге создаст собственный XMLHttpRequest.

Посмотрим, как это работает под капотом . Простейший пример, найденный на MDN , показывает нам это:

 var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (xhttp.readyState == 4 && xhttp.status == 200) { // Action to be performed when the document is read; } }; xhttp.open("GET", "filename", true); xhttp.send(); 

Мы видим, что создан новый экземпляр XMLHttpRequest . У него есть метод onreadystatechange о onreadystatechange мы на самом деле не заботимся, а также есть методы open и send . Отлично. Таким образом, идея состоит в том, чтобы обезопасить исправление метода send и сказать ему не выполнять вызовы по определенному URL.

 var originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function(data) { if ( URL DOES NOT POINT TO AD SERVER ) { return originalSend.apply(this, arguments); } return false; }; 

Что ж, получается, что вы не можете получить целевой URL от самого объекта. Дерьмо. Так что же нам делать? Мы делаем его доступным на объекте. Ища первый шанс получить URL, мы видим, что метод open принимает его как второй параметр. Для того, чтобы сделать URL доступным для самого объекта, давайте сначала MP метод open.

Как и прежде, мы будем хранить исходный метод open в переменной для дальнейшего использования. Затем мы перезаписываем оригинал нашей пользовательской реализацией. Поскольку у нас есть радость работы с JavaScript, который является динамическим языком, мы можем просто создать новое свойство на лету и назвать его _url , для которого устанавливается значение переданного аргумента.

 var originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url) { this._url = url; return originalOpen.apply(this, arguments); }; 

Кроме того, мы вызываем оригинальный метод open и больше ничего не делаем.

Возвращаясь к нашему отправляющему MP, теперь довольно очевидно, как выполнить проверку состояния. Вот модифицированная версия:

 var originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function(data) { if (this._url !== "./vendor/ad-server.json") { return originalSend.apply(this, arguments); } return false; }; 

Вывод

Здесь мы увидели краткое введение в использование Monkey Patching для изменения поведения кода во время выполнения. Но, более того, я надеюсь, что статья смогла дать вам представление о том, как вы должны подходить к проблеме с патчами обезьян. Хотя сам патч часто бывает довольно простым, важно, как и где настроить код во время выполнения.

Кроме того, я надеюсь, что независимо от того, как вы относитесь к Monkey Patching, у вас была возможность увидеть всю прелесть работы с динамическим языком, позволяющим динамически изменять даже собственные реализации во время выполнения.