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