Мой последний пост набрал более 5,3 тыс. просмотров! Спасибо за чтение. Я собираюсь продолжить документировать свое исследование разработки расширений для браузеров. На этот раз я займусь простой игрой, которая должна показать, как использовать синхронизированное хранилище, сигналы тревоги и уведомления браузера.

Все в этом посте есть на github — но расширение еще не опубликовано, пока немного подчищаю. Дайте мне знать, если вы будете использовать его!

История до сих пор…

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

Что мы строим?

Я не мог придумать никаких расширений производительности, поэтому я решил, чтобы это было весело, и выбрал простую игру в стиле «кликер печенья». Предложения по будущим расширениям приветствуются в комментариях или в твиттере!

помещение

Пользователь получает баллы за каждый уникальный веб-сайт, который он посетил. Очки суммируются каждые 10 минут. Чем больше веб-сайтов посещает пользователь, тем больше баллов он получает каждые 10 минут.

Обсуждение

Я собираюсь ограничить термин веб-сайт, чтобы люди не использовали дополнительные случайные параметры URL для мошенничества. Поэтому я попытаюсь (наивно) найти корневой домен для любого заданного URL-адреса и сохранить его.

Мне также придется подумать о том, чтобы убедиться, что домен действительно существует. Это может быть сложно с одним javascript, и я не хочу использовать сервер для этого проекта (пока).

Игра очень проста, но имеет потенциал для некоторого забавного развития в будущем, например, популярные домены, дающие пользователю гораздо больше очков за ход, но позволяющие только одному пользователю получать очки с любого одного домена за раз. Это было бы очень весело, но для подтверждения «права собственности» требуется централизованная база данных. Возможно, я займусь этим в следующем посте.

Проблемы

Во-первых, я хочу, чтобы домены, которые посетил человек, отслеживались на всех его устройствах. Мы можем добиться этого, используя синхронизированное хранилище.

Во-вторых, я хочу, чтобы счет игрока обновлялся только каждые 10 минут. Мы не можем использовать setTimeout, потому что фоновая страница будет выгружена для экономии вычислительной мощности, но мы можем использовать для этого chrome.alarms.

Наконец, я хочу сообщить игроку, когда он заработал очки и когда он добавил новый URL в свой арсенал. Уведомления браузера должны быть идеальными для этого.

Создание игры

Я собираюсь очистить свое предыдущее репо и использовать его в качестве шаблона для этого и любых других расширений, которые я, возможно, захочу создать в будущем. Я собираюсь включить jQuery и ссылку на шрифт Google для удобства. Шаблон доступен на github здесь.

Получение URL

Поэтому мы хотим начислять баллы пользователям за каждый уникальный веб-сайт, на который они заходят. Для начала давайте удостоверимся, что у нас есть разрешение tabs для нашего файла манифеста (manifest.json):

//manifest.json
"permissions": [
          "tabs","storage"
        ],

Выглядит неплохо. Теперь давайте получим URL. Мы хотим сделать это, когда пользователь переходит на новую страницу, что мы можем проверить с помощью вкладок onUpdated в файле background.js. Функция обратного вызова дает нам обновленную вкладку, и мы можем прочитать URL-адрес непосредственно из этого объекта.

//background.js
chrome.tabs.onUpdated.addListener(function(id, changeInfo, tab){
  console.log("Updated tab: "+tab.url);
});

Как я уже говорил, я хочу ограничить веб-сайты доменом. Если бы я планировал использовать сервер в этом проекте, я бы выполнил поиск DNS, который недоступен в JS на стороне клиента из-за политики того же происхождения, но я не хочу этих хлопот (и затрат). Более простой и менее надежный способ сделать это — с помощью некоторых манипуляций со строками, и в правильном стиле быстрого взлома я собираюсь использовать функцию, которую я нашел в stackoverflow, чтобы сделать это. Используя функцию extractDomain(), взятую здесь, наш слушатель теперь выглядит так:

//background.js
chrome.tabs.onUpdated.addListener(function(id, changeInfo, tab){
  console.log("Updated tab: "+extractDomain(tab.url));
});

