Restructuring a large Chrome Extension/WebApp
- by A.M.K
I have a very complex Chrome Extension that has gotten too large to maintain in its current format. I'd like to restructure it, but I'm 15 and this is the first webapp or extension of it's type I've built so I have no idea how to do it.
TL;DR: I have a large/complex webapp I'd like to restructure and I don't know how to do it. Should I follow my current restructure plan (below)? Does that sound like a good starting point, or is there a different approach that I'm missing? Should I not do any of the things I listed?
While it isn't relevant to the question, the actual code is on Github and the extension is on the webstore.
The basic structure is as follows:
index.html
<html>
<head>
<link href="css/style.css" rel="stylesheet" /> <!-- This holds the main app styles -->
<link href="css/widgets.css" rel="stylesheet" /> <!-- And this one holds widget styles -->
</head>
<body class="unloaded">
<!-- Low-level base elements are "hardcoded" here, the unloaded class is used for transitions and is removed on load. i.e: -->
<div class="tab-container" tabindex="-1">
<!-- Tab nav -->
</div>
<!--
Templates for all parts of the application and widgets are stored as elements here.
I plan on changing these to <script> elements during the restructure since <template>'s need valid HTML.
-->
<template id="template.toolbar">
<!-- Template content -->
</template>
<!-- Templates end -->
<!-- Plugins -->
<script type="text/javascript" src="js/plugins.js"></script>
<!-- This contains the code for all widgets, I plan on moving this online and downloading as necessary soon. -->
<script type="text/javascript" src="js/widgets.js"></script>
<!-- This contains the main application JS. -->
<script type="text/javascript" src="js/script.js"></script>
</body>
</html>
widgets.js
(initLog || (window.initLog = [])).push([new Date().getTime(), "A log is kept during page load so performance can be analyzed and errors pinpointed"]);
// Widgets are stored in an object and extended (with jQuery, but I'll probably switch to underscore if using Backbone) as necessary
var Widgets = {
1: { // Widget ID, this is set here so widgets can be retreived by ID
id: 1, // Widget ID again, this is used after the widget object is duplicated and detached
size: 3, // Default size, medium in this case
order: 1, // Order shown in "store"
name: "Weather", // Widget name
interval: 300000, // Refresh interval
nicename: "weather", // HTML and JS safe widget name
sizes: ["tiny", "small", "medium"], // Available widget sizes
desc: "Short widget description",
settings: [
{ // Widget setting specifications stored as an array of objects. These are used to dynamically generate widget setting popups.
type: "list",
nicename: "location",
label: "Location(s)",
placeholder: "Enter a location and press Enter"
}
],
config: { // Widget settings as stored in the tabs object (see script.js for storage information)
size: "medium",
location: ["San Francisco, CA"]
},
data: {}, // Cached widget data stored locally, this lets it work offline
customFunc: function(cb) {}, // Widgets can optionally define custom functions in any part of their object
refresh: function() {}, // This fetches data from the web and caches it locally in data, then calls render. It gets called after the page is loaded for faster loads
render: function() {} // This renders the widget only using information from data, it's called on page load.
}
};
script.js
(initLog || (window.initLog = [])).push([new Date().getTime(), "These are also at the end of every file"]);
// Plugins, extends and globals go here. i.e. Number.prototype.pad = ....
var iChrome = function(refresh) { // The main iChrome init, called with refresh when refreshing to not re-run libs
iChrome.Status.log("Starting page generation"); // From now on iChrome.Status.log is defined, it's used in place of the initLog
iChrome.CSS(); // Dynamically generate CSS based on settings
iChrome.Tabs(); // This takes the tabs stored in the storage (see fetching below) and renders all columns and widgets as necessary
iChrome.Status.log("Tabs rendered"); // These will be omitted further along in this excerpt, but they're used everywhere
// Checks for justInstalled => show getting started are run here
/* The main init runs the bare minimum required to display the page, this sets all non-visible or instantly need things (such as widget dragging) on a timeout */
iChrome.deferredTimeout = setTimeout(function() {
iChrome.deferred(refresh); // Pass refresh along, see above
}, 200);
};
iChrome.deferred = function(refresh) {}; // This calls modules one after the next in the appropriate order to finish rendering the page
iChrome.Search = function() {}; // Modules have a base init function and are camel-cased and capitalized
iChrome.Search.submit = function(val) {}; // Methods within modules are camel-cased and not capitalized
/*
Extension storage is async and fetched at the beginning of plugins.js, it's then stored in a variable that iChrome.Storage processes.
The fetcher checks to see if processStorage is defined, if it is it gets called, otherwise settings are left in iChromeConfig
*/
var processStorage = function() {
iChrome.Storage(function() {
iChrome.Templates(); // Templates are read from their elements and held in a cache
iChrome(); // Init is called
});
};
if (typeof iChromeConfig == "object") {
processStorage();
}
Objectives of the restructure
Memory usage: Chrome apparently has a memory leak in extensions, they're trying to fix it but memory still keeps on getting increased every time the page is loaded. The app also uses a lot on its own.
Code readability: At this point I can't follow what's being called in the code. While rewriting the code I plan on properly commenting everything.
Module interdependence: Right now modules call each other a lot, AFAIK that's not good at all since any change you make to one module could affect countless others.
Fault tolerance: There's very little fault tolerance or error handling right now. If a widget is causing the rest of the page to stop rendering the user should at least be able to remove it.
Speed is currently not an issue and I'd like to keep it that way.
How I think I should do it
The restructure should be done using Backbone.js and events that call modules (i.e. on storage.loaded = init).
Modules should each go in their own file, I'm thinking there should be a set of core files that all modules can rely on and call directly and everything else should be event based.
Widget structure should be kept largely the same, but maybe they should also be split into their own files.
AFAIK you can't load all templates in a folder, therefore they need to stay inline.
Grunt should be used to merge all modules, plugins and widgets into one file. Templates should also all be precompiled.
Question:
Should I follow my current restructure plan? Does that sound like a good starting point, or is there a different approach that I'm missing? Should I not do any of the things I listed?
Do applications written with Backbone tend to be more intensive (memory and speed) than ones written in Vanilla JS?
Also, can I expect to improve this with a proper restructure or is my current code about as good as can be expected?