Разработка кросс-браузерных расширений
В своей прошлой статье, я упомянул о выпуске браузерного расширения для Google Chrome, который способен повысить эффективность поиска, за счет предоставления релевантной информации из статей понравившихся вам в социальных сетях.На сегодня мы поддерживаем 3 главных браузера Chrome, Firefox и Safari, причем, не смотря на разницу платформ, все собираются из одной кодовой базы. Я расскажу, как это было сделано и как упростить себе жизнь разрабатывая браузерные расширения.
В начале путиНачалось все с того, что я сделал простое расширение к Chrome. К слову замечу, что разработка под Chrome оказалась самой приятной и удобной. Особо не заморачиваясь никакой автоматизацией, после локальной отладки паковал содержимое расширения в .zip и аплоадил в Web Store.Расширение хорошо адаптировалось нашей аудиторией, метрики и отзывы пользователей говорили о том, что это то, что надо. И так как 15% нашего траффика приходится на Firefox, следующим должен быть он.
Суть всех браузерных расширений одна — это HTML/CSS/JS приложения, со своим манифест файлом, описывающий свойства и контент и собственно исходный код. Поэтому моя первичная идея была следующей — копирую репозиторий расширения для Chrome и адаптирую его для Firefox.
Но в процессе работы я почувствовал знакомое многим программистам чувство «виновности» за copy-paste. Было очевидно, что 99% кода переиспользуется между расширениями и перспективе роста функциональности поддержка различных веток может превратится в проблему.
Так получилось, что мне попался на глаза отличное расширение octotree (рекомендую всем, кто активно пользуется GitHub), я заметил в нем баг и решил исправить его. Но когда я склонировал репозиторий и начал разбираться с содержимым, то обнаружил интересную особенность — все 3 расширения octotree собираются из одного репозитория. Как и случае Likeastore, Octotree это простой content injection и поэтому их модель отлично подходила и для меня.
Я адаптировал и улучшил процесс сборки в Octotree для своего проекта (баг кстати тоже был пофикшен) смотрите, что получилось.
Структура приложения Я предложу структуру приложения, которая по моему мнению будет подходить для любых расширений.
build, dist — автогенерируемые папки, в которые укладываются исходный код расширений и готовое к дистрибуции приложение, соответвенно.
css, img, js — исходный код расширения.
vendor — платформо-зависимый код, отдельная папка под каждый броузер.
tools — инструменты необходимые для сборки.
Все собирается gulp’ом — «переосмысленным» сборщиком проектом для node.js. И даже если вы не используете ноду в производстве, я крайне рекомендую установить ее на свою машину, уж очень много полезного появляется сейчас в галактике npm.
Платформо-зависимый код Начнем с самого главного — если вы начинаете новый проект, или хотите адаптировать существующий, необходимо четко понять, какие платформо-зависимые вызовы будут нужны и выделить их отделый модуль.В моем случае, такой вызов оказался только один — получение URL к ресурсу внутри расширения (в моем случае, к картинкам). Поэтому выделился отдельный файл, browser.js.
;(function (window) { var app = window.app = window.app || {};
app.browser = { name: 'Chrome',
getUrl: function (url) { return chrome.extension.getURL (url); } }; })(window); Соответвующие версии для Firefox и Safari.В более сложных случаях, browser.js расширяется под все необходимые вызовы, образуя фасад между вашим кодом и браузером.
Помимо фасада, к платформо-зависимому коду относятся манифесты и настройки расширения. Для Chome это manifest.json, Firefox main.js + package.json и наконец Safari, который по-старинке использует .plist файлы — Info.plist, Settings.plist, Update.plist.
Автоматизируем сборку с gulp Задача сборки, суть копирование файлов исходного кода расширения и платформо-зависимого кода в папки, структуру которых диктует сам браузер.Для этого создаем 3 gulp таска,
var gulp = require ('gulp'); var clean = require ('gulp-clean'); var es = require ('event-stream'); var rseq = require ('gulp-run-sequence'); var zip = require ('gulp-zip'); var shell = require ('gulp-shell'); var chrome = require ('./vendor/chrome/manifest'); var firefox = require ('./vendor/firefox/package');
function pipe (src, transforms, dest) { if (typeof transforms === 'string') { dest = transforms; transforms = null; }
var stream = gulp.src (src); transforms && transforms.forEach (function (transform) { stream = stream.pipe (transform); });
if (dest) { stream = stream.pipe (gulp.dest (dest)); }
return stream; }
gulp.task ('clean', function () { return pipe ('./build', [clean ()]); });
gulp.task ('chrome', function () { return es.merge ( pipe ('./libs/**/*', './build/chrome/libs'), pipe ('./img/**/*', './build/chrome/img'), pipe ('./js/**/*', './build/chrome/js'), pipe ('./css/**/*', './build/chrome/css'), pipe ('./vendor/chrome/browser.js', './build/chrome/js'), pipe ('./vendor/chrome/manifest.json', './build/chrome/') ); });
gulp.task ('firefox', function () { return es.merge ( pipe ('./libs/**/*', './build/firefox/data/libs'), pipe ('./img/**/*', './build/firefox/data/img'), pipe ('./js/**/*', './build/firefox/data/js'), pipe ('./css/**/*', './build/firefox/data/css'), pipe ('./vendor/firefox/browser.js', './build/firefox/data/js'), pipe ('./vendor/firefox/main.js', './build/firefox/data'), pipe ('./vendor/firefox/package.json', './build/firefox/') ); });
gulp.task ('safari', function () { return es.merge ( pipe ('./libs/**/*', './build/safari/likeastore.safariextension/libs'), pipe ('./img/**/*', './build/safari/likeastore.safariextension/img'), pipe ('./js/**/*', './build/safari/likeastore.safariextension/js'), pipe ('./css/**/*', './build/safari/likeastore.safariextension/css'), pipe ('./vendor/safari/browser.js', './build/safari/likeastore.safariextension/js'), pipe ('./vendor/safari/Info.plist', './build/safari/likeastore.safariextension'), pipe ('./vendor/safari/Settings.plist', './build/safari/likeastore.safariextension') ); });
Таск по умолчанию, который собирает все три расширения, gulp.task ('default', function (cb) { return rseq ('clean', ['chrome', 'firefox', 'safari'], cb); });
А также, для разработки очень удобно, когда код меняется и при этом сборка выполняется автоматически. gulp.task ('watch', function () { gulp.watch (['./js/**/*', './css/**/*', './vendor/**/*', './img/**/*'], ['default']); });
Готовим расширение к дистрибуции Но сама сборка это еще не все, хочется иметь возможность упаковать приложение к формату готовому к размещению на соответвующих App Store (отмечу, что для Safari такого стора нет, но при соблюдении определенных правил они могут разместить информацию в галерее, задачу хостинга вы берете на себя).В случае Chrome, все что необходимо сделать это .zip архив, который подписывается и верифицируется уже на строне Chrome Web Store.
gulp.task ('chrome-dist', function () { gulp.src ('./build/chrome/**/*') .pipe (zip ('chrome-extension-' + chrome.version + '.zip')) .pipe (gulp.dest ('./dist/chrome')); });
Для Firefox, немного сложнее — необходимо иметь SDK, в состав которой входит тул cfx, способный «завернуть» расширение в xpi файл. gulp.task ('firefox-dist', shell.task ([ 'mkdir -p dist/firefox', 'cd ./build/firefox && …/…/tools/addon-sdk-1.16/bin/cfx xpi --output-file=…/…/dist/firefox/firefox-extension-' + firefox.version + '.xpi > /dev/null', ]));
А вот с Safari, вообще получится «облом». Собрать приложение в .safariextz пакет, можно только внутри самого Safari. Я потратил не один час, чтобы заставить инструкцию работать, но все тщетно. Сейчас, к сожалению, не возможно экспортировать свой девелоперский сертификат в .p12 формат, как следствие невозможно создать нужные ключи для подписи пакета. Safari приходится все еще упаковывать вручную, задача дистрибуции упрощается до копирования Update.plist файла. gulp.task ('safari-dist', function () { pipe ('./vendor/safari/Update.plist', './dist/safari'); });
В итоге Процесс разработки из одного репозитория легок и приятен. Как я упомянул выше, Chrome, как по мне, самая удобная среда разработки, поэтому все изменения добавляются и тестируются там, $ gulp watch После того, как все функционирует нормально в Chrome, проверяем Firefox $ gulp firefox-run А также, в «ручном» режиме в Safari.Принимаем решение о выпуске новой версии, апдейтим соответсвующие манифест файлы с новой версией и запускаем,
$ gulp dist В результате, в папке /dist которые к распространению файлы. Идеально было бы, если App Store имел API через который можно залить новую версию, но пока приходится делать это руками. Все подробности, пожалуйста сюда.