Skip to content

Instantly share code, notes, and snippets.

@a1300
Forked from tim545/lg-jquery-app-struct.md
Created May 8, 2019 08:45
Show Gist options
  • Save a1300/6cd4f0759bcc288892c2fcb28d9d489d to your computer and use it in GitHub Desktop.
Save a1300/6cd4f0759bcc288892c2fcb28d9d489d to your computer and use it in GitHub Desktop.
Structuring a large jQuery application

Structuring a large jQuery application

This document assumes you are building a traditional backend-heavy application as opposed to a frontend-heavy appliction which would typically use a framework like Angular or React. The use of these frameworks make this document irrelevant, however also require a change to your application architecture and a much larger overhead in order to get content onto a page, so as a simple way to build interactive web content a simple jquery based js stack will do fine.

Directory structure

It's important you use a directory structure which is impartial to your development environment, chosen server language (Python v. Java v. C# ...), and styling framwork (Twitter Bootstrap etc). This layer of separation means you can swap out the styles or the backend with minimal changes to the Js, simple and maintainable.

Here's an example from the project root:

/
-- source/
-- -- assets/
-- -- -- js/
-- -- -- -- dependencies/
-- -- -- -- -- jquery-x.xx.xx.min.js
-- -- -- -- -- ...
-- -- -- -- shared/
-- -- -- -- -- global.js
-- -- -- -- -- service.js
-- -- -- -- -- ...
-- -- -- -- modules/
-- -- -- -- -- home.js
-- -- -- -- -- products.js
-- -- -- -- -- contact.js
-- -- -- -- -- about.js
-- -- -- -- -- ...

The important point in the above structure is that there is not Js embedded within html files themselves, instead each module is loaded to a relevant html view using a script tag.

The above example excludes all other project files from the js and simply places them in a typical folder where front end assets are kept.

Modules

Each file should be wrapped inside a self calling function, effectively making a single module. This isolates all variables to avoid global name space conflicts and allows a simple form of dependency injection which can make each file more readable by seeing the dependencies used by the function.

The module itself should be organised in simple pure functions, that is to say that every bit of logic is done by a function which is both simple and pure. Simple in that it fullfils just one purpose/responsbility and pure in that it only requires the variables passed in by parameters and does not mutate any of those variables directly but returns a new value instead.

Javascript typically has a requirement that any reference to a variable must be defined above the reference, as the file will be interprited in order. Depending on the compiler this may not produce errors but you should follow this rule anyway as it makes the code more readable.

A simple module, todo.js which loads a list of to do list items from an imaginary API and populates the list in a html element:

(function($, otherDependency, ...) {

  function ToDo(data) {
    this.label = data.label;
    this.completed = false;
    this.render = function() {
      return '<li class="todo-list-item">'+this.label+'</li>';
    };
  }
  
  var getAllToDos = function(success, error) {
    return $.ajax({
      method: 'GET',
      url: '/todos',
      success: success,
      error: error
    });
  };
  
  var populateToDoList = function(data) {
    $('#toDoList').empty();
    
    data.map(function(item, index, array) {
      var listItem = new ToDo(item);
      list.append(listItem.render());
    });
  };
  
  $(document).ready(function() {
    var success = function(data) {
      populateToDoList(data);
    };

    var error = function(error) {
      console.log(error);
    };

    var toDos = getAllToDos(success, error);

  });

})(jQuery, otherDependency, ...)

Shared files

You may have noticed a couple of other files in addtion to the typical modules:

global.js - This file can store any global variables you want to keep application-wide, the file would not by wrapped inside a self calling function (that would prevent variables being global). It should be considered an anti-pattern and avoided as much as possible, if anything you would use to initialize jQuery plugins on page load.

service.js - This file should contain re-usable methods & classes which you inject into your modules. For example, the ToDo class should be a service which get's injected to the module which displays the to do list:

// Service
(function() {

  function ToDo(data) {
    this.label = data.label;
    this.completed = false;
    this.render = function() {
      return '<li class="todo-list-item">'+this.label+'</li>';
    };
  }

})()

// Module
(function($, ToDo) {

  ...
  
  var populateToDoList = function(data) {
    $('#toDoList').empty();
    
    data.map(function(item, index, array) {
      var listItem = new ToDo(item);
      list.append(listItem.render());
    });
  };
  
  ...

})(jQuery, ToDo)

Dependencies

These can be stored here in separate files, manually concatinated to a single file, or managed via an automated method such as Bower, GruntJs, NPM or similar. If your not familiar with either of those then simply do what's easiest in the short term but make sure you learn them for your next project.

note: Bower and Grunt are in fact NPM packages, however depending on your development environment you can use just NPM packages with a module loader. This level of complexity is required for Angular/React/etc. apps but not necessarily for simple jQuery enhanced pages.

Notes on coding style

The subject of dependency management brings me to the next step in managing large Js applications. The standard today has moved to using automation tools so you don't have to manually manage a list of Js libraries etc. The simplest way is Grunt, there are other options like Gulp, Browserify and Webpack however if your reading this then those options are likely too much for your requirements.

With Grunt you can take all your dependency files such as the ones stored in /source/assets/dependencies and concatenate+minify them automatically. Other things you can do are write all your code in ES6 and have Grunt convert it for you, run tests, compile LESS/SASS, write Jade/Emmet instead of html and compile it, plus more. You will have to add NodeJs to your development environment and there's definately a large overhead in learning this method compared to writing static front end's the way you would be used to but it's worth the effort.

Performance tips

  • Only use single-level selectors, such as $('.todo-item'), as opposed to $('#todos .list li.todo-item'). This first selector maps straight to a default document.getElementsByClassName('todo-list') call, which is faster then forcing jQuery to it's own complex deep select.
  • Sometimes you need to tie functionality to elements which are not created in the initial DOM loading, but added later by JavaScript. In these cases it can be better to create the function as a method attached to the window object and then add an on-click="" attribute to the elements which call it. The alternative is to use a jQuery contruct like .delegate() which adds extra event listeners to the page. Causing more memory usage and potential memory leaks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment