Best way to organize jQuery/JavaScript code (2013)

46,839

Solution 1

I'll go over some simple things that may, or may not, help you. Some might be obvious, some might be extremely arcane.

Step 1: Compartmentalize your code

Separating your code into multiple, modular units is a very good first step. Round up what works "together" and put them in their own little encased unit. don't worry about the format for now, keep it inline. The structure is a later point.

So, suppose you have a page like this:

enter image description here

It would make sense to compartmentalize so that all the header-related event handlers/binders are in there, for ease of maintenance (and not having to sift through 1000 lines).

You can then use a tool such as Grunt to re-build your JS back to a single unit.

Step 1a: Dependency management

Use a library such as RequireJS or CommonJS to implement something called AMD. Asynchronous Module Loading allows you to explicitely state what your code depends on, which then allows you to offload the library-calling to the code. You can just literally say "This needs jQuery" and the AMD will load it, and execute your code when jQuery is available.

This also has a hidden gem: the library loading will be done the second the DOM is ready, not before. This no longer halts load-up of your page!

Step 2: Modularize

See the wireframe? I have two ad units. They'll most likely have shared event listeners.

Your task in this step is to identify the points of repetition in your code and to try to synthesise all this into modules. Modules, right now, will encompass everything. We'll split stuff as we go along.

The whole idea of this step is to go from step 1 and delete all the copy-pastas, to replace them with units that are loosely coupled. So, instead of having:

ad_unit1.js

 $("#au1").click(function() { ... });

ad_unit2.js

 $("#au2").click(function() { ... });

I will have:

ad_unit.js:

 var AdUnit = function(elem) {
     this.element = elem || new jQuery();
 }
 AdUnit.prototype.bindEvents = function() {
     ... Events go here
 }

page.js:

 var AUs = new AdUnit($("#au1,#au2"));
 AUs.bindEvents();

Which allows you to compartmentalize between your events and your markup in addition to getting rid of repetition. This is a pretty decent step and we'll extend this further later on.

Step 3: Pick a framework!

If you'd like to modularize and reduce repetitions even further, there are a bunch of awesome frameworks around that implement MVC (Model - View - Controller) approaches. My favourite is Backbone/Spine, however, there's also Angular, Yii, ... The list goes on.

A Model represents your data.

A View represents your mark-up and all the events associated to it

A Controller represents your business logic - in other words, the controller tells the page what views to load and what models to use.

This will be a significant learning step, but the prize is worth it: it favours clean, modular code over spaghetti.

There are plenty of other things you can do, those are just guidelines and ideas.

Code-specific changes

Here are some specific improvements to your code:

 $('.new_layer').click(function(){

    dialog("Create new layer","Enter your layer name","_input", {

            'OK' : function(){

                    var reply = $('.dialog_input').val();

                    if( reply != null && reply != "" ){

                            var name = "ln_"+reply.split(' ').join('_');
                            var parent = "";

                            if(selected_folder != "" ){
                            parent = selected_folder+" .content";
                            }

                            $R.find(".layer").clone()
                            .addClass(name).html(reply)
                            .appendTo("#layer_groups "+parent);

                            $R.find(".layers_group").clone()
                            .addClass(name).appendTo('#canvas '+selected_folder);

            }

        }

    });
 });

This is better written as:

$("body").on("click",".new_layer", function() {
    dialog("Create new layer", "Enter your layer name", "_input", {
         OK: function() {
             // There must be a way to get the input from here using this, if it is a standard library. If you wrote your own, make the value retrievable using something other than a class selector (horrible performance + scoping +multiple instance issues)

             // This is where the view comes into play. Instead of cloning, bind the rendering into a JS prototype, and instantiate it. It means that you only have to modify stuff in one place, you don't risk cloning events with it, and you can test your Layer stand-alone
             var newLayer = new Layer();
             newLayer
               .setName(name)
               .bindToGroup(parent);
          }
     });
});

Earlier in your code:

window.Layer = function() {
    this.instance = $("<div>");
    // Markup generated here
};
window.Layer.prototype = {
   setName: function(newName) {
   },
   bindToGroup: function(parentNode) {
   }
}

Suddenly, you have a way to create a standard layer from anywhere in your code without copy pasting. You're doing this in five different places. I've just saved you five copy-pastes.

One more:

// Ruleset wrapper for actions

var PageElements = function(ruleSet) {
ruleSet = ruleSet || [];
this.rules = [];
for (var i = 0; i < ruleSet.length; i++) {
    if (ruleSet[i].target && ruleSet[i].action) {
        this.rules.push(ruleSet[i]);
    }
}
}
PageElements.prototype.run = function(elem) {
for (var i = 0; i < this.rules.length; i++) {
    this.rules[i].action.apply(elem.find(this.rules.target));
}
}

var GlobalRules = new PageElements([
{
    "target": ".draggable",
    "action": function() { this.draggable({
        cancel: "div#scrolling, .content",
        containment: "document"
        });
    }
},
{
    "target" :".resizable",
    "action": function() {
        this.resizable({
            handles: "all",
            zIndex: 0,
            containment: "document"
        });
    }
}

]);

GlobalRules.run($("body"));

// If you need to add elements later on, you can just call GlobalRules.run(yourNewElement);

This is a very potent way to register rules if you have events that are not standard, or creation events. This is also seriously kick-ass when combined with a pub/sub notification system and when bound to an event you fire whenever you create elements. Fire'n'forget modular event binding!

Solution 2

Here is a simple way to split your current codebase into multiple files, using require.js. I will show you how to split your code into two files. Adding more files will be straightforward after that.

Step 1) At the top of your code, create an App object (or whatever name you prefer, like MyGame):

var App = {}

Step 2) Convert all of your top-level variables and functions to belong to the App object.

Instead of:

var selected_layer = "";

You want:

App.selected_layer = "";

Instead of:

function getModified(){
...
}

You want:

App.getModified = function() {

}

Note that at this point your code will not work until you finish the next step.

Step 3) Convert all global variable and function references to go through App.

Change stuff like:

selected_layer = "."+classes[1];

to:

App.selected_layer = "."+classes[1];

and:

getModified()

to:

App.GetModified()

Step 4) Test Your code at this stage -- it should all work. You will probably get a few errors at first because you missed something, so fix those before moving on.

Step 5) Set up requirejs. I assume you have a web page, served from a web server, whose code is in:

www/page.html

and jquery in

www/js/jquery.js

If these paths are not exactly like this the below will not work and you'll have to modify the paths.

Download requirejs and put require.js in your www/js directory.

in your page.html, delete all script tags and insert a script tag like:

<script data-main="js/main" src="js/require.js"></script>

create www/js/main.js with content:

require.config({
 "shim": {
   'jquery': { exports: '$' }
 }
})

require(['jquery', 'app']);

then put all the code you just fixed up in Steps 1-3 (whose only global variable should be App) in:

www/js/app.js

At the very top of that file, put:

require(['jquery'], function($) {

At the bottom put:

})

Then load page.html in your browser. Your app should work!

Step 6) Create another file

Here is where your work pays off, you can do this over and over.

Pull out some code from www/js/app.js that references $ and App.

e.g.

$('a').click(function() { App.foo() }

Put it in www/js/foo.js

At the very top of that file, put:

require(['jquery', 'app'], function($, App) {

At the bottom put:

})

Then change the last line of www/js/main.js to:

require(['jquery', 'app', 'foo']);

That's it! Do this every time you want to put code in its own file!

Solution 3

For your question and comments I'll assume you are not willing to port your code to a framework like Backbone, or use a loader library like Require. You just want a better way to orgainze the code that you already have, in the simplest way possible.

I understand it is annoying to scroll through 2000+ lines of code to find the section that you want to work on. The solution is to split your code in different files, one for each functionality. For example sidebar.js, canvas.js etc. Then you can join them together for production using Grunt, together with Usemin you can have something like this:

In your html:

<!-- build:js scripts/app.js -->
<script src="scripts/sidebar.js"></script>
<script src="scripts/canvas.js"></script>
<!-- endbuild -->

In your Gruntfile:

useminPrepare: {
  html: 'app/index.html',
  options: {
    dest: 'dist'
  }
},
usemin: {
  html: ['dist/{,*/}*.html'],
  css: ['dist/styles/{,*/}*.css'],
  options: {
    dirs: ['dist']
  }
}

If you want to use Yeoman it will give you a boilerplate code for all this.

Then for each file itself, you need to make sure you follow best practices and that all the code and variables are all in that file, and don't depend on other files. This doesn't mean you can't call functions of one file from other, the point is to have variables and functions encapsulated. Something similar to namespacing. I'll assume you don't want to port all your code to be Object Oriented, but if you don't mind refactoring a bit, I'd recommend to add something equivalent with what is called a Module pattern. It looks something like this:

sidebar.js

var Sidebar = (function(){
// functions and vars here are private
var init = function(){
  $("#sidebar #sortable").sortable({
            forceHelperSize: true,
            forcePlaceholderSize: true,
            revert: true,
            revert: 150,
            placeholder: "highlight panel",
            axis: "y",
            tolerance: "pointer",
            cancel: ".content"
       }).disableSelection();
  } 
  return {
   // here your can put your "public" functions
   init : init
  }
})();

Then you can load this bit of code like this:

$(document).ready(function(){
   Sidebar.init();
   ...

This will allow you to have a much more maintainable code without having to rewrite your code too much.

Solution 4

Use javascript MVC Framework in order to organize the javascript code in a standard way.

Best JavaScript MVC frameworks available are:

Selecting a JavaScript MVC framework required so many factors to consider. Read the following comparison article that will help you to select best framework based on the factors important for your project: http://sporto.github.io/blog/2013/04/12/comparison-angular-backbone-can-ember/

You can also use RequireJS with the framework to support Asynchrounous js file & module loading.
Look the below to get started on JS Module loading:
http://www.sitepoint.com/understanding-requirejs-for-effective-javascript-module-loading/

Solution 5

Categorize your code. This method is helping me a lot and does work with any js framework:

(function(){//HEADER: menu
    //your code for your header
})();
(function(){//HEADER: location bar
    //your code for your location
})();
(function(){//FOOTER
    //your code for your footer
})();
(function(){//PANEL: interactive links. e.g:
    var crr = null;
    $('::section.panel a').addEvent('click', function(E){
        if ( crr) {
            crr.hide();
        }
        crr = this.show();
    });
})();

In your preferred editor (the best is Komodo Edit) you may fold in by collapsing all entries and you will see only the titles:

(function(){//HEADER: menu_____________________________________
(function(){//HEADER: location bar_____________________________
(function(){//FOOTER___________________________________________
(function(){//PANEL: interactive links. e.g:___________________
Share:
46,839

Related videos on Youtube

Kivylius
Author by

Kivylius

Just an average programmer. I'm not the best I love to program &amp;*%^$ the rest.

Updated on August 02, 2020

Comments

  • Kivylius
    Kivylius almost 4 years

    The Problem

    This answer has been answered before but are old and not up to date. I have over 2000 lines of code in a single file, and as we all know this is bad practice, especially when i'm looking through code or adding new features. I want to better organize my code, for now and for the future.

    I should mention that I'm building a tool (not a simple website) with lots of buttons, UI elements, drag, drops, action listeners/handlers and function in the global scope where several listeners may use the same function.

    Example code

    $('#button1').on('click', function(e){
        // Determined action.
        update_html();
    });
    
    ... // Around 75 more of this
    
    function update_html(){ .... }
    
    ...
    

    More example code

    Conclusion

    I really need to organize this code for best use and not to repeat myself and be able to add new features and update old ones. I will be working on this by myself. Some selectors can be 100 lines of code others are 1. I have looked a bit at require.js and found it kinda repetitive, and actually writing more code than needed . I'm open to any possible solution that fit this criteria and link to resource / examples are always a plus.

    Thanks.

    • jantimon
      jantimon almost 11 years
      If you want to add backbone.js and require.js it will be a lot of work.
    • Bart
      Bart almost 11 years
      I guess lazy loading separate files is not preferable. You likely need to halt execution to load the external files before you can procede. Another route you could take is just load the bare minimum initially and just keep loading the rest behind the scenes.
    • Loamhoof
      Loamhoof almost 11 years
      requirejs with backbone could still be a good option just to organize your code. I personally prefer using views to bind listeners than using jQuery. It's not necessarily a lot of word. Well, just an opinion.
    • Mike Samuel
      Mike Samuel almost 11 years
      What tasks do you find yourself doing over and over when writing this?
    • Antony
      Antony almost 11 years
      Have you visited codereview.stackexchange.com?
    • Kivylius
      Kivylius almost 11 years
      @MikeSamuel writing listeners, lots of them, looking true file to find them and update them if any bug occur, I change one listener and other might brake overall a mess. This is why i provided that code to understand what i'm writing and how messy it can be to update and organised.
    • TecHunter
      TecHunter almost 11 years
      maybe organize your code with namespace like creating a global object like in jquery where the global object is jQuery and you will find jQuery.events for exemple which regroups every events assiociated methods and objects etc...
    • Onur Yıldırım
      Onur Yıldırım almost 11 years
      Learn Angular! It's the future.
    • George Stocker
      George Stocker almost 11 years
      Your code should not be at an external link, it should be here. Also, @codereview is a better place for tehse types of questions.
    • Tony
      Tony over 9 years
      This is not 100% answer to your question but I use this site nearly every time I work with JS. It has a lot of good patterns in it that you can use to organize your code a little better. I have not found the sweet spot yet but I do use the module pattern quite a bit. addyosmani.com/resources/essentialjsdesignpatterns/book/…
  • Kivylius
    Kivylius almost 11 years
    Great write up, not sure if you copy/pasted but this is not a focus on my app. This is more for websites and not an online tool which I have mentions above in the description. Thanks anyway.
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    @Jessica: why should an online tool be any different? The approach is still the same: compartmentalize/modularize, promote loose coupling between components using a framework (they all come with event delegation these days), split your code apart. What is there that does not apply to your tool there? The fact that you have plenty of buttons?
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    P.S: I don't copy paste. And if I copy-pasted from somewhere, do you really think I'd have drawn the picture using Paint?
  • Kivylius
    Kivylius almost 11 years
    I'm not trying to say anything bad, but I'm looking for a more focused approach for my application tool and maybe someone actauly looking over my code and help me better find the right solution.
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    @Jessica: Steps 1, 1a and 3 apply. Step 2 may be different depending on how much you copy-paste your code around. Also, open-mindedness on your side is probably a good thing when asking for help. Looks like everyone recommended require.js. What is so repetitive about it? The only issue it has is the inability to deal with Google Maps as a dependency. How you get rid of repetition in your code is by noticing where you're repeating the same action over and over. THIS is where the repetition is coming from.
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    @Jessica: Another couple of points. MVC approach will allow you to unit-test your code, which will in turn allow you to test for broken builds using a tool like QUnit (built by jQuery devs). You can automate the process using phantomjs. This will allow you to cut down your dev time by noticing automatically what went wrong, and not have stuff go wrong in the first place.
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    @Jessica: just saw the full pastebin. I'm going to rewrite a significant chunk of it to show you how trivial stuff can be made.
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    @Jessica: Updated. I've simplified and streamlined the creation of Layers using a concept similar to a View. So. How does this not apply to your code?
  • Kivylius
    Kivylius almost 11 years
    This is great code optimizing and somewhat over complicating simple code but i'm looking more into code organizing, meaning separating my code into more files or something similar so i don't have to look at one "main.js" file. You have mentioned briefly, but my whole question is about it.
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    @Jessica: Splitting into files without optimizing is like buying more drawers to store junk. One day, you've got to clear out, and it's easier to clear out before the drawers fill over. Why not do both? Right now, looks like you'll want a layers.js, sidebar.js, global_events.js, resources.js, files.js, dialog.js if you're just going to split your code up. Use grunt to rebuild them into one and Google Closure Compiler to compile and minimize.
  • Kivylius
    Kivylius almost 11 years
    Can you expand on this with examples / resources?
  • Kivylius
    Kivylius almost 11 years
    So going the road of require.js would be a bad way to go?
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    @Jessica: require allows you to manage dependencies. That's all. It's great for stuff on CDNs or if you plan on writing reusable code. grunt will allow you to take all your little files and recombine them into one, which means that you can have the best of both: compartmentalized code, but one final script entity. One script entity is widely advocated by Yahoo, Google et al as a performance gain, too... provided that you don't do what my boss used to do and wrap jQuery in it too.
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    (TL;DR version: require.js is one piece of a much bigger puzzle)
  • Willem D'Haeseleer
    Willem D'Haeseleer almost 11 years
    When using require.js you must really look into the r.js optimizer as well, this is really what makes require.js worth using. It will combine and optimize all your files: requirejs.org/docs/optimization.html
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    @WillemD'haeseleer: Correct. However, it looks as though the OP is only interesting in pasting into more files.
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    This has multiple problems - the obvious one being that you're fragmenting all your files at the end and forcing 400 bytes of wasted data to every user per script per page load by not using the r.js preprocessor. Furthermore, you haven't actually addressed the OP's issue - merely provided a generic require.js howto.
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    You may want to seriously reconsider that second-to-last snippet, which is no better than having code written inline: your module requires #sidebar #sortable. You may as well save yourself memory by just inlining the code and saving the two IETFs.
  • Jesús Carrera
    Jesús Carrera almost 11 years
    The point there is that you can use whatever code you need. I'm just using an example from the original code
  • Lyn Headley
    Lyn Headley almost 11 years
    Huh? My answer is specific to this question. And r.js is obviously the next step but the issue here is organization, not optimization.
  • hobberwickey
    hobberwickey almost 11 years
    +1 for a standard JS solution that doesn't rely on libraries.
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    -1 for multiple reasons. Your code equivalent is exactly the same as the OP's... + one IETF per "section". Furthermore, you are using overly broad selectors without allowing module developers to override the creation/removal of those or to extend behaviour. Finally, IETFs are not free.
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    @hobberwickey: Don't know about you, but I'd rather rely on something that is community-driven and where bugs will be found quickly if I can. Especially if doing otherwise condemns me to re-invent the wheel.
  • hobberwickey
    hobberwickey almost 11 years
    All this is doing is organizing code into discrete sections. Last time I checked that was A: good practice, and B: not something you really need a community supported library for. Not all projects fit into Backbone, Angular, etc. and modularizing code by wrapping it in functions is a good general solution.
  • Admin
    Admin almost 11 years
    It is possible anytime you want rely on any favorite library to use this approach. But the above solution works with pure javascript, custom libraries or any famous js framework
  • necromancer
    necromancer almost 11 years
    @SébastienRenauld upvoted for pure javascript
  • Bergi
    Bergi almost 11 years
    What do you mean by "object orientation"? Nearly everything in JS is an object. And there are no classes in JS.
  • Bergi
    Bergi almost 11 years
    @SébastienRenauld: What's an IETF? I've heard the terms IEFE and IIFE if you mean the closures. And what do you mean by "they're not free"?
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    @bergi: Immediately Executed Temporary Function. And they cost memory and processing power - more so than you think. There are a bunch of benchmarks around.
  • Bergi
    Bergi almost 11 years
    @SébastienRenauld: I'd say they save memory as they allow garbage collection instead of creating global variables… and the additional processing will be negligible for the above ones. Can you link to such a benchmark that shows relevant disadvantages?
  • Sébastien Renauld
    Sébastien Renauld almost 11 years
    @Bergi: You're focusing on specific examples. Look at the answer. Can you see any global variables that would not be GCed otherwise? I don't.
  • Tony
    Tony over 9 years
    I agree with Jesus this is just an example, the OP can easily add an options "object" that would allow them to specify the selector of the element instead of hard coding it but this was just a quick example. I would like to say that I love the module pattern it is the primary pattern I use but even with that said I am still trying to better organize my code. I use C# normally so the function naming and creation feels so generic. I try keeping a "pattern" like underscores are local and private, variables are just "objects" and then I reference the function in my return which is public.
  • Tony
    Tony over 9 years
    I however still find challenges with this approach and desire to have a better way of doing this. But it works a lot better than just declaring my variables and functions in the global space to cause conflicts with other js.... lol
  • Tony
    Tony over 9 years
    I like this answer, I have never used require.js so I will have to see if I can use it and get any benefit from it. I use the module pattern heavily but perhaps this will allow me to abstract some things out and then require them in.
  • Mithun Satheesh
    Mithun Satheesh over 9 years
    @SébastienRenauld : this answer is not merely about require.js. It speaks mostly about having a namespace for the code you are building. I think you should appreciate the good parts and make an edit you find any issues with it. :)
  • Adrien Be
    Adrien Be over 9 years
    @SébastienRenauld your answer & comments are still very appreciated by other users. If that can make you feel better ;)