Увидеть скрытое или как хорошее сделать еще лучше

в 9:24, , рубрики: Extensions, javascript, opera, userjs, браузеры, расширения, метки: , , , , ,

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

Периодически я вспоминал об этой идее, но беглый взгляд в гугл не давал ничего интересного. До недавнего времени.

Когда в январе мне снова захотелось посмотреть, а не появилось ли чего интересного на эту тему, я нашел прекрасный букмарклет Visual Event 2, написанный Allan Jardine. Букмарклет работал как часы, но нашлась и пара ложек дегтя, маленьких, но неприятных.

Я использую Оперу и все букмарклеты на панели инструментов имеют одинаковую иконку: Иконка букмарклета. Стоит добавить пару-тройку таких закладок и ориентироваться среди них становиться весьма некомфортно. Второй неприятный момент — букмарклет грузил весь свой код с сайта разработчика, что приводило пусть к небольшим, но задержкам.

Поэтому было решено оформить букмарклет в виде расширения для браузера Opera. К тому же я давно хотел познакомиться с Opera Extension API. Вот ссылка на страницу расширения для самых нетерпеливых. Остальных же прошу под кат, где описаны все этапы конвертации букмарклета в расширение для Оперы, проблемы, с которыми я столкнулся и методы их решения.

Ради чего все это

Кнопка расширения
Для начала хочу вас познакомить с тем, что же в итоге получилось. После установки расширения
Visual Event на панели инструментов Opera появится новая кнопка. С ее помощью можно включать и выключать режим VisualEvent.

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

После активации кнопки открытая HTML-страница будет затемнена и все элементы, на которые повешены какие-либо обработчики событий подсветятся разноцветными прямоугольниками:
Режим VisualEvent

Подведя курсор к любому из прямоугольников вы увидите код функции-обработчика, имя файла и строку, в которой она была объявлена:
Вывод кода функци-обработчика

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

Разбираем букмарклет

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

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

(function() {
    var url = 'http://example.com/VisualEvent/builds/VisualEvent_Loader.js';
    
    if( typeof VisualEvent!='undefined' ) {
        if ( VisualEvent.instance !== null ) {
            VisualEvent.close();
        }
        else {
            new VisualEvent();
        }
    }
    else {
        var n=document.createElement('script');
        n.setAttribute('language','JavaScript');
        n.setAttribute('src',url+'?rand='+new Date().getTime());
        document.body.appendChild(n);
    }
})();

Переменная url содержит адрес скрипта-загрузчика, который обеспечивает активацию всей логики. Из листинга видно, что код проверяет наличие объекта VisualEvent и, если он не обнаружен, инициирует его загрузку. Если же объект обнаружен то проверяется, активирован ли режим VisualEvent или нет. И, в зависимости от результатов проверки, либо выключает его либо включает.

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

Весь код букмарклета распределен по трем файлам:

  • VisualEvent_Loader.js
  • VisualEvent.js
  • VisualEvent-jQuery.js

VisualEvent_Loader.js загружается первым. он снова проверяет был ли загружен VisualEvent прежде и, если это имело место, активирует либо деактивирует режим отладки. В случае же, если VisualEvent ранее не загружался, то проверяет, использует ли страница jQuery. Если jQuery используется то грузится файл VisualEvent.js с кодом самого букмарклета. Если же jQuery не загружен, то грузится файл VisualEvent-jQuery.js, содержащий как код VisualEvent, так и код библиотеки jQuery.

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

Дополнительно к JS-файлам букмарклет использует собственную таблицу стилей из внешнего файла VisualEvent.css.

Делаем кнопку на панели

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

Как создать базовую структуру расширения, как заполнить файл описания config.xml и как добавить кнопку на тулбар я рассказывать не буду, все это есть в официальном руководстве, да и на Хабре описывалось.

Благополучно создав файл config.xml, найдя иконку и добавив кнопку на тулбар я перешел к следующему этапу — адаптации скриптов для работы в режиме расширения. И вот тут меня ждал первый сюрприз. Оказывается код, обрабатывающий нажатие кнопки на тулбаре не имеет доступа к странице, над которой он был нажат. Вначале я был обескуражен подобным поведением и даже попытался найти какой-нибудь обходной путь, но после пары экспериментов я понял, что влоб эту проблему не решить.

