pacify.ru \ chrome-crx

Как написать расширение для Google Chrome, Chromium (crx)

февраль 2013 года

Расширения для chrome иногда [ошибочно] называются плагинами.

В чём суть задачи, и в чём сложности

Наиболее авторитетные люди в этой области - Wladimir Palant (он написал AdBlock Plus), ... Расширения для Chrome писать проще, чем для Firefox. Что интересно, самое популярное расширение для Chrome - это "Тюряга ВКонтакте" (по статистике Яндекса).

Задача состоит в разработке расширения, которое будет реагировать на открытие определенных HTML-страниц в браузере. Например, модифицировать HTML-код страницы сайта для более удобного использования этого сайта.
Решение опробовано на Google Chrome 24.x и Chromium 6.x (Debian 6.0.6, amd64).
Смотрите на сайте тур Кандалакша из Мурманска.

Решение задачи

Конспект основных вопросов, возникающих по ходу написания crx-расширения.

  1. Как оформить расширение?
  2. Как проверять текущий URL, обрезать его и вычислять хэш?
  3. Как установить таймер?
  4. Как скачивать config.xml/time.txt, и как их парсить?
  5. Как использовать глобальные атомарные переменные и табличные данные в chrome extension?
  6. Как отрабатывать событие об открытии страницы?
  7. Как оформлять HTML-код страницы?
  8. Как упаковать расширение, и где его расположить?

Как оформить расширение?

Для минимального расширения достаточно 4-х файлов:

128.png background.js content.js manifest.json
Где manifest.json:
{ "manifest_version": 2, "name": "DomainCheck extension", "version": "0.1", "background": { "scripts": ["background.js"] }, "content_scripts": [ { "matches": [ "*://*/*" ], "js": [ "content.js" ], "run_at": "document_start" } ], "permissions": [ "http://pacify.ru/*" ], "icons": { "128": "128.png" } // no web_accessible_resources }
Файл background.js содержит код, исполняемый при старте браузера. В этом скрипте можно "повесить" обработчик загрузки контента документа (document.location.href).

Chrome самостоятельно генерирует _generated_background_page.html:

<!DOCTYPE html> <body> <script src="background.js"></script>

Для взаимодействия между background.js и content script, можно использовать сообщения (request/message) и chrome.extension.getBackgroundPage(). См. также описание архитектуры расширений Chrome (the architecture overview). Там сказано, что "A content script is some JavaScript that executes in the context of a page that's been loaded into the browser".

Примечание: в Chromium сложно отлаживать background page (background.js), так как нет соотв. вкладки на странице "Extensions" в developer mode.

Как проверять текущий URL, обрезать его и вычислять хэш?

Как устанавливать обработчик на DOMContentLoaded, рассказано на developer.chrome.com. См. также документацию про Background Pages (background.js).

Для внедрения HTML-кода в страницу, можно использовать совет из статьи на Хабре (см. там упоминание глобальной переменной document), но нам нужно ещё и сравнивать текущий URL со списком.

Is document.location.href deprecated?

Пример со stackoverflow.com получения полного имени домена:

var url = "http://www.domain.ru/tmp/file.txt?abc=1&cde=2#123" var getLocation = function(href) { var l = document.createElement("a"); l.href = href; return l; }; var l = getLocation(url); alert(l.hostname)

Выделить поддомен 2-го уровня можно так:

var l = getLocation(url); var d = l.hostname; function cutd(str) { var re = /.*?\.([\w\d-\u0100-\uffff-\.]+\.[\w\d-\u0100-\uffff-\.]+)/; return str.replace(re,"$1"); } alert(cutd(d));
(см. подсказку на stackoverflow.com).

sha1.js injection ...

Как установить таймер?

См. Sample Extensions: Event Page Example, background.js:
chrome.alarms.create({delayInMinutes: 0.1}); chrome.alarms.onAlarm.addListener(function() { alert("Time's up!"); });
Предупреждение: под Chromium 26.0 (Feb 2013), объект chrome.alarms недоступен:
background.js:2 Uncaught TypeError: Cannot call method 'create' of undefined
Этот баг в Chromium закрыт 9 января 2013 года, но ещё не появился в свежих сборках для Windows.

Если всё-же решили использовать "современный" alarms, то как правильно устанавливать alarm-таймер произвольной длительности/периода, рассказано на stackoverflow.com.

Для совместимости со старыми браузерами, лучше использовать window.setInterval() в background.js:

var i = 0; window.setInterval(function() { alert(i); i++ }, 2*1000); // in milliseconds
Функция setTimeout() - это одиночный "будильник".

Disable-Enable chrome-расширения запускает по-новой background.js. *) надо проверить тоже самое для ухода в режим ? и sleep.