Кажется, это работает хорошо, но мы не проверили, что URL-адрес настоящий. Опять же, здесь может пригодиться поиск DNS, но этого можно избежать. Моей первой мыслью было использовать запрос JSONP, чтобы попытаться обойти политику того же источника и просто проверить ответ. После долгих экспериментов я решил, что это будет ненадежный подход даже для этой простой игры.

Дальнейший поиск обнаружил этот ответ stackoverflow. Он использует Yahoo! YQL API для проверки домена — это похоже на поиск DNS, за исключением того, что нам не нужно писать код на стороне сервера. Счастливые дни!

Вот только это тоже не сработало идеально. Теперь проблема заключается в использовании синхронного запроса AJAX в основном потоке. Решение состоит в том, чтобы нарезать функцию из stackoverflow и делать все асинхронно. :(

Начните с добавления jQuery в наши фоновые скрипты, чтобы мы могли использовать $.ajax() в background.js.

"background": {
    "scripts": ["jquery-3.1.1.min.js","background.js"],
    "persistent": false
  }

Затем используйте функцию проверки URL:

//background.js
//testing the domain exists...
$.ajax({
        url: "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20html%20where%20url%3D%22" + encodedURL + "%22&format=json",
        type: "get",
        dataType: "json",
        success: function(data) {
          if(data.query.results != null){
...

Наконец, прослушиватель onUpdated срабатывает несколько раз для каждого домена. Нам это нужно только один раз, поэтому давайте попробуем уменьшить количество запросов, которые мы делаем, используя объект changeInfo, который мы получили от слушателя.

//background.js
//..listener starts up here
//try to minimize the amount of times we do the check
if(changeInfo.status === "complete"){
    //..url validation goes here.
}

Синхронизированное хранилище

Chrome предлагает хороший API для хранения данных в любом браузере, в который вошел пользователь.

(К сожалению, это недоступно в Opera, но должно работать в Firefox.)

Использовать storage.sync так же просто, как использовать API localstorage, который мы использовали в предыдущем посте. Однако я должен отметить, что синхронизированное хранилище имеет ограничения хранения и ограничения чтения/записи, поэтому я могу в конечном итоге вернуться к локальному хранилищу, если это станет проблемой. А пока давайте притворимся, что это не так, и начнем с проверки, посещал ли пользователь веб-сайт раньше.

//background.js

    //try to find the url in storage
    chrome.storage.sync.get(url, function(result){
      if(result[url]===undefined){
        //domain not found in storage, so store it.
        var storeURL = {};
        storeURL[url] = 1;
        chrome.storage.sync.set(storeURL, function(){
          console.log("Stored: "+url);
        });
      }else{
        //domain has been found in storage, so do nothing.
      }
    });

Тайминги и будильники

Теперь у нас есть несколько сохраненных доменов, давайте дадим пользователю несколько баллов!

Мы хотим обновлять счет каждые 10 минут. Очевидным выбором для тех, кто знаком с javascript, будет setTimeout, который позволяет нам запускать функцию с указанной задержкой перед ее фактическим выполнением. Мы не можем использовать это в нашем расширении, потому что фоновая страница выгружается, когда она не нужна для экономии вычислительной мощности.

Ответ сигнализация:

Используйте API chrome.alarms, чтобы запланировать запуск кода периодически или в указанное время в будущем.

Идеально. Давайте добавим разрешение сигналов тревоги в наш файл манифеста:

//manifest.json
"permissions": [
          "tabs","storage", "alarms"
        ],

Нам не нужно беспокоиться о дублирующихся алармах, поскольку в документации говорится о создании алармов:

Если есть другая тревога с тем же именем… она будет отменена и заменена этой тревогой.

Когда мы распространяем наше расширение, Chrome позволяет планировать будильники только с точностью до минуты. И даже тогда хром не гарантирует, что наш будильник сработает точно в нужное время. Это все для экономии вычислительной мощности. Однако, к счастью, мы можем указать более короткую задержку при тестировании расширения, поэтому нам не нужно ждать несколько минут каждый раз, когда мы хотим проверить наш сигнал тревоги. Подробнее о сигналах тревоги здесь.

//background.js
chrome.alarms.create("Update-score", {
  periodInMinutes:0.1
});

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

//background.js
chrome.alarms.onAlarm.addListener(function(alarm){
  if(alarm.name==="update-score"){
    console.log("alarm fired.");
  }
});

На самом деле нам не нужно проверять название будильника, потому что это наш единственный будильник, но давайте все равно сделаем это на случай, если позже нам понадобится больше.

Теперь нам нужно обновить счет. Мы заменим console.log("alarm fired.")следующим; сначала находим все найденные нами домены, а затем обновляем значение оценки.

//background.js
    //calculate the score the user has earned
    //sync.get(null) should give us all of the keys stored.
    chrome.storage.sync.get(null, function(result){
      //Find how much we should increment the score by
      var scoreIncr = Object.keys(result).length;
      //get the current score
      chrome.storage.sync.get('score', function(result){
        //handle the first ever score update
        if (result.score===undefined) result.score = 0;
        //get new value of score
        var updatedScore = result.score+scoreIncr;
        //store new value
        chrome.storage.sync.set({score:updatedScore}, function(){
          console.log("Score updated!");
        });
      });
    });

Теперь нам нужно отобразить счет для игрока. Добавьте диапазон в popup.html, чтобы отобразить счет.

//popup.html
Score:<span class="score"></span>

Затем обновите значение в диапазоне до текущей оценки внутри нашей функции $(document).ready.

//popup.js
  //display the score
  chrome.storage.sync.get('score', function(result){
    if (result.score===undefined) result.score = 0;
    $(".score").text(result.score);
  });

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

//popup.js
//update the displayed score
chrome.storage.onChanged.addListener(function(changes){
  //make sure it was the score that has been changed.
  if(changes['score']!==undefined){
    //get the score
    chrome.storage.sync.get('score', function(result){
      if (result.score===undefined) result.score = 0;
      $(".score").text(result.score);
    });
  }
});

Уведомления

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

Chrome предоставляет для этого API уведомлений. Давайте начнем с добавления разрешения в наш файл манифеста.

//manifest.json
"permissions": [
          "tabs","storage", "alarms", "notifications"
        ],

Теперь мы воспользуемся методом notifications.create, чтобы отобразить новое уведомление. Мы хотим показывать уведомление при обновлении счета, поэтому давайте заменим console.log("Оценка обновлена!"); сообщение следующего содержания:

//background.js
//show notification
chrome.notifications.create("update-score", {
    type:"image",
    iconUrl:"icon.png",
    title:"New Score!",
    message:"You've earned "+scoreIncr+" more points! Your new score is "+updatedScore,
    imageUrl:"thumbs.jpg"
});

Документация по параметрам уведомлений описывает, что мы должны включить. Я тут поленился и использовал обычную иконку 19x19. Я действительно должен заменить это на более крупный значок, который лучше подходит. Изображение просто для удовольствия.

Я собираюсь установить время будильника на 10 минут, чтобы уведомление не так раздражало. Я также собираюсь создать другое уведомление, чтобы сообщить пользователю, когда он обнаружил новый веб-сайт. Это происходит в нашем слушателе tabs.onUpdated:

//background.js
//show notification
chrome.notifications.create("new-url found", {
    type:"image",
    iconUrl:"icon.png",
    title:"New website discovered!",
    message:"You've discovered a new website and increased your scoring capacity!",
    imageUrl:"internet.jpg"
});

Заканчивать

Вот и все функции, которые я хотел реализовать.

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

Кроме того, мы могли бы также использовать централизованный сервер для отслеживания рекордов и других функций, описанных ранее.

Твоя поддержка

Я не уверен, насколько хорошо это будет воспринято. Последний пост был действительно хорош, но, возможно, это была глупая удача. В любом случае, мне очень нравится делать эти посты и узнавать что-то новое. Если вы хотите видеть больше подобных сообщений, дайте мне знать в комментариях или в твиттере.

Спасибо за чтение.