PageRenderTime 3ms CodeModel.GetById 2ms app.highlight 19ms RepoModel.GetById 2ms app.codeStats 0ms

/published/step-by-step-modules.md

https://github.com/bgabel/writings
Markdown | 761 lines | 629 code | 132 blank | 0 comment | 0 complexity | d25ae6b37a8160871e9b31c36b43fd6b MD5 | raw file
  1Step by step to Backbone.js — Require.js modules
  2======================================================
  3
  4In [my last step by step article][stepbystep] I took a piece of regular
  5jQuery-based JavaScript code and transformed it into Backbone using
  6Models, Collections, Views and Events. In this blog post I'll build on
  7the code and step by step introduce Require.js modules. We'll finish
  8off with creating a production ready version of the code, minified into
  9a single JavaScript file.
 10
 11Initial setup
 12-------------
 13
 14This article starts off where we finished last time around. The app is
 15[up and running][appinit], and here is the final JavaScript from last
 16time, `monologue.js`:
 17
 18```javascript
 19var Status = Backbone.Model.extend({
 20    url: '/status'
 21});
 22
 23var Statuses = Backbone.Collection.extend({
 24    model: Status
 25});
 26
 27var NewStatusView = Backbone.View.extend({
 28    events: {
 29        'submit form': 'addStatus'
 30    },
 31
 32    initialize: function() {
 33        this.collection.on('add', this.clearInput, this);
 34    },
 35
 36    addStatus: function(e) {
 37        e.preventDefault();
 38
 39        this.collection.create({ text: this.$('textarea').val() });
 40    },
 41
 42    clearInput: function() {
 43        this.$('textarea').val('');
 44    }
 45});
 46
 47var StatusesView = Backbone.View.extend({
 48    initialize: function() {
 49        this.collection.on('add', this.appendStatus, this);
 50    },
 51
 52    appendStatus: function(status) {
 53        this.$('ul').append('<li>' + status.escape('text') + '</li>');
 54    }
 55});
 56
 57$(document).ready(function() {
 58    var statuses = new Statuses();
 59    new NewStatusView({ el: $('#new-status'), collection: statuses });
 60    new StatusesView({ el: $('#statuses'), collection: statuses });
 61});
 62```
 63
 64And here is the HTML:
 65
 66```html
 67<!DOCTYPE html>
 68<html>
 69  <head>
 70    <title>Step by step</title>
 71    <meta charset="utf-8">
 72    <script src="vendor/jquery-1.8.3.js"></script>
 73    <script src="vendor/underscore-1.4.2.js"></script>
 74    <script src="vendor/backbone-0.9.2.js"></script>
 75    <script src="monologue.js"></script>
 76  </head>
 77  <body>
 78    <div id="new-status">
 79      <h2>New monolog</h2>
 80      <form>
 81        <textarea></textarea><br>
 82        <input type="submit" value="Post"/>
 83      </form>
 84    </div>
 85
 86    <div id="statuses">
 87      <h2>Monologs</h2>
 88      <ul></ul>
 89    </div>
 90  </body>
 91</html>
 92```
 93
 94You can find this code [on GitHub][monologue]. If you download the code,
 95you can run `node app.js` to get a server which responds to the adding
 96of statuses, which makes it far easier to follow along in this blog
 97post.
 98
 99Modules using Require.js
