Your SlideShare is downloading. ×
Modular and Event-Driven JavaScript
Upcoming SlideShare
Loading in...5
×

Thanks for flagging this SlideShare!

Oops! An error has occurred.

×
Saving this for later? Get the SlideShare app to save on your phone or tablet. Read anywhere, anytime – even offline.
Text the download link to your phone
Standard text messaging rates apply

Modular and Event-Driven JavaScript

16,874
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 …

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.


0 Comments
37 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
16,874
On Slideshare
0
From Embeds
0
Number of Embeds
5
Actions
Shares
0
Downloads
36
Comments
0
Likes
37
Embeds 0
No embeds

Report content
Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
No notes for slide

Transcript

  • 1. Modular JavaScript Heaven with AMD and Events @shiota ConFoo 2015
  • 2. HELLO!slideshare.net/eshiota github.com/eshiota @shiota
  • 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. JavaScript is awesome!
  • 5. But sometimes it sucks.
  • 6. It may become unreadable and unmaintainable.
  • 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. (single JS file, 173 LOC inside one function, at least 7 different concerns)
  • 9. It may become a CALLBACK HELL (and some libraries just make it worse)
  • 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. A modular, event-based structure solves these problems and much more.
  • 12. Modular JavaScript Heaven with AMD and Events (on the web)
  • 13. Agenda Making it modular Controlling the flow Communicating through events
  • 14. MODULES
  • 15. Single responsability, part of a complex system.
  • 16. Isolated behaviour and knowledge.
  • 17. Testable.
  • 18. Extensible and modifiable.
  • 19. (good taste is optional)
  • 20. May be replaced and reused.
  • 21. Namespaces
  • 22. Helps organising the code in separate blocks.
  • 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. MyApp.components.photoGallery($(".gallery")); MyApp.ui.tabs($(".tabs"))
  • 25. var MyApp = MyApp || {}; MyApp.components = MyApp.components || {}; MyApp.components.photoGallery = function (element) { // ... };
  • 26. var MyApp = MyApp || {}; MyApp.ui = MyApp.ui || {}; MyApp.ui.tabs = function (element) { // ... };
  • 27. Provides a fairly logical structure.
  • 28. /javascript /myapp /components /photoGallery.js /ui /tabs.js
  • 29. Avoids polluting the global context.
  • 30. window $ photoGallery tabs profile login plugin
  • 31. window $ MyApp plugin components ui sections photoGallery tabs profile login
  • 32. Module pattern
  • 33. Provides a scope, and allows "public" and "private" APIs.
  • 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. 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. 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. It's "singleton-like".
  • 38. Constructors & Prototypes
  • 39. Allows multiple instances of the same behaviour, and prototype inheritance.
  • 40. B.wishlistMap.ListItem = function (element) { this.element = $(el); this.init(); }; B.wishlistMap.ListItem.prototype = { init : function () { // ... }, expandCard : function () { // ... }, contractCard : function () { // ... } };
  • 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. AMD
  • 43. Asynchronous Module
 Definition
  • 44. // photoGallery.js define([ "jQuery", "myapp/lib/keycodes", “myapp/components/Map” ], function ($, keycodes, Map) { // ... });
  • 45. Provides a consistent API to define modules.
  • 46. // photoGallery.js define([ ], }); // photoGallery.js define( );
  • 47. It handles dependencies.
  • 48. // photoGallery.js define([ ], }); // photoGallery.js [ "jQuery", "myapp/lib/keycodes", “myapp/components/Map” ]
  • 49. It injects the dependencies in a callback function…
  • 50. // photoGallery.js define([ ], }); // photoGallery.js function ($, keycodes, Map) { }
  • 51. … which gives you freedom to implement your solution.
  • 52. // Module pattern define([ "myDependency" ], function(myDependency) { var myPrivateMethod = function() { }; return { myPublicMethod : function() { myPrivateMethod(); } } });
  • 53. // Constructor define([ "myDependency" ], function(myDependency) { var MyConstructor = function() { }; MyConstructor.prototype.myPublicMethod = function() { }; return MyConstructor; });
  • 54. // Simple execution define([ "myDependency" ], function(myDependency) { // do something });
  • 55. Whatever is returned by the callback will be injected when the module is required.
  • 56. myapp/ components/ myComponent.js define(function() { return "foobar"; });
  • 57. myapp/ components/ anotherComponent.js define([ "myapp/components/myComponent" ], function(myComponent) { console.log(myComponent === "foobar"); // true })
  • 58. You can also assign an object literal to the module.
  • 59. define({ color : "yellow", size : "medium", quantity : 2 });
  • 60. Modules are defined by their path…
  • 61. /myapp /components photoGallery.js define(function() { });
  • 62. /myapp /components anotherComponent.js define([ "myapp/components/photoGallery" ], function(photoGallery) { });
  • 63. /myapp /components anotherComponent.js define([ "./photoGallery" ], function(photoGallery) { });
  • 64. … or by their identifier.
  • 65. define(“MyApp.components.photoGallery", function() { });
  • 66. define("MyApp.components.anotherComponent", [ "MyApp.components.photoGallery" ], function(photoGallery) { });
  • 67. // jQuery does this internally define("jquery", function() { return jQuery; });
  • 68. You can also use the require method.
  • 69. var photoGallery = require("MyApp.components.photoGallery"); photoGallery.init();
  • 70. require(["MyApp.components.photoGallery"], function(photoGallery) { photoGallery.init(); });
  • 71. Its behaviour depends on the AMD implementation.
  • 72. RequireJS vs. Almond
  • 73. RequireJS allows asynchronous module loading.
  • 74. <script src="javascript/require.js" data-main="javascript/myapp/app"></script>
  • 75. Be REALLY careful with that.
  • 76. r.js optimises and combines dependencies.
  • 77. (be careful with dynamic module requests)
  • 78. // won't work require(['section/' + section]);
  • 79. Lean, 1kb AMD loader.
  • 80. It expects all modules to be already loaded.
  • 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. <script src="javascript/almond.js"></script> <script src="javascript/app-41884634e80f3516.js"></script>
  • 83. It expects all modules to have a name.
  • 84. define("myModule", [ "myDependencyA", "myDependencyB" ], function() { });
  • 85. Since all modules are loaded, you can use the simple require function anywhere.
  • 86. var photoGallery = require("MyApp.components.photoGallery"); photoGallery.init();
  • 87. Gives the AMD syntax without extra headaches.
  • 88. It (probably) works with your existing codebase.
  • 89. APPLICATION FLOW
  • 90. Single entry points
  • 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. Page load jQuery load jQuery plugins application.js
  • 93. if ($("#my-tabs").length) { // tabs code } if ($("#my-gallery").length) { // gallery code } if ($("#my-map").length) { // map code } ✗
  • 94. Without a flow control, it’s difficult separating code execution per page/need.
  • 95. Single entry points control the flow of the application.
  • 96. Page load Vendor code Application modules sectionInitializer section module1 module2 module3 module4
  • 97. Page load Vendor code Application modules sectionInitializer section module1 module2 module3 module4
  • 98. <body data-section='profile'>
  • 99. define('myapp.core.sectionInitializer', function() { var section = document.querySelector('body').dataset.section; require('myapp.section.' + section); });
  • 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. Those initializers contain no logic, they only init modules.
  • 102. Self-registered Modules
  • 103. Page load Vendor code Application modules moduleInitializer module1 module2 module3 module4 module3 HTML module4 HTML module1 HTML module2 HTML
  • 104. <div data-module="photoGallery"> <!-- gallery markup --> </div> <div data-module="wishlistMap"> <!-- map markup --> </div>
  • 105. // moduleInitializer $("[data-module]").each(function () { var module = $(this).data("module"); require("myapp.modules." + module).init(this); });
  • 106. Each module “initializes itself”.
  • 107. Action/section free, global use.
  • 108. EVENTS
  • 109. Observer Pattern
  • 110. Observable (server) “When a new episode is available, I’ll notify all pirat—I mean—clients."
  • 111. Observer (client/pirate) “Whenever the server notifies me about a new episode, I’ll download it.”
  • 112. OBSERVER OBSERVABLE
  • 113. Yo, can you give me a shout out whenever a new GoT episode comes out? Sure, my good sire!
  • 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. // 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. Mediator
  • 117. Facilitates the communication between modules.
  • 118. MEDIATOR
  • 119. All modules have no knowledge about each other, except for the Mediator.
  • 120. MEDIATOR Mediator, please tell me when a new The Walking Dead episode comes out Sure thing
  • 121. MEDIATOR Mediator, please tell me when a new Mythbusters episode comes out Groovy. Hey, I’ll want that as well! Jammin'.
  • 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. 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. // 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. Everyone knows only the Mediator.
  • 126. jQuery
  • 127. MyApp.mediator = $({});
  • 128. Mixin
  • 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. GETTING REAL
  • 131. AMD vs. CommonJS vs. Plain Objects vs. ES6
  • 132. How does your product work?
  • 133. Synchronous or asynchronous?
  • 134. Do you have a build process?
  • 135. Will you use client-side code on the server-side?
  • 136. Are you feeling lucky?
  • 137. Events vs. Promises vs. Dependency Injection vs. ES6
  • 138. ¯_(ツ)_/¯
  • 139. In the end, there’s no unique solution.
  • 140. Choose whatever makes you and your team more comfortable.
  • 141. If you do it right, JavaScript can be fun.
  • 142. THANKS!slideshare.net/eshiota github.com/eshiota @shiota

×