На сегодня мы поддерживаем 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 через который можно залить новую версию, но пока приходится делать это руками. Все подробности, пожалуйста сюда.
ссылка на оригинал статьи http://habrahabr.ru/company/likeastore/blog/227881/
Добавить комментарий