100------------------------
101
102Most of us have written 1000+ lines of JavaScript code in a single file
103at some point. For large projects this is hard to work with, difficult
104to test, and next to impossible to reuse and extend.
105
106The code we have above looks good for now, but gradually the size and
107complexity will increase, and suddenly the file is too long and
108unwieldy. In this blog post we'll use Require.js to split the code into
109several files. Require.js uses the Asynchronous Module Definition (AMD)
110API for handling JavaScript modules, which you can read more about in 
111[their documentation][whyamd].
112
113So, let's start using Require.js. First of all we must include the
114library and tell it what will be our main application entry point. In
115the HTML this can be done as follows:
116
117```diff
118 <!DOCTYPE html>
119 <html>
120   <head>
121     <title>Step by step</title>
122     <meta charset="utf-8">
123-    <script src="vendor/jquery-1.8.3.js"></script>
124-    <script src="vendor/underscore-1.4.2.js"></script>
125-    <script src="vendor/backbone-0.9.2.js"></script>
126-    <script src="monologue.js"></script>
127+    <script data-main="monologue.js" src="vendor/require-2.1.2.js"></script>
128   </head>
129   <body>
130     <div id="new-status">
131       <h2>New monolog</h2>
132       <form>
133         <textarea></textarea><br>
134         <input type="submit" value="Post"/>
135       </form>
136     </div>
137 
138     <div id="statuses">
139       <h2>Monologs</h2>
140       <ul></ul>
141     </div>
142   </body>
143 </html>
144```
145
146Now we must wrap our JavaScript, `monologue.js`, in a little Require.js
147setup:
148
149```diff
150+requirejs.config({
151+    paths: {
152+        'jquery': 'vendor/jquery-1.8.3'
153+      , 'underscore': 'vendor/underscore-1.4.2'
154+      , 'backbone': 'vendor/backbone-0.9.2'
155+    },
156+    shim: {
157+        'backbone': {
158+            deps: ['underscore', 'jquery'],
159+            exports: 'Backbone'
160+        },
161+        'underscore': {
162+            exports: '_'
163+        }
164+    }
165+});
166+
167+require([
168+    'jquery'
169+  , 'backbone'
170+], function($, Backbone) {
171+
172 var Status = Backbone.Model.extend({
173     url: '/status'
174 });
175 
176 var Statuses = Backbone.Collection.extend({
177     model: Status
178 });
179 
180 var NewStatusView = Backbone.View.extend({
181     events: {
182         'submit form': 'addStatus'
183     },
184 
185     initialize: function() {
186         this.collection.on('add', this.clearInput, this);
187     },
188 
189     addStatus: function(e) {
190         e.preventDefault();
191 
192         this.collection.create({ text: this.$('textarea').val() });
193     },
194 
195     clearInput: function() {
196         this.$('textarea').val('');
197     }
198 });
199 
200 var StatusesView = Backbone.View.extend({
201     initialize: function() {
202         this.collection.on('add', this.appendStatus, this);
203     },
204 
205     appendStatus: function(status) {
206         this.$('ul').append('<li>' + status.escape('text') + '</li>');
207     }
208 });
209 
210 $(document).ready(function() {
211     var statuses = new Statuses();
212     new NewStatusView({ el: $('#new-status'), collection: statuses });
213     new StatusesView({ el: $('#statuses'), collection: statuses });
214 });
215+
216+});
217```
218
219In the Require.js config we use `paths` to say where we find a library.
220This lets us require `backbone` instead of `vendor/backbone-0.9.2` all
221over the place. Also, when we update our version of Backbone we only
222have one place we need change the mapping and we are good to go. The
223[`shim`][shim] options is used for those JavaScript libraries we want to
224pull in which do not register as an AMD module, such as Backbone and
225Underscore. jQuery, however, do register as an AMD module, so we don't
226need to shim it.
227
228Splitting out modules
229---------------------
230
231Now that we have our base setup and our app is still up and running, we
232can start moving the separate parts out of `monologue.js`. Lets start by
233creating a `modules/status` folder, then we can start by moving the
234Status model into this folder:
235
236```diff
237 requirejs.config({
238     paths: {
239         'jquery': 'vendor/jquery-1.8.3'
240       , 'underscore': 'vendor/underscore-1.4.2'
241       , 'backbone': 'vendor/backbone-0.9.2'
242     },
243     shim: {
244         'backbone': {
245             deps: ['underscore', 'jquery'],
246             exports: 'Backbone'
247         },
248         'underscore': {
249             exports: '_'
250         }
251     }
252 });
253 
254 require([
255     'jquery'
256   , 'backbone'
257+  , 'modules/status/status'
258-], function($, Backbone) {
259+], function($, Backbone, Status) {
260-
261-    var Status = Backbone.Model.extend({
262-        url: '/status'
263-    });
264 
265     var Statuses = Backbone.Collection.extend({
266         model: Status
267     });
268     
269     var NewStatusView = Backbone.View.extend({
270         events: {
271             'submit form': 'addStatus'
272         },
273     
274         initialize: function() {
275             this.collection.on('add', this.clearInput, this);
276         },
277     
278         addStatus: function(e) {
279             e.preventDefault();
280     
281             this.collection.create({ text: this.$('textarea').val() });
282         },
283     
284         clearInput: function() {
285             this.$('textarea').val('');
286         }
287     });
288     
289     var StatusesView = Backbone.View.extend({
290         initialize: function() {
291             this.collection.on('add', this.appendStatus, this);
292         },
293     
294         appendStatus: function(status) {
295             this.$('ul').append('<li>' + status.escape('text') + '</li>');
296         }
297     });
298     
299     $(document).ready(function() {
300         var statuses = new Statuses();
301         new NewStatusView({ el: $('#new-status'), collection: statuses });
302         new StatusesView({ el: $('#statuses'), collection: statuses });
303     });
304 
305 });
306```
307
308As you can see above we include `modules/status/status.js`, so we copy
309the Status model into this file:
310
311```javascript
312define(['backbone'], function(Backbone) {
313
314    var Status = Backbone.Model.extend({
315        url: '/status'
316    });
317
318    return Status;
319
320});
321```
322
323Let's do the same with the `Statuses` collection.
324
325```diff
326 requirejs.config({
327     paths: {
328         'jquery': 'vendor/jquery-1.8.3'
329       , 'underscore': 'vendor/underscore-1.4.2'
330       , 'backbone': 'vendor/backbone-0.9.2'
331     },
332     shim: {
333         'backbone': {
334             deps: ['underscore', 'jquery'],
335             exports: 'Backbone'
336         },
337         'underscore': {
338             exports: '_'
339         }
340     }
341 });
342 
343 require([
344     'jquery'
345   , 'backbone'
346-  , 'modules/status/status'
347+  , 'modules/status/statuses'
348-], function($, Backbone, Status) {
349+], function($, Backbone, Statuses) {
350-
351-    var Statuses = Backbone.Collection.extend({
352-        model: Status
353-    });
354     
355     var NewStatusView = Backbone.View.extend({
356         events: {
357             'submit form': 'addStatus'
358         },
359     
360         initialize: function() {
361             this.collection.on('add', this.clearInput, this);
362         },
363     
364         addStatus: function(e) {
365             e.preventDefault();
366     
367             this.collection.create({ text: this.$('textarea').val() });
368         },
369     
370         clearInput: function() {
371             this.$('textarea').val('');
372         }
373     });
374     
375     var StatusesView = Backbone.View.extend({
376         initialize: function() {
377             this.collection.on('add', this.appendStatus, this);
378         },
379     
380         appendStatus: function(status) {
381             this.$('ul').append('<li>' + status.escape('text') + '</li>');
382         }
383     });
384     
385     $(document).ready(function() {
386         var statuses = new Statuses();
387         new NewStatusView({ el: $('#new-status'), collection: statuses });
388         new StatusesView({ el: $('#statuses'), collection: statuses });
389     });
390 
391 });
392```
393
394As you can see, we no longer depend on the `Status` model in
395`monologue.js` as it is only needed in the Statuses collection, so we no
396longer include it. Our `modules/status/statuses.js`:
397
398```javascript
399define([
400    'backbone',
401    'modules/status/status'
402], function(Backbone, Status) {
403
404    var Statuses = Backbone.Collection.extend({
405        model: Status
406    });
407
408    return Statuses;
409
410});
411```
412
413We do the same with `NewStatusView`:
414
415```diff
416 requirejs.config({
417     paths: {
418         'jquery': 'vendor/jquery-1.8.3'
419       , 'underscore': 'vendor/underscore-1.4.2'
420       , 'backbone': 'vendor/backbone-0.9.2'
421     },
422     shim: {
423         'backbone': {
424             deps: ['underscore', 'jquery'],
425             exports: 'Backbone'
426         },
427         'underscore': {
428             exports: '_'
429         }
430     }
431 });
432 
433 require([
434     'jquery' 
435   , 'backbone'
436   , 'modules/status/statuses'
437   , 'modules/status/newStatusView'
438-], function($, Backbone, Statuses) {
439+], function($, Backbone, Statuses, NewStatusView) {
440-
441-    var NewStatusView = Backbone.View.extend({
442-        events: {
443-            "submit form": "addStatus"
444-        },
445-
446-        initialize: function(options) {
447-            this.collection.on("add", this.clearInput, this);
448-        },
449-
450-        addStatus: function(e) {
451-            e.preventDefault();
452-
453-            this.collection.create({ text: this.$('textarea').val() });
454-        },
455-
456-        clearInput: function() {
457-            this.$('textarea').val('');
458-        }
459-    });
460     
461     var StatusesView = Backbone.View.extend({
462         initialize: function() {
463             this.collection.on('add', this.appendStatus, this);
464         },
465     
466         appendStatus: function(status) {
467             this.$('ul').append('<li>' + status.escape('text') + '</li>');
468         }
469     });
470     
471     $(document).ready(function() {
472         var statuses = new Statuses();
473         new NewStatusView({ el: $('#new-status'), collection: statuses });
474         new StatusesView({ el: $('#statuses'), collection: statuses });
475     });
476 
477 });
478```
479
480`modules/status/newStatusView.js`:
481
482```javascript
483define([
484    'backbone'
485], function(Backbone) {
486
487    var NewStatusView = Backbone.View.extend({
488        events: {
489            "submit form": "addStatus"
490        },
491
492        initialize: function(options) {
493            this.collection.on("add", this.clearInput, this);
494        },
495
496        addStatus: function(e) {
497            e.preventDefault();
498
499            this.collection.create({ text: this.$('textarea').val() });
500        },
501
502        clearInput: function() {
503            this.$('textarea').val('');
504        }
505    });
506
507    return NewStatusView;
508
509});
510```
511
512And then StatusesView:
513
514```diff
515 requirejs.config({
516     paths: {
517         'jquery': 'vendor/jquery-1.8.3'
518       , 'underscore': 'vendor/underscore-1.4.2'
519       , 'backbone': 'vendor/backbone-0.9.2'
520     },
521     shim: {
522         'backbone': {
523             deps: ['underscore', 'jquery'],
524             exports: 'Backbone'
525         },
526         'underscore': {
527             exports: '_'
528         }
529     }
530 });
531 
532 require([
533     'jquery' 
534-  , 'backbone'
535   , 'modules/status/statuses'
536   , 'modules/status/newStatusView'
537+  , 'modules/status/statusesView'
538-], function($, Backbone, Statuses, NewStatusView) {
539+], function($, Statuses, NewStatusView, StatusesView) {
540-
541-    var StatusesView = Backbone.View.extend({
542-        initialize: function(options) {
543-            this.collection.on("add", this.appendStatus, this);
544-        },
545-
546-        appendStatus: function(status) {
547-            this.$('ul').append('<li>' + status.escape("text") + '</li>');
548-        }
549-    });
550 
551     $(document).ready(function() {
552         var statuses = new Statuses();
553         new NewStatusView({ el: $('#new-status'), collection: statuses });
554         new StatusesView({ el: $('#statuses'), collection: statuses });
555     });
556 
557 });
558```
559
560`modules/status/statusesView.js`:
561
562```javascript
563define([
564    'backbone'
565], function(Backbone) {
566
567    var StatusesView = Backbone.View.extend({
568        initialize: function(options) {
569            this.collection.on("add", this.appendStatus, this);
570        },
571
572        appendStatus: function(status) {
573            this.$('ul').append('<li>' + status.escape("text") + '</li>');
574        }
575    });
576
577    return StatusesView;
578
579});
580```
581
582And now `monologue.js` looks quite good:
583
584```javascript
585requirejs.config({
586    paths: {
587        'jquery': 'vendor/jquery-1.8.3'
588      , 'underscore': 'vendor/underscore-1.4.2'
589      , 'backbone': 'vendor/backbone-0.9.2'
590    },
591    shim: {
592        'backbone': {
593            deps: ['underscore', 'jquery'],
594            exports: 'Backbone'
595        },
596        'underscore': {
597            exports: '_'
598        }
599    }
600});
601
602require([
603    'jquery'
604  , 'modules/status/statuses'
605  , 'modules/status/newStatusView'
606  , 'modules/status/statusesView'
607], function($, Statuses, NewStatusView, StatusesView) {
608
609    $(document).ready(function() {
610        var statuses = new Statuses();
611        new NewStatusView({ el: $('#new-status'), collection: statuses });
612        new StatusesView({ el: $('#statuses'), collection: statuses });
613    });
614
615});
616```
617
618Pretty sweet. This file is now focused on kickstarting our application.
619Outside of this file, none of the other files fetch anything directly
620from the DOM. One of the primary benefits of this, is that it
621significantly increases the testability of the code. I've written [a
622little bit][responsibility] about this before.
623
624Getting ready for production
625----------------------------
626
627As we no longer have only one JavaScript file, we need to concatenate
628our files when preparing our code for production. When
629using Require.js the natural choice is using its minifier, [r.js][rjs].
630
631For Require.js we need to create a config file for the minification,
632`config/buildconfig.js`:
633
634```javascript
635({
636    // all modules are located relative to this path
637    baseUrl: '../public',
638
639    // name of file which kickstarts the application, aka the main file
640    name: 'monologue',
641
642    // use the main JS file configuration so we don't need to duplicate the values
643    mainConfigFile: '../public/monologue.js',
644
645    // additionally include Require.js itself as a dependency
646    include: ['vendor/require-2.1.2.js'],
647
648    // name the optimized file
649    out: '../build/monologue.js',
650
651    // keep 'em comments
652    preserveLicenseComments: true
653})
654```
655
656There is quite a lot of options for the build config, so I recommend
657checking out this [file][buildconfig], which contains all the options
658and a whole lot of documentation.
659
660Run the build using Node.js (remember to run from the project root):
661
662```sh
663$ node public/vendor/r.js -o config/buildconfig.js
664```
665
666Or using Java on OS X/Linux/Unix:
667
668```sh
669$ java -classpath lib/rhino/js.jar:lib/closure/compiler.jar \
670    org.mozilla.javascript.tools.shell.Main \
671    public/vendor/r.js -o config/buildconfig.js
672```
673
674Or using Java on Windows:
675
676```sh
677$ java -classpath lib/rhino/js.jar;lib/closure/compiler.jar \
678    org.mozilla.javascript.tools.shell.Main \
679    public/vendor/r.js -o config/buildconfig.js
680```
681
682And now we should see something similar to:
683
684```
685Tracing dependencies for: monologue
686Uglifying file: /Users/kjbekkelund/dev/monologue/build/monologue.js
687
688/Users/kjbekkelund/dev/monologue/build/monologue.js
689----------------
690/Users/kjbekkelund/dev/monologue/public/vendor/require-2.1.2.js
691/Users/kjbekkelund/dev/monologue/public/vendor/jquery-1.8.3.js
692/Users/kjbekkelund/dev/monologue/public/vendor/underscore-1.4.2.js
693/Users/kjbekkelund/dev/monologue/public/vendor/backbone-0.9.2.js
694/Users/kjbekkelund/dev/monologue/public/modules/status/status.js
695/Users/kjbekkelund/dev/monologue/public/modules/status/statuses.js
696/Users/kjbekkelund/dev/monologue/public/modules/status/newStatusView.js
697/Users/kjbekkelund/dev/monologue/public/modules/status/statusesView.js
698/Users/kjbekkelund/dev/monologue/public/monologue.js
699```
700
701And we'll have a minified JavaScript file in `build/monologue.js`. Now,
702making a production ready `index.html` is as simple as using the
703minified JavaScript file:
704
705```diff
706 <!DOCTYPE html>
707 <html>
708   <head>
709     <title>Step by step &mdash; Modules in Backbone</title>
710     <meta charset="utf-8">
711-    <script data-main="monologue.js" src="vendor/require-2.1.2.js"></script>
712+    <script src="monologue.js"></script>
713   </head>
714   <body>
715     <div id="new-status">
716     </div>
717 
718     <div id="statuses">
719     </div>
720   </body>
721 </html>
722```
723
724As you can see we only need to include `monologue.js` and nothing else.
725Now Require.js will fetch our JavaScript files as they are needed in
726development, while we have a single file which contains everything in
727production.
728
729And we are done! You can find the finished code [on
730GitHub][monologuedone].
731
732Finishing up
733------------
734
735In this blog post we have taken some steps further from my initial step
736by step introduction to Backbone.js, and introduced modules using
737Require.js into the mix. There are still many things that need to be
738done when setting up a large-scale JavaScript application, but we have
739taken some significant steps further. We have also created a
740production-ready version of our app.
741
742If you want a setup similar to this in a Java-only world, you can find a
743lot of inspiration in [this setup][js-java-setup].
744
745[responsibility]: http://open.bekk.no/a-views-responsibility/
746[stepbystep]: https://github.com/kjbekkelund/writings/blob/master/published/understanding-backbone.md
747[appinit]: http://monologue-2.herokuapp.com/
748[amd]: https://github.com/amdjs/amdjs-api/wiki/AMD
749[whyamd]: http://requirejs.org/docs/whyamd.html
750[text]: https://github.com/requirejs/text
751[hogan]: http://twitter.github.com/hogan.js/
752[handlebars]: http://handlebarsjs.com/
753[rjs]: https://github.com/jrburke/r.js/
754[mustachespec]: http://mustache.github.com/mustache.5.html
755[hgn]: https://github.com/millermedeiros/requirejs-hogan-plugin
756[buildconfig]: https://github.com/jrburke/r.js/blob/master/build/example.build.js
757[optimize]: https://github.com/jrburke/r.js/blob/c1be5af39ee8a0c0bdb74ce1df4ffe35277b2f49/build/example.build.js#L80-L91
758[js-java-setup]: https://github.com/kjbekkelund/js-java-setup
759[shim]: http://requirejs.org/docs/api.html#config-shim
760[monologue]: https://github.com/kjbekkelund/monologue/tree/modules-start
761[monologuedone]: https://github.com/kjbekkelund/monologue/tree/modules-end