Пишем простое Opera-расширение

    Эта статья посвящена написанию простого расширения для браузера Opera. Наше расширение будет примитивным, т.к. весь его функционал будет заключаться в user-JS для habrahabr.ru. Лента комментариев оснащена блоком, который отображает количество новых комментариев в топику и кнопку, позволяющую эту ленту обновить. Давайте добавим туда стрелки для навигации по новым комментариям.



    C чего начнём?


    • Создадим новую директорию для файлов расширения
    • В ней создадим файл config.xml

    Содержимое XML:
    <?xml version="1.0" encoding="UTF-8"?>
    <widget xmlns="http://www.w3.org/ns/widgets" id="http://faiwer.ru" version="0.9a" defaultlocale="en">
    	<name xml:lang="en">HabrCommentSwitcher</name>
    	<description xml:lang="en">Habrahabr. New comment switcher</description>
    	<description xml:lang="ru">Habrahabr. Переключение новых комментариев</description>
      	<author href="faiwer.ru" email="faiwer@gmail.com">Faiwer</author>
      	<icon src="icons/64x64.png"/>
      	<icon src="icons/48x48.png"/>
      	<icon src="icons/32x32.png"/>
    </widget>
    

    Так как эта статья обучающая, я не буду подробно описывать каждый пункт, с этим лучше меня справится документация. Остановимся на самом главном:
    • <name>Название нашего расширения</name>. Не стоит делать слишком длинным
    • <description>Краткое описание</description>. Хватит и пары строчек
    • <icon />. Иконки используются на странице расширений, на сайте-репозитории (если ваше расширение там примут), и в кнопке, которой в данном расширении не будет. Желательно вынести в отдельную директорию, дабы не создавать беспорядок

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

    В качестве последнего штриха нужно создать index.html. Он нужен для функционирования «закадрового» скрипта, который будет запущен вместе со стартом браузера, и не будет привязан ни к одной из вкладок. Нам он не нужен, но без него Opera не даст нам «подебажить». Файл можно оставить пустым. Теперь при помощи drag-n-drop перетаскиваем наш config.xml в браузер. Если всё прошло хорошо, откроется страница со списком установленных расширений, и наше там будет сверху, в разделе «режим разработчика».

    UserJS


    В начале следует определиться с тем, что же должно делать будущее расширение:
    • дожидаться окончания загрузки страницы и появления искомого блока (далее я буду называть его slider).
    • разместить в нём наши кнопки-стрелки.
    • разместить необходимый CSS-код для стрелок и выделения текущего комментария.
    • оживить стрелки — они должны перемещать скролл страницы по новым комментариям.

    Для всего этого достаточно 1-го файла, который будет исполняться для каждой habrahabr-страницы. Т.е. нам идеально подходит UserJS. Но если Chrome умеет преобразовывать UserJS в расширения сам, а Firefox-у для этого нужен Greasemonkey, то в случае Opera мы можем его оформить в виде расширения или установить вручную (F12 — Настройки для сайта — Скрипты).

    Создадим директорию includes, Opera будет искать «внедрённые» скрипты именно там. В ней создадим файл habr_comment_switcher.js (тут название можно выбрать любое). В начало файла поместим:
    // ==UserScript==
    // @include http://habrahabr.ru/*
    // ==/UserScript==
    

    Это не просто js-комментарии, это специальная разметка для UserJS, которая в нашем случае объясняет опере, что сий внедряемый файл должен запускаться только на habrahabr.ru.

    JavaScript


    Помимо этого файла мы могли бы внедрить ещё и какую-либо библиотеку вроде jQuery или Prototype. Но я строго не рекомендую так поступать. Такого рода библиотеки весьма весомые, а т.к. они будут загружаться не только для каждой вкладки, а ещё и для каждого iframe, которых на странице бывает много, 5-10 таких расширений могут вызвать тормоза. Учитывая что наши задачи весьма скромны, мы не сильно много теряем.

    UPD 2. Спасибо, kns. Если же на странице уже используется 1 из популярных библиотек, то мы можем воспользоваться ею. Подробнее об этом можно прочесть здесь. В случае Opera это будет выглядеть примерно так:
    var $ = window.jQuery;


    Начнём писать код. Для начала, для удобства, поместим всё в анон.функцию. Т.к. наше javascript-окружение изолировано от javascript-окружения сайта, этого делать не обязательно, но на мой взгляд, подобное уже давно стало правилом хорошего тона:
    +function( w )
    {
    }( window );
    

    Не знаю как вы, но я привык работать в пределах конкретного объекта, а посему определим его:
    	var Engine = function(){ this._init(); }
    	Engine.prototype =
    	{
    		_init: function()
    		{
    		}
    	}
    

    Теперь необходимо оформить условия его создания:
    	if( w.location.href.indexOf( 'habrahabr.ru' ) > 0 )
    	{
    		var engine = false;
    		d.addEventListener( 'DOMContentLoaded', function()
    		{
    			setTimeout( function(){ engine = new Engine(); }, 1500 );
    		}, false );
    	}
    

    Проверка на адрес страницы вызвана не логическими доводами, а паранойей. Дело в том, что я пару раз натыкался в сети на сведения о том, что Opera иногда не справляется с правилами для UserJS. Наш объект будет запущен после того, как всё DOM-древо страницы будет построено + 1.5 сек. Почему 1.5 сек.? Дело в том, что slider появляется не сразу, поэтому мы его подождём. Сие можно реализовать более изящно, но пока сойдёт и это.

    Работа расширения


    Сейчас Opera не самый высоко-технологичный браузер, но всё же его возможности намного опередили IE6,7,8. Следовательно мы можем воспользоваться такими вещами, которые не стали бы применять в обычном web-программировании. Немного упростим себе работу:
        var d = w.document,
            $ = d.querySelector.bind( d ),
            $$ = d.querySelectorAll.bind( d )
    

    Методы querySelector и querySelectorAll позволяют находить DOM-объекты по CSS-селекторам. Такой подход вам наверняка знаком по опыту использования jQuery. В нашем случае функция $ будет искать один элемент, удовлетворяющий запросу, а $$ список.

    Что там у нас по списку? Да не важно, давайте внедрим на страницу нужный нам CSS:
    _cssInject: function()
    {
    	var style = this._createElem( this.elem.style_inject ),
    		text = '';
    	for( var i = 0, n = this.css.length; i < n; ++ i )
    	{
    		text += this.css[ i ];
    	}
    	style.innerHTML = text;
    	d.head.appendChild( style );
    }
    

    Здесь мы создаём новый DOM-объект <style /> и в качестве содержимого задаём необходимый CSS-код. Т.к. страница уже готова нам доступен document.head, куда мы и поместим наш тег. Теперь о функции _createElem:
    		_createElem: function( data )
    		{
    			var item = d.createElement( data.tagName );
    
    			if( data.attr )
    			{
    				for( var rule in data.attr )
    				{
    					item.setAttribute( rule, data.attr[ rule ] );
    				}
    			}
    
    			return item;
    		},
    

    Организовать работу с настройками можно как угодно, например так:
    _initConst: function()
    {
    	this._extend( this,
    	{
    		css: [
    			'.__hcsc_button { border-top: 1px solid white; line-height: 22px; height: 22px; ' +
    			'cursor: pointer; }',
    			'.__hcsc_button:hover { color: white; }',
    			'.info.__hcsc_active { outline: 2px solid #222; }',
    		],
    		elem:
    		{
    			style_inject: {
    				tagName: 'style',
    				attr: { id: '__habr_comment_switcher_css' }
    			}
    		}
    	} );
    },
    
    _extend: function( object, extend )
    {
    	for( var name in extend ) if( extend.hasOwnProperty( name ) )
    	{
    		object[ name ] = extend[ name ];
    	}
    },
    

    Перейдём к основной логике. Нам нужно найти slider и добавить к нему две кнопки:
    _prepareSlider: function()
    {
    	var slider = $( this.s.slider );
    	if( !slider )
    	{
    		return;
    	}
    
    	this.up_button = this._createElem( this.elem.button );
    	this.up_button.innerHTML = '▲';
    	slider.appendChild( this.up_button );
    
    	this.down_button = this._createElem( this.elem.button );
    	this.down_button.innerHTML = '▼';
    	slider.appendChild( this.down_button );		
    },
    

    Стрелки можно задать текстом. Теперь нам нужно эти кнопки оживить:
    _observe: function()
    {
    	this.up_button.addEventListener( 'click', this._slideClick.bind( this, -1 ), false );
    	this.down_button.addEventListener( 'click', this._slideClick.bind( this, +1 ), false );
    },
    

    И наконец, долгожданная листалка:
    _checkItems: function()
    {
    	var items = $$( this.s.info_panel );
    
    	if( !this.items || !this.items.length || !items.length || ( this.items[ 0 ] !== items[ 0 ] ) )
    	{
    		this.position = -1;
    		this.items = items;
    	}
    
    	return this.items;
    },
    
    _slideClick: function( diff )
    {
    	if( this.current )
    	{
    		this.current.classList.remove( this.c.active );
    	}
    
    	if( !this._checkItems().length )
    	{
    		return;
    	}
    
    	this.position += diff;
    	if( this.position < 0 )
    	{
    		this.position = this.items.length - 1;
    	}
    	else if( this.position >= this.items.length )
    	{
    		this.position = 0;
    	}
    
    	this.current = this.items[ this.position ];
    	this.current.scrollIntoView( true );
    	this.current.classList.add( this.c.active );
    }
    

    Её логика проста. Ищем все новые комментарии по CSS-селектору, заданному в this.s.info_panel ( ".comment_item > .info.is_new" ). Он находит нам все блоки-заголовки новых комментариев. Затем, в зависимости от того, на какую кнопку мы нажали, перемещаем скролл страницы к нужному комментарию, используя scrollIntoView. Чтобы сие событие было более наглядным, добавляем к нему класс, для которого выше определили CSS с тёмной рамкой (outline).

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

    Отдельно я хотел бы остановится на функциях работы с классом DOM-объекта. Нет нужны вручную парсить строчку item.className, т.к. доступны следующие методы:
    this.current.classList.add( 'my_class' );
    this.current.classList.remove( 'my_class' );
    


    Немного о «дебаге»


    Начнём с того, что у нас есть такой инструмент как Dragonfly(стрекоза), который вызывается через ctrl+shift+i (либо правая кнопка мыши — «проинспектировать объект»). В нём на вкладке «Скрипты» мы можем отыскать в выпадающем списке наш habr_comment_switcher.js. Теперь нам доступны точки останова и «трейсинг» (F8, F10, F11). Также нам доступна консоль, но чтобы она работала в том же js-окружении, что и наш скрипт, нам нужно предварительно посмотреть его номер в выпад.списке скриптов.

    Посмотреть ошибки можно путём нажатия кнопки «открыть консоль ошибок» на странице установленных расширений. Объект console для расширений не работает. Чтобы обновить расширение, нам нужно закрыть стрекозу, на странице расширений нажать «обновить», открыть стрекозу на нужной вкладке и нажать в браузере «обновить». В целом, впечатления от работы с расширением в стрекозе самые ужасные. Особенно после опыта разработки расширения для Chrome.

    Финальный штрих


    Вроде всё работает, так что самое время упаковать расширение. Для этого сожмём содержимое папки расширения в zip-архив, и сменим расширение файла на oex. Всё, расширение готово. Можно пользоваться. Если Opera ругается на то, что расширение повреждено, проверьте — возможно вы сжали не содержимое папки, а её саму. Так же проверьте наличие файлов config.xml и index.html.
    image

    Эпилог


    УРА! Наше расширение готово, в стадии альфа-версии. Его можно улучшить, добавить поддержку Chrome и Greasemonkey (хотя я не уверен, что не взлетит так), добавить страницу настроек (к примеру, чтобы задавать цвета или изменять CSS-селекторы).

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

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 12

      +1
      Молодцы!
        +1
        Я возможно что-то не понял, но разве, если кликнуть на цифру количества коментов, не перейдем к следующему? Или это критично назад возвращаться? Если делать, так делать уже что-то полезное.
          0
          Честно говоря, я и понятия не имел об этом, спасибо. Касательно полезного — ну статья про расширение получилась :)
          +2
          > а почему бы нам не воспользоваться уже используемой на сайте, к примеру, jQuery. Ответ — да с радостью, вот только никто не позволит
          Ну, вообще, вполне позволяет. Просто придется обращаться к jQuery через window.jQuery. И, надеюсь, это не перекроют.
            +1
            Точно, оставили, спасибо, был не прав. В Chrome мне такого добиться не удавалось, лезли разного рода ошибки доступа.
              +1
              В соседнем топике пишут, что в Хроме через финт ушами тоже можно рулить глобальными объектами.
                0
                Хитро. Таким образом, получается, что все эти предмурости в хроме, необходимы для того, чтобы user-скрипт случайно не засорил js-окружение страницы своими переменными. Жаль что я не смог нагуглить это полгода ранее :)
                  0
                  Подправил эту часть статьи.
              +1
              а почему бы нам не воспользоваться уже используемой на сайте, к примеру, jQuery. Ответ — да с радостью, вот только никто не позволит, т.к. в противном случае наше расширение могло бы влезть в логику работы сайта и сделать что-нибудь нехорошее (например увести ваши пароли). Ранее мне удавалось обойти этот запрет в opera, и я надеюсь эту «дыру» уже залатали.

              Странное суждение, как будто вы не знакомы с юзерскриптами вообще. Любой юзерскрипт (даже в Хроме) способен с помощью написанного своего кода вмешаться в логику работы приложения, если его допустили в домен. Этого не может никто не позволить, только пользователь может не установить скрипт. То же относится к любым расширениям (с соответствующим допуском в домен).

              Но если Chrome умеет преобразовывать UserJS в расширения сам, а Firefox-у для этого нужен Greasemonkey, то в случае Opera нам нужно его оформить в виде расширения (чем мы уже и занялись).

              Вы разве не знаете, что *user.js тоже поддерживаются Оперой? Все 2 спсосба.

              Его можно улучшить, добавить поддержку Chrome и Greasemonkey

              Простите, ещё один признак некомпетентности. Если про Chrome можно подумать, что вы предлагаете расширение сделать совместимым по формату с аддоном Хрома (это возможно), то как понимать совместимость с GreaseMonkey? Это — аддон для Firefox, позволяющий запускать юзерскрипты (а не аддоны). Юзерскрипт не упаковывается в zip-архив и представляет собой единственный скрипт формата .js, без манифестов, без рисунков и HTML. Вы утверждаете, что возможно поддержать аддон Оперы в GreaseMonkey?

              Ладно, допустим, вы написали своё первое расширение для Оперы. Но как можно учить других, самому не разбираясь в происходящем? Написали бы как есть: я только учусь, я сделал это и это, но зачем блистать эрудицией в том, чего не касались? Таких самоуверенных утверждений хватает в личных бложиках, которые читают гораздо меньше людей, но не надо, пожалуйста, их выносить на Хабр, это вводит массу людей в заблуждения.

              мы могли бы внедрить ещё и какую-либо библиотеку вроде jQuery или Prototype. Но я строго не рекомендую так поступать. Такого рода библиотеки весьма весомые, а т.к. они будут загружаться не только для каждой вкладки, а ещё и для каждого iframe, которых на странице бывает много, 5-10 таких расширений могут вызвать тормоза, а то и вовсе заставить зависнуть браузер пользователя. Учитывая что наши задачи весьма скромны, мы не сильно много теряем.
              Вот после этого вызывают недоверие любые подобные утверждения. Вы действительно проверяли, что скрипты аддонов Опера не кеширует, а копирует в каждое окно? Даже если копирует, то в 50 окнах и фреймах копии jQuery займут 5 мегабайт. И они «могут вызвать тормоза»? Они будут чем-то отличаться от 50 копий настоящих скриптов с jQuery?

              (Об ответе догадываюсь, можно не отвечать, это к примеру.)
                0
                Вы разве не знаете, что *user.js тоже поддерживаются Оперой? Все 2 спсосба.

                Отчего же, знаю оба. Оба достаточно мудрёные. Это что-то меняет? Способ с расширением, в теории, способен к автообновлению и прочим плюшкам.
                Любой юзерскрипт (даже в Хроме) способен с помощью написанного своего кода вмешаться в логику работы приложения, если его допустили в домен

                Вы имеете ввиду то, что расширение может выполнять произвольные запросы к сайту? Или доступ к js-окружению тоже?
                Юзерскрипт не упаковывается в zip-архив и представляет собой единственный скрипт формата .js, без манифестов, без рисунков и HTML. Вы утверждаете, что возможно поддержать аддон Оперы в GreaseMonkey?

                У нас есть список файлов. В случае GreaseMonkey используем не oex, а только js и все дела. Касательно того взлетит ли конкретно этот файл в GM, я не знаю. Но в целом с GreaseMonkey уже сталкивался, и вроде особых сложностей там не встречал.
                Вы действительно проверяли, что скрипты аддонов Опера не кеширует, а копирует в каждое окно?

                У меня есть расширение, позволяющее внедрять произвольный CSS код на любые страницы. Оно загружается всякий раз для каждой страницы и iframe-а. В случае если там будет jQuery, и опера закеширует уже пропарсенный js-код, выполнять её его придётся всё равно каждый раз заного. Учитывая как в последнее время располнела jQuery…
                Простите, ещё один признак некомпетентности
                Но как можно учить других, самому не разбираясь в происходящем?

                А можно немного меньше напыщенности? Я ошибся с возможностью использования js-окружения сайта (спасибо kns), на меня так повлиял Chrome и некоторые эксперименты с ним, но в остальном я разве ошибся? Статья излагает какие-либо вредные идеи?
                  0
                  Отчего же, знаю оба. Оба достаточно мудрёные. Это что-то меняет?

                  Конечно. Вы не должны были писать слова «нам нужно», которые в цитате я выделил красным. Они говорят, что иного способа, чем писать аддон для Оперы, нет.
                  Способ с расширением, в теории, способен к автообновлению и ...

                  Никто не спорит, вопрос в другом, в формулировке, тем более, если Вы всё знаете.
                  Кстати, к автообновлению не способен никакой ни юзерскрипт, ни аддон ни в одном браузере. Везде требуется подтверждение пользователя на установку и скрипта, и аддона. (Что-то Вы не в ударе сегодня с формулировками :). ) Разница только в количестве кликов у Оперы. А вот автопроверку обновления (наверное, Вы про неё хотели сказать) способны делать все, в том числе юзерскрипты в любом браузере. Но это тоже к делу не относится.

                  Вы имеете ввиду то, что расширение может выполнять произвольные запросы к сайту? Или доступ к js-окружению тоже?
                  Произвольные действия на клиенте (и запросы в частности). После своего момента запуска, конечно. Про JS-окружение я ничего не говорил — известно, что даже рядом написанные нативные скрипты не могут получить доступ к соседям в их локальные скоупы. Но всё, что способен сделать обычный скрипт (написанный не выше точки запуска юзерскрипта), то способен и любой юзерскрипт или аддон.
                  В случае GreaseMonkey используем не oex, а только js и все дела
                  Но в том и дело, что оех — это не только js. Кое-что (работа бекграунда Оперы, например) юзерскрипт не сумеет. Если речь о конкретном аддоне, то да, он может быть переписан как юзерскрипт, но не каждый.
                  В случае если там будет jQuery, и опера закеширует уже пропарсенный js-код, выполнять её его придётся всё равно каждый раз заного.

                  Ну это очень небольшое время — инициализация библиотеки. И ею пользуются, не беспокоясь, на каждой странице многих сайтов. Даже удвоение инициализаций не страшно, а при желании — в Опере/Fx/Safari легко обойти, спросив if(!window.jQuery) (далее подгрузка файла)
                  А можно немного меньше напыщенности? Статья излагает какие-либо вредные идеи?
                  Соответственно Вашим утверждениям. Все 4 цитаты содержат ошибки. И я не анализировал все утверждения в статье, хватило и этих.
                  Вот ещё:
                  Начнём писать код. Для начала, для удобства, поместим всё в анон.функцию. Т.к. наше javascript-окружение изолировано от javascript-окружения сайта, этого делать не обязательно, но на мой взгляд, подобное уже давно стало правилом хорошего тона:
                  Тут для скриптов аддонов сказать уверенно не могу, что они изолированы; а в юзерскриптах Оперы такая обёртка была бы вызвана необходимостью (а не мифическим «правилом хорошего тона»), потому что Опера — единственный (из основных) браузер, который не разделяет окружения юзерскриптов и скриптов страницы.
                    0
                    В связи с размерами наших с вами комментариев — перенёс дискуссию в личные сообщения. Почти во всём я с вами не согласен.
                    P.S. минусовал не я.

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

              Самое читаемое