Importação nativa de módulos no browser
A especificação ES2015 (ES6) introduziu um sistema de módulos baseado na sintaxe import
e export
, porém ela não define como o carregamento e a resolução de módulos devem ser implementados por falta de consenso entre os fabricantes de browsers. Essa situação abriu espaço para que a comunidade criasse carregadores de módulos, os famosos loaders
.
A boa notícia é que os browsers já começaram a implementar o suporte ao carregamento nativo de ESM (ECMASCRIPT modules). Neste post veremos como utilizar este recurso que esta sendo consolidado.
Você pode pular, se preferir, a explicação do problema que o sistema de módulos veio resolver e ir direto para a importação nativa de módulos indo direto para esta seção.
O problema
Temos um projeto com a seguinte estrutura:
├── index.html
└── js
└── script-c.js
└── script-b.js
├── script-a.js
O script-a.js
depende da função sumTreeMumbers
declarada no script-b.js
que por sua vez depende da função sumTwoNumbers
declarada no módulo script-c.js
. Vejamos cada um dos scripts:
script-c.js, não depende de ninguém:
const sumTwoNumbers = (number1, number2) => number1 + numer2;
script-b.js, depende de script-c.js
const sumThreeNumbers = (mumber1, number2, number3) =>
sumTwoNumber(number1, number2) + number3;
script-a.js, depende de script-b.js
const total = sumTreeNumbers(10, 20, 30);
console.log(total);
Precisaremos importar os três scripts em index.html
e obrigatoriamente seguir a ordem de dependência, caso contrário teremos um erro:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Exemplo</title>
</head>
<body>
<script src="js/script-c.js"></script>
<script src="js/script-b.js"></script>
<script src="js/script-a.js"></script>
</body>
</html>
Temos apenas três scripts e não teríamos problemas de verificar se cada script da árvore de dependências esta presente em index.html
, inclusive garantir a ordem de importação. Todavia, em um projeto mais complexo, isso pode se tornar um pesadelo. Por fim, todas as funções declaradas estão no escopo global. Faz sentido que nosso script-a.js
enxergue a função declarada em script-b.js
, mas não aquela declarada em script-c.js
.
Para resolver todos esses problemas que acabamos de listar, vamos transformar cada script em um módulo do ES2015 (ES6) e utilizar as instruções import
e export
.
Módulo e as instruções import e export
Quando trabalhamos com o sistema de módulos do ES2015 cada script por padrão é um módulo que confina tudo o que possui, em outras palavras, nenhuma variável ou função escapará para o escopo global. Apenas os artefatos explicitamente exportados com a instrução export
poderão ser importados por outros módulos através da instrução import
.
Modificando nosso código:
script-c.js:
export const sumTwoNumbers = (number1, number2) => number1 + number2;
script-b.js:
import { sumTwoNumbers } from './script-c.js';
export const sumThreeNumbers = (number1, number2, number3) =>
sumTwoNumbers(number1, number2) + number3;
script-a.js:
import { sumThreeNumbers } from './script-b.js';
const total = sumThreeNumbers(10, 20, 30);
alert(total);
Excelente, conseguimos resolver o problema do escopo global fazendo cada módulo importar explicitamente o que precisa de outro. Todavia, como faremos o carregamento desses módulos no navegador? Não podemos simplesmente importar cada módulo em separado como no exemplo anterior, vejamos:
<!-- não rola fazer isso! -->
<script src="js/script-c.js"></script>
<script src="js/script-b.js"></script>
<script src="js/script-a.js"></script>
O navegador não entenderá que cada script é na verdade um módulo e não reconhecerá a sintaxe import
, muito menos export
. Além disso, importar cada módulo que desejamos utilizar no navegador também é um problema, pois teremos que saber quais são todos os scripts que script-a.js
depende. Já imaginaram isso com uma árvore de dependência muito maior? Seríamos obrigados a ter tudo explicado em um documento, aquele que será abandonado com o tempo e que não refletirá mais a realidade do sistema.
A especificação ES2015 deixou em aberto como o carregamento dos módulos deve ser feita, abrindo espaço para que a comunidade criasse carregadores de módulos, os famosos loaders (scripts especializados no carregamento de módulos) para realizar essa tarefa. A boa notícia é que os fabricantes de browsers parecem ter chegado a um consenso e hoje podemos carregar módulos nativamente em boa parte dos navegadores.
Carregando módulos nativamente nos navegadores.
O autor testou o código a seguir no Chrome 62.0.3202.75 e no Safari Versão 11.0.1 sem problema algum. Porém, no Firefox (56.0.2) e no Edge é necessário ativar flags experimentais para que o recurso funcione.
Para carregarmos um módulo no navegador continuamos utilizando a tag <script>
, mas adicionando o atributo type="module"
. Vejamos:
<!-- único script apenas -->
<script src="js/script-a.js" type="module"></script>
Veja que além de usarmos <script type="module">
importamos apenas um único módulo, o módulo ponto de entrada da aplicação. A partir dele todas as dependências da aplicação serão resolvidas sem que o programador tenha que se preocupar com a ordem de carregamento. Mas atenção, essa importação só funcionará se estivermos servindo nossa página através de um servidor web, pois a busca dos módulos serão realizadas através do JavaScript, isto é, através de requisições Ajax. Outra ponto é que você não pode remover a extensão .js
do nome do módulo importado com a instrução import
.
Conclusão
O suporte ao carregamento nativo de módulos nos navegadores veio tornar mais acessível esse fantástico recursos para aplicações corriqueiras e que não utilizam frameworks ou sistemas de empacotamento de módulos como Webpack entre outros.