Статьи

Создание расширения Chrome для Diigo, часть 2

В первой части мы представили несколько новых концепций, объяснили, как мы собираемся создать расширение, и продемонстрировали использование API Diigo . В этой части мы создадим большинство наших вспомогательных функций и разберемся с обработкой ошибок.

Обработка ошибок

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

Чтобы немного очистить код и сделать background.js более кратким, я сжал объект Base64 в минимизированную строку. Файл background.js в том виде, как он есть сейчас, выглядит следующим образом . Вы можете начать с этого, если вы следуете вместе с кодом.

Часть xml.readyState === 4 проверяет, завершен ли запрос. После завершения мы можем проверить код состояния. Только 200 означает «успех», все остальные означают, что что-то пошло не так. Используя список возможных ответов , мы изменим наш код, чтобы создать удобочитаемое описание произошедшей ошибки.

 var possibleErrors = { 400: 'Bad Request: Some request parameters are invalid or the API rate limit is exceeded.', 401: 'Not Authorized: Authentication credentials are missing or invalid.', 403: 'Forbidden: The request has been refused because of the lack of proper permission.', 404: 'Not Found: Either you\'re requesting an invalid URI or the resource in question doesn\'t exist (eg no such user).', 500: 'Internal Server Error: Something is broken.', 502: 'Bad Gateway: Diigo is down or being upgraded.', 503: 'Service Unavailable: The Diigo servers are too busy to server your request. Please try again later.', other: 'Unknown error. Something went wrong.' }; xml.onreadystatechange = function() { if (xml.readyState === 4) { if (xml.status === 200) { console.log(xml.responseText); } else { if (possibleErrors 
 

! == undefined) {
console.error (xml.status + '' + возможных ошибок)

);
} еще {
console.error (possibleErrors.other);
}
}
}
};

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

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

Мы также можем заключить весь shebang в функцию, просто чтобы он был аккуратно инкапсулирован и глобальное пространство имен не загрязнено:

 var doRequest = function() { var xml = new XMLHttpRequest(); xml.open('GET', url); xml.setRequestHeader('Authorization', auth); xml.send(); xml.onreadystatechange = function() { if (xml.readyState === 4) { if (xml.status === 200) { console.log(xml.responseText); } else { if (possibleErrors 
 

! == undefined) {
console.error (xml.status + '' + возможных ошибок)

);
} еще {
console.error (possibleErrors.other);
}
}
}
};
};

doRequest ();

Неожиданно возникнуть

Теперь, когда у нас есть наш ответный текст, мы можем его обработать. Сначала нам нужно превратить его в правильный массив, потому что он бесполезен для нас в виде строки. Замените console.log(xml.responseText); с:

 var response = JSON.parse(xml.responseText); console.log(response); 

Приведенное выше должно создать массив JavaScript объектов, когда вы смотрите на консоль JavaScript сгенерированной фоновой страницы.

Я создал тестовый аккаунт под названием «testerguy» на Diigo, с некоторыми примерами закладок. Вероятно, вам стоит попробовать свои силы, чтобы поэкспериментировать, поскольку пока вы не читаете эту статью, неизвестно, что может произойти с этим.

Как упоминалось в части 1 , структура папки закладок будет такой: все помеченные закладками «bbs-root» в корне папки и все теги во вложенных папках в папке «метки». Это сделано для того, чтобы пользователь мог расставить приоритеты для определенных закладок, пометив их «bbs-root» и убедившись, что они отображаются вне соответствующих папок для быстрого доступа.

Чтобы правильно создать папку панели закладок, нам нужно выяснить все уникальные теги, создать корневую папку, создать подпапки «теги» и создать подпапки для каждого известного нам тега в указанном порядке. Чтобы упростить тестирование, мы добавим в наше расширение всплывающее окно с кнопкой «Обновить», которая повторяет запрос XHR. Обновите блок manifest.json browser_action следующим образом:

 "browser_action": { "default_icon": { "19": "icons/19.png", "38": "icons/38.png" }, "default_title": "Diigo BBS", "default_popup": "popup/popup.html" }, 

и создайте папку с именем popup в корне вашего проекта. Создайте еще три файла в этой папке: popup.html , popup.js и popup.css со следующим содержимым:

 <!-- popup.html --> <!DOCTYPE html> <html> <head> <title>BBS popup</title> <script src="popup.js"></script> <link rel="stylesheet" type="text/css" href="popup.css"> <link rel="icon" href="../icons/19.png"> </head> <body> <button id="refreshButton">Refresh</button> </body> </html> 
 // popup.js var bg = chrome.extension.getBackgroundPage(); document.addEventListener('DOMContentLoaded', function () { document.getElementById('refreshButton').addEventListener('click', function() { bg.doRequest(); }); }); 
 /* popup.css */ #refreshButton { margin: 10px; } 

Код JS здесь делает следующее: сначала мы выбираем объект окна автоматически сгенерированной страницы скрипта background.js . Всплывающие сценарии имеют прямой доступ к фоновому коду страницы, в отличие от сценариев содержимого, которые должны передавать сообщения. Затем мы привязываем обработчик нажатия к событию click кнопки «Обновить», которое вызывает наш метод doRequest из background.js .

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

Теперь мы можем продолжить кодирование в background.js .

Обработка массива ответов

Мы находим все теги, просматривая все выбранные закладки, сохраняя их в массиве, а затем удаляя дубликаты. Пока мы выполняем итерацию, мы можем проверить все закладки, содержащие тег «bbs-root», и записать их в отдельную переменную. Давайте добавим функцию process :

 var process = function(response) { var iLength = response.length; if (iLength) { console.info(iLength + " bookmarks were found."); } else { console.info("Response is empty - there are no bookmarks?"); } }; 

Также в функции doRequest давайте заменим

 var response = JSON.parse(xml.responseText); console.log(response); 

с process(JSON.parse(xml.responseText)); ,

Перезагрузка расширения распечатает количество найденных закладок для выбранного пользователя. Давайте включим вспомогательную функцию, которая поможет нам отфильтровать дубликаты тегов из массива тегов. Эта функция расширяет собственный массив JavaScript, так что вы можете вызывать его так, как если бы он был встроен все время. Поместите его под часть Base64, в верхней части файла:

 /** * Removes duplicate elements from the array */ Array.prototype.unique = function () { var result = []; var len = this.length; while (len--) { if (result.indexOf(this[len]) == -1) { result.push(this[len]); } } this.length = 0; len = result.length; while (len--) { this.push(result[len]); } }; 

Теперь давайте построим функцию process .

 var process = function(response) { var iLength = response.length; var allTags = []; var rootBookmarks = []; if (iLength) { console.info(iLength + " bookmarks were found."); var i = iLength; while (i--) { var item = response[i]; if (item.tags !== undefined && item.tags != "") { var tags = item.tags.split(','); if (tags.indexOf('bbs-root') > -1) { rootBookmarks.push(item); } allTags = allTags.concat(tags); } } allTags.unique(); allTags.sort(); console.log(allTags); } else { console.info("Response is empty - there are no bookmarks?"); } }; 

Мы перебираем все закладки, если таковые имеются, и для каждой из них мы превращаем их свойство тегов в массив. Затем этот массив объединяется с массивом allTags для которого мы вызываем unique() чтобы удалить дубликаты, и сортируется по алфавиту. При этом мы также следим за закладками с тегами bbs-root и копируем их ссылки в массив rootBookmarks .

Теперь мы готовы манипулировать панелью закладок.

Панель закладок

Во-первых, нам нужно проверить, существует ли «Diigo #BBS» в виде папки на панели закладок. Если нет, мы создаем это. Сразу же поместите следующий код в allTags.sort(); :

 var folderName = 'Diigo #BBS'; chrome.bookmarks.getChildren("1", function(children) { var numChildren = children.length; var folderId; while (numChildren--) { if (children[numChildren].title == folderName) { folderId = children[numChildren].id; break; } } if (folderId === undefined) { chrome.bookmarks.create({ parentId: "1", title: folderName }, function(folder) { folderId = folder.id; console.log(folderName + " not found and has been created at ID " + folder.id); }); } }); 

Сначала мы получаем дочерние узлы с идентификатором 1, который является панелью закладок (вы можете увидеть это с помощью getTree ). Затем мы перебираем их и сравниваем их названия с нужным именем нашей папки. Если папка найдена, мы сохраняем ее идентификатор и выходим из цикла. Если он так и не был найден, мы создаем его и сохраняем идентификатор.

Теперь нам нужно выяснить, содержит ли наша папка папку «Теги». Как только мы это сделаем, нам нужно выяснить, содержит ли наша папка «Теги» подпапку, соответствующую имени каждого найденного нами тега. Заметив образец здесь? Похоже, нам понадобится общая функция для проверки, содержит ли папка закладок другую папку. Мы могли бы также сделать другой вспомогательный метод для проверки на наличие реальных закладок. Давайте добавим следующие функции в наш файл background.js (например, над функцией process ):

 chrome.bookmarks.getFirstChildByTitle = function (id, title, callback) { chrome.bookmarks.getChildren(id, function (children) { var iLength = children.length; while (iLength--) { var item = children[iLength]; if (item.title == title) { return callback(item); } } return callback(false); }); }; chrome.bookmarks.getFirstChildByUrl = function (id, url, callback) { chrome.bookmarks.getChildren(id, function (children) { var iLength = children.length; while (iLength--) { var item = children[iLength]; if (item.hasOwnProperty('url') && item.url == url) { return callback(item); } } return callback(false); }); }; 

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

Вы также можете объединить их в один метод и использовать третий параметр, который сообщает методу, какое свойство мы ищем (title или url), тем самым соблюдая принцип DRY немного больше. Сейчас я оставлю это на ваше усмотрение и вернусь к этому в следующей статье, посвященной оптимизации.

Давайте перепишем наш метод process чтобы использовать это сейчас.

  chrome.bookmarks.getFirstChildByTitle("1", folderName, function(value) { if (value === false) { chrome.bookmarks.create({ parentId: "1", title: folderName }, function (folder) { console.log(folderName + " not found and has been created at ID " + folder.id); }); } }); 

Гораздо лаконичнее, не правда ли? Когда мы рассмотрим дальнейшие шаги, станет ясно, что нам нужно провести различие между списком существующих тегов и списком тегов, которые мы недавно получили с сервера. Для этого мы добавим два новых вспомогательных метода в собственный объект JavaScript Array: intersect и diff . Давайте разместим их в верхней части файла, прямо там, где находится Array.unique() , и пока мы там, давайте также переместим туда методы getFirstChildByTitle и getFirstChildByUrl .

 /** * Returns an array - the difference between the two provided arrays. * If the mirror parameter is undefined or true, returns only left-to-right difference. * Otherwise, returns a merge of left-to-right and right-to-left difference. * @param array {Array} * @param mirror * @returns {Array} */ Array.prototype.diff = function (array, mirror) { var current = this; mirror = (mirror === undefined); var a = current.filter(function (n) { return array.indexOf(n) == -1 }); if (mirror) { return a.concat(array.filter(function (n) { return current.indexOf(n) == -1 })); } return a; }; /** * Returns an array of common elements from both arrays * @param array * @returns {Array} */ Array.prototype.intersect = function (array) { return this.filter(function (n) { return array.indexOf(n) != -1 }); }; 

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

 const CONSOLE_LOGGING = true; function clog(val) { if (CONSOLE_LOGGING) { console.log(val); } } 

Теперь вы можете заменить все ваши вызовы console.log () в коде на clog . Когда вам нужно отключить регистрацию, просто переключите константу CONSOLE_LOGGING на false и все выходные данные остановятся. Это замечательно при переходе от разработки к производству - это приводит к очень небольшим накладным расходам, но сокращает время подготовки, так как вам не нужно вручную выслеживать и комментировать или удалять все ваши выводы консоли.

Заключение части 2

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