RequireJS - Carregando JavaScript sob demanda

RequireJS

Quando você tem uma aplicação onde vai usar muito código Javascript, é sempre importante pensar na performance. Mas além disso, para que sua aplicação possa escalar, seu código precisa ser modular.

Nesse momento surgem as dúvidas:
- Escrevo todo o meu código em um arquivo só, e minifico para não sobrecarregar os requests do browser?
- Separo em vários arquivos para facilitar o desenvolvimento, sem me preocupar com a quantidade de requests http?

Qual a melhor solução?

A resposta correta é: depende do seu projeto. Você vai precisar medir isso e ver o que é mais viável no seu caso. O problema que temos hoje é que, quando você adiciona vários scripts no seu app, ao baixar um arquivo css ou js, o browser bloqueia a renderização do navegador até que o arquivo seja baixado e parseado. Feito isso, a renderização continua, e faz o mesmo ao encontrar o próximo script.

Isso deixa sua aplicação muito lenta, pois cada request pode levar em torno de 20ms a 120ms, dependendo do tamanho do arquivo, para executar esse processo. Se saber mais sobre performance, e porque isso acontece, leia este artigo..

RequireJS

Mas ao carregarmos nossos scripts assincronamente, o browser não bloqueia a renderização, mas faz o download e parse dos arquivos em segundo plano, disponibilizando-os assim que estiverem prontos.

E o RequireJS nos ajuda exatamente com isso: usando uma estrutura modular, baseada no AMD, você usa Dependency Injection, para que os scripts necessários sejam carregados de forma assincrona, sob demanda, ou seja, você só vai carregar um arquivo quando realmente precisar dele!

A configuração inicial do RequireJS é um pouco chata, mas vou mostrar o passo-a-passo para que você possa utilizar ele em seus projetos sem maiores problemas :D

Obtendo o RequireJS

Você pode baixar o RequireJS à partir desse link. Pode baixar a versão minificada aqui.

Com o Require em mãos, vamos criar a seguinte estrutura de diretórios:

1
2
3
4
5
6
7
8
9
10
11
.
├── index.html
└── js
   ├── App.js
   ├── boot.js
   ├── controllers
   │   └── HomeController.js
   └── vendor
   ├── jquery.js
   ├── lodash.js
   └── require.js

No diretório js/vendor/ é onde iremos centralizar libs de terceiros. Baixe o jQuery e a LoDash, e adicione nesse diretório.

Na index.html:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<title>RequireJS</title>
</head>
<body>

<script data-main="js/boot" src="js/vendor/require.js"></script>
</body>
</html>

Se você reparar na linha 9, onde adicionamos o script do RequireJS, temos um atributo data-main. Esse atributo diz ao Require que, quando ele for baixado, esse é o arquivo que deve ser executado: js/boot.js. Então é nesse arquivo que nós iremos fazer nossas configurações iniciais.

O Require faz a adição do .js no final do arquivo, então sempre chame-o sem a extensão.

Se você abrir a aba Network no seu navegador, com a index.html do nosso projeto aberta, pode constatar que os dois arquivos foram baixados corretamente:

boot.js

Agora no js/boot.js, vamos iniciar as configurações:

1
2
3
4
5
6
7
;(function( undefined ) {
'use strict';

require([ 'vendor/jquery' ], function() {
console.log( 'jQuery carregado!', $( document ) );
});
})();

O RequireJS tem 3 funções:

  • require() / requirejs() - Injeta um arquivo;
  • define() - Cria um componente no padrão AMD.

Para fazer a injeção de dependências, você pode usar tanto o require() quanto o requirejs(), que fazem exatamente a mesma coisa.

E pra quê duas funções que fazem a mesma coisa?

Porque você pode estar usando alguma outra lib que use a função require(). Nesse caso, o RequireJS não sobrescreve essa função, mas te dá a opção de usar requirejs() no lugar, mantendo tudo sob controle :)

A sintaxe das funções require() e requirejs() é a seguinte:

1
require( deps, callback );

E a sintaxe da função define():

1
define( 'moduleName', deps, callback );

A única diferença das funções require() e requirejs() para a função define(), é que na define() você pode - opcionalmente - adicionar o nome do seu módulo.

E como saber quando usar define e quando usar require ou requirejs?

A regra basicamente é essa:

  • define será usado sempre no início de novos arquivos - levando em consideração que cada módulo será um arquivo;
  • require ou requirejs, se você precisar injetar uma dependência no corpo de um módulo já existente.

Ainda confuso? Quando estivermos criando nossos módulos, você vai entender melhor :)

Entendendo os parâmetros

O parâmetro moduleName, na função define(), é o nome do seu módulo.

No parâmetro deps, você vai passar um array de dependências. Coloque o caminho completo dos arquivos, tomando como base o caminho do arquivo chamado no atributo data-main do seu script. No nosso caso, o js/boot.js. E não é necessário adicionar o .js no final do arquivo.

Nota: ainda que você vá utilizar somente uma dependência, é necessário utilizar a notação de array. Se não houver dependências (quando utilizar o define()), esse parâmetro é opcional. Nesse caso, passe somente a função de callback. E se você não tiver dependências e precisar retornar, por exemplo, somente um objeto, você pode passar ele diretamente para a função define(), sem precisar instanciar a função de callback:

1
2
3
4
define({
color: 'black',
size: 'large'
});

Quer dizer que só no require() e requirejs() as dependências são obrigatórias?

Se não há a necessidade de injetar dependências, então você não precisará do require ;)

E o parâmetro callback é uma função que será executada assim que todas as dependências forem resolvidas (ou um objeto, como mostrado acima).

require.config()

As funções require e requirejs - a partir daqui vou parar de citar as duas. Quando eu me referir a require, estou falando das duas, ok? ;) - têm um método config(), onde você pode configurar algumas coisas para facilitar a injeção de dependências. Vamos adicionar essas configurações no nosso js/boot.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;(function( undefined ) {
'use strict';

require.config({
baseUrl: './js',
paths: {
jquery: [
'https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min',
'vendor/jquery'
],
lodash: 'vendor/lodash'
}
});

require([ 'jquery', 'lodash' ], function( $, _ ) {
console.log( 'jQuery carregado!', $( document ) );
});
})();

Primeiro vou explicar o parâmetro na função de callback do require().

Parâmetros do callback

Quando uma dependência é injetada, o retorno dessa dependência pode ser obtido a partir dos parâmetros na função de callback. Os parâmetros do callback devem ser passados na mesma ordem das dependências injetadas.

No nosso primeiro exemplo, no início do artigo, fizemos a injeção do jQuery, mas não passamos parâmetro algum no callback. O código funciona corretamente porque o jQuery passa para o objeto window os objetos jQuery e $. Logo, você pode usar esses objetos globais. Mas, por questões de boas práticas e performance, é melhor você usar sempre objetos e variáveis locais. Nesse caso, no último exemplo, passamos como parâmetro para a função de callback o $, que recebe o jquery injetado, para que possamos usar $ localmente.

Injetamos também a lodash, e passamos como parâmetro no callback o _, para ser usado também localmente.

Agora sim, o método config()

Como já havia dito, para fazer a injeção de dependências, o RequireJS toma como base o caminho do arquivo passado no parâmetro data-main do nosso script. Mas nós podemos passar um parâmetro baseUrl para setar manualmente esse caminho. A partir daí, todos os caminhos de scripts injetados tomam essa URL como base. Você pode passar tanto caminhos absolutos como relativos.

Para usar caminhos relativos com o baseUrl, a base será o caminho do HTML onde está inserido o <script> que chama o RequireJS.

A propriedade paths é um objeto, que recebe como chave uma referência para o módulo que será injetado, e como valor, a URL desse módulo. Isso facilita na hora da injeção, para que você não precise passar todo o caminho da dependência.

Se você voltar no primeiro exemplo, vai ver que injetamos o jQuery com vendor/jquery. Nesse segundo exemplo, usamos somente jquery, pois no paths já setamos o caminho completo.

Você deve ter percebido também que podemos usar um array como valor da dependência. O que o RequireJS faz nesse caso é tentar carregar os arquivos na ordem que está no array. Se a primeira URL falhar, ele tenta carregar a segunda, e assim por diante. No caso do jQuery, a vantagem é poder usar um CDN e, se esse método falhar, chamamos a lib localmente, em vendor/jquery.

A ideia de usar o RequreJS é basicamente essa. Mas para facilitar a visualização, vamos escrever um pouco mais de código, pois ainda não vimos a função define() em ação.

No nosso arquivo js/App.js coloque o seguinte código:

1
2
3
define([ 'jquery' ], function( $ ) {
console.log( 'Carregou App.js' );
});

E no arquivo js/boot.js:

1
2
3
4
5
6
7
8
9
10
11
;(function( undefined ) {
'use strict';

require.config({
// ...
});

require([ 'jquery', 'lodash' ], function( $, _ ) {
require([ 'App' ]);
});
})();

Perceba que podemos aninhar os métodos require para garantir que, ao carregar js/App.js, o jquery e a lodash já estejam carregados!

No arquivo js/App.js, nós usamos o define para definir esse módulo. Porque não utilizamos o require?

Por que esse módulo ainda não existe, e está sendo definido. Se eu precisar injetar dependências dentro do módulo, que dependam das dependências passadas no array do primeiro parâmetro, aí sim eu vou usar o require(). Ficou mais claro o uso deles agora?

Hey, mas você passou o jquery como dependência do módulo App. Isso não vai fazer o RequireJS trabalhar dobrado e baixar duas vezes o jQuery?

Não. Em cada módulo você deve injetar todas as dependências que você precisa usar internamente nele, ou que você precisa que estejam carregadas para usá-lo. O RequireJS se encarrega de - se essa dependência já tiver sido carregada antes - usar o arquivo que foi carregado da primeira vez.

Nesse nosso exemplo, o arquivo js/boot.js vai fazer o boot do RequireJS, e o js/App.js vai controlar toda a nossa aplicação.

Dentro do js/App.js, você pode escrever qualquer código Javascript para chamar os seus scripts sob demanda, somente quando necessário.

Vou deixar nesse repositório um exemplo de workflow com RequireJS, tomando como base os exemplos passados nesse artigo, mas com algumas coisas a mais, para que você possa se basear nele para montar seu próprio workflow, dependendo do seu projeto, ok?

Ficou com dúvidas? Poste nos comentários!

Até o próximo artigo!

Sobre o #1postperday: https://blog.da2k.com.br/2014/12/31/um-post-por-dia/

Tem alguma sugestão para os próximos posts do #1postperday? Deixe ela aqui: https://github.com/fdaciuk/fdaciuk.github.io/issues/1