В этой статье я завершаю цикл публикаций, в котором я хотел рассказать о своём опыте написания веб-расширения для браузеров. У меня уже был опыт создания веб-расширения, которое установили около 100 000 пользователей Chrome, которое работало автономно, но в данном цикле статей я решил углубиться в процесс разработки веб-расширения тесно интегрировав его с серверной частью.
Часть 1, Часть 2, Часть 3
Веб-расширение позволяет настроить пользовательский скрипт для автоматического выполнения его кода после завершения загрузки страницы. Это удобно, например, когда у вас есть список однотипных страниц для обхода и получения данных от выполнения скрипта. Либо если необходимо на любой странице в интернет удалять назойливую рекламу, код для отслеживания ваших действий на веб-ресурсе, менять фон страницы на тёмный и т. п.
При этом часто необходимо получать данные со страницы в режиме ежедневного расписания. Например, если на сайте размещены курсы валюты, а прямой запрос по URL защищает хешированный аргумент. В качестве такого экземпляра можно рассмотреть сайт Центрального банка Европы. В этом случае для получения актуальных курсов валют, необходимо знать хэшированный URL для получения правильных данных в формате XML.
С помощью веб-расширения можно задать необходимую частоту для получения данных через выполнение скрипта для запроса курсов валют. Веб-расширение принимает на вход строку в формате крон, поэтому для получения курсов валют в ежедневном режиме необходимо указывать * */1 * * *.
С технической точки зрения серверная часть запускает Puppeteer с добавлением на страницу браузера Chromium пользовательского скрипта. В этом случае мы сталкиваемся с проблемой преднамеренного завершения работы процесса Chromium с течением времени, так как увеличение количества процессов будет негативно сказываться на общей производительности серверной части. В случае планирования запуска пользовательского скрипта в периодическом режиме для решения вышеуказанной проблемы необходимо отправить запрос из пользовательского скрипта в Puppeteer для завершения процесса Chromium.
После долгих тестов процесса реализации планировщика задач было принято решение об отслеживании вывода консоли скрипта. Таким образом для остановки процесса Chromium пользовательский скрипт должен иметь команду для остановки процесса на необходимом участке кода:
При помощи этой команды пользовательский скрипт может закрыть серверный процесс Chromium, порождённый Puppeteer. Это реализовано путем отслеживания события Puppeteer “console”:
Если пользовательский скрипт не имеет такую команду для завершения процесса выполнения в режиме планировщика задач необходимо иметь принудительное завершение такого скрипта по таймеру. Это реализовано простым способом при помощи setTimeout:
Так как планировщик задач работает на серверной части, а пользователю часто необходимо использовать определенный IP-адрес, была добавлена опция использования прокси-сервера.
Также пользователь может скачать последний лог сообщений из консоли Puppeteer. Например, это удобно для проверки корректной работы прокси-сервера.
Во время тестирования и полевых испытаний выяснилось, что для некоторых видов скриптов полезно иметь горячие клавиши для их выполнения на веб-ресурсах. Примером такого скрипта может служить Redability.js. Этот скрипт создает “чистый вид” для чтения содержимого сайта. То есть js библиотека анализирует структуру страницы с содержимым и позволяет получать с высокой долей вероятности заголовок, автора и содержание страницы. После чего пользовательский скрипт может перезаписать html веб-ресурса и позволить пользователю читать в “чистом виде”, без лишней разметки, рекламы и т. п.
Изначально запуск пользовательского скрипта был возможен только из pop-up веб-расширения по клику на кнопку “Execute”. Такая логика взаимодействия с интерфейсом часто оправдана соображениями безопасности. Но в примере выше она не позволяет легко приводить содержание веб-ресурса к “чистому виду”.
Как было изложено выше, веб-расширение позволяет работать с DOM через content.js, но объект window не может быть использован, например, для хранения параметров, так как он не является объектом window открытой вкладки. Это условие ограничивает работу веб-расширения в качестве объекта для отслеживания нажатия клавиш.
В решаемой задаче необходимо передать “горячие клавиши” из интерфейса веб-расширения на серверную сторону для хранения. Далее при каждой загрузке страницы веб-ресурса получать список “горячих клавиш” и загружать пользовательский скрипт после нажатия правильной комбинации. В качестве библиотеки для работы с “горячими клавишами” используется hotkeys.js.
После получения списка “горячих клавиш” от серверной части выполняется следующий код:
Функция getScript формирует html код для тэга script и записывает его в страницу веб-ресурса. Таким образом мы имеем на каждой странице возможность отслеживания нажатия клавиш. Также мы должны передать массив соответствия “горячей клавиши” с id скрипта для выполнения. Это реализовано через добавление html кода для тэга script, содержание которого инициирует глобальную переменную для хранения массива соответствия в JSON формате.
Выше уже было упомянуто, что существует проблема сообщения между открытой страницей веб-ресурса и скриптом content.js веб-расширения, который используется для манипуляции с DOM. В этом случае можно прибегнуть к простой технике проверки значения в localStorage, который является общим объектом для двух упомянутых выше точек взаимодействия.
В content.js можно просто раз в секунду проверять значение localStorage и выполнять те же самые манипуляции с DOM, что и при нажатии на кнопку “Execute” в pop-up веб-расширения.
Таким образом реализована техника “горячих клавиш”, которая позволяет быстро запускать пользовательские скрипты, используя мощь внутренней библиотеки для нужд решения практических задач.
В процессе реализации данного проекта были решены практические задачи написания интеграции между серверной частью основанной на meteor.js и кросс-браузерным веб-расширением. Ключевыми камнями преткновения были SCP и процессы взаимодействия между тремя компонентами клиентской части — страницы открытой в браузере, скриптами content.js и background.js.
Надеюсь, что изложенный мною опыт позволит упростить написание более узкоспециализированных кросс-браузерных веб-расширений.
В дальнейшем планируется локализация веб-расширения и написание полезных для сообщества скриптов. Если у читателя есть идея или желание помочь в написании таких пользовательских скриптов, то просьба писать в личные сообщения.
Часть 1, Часть 2, Часть 3
Задачи по расписанию
Веб-расширение позволяет настроить пользовательский скрипт для автоматического выполнения его кода после завершения загрузки страницы. Это удобно, например, когда у вас есть список однотипных страниц для обхода и получения данных от выполнения скрипта. Либо если необходимо на любой странице в интернет удалять назойливую рекламу, код для отслеживания ваших действий на веб-ресурсе, менять фон страницы на тёмный и т. п.
При этом часто необходимо получать данные со страницы в режиме ежедневного расписания. Например, если на сайте размещены курсы валюты, а прямой запрос по URL защищает хешированный аргумент. В качестве такого экземпляра можно рассмотреть сайт Центрального банка Европы. В этом случае для получения актуальных курсов валют, необходимо знать хэшированный URL для получения правильных данных в формате XML.
С помощью веб-расширения можно задать необходимую частоту для получения данных через выполнение скрипта для запроса курсов валют. Веб-расширение принимает на вход строку в формате крон, поэтому для получения курсов валют в ежедневном режиме необходимо указывать * */1 * * *.
С технической точки зрения серверная часть запускает Puppeteer с добавлением на страницу браузера Chromium пользовательского скрипта. В этом случае мы сталкиваемся с проблемой преднамеренного завершения работы процесса Chromium с течением времени, так как увеличение количества процессов будет негативно сказываться на общей производительности серверной части. В случае планирования запуска пользовательского скрипта в периодическом режиме для решения вышеуказанной проблемы необходимо отправить запрос из пользовательского скрипта в Puppeteer для завершения процесса Chromium.
После долгих тестов процесса реализации планировщика задач было принято решение об отслеживании вывода консоли скрипта. Таким образом для остановки процесса Chromium пользовательский скрипт должен иметь команду для остановки процесса на необходимом участке кода:
console.log(‘script is ended’);
При помощи этой команды пользовательский скрипт может закрыть серверный процесс Chromium, порождённый Puppeteer. Это реализовано путем отслеживания события Puppeteer “console”:
//initialize browser and page
page.on('console', async msg => {
for (let i = 0; i < msg.args().length; ++i) {
if(await msg.args()[i].jsonValue() == 'script is ended') {
await browser.close();
}
}
});
Если пользовательский скрипт не имеет такую команду для завершения процесса выполнения в режиме планировщика задач необходимо иметь принудительное завершение такого скрипта по таймеру. Это реализовано простым способом при помощи setTimeout:
const timeLimitCron = 30;
const timeout = setTimeout(async () => {
if(browser) {
await browser.close();
}
clearTimeout(timeout);
}, timeLimitCron * 1000
); //time limit for cron, then forced closing, default 30 seconds
Так как планировщик задач работает на серверной части, а пользователю часто необходимо использовать определенный IP-адрес, была добавлена опция использования прокси-сервера.
Также пользователь может скачать последний лог сообщений из консоли Puppeteer. Например, это удобно для проверки корректной работы прокси-сервера.
Горячие клавиши для пользовательских скриптов
Во время тестирования и полевых испытаний выяснилось, что для некоторых видов скриптов полезно иметь горячие клавиши для их выполнения на веб-ресурсах. Примером такого скрипта может служить Redability.js. Этот скрипт создает “чистый вид” для чтения содержимого сайта. То есть js библиотека анализирует структуру страницы с содержимым и позволяет получать с высокой долей вероятности заголовок, автора и содержание страницы. После чего пользовательский скрипт может перезаписать html веб-ресурса и позволить пользователю читать в “чистом виде”, без лишней разметки, рекламы и т. п.
Изначально запуск пользовательского скрипта был возможен только из pop-up веб-расширения по клику на кнопку “Execute”. Такая логика взаимодействия с интерфейсом часто оправдана соображениями безопасности. Но в примере выше она не позволяет легко приводить содержание веб-ресурса к “чистому виду”.
Как было изложено выше, веб-расширение позволяет работать с DOM через content.js, но объект window не может быть использован, например, для хранения параметров, так как он не является объектом window открытой вкладки. Это условие ограничивает работу веб-расширения в качестве объекта для отслеживания нажатия клавиш.
В решаемой задаче необходимо передать “горячие клавиши” из интерфейса веб-расширения на серверную сторону для хранения. Далее при каждой загрузке страницы веб-ресурса получать список “горячих клавиш” и загружать пользовательский скрипт после нажатия правильной комбинации. В качестве библиотеки для работы с “горячими клавишами” используется hotkeys.js.
После получения списка “горячих клавиш” от серверной части выполняется следующий код:
var hotKeysMap = {keys: [], id: []};
for(var i in data.response.data) {
hotKeysMap.keys.push(data.response.data[i]['hotkeys']);
hotKeysMap.id.push(data.response.data[i]["_id"]);
}
if(hotKeysMap.keys) {
getScript("hotkeys.js", function() {
var script = document.createElement("script");
script.setAttribute("class", "gCore-hotKeys");
script.setAttribute("data-endpoint", event.data.endPoint);
script.setAttribute("data-token", event.data.token);
script.setAttribute("data-userid", event.data.userId);
script.innerHTML = "GChotKeys = JSON.parse(\"" + JSON.stringify(hotKeysMap).replace(/"/g, "\\\"") + "\");\n" + "hotkeys(GChotKeys.keys.join(','), function(event, handler) {" + " event.preventDefault();" +
" localStorage.setItem('runHotKeysScript', GChotKeys.id[GChotKeys.keys.indexOf(handler.key)]);" + "});";
document.body.appendChild(script);
});
}
Функция getScript формирует html код для тэга script и записывает его в страницу веб-ресурса. Таким образом мы имеем на каждой странице возможность отслеживания нажатия клавиш. Также мы должны передать массив соответствия “горячей клавиши” с id скрипта для выполнения. Это реализовано через добавление html кода для тэга script, содержание которого инициирует глобальную переменную для хранения массива соответствия в JSON формате.
Выше уже было упомянуто, что существует проблема сообщения между открытой страницей веб-ресурса и скриптом content.js веб-расширения, который используется для манипуляции с DOM. В этом случае можно прибегнуть к простой технике проверки значения в localStorage, который является общим объектом для двух упомянутых выше точек взаимодействия.
В content.js можно просто раз в секунду проверять значение localStorage и выполнять те же самые манипуляции с DOM, что и при нажатии на кнопку “Execute” в pop-up веб-расширения.
setInterval(function() {
if(localStorage.getItem('runHotKeysScript')) {
// run user script
localStorage.removeItem('runHotKeysScript');
}
}, 1000);
Таким образом реализована техника “горячих клавиш”, которая позволяет быстро запускать пользовательские скрипты, используя мощь внутренней библиотеки для нужд решения практических задач.
Заключение
В процессе реализации данного проекта были решены практические задачи написания интеграции между серверной частью основанной на meteor.js и кросс-браузерным веб-расширением. Ключевыми камнями преткновения были SCP и процессы взаимодействия между тремя компонентами клиентской части — страницы открытой в браузере, скриптами content.js и background.js.
Надеюсь, что изложенный мною опыт позволит упростить написание более узкоспециализированных кросс-браузерных веб-расширений.
В дальнейшем планируется локализация веб-расширения и написание полезных для сообщества скриптов. Если у читателя есть идея или желание помочь в написании таких пользовательских скриптов, то просьба писать в личные сообщения.