I have a rather large knockout.js single page application. (20K+ lines of code currently) that is very easy for anyone to maintain and add additional sections to. I have hundreds of observables and the performance is still great, even on mobile devices like an old iPod touch. It is basically an application that hosts a suite of tools. Here are some insights into the application I use:
1. Only one view model. It keeps things simple IMHO.
The view model handles the basics of any single page application, such as visibility of each page (app), navigation, errors, load and toast dialogs, etc. Example Snippet of View Model: (I separate it out even further js files, but this is to give you an overview of what it looks like)
var vm = {
error:
{
handle: function (error, status)
{
//Handle error for user here
}
},
visibility:
{
set: function (page)
{
//sets visibility for given page
}
},
permissions:
{
permission1: ko.observable(false),
permission2: ko.observable(false)
//if you had page specific permissions, you may consider this global permissions and have a separate permissions section under each app
},
loadDialog:
{
message: ko.observable(''),
show: function (message)
{
//shows a loading dialog to user (set when page starts loading)
},
hide: function()
{
//hides the loading dialog from user (set when page finished loading)
}
},
app1:
{
visible: ko.observable(false),
load: function ()
{
//load html content, set visibility, app specific stuff here
}
},
app2:
{
visible: ko.observable(false),
load: function ()
{
//load html content, set visibility, app specific stuff here
}
}
}
2. All models go into a separate .js files.
I treat models as classes, so all they really do is store variables and have a few basic formatting functions (I try to keep them simple). Example Model:
//Message Class
function Message {
var self = this;
self.id = ko.observable(data.id);
self.subject = ko.observable(data.subject);
self.body = ko.observable(data.body);
self.from = ko.observable(data.from);
}
3. Keep AJAX database calls in their own js files.
Preferably separated by section or "app". For example, your folder tree may be js/database/ with app1.js and app2.js as js files containing your basic create retrieve, update, and delete functions. Example database call:
vm.getMessagesByUserId = function ()
{
$.ajax({
type: "POST",
url: vm.serviceUrl + "GetMessagesByUserId", //Just a default WCF url
data: {}, //userId is stored on server side, no need to pass in one as that could open up a security vulnerability
contentType: "application/json; charset=utf-8",
dataType: "json",
cache: false,
success: function (data, success, xhr)
{
vm.messaging.sent.messagesLoaded(true);
for (var i = 0; i < data.messages.length; i++)
{
var message = new Message({
id: data.messages[i].id,
subject: data.messages[i].subject,
from: data.messages[i].from,
body: data.messages[i].body
});
vm.messaging.sent.messages.push(message);
}
},
error: function (jqXHR)
{
vm.error.handle(jqXHR.getResponseHeader("error"), jqXHR.status);
}
});
return true;
};
4. Merge and Minify all your model, view model, and database js files into one.
I use the Visual Studio "Web Essentials" extension that allows you to create "bundled" js files. (Select js files, right click on them and go to Web Essentials --> Create Javascript Bundle File) My Bundle file is setup like so:
<?xml version="1.0" encoding="utf-8"?>
<bundle minify="true" runOnBuild="true">
<!--The order of the <file> elements determines the order of them when bundled.-->
<!-- Begin JS Bundling-->
<file>js/header.js</file>
<!-- Models -->
<!-- App1 -->
<file>js/models/app1/class1.js</file>
<file>js/models/app1/class2.js</file>
<!-- App2 -->
<file>js/models/app2/class1.js</file>
<file>js/models/app2/class2.js</file>
<!-- View Models -->
<file>js/viewModel.js</file>
<!-- Database -->
<file>js/database/app1.js</file>
<file>js/database/app2.js</file>
<!-- End JS Bundling -->
<file>js/footer.js</file>
</bundle>
The header.js and footer.js are just a wrapper for the document ready function:
header.js:
//put all views and view models in this
$(document).ready(function()
{
footer.js:
//ends the jquery on document ready function
});
5. Separate your HTML content.
Don't keep one big monstrous html file that is hard to navigate through. You can easily fall into this trap with knockout because of the binding of knockout and the statelessness of the HTTP protocol. However, I use two options for separation depending on whether i view the piece as being accessed by a lot by users or not:
Server-side includes: (just a pointer to another html file. I use this if I feel this piece of the app is used a lot by users, yet I want to keep it separate)
<!-- Begin Messaging -->
<!--#include virtual="Content/messaging.html" -->
<!-- End Messaging -->
You don't want to use server-side includes too much, otherwise the amount of HTML the user will have to load each time they visit the page will become rather large. With that said, this is by far the easiest solution to separate your html, yet keep your knockout binding in place.
Load HTML content async: (I use this if the given piece of the app is used less frequent by users)
I use the jQuery load function to accomplish this:
// #messaging is a div that wraps all the html of the messaging section of the app
$('#messaging').load('Content/messaging.html', function ()
{
ko.applyBindings(vm, $(this)[0]); //grabs any ko bindings from that html page and applies it to our current view model
});
6. Keep the visibility of your pages/apps manageable
Showing and hiding different sections of your knockout.js application can easily go crazy with tons of lines of code that is hard to manage and remember because you are having to set so many different on and off switches. First, I keep each page or app in its own "div" (and in its own html file for separation). Example HTML:
<!-- Begin App 1 -->
<div data-bind="visible: app1.visible()">
<!-- Main app functionality here (perhaps splash screen, load, or whatever -->
</div>
<div data-bind="visible: app1.section1.visible()">
<!-- A branch off of app1 -->
</div>
<div data-bind="visible: app1.section2.visible()">
<!-- Another branch off of app1 -->
</div>
<!-- End App 1 -->
<!-- Begin App 2 -->
<div data-bind="visible: app2.visible()">
<!-- Main app functionality here (perhaps splash screen, load, or whatever -->
</div>
<!-- End App 2 -->
Second, I would have a visibility function similar to this that sets the visibility for all content on your site: (it also handles my navigation as well in a sub function)
vm.visibility:
{
set: function (page)
{
vm.app1.visible(page === "app1");
vm.app1.section1.visible(page === "app1section1");
vm.app1.section2.visible(page === "app1section2");
vm.app2.visible(page === "app2");
}
};
Then just call the app or page's load function:
<button data-bind="click: app1.load">Load App 1</button>
Which would have this function in it:
vm.visibility.set("app1");
That should cover the basics of a large single page application. There are probably better solutions out there than what I presented, but this isn't a bad way of doing it. Multiple developers can easily work on different sections of the application without conflict with version control and what not.