Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Large scale KnockoutJS application architecture

Tags:

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?

like image 633
Ryan Avatar asked May 14 '12 18:05

Ryan


People also ask

What is KnockoutJS used for?

KnockoutJS is basically a library written in JavaScript, based on MVVM pattern that helps developers build rich and responsive websites.

Which of the following architecture is implemented by knockout?

Knockout is a JavaScript library that uses a Model-View-View Model (MVVM) architecture that makes it easy for developers to create dynamic and interactive UI with a logical underlying data model.

Is KnockoutJS still used?

Today, Knockout. js mostly exists in legacy applications and developers try to move to something newer, like Vue. js.

Is KnockoutJS a framework or library?

Knockout. js is an open source library (under the MIT License) that is pure JavaScript that works with any existing web framework and every mainstream browser. Further, Knockout. js is fully documented with a complete set of online tutorials.


2 Answers

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.

like image 87
Gaff Avatar answered Oct 16 '22 17:10

Gaff


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.

like image 40
Brian Zengel Avatar answered Oct 16 '22 19:10

Brian Zengel