BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos Javascript: Desenvolvendo Componentes Modulares

Javascript: Desenvolvendo Componentes Modulares

Embora a maioria das aplicações web hoje em dia utilizem uma grande quantidade de JavaScript, mantendo o foco na funcionalidade do lado do cliente, a robustez e sustentabilidade continuam sendo um grande desafio.

Ainda que os princípios básicos como separação de interesses ou DRY serem adquiridos de outras linguagens e ecossistemas, muitos desses princípios são muitas vezes ignorados quando se trata de partes do lado do navegador de uma aplicação.

Isto é em parte devido à desafiante história do JavaScript, uma linguagem que por muito tempo lutou para ser levada a sério.

Talvez o pensamento mais importante seja os distintos cliente-servidor: Embora exista numerosas elaborações nos estilos arquiteturais explicando como manipular essas distinções (por exemplo, ROCA), muitas vezes há uma falta de orientação concreta sobre como implementar esses conceitos.¹

Isso frequentemente leva a um código altamente procedural e comparativamente desestruturado aumentando o custo do front-end. Embora seja útil o JavaScript e o navegador permitirem esta abordagem direta e sem mediação incentivando e simplificando explorações iniciais e reduzindo sobrecarga, esse estilo leva rapidamente a implementações que são difíceis de manter.

Esse artigo apresentará um exemplo de evolução de um widget simples a partir de um código bastante desestruturado a um componente reutilizável.

Filtrando Contratos

A proposta deste widget de exemplo é filtrar uma lista de contatos por nome. O resultado final incluindo o histórico de evolução é fornecido no repositório GitHub. Os leitores são encorajados a rever os commits e comentar lá.

Em conformidade com os princípios da melhoria progressiva, começamos com uma estrutura básica HTML descrevendo os nossos dados, aqui usando o microformato h-card para tirar proveito de semânticas estabelecidas, o que ajuda a fornecer um contrato significativo:

<!-- index.html -->

<ul>
<li class="h-card">
<img src="http://example.org/jake.png" alt="avatar" class="u-photo">
<a href="http://jakearchibald.com" class="p-name u-url">Jake Archibald</a>
(<a href="mailto:jake@example.com" class="u-email">e-mail</a>)
</li>
<li class="h-card">
<img src="http://example.org/christian.png" alt="avatar" class="u-photo">
<a href="http://christianheilmann.com" class="p-name u-url">Christian Heilmann</a>
(<a href="mailto:christian@example.com" class="u-email">e-mail</a>)
</li>
<li class="h-card">
<img src="http://example.org/john.png" alt="avatar" class="u-photo">
<a href="http://ejohn.org" class="p-name u-url">John Resig</a>
(<a href="mailto:john@example.com" class="u-email">e-mail</a>)
</li>
<li class="h-card">
<img src="http://example.org/nicholas.png" alt="avatar" class="u-photo">
<a href="http://www.nczonline.net" class="p-name u-url">Nicholas Zakas</a>
(<a href="mailto:nicholas@example.com" class="u-email">e-mail</a>)
</li>
</ul>

Observe que não é relevante se a estrutura DOM é baseada no HTML fornecido pelo servidor ou gerado por outro componente, desde que nosso componente possa confiar neste estrutura (que essencialmente constitui uma estrutura DOM do form [{ photo, website, name, e-mail }] ) para estar presente na inicialização. Isto assegura o baixo acoplamento e evita amarrar-nos em qualquer sistema particular.

Com isso no lugar, podemos começar a implementar o nosso widget. A primeira etapa é prover um input field para o usuário informar o nome desejado. Isto não é parte com contrato do DOM, mas inteiramente a responsabilidade de nosso widget e assim injetado dinamicamente (afinal, sem o nosso widget não haveria nenhum objetivo em ter tal campo em tudo).

// main.js

var contacts = jQuery("ul.contacts");
jQuery('<input type="search" />').insertBefore(contacts);

(Estamos usando jQuery aqui meramente por conveniência e pelo fato dele ser amplamente conhecido; o mesmo princípio se aplica independente de qual seja a biblioteca de manipulação do DOM, se alguma for usada.)

O arquivo de script, juntamente com a dependência do JQuery é referenciada no final do arquivo HTML.

A seguir adicionamos a funcionalidade desejada (ocultando entradas que não correspondem ao input) para este campo criado recentemente:

