Розробка крос-браузерних розширень

    У своїй минулій статті, я згадав про випуску браузерного розширення для 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 через який можна залити нову версію, але поки доводиться робити це руками. Всі подробиці, будь ласка сюди .
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.