Patterns of Large-Scale JavaScript Applications

Patterns of Large-Scale JavaScript Applications

Talk held at front/> in Zürich

6c51c14716e24bc1f1a3fb5ad234e773?s=128

Kim Joar Bekkelund

August 29, 2013
Tweet

Transcript

  1. of large-scale JavaScript applications patterns

  2. Kim Joar Bekkelund kimjoar.net @kimjoar

  3. We’re going to talk about large-scale JavaScript apps

  4. We’re going to talk about large-scale JavaScript apps where “large”

    really doesn’t need to be that large
  5. Unknown unknowns Complex domain Many developers Testing Long-lived Adapt to

    changes
  6. $('#my-button').click(function() { $.get('https://api.github.com', function(data) { $('#res').html(data.emojis_url); }); }); we always

    start with a small feature … The problem with JavaScript is that
  7. suddenly 5k lines in one file “how did this happen?”

  8. None
  9. Unobtrusive JavaScript circa 2006

  10. Unobtrusive JavaScript + circa 2006

  11. $

  12. jQuery Anti-patterns

  13. jQuery Anti-patterns $(“.test”).parent().parent().find(“.open”).fi $(“ul.test li p > a.open”)

  14. jQuery Anti-patterns $(“.test”).parent().parent().find(“.open”).fi $(document).ready(everything) $(“ul.test li p > a.open”)

  15. $(“.test”).data(“foo”, { some: “state” }) $(document).ready(everything) jQuery Anti-patterns $(“.test”).parent().parent().find(“.open”).fi $(“ul.test

    li p > a.open”)
  16. jQuery Anti-patterns making the DOM the source of truth is

    a bad idea
  17. Too often, the business logic is hidden in a mess

    of code
  18. What happens when the requirements change?

  19. jQuery(document).ready(function() { $(".user form").submit(function(e) { e.preventDefault(); $.ajax({ // ... success:

    function(data) { var name = data.name || "Unknown"; $(".user .info").append("<h1>" + name + "</h1>"); } }) }); }); Inspired by https://speakerdeck.com/bkeepers/the-plight-of-pinocchio and @searls
  20. jQuery(document).ready(function() { $(".user form").submit(function(e) { e.preventDefault(); $.ajax({ // ... success:

    function(data) { var name = data.name || "Unknown"; $(".user .info").append("<h1>" + name + "</h1>"); } }) }); }); Page event User event Hitting the server Nested callback Parsing response Templating Callback hell Inspired by https://speakerdeck.com/bkeepers/the-plight-of-pinocchio and @searls
  21. Considerable increase in the amount of JavaScript

  22. Highly interactive frontends now

  23. backend --> frontend

  24. New capabilities: geolocation, local storage, canvas, web workers, WebSockets backend

    --> frontend
  25. The biggest problem is no longer the browser it’s the

    size and complexity of our apps
  26. None
  27. We want testable, scalable, maintainable JavaScript

  28. This is difficult! actually

  29. we need to unlearn

  30. we need to unlearn our DOM-centric approach

  31. we need to unlearn our DOM-centric approach The DOM is

    Inherently global and stateful hard to test and maintain
  32. we need to unlearn how we write JavaScript

  33. we need to unlearn how we write JavaScript No more

    5k lines of code in a single file Watch out for global variables Learn from the backend
  34. we need to unlearn how we use jQuery

  35. we need to unlearn how we use jQuery “Our experience

    is that, when code is difficult to test, the most likely cause is that our design needs improving.” – Growing Object-Oriented Software, Guided by Tests
  36. Structure We need

  37. – Justin Meyer “The secret to building large apps is

    never build large apps. Break up your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application.” http://bitovi.com/blog/2010/11/organizing-a-jquery-application.html
  38. None
  39. We need a foundation

  40. move state and business logic away from the DOM First

    of all
  41. MVC

  42. MVC Google Trends for “JavaScript mvc”

  43. MVC A clean separation between data and the DOM

  44. MVC Mediates inputs and manipulates the model

  45. MVC NOT THE ONLY WAY. MANY VARIATIONS.

  46. MV*

  47. MV* everyone has a different take

  48. MVP all presentation logic is pushed to the presenter

  49. MVVM exposes model so it can be easily managed and

    consumed Bi-directional binding
  50. “I hereby declare AngularJS to be MVW framework – Model-View-Whatever.

    Where Whatever stands for ‘whatever works for you’.” – Igor Minar https://plus.google.com/app/basic/stream/z13vitkjpya0zxcee23ztzmqcor3fzen2
  51. The most important aspect is to separate STATE and the

    DOM
  52. First step ✓

  53. However, MV* is not enough

  54. None
  55. “Lots of monolithic single files that contained too much code.”

  56. “Actually how you partition your code has a lot to

    do with how you organize your project source code and how much easier it is to maintain as your project grows.” – Jim Lavin http://codingsmackdown.tv/blog/2013/04/19/angularjs-modules-for-great-justice/
  57. # files ++ filesize --

  58. Package by feature Package by layer vs

  59. Package by feature Package by layer vs

  60. Package by feature I prefer

  61. Package by feature I prefer I call them Modules

  62. Module

  63. Module

  64. Module

  65. Modules

  66. Modulescontain business logic

  67. Modules small are

  68. Modules focused are

  69. Modulesare decoupled from other modules

  70. None
  71. Modulesare preferably reusable

  72. Modulesare easily testable

  73. “A system is easier to change if its objects are

    context-independent; that is, if each object has no built-in knowledge about the system in which it executes.” – Growing Object-Oriented Software, Guided by Tests
  74. Module rules http://www.slideshare.net/nzakas/scalable-javascript-application-architecture-2012

  75. Module rules Never access the DOM outside the module http://www.slideshare.net/nzakas/scalable-javascript-application-architecture-2012

  76. Module rules Never access the DOM outside the module Don’t

    create global variables http://www.slideshare.net/nzakas/scalable-javascript-application-architecture-2012
  77. Module rules Never access the DOM outside the module Don’t

    create global variables Don’t access global, non-native objects http://www.slideshare.net/nzakas/scalable-javascript-application-architecture-2012
  78. Keeping this in mind should get you a long way

    http://www.flickr.com/photos/lrargerich/4999906554/
  79. Next up: dependencies

  80. We depend on other code dependencies We depend on other

    files We depend on Third-party libraries
  81. We depend on other code

  82. We usually depend far too much on globals We depend

    on other code
  83. We usually depend far too much on globals JavaScript makes

    this really easy We depend on other code
  84. $('#my-button').click(function() { $.get('https://api.github.com', function(data) { $('#res').html(data.emojis_url); }); });

  85. $('#my-button').click(function() { $.get('https://api.github.com', function(data) { $('#res').html(data.emojis_url); }); }); Cannot be

    reused Globals DIFFICULT TO TEST
  86. var downloadEmojis = function() { $.get('https://api.github.com', function(data) { $('#res').html(data.emojis_url); });

    }; $('#my-button').click(downloadEmojis);
  87. var downloadEmojis = function() { $.get('https://api.github.com', function(data) { $('#res').html(data.emojis_url); });

    }; $('#my-button').click(downloadEmojis); Globals DIFFICULT TO TEST
  88. var downloadEmojis = function(ajax, $el) { ajax.get('https://api.github.com', function(data) { $el.html(data.emojis_url);

    }); }; $('#my-button').click(function() { downloadEmojis($, $('#res')); });
  89. var downloadEmojis = function(ajax, $el) { ajax.get('https://api.github.com', function(data) { $el.html(data.emojis_url);

    }); }; $('#my-button').click(function() { downloadEmojis($, $('#res')); }); Now we can control the dependencies
  90. None
  91. var UserView = function(el, user) { this.el = el; this.user

    = user; }; // let's just create a super simple user object var user = { image: 'http://example.com/image.png' }; // initialize with the element the view owns and a user var view = new UserView($('.user'), user);
  92. var UserView = function(el, user) { this.el = el; this.user

    = user; }; UserView.prototype.showImage = function() { this.el.append('<img src=' + this.user.image + '>'); }; // let's just create a super simple user object var user = { image: 'http://example.com/image.png' }; // initialize with the element the view owns and a user var view = new UserView($('.user'), user);
  93. var UserView = function(el, user) { this.el = el; this.user

    = user; }; UserView.prototype.showImage = function() { this.el.append('<img src=' + this.user.image + '>'); }; // let's just create a super simple user object var user = { image: 'http://example.com/image.png' }; // initialize with the element the view owns and a user var view = new UserView($('.user'), user); // ... and now we can do stuff which changes the DOM view.showImage();
  94. var UserView = function(el, user) { this.el = el; this.user

    = user; }; UserView.prototype.showImage = function() { this.el.append('<img src=' + this.user.image + '>'); }; // let's just create a super simple user object var user = { image: 'http://example.com/image.png' }; // initialize with the element the view owns and a user var view = new UserView($('.user'), user); // ... and now we can do stuff which changes the DOM view.showImage(); $(‘<div></div>’) In tests
  95. Always inject dependencies

  96. has embraced this

  97. var MyController = function($scope, $http) { $http.get('https://api.github.com') .success(function(data) { $scope.emojis_url

    = data.emojis_url; }); } An example using AngularJS
  98. var MyController = function($scope, $http) { $http.get('https://api.github.com') .success(function(data) { $scope.emojis_url

    = data.emojis_url; }); } the ordering of these does not matter An example using AngularJS
  99. var MyController = function($http, $scope) { $http.get('https://api.github.com') .success(function(data) { $scope.emojis_url

    = data.emojis_url; }); } the ordering of these does not matter An example using AngularJS
  100. // Create an AngularJS module with no dependencies var conf

    = angular.module('conf', []);
  101. // Create an AngularJS module with no dependencies var conf

    = angular.module('conf', []); // Register a function conf.factory('awesome', function() { return 'awesome conference'; });
  102. // Create an AngularJS module with no dependencies var conf

    = angular.module('conf', []); // Register a function conf.factory('awesome', function() { return 'awesome conference'; }); // Get the injector (happens behind the scenes in AngularJS apps) var injector = angular.injector(['conf', 'ng']);
  103. // Create an AngularJS module with no dependencies var conf

    = angular.module('conf', []); // Register a function conf.factory('awesome', function() { return 'awesome conference'; }); // Get the injector (happens behind the scenes in AngularJS apps) var injector = angular.injector(['conf', 'ng']);
  104. // Create an AngularJS module with no dependencies var conf

    = angular.module('conf', []); // Register a function conf.factory('awesome', function() { return 'awesome conference'; }); // Get the injector (happens behind the scenes in AngularJS apps) var injector = angular.injector(['conf', 'ng']); // Call a function with dependency injection injector.invoke(function(awesome) { console.log('awesome() == ' + awesome); });
  105. // Create an AngularJS module with no dependencies var conf

    = angular.module('conf', []); // Register a function conf.factory('awesome', function() { return 'awesome conference'; }); // Get the injector (happens behind the scenes in AngularJS apps) var injector = angular.injector(['conf', 'ng']); // Call a function with dependency injection injector.invoke(function(awesome) { console.log('awesome() == ' + awesome); });
  106. http://docs.angularjs.org/guide/dev_guide.templates.databinding

  107. http://docs.angularjs.org/guide/dev_guide.templates.databinding

  108. {{#if isExpanded}} <div class='body'>{{body}}</div> <button {{action contract}}>Contract</button> {{else}} <button {{action

    expand}}>Show More...</button> {{/if}}
  109. {{#if isExpanded}} <div class='body'>{{body}}</div> <button {{action contract}}>Contract</button> {{else}} <button {{action

    expand}}>Show More...</button> {{/if}} App.PostController = Ember.ObjectController.extend({ body: 'body', isExpanded: false, expand: function() { this.set('isExpanded', true); }, contract: function() { this.set('isExpanded', false); } });
  110. {{#if isExpanded}} <div class='body'>{{body}}</div> <button {{action contract}}>Contract</button> {{else}} <button {{action

    expand}}>Show More...</button> {{/if}} App.PostController = Ember.ObjectController.extend({ body: 'body', isExpanded: false, expand: function() { this.set('isExpanded', true); }, contract: function() { this.set('isExpanded', false); } }); no jquery!
  111. dependencies We depend on other code We depend on other

    files We depend on Third-party libraries
  112. We depend on other files

  113. The order of <script> elements

  114. The order of <script> elements

  115. The order of <script> elements Lots of globals Weakly stated

    dependencies
  116. CommonJS AMD ECMAScript 6

  117. CommonJS AMD ECMAScript 6 var $ = require('jquery'); exports.myFn =

    function() {};
  118. CommonJS AMD ECMAScript 6 var $ = require('jquery'); exports.myFn =

    function() {}; define(['jquery'] , function($) { return function() {}; });
  119. CommonJS AMD ECMAScript 6 var $ = require('jquery'); exports.myFn =

    function() {}; define(['jquery'] , function($) { return function() {}; }); import $ from 'jquery'; var myFn = function() {} export { myFn };
  120. CommonJS AMD ECMAScript 6 var $ = require('jquery'); exports.myFn =

    function() {}; define(['jquery'] , function($) { return function() {}; }); import $ from 'jquery'; var myFn = function() {} export { myFn }; works better in current browsers
  121. AMD

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

  123. <script data-main="scripts/main" src="require.js"></script> // scripts/main.js requirejs.config({ paths: { 'jquery': 'lib/jquery',

    'underscore': 'lib/underscore', 'backbone': 'lib/backbone', } }); define(['app', 'jquery'], function(App, $) { var app = new App({ el: $('body') }); app.start(); });
  124. main.js app.js jQuery backbone underscore ...

  125. dependencies We depend on other code We depend on other

    files We depend on Third-party libraries
  126. We depend on Third-party libraries

  127. Package manager

  128. Bower // bower.json { "dependencies": { "angular": "~1.0.7", "angular-resource": "~1.0.7",

    "jquery": "1.9.1" } }
  129. Bower // bower.json { "dependencies": { "angular": "~1.0.7", "angular-resource": "~1.0.7",

    "jquery": "1.9.1" } } $ bower install
  130. None
  131. … and that’s as far as we’ll go today

  132. is all of this necessary? … but seriously,

  133. Want to learn more?

  134. None
  135. Questions?