Módulos ES6, Node.js e a solução Michael Jackson

O JavaScript nunca teve uma maneira padrão de importar e exportar funcionalidades de um arquivo de origem para outro. Bem, tem: variáveis ​​globais. Por exemplo:

<script src="https://code.jquery.com/jquery-1.12.0.min.js"></script><script>
// `$` variable available here
</script>

Isso está longe de ser ideal por algumas razões:

  • Você pode ter conflitos com outras bibliotecas usando os mesmos nomes de variáveis. É por isso que muitas bibliotecas possuem um método noConflict () .
  • Não é possível fazer referências cíclicas corretamente . Se um módulo A depende de um módulo B e vice-versa, em qual ordem colocamos as <script>tags?
  • Mesmo que não haja referências cíclicas, a ordem na qual você coloca as <script>tags é importante e difícil de manter .

CommonJS para o resgate

Quando Node.js e outras soluções de JavaScript do lado do servidor começaram a aparecer, eles concordaram em uma maneira de corrigir esse problema. Eles criaram uma especificação mais ampla chamada CommonJS . Em relação ao problema de importação / exportação, esta especificação define uma require()função que é injetada pelo tempo de execução e uma exportsvariável para exportar a funcionalidade.

Nota: CommonJS não é a única especificação. Existem outros como UMD que, de fato, podem ser usados ​​em ambos: frontend e backend.

À medida que passava o tempo, havia uma explosão de ferramentas, especialmente para criar aplicativos de uma única página. Com bases de código maiores no frontend e a necessidade de compartilhar o código entre frontend e backend, muitas ferramentas como o browser e o webpackcomeçaram a implementar e entender a especificação CJS como uma maneira de ignorar as limitações da plataforma: a falta de um bom sistema de módulos em A plataforma subjacente (JavaScript e os navegadores).

Isso é claramente um hack porque o navegador não implementa require()ou o exportsque essas ferramentas fazem é implementar esta funcionalidade, empacotando todo o código em conjunto. Leia mais sobrecomo os pacotes de JavaScript funcionam .

Como os módulos ES6 funcionam e por que o Node.js ainda não o implementou

O JavaScript está evoluindo muito, especialmente com ES6, e esse problema teve que ser resolvido. É por isso que os módulos ES nasceram. Eles se parecem muito com o CJS de forma sintaxe.

Vamos compará-los. É assim que importamos em ambos os sistemas:

const { helloWorld } = require('./b.js') // CommonJS
import { helloWorld } from './b.js' // ES modules

É assim que exportamos a funcionalidade:

// CommonJS
exports.helloWorld = () => {
console.log('hello world')
}
// ES modules
export function helloWorld () {
console.log('hello world')
}

Muito parecido, certo?

Faz muito tempo que o Node.js implementou 99% do ECMAScript 2015 (também conhecido como ES6), mas precisamos esperar até o final de 2017 para suporte para módulos ES6 . E estará disponível apenas por trás de uma bandeira de tempo de execução! Por que demora tanto para implementar módulos ES6 no Node.js se eles são tão parecidos com o CJS?

Bem, o diabo está nos detalhes. A sintaxe é bastante semelhante entre ambos os sistemas, mas a semântica é bastante diferente . Existem também casos de bordas sutis que exigem um esforço especial para serem 100% compatíveis com as especificações.

Mesmo que os módulos ES não sejam implementados em Node.js, eles já estão implementados em alguns navegadores. Por exemplo, podemos testá-los no Safari 10.1. Vamos ver alguns exemplos e veremos por que a semântica é tão importante. Criei estes três arquivos:

// index.html
<script type="module" src="./a.js"></script>
// a.js
console.log('executing a.js')
import { helloWorld } from './b.js'
helloWorld()
// b.js
console.log('executing b.js')
export function helloWorld () {
console.log('hello world')
}

O que vemos no console quando isso é executado? Este é o resultado:

executing b.js
executing a.js
hello world

No entanto, o mesmo código usando CJS e executando-o em Node.js:

// a.js
console.log('executing a.js')
import { helloWorld } from './b.js'
helloWorld()
// b.js
console.log('executing b.js')
export function helloWorld () {
console.log('hello world')
}

