Chunliang Lyu

I am a developer and researcher from China. Love to make fun and useful stuff. Co-founded Hyperlink.

From Chrome Extension to Firefox Add-on

Published: 2014-11-28

Firefox takes a quite different approach when developing add ons.

Firefox now uses Add-on SDK, which provides the cfx command, to facilitate the development of Firefox Add-on. To get started, start a new emtpy folder, execute cfx init and you would get a simplest Firefox add-on. Use cfx run to start a brand new Firefox to test you add-on. And finally, use cfx xpi to package the add-on to a unsigned .xpi package. You are good to go with these three commands.

A newly created add-on by cfx init contains only three files:

.
|-- data
|-- lib
|   `-- main.js
|-- package.json
`-- test
    `-- test-main.js

NOTE: I assume you are already familiar with the Chrome extension development. If not, you can check my previous post on Developing Chrome Extension with AngularJS.

Meta data

package.json in Firefox add-ons is the manifest.json in Chrome extensions. You define various meta data like package name, description, author there. Nothing fancy.

Background page vs main.js

The add-on login are defined in main.js file. It is a bit like Chrome's background page, or Safari's global page. However, be careful that main.js is not a page, and there are no window/document global variables.

If you do need a virtual HTML document, you can declare one using the Page Worker. In our case, since we use AngularJS in the background and AngularJS relies heavily on an DOM environment, we define the background page as follows:

var backgroundPage = require("sdk/page-worker").Page({
	contentURL: data.url("background.html"),
	contentScriptFile: data.url('scripts/background.js')
});

We could also include the scripts in <script> tag in background.html. We will explain why we prefer to include script in contentScriptFile in CORS section.

Content script injection

Use the page-mod module to inject content scripts/styles based on domain patterns.

var pageMod = require('sdk/page-mod');
var contentScript = pageMod.PageMod({
	include: [
		"*.google.com",
		"*.baidu.com"
	],

	contentScriptOptions: {
		rootUrl: data.url("")
	},

	contentScriptFile: [
		data.url("scripts/content.js")
	],

	contentStyleFile: [
		data.url("styles/content.css")
	],

	contentScriptWhen: 'ready',
});

Message passing

Firefox uses port to do message passing.

If multiple documents that match the page-mod's include pattern are loaded, then each document is loaded into its own execution context with its own copy of the content scripts. In this case the listener assigned to onAttach is called once for each loaded document, and the add-on code will have a separate worker for each document.

In our case, we want to pass the message from content scripts to background scripts, through the main.js. When defining the content scripts PageMod,

var workers = [];
var contentScript = pageMod.PageMod({

	/// ...
	/// ...
	
	///
	onAttach: function(worker) {

		// keep track of all workers
		workers.push(worker);
		worker.on('detach', function () {
			detachWorker(this, workers);
		});

		// when receive messages from content pages, 
		// proxy it to background page
		worker.port.on("message", function(payload) {
			console.log('receive message in main.js');
			console.log(payload);
			backgroundPage.port.emit('message', payload);
		});
	}
});


// detached workers
function detachWorker(worker, workerArray) {
	var index = workerArray.indexOf(worker);
	if(index != -1) {
		workerArray.splice(index, 1);
	}
}

We need to keep track of current workers since when we get message from background page, we need to proxy it to all content scripts:

// proxy message from background page to all content pages
backgroundPage.port.on('message', function(message) {
	workers.forEach(function(worker) {
		worker.port.emit('message', message);
	})
});

Cross-Origin Request

Firefox add-on support CORS. You can specify cross-domain-content in package.json. However, there are some constraints:

This feature is currently only available for content scripts, not for page scripts included in HTML files shipped with your add-on.

That means, when creating the background page use page-mod, you should not include scripts in your background.html file. Instead, you specify your background.js as contentScriptFile option when you create the Page object. Your background.html is only a dummy file.

Assets

As far as I know, fonts/images cannot be loaded in content page. So you better encode your fonts/images in base64 data and embed in your style files.

Preferences

simple-pref module is the preferred way for Firefox add-ons. However, we want to provide a unified interface with Chrome/Safari, so we ended up with a custom add-on page using page-mod.

// setup the options page
pageMod.PageMod({
	include: data.url("options.html"),
	contentScriptFile: data.url('scripts/background.js')
});

// Create a button
require("sdk/ui/button/action").ActionButton({
	id: "hyperlink-options",
	label: "Hyperlink Options",
	icon: {
		"16": "./images/icon-16.png",
		"32": "./images/icon-38.png",
		"64": "./images/icon-128.png"
	},
	onClick: function() {
		tabs.open(data.url("options.html"));
	}
});

Publish you add-on

To publish your add-on, you need to register your package on https://addons.mozilla.org/en-US/firefox/. Mozilla is really strict about the quality of add-ons, and requires every add-on to be manually reviewed. If you are submitting a new extension and requires a full review, it may take up to 10 days before you add-on appearing on their market.

Some common pitfalls:

  • Remove any debugging information to the console
  • innerHTML should not be used to set using dynamic values: For inserting text, textContent or createTextNode() should be used instead of innerHTML; For inserting HTML, the safer method is to use createElement(), textContent, appendChild() instead of innerHTML.
  • A Firefox addon should only have code relating to Firefox.
  • Binary, obfuscated or minified code are not allowed. They need to review all of your source code in order to approve it.

References