Индикаторы ключевой информации на сайтах для Firefox на скорую руку

в 23:58, , рубрики: ajax, Firefox, javascript, xhr, xmlhttprequest, метки: , , , ,

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

Большинство пользователей Firefox наверняка знакомы с расширениями, сигнализирующими об обновлениях на популярных сетевых ресурсах (почте, новостном агрегаторе, социальной сети, сайте погоды и так далее). Но на все сайты и на все типы информации расширений не напасёшься. К счастью, если вы немного знакомы с JavaScript, некоторое упрощённое и гибкое подобие таких расширений можно создавать в считанные минуты при помощи расширения-посредника: Custom Buttons. Оно позволяет быстро создавать кнопки для панелей инструментов, которые выполняют при нажатии произвольный код или могут инициализировать определённое поведение с запуском браузера. Причём код они могут выполнять как в контексте страницы (чем напоминают пользовательские скрипты), так и контексте браузера (чем уподобляются полноценным расширениям).

В цели данной статьи не входит объяснение работы с Custom Buttons: по ссылке можно найти достаточно ссылок на документацию и форумы. Впрочем, всё устроено довольно просто: через контекстное меню панели инструментов создаётся кнопка, в диалоге новой кнопки вводится код, выбираются некоторые дополнительные плюшки, потом кнопка перетаскивается на любую удобную панель инструментов — и вот она уже готова к работе (кстати, расширение вводит дополнительный протокол custombutton:// — он позволяет с лёгкостью публиковать кнопки в сети и делится ими тем же способом, как мы используем обычные ссылки или букмарклеты).

Я же попробую поделиться некоторыми рецептами кода, посвящёнными упомянутым индикаторам. Будем двигаться от простого к более сложному. Код примеров должен работать в Firefox, начиная с 12-й версии (впрочем, строчки с установкой xhr.mozBackgroundRequest могут вызывать ошибку на Firefox 13, тогда их нужно закомментировать до лучших времён). Осознаю, что иные элементы кода могут показаться разным людям по-разному дурацкими, но примеры и не претендуют на безупречность: я только делюсь бытовым опытом при помощи любительского кода и искренне прошу прощения, что не могу этого сделать лучше.

1. Простой запрос информации.

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

Для примера возьмём не слишком заманчивый, но простой и яркий случай. Затребуем текущее количество статей в английской Википедии (которое сейчас приближается к четырём миллионам). Для этого в самую первую вкладку диалога для создания кнопки вставим следующий код (вкладка так и называется — «Код»):

(function(){
	var pageURL = "http://en.wikipedia.org/wiki/Main_Page";
	var keyElementXPath = "/html/body//div[@id='articlecount']";

	var imgThrobber = "data:image/gif;base64,R0lGODlhEAAQAOMIAAAAABoaGjMzM0xMTGZmZoCAgJmZmbKysv///////////////////////////////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgAIACwAAAAAEAAQAAAESBDJiQCgmFqbZwjVhhwH9n3hSJbeSa1sm5GUIHSTYSC2jeu63q0D3PlwCB1lMMgUChgmk/J8LqUIAgFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UKgmFqbpxDV9gAA9n3hSJbeSa1sm5HUMHTTcTy2jeu63q0D3PlwDx2FQMgYDBgmk/J8LqWPQuFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YSgmFqb5xjV9gQB9n3hSJbeSa1sm5EUQXQTADy2jeu63q0D3PlwDx2lUMgcDhgmk/J8LqUPg+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+cagmFqbJyHV9ggC9n3hSJbeSa1sm5FUUXRTEDy2jeu63q0D3PlwDx3FYMgAABgmk/J8LqWPw+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+QihmFqbZynV9gwD9n3hSJbeSa1sm5GUYXSTIDy2jeu63q0D3PlwDx3lcMgEAhgmk/J8LqUPAOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UqhmFqbpzHV9hAE9n3hSJbeSa1sm5HUcXTTMDy2jeu63q0D3PlwDx0FAMgIBBgmk/J8LqWPQOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YyhmFqb5znV9hQF9n3hSJbeSa1sm5EUAHQTQTy2jeu63q0D3PlwDx0lEMgMBhgmk/J8LqUPgeBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+c6hmFqbJwDV9hgG9n3hSJbeSa1sm5FUEHRTUTy2jeu63q0D3PlwDx1FIMgQCBgmk/J8LqWPweBRhV6z2q0VF94iJ9pOBAA7";
	var imgMain = "data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAEAgQAhIOEAMjHyABIR0gA6ejpAGlqaQCpqKkAKCgoAPz9/AAZGBkAmJiYANjZ2ABXWFcAent6ALm6uQA8OjwAiIiIiIiIiIiIiI4oiL6IiIiIgzuIV4iIiIhndo53KIiIiB/WvXoYiIiIfEZfWBSIiIEGi/foqoiIgzuL84i9iIjpGIoMiEHoiMkos3FojmiLlUipYliEWIF+iDe0GoRa7D6GPbjcu1yIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";

	var btn = arguments[0];
	
	function getDoc(pageURL, pureXHR) {
		btn.image = imgThrobber;
		var xhr = new XMLHttpRequest();
		xhr.open("GET", pageURL, true);
		xhr.mozBackgroundRequest = true;
		xhr.timeout = 3000;
		xhr.channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
		if (pureXHR) {
			xhr.responseType = "document";
			xhr.onload = function() {
				btn.image = imgMain;
				processDoc(this.responseXML);
			}
			xhr.ontimeout = function() {
				getDoc(pageURL, false);
			}
		}
		else {
			xhr.onload = function() {
				btn.image = imgMain;
				processDoc((new DOMParser()).parseFromString(this.responseText, "text/html"));
			}
			xhr.ontimeout = function() {
				btn.image = imgMain;
				alert("Timeout");
			}
		}
		xhr.onerror = function() {
			btn.image = imgMain;
			alert("HTTP error");
		}
		xhr.send(null);
	}

	function processDoc(doc) {
		var keyElement = doc.evaluate(keyElementXPath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
		if (keyElement) {
			alert(keyElement.textContent);
		}
		else {
			alert("Parsing error");
		}
	}

	getDoc(pageURL, true);
})(this)

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

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

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

XPath ключевого элемента очень легко установить при помощи расширения
XPather: он не только встраивается в известный DOM Inspector, но и добавляет свою позицию в контекстное меню, позволяющую любой элемент на странице открыть в окошке этого расширения и получить его XPath. Расширение выдаёт полный адрес со всеми звеньями и порой его можно сократить, опираясь, например, на id элементов. В нашем примере мы как раз сократили адрес (конечно, когда у ключевого элемента есть id, можно и не использовать XPath, но тут мы исходим из универсальности кода и применяем способ, годный для всех случаев).

Формируя XPath, нужно помнить, что XHR не исполняет скрипты загружаемой страницы. Поэтому в процессе создания кода анализировать нужно страницу с отключённым JavaScript или же первичный html. Бывает так, что ключевой элемент формируется скриптом: тогда наш способ не годится и нужно применять более сложный подход со скрытыми фоновыми браузерами, чего мы в этой статье касаться не будем. Впрочем, такие случаи не так уж часты.

Окошко создания кнопки имеет удобный инструмент создания data:-адресов для встраивания ресурсов, можно легко преобразовывать адреса иконок сайтов в base64 строки. Конечно, ничто не мешает указать URL, просто таким образом мы создаём полностью автономный код и уменьшаем сетевые и локальные запросы.

Далее мы назначаем переменную для переданного в аргументе элемента кнопки. И переходим к двум подфункциям: одна из них будет получать код документа, вторая разбирать его и выдавать нужную информацию.

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

Надеясь на лучшее, мы используем xhr.responseType = «document», чтобы сразу получать доступ к DOM. Однако при таком подходе можно напороться на ошибку, вызываемую нечастым стечением условий и приводящую к псевдотаймаутам (см. обсуждение со ссылками на багзиллу). Поэтому мы вставляем страховочный код: при первом таймауте мы пробуем запросить код страницы повторно и разобрать его при помощи DOMParser — это немного сложнее и расточительнее (DOMParser почему-то вызывает загрузку некоторых добавочных ресурсов, хотя по идее должен только разбирать код), зато надёжнее до тех пор, пока ошибку не исправят (возможно, к 16-му релизу).

В итоге мы имеем несколько исходов запроса: первый успешный вызов (с аргументом pureXHR == true) с непосредственным получением DOM ведёт к вызову функции разбора документа, первый таймаут ведёт к рекурсивному повторному запросу чистого кода (с аргументом pureXHR == false) с последующим преобразованием в DOM и опять-таки вызовом функции разбора документа, второй таймаут или ошибка связи ведут к соответствующим сообщениям о неудаче.

Подфункция разбора документа довольно проста. Сначала мы пытаемся получить ключевой элемент. Если элемент найден, мы выдаём нужную информацию. Если элемента нет, мы подозреваем, что сайт отдал какую-то не такую страницу и сообщаем об ошибке. Причины могут быть разные: редирект (при этом XHR получает пустой документ), слетела авторизация и сайт выдал другую страницу для неавторизованных пользователей, разработчики сменили структуру DOM — или что-нибудь ещё. Тогда нужно загрузить страницу в браузере, проверить причину сбоя (в том числе при помощи расширений, исследующих HTTP-активность) и внести нужные корректировки.

Таков простой способ экономного запроса информации на 50 строк кода с мелочью. Теперь попробуем немного обогатить инструмент.

2. Индикаторы новостей с проверками по таймеру.

Хотелось бы, чтобы кнопка сама через промежутки времени запрашивала информацию и отображала её статус: есть новости — нет новостей — ошибка. Применять такую кнопку можно, например, к форумам, которые не предоставляют почтовых оповещений о новых темах, комментариях, личных сообщениях и тому подобных событиях. Однако добавляют на страницы ясные знаки, что подобные новости появились (это может быть особый элемент или смена атрибутов элемента).

Как пример возьмьём личные сообщения на сайте rutracker.org. Расширение будет проверять страницу личных сообщений (можно и любую другую, нас будет интересовать общая для большинства страниц шапка; просто страница личных сообщений может быть наиболее экономным вариантом), искать нужный элемент, проверять его свойства и сообщать о статусе сменой иконки на кнопке и вдобавок всплывающей подсказкой (можно добавлять звуковое оповещение или всплывающее окошко над треем, но в этой статье мы о таких сложностях говорить не будем).

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

(function(){
	var pageURL = "http://pm.rutracker.org/forum/privmsg.php?folder=inbox";
	var keyElementXPath = "/html/body/div[@id='body_container']/div[@id='page_container']/div[@id='page_header']/div[@id='main-nav']/table/tbody/tr/td[2]/a";
	var delay = 10 * 1000;
	var interval = 60 * 60000;

	var imgThrobber = "data:image/gif;base64,R0lGODlhEAAQAOMIAAAAABoaGjMzM0xMTGZmZoCAgJmZmbKysv///////////////////////////////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgAIACwAAAAAEAAQAAAESBDJiQCgmFqbZwjVhhwH9n3hSJbeSa1sm5GUIHSTYSC2jeu63q0D3PlwCB1lMMgUChgmk/J8LqUIAgFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UKgmFqbpxDV9gAA9n3hSJbeSa1sm5HUMHTTcTy2jeu63q0D3PlwDx2FQMgYDBgmk/J8LqWPQuFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YSgmFqb5xjV9gQB9n3hSJbeSa1sm5EUQXQTADy2jeu63q0D3PlwDx2lUMgcDhgmk/J8LqUPg+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+cagmFqbJyHV9ggC9n3hSJbeSa1sm5FUUXRTEDy2jeu63q0D3PlwDx3FYMgAABgmk/J8LqWPw+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+QihmFqbZynV9gwD9n3hSJbeSa1sm5GUYXSTIDy2jeu63q0D3PlwDx3lcMgEAhgmk/J8LqUPAOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UqhmFqbpzHV9hAE9n3hSJbeSa1sm5HUcXTTMDy2jeu63q0D3PlwDx0FAMgIBBgmk/J8LqWPQOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YyhmFqb5znV9hQF9n3hSJbeSa1sm5EUAHQTQTy2jeu63q0D3PlwDx0lEMgMBhgmk/J8LqUPgeBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+c6hmFqbJwDV9hgG9n3hSJbeSa1sm5FUEHRTUTy2jeu63q0D3PlwDx1FIMgQCBgmk/J8LqWPweBRhV6z2q0VF94iJ9pOBAA7";
	var imgNews = "data:image/gif;base64,R0lGODlhEAAQAOZAAPv/++Xh5THeANvf293Z3dXR1f4AOuvv66Ono6WhpZ2Zne3p7Zj/cwAQ+G1pbc3JzZOXk/Xx9TriACAs7zs/O9PX03t/e/P38/8HPP+rvk1JTY2JjbWxtVNXU/+XriAkUNnb/wES//8zWVn/GwkV/wAMoGNnY/8LRf8TSUtPS/9/nkf/C/8fUn7/S0RQ44T/X/+LpsvPy3T/QwAIYIWBhZGY//9HcQAM4DE9/xkq/3VxdQAOuKuvq6Gk/9r/w1Fd/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAEAALAAAAAAQABAAAAezgECCg4IVC4SIgwcAAEABB4mEFTA2jAADAYkDBxcsBgYYABYEiAEADyKfBhkaCpErBxAYBigqEIQEDC8yAgAIJxkAHgMchAACAhICLQAKFCYFCAOCpsoCDAAOMzsTLgUJggMAIwy9FB8lNw0934ILEQg8PtkTDQ0hAB0Pgt8KEDQRNAwgkeNHjQAFBB0gEIAAAiAbAuAAYalYIgIEClxI4UCHhUiDYmxIwDDAIZABHkxDFAgAOw==";
	var imgNoNews = "data:image/gif;base64,R0lGODlhEAAQANU1AP39/erq6pOTk5+fn+Xl5eTk5N7e3vHx8bu7u7q6ut/f36KiotjY2LS0tPDw8JycnK+vr5CQkKioqMrKym1tbfb29pSUlJ2dnaOjo3d3d6Wlpff398bGxoSEhH9/f+jo6KmpqbKystPT05aWlmlpab29vaurq8/Pz7i4uKamprGxsYuLi66urnt7e9bW1sPDw8HBwXl5ednZ2WNjY9zc3P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAADUALAAAAAAQABAAAAajwJpwKFQ4iMjhAQCoBQ5JokKUYAIIgSThsDENBgvApYAMABih70CRaUQxB8hikDpBiIXJCyUAIDAKZwQcRAACAhYCCAANFCsGCARCZokCEwARMx0aLAYJQgQAIBN9FCQtAg80nkIOFQgwH5gaDw8DAB4MQp4NchUZBAsSJS4BBkIHBQEFCDUSASpLTIRJBQUGGzERIxdRQzISCcoBR94BDJJIQQA7";
	var imgErrorOrTimeout = "data:image/gif;base64,R0lGODlhEAAQANU6AP98fP+AgP/m5v/ExP+2tv+cnP9ycv/k5P8rK/88PP8CAv9AQP/v75MAAP9HR//y8v8uLv8QELUAAP+Tk/9OTnQAAP89Pf+xsf81Nf+trf/V1f9sbP9YWP9RUf+Li/96ev+Ojv/l5f9ubsAAAP9lZf+EhNwAAPMAAP+5uf+lpbkAAP8WFv+9vf9KSv8NDc0AAIYAAP9UVP8yMv/Bwf/Q0P8MDP/Y2P/9/f9mZv/MzP///wAAAAAAAAAAAAAAAAAAACH5BAEAADoALAAAAAAQABAAAAajQJ1wKBwciMih4HbT2QRJ4iADYN5otiRN8ODIZIkbJIe03Qiir2wgMUQXAlxC1krhiLkC6FO7BRYDZzQTRDc1NRE1ATcGDSczATRCZok1BTcKFSYOJDMAQjQ3MQV9DTAjLggsnkIHDAEeGpgOCAgYNy8EQp4GcgwSNBYdJRc2M0ICOTY5AToUNhshVoRJOTkzDyoKKxBRQygUAMo2R942BJJIQQA7";

	var btn = arguments[0];
	var parser = new DOMParser();

	function clickBtn(event) {
		if (event.button == 0) {
			event.preventDefault();
			window.clearInterval(checker);
			checker = window.setInterval(getDoc, interval, pageURL, true);
			getDoc(pageURL, true);
		}
		else if (event.button == 1) {
			event.preventDefault();
			window.clearInterval(checker);
			checker = window.setInterval(getDoc, interval, pageURL, true);
			btn.image = imgNoNews;
			btn.tooltipText = ((new Date()).toLocaleString() + " No news");
			if (gBrowser.selectedBrowser.currentURI.spec == "about:blank" && !gBrowser.selectedBrowser.webProgress.isLoadingDocument) {
				gBrowser.selectedBrowser.loadURI(pageURL);
			}
			else {
				gBrowser.selectedTab = gBrowser.addTab(pageURL);
			}
		}
	}

	function getDoc(pageURL, pureXHR) {
		btn.image = imgThrobber;
		var xhr = new XMLHttpRequest();
		xhr.open("GET", pageURL, true);
		xhr.mozBackgroundRequest = true;
		xhr.timeout = 3000;
		xhr.channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
		if (pureXHR) {
			xhr.responseType = "document";
			xhr.onload = function() {
				processDoc(this.responseXML);
			}
			xhr.ontimeout = function() {
				getDoc(pageURL, false);
			}
		}
		else {
			xhr.onload = function() {
				processDoc(parser.parseFromString(this.responseText, "text/html"));
			}
			xhr.ontimeout = function() {
				btn.image = imgErrorOrTimeout;
				btn.tooltipText = ((new Date()).toLocaleString() + " Timeout");
			}
		}
		xhr.onerror = function() {
			btn.image = imgErrorOrTimeout;
			btn.tooltipText = ((new Date()).toLocaleString() + " HTTP error");
		}
		xhr.send(null);
	}

	function processDoc(doc) {
		var keyElement = doc.evaluate(keyElementXPath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
		if (keyElement) {
			if (keyElement.className.indexOf("new-pm-link") > -1) {
				btn.image = imgNews;
				btn.tooltipText = ((new Date()).toLocaleString() + " News");
			}
			else {
				btn.image = imgNoNews;
				btn.tooltipText = ((new Date()).toLocaleString() + " No news");
			}
		}
		else {
			btn.image = imgErrorOrTimeout;
			btn.tooltipText = ((new Date()).toLocaleString() + " Authentication or parsing error");
		}
	}
	
	btn.addEventListener("click", clickBtn, true);
	window.setTimeout(getDoc, delay, pageURL, true);
	var checker = window.setInterval(getDoc, interval, pageURL, true);

})(this)

К паре начальных параметров добавилось ещё два: delay (количество секунд между запуском браузера и первым запросом по таймеру) и interval (интервалы между дальнейшими запросами в минутах). В нашем примере это десять секунд и час соответственно.

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

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

Потом мы добавляем обработчик нажатий на кнопку мышью. Нам достаточно будет нажатий левой и средней кнопкой мыши. При первом нажатии мы будем инициировать внеочередной запрос (при этом таймер будет перезапускаться), при нажатии средней кнопкой мы будем открывать нужную страницу в браузере, чтобы детальнее ознакомиться с новостями. Обработчик средней кнопки будет проверять текущую вкладку и, если она пуста и при этом в неё ничего не начало загружаться, будет открывать страницу в ней, в противном случае — в новой вкладке. Также при таком открытии мы будем перезапускать таймер и сбрасывать показатель новостей до следующего запроса, подразумевая, что открытие страницы равно внеочередному запросу и всё равно приведёт к прочтению (то есть сбросу) новостей на сайте.

Обработчики результатов XHR в этом примере выдают не alert-ы, а изменения иконок и всплывающих подсказок (в последние входит время запроса и краткая информация о результате).

При наличии новых сообщений сервер добавляет к ключевому элементу (ссылке на ящик входящих в правом верхнем углу страницы) атрибут определённого класса. Его мы и будем проверять. Если авторизация на сайте слетает, сервер выдаёт редирект на страницу входа: в таком случае XHR получает пустой документ, элемент не находится и мы получаем ошибку, общую и для других случаев изменений в DOM.

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

3. Индикаторы новостей с проверками по таймеру и сохранением состояний между сессиями браузера.

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

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

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

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

Теперь о добавлениях в коде.

(function(){
	var pageURL = "http://www.nikse.dk/";
	var keyElementXPath = "/html/body/table/tbody/tr/td[2]/table/tbody/tr[1]/td[3]/a";
	var delay = 20 * 1000;
	var interval = 60 * 60000;

	var imgThrobber = "data:image/gif;base64,R0lGODlhEAAQAOMIAAAAABoaGjMzM0xMTGZmZoCAgJmZmbKysv///////////////////////////////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgAIACwAAAAAEAAQAAAESBDJiQCgmFqbZwjVhhwH9n3hSJbeSa1sm5GUIHSTYSC2jeu63q0D3PlwCB1lMMgUChgmk/J8LqUIAgFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UKgmFqbpxDV9gAA9n3hSJbeSa1sm5HUMHTTcTy2jeu63q0D3PlwDx2FQMgYDBgmk/J8LqWPQuFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YSgmFqb5xjV9gQB9n3hSJbeSa1sm5EUQXQTADy2jeu63q0D3PlwDx2lUMgcDhgmk/J8LqUPg+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+cagmFqbJyHV9ggC9n3hSJbeSa1sm5FUUXRTEDy2jeu63q0D3PlwDx3FYMgAABgmk/J8LqWPw+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+QihmFqbZynV9gwD9n3hSJbeSa1sm5GUYXSTIDy2jeu63q0D3PlwDx3lcMgEAhgmk/J8LqUPAOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UqhmFqbpzHV9hAE9n3hSJbeSa1sm5HUcXTTMDy2jeu63q0D3PlwDx0FAMgIBBgmk/J8LqWPQOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YyhmFqb5znV9hQF9n3hSJbeSa1sm5EUAHQTQTy2jeu63q0D3PlwDx0lEMgMBhgmk/J8LqUPgeBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+c6hmFqbJwDV9hgG9n3hSJbeSa1sm5FUEHRTUTy2jeu63q0D3PlwDx1FIMgQCBgmk/J8LqWPweBRhV6z2q0VF94iJ9pOBAA7";
	var imgNews = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAJKSURBVHjalJI/axRRFMXvve/NzO7M7oaQNe5uig2aSExIiPgRFBttkhCihfkCYmNjY6NgYyWIlYWFVjFFIKRIHwNCMIkiZCF/Vth1dl1IMjtmd17ezFyLkQhq4+0u/A73nMsB+M9BAJBZ2XetL11OA/+LEEgWtVZb/iefI0aZk6PPR4uzRY4ZAJAw4ZIVGDhmYOCQdx7u1N/UZe5Kzr5oVx5V/M8+mYQSySQUCPRbjAKz49nCdKG12pJWwfI2PG/Dy1/PZ8ezKPH4w7FyVelO6cyVu+D2XO0J6oFIC0KBsYpTpVT5XjmoB8pVl55cQolzN+ZUU6mmUg2FAs3zJiAAggT+FQsQgloQtsP62zpZ1Ib26fdTjlkfaTTw7BpxxIAQfAsai42BuwNjL8dYM0qchMnCTKE4W8yMZFDgWR4ZBzEAyJwEhP1n+5SiqVdTyw+W12Ct+qKaPAoSmBOBjpGQLCrMFFIDKTKp0qlwzBMwsT69nnDuOzfxDAzCGXLsC7b30evsdYAh9MPmUjPqRNvedngchl6oDzUZNH9zfnN7s7nUlLGKhSNY80nlpFvtUorIJDKpvdW2Sla6nHaGnPJEeaWz0lptRSeR1EfaGXb6b/XrQ525nLGH7NxkbiQ3YoPtgXegDoJasPd+r/a65i64HDKa58zS7dLw42Gj14g6kT7S0Y8oVjFKFLZAAxuLjd2nu6xZexoYEADMvGn0GlbRGrw/iAaKtOh+7ba32v4Xv1vtqobiiDnkP1tLFiVfMvOmsAVKRIF/l/fnAIYyDbro6Ua2AAAAAElFTkSuQmCC";
	var imgNoNews = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAI4SURBVHjalJK/aipREMbPnDNn/YMYdGWJgiLRIqWVhYqB1PoG1nkKX8E3ECysEtLZGUgKt14RQiAoKiKI/3YXXNTsuntusVzhkupON/Ab5vu+GUL+s4AQEgqFCoWCoihCiN8EpZRzrmnabDbzPA/C4fDT01OlUvFpAPA5vxV/y3XdTqfz/v6OuVwumUx2u935fI6IjDFEpJRSSq/DlNJsNlsqlTRNw1gsNh6Px+NxoVDIZrOMse/vb13XHx4erqoGg0E+n9/v95IkUUqp4zjxeLxWq+33e13XG40GY6xer5umaZqmYRiU0lgsBgAAgEII3xYA7Ha74/H48fHBObdt2zRNIYRlWYh43Yae5wGAruuqqj4+PmYymX6/zxhLJBLlcpkQ8vn5OZlMrn7Qtm1CSDgcBoDX11dJkprNZqvVWq1WvV7PD8qn/dzQdV0A4JyXy2VZlhFxu90KIWRZLpVKPqeqqq9ZCIH+nGEYLy8viqJwzkejkeM47XbbP4vneYqiFIvFr68vy7LQcZxgMHi5XJbL5Xq9liQJERFxOp3KsqwoSjKZvL+/n0wmmqadz2e0LCuVShWLxcPhkE6nU6nU3d3d7e0tItq2bRjGbrcbDodvb2+DwcB1Xbi5ualWq41GIxKJ/Pz8WJZ1Op0cx2GMBQIBRFRV9fn5+XK5HI9HIQQQQqLRaCQSicfj9XodESVJ2mw20+l0sVis12vDMDzPc13336cF4Jz7KUWj0UAgwBijlP5+3j8DAOp7HGipYoAdAAAAAElFTkSuQmCC";
	var imgErrorOrTimeout = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAJtSURBVHjalFLPSxRxHP18f8zMOuu67JCLm4m2omzMIthhQaxDXjwKHuzSRZEgQoigg9AtqINIV+nSHyCJhyAMBGXNX2yoyIqJG26u6+gqszUzzszuzPfbYVUKuvRuH3iP93mPB/CfQAAQFsXBeLwjHOb/YhCE6iidzuVWNM3nHEUk6UN//4PeXuAcAAChS2Lt5BwYA87B817Mzr7d2qL3YjFVUd7MzKyfnkqECBhLhBCECEL4SkwxTkWjI4nEdC5HbwWDi8XiYrE4GI+nolGRkPlC4YdpPk0mr7+aymbvx2IHhiFTiinGju+3hkKP+/oODCNvGGNDQwLGbcPDR5Z1ZFmHpkkQaqmvxwhhhCjjvBYLEPr+65fuuvL8fIAQsO2CaXKAkm1LhFy7UZ9zjFDeMD6trT3r6sKq+nluTsAYWloedncDQHpvL318DFcaeuF5ABCRJIzQ8+VleX399fj4xuQk7O9PpNMAwDivZWecAwCt+D4CCBDSn0q1hUISIZDPM86huXkkkQAADvBuZwdEESPEOCeqotyJRL5oWkHTGOc/K5WPGxtmtSptb587ju66JccRMVZHR/Orq+93d6nteQ2i6Pr+5tnZt3JZplQiRMR4WdNuNzR0hMOqopCeHshkpnM5o1qlZ46TVJRHnZ0nFxd3GxuTihJNJKC9HQQBbBs0DQ4PzxcWJjY3p7LZKmPoZjD4RFVfDgyAooBlga6DYfiVCqEUZBkEYSaTGVtacn1fd93LAppk+UYg0BoKvUqlJEJkSvfK5ZWTk6+l0q6uFyzLY6zK2F+TxAjVUVprqUmW6wVBwJhcD/EP/B4AKOgP3B2vzLEAAAAASUVORK5CYII=";

	var btn = arguments[0];
	var parser = new DOMParser();
	var prefService = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService);
	var prefBranch = prefService.getBranch("cb_storage.");
	var supportsString = Components.interfaces.nsISupportsString;
	var uStr = Components.classes["@mozilla.org/supports-string;1"].createInstance(supportsString);
	
	function clickBtn(event) {
		if (event.button == 0) {
			event.preventDefault();
			window.clearInterval(checker);
			checker = window.setInterval(getDoc, interval, pageURL, true);
			getDoc(pageURL, true);
		}
		else if (event.button == 1) {
			event.preventDefault();
			window.clearInterval(checker);
			checker = window.setInterval(getDoc, interval, pageURL, true);
			var previousStat = JSON.parse(prefBranch.getComplexValue("Subtitle_Edit", supportsString).data);
			previousStat.isNew = false;
			uStr.data = JSON.stringify(previousStat);
			prefBranch.setComplexValue("Subtitle_Edit", supportsString, uStr);
			prefService.savePrefFile(null);
			btn.image = imgNoNews;
			btn.tooltipText = ((new Date()).toLocaleString() + " No news: " + previousStat.commentsInfo);
			if (gBrowser.selectedBrowser.currentURI.spec == "about:blank" && !gBrowser.selectedBrowser.webProgress.isLoadingDocument) {
				gBrowser.selectedBrowser.loadURI(pageURL);
			}
			else {
				gBrowser.selectedTab = gBrowser.addTab(pageURL);
			}
		}
	}

	function getDoc(pageURL, pureXHR) {
		btn.image = imgThrobber;
		var xhr = new XMLHttpRequest();
		xhr.open("GET", pageURL, true);
		xhr.mozBackgroundRequest = true;
		xhr.timeout = 3000;
		xhr.channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
		if (pureXHR) {
			xhr.responseType = "document";
			xhr.onload = function() {
				processDoc(this.responseXML);
			}
			xhr.ontimeout = function() {
				getDoc(pageURL, false);
			}
		}
		else {
			xhr.onload = function() {
				processDoc(parser.parseFromString(this.responseText, "text/html"));
			}
			xhr.ontimeout = function() {
				btn.image = imgErrorOrTimeout;
				btn.tooltipText = ((new Date()).toLocaleString() + " Timeout");
			}
		}
		xhr.onerror = function() {
			btn.image = imgErrorOrTimeout;
			btn.tooltipText = ((new Date()).toLocaleString() + " HTTP error");
		}
		xhr.send(null);
	}

	function processDoc(doc) {
		var keyElement = doc.evaluate(keyElementXPath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
		if (keyElement) {
			var previousStat = JSON.parse(prefBranch.getComplexValue("Subtitle_Edit", supportsString).data);
			if (previousStat.pageURL != keyElement.href || previousStat.commentsInfo != keyElement.textContent || previousStat.isNew) {
				previousStat.pageURL = keyElement.href;
				previousStat.commentsInfo = keyElement.textContent;
				previousStat.isNew = true;
				uStr.data = JSON.stringify(previousStat);
				prefBranch.setComplexValue("Subtitle_Edit", supportsString, uStr);
				prefService.savePrefFile(null);
				btn.image = imgNews;
				btn.tooltipText = ((new Date()).toLocaleString() + " News: " + keyElement.textContent);
			}
			else {
				btn.image = imgNoNews;
				btn.tooltipText = ((new Date()).toLocaleString() + " No news: " + keyElement.textContent);
			}
		}
		else {
			btn.image = imgErrorOrTimeout;
			btn.tooltipText = ((new Date()).toLocaleString() + " Parsing error");
		}
	}
	
	if (!prefBranch.prefHasUserValue("Subtitle_Edit")) {
		uStr.data = JSON.stringify({"pageURL": "", "commentsInfo": "", "isNew": false});
		prefBranch.setComplexValue("Subtitle_Edit", supportsString, uStr);
		prefService.savePrefFile(null);
	}
	
	btn.addEventListener("click", clickBtn, true);
	window.setTimeout(getDoc, delay, pageURL, true);
	var checker = window.setInterval(getDoc, interval, pageURL, true);
	
})(this)

Как мы видим, добавились переменные для некоторых внутренних механизмов: все они нам пригодятся для сохранения данных между сессиями. Мы будем использовать ту же базу настроек, что и расширения (к помощи простых файлов или баз данных обращаться не будем, чтобы избежать лишней сложности). Мы выбрали самый универсальный способ хранения строковых данных (***ComplexValue), чтобы при необходимости можно было хранить в базе юникод.

К обработчику нажатия на кнопку добавились инструкции по сбросу флага новостей (кнопка запрашивает ключ из настроек, превращает его в объект через JSON, изменяет флаг, запаковывает объект в строку и опять сохраняет его в базе настроек).

Больше всего добавлено к подфункции обработки документа. Проверив наличие ключевого элемента, мы запрашиваем из базы настроек объект с информацией о прежнем состоянии (ссылку на последний пост, количество комментариев и не был ли поднят флаг новостей в последний раз: ведь после этого все последующие запросы могут натыкаться на одинаковые результаты, которые всё равно должны оставаться новостями, пока пользователь не обратит внимание на кнопку и не откроет сайт). Если текущее и прежнее состояние одинаковы и флаг новостей не активизирован, мы оставляем индикатор в нейтральном состоянии и в настройках ничего не меняем. Если же появился новый пост, добавились комментарии или флаг новостей поднят с прежних запросов, мы заменяем прежнее состояние текущим, сохраняем изменённый объект с параметрами в базу настроек и сигнализируем о новостях.

После определения переменных и функций и перед привязкой обработчиков, отложенным вызовом и запуском таймера нам нужно сделать ещё одно: проверить, есть ли в базе настроек ключ для хранения наших данных. Если инициализация кнопки запущена в первый раз (мы только что создали кнопку), ключа ещё нет, и тогда мы создаём заготовку объекта с пустыми значениями параметров и неактивным флагом новостей.

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

Примеры можно наращивать по мере необходимости: усложнять способы оповещения о новостях, добавлять ключевые элементы и параметры сравнения, создавать на лету новые детали интерфейса (например, панели с динамической текстовой информацией рядом с кнопкой) и так далее. Обо всём этом можно почитать на сайте разработчиков Firefox или подсмотреть в примерах готовых кнопок на сайте разработчика Custom Buttons.

Спасибо всем, кто дочитал. И удачных вам экспериментов)

Автор: vmb

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


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