// main.js

var contacts = jQuery("ul.contacts");
jQuery('<input type="search" />').insertBefore(contacts).on("keyup", onFilter);

function onFilter(ev) {
var filterField = jQuery(this);
var contacts = filterField.next();
var input = filterField.val();

var names = contacts.find("li .p-name");
names.each(function(i, node) {
var el = jQuery(node);
var name = el.text();

var match = name.indexOf(input) === 0;
var contact = el.closest(".h-card");
if(match) {
contact.show();
} else {
contact.hide();
}
});
}

(Referenciando uma função separada ao invés de definir uma função anônima interna frequentemente torna os callbacks mais gerenciáveis.)

Observe que esse manipulador de eventos depende de um ambiente DOM particular do elemento de disparo desse evento (o mapeamento para o contexto de execução se encontra aqui). A partir desse elemento percorremos o DOM para acessar a lista de contatos e encontrar todos os elementos que contêm um nome dentro (como definido nas semânticas do microformato). Quando um nome não começa com a entrada atual, escondemos o respectivo elemento do contêiner (percorrendo ascendentemente novamente), caso contrário tornamos o mesmo visível.

Testando

Já temos a funcionalidade básica que foi pedida (então é um bom momento para solidificar que escrevendo um teste2). Neste exemplo usaremos o QUnit.

Iniciamos com uma página HTML mínima como ponto de partida para nossa suíte de testes. Evidente que também precisamos referenciar nosso código com suas dependências (neste caso, jQuery), assim como na página HTML que criamos anteriormente.

<!-- test/index.html -->

<div id="qunit"></div>
<div id="qunit-fixture"></div>

<script src="jquery.js"></script>
<script src="../main.js"></script>

<script src="qunit.js"></script>

