пятница, 2 декабря 2011 г.

Расширения для Google Chrome. Часть первая. Getting started

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

В первой (этой, то бишь) статье, на примере простейшего расширения, будут рассмотрены все основные моменты, связанные с разработкой, отладкой и использованием расширения, конфигурационный файл manifest.json и начала chrome.* API. Первая же статья, думаю, будет не очень полезна опытным разработчикам (это дисклеймер).


Hello world


Лучшая теория — практика, а поэтому, не откладывая в долгий ящик, создаём папку hello_world, а в ней текстовый документ manifest.json и печатаем туда следующий код:
{
    "name" : "Hello world", //Название расширения
    "version" : "1.0", //Версия
    "description" : "This is a simple chrome extention" //Краткое описание
}

Это — программа минимум. Если зайти в «Гаечный ключ → Инструменты → Расширения», установить галку «Режим разработчика», нажать кнопку «Загрузить распакованное расширение...» и указать нашу папку «Hello world», то расширение появится в списке установленных, но делать, естественно, оно пока ничего не будет, потому как не умеет.

image

Учим и отлаживаем


А раз не умеет — надо научить. Отредактируем manifest.json:
{
    "name" : "Hello world", 
    "version" : "1.0", 
    "description" : "This is a simple chrome extention",
    "background_page" : "background.html"
}

И создадим файл background.html в котором будет написан сценарий, выполняемый в фоновом режиме. Например, такой:
<script type="application/javascript">
    window.onload = function() {
        window.setInterval( function() {
            console.log("Hello world");
        }, 10000);
    }
script>

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

Для отладки удобно использовать служебную страницу chrome://extensions/ со включённым режимом разработчика.

image

В принципе, она дублирует функционал страницы управления расширениями из «Гаечного ключа», но мне, субъективно, нравится больше. Как-то компактнее, что ли?

Здесь нас интересуют две позиции: строка «ID» с внутренним идентификатором расширения и подраздел «Проверить активные режимы просмотра» в котором мы видим созданный нами background.html и, щёлкнув по нему, можем проконтролировать исполняемый сценарий.

Смотрим и убеждаемся, что сценарий исправно пишет в консоль хэллоуворлды:

image

Обратите внимание на заголовок формата chrome-extension://ID/filename. Зная идентификатор расширения таким образом можно добраться до любого его файла. Опять же удобно в процессе отладки расширения.

Взаимодействие с браузером


Пока наше расширение представляет эдакую вещь в себе, исполняя в фоне некий сценарий. Для того, чтобы оно начало взаимодействовать с браузером и его компонентами нужно познакомиться с chrome.* API. Так, например, для взаимодействия с окном браузера используются методы chrome.browserAction, а значения по умолчанию задаются в manifest.json следующим образом:
{
    "name" : "Hello world", 
    "version" : "1.0", 
    "description" : "This is a simple chrome extention",
    "background_page" : "background.html",

    "browser_action" : {
        "default_title" : "Hello world!!!", //Текст, всплывающий при наведении курсора на иконку (если не задан, то всплывает название расширения)
        "default_icon" : "img/icon_world.png", //Иконка для панели расширений (по умолчанию)
        "default_popup" : "popup.html" //Всплывающее окно при клике на иконке
    }
}

Не забываем создать popup.html (пока оставим его пустым) и положить иконку в папку img, щёлкаем на «Перезагрузить» на странице chrome://extensions/ и смотрим на результат. Иконка нашего расширения появилась на панели расширений, а при клике на неё возникает пустое всплывающее окошко.

image

Иконка для тех, кто проходит по шагам:
image

Всё это управлябельно с помощью методов chrome.browserAction из сценариев:
<script type="text/javascript">
    chrome.browserAction.setTitle({title:"New title"}); //Устанавливает новый всплывающий при наведении на иконку текст
    chrome.browserAction.setPopup({popup:"new_popup.html"}); //Устанавливает новое всплывающее окно при клике на иконке
    chrome.browserAction.setIcon({path:"new_icon.png"}); //Устанавливает новую иконку
    chrome.browserAction.setBadgeText({text:"text"}); //Устанавливает текст поверх иконки
    chrome.browserAction.setBadgeBackgroundColor({color:[0,0,0]}); //Устанавливает фон текста поверх иконки
script>


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

background.html
<script type="application/javascript">
    window.onload = function() {
        window.setInterval( function() {
            var now = new Date();
            var h = now.getHours();
            var m = now.getMinutes();
            var s = now.getSeconds();
            
            var badge_text = (m < 10 ? "0" + m : m).toString();
            var title_text = (h < 10 ? "0" + h : h) + ":" + (m < 10 ? "0" + m : m) + ":" + (s < 10 ? "0" + s : s);
            
            chrome.browserAction.setBadgeText({text: badge_text});
            chrome.browserAction.setTitle({title: title_text});
        }, 1000);
    }
script>


popup.html

<html>
    <head>
        <style type="text/css">
            * { margin: 0; padding: 0; border: 0; }
            body { background: #000; }
        style>
        <script type="text/javascript">
            Clock = function() {
                this.canvas = false;
                this.pi = Math.PI;
            }

            Clock.prototype = {

                get_time: function() {
                    var now = new Date();
                    var result = {
                        milliseconds: now.getMilliseconds(),
                        seconds: now.getSeconds(),
                        minutes: now.getMinutes(),
                        hours: now.getHours()
                    }
                    return result;
                },

                init: function() {
                    this.canvas = document.getElementById("clock").getContext("2d");
                },
    
                draw: function() {		
                    var now = this.get_time();
                    var hangle = (this.pi/6)*now.hours + (this.pi/360)*now.minutes + (this.pi/21600)*now.seconds + (this.pi/21600000)*now.milliseconds;
                    var mangle = (this.pi/30)*now.minutes + (this.pi/1800)*now.seconds + (this.pi/1800000)*now.milliseconds;
                    var sangle = (this.pi/30)*now.seconds + (this.pi/30000)*now.milliseconds;
        
                    this.canvas.save();
                    this.canvas.fillStyle = "#000";
                    this.canvas.strokeStyle = "#000";
                    this.canvas.clearRect(0,0,200,200);
                    this.canvas.fillRect(0,0,200,200);
                    this.canvas.translate(100,100);
                    this.canvas.rotate(-this.pi/2);		
        
                    this.canvas.save();
                    this.canvas.rotate(hangle);
                    this.canvas.lineWidth = 8;
                    this.canvas.strokeStyle = "#ffffff";
                    this.canvas.fillStyle = "#ffffff";
                    this.canvas.lineCap = "round";
                    this.canvas.beginPath();
                    this.canvas.moveTo(-10,0);
                    this.canvas.lineTo(50,0);
                    this.canvas.stroke();
                    this.canvas.restore();

                    this.canvas.save();
                    this.canvas.rotate(mangle);
                    this.canvas.lineWidth = 4;
                    this.canvas.strokeStyle = "#ffffff";
                    this.canvas.lineCap = "square";
                    this.canvas.beginPath();
                    this.canvas.moveTo(-20,0);
                    this.canvas.lineTo(75,0);
                    this.canvas.stroke();
                    this.canvas.restore();

                    this.canvas.save();
                    this.canvas.lineWidth = 2;
                    this.canvas.strokeStyle = "#ffffff";
                    this.canvas.fillStyle = "#333";
                    this.canvas.beginPath();
                    this.canvas.arc(0,0,8,0,this.pi*2,true);
                    this.canvas.fill();
                    this.canvas.stroke();
                    this.canvas.restore();		
        
                    this.canvas.save();
                    this.canvas.rotate(sangle);
                    this.canvas.lineWidth = 2;
                    this.canvas.strokeStyle = "#ff0000";
                    this.canvas.lineCap = "square";
                    this.canvas.beginPath();
                    this.canvas.moveTo(-30,0);
                    this.canvas.lineTo(85,0);
                    this.canvas.stroke();
                    this.canvas.restore();		

                    this.canvas.save();
                    this.canvas.lineWidth = 6;
                    this.canvas.fillStyle = "#ff0000";
                    this.canvas.beginPath();
                    this.canvas.arc(0,0,3,0,this.pi*2,true);
                    this.canvas.fill();
                    this.canvas.restore();
        
                    this.canvas.save();
                    this.canvas.lineWidth = 6;
                    this.canvas.strokeStyle = "#ffffff";
                    this.canvas.beginPath();
                    this.canvas.arc(0,0,95,0,this.pi*2,true);
                    this.canvas.stroke();
                    this.canvas.restore();
        
                    this.canvas.restore();
                }
        
            }

            window.onload = function() {
                var clock = new Clock();
                clock.init();
                window.setInterval(function() {
                    clock.draw();
                }, 10);
            }		
        script>
    head>
    <body>
         id="clock" width="200" height="200">
    body>
html>


Сохраняем, перезапускаем, проверяем — красота!
image

Собственно, мы сделали простое расширение (а заодно и canvas припомнили). Для Getting Started, во всяком случае, достаточно. Осталось только привести его к годному для распространения виду — упаковать. Для этого на той же странице chrome://extensions/ давим на «Упаковка расширений...», указываем корневой каталог (тот, где лежит manifest.json), давим «Ок» и получаем файл формата *.crx на выходе. Это и есть наше упакованное расширение. Открыв его с помощью Хрома, мы установим расширение.

Упакованный пример из статьи для установки
Архив с исходниками

В следующей статье цикла я планирую подробно разобрать chrome.* API, а в дальнейшем — взаимодействие с различными сайтами и использование локальных хранилищ данных. Если вы считаете, что я что-то упустил в азах или у вас есть пожелания по поводу следующих статей цикла — прошу изложить их в комментариях.

See ya!

Комментариев нет: