Как и любой современный web обозреватель, Google Chrome поддерживает расширения. В этой статье я покажу общий подход к написанию extension`ов, после чего Вы без труда напишете свой собственный плагин.
Пример, который я буду показывать, будет называться GoogleMark и его назначением будет метка результатов поиска в google,как показано на скриншоте ниже.

Дабы не утомлять тех, кому достаточно прочесть документацию, приведу сразу на неё ссылку. Там Вы найдёте краткий обзор и руководство по быстрому старту. Но, хочу предостеречь читателя о том, что этих вводных данных не достаточно для комфортной работы. Информация о необходимых мелочах раскидана по всей документации. В этой статье я постараюсь дать ссылки на самые важные части документации.
Итак, расширения для Google Chrome пишутся на JavaScript. Все файлы расширения должны лежать в одном каталоге. Я буду называть этот каталог каталогом расширения.
Для начала я приведу скрипт, который будет раскрашивать страничку результатов поиска google. В нём совсем немного специфичного для расширения кода, по большей части — это простой JavaScript, который Вы используете при обработке любых web-страниц.

googleMarker = {
preparePage: function() {
var links = document.getElementsByClassName("l");
var count = links.length;
for(var i = 0; i < count; ++i) {
links[i].removeAttribute("onmousedown");
}
},

markUrl: function(url, color) {
var block = googleMarker.findMarkingBlockByUrl(url);
googleMarker.markBlock(block, color);
googleMarker.saveMarker(block, color);
},

restoreUrlMark: function(url, color) {
googleMarker.markBlock(googleMarker.findMarkingBlockByUrl(url), color);
},

findMarkingBlockByUrl: function(url) {
var listItems = document.getElementsByClassName("l");
var itemsCount = listItems.length;
for(var i = 0; i < itemsCount; ++i) {
var item = listItems[i];
if(item.href == url) {
return googleMarker.findMarkingBlockByNestedElement(item);
}
}
return null;
},

findMarkingBlockByNestedElement: function(element) {
if(element == null || element == 'undefined') {
return null;
}
if(element.className == 'r') {
return element;
}
return googleMarker.findMarkingBlockByNestedElement(element.parentNode);
},

markBlock: function(block, color) {
if(block == null || block == 'undefined') {
return;
}
var image = googleMarker.getImage(color);
if(image == null) {
return;
}
block.insertBefore(image, block.firstChild);
},

getImage: function(color) {
var url = null;
switch(color) {
case "green":
url = chrome.extension.getURL("green.png");
break;
case "yellow":
url = chrome.extension.getURL("yellow.png");
break;
case "red":
url = chrome.extension.getURL("red.png");
break;
}
if(url == null) {
return null;
}
var image = new Image();
image.src = url;
return image;
},

saveMarker: function(block, color) {
var link = block.getElementsByClassName("l")[0];
var url = link.href;
chrome.extension.sendRequest( { color: color, url: url } );
}
}

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

<div class="vsc" pved="0CBAQkgowAQ" bved="0CBEQkQo" sig="qmv">
<h3 class="r">
<a href="http://brainstream-dev.blogspot.com/2012/03/c-visual-studio-11.html" target="_blank" class="l">
<em>Ещё один блог о программировании</em>: Асинхронное <b>...</b></a>
</h3>
<div class="vspib" aria-label="Подробнее..." role="button" tabindex="0">
<div class="vspii">
<div class="vspiic"></div>
</div>
</div>
<div class="s">
<div class="f kv">
<cite>brainstream-dev.<b>blogspot</b>.com/2012/03/c-visual-studio-11.html</cite>
<span class="vshid">
<a href="http://webcache.googleusercontent.com/search?q=cache:wtBn88UhcVYJ:brainstream-dev.blogspot.com/2012/03/c-visual-studio-11.html+&amp;cd=2&amp;hl=ru&amp;ct=clnk&amp;gl=ru"
onmousedown="return rwt(this,'','','','2','AFQjCNHoJc76l2Yol_mH3A94wFMHV1Z5AA','t0iOo1MA-N91FmlNaAEdNQ','0CBMQIDAB',null,event)"
target="_blank">

Сохраненная&nbsp;копия
</a>
</span>
<button class="gbil esw eswd"
onclick="window.gbar&amp;&amp;gbar.pw&amp;&amp;gbar.pw.clk(this)"
onmouseover="window.gbar&amp;&amp;gbar.pw&amp;&amp;gbar.pw.hvr(this,google.time())"
g:entity="http://brainstream-dev.blogspot.com/2012/03/c-visual-studio-11.html"
g:undo="poS1" title="Рекомендовать эту страницу"
g:pingback="/gen_204?atyp=i&amp;ct=plusone&amp;cad=S1">

</button>
</div>
<div class="esc slp" id="poS1" style="display:none">
Вы уже поставили +1 этой странице.&nbsp;
<a href="#" class="fl">Отменить</a>
</div>
<span class="st">
<span class="f">4 мар 2012 – </span>
<em>Ещё один блог о программировании</em>.
Все статьи этого блога могут быть использованы в соответствии с лицензией GNU FDL.<br>
</span>
</div>
</div>

Добавлять картинки я буду слева от ссылки в блоке «h3» с классом «l». Теперь разберём скрипт чуть подробнее.
Функция preparePage нужна для того, чтобы убрать все обработчики «onmousedown» из ссылок, иначе ссылка изменится чудесным образом и скрипт её уже никогда не узнает.
markUrl и restoreUrlMark делают одно и то же, за исключением того, что markUrl сохраняет метку (об этом чуть позже).
Функция getImage получает картинки из расширения. Об этом тоже чуть позже.
Все остальные функции являются вспомогательными и просто работают с DOM документа. Сохраним файл со скриптом в каталог расширения под названием googleMarker.js.
Итак, я надеюсь, что с тем, как метки будут появляться на страничке, мы разобрались и я перейду непосредственно к теме статьи.

Манифест

Любое расширение Google Chrome должно содержать файл манифеста, определяющий структуру и права Вашего расширения. Манифест для нашего примера будет следующим:

{
"name": "GoogleMark",
"version": "1.0",
"manifest_version": 2,
"description": "Marks google search results",
"content_scripts": [
{
"matches": [
"*://*.google.com/*",
"*://*.google.ru/*"
]
,
"js": [
"googleMarker.js"
]
,
"css": [
"googleMarker.css"
]
}
]
,
"background": {
"page": "background.html"
}
,
"permissions": [
"tabs",
"webNavigation",
"contextMenus",
"*://*.google.com/*",
"*://*.google.ru/*"
]
,
"icons": {
"16": "menu-icon-16x16.png"
}
,
"web_accessible_resources": [
"green.png",
"yellow.png",
"red.png"
]
}

Как видно, манифест представляет собой JSON объект. Файл манифеста сохраняется в корневой каталог расширения и должен иметь имя «manifest.json». Разберём структуру объекта примера.
Поля «name», «version», «manifest_version» и «description» понятны без объяснений.
Ваше расширение может встраивать в страницы собственные JavaScript`ы и каскадные таблицы стилей. Всё это описывается в поле «content_scripts», которое представляет собой массив объектов с указанием списка файла JavaScript`ов и файлов CSS. Кроме указания файлов, можно указывать шаблоны URL тех сайтов, в которые данный контент должен быть встроен. Спецификацию шаблонов можно найти в документации.
Каждое расширение может иметь свою теневую страницу. Эта страница загружается при запуске дополнения и может не иметь никакой разметки, кроме подключения JavaScript`ов. Теневая страница указывается в поле «background».
Вы можете явно задать HTML страницу в параметре «page» или, если Вам не нужна страница, то она может быть сгенерирована автоматически, если Вы укажете список скриптов в параметре «scripts».
Права, которыми обладает расширение вообще и теневая страница в частности, перечисляются в поле «permissions». В это поле вносятся шаблоны URL, к которым есть доступ у скриптов и список разрешённых модулей API.
В поле «icons» перечислены иконки расширения разных размеров. В частности, иконка 16×16 будет использоваться в меню (см. скриншот в начале статьи).
В списке «web_accessible_resources» перечисляются те ресурсы дополнения, которые могут быть доступны из страницы браузера. В частности, те скрипты, которые определены в поле «content_scripts» могут получить только те ресурсы, которые здесь перечислены. Функция getImage, в скрипте, приведённом в начале статьи, загружает файлы «green.png», «yellow.png» и «red.png» используя функцию chrome.extension.getURL для получения URL на эти ресурсы. URL ресурсов дополнения выглядит так:
chrome-extension://[ID пакета дополнения]/[путь]
Полную спецификацию манифеста можно найти в документации.

Теневая страница

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

<html>
<head>
<script type="text/javascript" src="jquery-1.7.2.min.js"></script>
<script type="text/javascript" src="background.js"></script>
</head>
<body>
<ul id="green"></ul>
<ul id="yellow"></ul>
<ul id="red"></ul>
</body>
</html>

Как видно, на теневой странице мы можем использовать любые скрипты, в частности, я подключил jQuery.

Контекстное меню

Для работы с контекстным меню в Google Chrome API служет модуль contextMenus. Для его работы нужно добавить строку «contextMenus» в поле «permissions» манифеста. Добавим в файл background.js функцию для создания меню. Внимание, большинство API функций Google Chrome являются асинхронными и возвращают управление сразу после вызова. Для обработки результатов все асинхронные вызовы опционально принимают функцию обратного вызова.

function createMenu() {
var urls = [
"*://*.google.com/*",
"*://*.google.ru/*"
];

var root = chrome.contextMenus.create({
title: "Mark as",
contexts: [ "link" ],
documentUrlPatterns: urls
});

chrome.contextMenus.create({
title: "Green",
contexts: [ "link" ],
parentId: root,
documentUrlPatterns: urls,
onclick: function(info, tab) {
chrome.tabs.executeScript(tab.id, {
code: "googleMarker.markUrl('" + info.linkUrl + "', 'green')"
});
}
});

chrome.contextMenus.create({
title: "Yellow",
contexts: [ "link" ],
parentId: root,
documentUrlPatterns: urls,
onclick: function(info, tab) {
chrome.tabs.executeScript(tab.id, {
code: "googleMarker.markUrl('" + info.linkUrl + "', 'yellow')"
});
}
});

chrome.contextMenus.create({
title: "Red",
contexts: [ "link" ],
parentId: root,
documentUrlPatterns: urls,
onclick: function(info, tab) {
chrome.tabs.executeScript(tab.id, {
code: "googleMarker.markUrl('" + info.linkUrl + "', 'red')"
});
}
});
}

createMenu();

Всё элементарно. Вызывая функцию chrome.contextMenus.create и передавая в неё детальное описание, мы получаем новый пункт меню. Полное описание объекта, описывающего пункт меню можно посмотреть в документации к функции, а я опишу лишь самые основные.
В параметре documentUrlPatterns перечисляются шаблоны URL адресов, на которых пункт меню будет появляться.
В списке contexts перечисляются типы элементов, при правом щелчке на которые, будет появляться данный пункт меню. Доступные варианты такие: «all», «page», «frame», «selection», «link», «editable», «image», «video», «audio».
Параметр parentId является дескриптором родительского пункта меню. Каждый вызов функции chrome.contextMenus.create возвращает такой дескриптор.
Наиболее интересным, для нас, является поле onclick. Сюда помещается обработчик выбора пункта меню.

Вкладки

В функцию обратного вызова onclick элемента меню, помимо информации об элементе, для которого было вызвано контекстное меню, передаётся информация о вкладке, в которой отображается документ с указанным элементом.
Одной из самых полезных функций модуля tabs (не забудьте указать значение «tabs» в поле «permissions», в манифесте) является функция chrome.tabs.executeScript. Как ясно из названия, она позволяет выполнить скрипт в документе, который находится во вкладке, идентификатор которой передаётся первым параметром. Вторым параметром передаётся объект, содержащий код скрипта или имя файла со скриптом. Работу этой функции и демонстрируют обработчики onclick пунктов меню.

Запросы

Для синхронизации работы встраиваемых скриптов с теневой страницей Google Chrome API предоставляют возможность скриптам обмениваться информацией посредством запросов. Напомню, как выглядит функция сохранения метки в файле googleMarker.js

saveMarker: function(block, color) {
var link = block.getElementsByClassName("l")[0];
var url = link.href;
chrome.extension.sendRequest( { color: color, url: url } );
}

В этой функции отсылается запрос с объектом, описывающим маркер для сохранения. Вообще говоря, Вы можете передавать любой объект в качестве запроса в функцию chrome.extension.sendRequest. Кроме того, этой функции можно передать идентификатор дополнения и функцию прослушки ответов.
Для работы с функциями из модуля extension не требуется никаких указаний в манифесте.
Чтобы наше расширение смогло реагировать на запросы, следует подписаться на их прослушивание. В нашем случае это будет выгладить так:

chrome.extension.onRequest.addListener(
function(request, sender, sendResponse) {
var $list = $("#" + request.color);
if($list.lengh == 0) {
return;
}
$list.append($("<li>").append(request.url));
}
);

Параметры функции обработки события chrome.extension.onRequest довольно очевидны; это запрос, отправитель запроса (идентификаторы вкладки и дополнения) и функция отправки ответа.
В нашем случае, мы, воспользовавшись jQuery, добавляем в теневую страницу новый элемент списка.

События навигации

Google Chrome API предоставляют возможность отслеживать события навигации с помощью модуля webNavigation. Для его использования следует добавить в Ваши «permissions» элемент «webNavigation» в файле манифеста. Модуль предоставляет несколько полезных событий и функций, я же продемонстрирую использование события chrome.webNavigation.onDOMContentLoaded, наступающего, как и следует из названия, после загрузки дерева элементов документа. Мы воспользуемся этим событием для подготовки страницы (удаления атрибута onmousedown) и восстановления сохранённых меток.

chrome.webNavigation.onDOMContentLoaded.addListener(
function(details) {
if(details.frameId != 0) {
return;
}
chrome.tabs.executeScript(details.tabId, {
code: "googleMarker.preparePage()"
}
);
var colors = ["green", "yellow", "red"];
for(var i = 0; i < colors.length; ++i) {
var $urls = $("#" + colors[i] + " li");
if($urls.length == 0) {
continue;
}

$urls.each(function() {
chrome.tabs.executeScript(details.tabId, {
code: "googleMarker.restoreUrlMark('" +
this.innerText + "', '" + colors[i] + "')"
}
);
});
}
}
);

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

Завершение

Последнее, что нам осталось сделать — это добавить файлы googleMarker.css, menu-icon-16×16.png, green.png, yellow.png и red.png для отображения всех необходимых элементов.

.r img {
margin-right: 15px;
}

Теперь плагин готов к установке. Для этого переходим в Google Chrome в меню -> Tools -> Extesions и ставим галку на Developer mode. Нажимаем Load unpacked extension и выбираем каталог с расширением.

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

Иконка и popup

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

Для этого добавляем файл иконки (icon.png) и два файла для представления popup окна: popup.html и popup.js. Заносим новые сведения в файл манифеста, добавляя поле browser_action

{
"name": "GoogleMark",
"version": "1.0",
"manifest_version": 2,
"description": "Marks google search results",
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
}
,

"content_scripts": [
{
"matches": [
"*://*.google.com/*",
"*://*.google.ru/*"
]
,
"js": [
"googleMarker.js"
]
,
"css": [
"googleMarker.css"
]
}
]
,
"background": {
"page": "background.html"
}
,
"permissions": [
"tabs",
"webNavigation",
"contextMenus",
"*://*.google.com/*",
"*://*.google.ru/*"
]
,
"icons": {
"16": "menu-icon-16x16.png"
}
,
"web_accessible_resources": [
"green.png",
"yellow.png",
"red.png"
]
}

Разметка popup.html очень проста

<html>
<body>
<button id="clear-btn">Clear markers</button>
<script src="popup.js" type="text/javascript"></script>
</body>
</html>

Здесь только кнопка и подключение скрипта. Выполнение inline скриптов снова отключено по соображениям безопасности.
В файле popup.js мы подписываемся на нажатие кнопки и делаем запрос к фоновой странице.

var button = document.getElementById("clear-btn");
button.addEventListener("click", function() {
chrome.extension.sendRequest( { action: "clear" } );
});

Так как у нас появляется второе событие, то вводим поле action в запрос. По этой причине дописываем значение «save» в запросе на сохранение маркера (файл googleMarker.js)

saveMarker: function(block, color) {
var link = block.getElementsByClassName("l")[0];
var url = link.href;
chrome.extension.sendRequest( { action: "save", color: color, url: url } );
}

Осталось немного поправить обработчик запросов:

chrome.extension.onRequest.addListener(
function(request, sender, sendResponse) {
switch(request.action) {
case "save":
var $list = $("#" + request.color);
if($list.lengh == 0) {
return;
}
$list.append($("<li>").append(request.url));
break;
case "clear":
$("ul").empty();
break;
}
}
);

Всё готово! Жмём кнопку Reload в менеджере расширений и у нас появляется значок в верхнем правом углу браузера. Теперь Вы можете упаковать ваше расширение в файл *.crx нажав кнопку Pack extestion в менеджере расширений или залить его в Chrome Web Store.
Скачать исходный код примера можно здесь.