Processo de Build e Deploy Automatizado para Aplicações Javascript

Publicado por Pedro Vitti e Luciano Marcelino no dia dev

Cada vez que um dos nossos clientes precisava integrar um formulário do seu site com o RD Station, era necessário um trabalho manual que levava tempo e esforço do time para ser realizado. Isso ocorria devido à grande quantidade de tecnologias que podem ser utilizadas para a criação de formulários de contato, por exemplo, HTML, PHP, Rails, ferramentas como Wordpress e seus plugins.

Para agilizar esse processo e facilitar a vida dos clientes, decidimos implementar uma biblioteca Javascript que permitisse que o próprio cliente fizesse essa integração de forma rápida e fácil, independente da tecnologia utilizada no formulário.

Nesse post iremos descrever como automatizamos todo o fluxo de desenvolvimento dessa biblioteca utilizando o Grunt, CircleCI e Karma/Jasmine.

Grunt

grunt logo Ao longo dos últimos anos, houve um considerável aumento da velocidade e do ritmo de crescimento do desenvolvimento com Javascript. Diversas novas bibliotecas surgiram, como React e Backbone, e novos frameworks, como Angular. Essa propagação da linguagem traz, não apenas melhorias na experiência dos usuários finais, mas também a exigência de uma mudança na forma do desenvolvimento das aplicações.

Ao desenvolver uma aplicação em Javascript, provavelmente você irá precisar executar tarefas repetidas vezes. Desde verificar a qualidade e consistência do seu código e executar testes até enviar sua aplicação para uma CDN. Toda essa repetição vai contra os príncipio de desenvolvimento ágil, tornando o trabalho repetitivo e demorado. Para solucionar esse problema, encontramos o Grunt.

O Grunt é uma aplicação open-source escrita em Javascript e desenvolvida para ajudar os desenvolvedores a automatizar suas tarefas. Pode ser entendido como uma alternativa Javascript para o Rake. A automatização de tarefas nos faz economizar tempo e diminui as chances que temos de cometer erros ao executar tarefas repetidas e tediosas.

O Grunt e seus plugins são instalados e gerenciados pelo Node Package Manager (npm). Após instalar o npm e o Grunt, é necessário criar os arquivos package.json e Gruntfile.js.

package.json

Esse arquivo nos permite manter e instalar todas as dependências da aplicação. Dessa forma, é possível trabalhar de forma colaborativa e garantir que os ambientes de desenvolvimento estarão sempre sincronizados.

Ele pode ser criado através do terminal executando:

1
npm init

É possível, também, adicionar a dependência de um plugin específico diretamente ao instalá-lo. Para isso basta passar para o npm o valor --save-dev como parâmetro. Para adicionar o grunt como dependência do projeto, utilizamos o comando:

1
npm install grunt --save-dev

Este é o arquivo package.json gerado:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "name": "rd-js-integration",
  "version": "0.1.0",
  "description": "rdstation-js-integration ========================",
  "main": "rd-js-integration.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/ResultadosDigitais/rdstation-js-integration.git"
  },
  "homepage": "https://github.com/ResultadosDigitais/rdstation-js-integration",
  "devDependencies": {
    "grunt": "^0.4.5",
  }
}

Gruntfile.js

No Gruntfile.js é onde registramos e configuramos as tasks criadas e o plugins instalados. Esse arquivo deve ser criado manualmente e adicionado a pasta raiz do projeto. Um arquivo padrão do Gruntfile pode ser encontrado aqui.

Tarefas Automatizadas

Utilizamos o Grunt para realizar as seguintes tarefas que serão descritas a seguir.

Análise de Código (JSHint)

O JSHint é uma ferramenta open-source que examina arquivos JavaScript em busca de erros, más práticas e possíveis problemas no código. Indentação mal feita, variáveis não utilizadas, espaços em branco, ausência de ponto e vírgula, entre outros são detectados. Dessa forma, o JSHint nos ajuda a manter um código mais limpo, estruturado e de fácil manutenção.

Felizmente já existem alguns plugin que adicionam a task do JSHint ao Grunt. Utilizamos o plugin grunt-contrib-jshint.

Instalando:

1
npm install grunt-contrib-jshint --save-dev

Carregando o plugin no Gruntfile:

1
grunt.loadNpmTasks('grunt-contrib-jshint');

