Pull to refresh
0

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

Reading time 5 min
Views 10K
В своей прошлой статье, я упомянул о выпуске браузерного расширения для 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 через который можно залить новую версию, но пока приходится делать это руками. Все подробности, пожалуйста сюда.
Tags:
Hubs:
+21
Comments 9
Comments Comments 9

Articles

Information

Website
likeastore.com
Registered
Founded
Employees
2–10 employees
Location
Украина