И тут я вспомнил о замечательном расширении cleanPages, которое по нажатию кнопки на тулбаре выделяет из страницы только значимый контент и оформляет его в виде, удобном для чтения. Т.е. по нажатию на кнопку что-то делается со страницей, подгружаются скрипты, модифицируется код и т.д. А это как раз то, что нам нужно!

Лирическое отступление Файл расширения, фактически представляет собой ZIP-архив со всеми используемыми скриптами, таблицами стилей и картинками. Следовательно, имея файл расширения совсем не сложно разархивировать и посмотреть как оно внутри устроено.

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

В результате, функция обработки нажатия кнопки приняла вид:

function buttonClicked() {
    var tab = opera.extension.tabs.getFocused();
    if ( tab ) {
        opera.extension.broadcastMessage(
            JSON.stringify(opera.extension.tabs.getFocused().url)+ '|' + 
            'VisualEvent-1.0');
   }
};

Где вызов opera.extension.broadcastMessage() как раз и шлет широковещательное сообщение. А background-скрипт получил такую логику:

opera.extension.onmessage = function(e) {
    var dataArray = e.data.split('|');
    if ( window.top == window.self && e.data && JSON.parse(dataArray[0]) == 
         window.decodeURI(window.location.href.replace(window.location.hash, '')) ) {
        if ( 'VisualEvent-1.0' == dataArray[1] ) {
           enableVisualEvent();
       }
   }
};

Здесь видно, что после получения сообщения проверяется его формат и URL страницы, на которой кнопка была нажата. Да проверяется именно URL, поэтому, если у вас открыто несколько одинаковых вкладок, то VisualEvent будет активироваться на всех одновременно.

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

Пишем скрипт для обработки страницы

Мы уже начали писать скрипт для обработки страницы и добавили в него обработчик сообщений от кнопки тулбара. И этот скрипт вызывает функцию enableVisualEvent(). В эту функцию я поместил код букмарклета с той лишь разницей, что он вместо подгрузки файла лоадера просто вызовет его код, оформленный в виде отдельной функции.

Дело в том, что я не смог найти способа подключить из бекгаунд-скрипта другой JavaScript-файл, относящйся к тому же расширению, поэтому пришлось код каждого из трех исходных файлов букмарклета (VisualEvent, VisualEvent_loader и jQuery) оформить в виде отдельной функции и включить в единый бекграунд-скрипт.

Адаптируем JavaScript

Как сказано в руководстве по конвертации UserJS в расшинения.

Sometimes the UserJS doesn't work as an extension, for various reasons. (Иногда UserJS не работает в режиме расширения, по разным причинам)

И это верно, по крайней мере у меня не заработало :). Но то же руководство содержит и рецепты как с этим безобразием бороться. Основной причиной является то, что расширение работает в своей песочнице и доступ к глобальной области видимости страницы имеет только через явное указание объекта window. Т.е. обращение к jQuery как к «$» или «jQuery» работать не будет, вместо этого следует использовать «window.$» или «window.jQuery». И такие исправления необходимо сделать для всех глобальных переменных, используемых в скриптах.

После того, как все замены были сделаны расширение полностью заработало. Но таблицы стилей не подключились.

Подключаем CSS

Исходный код использовал собственную таблицу стилей и подгружал ее из внешнего файла. Как я уже говорил, добиться подключения к странице дополнительных файлов из каталога расширения не удалось.В качестве обходного пути я просто включил весь код CSS в бекграунд-скрипт и при первой инициализации создаю в <head> тег <style>, прописывая в него все CSS-правила:

function initCss() {
   var css = '<Много CSS-кода>';
   var n = document.createElement('style');
   n.type = 'text/css';
   var rules = document.createTextNode(css);

   if ( n.styleSheet ) {
       n.styleSheet.cssText = rules.nodeValue;
   }
   else {
       n.appendChild(rules);
   }
   document.getElementsByTagName('head')[0].appendChild(n);
};

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

Выводы

Спасибо всем, кто дочитал! :)

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

Кстати, пользователи Chrome могут найти аналогичное расширение и для него.

Автор: zenon

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js