O plugin é bastante flexível e permite diversas configurações que podem ser feitas diretamente no Gruntfile. Dessa forma é possível definir quais os arquivos serão validados, variáveis e funções que não precisarão estar declaradas, como variáveis globais, ou variáveis definidas em outros arquivos (jQuery, console, document).

Para executar a task, basta digitar no console grunt jshint.

1
2
3
4
5
6
7
8
9
10
jshint: {
  files: ['gruntfile.js', 'app/<%= pkg.name %>.js', 'test/*.js'],
  options: {
   globals: {
      jQuery: true,
      console: true,
      module: true
    }
  }
}

Minificação (Uglify)

A minificação em Javascript é o processo de remoção de todos os caracteres desnecessários e que não serão utilizados do código fonte. Espaços em branco extras, comentários e quebras de linhas são removidos, por exemplo. Esse processo tem esse nome pois acaba gerando um código mínimo que mantém o mesmo comportamento sem comprometer a funcionalidade do seu código.

O principal objetivo da minificação é reduzir o tamanho dos arquivos, diminuindo a quantidade de dados e com isso acelerar o processo de transferência do código Javascript do servidor para a browser do usuário. Isso permite um aumento de performance, melhor desempenho do site, além de uma diminuição do consumo de banda.

Os arquivos minificados são criados separadamente do arquivo original. Dessa forma, a edição dos arquivos é feita sempre no arquivo original e a cada alteração definitiva, minificamos o arquivo novamente, que será disponibilizado para o cliente.

Para criar a nossa task de minificação, utilizamos o plugin grunt-contrib-uglify.

Instalando:

1
npm install grunt-contrib-uglify --save-dev

Carregando o plugin no Gruntfile:

1
grunt.loadNpmTasks('grunt-contrib-uglify');

Através da task criada no Gruntfile, podemos configurar quais arquivos devem ser minificados e onde devem ser salvos. Além disso, definimos uma mensagem com o nome do projeto e a data de minificação do arquivo.

Para executar a task, basta digitar no console grunt uglify.

1
2
3
4
5
6
7
8
9
uglify: {
  options: {
    banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
  },
  build: {
    src: 'app/<%= pkg.name %>.js',
    dest: 'app/<%= pkg.name %>.min.js'
  }
}

Execução de Testes (Karma/Jasmine)

O Karma é uma ferramenta open-source para execução de testes. Com ele é possível executar e obter um rápido feedback para seus testes criados a partir de várias suítes de teste para Javascript, como Jasmine, Mocha, QUnit. O Karma é altamente configurável e com ele possível rodar os seus testes usando navegadores reais de sua preferência. Além disso, possui integração com diversas ferramentas e um excelente suporte a plugins.

Os testes foram escritos utilizando o Jasmine. O Jasmine é framework para desenvolvimento de testes baseado em comportamento (Behaviour-Driven Development) e possui uma sintaxe parecida com o RSpec, com o qual já estamos familiarizados.

De acordo com o site oficial do Karma:

O principal objetivo do Karma é trazer um ambiente de testes produtivo para os desenvolvedores. Aquele ambiente onde não são necessárias diversas configurações, mas sim um lugar onde os desenvolvedores possam apenas desenvolver o seu código e obter um feedback instantâneo. Pois, um feedback rápido é o que nos torna produtivo e criativo.

Utilizamos o plugin grunt-karma.

Instalando:

1
npm install grunt-karma --save-dev

Carregando o plugin no Gruntfile:

1
grunt.loadNpmTasks('grunt-karma');

Ao executar uma tarefa que utilize esse plugin pela primeira vez, será solicitado que você instale e crie as dependências do Jasmine. Basta seguir os comandos que foram sugeridos.

1
2
3
4
5
karma: {
  unit: {
    configFile: 'test/karma.conf.js'
  }
}

Todas as configurações de teste são feitas no arquivo karma.conf.js.

Integração Contínua (CircleCI)

Segundo Martin Fowler:

“Integração Contínua é uma prática de desenvolvimento de software onde os membros de uma equipe integram seu trabalho frequentemente, geralmente pelo menos uma vez por dia. Cada integração é verificada por um build automatizado (incluindo testes que são fundamentais) para detectar erros e devolver um feedback o mais rápido possível. Essa abordagem reduz significativamente problemas com integração e permite que a equipe desenvolva software mais coeso de forma mais rápida.”

