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

    В своей прошлой статье, я упомянул о выпуске браузерного расширения для 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 через который можно залить новую версию, но пока приходится делать это руками. Все подробности, пожалуйста сюда.
    Метки:
    • +21
    • 9,2k
    • 9
    Likeastore 20,43
    Компания
    Поделиться публикацией
    Похожие публикации
    Комментарии 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 в страницу.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое