The extensions of Simple Gmail Notes have been uploaded to the Chrome Store and Firefox Addon Market for than two weeks. I believe it's time consolidate the code of two sides now, so the project could be more easily maintained in the future.
Refactoring
The system structure after code refactoring is illustrated the following diagram:
There are basically two main steps for the refactoring:
-
Both Firefox and Chrome extensions are composed of three main modules:
page.js
,content.js
andbackground.js
. The common functionalities (e.g. Google Document API call) are extracted and isolated into three core js files (xxx-common.js
) -
Some APIs are browser dependent (e.g. storage API). In such cases, some pseudo-abstract functions are provided in the common js files. Those pseudo-abstract functions must be overwritten by the subsequent (browser specific) js files, otherwise exceptions would be thrown.
For example, the API to send messages from background script to content script is different from Firefox and Chrome. So, we would have the followings:
- background-common.js (Shared):
1 2 3 |
sendContentMessage = function(sender, message) { throw "sendContentMessage not implemented"; } |
- background.js (Firefox):
1 2 3 4 |
sendContentMessage = function(sender, message) { debugLog("Sending message", sender, message); sender.worker.port.emit("SGN_background", message); } |
- background.js (Chrome):
1 2 3 4 5 |
sendContentMessage = function(sender, message) { chrome.tabs.sendMessage(sender.worker.tab.id, message, function(response) { debugLog("Message response:", response); }); } |
The Script Loading Sequence
By design the background-common.js will be loaded BEFORE the background.js, so the browser specific functions would eventually overwrite those common functions. (Late loaded functions would overwrite the early functions, if they are of the same name.)
Here is the code for script loading for Chrome side:
1 2 3 4 |
//manifest.json "background": { "scripts" : ["lib/jquery-1.11.3.min.js", "common/background-common.js", "background.js"] }, |
Here is the code for script loading for Firefox side:
1 2 3 4 5 6 7 |
//beginning of background.js var instance = Cc["@mozilla.org/moz/jssubscript-loader;1"]; var loader = instance.getService(Ci.mozIJSSubScriptLoader); loader.loadSubScript(self.data.url("common/background-common.js")); //logic for background.js ... |
Partial Refactoring
Things would be a little bit tricky if the functions are just '''partially''' shared. For example, the success
callback logic are the same for both extensions, but the way to launch the OAuth of Google Document varies.
Here is an example:
- background-common.js (Shared):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
launchAuthorizer = function(sender, callback) { throw "launchAuthorizer not implemented"; } loginGoogleDrive = function(sender, messageId){ debugLog("Trying to login Google Drive."); launchAuthorizer(sender, function(code) { debugLog("Code collected", code); if(!code){ sendContentMessage(sender, {action:"show_log_in_prompt"}); sendContentMessage(sender, {action:"disable_edit"}); sendContentMessage(sender, {action:"show_error", message:"Failed to login Google Drive."}); } else{ //get code from redirect url if(code.indexOf("=") >= 0) //for chrome code = code.split("=")[1]; code = code.replace(/[#]/g, ""); debugLog("Collected code:" + code); setStorage(sender, "code", code); updateRefreshTokenFromCode(sender, messageId); } } ); } |
- background.js (Chrome):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
launchAuthorizer = function(sender, callback) { debugLog("Trying to login Google Drive."); chrome.identity.launchWebAuthFlow( {"url": "https://accounts.google.com/o/oauth2/auth?" + $.param({"client_id": settings.CLIENT_ID, "scope": settings.SCOPE, "redirect_uri": getRedirectUri(), "response_type":"code", "access_type":"offline", "login_hint":sender.email, "prompt":"consent" }), "interactive": true }, callback ); } |
- background.js (Firefox):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
launchAuthorizer = function(sender, callback){ var calls = { userAuthorizationURL: "https://accounts.google.com/o/oauth2/auth" }; var p = OAuthConsumer.makeProvider('google-oauth2', 'Google', settings.CLIENT_ID, settings.CLIENT_SECRET, getRedirectUri(), calls); p.version = "2.0"; p.tokenRx = /\?code=([^&]*)/gi; p.useInternalStorage = false; p.requestParams = { 'response_type': 'code', 'access_type' : 'offline', 'login_hint' : sender.email, 'prompt' : 'consent', 'xoauth_displayname': "Simple Gmail Notes", 'scope': settings.SCOPE }; var handler = OAuthConsumer.getAuthorizer(p, function(svc){ var code = svc.token; callback(code); }); handler.startAuthentication(); } |
Once the structure is fixed and refactoring is applied to one side, the transfer to the other side is actually easy. In this case, I worked for one day to extract out the common utility functions from Firefox extension, and then it took me another 2 hours to finish the refactoring of the Chrome extension.
Leave a comment