Как скачивать config.xml/time.txt, и как их парсить?

Если скачать просто через XMLHttpRequest, то может вывалиться сообщение: "XMLHttpRequest cannot load http://pacify.ru/config.xml. Origin http://www.google.ru is not allowed by Access-Control-Allow-Origin.". Это означает, что выполняя кросс-доменный запрос, вы не включили опцию для CORS, .htaccess:

<IfModule mod_headers.c> Header set Access-Control-Allow-Origin "*" </IfModule>

Можно разрешить кросс-доменные запросы в расширении Chrome и через permissions в manifest.json:

{ "manifest_version": 2, ... "permissions": [ "http://pacify.ru/" ],

Впрочем, оба этих случая не работают для распакованных расширений Chromium. Для запакованных расширений, Chromium версий 6.0-7.0 иногда выдаёт "Bad magic number" (для чистого Chrome такая ошибка не наблюдается).
Примечание: защиту CORS можно обойти в Chromium через опцию --disable-web-security:

$ chromium-browser --disable-web-security http://domain/path

Дополнение: для Chromium нужно подравить строки в manifest.json на следующие (указав явно домен и добавив звёздочку в пути):

{ "permissions": [ "http://pacify.ru/*" ],

Чтобы web inspector(?) в chrome не ругался при отключенном Интернете на XMLHttpRequest(), можно сделать так:

var req = null; try { req = new XMLHttpRequest(); } catch(err) {} // see advice at stackoverflow.com
(это написано по совету со stackoverflow.com).

...

Для отладки парсинга, можно использовать console.log("строка"). Строки допускают переводы каретки с помощью "\n".

Парсинг XML на JavaScript в chrome extension делается так:

var xml = req.responseXML.documentElement; var ts = xml.getElementsByTagName("timeout"); var timeout = ts[0].textContent; if (ts) {console.log("timeout="+timeout);} var ds = xml.getElementsByTagName("domain"); if (ds) { for (var i = 0; i < ds.length; i++) { console.log("domain: "+ds[i].textContent); } }

Как использовать глобальные атомарные переменные и табличные данные в chrome extension?

Чтобы синхронизировать данные расширений при помощи Storage API, нужен Chrome версии >= 20. В манифесте (manifest.json) нужно написать следующее:

"premissions": [ "storage" ]

Как пишут в google group "Chromium HTML5", "I turned my attention to Web SQL Database but it seems Web SQL is no longer in "active maintenance" which leads me to believe that it will be dropped from HTML spec". См. подробнее W3C Web SQL Database, note. Можно попробовать использовать Basic concepts - для расширения места под базу можно использовать permissions: unlimitedStorage в manifest.json. Для использования "Unlimited storage" есть следующие Offline APIs: 1) App Cache; 2) File System; 3) IndexedDB; 4) WebSQL (deprecated). Пример использования IndexedDB в Chrome см. . Пример работы с IndexedDB см. на gist.github.com:

window.indexedDB = window.indexedDB || window.webkitIndexedDB; var req = indexedDB.open("my db") req.onerror = function() { console.log("error"); }

Расположение файлов IndexedDB,
Windows: C:\Users\[USER_NAME]\AppData\Local\Google\Chrome\User Data\Default\IndexedDB,
Linux: /home/[username]/.config/google-chrome/Default/IndexedDB/chrome-xxx.indexeddb.leveldb/:

$ sudo ls -la /home/anonymous/.config/google-chrome/Default/IndexedDB/chrome-extension_ojeihbjghbabiocoglbfhdebhhckdnol_0.indexeddb.leveldb/ total 24 drwx------ 2 anonymous anonymous 4096 Фев 7 03:08 . drwx------ 3 anonymous anonymous 4096 Фев 7 03:08 .. -rw-r--r-- 1 anonymous anonymous 285 Фев 7 03:08 000003.log -rw-r--r-- 1 anonymous anonymous 16 Фев 7 03:08 CURRENT -rw------- 1 anonymous anonymous 0 Фев 7 03:08 LOCK -rw-r--r-- 1 anonymous anonymous 46 Фев 7 03:08 LOG -rw-r--r-- 1 anonymous anonymous 32 Фев 7 03:08 MANIFEST-000002

Можно посмотреть на примеры использования IndexedDB в Mozilla Developer Network.

Вставка большого числа записей в IndexedDB рассмотрена на stackoverflow.com.

Для добавления элементов в IndexedDB, надо использовать

indexedDB.db.transaction().objectStore().put({})

В папке базы IndexedDB-данных старые базы хранятся как .sst-файлы, а новые (текущие) - как .log-файлы.

setVersion() is Deprecated. Но тут есть одна хитрость [про onupgradeneeded()]: ...

Как сказано в блоге Parashuram Narasimhan, "For Chrome: In case of chrome, the onupgradeneeded function is not called. The database's onsuccess function is called. Here, the existence of the setVersion method is checked. If the method exists, and the specified version is greater than the database version, a the setVersion method is called. The onsuccess of the setVersion's request call invokes the user's onupgradeneeded method with the version transaction. Once the method completes, the versionTrasnaction is committed by closing the database. The database is opened again with the latest version and this is passed to the onsuccess defined by the user." (поэтому для вызова onupgradeneeded(), я делаю db.setVersion("3")).

Как делать выборку данных в content.js:

dbreq.onupgradeneeded = function(event) { console.log("dbreq.onupgradeneeded"); var db = event.target.result; var tx = db.transaction(["test_db"], "readonly"); var store = tx.objectStore("todo");

Для открытия базы, используйте код от axemclion+jepp (функцию openReqShim).

Когда открываем IndexedDB-базу, которая не существует, она создаётся (will be created) с номером версии, version = 0. При этом, вызываются onupgradeneeded() и onsuccess() последовательно. При первом вызове onupgradeneeded(), version уже = 1. Когда открываем второй раз [существующую базу], onupgradeneeded() уже не вызывается, а номер версии = 1. (?не увеличивается) Вызывается только dbreq.onsuccess().

Ещё один момент. Тут сказано, что "With Chrome prior to 23 you need to create such a transaction manually by calling setVersion() - an API that has been removed from the spec. The older spec can be found at: http://www.w3.org/TR/2011/WD-IndexedDB-20110419/", то есть для того чтобы не было ошибки "NotFoundError: DOM IDBDatabase Exception 8" в логах хрома, требуется вызвать setVersion().

В Chromium 6.0.472.63 (59945) реализация IndexedDB не является stable, поэтому отключена и не работает =)

В общем, надо использовать background.js + iframe + обмен с контент-скриптами через messages

Как отрабатывать событие об открытии страницы?

manifest.json:

"content_scripts": [ { "matches": [ "*://*/*" ], "js": [ "content.js" ], "run_at": "document_start" } ], ...
content.js:
document.addEventListener("DOMContentLoaded", function () { alert("Abc "+document.location.href); });

Как упаковать расширение, и где его расположить?

Чтобы расположить расширение на Chrome WebStore, нужно заплатить гуглю вступительный взнос 5$ (затем можно будет помещать любое количество расширений). Заплатить можно через VISA, MasterCard, AMEX или DISCOVER (дополнительно, при оплате, надо указать свой полный почтовый адрес и Имя-Фамилию).

Для размещения расширения нужен Google-аккаунт, и далее. Там понадобятся скриншот и рекламная картинка. Обновлять код расширения надо будет вручную, через тот же Chrome WebStore (как я понял, тут нет автоматического обновления по URL, как в Firefox). В файле manifest.json нужно обновить версию расширения. Спустя несколько минут после добавления, расширение будет доступно в поиске по расширениям Chrome.

Упаковка расширения под Linux:

#!/bin/bash 7z a -tzip ../domainck-chromium.zip ./* mv ../domainck-chromium.zip ../domainck-chromium.crx
Ключевые слова: Chromium builds for windows HOWTO, Google Chrome download page, Google Chrome sample extensions.


> Заглавная <