В своей прошлой статье, я упомянул о выпуске браузерного расширения для 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 через который можно залить новую версию, но пока приходится делать это руками. Все подробности, пожалуйста сюда.
Автор: alexbeletsky