Padronizando código JavaScript com IIFE, AMD e RequireJS
Veja nesse artigo como padronizar e organizar seu código JavaScript usando módulos IIFE, AMD e RequireJS.
O JavaScript vem ganhando uma espantosa força nos últimos anos e, a cada dia que passa, novas APIs e frameworks MV* são criados por entusiastas e empresas envolvidas com a comunidade web, como foi o caso do Google com o AngularJS. Ele passou a ser o protagonista de grande parte das aplicações Web atuais.
Hoje, é possível conferir aplicações “web based” como o Google Drive, Facebook, Gmail, ou ainda é possível desenvolvermos aplicações nativas utilizando o combo HTML/CSS/JS para Windows e Firefox OS.
Com a popularização da colaboração coletiva entre desenvolvedores, houve um rápido crescimento nas pesquisas para melhorias dos engines JavaScript, como o V8 do Google, e em melhorias de código, melhores práticas e buscas por maior performance das aplicações JavaScript.
O ECMAScript, especificação na qual o JavaScript foi baseado, está cada vez mais sofisticado e trazendo novidades que vão de syntax sugar a melhorias críticas de performance.
O custo/tempo de desenvolvimento tende a diminuir; as aplicações web estão se tornando cada dia mais sofisticadas; o reconhecimento do JavaScript como “a linguagem da Web” faz com que os desenvolvedores falem a mesma língua.
Mas qual é a melhor maneira de escrever nosso código? Qual é o melhor padrão? Que ferramentas podemos usar como auxílio de uso desses padrões?
Linguagens dinâmicas e fracamente tipadas como o JavaScript exigem maior disciplina do desenvolvedor na hora de codificar. Diferente de linguagens estáticas e fortemente tipadas, como é o caso do Java, que precisam ser compiladas, o JavaScript pode ter seu comportamento alterado em tempo de execução. E mais ainda, em um ambiente em que inúmeros scripts podem estar sendo carregados e executados ao mesmo tempo, em concorrência.
E se um desses códigos declara um nome de variável que sobrescreve outra variável já existente? E se um plugin jQuery que está sendo executado em um laço começa a travar o browser do usuário?
Enfim, muitas dessas questões sobre concorrência e execução paralela de atividades em se tratando do universo front-end são pertinentes e neste artigo veremos como aplicar alguns destes conceitos através da construção de uma pequena aplicação de lista de tarefas, essas bem famosas no mundo web e mobile. Mas antes, vejamos alguns conceitos importantes para a construção da mesma.
Organização do código
Em muitas linguagens de programação, é possível organizar o código através de convenções, como em Java ou frameworks como o Code Igniter para PHP ou o Django para Python.
JavaScript, por outro lado, sofre da ausência de ambos os estilos de organização. Para piorar, normalmente a camada JavaScript acaba sendo uma mistura de regra de negócio, comunicação entre o servidor e a camada de apresentação, e a manipulação da tela. Isso quer dizer que fica mais difícil separar o código em categorias lógicas reaproveitáveis.
Tomando este cenário como ponto de partida é possível observar a também necessária implantação de uma organização do código para que o desenvolvedor não se perca dentro da implementação, tanto quanto fuja dos padrões básicos de qualidade. Pensando nisso, alguns padrões e soluções podem ser aplicados de forma a maximizar os efeitos da codificação em uma camada tão dinâmica quanto a camada cliente.
Namespaces
Algumas ações podem ser bastante básicas e simples, mas ao mesmo tempo ajudam em muito a desenvolver um código bom. Em um projeto com um número grande de desenvolvedores, os riscos de se sobrescrever um componente importante, ou um objeto global são grandes. Foi pensando nessas possibilidades que, há alguns anos atrás, tornou-se popular a utilização de objetos literais como namespaces para evitar o conflito das propriedades.
Essa prática ajudou muito na organização do código e separação de responsabilidades, permitindo certo padrão nos verbos do CRUD e demais funcionalidades.
Este formato ainda é uma prática bastante usada, e pode funcionar bem se toda a equipe for disciplinada e compreender a importância deste isolamento. Contudo, o uso de namespaces ainda estava no escopo global window. Ou seja, ainda que os namespaces mantivessem os nomes de variáveis e funções protegidas dentro deles, eles próprios estariam contidos no escopo global, e poderiam ser acidentalmente sobrescritos ou apagados. Nesse sentido, cabe ao desenvolvedor dedicar atenção especial ao código fonte, quando estiver lidando com esse tipo de estrutura.
Funções autoexecutáveis ou IIFE
As funções autoexecutáveis ou Immediately-Invoked Function Expressions, são funções que nascem, executam e morrem sem deixar muitos rastros para o escopo global. Elas basicamente não são diferentes das funções convencionais, com o diferencial de serem executadas imediatamente após serem lidas pelo interpretador.
Ao mesmo tempo, são muito úteis, entre outras coisas, para a criação de módulos, isolando atributos que não desejamos expor ao mundo exterior, dando visibilidade pública apenas aos componentes relevantes.
Essa prática é bem comum em linguagens orientadas a objeto, onde alguns métodos e atributos só fazem sentido no escopo privado, e o desenvolvedor externaliza apenas os métodos que deseja expor. Essa estratégia vem sido aplicada em diversos cenários, e tem se mostrado bastante eficiente em todos eles.
IIFE e os boilerplate codes
Os boilerplate codes ajudam os programadores a economizar tempo na hora de começar a codificar. De forma geral, boilerplate codes são trechos de código que acabam sendo repetidos continuamente durante o tempo de desenvolvimento da aplicação. Em outras palavras, assumindo que a adoção de determinada prática ou estilo de codificar é benéfica para todos, ter o boilerplate code “na manga” faz com que não percamos tempo ao escrever algo óbvio e podemos focar no que realmente interessa. Essa prática adere ao famoso e desejado conceito de reaproveitamento de código.
Hoje é possível encontrar diversos boilerplate codes para IIFEs, seja para o desenvolvimento de módulos, plugins jQuery ou objetos mais complexos.
AMD: Programando em módulos de maneira organizada
Chegamos ao ponto em que as IIFEs e os módulos se tornaram parte do nosso dia a dia. Definimos boas práticas de como organizar o código baseado na nossa experiência ou usando boilerplate codes populares.
O constante uso do JavaScript de uma forma mais avançada nos ensinou a utilizar e entender as chamadas assíncronas e funções de callback. Aprendemos que é uma boa prática posicionar nossos scripts no final do documento HTML, pois os browsers interrompem o carregamento da página para avaliar o conteúdo dos scripts.
O aperfeiçoamento dessas técnicas tem motivado iniciativas para melhorar ainda mais o desenvolvimento de aplicações web. Uma dessas iniciativas é o AMD: Asynchronous Module Definition.
O paradigma do AMD sugere que os módulos de uma aplicação devem ser carregados de maneira assíncrona, respeitando suas dependências e as gerenciando para que não sejam carregadas novamente caso outro módulo as solicite.
De acordo com sua especificação, uma série de métodos e regras deve ser seguida a fim de garantir o bom funcionamento da carga assíncrona e o gerenciamento das dependências entre os módulos. Ainda que não seja formalmente um padrão, o AMD tem sido largamente utilizado e tem se popularizado bastante, e é bem provável que venha a se tornar um requisito para qualquer projeto no futuro.
RequireJS: AMD na prática
Uma das implementações que melhor reflete a utilização do AMD é a API RequireJS.
Ainda que o RequireJS não siga completamente à risca as especificações do AMD, ele representa de forma bastante concisa seu funcionamento, atendendo aos seus requisitos mais importantes: carregamento assíncrono de módulos e o gerenciamento de dependências.
Ele é composto de um arquivo js bem pequeno (cerca de 15kb minificado) e é o único arquivo que deve ser incluído no documento HTML, bastando referenciar, na mesma tag script, qual será o módulo principal da sua página.
Este módulo principal informa ao RequireJS quais são os módulos necessários para que ele funcione corretamente. Cada um desses módulos pode ter suas próprias dependências de outros módulos e assim por diante.
Se você está trabalhando em um módulo gerenciado pelo RequireJS, não precisa se preocupar se suas dependências possuem outras dependências. Se estiver trabalhando com uma aplicação utilizando a forma tradicional, você precisa conhecer todas as dependências de seus módulos e adicioná-las uma a uma com tags script no documento HTML na ordem correta de acordo com as dependências.
Aplicativo de Lista de tarefas
Para demonstrarmos a eficácia do RequireJS, vamos implementar a famosa To do List. Essa aplicação simples utiliza AngularJS na camada MVC, Twitter Bootstrap para a identidade visual e um pequeno wrapper para trabalhar com IndexedDB de maneira mais simples.
No decorrer do desenvolvimento da aplicação, diversos módulos serão criados, cada qual com as suas dependências, todas gerenciadas pelo RequireJS.
A estrutura de pastas e arquivos da aplicação será igual à apresentada na Listagem 1.
Listagem 1. Estrutura de diretórios do projeto
├── css
│ ├── bootstrap.min.css
│ └── bootstrap-theme.min.css
├── fonts
├── index.html
├── js
│ ├── controllers
│ │ └── task.js
│ ├── localdb
│ │ └── db.js
│ ├── main.js
│ └── todo.js
├── lib
│ ├── angular.min.js
│ ├── angular-route.min.js
│ ├── bootstrap.min.js
│ ├── jquery.min.js
│ ├── require.js
│ └── zondb-min.js
└── partials
└── home.html
Alguns arquivos do projeto foram ocultados para que possamos nos concentrar nos diretórios mais relevantes do mesmo. O diretório lib contem todas as bibliotecas das quais o aplicativo utiliza, e o diretório js possui subdiretórios com os controllers, arquivo principal e a biblioteca de utilização do IndexedDB.
No mesmo nível do diretório js está o diretório partials, com os templates do AngularJS.
Conforme explicado anteriormente, nossa aplicação possui apenas uma importação de arquivos JS contendo a chamada para o require.js e um atributo data-main apontando para o arquivo principal da aplicação, conforme o conteúdo do index.html exibido na Listagem 2.
Listagem 2. Página HTML e o include único do RequireJS
<!doctype html>
<html>
<head>
<title>Welcome to the new project</title>
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="css/bootstrap-theme.min.css"/>
</head>
<body ng-app="Todo">
<div class="container">
<div class="row">
<div class="col-md-2"></div>
<div class="col-md-8">
<h1>Meu TODO List</h1>
<div ng-view></div>
</div>
<div class="col-md-2"></div>
</div>
</div>
<script type="text/JavaScript" data-main="js/main" src="lib/require.js"></script>
</body>
</html>
Quando o RequireJS é incluído, procurará pelo atributo data-main e tentará carregar esse arquivo e suas dependências.
É também nesse arquivo, no nosso caso, js/main.js (a extensão .js é omitida pois o RequireJS assume que todos os módulos são arquivos JavaScript), onde estão as configurações do RequireJS, tais como bibliotecas, caminhos e mapeamentos como mostrado na Listagem 3.
Listagem 3. Configuração do RequireJS no arquivo main
window.name = "NG_DEFER_BOOTSTRAP!";
requirejs.config({
baseUrl: './js',
paths: {
'bootstrap': '../lib/bootstrap.min',
'angular': '../lib/angular.min',
'angular-route': '../lib/angular-route.min',
'zondb': '../lib/zondb'
},
shim: {
'angular': {
exports: 'angular'
},
'angular-route': {
deps: ['angular']
},
'zondb': {
exports: 'zonDB'
}
}
});
require(['todo'], function(todo) {
angular.element().ready(function() {
angular.resumeBootstrap();
});
});
A função requirejs.config recebe um objeto como argumento com as configurações da aplicação, e como o RequireJS deve lidar com as dependências.
O atributo basePath diz a partir de qual diretório os módulos devem ser carregados sem a necessidade de especificar um caminho. Ainda é possível utilizar caminhos relativos “abaixo” do diretório base, porém, este deve ser um caminho de fácil acesso aos arquivos da aplicação.
O atributo paths mapeia bibliotecas que precisamos utilizar, por qual nome elas serão reconhecidas e em qual diretório elas se encontram.
É possível, por exemplo, mapear mais de uma versão de uma mesma biblioteca com nomes diferentes, já que o RequireJS irá permitir apenas um nome de módulo exclusivo dentro de seu contexto. Isso garante que não haja conflitos de nomes, evitando a perda de módulos que viriam a ser sobrescritos, e cujo problema seria muito difícil de detectar posteriormente.
O atributo shim, que em português quer dizer calço, serve para incluir bibliotecas de contexto global dentro do contexto do AMD.
JQuery, AngularJS e outros frameworks residem no contexto global window, o que vai contra o paradigma do AMD. Para resolver esse problema, criamos esses calços, que importam esses frameworks imitando o comportamento do AMD.
Após a configuração da aplicação, utilizamos a função require para executar o main.js, carregando todas as suas dependências. Esta função recebe dois argumentos: o primeiro é um array de strings opcional com a lista de módulos do qual o main.js depende, e o segundo argumento é uma função de callback que executará efetivamente o nosso programa principal.
À medida que vamos incluindo módulos no array de strings, cada módulo carregado será um argumento passado para a função de callback.
No caso de nosso main.js está carregando o módulo todo.js (lembre-se que o RequireJS assume que todos os módulos são arquivos js, e portanto não devemos explicitar a extensão na importação dos mesmos).
Vamos então dar uma olhada no módulo todo.js, conforme mostrado na Listagem 4.
Listagem 4. Módulo todo.js
define(['angular', 'controllers/task', 'angular-route'],
function(ng, Task) {
var Todo = ng.module('Todo', ['ngRoute']);
Todo.controller('Task', Task);
Todo.config(function($routeProvider, $locationProvider) {
$routeProvider.when('/', {
templateUrl: 'partials/home.html',
controller: Task
});
});
return Todo;
});
O módulo todo.js cria um módulo do AngularJS chamado Todo e o retorna ao final da função define, do RequireJS. Notem que a função define funciona da mesma maneira que a função require, porém seu intuito é exportar um módulo, enquanto que a função require tem a finalidade de executar instruções, ao invés de exportá-las.
Por se tratar de um módulo do AngularJS, ele depende da biblioteca do Angular e ngRoute, e também carrega uma controller chamada de Task.
Todas essas dependências estão sendo carregadas no primeiro argumento da função require, e os objetos relevantes (note que não especificamos uma referência para o ngRoute, pois não o utilizaremos explicitamente) são passados como argumento para a função de callback, para que este possa ser usado na definição do módulo Todo.
Tanto angular quanto angular-route são módulos que configuramos como sendo bibliotecas que nossa aplicação depende, porém o módulo task foi desenvolvido exclusivamente para a aplicação.
Repare que, por estar em outro diretório, seu caminho precisa ser detalhado na lista de módulos a partir do basePath que definimos anteriormente.
O conteúdo do arquivo controllers/task.js é exibido na Listagem 5.
Listagem 5. Módulo controllers/task.js
define(['angular', '../localdb/db'], function(ng, db) {
function Task($scope, $rootScope, $timeout) {
$scope.title = "My tasks";
$scope.taskName = null;
$scope.tasks = null;
$scope.taskId = null;
function getRowIndex(taskId) {
var i=0,
len = $scope.tasks.length;
for(i; i < len; i++) {
if($scope.tasks[i].id == taskId) {
return i;
}
}
}
function loadTasks() {
db.query('tasks', function(res) {
$scope.tasks = res;
$scope.$digest();
});
}
$scope.saveTask = function() {
var task = {name: $scope.taskName};
if($scope.taskId) {
task.id = $scope.taskId;
updateTask(task);
return;
}
db.addRow('tasks', task, function(data) {
task.id = data;
$scope.tasks.push(task);
$scope.taskName = null;
$scope.$digest();
});
};
function updateTask(task) {
db.updateRow('tasks', task, function(data) {
$scope.taskName = '';
$scope.taskId = null;
var idx = getRowIndex(data);
$scope.tasks[idx] = task;
$scope.$digest();
});
}
$scope.removeTask = function(taskId) {
$scope.tasks.forEach(function(obj, idx) {
if(obj.id == taskId) {
db.deleteRow('tasks', taskId, function() {
$scope.tasks.splice(idx, 1);
$scope.$digest();
});
}
});
};
$scope.editTask = function(taskId) {
$scope.taskId = taskId;
var task = $scope.tasks.filter(function(task, idx) {
if(task.id == taskId) return task;
})[0];
$scope.taskName = task.name;
};
loadTasks();
}
return Task;
});
Este módulo é um pouco mais extenso, pois possui a interface de comunicação com a biblioteca de manipulação de dados do IndexedDB e todos os CRUDs, bem como a cola entre os dados e a camada de apresentação através do scope.
O módulo Task depende essencialmente das bibliotecas responsáveis por essas ações, ou seja, o angular e o wrapper do IndexedDB.
Note que no decorrer da construção da aplicação, muitas dependências de módulos se repetem. Isso é resolvido internamente pelo RequireJS, ou seja, ele faz o controle de módulos que já foram carregados, evitando que sejam carregados novamente.
Se algum dos módulos a ser carregado possui outras dependências, isso também é resolvido pelo RequireJS que fará o download assíncrono e sob demanda dos arquivos somente quando for necessário.
No final, se você fizer o deploy dessa aplicação em qualquer servidor web, terá um resultado semelhante ao mostrado na Figura 1.
Figura 1. Exemplo da tela da aplicação
A aplicação basicamente se encarrega de adicionar tarefas, editá-las e removê-las, ações básicas de um CRUD, porém usando o que há de mais organizado no que se refere a JavaScript em conjunto com o RequireJS.
Otimização dos módulos
Além disso, é importante mencionar que o RequireJS conta com uma ferramenta otimizadora chamada r.js.
Essa ferramenta precisa ser baixada separadamente e depende de outras ferramentas para ser executada, como por exemplo, NodeJS.
Com ela é possível, por exemplo, unir módulos interdependentes e criar um único arquivo minificado. Por exemplo, suponha que você possua um módulo usuário, que foi separado nas camadas view, de apresentação, e model, com as ações de CRUD.
Esses dois submódulos só fazem sentido para o seu módulo principal usuário. Logo, ao serem preparados para o ambiente de produção, podem seguramente ser unidos em um único arquivo e minificados.
O r.js é capaz de realizar esse merge sem prejudicar as dependências que você criou na fase de desenvolvimento. Funciona mais ou menos como se o r.js compilasse sua aplicação, reduzindo a quantidade de arquivos e seus tamanhos, e, dependendo da forma utilizada para minificar os arquivos, pode até otimizar o seu código.
Isso constitui mais uma ferramenta à mão dos desenvolvedores que facilitariam significativamente o desenvolvimento e manutenção do código, assim como implicariam em melhorias de performance e otimização.
Muito se tem discutido e implementado sobre as funções autoexecutáveis IIFEs e o aparecimento de módulos, tanto que se propagou por todo lado, o que nos deixou mais próximos de o que poderia vir a ser um padrão de desenvolvimento.
A experiência adquirida pelos desenvolvedores e o aperfeiçoamento dos módulos e utilização das IIFEs levou à criação e expansão do AMD, que reforça a utilização de módulos que possam ser carregados de forma assíncrona, e que sejam gerenciados de maneira inteligente.
A responsabilidade de manter o código limpo e organizado pertence, obviamente, ao desenvolvedor e o AMD pode fazer isso pelo mesmo. Esse artigo mostrou através do desenvolvimento de uma pequena aplicação, que as pequenas partes podem de fato fazer uma grande diferença no final.
Links
Site
do RequireJS
http://requirejs.org/
Documentação
da API do RequireJS
http://requirejs.org/docs/api.html
AMD
(Asynchronous Module Definition)
https://github.com/amdjs/amdjs-api/wiki/AMD
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Artigo