В своей прошлой статье, я упомянул о выпуске браузерного расширения для Google Chrome, который способен повысить эффективность поиска, за счет предоставления релевантной информации из статей понравившихся вам в социальных сетях.
На сегодня мы поддерживаем 3 главных браузера Chrome, Firefox и Safari, причем, не смотря на разницу платформ, все собираются из одной кодовой базы. Я расскажу, как это было сделано и как упростить себе жизнь разрабатывая браузерные расширения.
Началось все с того, что я сделал простое расширение к Chrome. К слову замечу, что разработка под Chrome оказалась самой приятной и удобной. Особо не заморачиваясь никакой автоматизацией, после локальной отладки паковал содержимое расширения в
Расширение хорошо адаптировалось нашей аудиторией, метрики и отзывы пользователей говорили о том, что это то, что надо. И так как 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.
Соответвующие версии для Firefox и Safari.
В более сложных случаях, browser.js расширяется под все необходимые вызовы, образуя фасад между вашим кодом и браузером.

Помимо фасада, к платформо-зависимому коду относятся манифесты и настройки расширения. Для Chome это
Задача сборки, суть копирование файлов исходного кода расширения и платформо-зависимого кода в папки, структуру которых диктует сам браузер.
Для этого создаем 3 gulp таска,
Таск по умолчанию, который собирает все три расширения,
А также, для разработки очень удобно, когда код меняется и при этом сборка выполняется автоматически.
Но сама сборка это еще не все, хочется иметь возможность упаковать приложение к формату готовому к размещению на соответвующих App Store (отмечу, что для Safari такого стора нет, но при соблюдении определенных правил они могут разместить информацию в галерее, задачу хостинга вы берете на себя).
В случае Chrome, все что необходимо сделать это
Для Firefox, немного сложнее — необходимо иметь SDK, в состав которой входит тул cfx, способный «завернуть» расширение в
А вот с Safari, вообще получится «облом». Собрать приложение в .safariextz пакет, можно только внутри самого Safari. Я потратил не один час, чтобы заставить инструкцию работать, но все тщетно. Сейчас, к сожалению, не возможно экспортировать свой девелоперский сертификат в
Процесс разработки из одного репозитория легок и приятен. Как я упомянул выше, Chrome, как по мне, самая удобная среда разработки, поэтому все изменения добавляются и тестируются там,
После того, как все функционирует нормально в Chrome, проверяем Firefox
А также, в «ручном» режиме в Safari.
Принимаем решение о выпуске новой версии, апдейтим соответсвующие манифест файлы с новой версией и запускаем,

В результате, в папке /dist которые к распространению файлы. Идеально было бы, если App Store имел API через который можно залить новую версию, но пока приходится делать это руками. Все подробности, пожалуйста сюда.
На сегодня мы поддерживаем 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 через который можно залить новую версию, но пока приходится делать это руками. Все подробности, пожалуйста сюда.

