Go to English Blog

Só mais um pouco sobre JavaScript Modular

Leia em 6 minutos

Uma coisa que sempre me perguntam sobre minha abordagem de definir namespaces é como faço para garantir que tudo irá funcionar. É preciso carregar tudo em uma ordem específica? E o que acontece se eu tiver mais de cem arquivos?

Toda vez que falo sobre usar namespaces alguém diz que é inviável e que é praticamente impossível garantir a ordem das dependências. Ou que o reuso de partes de um projeto fica comprometido. Será?

Em um dos projetos que estou trabalhando agora tenho pouco mais de setenta arquivos.

$ find . -name '*.js' | wc -l
72

Alguns desses arquivos foram extraídos de outros projetos sem ter que modificar uma linha de código sequer, justamente por serem modulares. E neste mesmo projeto estou usando exatamente a mesma abordagem de namespace.

A verdade é que para escrever JavaScript modular você precisa algumas coisas em mente, independente de usar namespaces ou AMD.

Não manipule o DOM global

Em vez de simplesmente referenciar elementos globais que existem no DOM, passe-os como dependência para os módulos que você está criando. Isso tornará sua vida infinitamente mais simples, já que você sabe exatamente qual elemento deve manipular, sem ter que se preocupar com classes e ids. Além disso, isso facilitará sua vida na hora de escrever testes.

Não faça coisas fora de seu namespace

Evite modificar outros módulos ou definir variáveis globais para guardar estado. Se precisar se comunicar com outros módulos, defina uma API que deve ser usada; caso ela mude, você terá, quase sempre, um único ponto de mudança.

Gerenciamento de dependências, AMD e namespaces

Existem maneiras diferentes de gerenciar as dependências de um projeto. Muitas pessoas irão argumentar que AMD é a solução, enquanto outros vão mostrar que nem tudo é lindo. E isso é bom!

Visões diferentes sobre um mesmo problema permitem criar soluções cada vez melhores. O importante é usar o que funciona melhor para você.

Já tentei usar AMD e não gostei. Um dos benefícios de bibliotecas como require.js é carregar suas dependências sob demanda (embora seja muito comum empacotar os módulos na hora do deploy). Como normalmente não tenho projetos absurdamente grandes, concatenar e minificar os arquivos já é suficiente.

Outro “benefício” que o AMD proporciona é a modularidade e, para falar a verdade, isso é uma das coisas que mais me incomoda. Modularidade não é uma característica exclusiva do AMD.

Se você trabalha com módulos que são definidos através de namespace, nada muda sobre ser modular ou não. Apenas que suas dependências serão definidas de modo explícito.

Ser modular significa que você irá escrever código que pode facilmente ser reutilizado. Significa que cada unidade terá uma responsabilidade bem definida e, de novo, isso não é exclusividade do AMD (e se fosse, significaria que todo o código JavaScript escrito até hoje nunca foi modular, o que não é verdade).

AMD tem os seus problemas, como o ruído gerado pela definição das dependências. Você pode configurar seu editor para gerar isso para você, mas o ruído ainda estará lá. Muitas bibliotecas não possuem suporte para AMD e você precisará configurar isso ou usar forks que adicionam o wrapper.

Namespaces também tem problemas. Você pode ter conflitos de namespace em projetos grandes com muitos desenvolvedores. Dependendo de suas influências de Java, pode acabar com namespaces bastante grandes, do tipo MyApp.Controllers.SomeController.SomeAction.

Não existe solução perfeita. Na prática, o que vale mesmo é pesar muito bem o que é melhor para o seu projeto. Teste diferentes abordagens e escolha a que se adeque melhor ao seu workflow.

Poucas responsabilidades

Escreva módulos especializados. Em vez de ter módulos “gordos”, que fazem mais do que deveriam, prefira quebrar as funcionalidades em módulos menores. Isso fará com que seu projeto tenha muitos arquivos, mas é melhor ter 100 arquivos de 100 linhas que um único arquivo com dez mil linhas. Lembre-se que você sempre pode otimizar o modo como estes arquivos serão servidos na hora do deploy.

Arquivos menores tem a tendência de serem mais simples de entender. Mas tome cuidado para não cair na armadilha de criar módulos que não fazem sentido existirem sozinhos. Infelizmente, somente a experiência e bom senso irão te ajudar aqui.

Escreva testes automatizados

Infelizmente, nem todos sabem como escrever testes. Não que seja difícil. Acredito ser mais uma questão de não saber como fazer isso. Saiba que temos boas alternativas para JavaScript. É mais uma questão de como começar a fazer isso (pretendo fazer alguns artigos sobre esse assunto).

Uma coisa que muitas pessoas acham é que o objetivo dos testes é evitar a introdução de bugs. Eu também pensei isso por muito tempo, mas agora acredito que este seja o objetivo principal.

A grande vantagem dos testes, na verdade, é dar a segurança para evoluir o seu software. São os testes que irão garantir que quando você modificar alguma funcionalidade existente, as coisas continuarão funcionando como você espera. Ou, que ao atualizar alguma biblioteca que seja dependência de seu projeto, nada parou de funcionar (inclusive a própria dependência que você atualizou).

Iniciando a aplicação

Não execute códigos automaticamente à partir dos módulos de sua aplicação. Tenha sempre um ponto central de inicialização, que pode variar de acordo com a aplicação. É isso que garantirá que seu aplicativo funcione sem ter uma ordem determinada.

Ou quase. Se você usa jQuery e seus plugins, precisa carregar o jQuery antes, já que os plugins irão depender dele. Isso pode valer para outras bibliotecas.

Você pode criar um módulo de aplicação que é algo como MyApp.Application(). Esse módulo será responsável por iniciar toda a aplicação e seu funcionamento pode variar.

Quando estou no Rails, é muito comum ter JavaScript que deve ser executado de acordo com o controller e action. Você pode, por exemplo, adicionar essas informações ao seu HTML.

<!DOCTYPE html>
<html
  lang="<%= I18n.locale %>"
  dir="ltr"
  data-controller="<%= controller.controller_name.camelize %>"
  data-action="<%= controller.action_name.camelize %>"
  >
  <head>
    <title>MyApp</title>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

Uma definição de aplicação pode, então, iniciar o controller/action.

Module("MyApp.Application", function(Application){
  Application.fn.initialize = function(controller, action) {
    Module.run(
        ["MyApp", controller + "Controller", action].join(".") // build the namespace
      , [document.body] // pass the body as the container
    );
  };
});

Finalmente, a execução dessa aplicação pode ser simplesmente um arquivo que faz muito pouco (e que normalmente chamo de boot.js).

$(function(){
  var html = $("html")
    , controller = html.data("controller")
    , action = html.data("action")
  ;

  MyApp.Application(controller, action);
});

Novamente, a abordagem que uso depende muito do tipo de aplicativo que estou trabalhando. Mas uma coisa é certa: sempre tenho um ponto único de inicialização da aplicação.

Colocando em produção

Se você seguiu uma abordagem modular deve estar com muitos arquivos para executar em seu projeto. E você provavelmente já deve ter ouvido falar que quanto menos requisições você fizer, melhor. Mas não se preocupe com isso, pois existem maneiras muito simples para resolver este problema.

Para quem usa Rails a vida é bastante simples. É só usar o asset pipeline e pronto! Na hora do deploy todo o código é concatenado e minificado.

//= require vendor/jquery
//= require_tree ./vendor
//= require_tree ./myapp

Nem todos podem contar com essa funcionalidade. Eu mesmo faço coisas que não envolvem o Rails e, nesses casos, uso o Grunt.js, um projeto que visa automatizar a execução de processos através da linha de comando.

Não irei mostrar como configurar o Grunt, mas veja como é o arquivo de configuração de um dos projetos que tenho:

module.exports = function(grunt) {
  var config = {};

  // Concat ================================
  config.concat = {
    options: {
      separator: ";"
    },
    dist: {
      src: [
          "public/javascripts/libs/jquery.js"
        , "public/javascripts/libs/**/*.js"
        , "public/javascripts/myapp/**/*.js"
      ],
      dest: "public/javascripts/myapp.js"
    }
  };

  // Minification ================================
  config.uglify = {
    options: {
      report: "gzip"
    },

    dist: {
      files: {
        "public/javascripts/myapp.min.js": ["public/javascripts/myapp.js"]
      }
    }
  };

  // JSHint =====================================
  config.jshint = {};
  config.jshint.dist = {
    options: {
      laxcomma: true
    },
    files: {
      dist: [
        "public/javascripts/myapp/**/*.js"
      ]
    }
  };

  grunt.initConfig(config);
  grunt.loadNpmTasks("grunt-contrib-concat");
  grunt.loadNpmTasks("grunt-contrib-uglify");
  grunt.loadNpmTasks("grunt-contrib-jshint");

  grunt.registerTask("default", ["concat", "jshint", "uglify"]);
};

Antes de fazer o deploy basta executar o comando grunt e todos os arquivos passarão pelo JSHint, serão concatenados e minificados. Perceba que o passo de concatenação poderia ser omitido, já que a minificação também faz isso, mas eu gosto de deixá-lo no projeto para fazer debugging (quando necessário).

Uma coisa que ouço com certa frequência é que carregar JavaScript é lento (do ponto de vista de parsing, não de tempo de requisição). Em um teste que fiz recentemente tive alguns resultados bastante interessantes. Ao carregar 1.7MB em um único arquivo com aproximadamente 55 mil linhas diria que esses tempos simplesmente não importam muito, mesmo em dispositivos como o iPhone.

--------------------------------------------
| Browser            |   Full |   Minified |
--------------------------------------------
| Safari 6           |   90ms |       69ms |
| iOS - iPhone 4     |  655ms |      475ms |
| Chrome 27          |   91ms |       60ms |
| Chrome Canary      |   94ms |       63ms |
| Firefox 21         |  178ms |      125ms |
| Firefox Aurora     |  135ms |      107ms |
| IE8                |  282ms |      156ms |
| IE9                |  402ms |      113ms |
| IE10               |  140ms |       70ms |
| Opera 12           |   80ms |       62ms |
| Opera Next         |   98ms |       62ms |
--------------------------------------------

Finalizando

Modularidade nada tem a ver com bibliotecas. É mais uma questão de boas práticas. E muitas dessas boas práticas são universais e servem para todas as linguagens que você souber.

Não existe bala de prata. Toda solução irá depender de uma série de fatores. Analize bem o que se adequa melhor ao seu workflow e divirta-se (ou ship it)!