Cross-browser extension development

    In my last article, I mentioned the release of a browser extension for Google Chrome, which can increase search efficiency by providing relevant information from articles you like on social networks.

    Today we support 3 main browsers Chrome, Firefox and Safari, and, despite the difference in platforms, all are assembled from one code base. I will tell you how this was done and how to simplify my life by developing browser extensions.

    At the beginning of the journey


    It all started with the fact that I made a simple extension to Chrome. By the way, I note that development for Chrome turned out to be the most pleasant and convenient. Especially without bothering with any automation, after local debugging, I packed the contents of the extension into .zipand uploaded to the Web Store.

    The extension was well adapted by our audience, metrics and user reviews said that this is what we need. And since 15% of our traffic comes from Firefox, it should be next.

    The essence of all browser extensions is the same - these are HTML / CSS / JS applications, with their own manifest file that describes the properties and content and the source code itself. Therefore, my primary idea was as follows - I copy the extension repository for Chrome and adapt it for Firefox.

    But in the process of work, I felt a feeling of “guilt” for copy-paste, familiar to many programmers. It was obvious that 99% of the code was reused between extensions and the prospect of increased functionality supporting various branches could turn into a problem.

    It so happened that I noticed an excellent octotree extension (I recommend it to everyone who actively uses GitHub), I noticed a bug in it and decided to fix it. But when I cloned the repository and began to deal with the contents, I found an interesting feature - all 3 octotree extensions are collected from one repository. Like Likeastore , Octotree is a simple content injection and that's why their model was great for me as well.

    I adapted and improved the build process in Octotree for my project (the bug was fixed by the way, too ) see what happened.

    Application structure


    I will propose an application structure that in my opinion will be suitable for any extensions.

    image

    build , dist - auto-generated folders into which the source code of the extensions and the ready-for-distribution application fit, respectively.

    css , img , js - extension source code.

    vendor - platform-specific code, a separate folder for each browser.

    tools - tools necessary for assembly.

    Everything is collected by gulp, the “rethought” collector project for node.js. And even if you do not use the node in production, I highly recommend installing it on your machine, there are a lot of useful things now appearing in the npm galaxy.

    Platform Dependent Code


    Let's start with the most important thing - if you are starting a new project, or want to adapt an existing one, you need to clearly understand what platform-specific calls will be needed and highlight their separate module.

    In my case, there was only one such call - getting the URL to the resource inside the extension (in my case, to the pictures). Therefore, a separate file, browser.js, was highlighted.

    ;(function (window) {
    	var app = window.app = window.app || {};
    	app.browser = {
    		name: 'Chrome',
    		getUrl: function (url) {
    			return chrome.extension.getURL(url);
    		}
    	};
    })(window);
    

    Relevant versions for Firefox and Safari .

    In more complex cases, browser.js expands to meet all the necessary calls, forming a facade between your code and the browser.

    image

    In addition to the façade, platform-specific code includes manifests and extension settings. For Chome, this manifest.jsonis Firefox main.js+ package.jsonand finally Safari, which in the old fashion uses .plist files - Info.plist, Settings.plist, Update.plist.

    Automate build with gulp


    The task of the assembly is to copy the source code files of the extension and platform-specific code into folders, the structure of which is dictated by the browser itself.

    To do this, create 3 gulp task,

    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')
    	);
    });
    

    The default task that collects all three extensions,

    gulp.task('default', function(cb) {
    	return rseq('clean', ['chrome', 'firefox', 'safari'], cb);
    });
    

    And also, for development it is very convenient when the code changes and at the same time the assembly is performed automatically.

    gulp.task('watch', function() {
    	gulp.watch(['./js/**/*', './css/**/*', './vendor/**/*', './img/**/*'], ['default']);
    });
    

    Preparing the expansion for distribution


    But the assembly itself is not everything, I want to be able to pack the application in a format ready for placement on the corresponding App Store (I note that for Safari there is no such store, but subject to certain rules, they can place information in the gallery, you take on the hosting task )

    In the case of Chrome, all you need to do is .ziparchive, which is signed and verified already on the side of the Chrome Web Store.

    gulp.task('chrome-dist', function () {
    	gulp.src('./build/chrome/**/*')
    		.pipe(zip('chrome-extension-' + chrome.version + '.zip'))
    		.pipe(gulp.dest('./dist/chrome'));
    });
    

    For Firefox, it’s a bit more complicated - you need to have an SDK that includes the cfx tool that can “wrap” the extension into a xpifile.

    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',
    ]));
    

    But with Safari, it’s generally a “bummer”. You can only build the application in the .safariextz package inside Safari itself. I spent more than one hour to get the instruction to work, but all in vain. Now, unfortunately, it is not possible to export your development certificate to a .p12format, as a result, it is impossible to create the necessary keys for signing a package. Safari still has to be packaged manually, the distribution task is simplified to copy the Update.plist file.

    gulp.task('safari-dist', function () {
    	pipe('./vendor/safari/Update.plist', './dist/safari');
    });
    

    Eventually


    The development process from one repository is easy and enjoyable. As I mentioned above, Chrome, for me, is the most convenient development environment, so all changes are added and tested there,

    $ gulp watch
    

    After everything works fine in Chrome, check Firefox

    $ gulp firefox-run
    

    And also, in the "manual" mode in Safari.

    We decide to release a new version, update the appropriate manifest files with the new version and run

    $ gulp dist
    

    image

    As a result, in the / dist folder which files are distributed. It would be ideal if the App Store had an API through which you can upload the new version, but for now you have to do it by hand. All details, please, here .

    Also popular now: