Upload
eduardo-shiota-yasuda
View
11.120
Download
0
Embed Size (px)
DESCRIPTION
Em aplicações de larga escala, certas interações que parecem simples aos olhos do usuário escondem a complexidade de múltiplas requisições assíncronas e updates no DOM toda vez que uma informação é modificada. Esta complexidade pode facilmente virar um inferno de callbacks, e nesta palestra mostrarei como através de uma estrutura event driven criada do zero é possível quebrar uma página em componentes modulares, desacoplados, e testáveis.
Citation preview
JAVASCRIPT MODULARE EVENT-DRIVEN
@shiota 2013
OLÁ!slideshare.net/eshiota
github.com/eshiota@shiota
front-end engineer@
* 28/08/2013 https://github.com/search?p=1&q=stars%3A%3E1&s=stars&type=Repositories
Dos 50 repositórios maispopulares do Github, 28 repositórios são relacionados a JavaScript.
JavaScript é legal!
Mas pode ser infernal também.
Ele pode ficar impossível de ser entendido ou alterado.
(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);
(um arquivo JS,uma função de 173 LOC,no mínimo 7 responsabilidades)
Ele pode virar um CALLBACK HELL(e algumas libraries facilitam ainda mais isso)
$(".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); }); }); } });});
Uma estrutura modular e baseada em eventos resolve esses problemas e muito mais.
DISCLAIMER #1Sem MV* frameworks
DISCLAIMER #2jQuery (para encurtar os exemplos)
DISCLAIMER #3Pseudo-implementações =)
MÓDULOS
Responsabilidade única, integrante de um sistema complexo.
Responsabilidades e comportamentos isolados.
Testáveis.
Extensíveis e modificáveis (até certo ponto).
Podem ser substituídos, ou reutilizados.
Nesta apresentaçãosingle entry points
namespacesmodule pattern
constructorsModule.js
mediator & pub/submixins
Single entry points
(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);
Page load
jQuery load
jQuery plugins
application.js
Sem pontos de entrada, é difícil segmentar a execução por página ou rota.
Pontos únicos de entrada controlam o flow da aplicação.
Page load
Vendor code
Application modules
application.js
dispatcher.js
beforeCommand controllerCommand actionCommand afterCommand
Page load
Vendor code
Application modules
application.js
dispatcher.js
beforeCommand controllerCommand actionCommand afterCommand
O dispatcher executa funções baseadas no controller e na action da página.
<body data-dispatcher="<%= dispatcher_label %>">
def dispatcher_label action_name = controller.action_name action_name.gsub!(/_/, "")
"#{controller_name}##{action_name}"end
<body data-dispatcher="products#show">
dispatcher.js
beforeCommand()
productsControllerCommand()
productsShowCommand()
afterCommand()
products#show
Os commands não contêm lógica, apenas inicializam outros módulos.
Namespaces
ns("MYAPP.commands.productsShowCommand", function () { console.log("Execute code from products#show page");});
// Same as:
window.MYAPP = { commands : { productsShowCommand : function () { console.log("Execute code from products#show page"); } }};
Ajuda na organização dos módulos.
/javascripts /myapp /commands /productsShowCommand.js
Não polui o contexto global.
Module pattern
ns("EDEN.modules.checkoutModule", (function () {
// Stores the module's main element var element;
// Private methods // ---------------
// Prints a silly phrase var printSillyPhrase = function () { console.log("I love sushi"); console.log(this); };
return {
// Public methods // --------------
// Inits the module init : function (el) { element = $(el);
printSillyPhrase.call(this); }
};})());
Isola um comportamento e disponibiliza uma API pública.
É "Singleton-like".
Constructors & Prototypes
ns("EDEN.forms.AddressForm", function (el) { this.element = $(el); this.init();});
$.extend(EDEN.forms.AddressForm.prototype, {
// Public methods // --------------
// Inits the instance init : function () { // Do something }
});
var shippingAddressForm = new EDEN.forms.AddressForm($("#shipping-address"));
Permite múltiplas instâncias e heranças por prototype.
Module.js
* github.com/fnando/module
Define namespaces e coloca açúcar sintático na definição de funções construtoras.
ns("EDEN.forms.AddressForm", function (el) { this.element = $(el); this.init();});
$.extend(EDEN.forms.AddressForm.prototype, {
// Public methods // --------------
// Inits the instance init : function () { // Do something }
});
var shippingAddressForm = new EDEN.forms.AddressForm($("#shipping-address"));
Module("EDEN.forms.AddressForm", function (AddressForm) {
AddressForm.fn.initialize = function (el) { this.element = $(el);
// Do something }
});
var shippingAddressForm = Module.run("EDEN.forms.AddressForm", $("#shipping-address"));
Padroniza a criação de novos módulos.
EVENTOS
Pub/Sub
PUB (server)"Quando sair o torrent do último episódio, vou avisar pra galera."
SUB (cliente piratinha)"Quando o server avisar que tem um episódio, vou começar a baixar."
SUB PUB
Yo, me avisa quando sair o eppy de Game of
Thrones? \o/
Blz é nóish véi
AE SAIU O"THE RAINS OF
CASTAMERE" PRA BAIXAR, GALERE!
Massa, vou baixar!Acho que vai ser um
episódio feliz =D
// Pirate subscribes to torrent servertorrentServer.on("new-got-episode", function (name) { this.download(name);});
// Torrent server publishes that it has a new GoT episodethis.trigger("new-got-episode", "The Rains of Castamere");
Mediator
Interface central de comunicação entre módulos.
MEDIATOR
Nenhum módulo tem conhecimento do outro.
MEDIATOR
Mediator, me avisa quando sair o novo do The
Walking Dead?
Blz
MEDIATOR
Mediator, me avisa quando sair o novo do
Mythbusters?
É nóish.
MEDIATOR
Mediator, saiu um eppy novo de The Walking
Dead.
Subscribers, saiu um eppy novo de The
Walking Dead!
Ae, vou baixar!
MEDIATOR
Mediator, saiu um eppy novo de Mythbusters.
Subscribers, saiu um eppy novo de Mythbusters!
Ae, vou baixar!
// Pirate 1 subscribes to mediatormediator.on("new-twd-episode", function (name) { console.log("Downloading The Walking Dead - " + name);});
// Pirate 2 subscribes to mediatormediator.on("new-mythbusters-episode", function (name) { console.log("Downloading Mythbusters - " + name);});
// Torrent server 1 publishes on mediatormediator.trigger("new-twd-episode", "The Suicide King");
// Torrent server 2 publishes on mediatormediator.trigger("new-mythbusters-episode", "Food Fables");
Todos conhecem apenas o Mediator.
Mixin
Module("EDEN.forms.AddressForm", function (AddressForm) {
// AddressForm now has the `on`, `off` and `trigger` methods $.extend(AddressForm.fn, EDEN.events);
AddressForm.fn.initialize = function (el) { this.element = $(el); }
});
MUNDO REAL
Modularizando
Exemplos
#1Mudança no endereço de entrega muda o endereço de cobrança
// Code inside ShippingAddressForm
_attachEvents : function () { this.element.find(".main-address-input").on("keyup paste cut change", this._onAddressModification.bind(this)); },
_onAddressModification : function (event) { EDEN.mediator.trigger("shipping-address-change", event.target.value);}
// Code inside BillingAddressSelector
_registerInterests : function () { EDEN.mediator.on("shipping-address-change", this.updateAddressInfo, this);}
updateAddressInfo : function (address) { this.element.find(".address-info").text(address);}
#2Mudança no CEP de entrega muda frete, total e parcelas
// Code inside ShippingAddressForm
_registerInterests : function () { this.element.find(".cep-input").on("keyup paste cut", this._onCepModification.bind(this)); },
_onCepModification : function (event) { if (this.isCepFilled()) { EDEN.mediator.trigger("shipping-cep-change", event.target.value); } else { EDEN.mediator.trigger("shipping-cep-incomplete", event.target.value); }}
// Code inside checkoutModule
_registerIntests : function () { EDEN.mediator.on("shipping-cep-change", this._onShippingCepChange, this); this.shippingService.on("get-success", this._onShippingGetSuccess, this);},
_onShippingCepChange : function (cep) { this.shippingService.get(cep);}
_onShippingGetSuccess : function (data) { EDEN.mediator.trigger("shipping-rate-change", data.rate); EDEN.mediator.trigger("delivery-estimate-change", data.estimate);}
// Code inside purchaseInfo
_registerInterests : function () { EDEN.mediator.on("shipping-rate-change", this._onShippingRateChange, this); EDEN.mediator.on("delivery-estimate-change", this._onDeliveryEstimateChange, this);},
_onShippingRateChange : function (rate) { this.updateShippingRate(rate);},
_onDeliveryEstimateChange : function (days) { this.updateDeliveryEstimate(days);},
updateShippingRate : function (rate) { var formatter = EDEN.currency.formatter;
this.element.find(".shipping-rate").text(formatter(rate)); this.shippingRate = rate;
this.updateTotal();},
updateTotal : function () { var total = this.subtotal + this.shippingRate, formatter = EDEN.currency.formatter;
this.element.find(".total").text(formatter(total));
EDEN.mediator.trigger("cart-total-change", total);}
// Code inside installmentSelector
_registerInterests : function () { EDEN.mediator.on("cart-total-change", this._onCartTotalChange, this);},
_onCartTotalChange : function (total) { this.updateInstallments(total);},
updateInstallments : function (total) { // Updates the values}
LIVE DEMO
futuro.
Sempre há espaço para melhoras.
Framework MV*?
Framework próprio?
Melhoria nas padronizações?
Sistema de eventos mais robusto?
É por isso que JavaScript é legal. =)
THANKSslideshare.net/eshiota
github.com/eshiota@shiota