Com a infraestrutura no lugar, podemos acrescentar alguns dados de exemplo (uma lista de h-cards), por exemplo, com a mesma estrutura que iniciamos (para o elemento #qunit-fixture). Este elemento é reiniciado a cada teste, fornecendo um cenário limpo e evitando efeitos colaterais.

O primeiro teste assegura que o widget foi inicializado corretamente e que o filtro funciona como o esperado, escondendo os elementos DOM que não correspondem à entrada simulada:


// test/test_filtering.js

QUnit.module("contacts filtering", {
setup: function() { // cache dos elementos comuns no módulo de objeto
this.fixtures = jQuery("#qunit-fixture");
this.contacts = jQuery("ul.contacts", this.fixtures);
}
});

QUnit.test("filtering by initials", function() {
var filterField = jQuery("input[type=search]", this.fixtures);
QUnit.strictEqual(filterField.length, 1);

var names = extractNames(this.contacts.find("li:visible"));
QUnit.deepEqual(names, ["Jake Archibald", "Christian Heilmann",
"John Resig", "Nicholas Zakas"]);

filterField.val("J").trigger("keyup"); // simula a entrada do usuário
var names = extractNames(this.contacts.find("li:visible"));
QUnit.deepEqual(names, ["Jake Archibald", "John Resig"]);
});

function extractNames(contactNodes) {
return jQuery.map(contactNodes, function(contact) {
return jQuery(".p-name", contact).text();
});
}

(strictEqual evita a conversão de tipos no Javascript, evitando erros sutis.)

Após alterarmos nossa suíte de testes com uma referência a este arquivo de teste (a seguir a referência QUnit), abrindo a suíte no navegador deve nos informar que todos os testes passaram:

Animações

Embora nosso widget funcione razoalvemente bem, ele ainda não é muito atraente, então adicionemos algumas animações simples. O jQuery torna isso muito fácil: temos somente que substituir o show e hide pelo slideUp e slideDown, respectivamente. Isso melhora significantemente a experiência do usuário em nosso modesto exemplo.

Entretanto, ao executar novamente a suíte de testes, somos informados que o filtro não funcionou, com todos os quatro contatos ainda sendo exibidos:

Isto ocorreu devido às animações serem assíncronas (como nas operações AJAX), portanto os resultados da filtragem são verificados antes da animação ser concluída. Podemos usar o asyncTest do QUnit para adiar essa verificação:

// test/test_filtering.js

QUnit.asyncTest("filtering by initials", 3, function() { // aguarda 3 verificações
// ...
filterField.val("J").trigger("keyup"); // simula a entrada do usuário
var contacts = this.contacts;
setTimeout(function() { // adia a verificação até a animação terminar
var names = extractNames(contacts.find("li:visible"));
QUnit.deepEqual(names, ["Jake Archibald", "John Resig"]);
QUnit.start(); // retoma a execução dos testes
}, 500);
});

Uma vez que verificar a suíte de testes no navegador pode tornar-se tedioso, podemos usar o PhantomJS, um navegador sem tela, juntamente com o QUnit runner para automatizar o processo e exibir os resultados no console:

$ phantomjs runner.js test/index.html Took 545ms to run 3 tests. 3 passed, 0 failed.

Isso também faz com que seja fácil de automatizar testes através de integração contínua. (Embora, claro, não cubra problemas cross-browser já que o PhantomJS usa apenas WebKit. Entretanto, existem navegadores sem tela, o Gecko para o Firefox e o Trident para o Internet Explorer.)

Contenção

Até o momento nosso código é funcional, porém não muito elegante: para começar, ele bagunça o namespace global com duas variáveis - contacts e onFilter - uma vez que os navegadores não executam arquivos JavaScript em escopos isolados. No entanto, podemos evitar vazamentos para o escopo global. Como as funções são os únicos mecanismos de escopo no JavaScript, simplesmente criamos uma função anônima em torno de todo arquivo e em seguida a chamamos na parte inferior:

(function() {

var contacts = jQuery("ul.contacts");
jQuery('<input type="search" />').insertBefore(contacts).on("keyup", onFilter);

function onFilter(ev) {
// ...
}

}());

Isto é conhecido como invocação imediata da expressão da função, do inglês immediately invoked function expression (IIFE).

Efetivamente, agora temos variáveis privadas dentro de um módulo independente.

Podemos dar um passo adiante para garantir que não seja possível introduzir novas variáveis ​​globais por esquecer uma declaração var. Para isso ativamos o strict mode, que protege contra uma série de armadilhas comuns3:

(function() {

"use strict"; // obj: deve ser a primeira declaração dentro da função

// ...

}());

Especificando isto dentro de um encapsulador IIFE nos assegura que se aplica somente aos módulos em que foi explicitamente solicitado.

Uma vez que temos variáveis em módulos locais, podemos usar isto também para introduzir apelidos locais por conveniência, por exemplo em nossos testes:

// test/test_filtering.js

(function($) {

"use strict";

var strictEqual = QUnit.strictEqual;

// ...

var filterField = $("input[type=search]", this.fixtures);
strictEqual(filterField.length, 1);

}(jQuery));

Agora temos dois atalhos - $ e strictEqual, sendo a primeira definida através de um argumento IIFE - que são válidas apenas dentro deste módulo.

API do Widget

Embora nosso código esteja muito bem estruturado agora, o widget é automaticamente inicializado, por exemplo, sempre que o código é carregado pela primeira vez. É difícil explicar as razões e prever a (re) inicialização dinâmica, por exemplo, em elementos diferentes ou recém-criados.

Corrigir isto simplesmente requer colocar o código de inicialização existente dentro da função:

// widget.js

window.createFilterWidget = function(contactList) {
$('<input type="search" />').insertBefore(contactList).
on(
"keyup", onFilter);
};

Desta maneira desacoplamos a funcionalidade do widget de seu ciclo de vida em sua respectiva aplicação. Assim, a responsabilidade pela inicialização é deslocada para a aplicação (ou em nosso caso, a suíte de testes) o que geralmente significa um pouquinho de "código colado" para gerenciar os widgets dentro de contexto da aplicação.

Observe que estamos anexando explicitamente nossa função para a global window, o que é a maneira mais simples de tornar essa funcionalidade acessível fora de nosso IIFE. Entretanto, isso acopla as partes internas do módulo para um particular contexto implícito: window pode não ser sempre o objeto global (por exemplo, no Node.js).

Uma abordagem mais elegante é ser explicito sobre quais partes estão expostas para o exterior e agrupar as informações em um só lugar. Para isso podemos tirar vantagem de nosso IIFE mais uma vez: Uma vez que é só uma função, simplesmente devolver as partes públicas (por exemplo, nossa API na parte inferior e atribuir esse valor de retorno para uma variável no escopo externo (global)):

// widget.js

var CONTACTSFILTER = (function($) {

function createFilterWidget(contactList) {
// ...
}

// ...

return createFilterWidget;

}(jQuery));

Isto é conhecido como padrão de revelação de módulo, do inglês revealing module pattern. O uso de maiúsculas é uma convenção para destacar variáveis ​​globais.

Encapsulando o Estado

Neste ponto, o nosso widget é funcional e razoavelmente estruturado, como uma API adequada. Entretanto, introduzir funcionalidades adicionais do mesmo modo (com base puramente na combinação de funções mutuamente independentes) pode facilmente levar ao caos. Isto é particularmente relevante para componentes UI no qual o estado é um fator importante.

Em nosso exemplo, queremos deixar os usuários decidirem se o filtro deve ser case-sensitive, então adicionamos um checkbox e estendemos nosso manipulador de eventos de acordo:

// widget.js

var caseSwitch = $('<input type="checkbox" />');

// ...

function onFilter(ev) {

var filterField = $(this);
// ...
var caseSwitch = filterField.prev().find("input:checkbox");
var caseSensitive = caseSwitch.prop("checked");

if(!caseSensitive) {
input = input.toLowerCase();
}
// ... }

Isso aumenta ainda mais a dependência do contexto DOM em particular, a fim de reconectar-se a elementos do widget dentro do manipulador de eventos. Uma opção é mover essa descoberta para uma função separada que determina as partes do componente no contexto. Uma opção mais convencional é a abordagem orientada a objeto. (O JavaScript faz uso tanto da programação orientada a objetos como funcional, permitindo o desenvolvedor escolher qual estilo é melhor para uma dada tarefa.)

Assim podemos reescrever nosso widget para gerar uma instância que mantém o controle de todos os seus componentes:

// widget.js

function FilterWidget(contactList) {
this.contacts = contactList;
this.filterField = $('<input type="search" />').insertBefore(contactList);
this.caseSwitch = $('<input type="checkbox" />');
}

Isso muda ligeiramente a API, mas significantemente: Ao invés de chamarmos, createFilterWidget(...), agora inicializamos nosso widget com new FilterWidget(...) - o qual invoca o construtor, no contexto de um objeto recém-criado (this). A fim de destacar a necessidade de o novo operador, os nomes do construtor são capitalizados por convenção (bem como nomes de classes em outras linguagens).5

Claro que também precisamos migrar a funcionalidade para este novo esquema - iniciando com um método para esconder os contatos com base na entrada dada, que se assemelha a funcionalidade anteriormente encontrada em onFilter:

// widget.js

FilterWidget.prototype.filterContacts = function(value) {
var names =
this.contacts.find("li .p-name");
var self = this;
names.each(function(i, node) {
var el = $(node);
var name = el.text();
var contact = el.closest(".h-card");

var match = startsWith(name, input, self.caseSensitive);
if(match) {
contact.show();
}
else {
container.hide();
}
});
}

(Aqui self é usado para tornar this acessível dentro do escopo de cada callback, na qual tem seu próprio this e assim não consegue acessar o escopo exterior diretamente. Desde modo referenciando self para o escopo interior criamos uma closure.)

Observe que o método filterContacts, ao invés de executar a descoberta DOM em um contexto dependente, simplesmente faz referência a elementos previamente definidos no construtor. A busca de caracteres na String foi extraída para uma função separada de propósito geral - ilustrando que nem tudo precisa necessariamente se tornar um método do objeto:

function startsWith(str, value, caseSensitive) {
if(!caseSensitive) {
str = str.toLowerCase();
value = value.toLowerCase();
}
return str.indexOf(value) === 0;
}

Na sequência vamos anexar o manipular de eventos, sem a qual esta funcionalidade nunca seria acionada:

// widget.js

function FilterWidget(contactList) {
// ...
this.filterField.on("keyup", this.onFilter);
this.caseSwitch.on("change", this.onToggle);
}

FilterWidget.prototype.onFilter = function(ev) {
var input = this.filterField.val();
this.filterContacts(input);
};

FilterWidget.prototype.onToggle = function(ev) {
this.caseSensitive = this.caseSwitch.prop("checked");
};

Executando nosso testes, que, independente das pequenas alterações da API anterior, não deve requerer nenhum ajuste, vai revelar um erro aqui, como this não é o que poderíamos esperar que ele fosse. Já aprendemos que os manipuladores de eventos são chamados com o respectivo elemento DOM como contexto de execução, então precisamos trabalhar para fornecer acesso para a instância do widget. Para isso podemos tirar vantagens das closures para remapear o contexto de execução:

 

// widget.js

function FilterWidget(contactList) {
// ...
var self = this;
this.filterField.on("keyup", function(ev) {
var handler = self.onFilter;
return handler.call(self, ev);
});
}

(call é um método interno para invocar qualquer função no contexto de um objeto arbitrário, com o primeiro argumento correspondendo para this dentro da função. Uma forma alternativa seria o uso do apply combinado com a variável implícita arguments para evitar a referência explicita a argumentos internos individualmente: handler.apply(self, arguments).6)

O resultado final é um widget no qual cada função é clara, com responsabilidade bem encapsulada.

jQuery API

Quando usando jQuery, a API atual parece como algo inelegante. Podemos acrescentar um encapsulador para fornecer uma API alternativa que parece mais natural para os desenvolvedores do jQuery:

jQuery.fn.contactsFilter = function() {
this.each(function(i, node) {
new CONTACTSFILTER(node);
});
return this;
};

(Uma contemplação mais elaborada é fornecida pelo próprio guia de plugins do jQuery.)

Deste modo podemos usar jQuery("ul.contacts").contactsFilter(), mantendo isso como uma camada separada, garante que não estamos amarrados a este particular ecossistema em especial; versões futuras podem prover encapsuladores adicionais na API para diferentes ecossistemas ou mesmo decidir remover ou substituir o jQuery como dependência. (Em nosso caso, abandonar o jQuery também significaria reescrever a parte interna.)

Conclusão e perspectivas

Esperançosamente este artigo conseguiu transmitir alguns dos princípios fundamentais de escrever componentes JavaScript sustentáveis​​. Evidente que nem todo componente deve seguir este exato padrão, mas os conceitos apresentados aqui devem fornecer o kit de ferramentas essencial e necessário para qualquer componente.

Outra melhoria o uso do Asynchronous Module Definition (AMD), que aumenta o encapsulamento e torna explicita a dependência entre os módulos, permitindo assim carregar o código por demanda (como por exemplo o RequireJS).

Além disso, há novos desenvolvimentos interessantes no horizonte: A nova versão do JavaScript (oficialmente ECMAScript 6) introduzirá um sistema de módulos no nível da linguagem, embora, como com qualquer novo recurso, a ampla disponibilidade depende do suporte do navegador. Similarmente, o Web Components é um futuro conjunto de APIs do navegador destinado a melhorar o encapsulamento e facilidade de manutenção - muitos dos quais podem ser experimentados hoje usando o Polymer. Apesar de serem custosos os Componentes Web com aprimoramento progressivo continuam a ser usados.

1Isso é menos que um problema para aplicações de uma página única, como os respectivos papéis de servidor e cliente neste contexto. No entanto, uma justa posição dessas abordagens está além do escopo deste artigo.

2Provavelmente o testes poderiam ter sido escritos primeiro.

3JSLint pode adicionalmente ser usado para proteger destes e outros problemas comuns. Em nosso repositório usamos o JSLint Reporter.

4O JavaScript usa protótipos ao invés de classes - a principal diferença é que enquanto classes normalmente são "especiais" de alguma maneira, aqui qualquer objeto pode atuar como um protótipo e pode assim ser usado com um template para criar novas instâncias. Para a proposta deste artigo, a diferença é insignificante.

5Versões modernas do JavaScript introduzem o Object.create como uma alternativa para a sintaxe "pseudo clássico". Os princípios fundamentais da herança mantêm se os mesmos.

6O jQuery.proxy pode ser usado como reduzir isto para this.filterField.on("keyup", $.proxy(self, "onFilter"));

 

Sobre o autor

Frederik Dohr iniciou sua carreira como relutante web developer hackeando o TiddlyWiki, às vezes chamado de aplicação original de uma única página. Após alguns anos de trabalho com um grupo de pessoas inteligentes no Osmosoft, equipe de inovação do BT, ele deixou Londres e voltou para a Alemanha. Trabalha atualmente no innoQ, onde continua sua busca pela simplicidade enquanto ganha toda uma nova perspectiva sobre o desenvolvimento com, para e na web.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT