Пишем расширение для google chrome

Написать расширение для google chrome несложно. Но при написании первого раширения могут возникнуть (и возникают) вопросы. Большинство мануалов по написанию первого расширения расчитаны на использования манифеста первой версии, поддержка которого в скором будущем прекратится.

В этой статье будет рассмотрено:
  • Как составлять манифест v.2
  • Как работать с удаленными ресурсами
  • Как работать с cookies
  • Как работать с local storage
  • Как работать с уведомлениями


Введение


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

Манифест


Начнем создавать расширение с самого начала, то есть с манифеста. Манифест – это тот самый файл, в котром прописываются все параметры расширения. Название, описание, версия, разрешение на доступ к сайтам, разрешение на использование кук, уведомлений, локального хранилища. В общем, манифест – это мозг расширения. Создаем файл manifest.json. Манифест – единственный файл, который должен иметь заранее предопределенное имя, все остальные файлы можно будет называть как угодно. В этом файле есть три обязательных поля:

manifest.json
{
	“name”:  “Organizer extension”,  // Название расширения
	“version”: “1.0”,  // Версия расширения.
	“manifest_version”: 2 // Версия манифеста
}


Тут есть пара правил:
  • Версия манифеста должна быть целочисленной, то есть должна писаться как 2, а не “2”.
  • Версия расширения должна быть строковой, но содержать только числа и точки, то есть “1.0” — хорошо, а 1.0 и “0.9 beta” — плохо.


С обязательными полями – все, перейдем к созданию всплывающего окна расширения. Для того, чтобы по нажатию на пиктограмму, открывалось окно, необходимо добавить в манифест поле “browser_action”

manifest.json
{
…
"browser_action": {
	"default_title": "Open organizer", // Заголовок. Его видно если навести курсор на иконку в браузере
	"default_icon": "icon_small.png", // Путь к иконке расширения
	"default_popup": "popup.html" // Путь к странице с попапом
}
}


Теперь создадим всплывающее окно. Это обычная html страница, которая может быть любого размера и цвета, никаких фокусов. Назовем файл “popup.html”. Создать этот файл мало – его надо указать в манифесте. Так мы и сделали: «default_popup»: «popup.html».

popup.html
<!DOCTYPE html>
<html>
	<head>
		<title></title>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	</head>
	<body>
		<div>It works!</div>
	</body>
</html>

Добавление расширения в браузер


Теперь пришло время проверить работоспособность нашего расширния. Для этого загрузим расширение в браузер. Открываем в хроме меню расширений. Ставим птицу на “Developer mode”.

После этого появятся три кнопки. Нажимаем “Load unpacked extension...”. Выбираем папку с файлами расширения. После этого появится наше расширение. Если все правильно, то по нажатию на иконку – повится окно:


Подключение скриптов


Теперь можно приступить к интересному. Подключим два javascript файла. Первый – popup.js, второй – jquery. С первым проблем не возникнет, но jquery будем подключать не локальный, а удаленный, взятый по адресу ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js. Проблемы возникнут от того, что по умолчанию расширение не имеет доступа к сторонним ресурсам. Чтобы получить доступ, надо его указать в манифесте. Доступ к чему-либо указывается в поле “permissions”. Так же, для удаленных скриптов и css надо указывать доступные удаленные ресурсы.

manifest.json
{
…
"permissions": [
	"https://ajax.googleapis.com/*"
],
"content_security_policy": "script-src 'self' https://ajax.googleapis.com; object-src 'self'"
}

Теперь подключим эти скрипты в popup.html

popup.html
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script src="popup.js"></script>

Storage


При помощи storage в хроме можно хранить пользовательские данные. И именно в storage наше расширение и будет хранить грядущие события. На то есть две причины. Во-первых, данные, хранищиеся в storage можно синхронизировать, если залогиниться в браузере. А во-вторых, данные можно хранить не только в виде строки, как в cookies, а в любом виде, то есть можно хранить и массивы и объекты. Чтобы это заработало, откроем доступ к storage в манифесте.

