Large scale KnockoutJS application architecture
Asked Answered
L

2

19

I love KnockoutJS but have been struggling to figure out the best way to build large scale Javascript applications with it.

Right now the way I'm handling the code is by building with a root view model which usually starts at the master page level and then expanding on that. I only ko.applyBindings() on the main view. Here is the example code I have:

var companyNamespace = {};

// Master page. (a.k.a _Layout.cshtml)
(function(masterModule, $, ko, window, document, undefined) {
    var private = "test";

    masterModule.somePublicMethod = function() {};
    masterModule.viewModel = function() {
        this.stuff = ko.observable();
    };
}(companyNamespace.masterModule = companyNamespace.masterModule || {}, jQuery, ko, window, document));

// Index.cshtml.
(function(subModule, $, ko, window, document, undefined) {
    var private = "test";

    subModule.somePublicMethod = function() {};
    subModule.viewModel = function() {
        this.stuff = ko.observable();
    };

    $(document).ready(function() {
        ko.applyBindings(companyNamespace.masterModule);
    });
}(companyNamespace.masterModule.subModule = companyNamespace.masterModule.subModule || {}, jQuery, ko, window, document));

I'm just worried since this is a tree structure that if I needed to insert a double master page or something like that, that this would be very cumbersome to re-factor.

Thoughts?

EDIT

I'm aware that you can apply bindings to separate elements to change the scope of the bindings however what if I have nested view models?

Laos answered 14/5, 2012 at 18:6 Comment(3)
Are you aware you can apply binding's to specific elements, and make multiple calls? Why not keep distinct viewmodels seperate?Wrigley
@Tyrsius - yes I am aware of that and I just stumbled upon this answer: #7964448 however what if I have nested view models? I'll update my answer with more detailLaos
Have yet to watch this presentation, but it is by none other than Steve Sanderson, author of Knockout, and the title is "Architecting large Single Page Applications with Knockout.js", so it has to be relevant: blog.stevensanderson.com/2014/06/11/…Unpolite
D
6

I like to set up my view models using prototypal inheritance. Like you I have a "master" view model. That view model contains instances of other view models or observable arrays of view models from there you can use the "foreach" and "with" bindings to in your markup. Inside your "foreach" and "with" bindings you can use the $data, $parent, $parents and $root binding contexts to reference your parent view models.

Here are the relevant articles in the KO documentation.

foreach binding

with binding

binding context

If you want I can throw together a fiddle. Let me know.

Despondent answered 19/5, 2012 at 20:32 Comment(2)
Here is a very simple fiddle. jsfiddle.net/bczengel/fMsxc As you can see I have a main view model. Each instance of Person and Child also acts as its own view model and inside the foreach bindings you write your markup as if you had done a seperate applyBindings. But you also have access to the parent view model(s) via the binding contexts.Despondent
BTW I know the prospect of manually creating constructors for every object you have may seem like a large task. But that's where the knockout mapping plugin can help. knockoutjs.com/documentation/plugins-mapping.html . I thought seeing how its done manually first would be more beneficial for you in the context of this question though.Despondent
J
44

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.

Jezabella answered 12/3, 2013 at 16:46 Comment(2)
+1 I really like the idea of separating HTML content into partials via #include. Though I do differ with you on one front: I prefer to have separate viewmodels per page/section.Interoffice
Top notch, superb real world example great how your separating everything, awesome stuffOctahedral
D
6

I like to set up my view models using prototypal inheritance. Like you I have a "master" view model. That view model contains instances of other view models or observable arrays of view models from there you can use the "foreach" and "with" bindings to in your markup. Inside your "foreach" and "with" bindings you can use the $data, $parent, $parents and $root binding contexts to reference your parent view models.

Here are the relevant articles in the KO documentation.

foreach binding

with binding

binding context

If you want I can throw together a fiddle. Let me know.

Despondent answered 19/5, 2012 at 20:32 Comment(2)
Here is a very simple fiddle. jsfiddle.net/bczengel/fMsxc As you can see I have a main view model. Each instance of Person and Child also acts as its own view model and inside the foreach bindings you write your markup as if you had done a seperate applyBindings. But you also have access to the parent view model(s) via the binding contexts.Despondent
BTW I know the prospect of manually creating constructors for every object you have may seem like a large task. But that's where the knockout mapping plugin can help. knockoutjs.com/documentation/plugins-mapping.html . I thought seeing how its done manually first would be more beneficial for you in the context of this question though.Despondent

© 2022 - 2024 — McMap. All rights reserved.