Разработка кросс-браузерных расширений

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

    Структура приложения


    Я предложу структуру приложения, которая по моему мнению будет подходить для любых расширений.

    image

    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 расширяется под все необходимые вызовы, образуя фасад между вашим кодом и браузером.

    image

    Помимо фасада, к платформо-зависимому коду относятся манифесты и настройки расширения. Для 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
    

    image

    В результате, в папке /dist которые к распространению файлы. Идеально было бы, если App Store имел API через который можно залить новую версию, но пока приходится делать это руками. Все подробности, пожалуйста сюда.
    Likeastore
    Company
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 9

      +2
      Существует способ автоматизации сборки под сафари — посмотрите на вот эту инструкцию.
      Сертификаты можно достать при помощи патченного xar-архиватора (согласно инструкции) из готового safariextz-файла, который достаточно единственный раз собрать через Extension Builder.
        0
        Спасибо, но я это все проделал (тот вопрос, что я указал в статье ведет на эту страничку) — проблема, в том что нужен свой сертификат для генерации .cer / .pem пары. И сейчас это не возможно (Keychain просто дизейблит эту опцию).
        0
        А отлаживаете расширение где и как?
          0
          Как и писал выше, всб разработку/отладку делаю в хроме (web tools) — экспириенс, такой же как и с обычным веб приложением. C фоксом и сафари, конечно не так удобно.
          +1
          В сторону фреймворков для написания кросс-браузерных расширений не смотрели (Crossrider, Kango)?
            0
            Очень интересно, спасибо. Не знал, что такие существуют :) На данный момент устраивает тот процесс, который есть — но вот в будщем, надо будет обязательно рассмотреть.
              0
              Последние несколько месяцев пилю на Kango одно расширение, более чем доволен. Плюс ребята наши соотечественники и легко идут на контакт и баг/фич реквесты.
              Автор кстати есть на Хабре: habrahabr.ru/users/kadot/
                0
                Kango, как наверное и любой фреймворк, страдает тем, что предусмотренные в нем вещи сделать легко и просто, а вот если надо что-то не совсем стандартное, то, как говорится, шаг влево — шаг вправо — расстрел.
            0
            Скажите, кто-нибудь знает, как именно функционирует Crossrider? После запрета Хрома на установку оффлайновых расширений они нашликакое-то решение. Было бы любопытно, как именно они вставляют JS в страницу.

            Only users with full accounts can post comments. Log in, please.