Nos dará:

executing a.js
executing b.js
hello world

Então … executou o código em uma ordem diferente! Isso ocorre porque os módulos ES6 são primeiro analisados ​​(sem serem executados), então o tempo de execução procura importações, carrega-os e, finalmente, executa o código. Isso é chamado de carregamento assíncrono .

Por outro lado, o Node.js carrega as dependências (requer) sob demanda durante a execução do código . O que é muito diferente. Em muitos casos, isso pode não fazer diferença, mas em outros casos é um comportamento completamente diferente.

Node.js e navegadores da web precisam implementar esta nova maneira de carregar o código mantendo o anterior. Como eles sabem quando usar um sistema e quando o outro? Os navegadores sabem disso porque você o especifica no <script>nível, como vimos no exemplo com a typepropriedade:

<script type="module" src="./a.js"></script>

No entanto, como o Node.js conhece? Houve muita discussão sobre isso e tem havido muitas propostas (verificando primeiro a sintaxe e depois decidir se deve ou não ser tratado como um módulo, definindo-o no arquivo package.json, …) . Finalmente, a proposta aprovada foi: a solução Michael Jackson. Basicamente, se você deseja que um arquivo seja carregado como um módulo ES6, você usará uma extensão diferente: .mjs em vez de .js.

O nome da extensão (.mjs) é a razão pela qual isso às vezes é apelidado de Solução Michael Jackson.

No início, pareceu-me uma decisão muito ruim, mas agora acho que é a melhor solução, porque é fácil e qualquer ferramenta (editor de texto, IDE, pré-processador) saberá a maneira mais fácil possível se um arquivo precisar ser tratado como um ES6 ou não. E acrescenta apenas as despesas gerais mínimas possíveis ao processo de carregamento.

Se você quiser saber mais sobre o status de implementação dos módulos ES6 no Node.js, você deve ler esta atualização .

Uma nota sobre Babel

Babel implementa módulos ES6, mas … incorretamente. Não implementa as especificações completas. Portanto, cuidado com isso se você estiver usando o Babel ao mudar para uma implementação de módulos ES6 nativos, você pode ter efeitos colaterais.

Por que os módulos ES6 são bons e como obter o melhor dos dois mundos

Os módulos ES6 são excelentes por dois motivos principais:

  • Eles são um padrão de plataforma cruzada. Eles trabalharão em Node.js e navegadores da web.
  • As importações e exportações são estáticas. Tem que ser assim por causa do funcionamento do processo de carregamento. Lembre-se de que dissemos que o tempo de execução primeiro carrega o arquivo, analisa e, antes de executá-lo, ele carrega as dependências? Isso só é possível se as importações e as exportações forem estáticas. Você não pode fazer import 'engine-' + browserVersionIsso é bom por um motivo: as ferramentas podem fazer análise estática do código, descobrir qual código está realmente sendo usado e a árvore o agita . Isso é especialmente útil ao usar bibliotecas de terceiros: você nunca usa todas as funcionalidades que elas fornecem, para que você possa remover muitos bytes de código que o usuário nunca executará.

Mas, isso significa que eu não posso mais importar funcionalidade dinamicamente? Para mim, isso é muito útil. Muitas vezes faço coisas como:

Const provider = process.env.EMAIL_PROVIDER 
const emailClient = require (`./email-providers / $ {provider}`)

Desta forma, recebo uma implementação diferente com a mesma interface apenas com uma mudança de configuração, sem ter que carregar o código de todas as implementações.

Então, o que acontece com os módulos ES6? Bem, não se preocupe, há uma proposta de stage 3 (o que significa que provavelmente será aprovado em breve) que adiciona uma import()function . Esta função aceita um caminho e retorna a funcionalidade exportada como uma promessa.

Então, com os módulos ES6 e import(), obteremos o melhor dos dois mundos. 🚀

Os módulos ES6 são ótimos, mas levará algum tempo para adotá-los. Espero que toda essa informação ajude você a estar preparado para quando chegar esse momento!

Créditos
ES6 modules, Node.js and the Michael Jackson Solution, escrito originalmente por Alberto Gimeno

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.