...

вторник, 21 октября 2014 г.

Создание расширения для Chrome за пару часов

В последнее время разработка расширений для Хрома так упростилась, что я решился наконец поставить галочку против одной из самых долгоживущих в моем ежедневнике задач: доставать из картинок на страницах GEO-таги, прицеплять картинкам title с местом, где фотография была сделана, и давать возможность в один клик глянуть на карту. Кроме того, на страницах с большим количеством фотографий имеет смысл показывать карту со всеми маркерами и предоставлять возможность перейти непосредственно к фотографии по клику на маркер.

Вот как это выглядит на моем сайте, куда я складываю кратенькие фотоотчеты о поездках (для друзей и родственников):



В современном мире на создание такого расширения у меня ушло около трех часов. Расширение доступно в Webstore, исходники традиционно лежат на гитхабе




Итак, начнем с создания скаффолда (я сверялся с календарем, 2014 год на дворе, с нуля писать не модно).


Подготовка



npm install -g yo generator-chrome-extension
mkdir mycoolext && cd $_
yo chrome-extension


Это нам скачает генератор для расширений Хрома и запустит его:



Отвечаем сообразно здравому смыслу, ждем некоторое время и на выходе имеем удобный проект, управляемый Grunt. Тесты, конечно, придется писать самому, но grunt debug с поддержкой горячего релоадинга расширения и grunt build, создающего пакет, пригодный для загрузки в Webstore — мы получили из коробки.


Манифест


Начнем с правки манифеста. Он не такой длинный, приведу его полностью, с комментариями.



{
"name": "__MSG_extName__", /* мы ❤ l10n */
"description": "__MSG_extDescription__", /* мы ❤ l10n */
"version": "1.0.0", /* каждый вызов grunt build будет увеличивать минор на 1 */
"manifest_version": 2, /* обязательно */
"default_locale": "en", /* обязательно, если мы ❤ l10n */
"icons": {
"16": "icons/16.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"background": {
"scripts": [
"scripts/chromereload.js", /* горячий релоадинг */
"scripts/background.js" /* наш исполняемый скрипт */
]
},
"page_action": {
"default_icon": {
"16": "icons/16.png",
"19": "icons/19.png",
"38": "icons/38.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"default_title": "__MSG_extName__",
"default_popup": "popup.html" /* я не использую popup, но пусть будет для наглядности */
},
"permissions": [
"contextMenus",
"tabs",
"storage",
"geolocation", /* расширению это не нужно, в демонстрационных целях */
"http://*/*",
"https://*/*"
],
"content_scripts": [
{
"matches": [
"http://*/*",
"https://*/*"
],
"js": [
"bower_components/jquery/dist/jquery.min.js", /* да, я тащу свою jQuery, я ламер */
"lib/jquery.exif.js", /* плагин для доставания exif http://ift.tt/o1ludR */
"lib/leaflet.js", /* скрипт карт от OpenMap http://leafletjs.com/ */
"scripts/main.js" /* мой код */
],
"css": [
"lib/leaflet.css" /* картам нужны стили */
]
}
],
"minimum_chrome_version": "16.0.0.0", /* для полярников и космонавтов, не видящих интернет */
"web_accessible_resources": [
"bower_components/jquery/dist/jquery.min.map", /* я не планирую отлаживать jQuery, но кто знает */
"icons/maps.png", /* иконка «карта» */
"lib/images/*" /* маркеры и прочие картинки для leaflet */
],
"options_page": "options.html" /* страница настроек */
}


Картинки, которые мы хотим отображать на чужих страницах (и скрипты, которые мы хотим подгружать), должны быть явням образом объявлены в соответствующих секциях. Приступим к кодированию.


После загрузки страницы мы пройдемся по картинкам, если они нормального размера — попытаемся вытащить из них exif, оттуда GEO-теги и (в случае успеха) — нарисуем рамку вокруг таких картинок, пропатчим их title и выведем все найденные картинки на карту, которая будет открываться по клику на маленькую иконку, появившуюся в правом верхнем углу страницы. Приступим (с этого момента текст заметки продолжается в комментариях к коду, так проще и явно понятнее что к чему относится).


Обработка exif



$('img').each(function(index, image) {
if (($(image).width() < 100) && ($(image).height() < 100)) { // слишком маленькая
$(image).attr('exif', false);
return true;
}

$(image).exifLoad(function() {
if (! $(image).attr('exif')) return;

// [CUT] тут кусок кода, который долго и муторно достает широту и долготу
// и рассовывает по fLat, fLon, sLat, sLon
// первые два — дробные, вторые — строки типа 53°20′18″N,37°5′18″E

$(image).attr('data-gps-latitude', fLat);
$(image).attr('data-gps-longitude', fLon);
$(image).attr('data-gps-latitude-pretty', sLat);
$(image).attr('data-gps-longitude-pretty', sLon);

// сейчас мы создадим анкор внутри страницы, чтобы на маркер можно было поставить ссылку
var hash = 'img_' + Date.now();
$('').attr('id', hash).insertBefore($(image));

// XHR из расширения дозволено только `background.js`, потому пляски с бубном
chrome.runtime.sendMessage(
{ method: 'getAddressByLatLng', id: counter, lat: sLat, lon: sLon },
function(response) {
var datas = JSON.parse(response.results).response.GeoObjectCollection;

// [CUT] тут кусок кода, который парсит ответ и достает оттуда адрес точки,
// где была сделана фотография

// к этой функции мы еще вернемся
handleLeaflet(iconsize, fLat, fLon, address ? address : sLat + ' ' + sLon, hash);
}
);
// нарисуем вокруг нашей картинки border (цвета задаются в настройках)
$(image).css({
'border-color': color,
'border-width': width,
'border-style': 'solid'
});
});
});



Вроде, все прокомментировал. Пора заглянуть в handleLeaflet.



var exifSpyMap = exifSpyMap || null;
var exifSpyMarkers = exifSpyMarkers || [];

function handleLeaflet(iconsize, fLat, fLon, tooltip, hash) {
if(!document.getElementById('expifspy-icon-mudasobwa-id')) {

// [CUT] тут создаем и обеспечиваем стилями/свойствами иконку

icon.addEventListener('click', function() {
var leaflet = document.getElementById('expifspy-leaflet-mudasobwa-id');
if(leaflet) {
// leaflet умеет корректно рендерить карту только на видимом (display !== 'none') контроле
leaflet.style.right = leaflet.style.right === '-10000px' ?
(+iconsize - Math.floor(+iconsize / 8)) + 'px' : '-10000px';
}
}, false);
document.body.appendChild(icon);
}
if(!document.getElementById('expifspy-leaflet-mudasobwa-id')) { /* create div to draw leaflet */
// [CUT] тут создаем и обеспечиваем стилями/свойствами карту

leaflet.style.right = '-10000px';
document.body.appendChild(leaflet);
}

if(!exifSpyMap) { // ленивое создание экземпляра карты
L.Icon.Default.imagePath = chrome.extension.getURL('lib/images');
exifSpyMap = L.map('expifspy-leaflet-mudasobwa-id').setView([fLat, fLon], 13);

// добавляем слой с благодарностью авторам
L.tileLayer('http://ift.tt/1lSj3Er', {
attribution: '© OpenStreetMap contributors'
}).addTo(exifSpyMap);
}

// создаем маркер
var marker = L.marker([fLat, fLon]).addTo(exifSpyMap).bindPopup(tooltip);

// по наведению мыши он будет показывать адрес
marker.on('mouseover', function(/*e*/) {
this.openPopup();
});
marker.on('mouseout', function(/*e*/) {
this.closePopup();
});

// по клику — будет проматывать страницу к фотографии
if(hash) {
marker.on('click', function(/*e*/) {
location.hash = '#' + hash;
});
}

// перерендерим карту, чтобы все маркеры попали
exifSpyMarkers.push(L.latLng(fLat, fLon));
exifSpyMap.fitBounds(L.latLngBounds(exifSpyMarkers));
}


Уф. Осталось разобраться с получением адреса по координатам. У гугла какая-то мутная политика, я хожу в Яндекс.



chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
switch(message.method) {
// [CUT] показано только важное

case 'getAddressByLatLng':
var url = 'http://ift.tt/1FwYmdX'+message.lat+','+message.lon;
var xmlHttpReq = new XMLHttpRequest();
if(xmlHttpReq) {
xmlHttpReq.open('GET', url);
xmlHttpReq.onreadystatechange = function () {
if(xmlHttpReq.readyState === 4 && xmlHttpReq.status === 200) {
sendResponse( { results: xmlHttpReq.responseText } );
}
};
xmlHttpReq.send(null); // 'null', ибо 'GET'
}
break;
}
return true;
});


Сводя воедино


Я не стану приводить код для изменения и хранения опций (все есть на github, плюс он тривиален). Плагин готов, можно тестировать.



$ grunt debug
Running "debug" task

Running "jshint:all" (jshint) task

✔ No problems


Running "concurrent:chrome" (concurrent) task

Running "connect:chrome" (connect) task
Started connect web server on http://localhost:9000

Running "watch" task
Waiting...
>> File "app/scripts/main.js" changed.
Running "jshint:all" (jshint) task

✔ No problems


Done, without errors.


Execution Time (2014-10-21 12:05:41 UTC)
loading tasks 3ms ▇▇ 2%
jshint:all 154ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 98%
Total 157ms

Completed in 1.172s at Tue Oct 21 2014 14:05:42 GMT+0200 (CEST) - Waiting...


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


В продакшн!



$ grunt build
Running "clean:dist" (clean) task
Cleaning dist/_locales...OK
Cleaning dist/background.html...OK
Cleaning dist/bower_components...OK
Cleaning dist/lib...OK
Cleaning dist/manifest.json...OK
Cleaning dist/options.html...OK
Cleaning dist/popup.html...OK
Cleaning dist/scripts...OK
Cleaning dist/styles...OK

Running "chromeManifest:dist" (chromeManifest) task
Build number has changed to 1, 0, 2
# ............. ⇛ еще тонна отладочного вывода
Running "compress:dist" (compress) task
Created package/exifspy-1.0.2.zip (90771 bytes)

Done, without errors.

Execution Time (2014-10-21 12:45:23 UTC)
clean:dist 104ms ▇▇▇▇ 3%
useminPrepare:html 73ms ▇▇▇ 2%
concurrent:dist 1.1s ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 30%
uglify:dist/scripts/background.js 47ms ▇▇ 1%
uglify:dist/bower_components/jquery/dist/jquery.min.js 993ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 28%
uglify:dist/lib/jquery.exif.js 101ms ▇▇▇▇ 3%
uglify:dist/lib/leaflet.js 979ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 27%
compress:dist 68ms ▇▇▇ 2%
Total 3.6s


Файл package/exifspy-1.0.2.zip готов и ждет отправки в Webstore. Если что-то упустил — потормошите, добавлю.


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


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

Отправить комментарий