Wednesday, February 27, 2019

Módulos JavaScript no servidor e navegador



Aplicações web, mais especialmente as que são de maior porte, necessitam de bibliotecas javascript e de outros recursos como folhas de estilo (Bootstrap é um exemplo típico) e outras páginas html ou de texto plano, para funcionarem a contento. Alguns desses recursos devem ser carregados sequencialmente à medida que a página vem sendo carregada, daí colocarmos elementos como <script> e <link> (esse último para folhas de estilo), mas muitos desses programas só serão necessários bem adiante. Alguns podem ser carregados em background, e em paralelo, vários deles ao mesmo tempo. Isso acelera consideravelmente a apresentação da página web para o cliente, que aguarda ansiosamente.  Além de usar os elementos acima (<script> e <link>), podemos efetuar chamadas XHR, ou seja, XMLHttpRequest, ou usando o padrão mais moderno do Fetch API. O problema nos elementos já mencionados é que o navegador precisa carregá-los sequencialmente, pois ele não tem idéia das dependências contidas nos scripts. Se soubéssemos que não há dependência entre os scripts, poderíamos fazer a carga em paralelo, daí a terminologia “Asynchronous Module Definition” ou AMD, que é um dos padrões para a definição de módulos cuja dependência é explicitamente identificada e que pode ser carregado sob demanda pelo navegador. Isso é explorado por carregadores de módulos como RequireJS (http://requirejs.org/) e muitos outros que visam a otimização da carga de módulos no navegador.



O formato AMD (Asynchronous Module Definition, em inglês) é uma tentativa de fornecer uma solução para módulos javascript que pode ser usada atualmente por desenvolvedores web do lado do cliente (navegador). Ele veio da experiência real do grupo do Dojo (https://dojotoolkit.org/) em modularizar seu código. O desenvolvimento desse padrão foi passado para o grupo AMDjs (https://github.com/amdjs). Ele é uma proposta para a definição de módulos onde tanto os módulos como suas dependências (em relação a outros módulos) podem ser carregados de forma independente, assincronamente.  AMD representa também um passo certo na direção do sistema de módulos proposto para a padronização futura nos padrões do ECMAscript (javascript). Apesar de ter sido inicialmente definido como um formato para o CommonJS, ele é um padrão separado deste último.
Para criar módulos, usamos alguns padrões possíveis no javascript.  O primeiro destes é o “immediately invoked function expression (IIFE)”, ou seja, uma expressão que define uma função e permite ela ser invocada imediatamente.  Conseguimos isso colocando a definição da função (anônima) entre parênteses. Se não fosse assim, a definição não seria uma expressão e a tentativa de executá-la de imediato resultaria num erro.

(function(){
 // definição da função
})()


Os dois parênteses ( ) no final são responsáveis pela invocação imediata da função.  Poderíamos incluir no interior destes, argumentos a serem passados para a função.
Esse padrão (IIFE) nos permite encapsular código no interior da função, de modo que não precisamos conhecer detalhadamente o que a função IIFE faz, mas podemos usar esse código externamente. Ademais, como o interior de uma função é uma closure, podemos criar variáveis no seu interior, sem poluir o espaço global. Podemos ter múltiplas definições de funções inclusive, retornando um objeto com as várias funções que queremos exportar, todas invisíveis de outra maneira, por estarem numa closure. Isso nos conduz ao segundo padrão útil, o “revealing module pattern”.

var globModulo = function(){
 function ola(){
   console.log('Olá, módulo!');
 }
 return {
   ola: ola
 }
}()

Após salvar o retorno em globModulo, podemos executar globModulo.ola( ) para invocar a função definida internamente. Obviamente, várias outras funções poderiam ser definidas no interior desse bloco. Observe que a definição da função não está entre parênteses como acima. Isso não é necessário porque a função não é a primeira declaração na linha, que começa por uma atribuição a uma variável. Nesse caso, a inclusão da definição da função entre parênteses não é necessária e ela pode ser invocada imediatamente.
Os formatos mais comuns para módulos são o  CommonJS, o AMD e o UMD, além do novo formato padronizado pelo ES6.

Módulos CommonJS

Os módulos padronizados pelo CommonJS são definidos como arquivos js regulares, incluindo a atribuição de exports ou module.exports no seu conteúdo. O sistema de módulos  então adicionará um wrapper que conterá essas variáveis como parâmetro, além de algumas outras, como veremos.

Um exemplo de módulo, usando a implementação do commonjs contida no NodeJS, é a seguinte:
// saudacoes.js
exports.bomdia = function() {
 return "Bom dia!";
};
exports.boanoite = function() {
 return "Boa noite!";
};
No programa principal, ou em outro módulo qualquer que precise utilizar esse módulo “saudacoes”, teremos:
var saudacoes = require("./saudacoes.js");
Ou simplesmente,
var saudacoes = require("saudacoes");
E após essa definição, podemos usar as funções saudacoes.bomdia() e saudacoes.boanoite(). Entretanto, nessa segunda forma (sem conter um caminho para um arquivo js diretamente, mas o nome do módulo, precisaremos de um arquivo package.json, que pode ser criado pelo comando “npm init”, num subdiretório do “node_modules”.
Todas as funções e variáveis criadas no módulo e não exportadas serão completamente invisíveis, graças ao wrapper introduzido.
(function(exports, require, module, __filename, __dirname) {
  module.exports = exports = {};
// o conteúdo do arquivo com o módulo reside aqui
});
Para nossa conveniência, __filename e __dirname são respectivamente o nome do arquivo fonte e o diretório onde ele se encontra.  A variável ‘module’ é um objeto com alguns ítens como “id”, “exports”, “parent”, “paths”, “children” (array com outros módulos). Ele tem referências circulares, o que o impede de ser visualizado por JSON.stringify(). Nesse caso, podemos dispor do módulo “json-stringify-safe” para essa função.

Módulos AMD

No navegador é mais interessante definirmos módulos AMD (assíncronos). Uma vantagem explícita desse tipo de módulos é que a carga destes pode ser feita independentemente, sem nos preocuparmos com a ordem de carga dos módulos. A definição de um módulo AMD é realizada num simples função:

define(id?, dependencias?, factory);
onde o id do módulo é uma string opcional, bem como é opcional o array de dependências de outros módulos. O parâmetro “factory” é uma função que efetivamente cria o módulo, como por exemplo, se o módulo fosse jQuery, retornaria a tão familiar função “$”.
Vejamos um exemplo com a página html, um módulo (amdmod.js), um script inicial (main.js), usando o carregador RequireJS (http://requirejs.org/) feito especialment para carregar módulos AMD no navegador.

index.html
-------------
<!DOCTYPE html>
<html>
   <head>
       <title>Uma pagina html</title>
       <!-- o atributo data-main attribute indica a require.js
            para carregar o script main.js depois que require.js
            é carregado -->
       <script data-main="main" src="require.js"></script>
   </head>
   <body>
       <h1>Texto do header h1</h1>
   </body>
</html>

amdmod.js
--------------
define(['jquery'] , function ($) {
   return function () {
       console.log("FACTORY em amdmod.js");
       // usa jquery para obter o texto do elemento h1
       console.log($("h1").text());
   };
});

main.js
---------
requirejs(["amdmod"], function(amdfun) {
   console.log("LOADED amdmod.js");
   amdfun();
});

Módulos UMD

Como desenvolvedor, nos deparamos frequentemente com a possibilidade de usar
um determinado módulo no ambiente do navegador, e tambḿe no servidor (nodejs).
Os métodos de inclusão funcionam de maneira diferente nos dois casos. Quando você inclui
um módulo ou biblioteca usando um tag <script>, usualmente você estará criando variáveis
globais que outros scripts poderão acessar, porém uma das vantagens de se usar RequireJS
(ou outros caregadores de módulos) é que ele elimina a necessidade de termos variáveis
globais. Como podemos ter a mesma biblioteca carregada de ambas as maneiras?
A resposta pode ser encontrada em módulos UMD (Universal Moule Definition). Praticamente,
o procedimento consiste em encapsular o código em uma espécie de boilerplate que
tenta definir os tipos de módulos existentes na biblioteca e no sistema, escolhendo o que
dá certo.
(function (root, factory) {
   if (typeof define === 'function' && define.amd) {
       // foi encontrada uma função define e esta parece ser AMD,
       // então crie um módulo AMD anônimo
       define(['b'], factory);
   } else if (typeof exports === 'object') {
       // parece ser nodejs (tipo commonjs)
       module.exports = factory(require('b'));
   } else {
       // nada suportado, defina como um global (root == window)
       root.returnExports = factory(root.b);
   }
}(this, function (b) {
   // use ‘b’ da forma apropriada
   // então retorne o que deve ser exportao pelo módulo
   // Pode ser um objeto ou simlesmente uma função.
   return {};
}));