📜 ⬆️ ⬇️

Development of cross-browser extensions

In my last article, I mentioned the release of a browser extension for Google Chrome, which can improve 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 collected from a single code base. I will tell you how it was done and how to simplify your 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 the development under Chrome was the most pleasant and convenient. Especially without any automation, after the local debugging, I packed the contents of the extension into .zip and uploaded in the Web Store.

The extension was well adapted by our audience, metrics and user feedback said that this is the right thing. And since 15% of our traffic comes from Firefox, it should be next.
')
The essence of all browser extensions is the same - it is HTML / CSS / JS applications, with its manifest file describing the properties and content and the source code itself. So my primary idea was the following - I copy the repository extension for Chrome and adapt it for Firefox.

But in the process of work, I felt the feeling of “guilt” for copy-paste familiar to many programmers. It was obvious that 99% of the code is reused between extensions and the prospect of growing functionality; support for different branches may become a problem.

It so happened that I caught the eye of the excellent octotree extension (I recommend it to anyone 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 extensions of octotree are collected from a single repository. Like the case of Likeastore , Octotree is a simple content injection and therefore their model is well suited for me.

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

Application structure


I will propose the structure of the application, which in my opinion will be suitable for any extensions.

image

build , dist - auto-generated folders that contain the source code of extensions and an application ready for distribution, respectively.

css , img , js - source code for the extension.

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

tools - tools needed for assembly.

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

Platform dependent code


Let's start with the most important thing - if you are starting a new project, or if you want to adapt an existing one, you need to clearly understand which platform-specific calls will be needed and select 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, stood out.

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

Corresponding versions for Firefox and Safari .

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

image

In addition to the facade, platform-specific code includes manifests and extension settings. For Chome, this is manifest.json , Firefox main.js + package.json and finally Safari, which uses .plist files in the old manner - Info.plist, Settings.plist, Update.plist.

Automating build with gulp


The task of the assembly is the essence of copying the files of the source code of the extension and the platform-dependent code into folders whose structure the browser itself dictates.

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

We are preparing an extension for distribution


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

In the case of Chrome, all you need to do is a .zip archive, which is signed and verified already on 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 is a bit more difficult - you need to have an SDK, which includes cfx tool that can wrap the extension in an xpi file.

 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, you’ll get a bummer. You can build an application into a .safariextz package, only inside Safari itself. I spent more than one hour to make the instruction work, but all in vain. Now, unfortunately, it is not possible to export your development certificate in the .p12 format, as a result it is impossible to create the necessary keys for signing the package. Safari still has to be manually packaged; the distribution task is simplified before copying the Update.plist file.

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

Eventually


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

 $ gulp watch 

After everything is working fine in Chrome, check Firefox

 $ gulp firefox-run 

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

We make a decision on the release of a new version, update the corresponding manifest files with the new version and run,

 $ gulp dist 

image

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

Source: https://habr.com/ru/post/227881/


All Articles