Modular and Event-Driven JavaScript

18,444 views

Published on

Ever wondered how to get rid of that spaghetti, single-filed JavaScript code? Wouldn't it be nice if you could write maintainable modules, easily test them, port them to different projects, handle its library dependencies, and have them decoupled from other modules?

In this talk, we'll see how using the AMD API and an event-driven design will help taming an application's JavaScript code and scaling it to the future and beyond.

Modular and Event-Driven JavaScript

  1. 1. Modular JavaScript Heaven with AMD and Events @shiota ConFoo 2015
  2. 2. HELLO!slideshare.net/eshiota github.com/eshiota @shiota
  3. 3. * 16/02/2015 https://github.com/search?p=1&q=stars%3A%3E1&s=stars&type=Repositories Out of the top 50 most starred repos in Github, 25 are JavaScript related.
  4. 4. JavaScript is awesome!
  5. 5. But sometimes it sucks.
  6. 6. It may become unreadable and unmaintainable.
  7. 7. (function(){ window.app = jQuery.extend({ init: function(){ tab = $('.tabs li > a.tab-toggle'); tabs = $('.tabs').find('> div'); if (tabs.length > 1){ tab.each(function (i){$(this).attr('href', '#content-' + ++i)}); tabs.each(function(i){$(this).attr('id', 'content-' + ++i)}); tabs.addClass('tab-inactive'); $('.tabs li:first-child a').addClass('state-active'); } $('#initial-cash, #financing_value_vehicles, #tax, #bid-initial-cash, #bid-product-value').maskMoney({ thousands: '.', decimal: ',', allowZero: true, allowNegative: false, defaultZero: true }); /** FINANCING CALCULATOR **/ $("#financing_value_vehicles").on("blur", function(){ var price = (accounting.unformat($(this).val(), ",")) || 0; var suggestedInitialPayment = price * 0.2; var formattedResult = accounting.formatMoney(suggestedInitialPayment, "", "2", ".", ","); $("#initial-cash").val(formattedResult); }); $("#calculate-financing").click(function(event){ var price = (accounting.unformat($("#financing_value_vehicles").val(), ",")) || 0; var rate = (accounting.unformat($("#tax").val(), ",") / 100) || 0; var initialCash = (accounting.unformat($("#initial-cash").val(), ",")) || 0; var value = (accounting.unformat($("#amount-finance").val(), ",")) || 0; var finance = price - initialCash; var months = (accounting.unformat($("#prize_parcela").val(), ",")) || 0; var tax = parseFloat(rate);
  8. 8. (single JS file, 173 LOC inside one function, at least 7 different concerns)
  9. 9. It may become a CALLBACK HELL (and some libraries just make it worse)
  10. 10. $(document).ready(function () { $(".submit-button").on("click", function () { $.ajax({ url : "/create", type : "POST", success : function (data) { $.each(data.created_items, function (index, value) { var item = $("<div />").text(value); $(".items-list").append(item).hide().fadeIn(400, function () { setTimeout(function () { item.fadeOut(function () { item.remove(); }); }, 1000); }); }); } }); }); });
  11. 11. A modular, event-based structure solves these problems and much more.
  12. 12. Modular JavaScript Heaven with AMD and Events (on the web)
  13. 13. Agenda Making it modular Controlling the flow Communicating through events
  14. 14. MODULES
  15. 15. Single responsability, part of a complex system.
  16. 16. Isolated behaviour and knowledge.
  17. 17. Testable.
  18. 18. Extensible and modifiable.
  19. 19. (good taste is optional)
  20. 20. May be replaced and reused.
  21. 21. Namespaces
  22. 22. Helps organising the code in separate blocks.
  23. 23. /////////////////////////////// // Code for photo gallery /////////////////////////////// var gallery = $(".gallery"); var galleryCurrent = 0; var galleryTimer = 5000; $(".thumbs").on("click", function (event) { // ... }); function goToNext () { // ... } setTimeout(function () { goToNext(); }, galleryTimer); /////////////////////////////// // Code for switching tabs /////////////////////////////// var tabs = $(".tabs"); tabs.on("click", function (event) { // ... }); function switchTab(tab) { // ... } ✗
  24. 24. MyApp.components.photoGallery($(".gallery")); MyApp.ui.tabs($(".tabs"))
  25. 25. var MyApp = MyApp || {}; MyApp.components = MyApp.components || {}; MyApp.components.photoGallery = function (element) { // ... };
  26. 26. var MyApp = MyApp || {}; MyApp.ui = MyApp.ui || {}; MyApp.ui.tabs = function (element) { // ... };
  27. 27. Provides a fairly logical structure.
  28. 28. /javascript /myapp /components /photoGallery.js /ui /tabs.js
  29. 29. Avoids polluting the global context.
  30. 30. window $ photoGallery tabs profile login plugin
  31. 31. window $ MyApp plugin components ui sections photoGallery tabs profile login
  32. 32. Module pattern
  33. 33. Provides a scope, and allows "public" and "private" APIs.
  34. 34. MyApp.components.photoGallery = (function (window, document, $) { // Private properties // ------------------ var element; var thumbs; var current = 0; var timer = 5000; // Private methods // --------------- var goToNext = function () { // ... }; var setupGallery = function () { thumbs.on("click", function () { // ... }); setTimeout(function () { goToNext(); }, timer); } return { // Public methods // -------------- init : function (el) { element = $(el); thumbs = element.find("[data-thumbs]"); setupGallery(); } }; })(window, document, jQuery);
  35. 35. MyApp.components.photoGallery // Private properties // ------------------ var var var var // Private methods // --------------- var }; var thumbs.on( }); setTimeout( goToNext(); }, timer); } return init element thumbs setupGallery(); } }; })(window // Private properties // ------------------ var element; var thumbs; var current = 0; var timer = 5000; // Private methods // --------------- var goToNext = function () { // ... }; var setupGallery = function () { thumbs.on("click", function () { // ... }); setTimeout(function () { goToNext(); }, timer); }
  36. 36. MyApp.components.photoGallery // Private properties // ------------------ var var var var // Private methods // --------------- var }; var thumbs.on( }); setTimeout( goToNext(); }, timer); } return init element thumbs setupGallery(); } }; })(window // Public methods // -------------- init : function (el) { element = $(el); thumbs = element.find("[data-thumbs]"); setupGallery(); }
  37. 37. It's "singleton-like".
  38. 38. Constructors & Prototypes
  39. 39. Allows multiple instances of the same behaviour, and prototype inheritance.
  40. 40. B.wishlistMap.ListItem = function (element) { this.element = $(el); this.init(); }; B.wishlistMap.ListItem.prototype = { init : function () { // ... }, expandCard : function () { // ... }, contractCard : function () { // ... } };
  41. 41. var list = $("#wl-cards"); var items = list.find(".wl-card"); items.each(function () { $(this).data("ListItem", new B.wishlistMap.ListItem(this)); }); // or var itemsInstances = []; items.each(function () { itemsInstances.push(new B.wishlistMap.ListItem(this)); });
  42. 42. AMD
  43. 43. Asynchronous Module
 Definition
  44. 44. // photoGallery.js define([ "jQuery", "myapp/lib/keycodes", “myapp/components/Map” ], function ($, keycodes, Map) { // ... });
  45. 45. Provides a consistent API to define modules.
  46. 46. // photoGallery.js define([ ], }); // photoGallery.js define( );
  47. 47. It handles dependencies.
  48. 48. // photoGallery.js define([ ], }); // photoGallery.js [ "jQuery", "myapp/lib/keycodes", “myapp/components/Map” ]
  49. 49. It injects the dependencies in a callback function…
  50. 50. // photoGallery.js define([ ], }); // photoGallery.js function ($, keycodes, Map) { }
  51. 51. … which gives you freedom to implement your solution.
  52. 52. // Module pattern define([ "myDependency" ], function(myDependency) { var myPrivateMethod = function() { }; return { myPublicMethod : function() { myPrivateMethod(); } } });
  53. 53. // Constructor define([ "myDependency" ], function(myDependency) { var MyConstructor = function() { }; MyConstructor.prototype.myPublicMethod = function() { }; return MyConstructor; });
  54. 54. // Simple execution define([ "myDependency" ], function(myDependency) { // do something });
  55. 55. Whatever is returned by the callback will be injected when the module is required.
  56. 56. myapp/ components/ myComponent.js define(function() { return "foobar"; });
  57. 57. myapp/ components/ anotherComponent.js define([ "myapp/components/myComponent" ], function(myComponent) { console.log(myComponent === "foobar"); // true })
  58. 58. You can also assign an object literal to the module.
  59. 59. define({ color : "yellow", size : "medium", quantity : 2 });
  60. 60. Modules are defined by their path…
  61. 61. /myapp /components photoGallery.js define(function() { });
  62. 62. /myapp /components anotherComponent.js define([ "myapp/components/photoGallery" ], function(photoGallery) { });
  63. 63. /myapp /components anotherComponent.js define([ "./photoGallery" ], function(photoGallery) { });
  64. 64. … or by their identifier.
  65. 65. define(“MyApp.components.photoGallery", function() { });
  66. 66. define("MyApp.components.anotherComponent", [ "MyApp.components.photoGallery" ], function(photoGallery) { });
  67. 67. // jQuery does this internally define("jquery", function() { return jQuery; });
  68. 68. You can also use the require method.
  69. 69. var photoGallery = require("MyApp.components.photoGallery"); photoGallery.init();
  70. 70. require(["MyApp.components.photoGallery"], function(photoGallery) { photoGallery.init(); });
  71. 71. Its behaviour depends on the AMD implementation.
  72. 72. RequireJS vs. Almond
  73. 73. RequireJS allows asynchronous module loading.
  74. 74. <script src="javascript/require.js" data-main="javascript/myapp/app"></script>
  75. 75. Be REALLY careful with that.
  76. 76. r.js optimises and combines dependencies.
  77. 77. (be careful with dynamic module requests)
  78. 78. // won't work require(['section/' + section]);
  79. 79. Lean, 1kb AMD loader.
  80. 80. It expects all modules to be already loaded.
  81. 81. <script src="javascript/almond.js"></script> <script src="javascript/vendor/jquery-1.11.2.js"></script> <script src="javascript/vendor/EventEmitter.js"></script> <script src="javascript/myapp/components/photoGallery.js"></script> <script src="javascript/myapp/components/myComponent.js"></script> <script src="javascript/myapp/section/login.js"></script> <script src="javascript/myapp/section/profile.js"></script> <script src="javascript/myapp/ui/dropdown.js"></script> <script src="javascript/myapp/ui/tabs.js"></script> <script src="javascript/myapp/core/sectionInitializer.js"></script> <script src="javascript/myapp/app.js"></script>
  82. 82. <script src="javascript/almond.js"></script> <script src="javascript/app-41884634e80f3516.js"></script>
  83. 83. It expects all modules to have a name.
  84. 84. define("myModule", [ "myDependencyA", "myDependencyB" ], function() { });
  85. 85. Since all modules are loaded, you can use the simple require function anywhere.
  86. 86. var photoGallery = require("MyApp.components.photoGallery"); photoGallery.init();
  87. 87. Gives the AMD syntax without extra headaches.
  88. 88. It (probably) works with your existing codebase.
  89. 89. APPLICATION FLOW
  90. 90. Single entry points
  91. 91. (function(){ window.app = jQuery.extend({ init: function(){ tab = $('.tabs li > a.tab-toggle'); tabs = $('.tabs').find('> div'); if (tabs.length > 1){ tab.each(function (i){$(this).attr('href', '#content-' + ++i)}); tabs.each(function(i){$(this).attr('id', 'content-' + ++i)}); tabs.addClass('tab-inactive'); $('.tabs li:first-child a').addClass('state-active'); } $('#initial-cash, #financing_value_vehicles, #tax, #bid-initial-cash, #bid-product-value').maskMoney({ thousands: '.', decimal: ',', allowZero: true, allowNegative: false, defaultZero: true }); /** FINANCING CALCULATOR **/ $("#financing_value_vehicles").on("blur", function(){ var price = (accounting.unformat($(this).val(), ",")) || 0; var suggestedInitialPayment = price * 0.2; var formattedResult = accounting.formatMoney(suggestedInitialPayment, "", "2", ".", ","); $("#initial-cash").val(formattedResult); }); $("#calculate-financing").click(function(event){ var price = (accounting.unformat($("#financing_value_vehicles").val(), ",")) || 0; var rate = (accounting.unformat($("#tax").val(), ",") / 100) || 0; var initialCash = (accounting.unformat($("#initial-cash").val(), ",")) || 0; var value = (accounting.unformat($("#amount-finance").val(), ",")) || 0; var finance = price - initialCash; var months = (accounting.unformat($("#prize_parcela").val(), ",")) || 0; var tax = parseFloat(rate); var nominator = (Math.pow(1 + tax, months)); var denominator = ((Math.pow(1 + tax, months)) - 1); var formattedFinance = accounting.formatMoney(finance, "", "2", ".", ","); $("amount-finance").val(formattedFinance); var financingValue = finance*nominator*tax/denominator; var result = accounting.formatMoney(financingValue, "R$ ", "2", ".", ","); $(".calculator_financing li.result p.value").text(result); this.button = $("#calc"); if( result != ""){ $("a.button").remove(); this.button.after("<a href='financiamento/new?vehicle_value="+price+"' class='button'>Cote Agora</a>"); }; event.preventDefault(); }); $("#initial-cash").bind("blur", function () { var price = (accounting.unformat($("#financing_value_vehicles").val(), ",")) || 0; var initialCash = (accounting.unformat($("#initial-cash").val(), ",")) || 0; var finance = price - initialCash; var formattedValue = accounting.formatMoney(finance, "", "2", ".", ","); $("#amount-finance").val(formattedValue); }); /** ------------ **/ /** BID CALCULATOR **/ $("input#calculate-bid").click(function(event){ var price = (accounting.unformat($("#bid-product-value").val(), ",")) || 0; var rate = (accounting.unformat($("#bid-tax").val(), ",") / 100) || 0; var initialCash = (accounting.unformat($("#bid-initial-cash").val(), ",")) || 0; var value = (accounting.unformat($("#bid-amount-finance").val(), ",")) || 0; var finance = price - initialCash; var months = (accounting.unformat($("#bid-prize_parcela").val(), ",")) || 0; var tax = parseFloat(rate); var nominator = (Math.pow(1 + tax, months)); var denominator = ((Math.pow(1 + tax, months)) - 1); var formattedFinance = accounting.formatMoney(finance, "", "2", ".", ","); $("#bid-amount-finance").val(formattedFinance); var result = accounting.formatMoney(((finance*nominator*tax/denominator)), "R$ ", "2", ".", ","); $(".calculator_bid li.result p.value").text(result); event.preventDefault(); }); $("#bid-initial-cash").bind("blur", function () { var price = (accounting.unformat($("#bid-product-value").val(), ",")) || 0; var initialCash = (accounting.unformat($("#bid-initial-cash").val(), ",")) || 0; var finance = price - initialCash; var formattedValue = accounting.formatMoney(finance, "", "2", ".", ","); $("#bid-amount-finance").val(formattedValue); }); /** ------------ **/ $('.state-active').each(function(i){ active_tab = $(this).attr('href') $(this).parents('section').find('div' + active_tab).addClass('tab-active') }); $('.tooltip').hide(); if ($("html").is(".lt-ie9")) { $('a').hover( function(){ $(this).siblings('.tooltip').show(); }, function(){ $(this).siblings('.tooltip').hide(); } ); } else { $('a').hover( function(){ $(this).siblings('.tooltip').fadeIn(); }, function(){ $(this).siblings('.tooltip').fadeOut(); } ); } tab.live('click', function(event){ event.preventDefault(); link = $(this).attr('href') el = $(this).parents('.tabs') el.find('div').removeClass('tab-active'); el.find('a').removeClass('state-active'); $(this).addClass('state-active') el.find('div' + link).addClass('tab-active'); }); $('a').unbind('click').hasClass('state-active'); $('a.state-active').unbind('click'); $("#schedule nav a").live("click", function(event){ $('#schedule nav a').removeClass('state-active') $(this).addClass('state-active') $(".window div").animate({ top: "" + ($(this).hasClass("prev") ? 0 : -210) + "px" }); event.preventDefault() }); app.advertisementNewForm(); }, advertisementNewForm: function(){ $('span.select-image').bind('click', function(){ $(this).parent('li').find('input[type="file"]').click(); }); } }); $().ready(app.init); }).call(this);
  92. 92. Page load jQuery load jQuery plugins application.js
  93. 93. if ($("#my-tabs").length) { // tabs code } if ($("#my-gallery").length) { // gallery code } if ($("#my-map").length) { // map code } ✗
  94. 94. Without a flow control, it’s difficult separating code execution per page/need.
  95. 95. Single entry points control the flow of the application.
  96. 96. Page load Vendor code Application modules sectionInitializer section module1 module2 module3 module4
  97. 97. Page load Vendor code Application modules sectionInitializer section module1 module2 module3 module4
  98. 98. <body data-section='profile'>
  99. 99. define('myapp.core.sectionInitializer', function() { var section = document.querySelector('body').dataset.section; require('myapp.section.' + section); });
  100. 100. // No logic, only modules bootstrapping define('myapp.section.profile', [ 'myapp.components.photoGallery', 'myapp.components.activityFeed', 'myapp.ui.Tabs' ], function(photoGallery, activityFeed, Tabs) { photoGallery.init($('[data-user-photo-gallery]')); activityFeed.init($('[data-user-activity-feed]')); new Tabs($('[data-user-section-tabs]')); new Tabs($(‘[data-user-report-type-tabs]')); });
  101. 101. Those initializers contain no logic, they only init modules.
  102. 102. Self-registered Modules
  103. 103. Page load Vendor code Application modules moduleInitializer module1 module2 module3 module4 module3 HTML module4 HTML module1 HTML module2 HTML
  104. 104. <div data-module="photoGallery"> <!-- gallery markup --> </div> <div data-module="wishlistMap"> <!-- map markup --> </div>
  105. 105. // moduleInitializer $("[data-module]").each(function () { var module = $(this).data("module"); require("myapp.modules." + module).init(this); });
  106. 106. Each module “initializes itself”.
  107. 107. Action/section free, global use.
  108. 108. EVENTS
  109. 109. Observer Pattern
  110. 110. Observable (server) “When a new episode is available, I’ll notify all pirat—I mean—clients."
  111. 111. Observer (client/pirate) “Whenever the server notifies me about a new episode, I’ll download it.”
  112. 112. OBSERVER OBSERVABLE
  113. 113. Yo, can you give me a shout out whenever a new GoT episode comes out? Sure, my good sire!
  114. 114. YO "THE RAINS OF CASTAMERE" IS OUT GET IT WHILE IT’S HOT Cool, then I’ll download it! It’ll surely by a very happy episode <3
  115. 115. // Pirate observes torrent server torrentServer.on("new-got-episode", function (name) { this.download(name); }); // Torrent server publishes that it has a new GoT episode this.trigger("new-got-episode", "The Rains of Castamere");
  116. 116. Mediator
  117. 117. Facilitates the communication between modules.
  118. 118. MEDIATOR
  119. 119. All modules have no knowledge about each other, except for the Mediator.
  120. 120. MEDIATOR Mediator, please tell me when a new The Walking Dead episode comes out Sure thing
  121. 121. MEDIATOR Mediator, please tell me when a new Mythbusters episode comes out Groovy. Hey, I’ll want that as well! Jammin'.
  122. 122. MEDIATOR Mediator, there’s a new The Walking Dead episode! Here’s the link! Yo folks, there’s a new The Walking Dead episode! Here’s the link! Oh yeah, I’ll get it right now!
  123. 123. MEDIATOR Mediator, there’s a new Mythbusters episode! Here’s the link! Yo everyone, there’s a new Mythbusters episode! Here’s the link! Cool, I’ll download it! Tnx yo!
  124. 124. // Pirate 1 subscribes to mediator mediator.on("new-twd-episode", function (data) { console.log("Downloading " + data.name + " from " + data.link); }); mediator.on("new-mythbusters-episode", function (data) { console.log("Downloading " + data.name + " from " + data.link); }); // Pirate 2 subscribes to mediator mediator.on("new-mythbusters-episode", function (data) { console.log("Downloading " + data.name + " from " + data.link); }); // Torrent server 1 publishes on mediator mediator.trigger("new-twd-episode", { link : "http://foo.bar", name : "The Suicide King" }); // Torrent server 2 publishes on mediator mediator.trigger("new-mythbusters-episode", { link : "http://theillegalbay.br", name : "Hollywood Myths" });
  125. 125. Everyone knows only the Mediator.
  126. 126. jQuery
  127. 127. MyApp.mediator = $({});
  128. 128. Mixin
  129. 129. // MyApp.myModule now has the `on`, `off` and `trigger` methods $.extend(MyApp.myModule, EventEmitter.prototype); MyApp.components.myModule.trigger("my-event", "my-data");
  130. 130. GETTING REAL
  131. 131. AMD vs. CommonJS vs. Plain Objects vs. ES6
  132. 132. How does your product work?
  133. 133. Synchronous or asynchronous?
  134. 134. Do you have a build process?
  135. 135. Will you use client-side code on the server-side?
  136. 136. Are you feeling lucky?
  137. 137. Events vs. Promises vs. Dependency Injection vs. ES6
  138. 138. ¯_(ツ)_/¯
  139. 139. In the end, there’s no unique solution.
  140. 140. Choose whatever makes you and your team more comfortable.
  141. 141. If you do it right, JavaScript can be fun.
  142. 142. THANKS!slideshare.net/eshiota github.com/eshiota @shiota

×