Utilizamos o CircleCI para o processo de integração. O escolhemos devido a sua facilidade de configuração e integração com ferramentas que utilizamos no nosso fluxo de desenvolvimento diário como Github e Slack.

O processo de integração segue o fluxo descrito abaixo:

Ao enviar um novo código para o Github, o CircleCI roda através do Grunt e de forma incremental as tarefas de análise de código, testes e, por final, a minificação dos arquivos. Caso algumas dessas tarefas falhe, o CircleCI (integrado ao Github) nos informa com uma mensagem no Commit. Através da ferramenta conseguimos avaliar quais os motivos da falha, e definir quais ações devem ser tomadas, tais como refatorar o código, reverter uma alteração ou corrigir a implementaçao.

Para configurar o CircleCI, é necessário permitir o acesso ao repositório no Github e criar um arquivo chamado circle.yml na raiz do seu projeto. Nesse arquivo são descritas as tarefas que o CircleCI deve realizar a cada nova alteração no repositório.

circle.yml:

1
2
3
4
5
6
7
8
9
10
machine:
  node:
    version: 0.10.26
test:
  pre:
    - grunt jshint
  override:
    - grunt karma
  post:
    - grunt uglify

Deploy

A cada nova versão da biblioteca, precisamos diponibilizá-la para o usuário de alguma forma. Decidimos utilizar o Amazon Cloudfront como CDN para este fim.

Existem diversas vantagens em hospedar seu arquivo em uma CDN. Algumas delas são:

  • Download dos arquivos podem ser feitos em paralelo, já que se encontram em domínios diferentes.
  • Diminuição no consumo do tráfego do seu servidor.
  • Estatísticas de download dos seus arquivos.
  • Facilidade para controlar versões.
  • Alta performance.

Utilizamos o plugin grunt-aws-s3 para automatizar essa tarefa.

Instalando:

1
npm install grunt-aws-s3 --save-dev

Carregando o plugin no Gruntfile:

1
grunt.loadNpmTasks('grunt-aws-s3');

O plugin permite diversas configurações que podem ser vistas na própria documentação. Para o nosso caso, configuramos a tarefa definindo as credenciais de acesso ao servidor, e informando quais arquivos devem ser enviados, e em qual repositório (Bucket) da Amazon eles devem ser hospedados.

Você deve ter percebido que não adicionamos essa tarefa para execução automática no CircleCI. Adotamos essa postura por questões de segurança, já que para realizar esse processo são necessárias informações sigilosas de dados de acesso ao servidor.

Uma vez que o repositório da aplicação é público, não é seguro mantermos nele informações de dados de acesso. Para resolver esse problema e tornar o projeto mais flexível, criamos o arquivo aws_credentials.

Nele configuramos as chaves de acesso AWSAccessKeyId, AWSAccessKeyKey, o Bucket utilizado no Cloudfront. Além disso, quaisquer outros detalhes referentes ao deploy podem ser configurados nesse arquivo. Assim, podemos manter esse arquivo localmente e configurar o grunt para utilizá-lo quando executar a task da seguinte forma:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
aws_s3: {
  options: {
    differential: true,
    accessKeyId: '<%= aws.AWSAccessKeyId %>',
    secretAccessKey: '<%= aws.AWSSecretKey %>',
    progress: 'dots',
    region: 'sa-east-1'
  },
  production: {
    options: {
      bucket: '<%= aws.bucket %>'
    },
    files: [
      { action: 'upload',
        expand: true,
        cwd: 'app/',
        src: ['<%= pkg.name %>.min.js'],
        dest: '<%= aws.destination %>/<%= pkg.version %>/'
      }
    ]
  },
}

Conclusão

Aqui na Resultados Digitais estamos sempre tentando evoluir o nosso processo de desenvolvimento a fim de torná-lo mais eficiente, ágil e simples. Abstrair tarefas manuais e repetitivas dos desenvolvedores, assim como obter um rápido feedback de erros nos ajuda bastante nessa evolução.

Todas os arquivos mencionados nesse post podem ser encontrados no repositório do projeto rdstation-js-integration no Github.

Contribuições, idéias e sugestões são sempre muito bem-vindas!

Pedro Vitti

Pedro Vitti

Full Stack Developer

Luciano Marcelino

Luciano Marcelino

Tech Leader

Comentários