manifest.json
{
...
"permissions": [
  …
  "storage"
]
...
}

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

popup.html
<div id="container">
  <div id="today_date">Date</div>
  <table class="table">
    <tr>
      <td>Дата</td>
      <td>Время</td>
      <td>Задача</td>
    </tr>
    <tr>
      <td><input type="text" id="new_date" value="" class="input_date" /></td>
      <td><input type="text" id="new_time" value="" class="input_date" /></td>
      <td><input type="text" id="new_task" value="" class="input_task" /></td>
    </tr>
  </table>
  <ul></ul>
</div>

И сразу же добавим отображение даты в блоке #today_date.

popup.js
$(function(){
  var today = new Date();
  $("#today_date").html(today.getDate()+"."+(parseInt(today.getMonth())+1)+"." + today.getFullYear());
}

Выглядеть должно так:


Итак, при нажатии на кнопку “+” у нас должно добавляться событие. Вначале файла объявим глобальную переменную storage – объект для работы с storage, а так же глобальный массив tasks для хранения событий.

popup.js
var storage = chrome.storage.sync;
var tasks = new Array();
$(function(){
  …
  $("#add_task").click(function(){
    var new_task = new Object();
    new_task.date = validateField($("#new_date").val(), "date");
    new_task.time = validateField($("#new_time").val(), "time");
    new_task.task = $("#new_task").val();
    if(!new_task.task || !new_task.date || !new_task.task){
       return false;
    }
    tasks[tasks.length] = new_task;
    storage.set({
      tasks:tasks
    });
  });
});

Функция валидации проверяет, что дата записана в формате d.m.yyyy, а время в формате hh:mm, а так же, что в описании события не меньше трех символов.

popup.js
var validateField = function(val, type){
    if(type == "date"){
        var date = val.split(".");
        var year = new Date();
        year = year.getFullYear();
        if(date.length == 3 && parseInt(date[0]) == date[0] && date[0] <= 31 && parseInt(date[1]) == date[1] && date[1] <= 12 && parseInt(date[2]) == date[2] && date[2] >= year){return val;}
    } else if(type == "time"){
        var time = val.split(":");
        if(time.length == 2 && parseInt(time[0]) == time[0] && time[0] < 24 && parseInt(time[1]) == time[1] && time[1] < 60){return val;}
    } else if(type == "task" && type.length >= 3){
        return val;
    }
    return null;
}

С добавлением разобрались, переходим к получению событий на сегодня. Для этого надо получить все события из базы, выбираем из всех только сегодняшние события и сортируем их по времени по возрастанию.

popup.js
$(function(){
  …
  var now_hours = today.getHours() < 10 ? "0" + today.getHours() :  today.getHours();
    var now_minutes = today.getMinutes() < 10 ? "0" + today.getMinutes() : today.getMinutes();
    var now_time = now_hours + "" + now_minutes;
    storage.get("tasks",function(items){
        if(items.tasks){
            tasks = items.tasks;
            var today_tasks = getTodayTasks(tasks);
            if(today_tasks.length >0){
                for(var i in today_tasks){
                    var this_time = today_tasks[i].time.replace(":", "");
                    var add = this_time > now_time ? "" : ' class="done"';
                    var add_html = '<li'+add+'><strong>'+today_tasks[i].time+'</strong> '+today_tasks[i].task+'</li>';
                    $("ul").append(add_html);
                }
            }
        }
    });
  …
});

Функция getTodayTasks() возвращает из общего списка только события с сегодняшней датой.

popup.js
var getTodayTasks = function(tasks){
    var today_tasks = new Array();
    var today = new Date();
    var today_date = today.getDate()+"."+(today.getMonth() + 1 )+ "." + today.getFullYear();
    for(var i in tasks){
        if(tasks[i].date == today_date){
            today_tasks[today_tasks.length] = tasks[i];
        }
    }
    if(today_tasks.length > 0){
        today_tasks = sortTasks(today_tasks);
    }
    return today_tasks;
}

Функция sortTasks() сортирует события по возрастанию времени.

popup.js
var sortTasks = function(tasks){
    if(tasks.length > 0){
        var swapped = true;
        while (swapped) {
            swapped = false;
            for (var i=0; i < tasks.length-1; i++) {
                var this_time = tasks[i].time.replace(":", "");
                var next_time = tasks[i+1].time.replace(":", "");
                if (this_time > next_time) {
                    var temp = tasks[i];
                    tasks[i] = tasks[i+1];
                    tasks[i+1] = temp;
                    swapped = true;
                }
            }
        }
    }
    return tasks;
}

Уведомления


Пришло время настроить отображение уведомлений на экране. Добавим во всплывающее окно специальный чекбокс. Если этот чекбокс будет отмечен – уведомлениея будут показываться, если не будет отмечен – не будут. Так же добавим текстовый инпут. Цифра в этом инпуте будет показывать, за какое время до событя будет показываться уведомление. То есть если у нас событие назначено на 19:00, в этом текстовом инпуте будет 5, значит в 18:55 появится уведомление. Добавим в popup.html код с этими инпутами

popup.html
<div>
  <input type="checkbox" id="show_notifications" checked="checked" />
  <input type="text" id="when_to_notify" class="input_date" value="" /> 
  Показывать уведомления
</div>



Теперь давайте разберемся с тем, как это будет работать. При нажатии на чекбокс, будет проверяться его атрибут checked, значение атрибута будет записываться в cookie “show_notifications”. Перейдем к текстовому инпуту. По изменению его значения, новое значение будет валидироваться, если оно целочисленное и не больше 120, записываем новое значение в cookie “when_to_notify”.

Но для того, чтобы у нас это заработало, надо открыть доступ к cookies. Для этого заходим в manifest.json и добавляем в “permissions”

manifest.json
{
...
"permissions": [
	…
	“cookies”
]
...
}

Теперь можно приступать к скрипту. Заходим в popup.js. Для начала установим первоначальные значения в инпутах. По-умолчанию чекбокс не отмечен, то есть уведомления не показываются, а время равно 0. При клике на чекбокс, будет меняться значение cookie “show_notifications”. При изменении значения в тектовом поле, будет меняться значение cookie “when_to_notify”.

popup.js
$(function(){
    setCheckbox();
    setWhenToNotify(getCookie("when_to_notify"));
    ...
    $("#show_notifications").click(function(){
        setCookie("show_notifications", document.getElementById("show_notifications").checked);
    });
    $("#when_to_notify").change(function(){
        setWhenToNotify($(this).val());
    });
});

Рассмотрим подробнее функции. Начнем с функций работы с cookies. В данном случае были взяты готовые функции с w3schools.com.

popup.js
var setCookie = function(c_name,value,exdays){
    /*
     *Взято с http://www.w3schools.com/js/js_cookies.asp
     */
    var exdate=new Date();
    exdate.setDate(exdate.getDate() + exdays);
    var c_value=escape(value) + ((exdays==null) ? "" : "; expires="+exdate.toUTCString());
    document.cookie=c_name + "=" + c_value;
}

var getCookie = function(c_name){Позвонить Васе П.
    /*
     *Взято с http://www.w3schools.com/js/js_cookies.asp
     */
    var i,x,y,ARRcookies=document.cookie.split(";");
    for (i=0;i<ARRcookies.length;i++){
        x=ARRcookies[i].substr(0,ARRcookies[i].indexOf("="));
        y=ARRcookies[i].substr(ARRcookies[i].indexOf("=")+1);
        x=x.replace(/^\s+|\s+$/g,"");
        if (x==c_name){
            return unescape(y);
        }
    }
}

Разберемся с функцией setCheckbox(). Она получает cookie “show_notifications” и проверяет, если полученное значение равно “true”(текстовое, да), то параметр checked у чекбокса true, иначе false.

popup.js
var setCheckbox = function(){
    var val = getCookie("show_notifications")
    document.getElementById('show_notifications').checked = ((val == "true") ? true : false);
}

Теперь рассмотрим setWhenToNotify(). Она принимает новое значение таймера. Если это значение – целочисленное и не больше 120 минут, то в cookie “when_to_notify” устанавливается новое значение. Если переменная не прошла эту валидацию, то в инпут возвращается предыдущее значение из cookies “when_to_notify”.

popup.js
var setWhenToNotify = function(val){
    var last_val = getCookie("when_to_notify");
    last_val = last_val != "undefined"? last_val: 0;
    val = (parseInt(val)==val && val <=120) ? val : last_val;
    setCookie("when_to_notify", val);
    $("#when_to_notify").val(val);
}

Перейдем к самим уведомлениям. Для этого откроем доступ к уведомлениям и подключим фоновый background.js. Нужно подключить именно фоновый файл, так как если уведомления вызывать из popup.js, то уведомления будут появляться только если открыто всплывающее окно.

manifest.json
{
...
"permissions": [
    …
    "notifications"
    ],
"background": { "scripts": ["background.js"] },
"web_accessible_resources": ["icon_small.png"],
...
}

Последняя строчка дает доступ к удаленному файлу. Дело в том, что картинка, которая отображается в уведомлении обязательно должна быть доступна расширению удаленно. В данном случае файл локальный, но доступ открывать все равно надо. Теперь возьмемся за background.js. Объявим переменную storage и пустой массив tasks. Далее раз в минуту скрипт будет получать список сегодняшних событий и получать из них список задач, которые должны произойти через указанное время. После этого для каждой такой задачи будет показано уведомление.

background.js
var storage = chrome.storage.sync;
var tasks = new Array();
setInterval(function() {
    storage.get("tasks",function(items){
        if(items.tasks && getCookie("show_notifications") == "true"){
            tasks = getTodayTasks(items.tasks);
            if(window.webkitNotifications){
                var texts = getNextTask(tasks);
                for(var i in texts){
                    show(texts[i]);    
                }
            }
        }
    });
}, 60000);

Функции getTodayTasks() и getCookie() взяты из popup.js. Так что начнем разбор с функции getNextTask(). Функция сравнивает текущее время и время события, если оно равно тому значению, которое хранится в cookie “when_to_notify”, то в массив next дописывается строка из времени события и его описания. После проверки всех событий возвращет массив next.

background.js
var getNextTask = function(tasks){
    var now = new Date();
    now = now.getTime();
    var next = new Array();
    for(var i in tasks){
        var date = tasks[i].date.split(".");
        var time = tasks[i].time.split(":");
        var task_date = new Date(parseInt(date[2]), parseInt(date[1]-1), parseInt(date[0]), parseInt(time[0]), parseInt(time[1]), 0, 0);
        task_date = task_date.getTime();
        var delta = Math.round((task_date-now)/60000);
        if(delta == getCookie("when_to_notify")){
            next[next.length] = tasks[i].time + " - " + tasks[i].task;
        }
    }
    return next;
}

Функция show() показывает уведомление с заданным текстом.

background.js
var show = function(text) {
    var notification = window.webkitNotifications.createNotification('icon_small.png','Есть дело', text);
    notification.show();    
}

Результатом работы этого скрипта будет такое вот уведомление:


Послесловие


Как и обещано, в конце статьи у нас есть готовое расширение-органайзер для Google Chrome.
Теперь добавим расширение в Chrome Web Store. Загружать надо расширение, запакованное в .zip-архив. Для начала заходим в Web Store. Для этого заходим в хроме на вкладку “Приложения” и нажимаем кнопку “Web Strore”

Теперь заходим в меню. Для этого нажимаем шестиренку и открываем “Developer dashboard”

Нажимаем большую кнопку “Add new item”. После этого надо будет выбрать zip-архив с расширением нажать “upload”

Далее надо заполнить небольшую форму с описанием расширения. Теперь есть выбор либо сохранить расширение в черновил либо опубликовать. Просто так опубликовать его не получится так как регистрация в Web Store стоит 5 $. Плату за регистрацию надо платить один раз для аккаунта, а не для каждого расширения. После оплаты появляется позможность опубликовать расширение, но и тут надо быть готовым к тому, что его будут валидировать несколько дней.

Полезные ссылки


Описание полей манифеста: developer.chrome.com/trunk/extensions/manifest.html
Работа с storage: developer.chrome.com/trunk/extensions/storage.html
Информация по уведомлениям: developer.chrome.com/extensions/notifications.html
Исходники расширения: github.com/bozheville/orginaizer_extension
Поделиться публикацией

Похожие публикации

Комментарии 17

    0
    Действительно, мануалы в интернет устаревшие… Спасибо за свежую статью!
      +6
      Ну почему же устаревшие, на developer.chrome.com все актуально обычно, даже доки для Canary-версий есть.
        +1
        Тогда поправлюсь: на русском языке.
      +1
      Мне кажется, зря вы дату и время в разных полях объекта task храните. Это чем-то обусловлено? Лучше бы сделать поле task.deadline, которому присвоить объект Date().

      Я не придираюсь, просто обсуждаю)
        0
        Так в чем отличие второго манифеста от первого?
          0
          Главное отличие на мой взгляд — гораздо более жесткие требования к секьюрити. Обязательное соблюдение CSP было для меня в свое время нехилой головной болью при миграции с первой на вторую версию.

          А так — просто небольшое изменение формата.
          +1
          Про Storage не знал, спасибо. Очень не хватало такой штуки когда-то.
          Кстати, в примерах Вы таки работаете с sync, а не local.
            –1
            Чего-то у меня не получилось событие дать через addEventListener...., пришлось jq подключать.
              0
              Всё должно отлично работать. Может элемент ещё не существовал?
                –1
                под window.onload положил не должно…
                +6
                пойду-ка спать лягу после такого кода:

                if(date.length == 3 && parseInt(date[0]) == date[0] && date[0] <= 31 && parseInt(date[1]) == date[1] && date[1] <= 12 && parseInt(date[2]) == date[2] && date[2] >= year){return val;}
                
                  0
                  Эм. При попытке загрузки расширения, хром мне говорит, что ключи в dictionary должны быть взяты в кавычки, код:

                  {
                  	"name" : "name",
                  	"version" : "1.0",
                  	"manifest_version" : 2,
                  	
                  	"browser_action" : {
                  		"default_title" : "my title",
                  		"default_icon" : "icon.png",
                  		"default_popup" : "popup.html"
                  	}
                  }
                  
                    0
                    Попробовал Ваш код. Работает, загружается. Может быть проблема в popup.html?
                    0
                    bozheville
                    Скажите, пожалуйста, в чем же все-таки отличие chrome.storage.sync от просто localStorage?
                      0
                      И то, и то дает доступ к хранилищу данных.

                      Через localStorage можно обращаться для чтения/записи данных во всех новых браузерах (в ИЕ 7 и ниже не работает). Данные хранятся на машине пока их не удалить. Аналогично этому есть sessionStorage, принцип работы тот же, но данные хранятся только до тех пор, пока не закроется вкладка сайта. В этом хранилище данные можно хранить только как строку, так что если необходимо хранить массив или объект надо перед записью закодировать в json. Для доступа к локальному хранилищу на сайте лучше использовать localStorage или sessionStorage.

                      chrome.storage — это хранилище браузера хром. В нем даннные хранятся пока их не удалить. Данные можно хранить в любом виде (число, строка, массив, объект). Если использовать chrome.storage.local, то данные будут храниться только на данной машине, если же использовать chrome.storage.sync, и если пользователь залогинен в хроме, то данные будут синхронизироваться между всеми браузерами, где залогинен пользователь. При написании расширений для хрома лучше использовать chrome.storage.
                      0
                      Код почти тот же, единственное, сделана работа под Crome 18, который последний стоит в Ubuntu 10.10 и там еще нет chrome.storage, а лишь window.localStorage github.com/tapin13/Tasker
                      Возможно доработаю этот плагин, но первый коммит (почти) здешний код.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое