Создание расширения для 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://blog.nihilogic.dk/ */ «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 === '-10000 px' ? (+iconsize — Math.floor (+iconsize / 8)) + 'px' : '-10000 px'; } }, false); document.body.appendChild (icon); } if (! document.getElementById ('expifspy-leaflet-mudasobwa-id')) { /* create div to draw leaflet */ // [CUT] тут создаем и обеспечиваем стилями/свойствами карту
leaflet.style.right = '-10000 px'; 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://{s}.tile.osm.org/{z}/{x}/{y}.png', { 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://geocode-maps.yandex.ru/1.x/? lang=en-US&format=json&geocode='+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. Если что-то упустил — потормошите, добавлю.