Desenvolvimento

24 out, 2017

Ecmascript Modules no Node.js e o que eu tenho a ver com isso?

Publicidade

Fala galera!

Há alguns dias saiu o Node.js 8.5.x, e dentre as adições, veio uma muito esperada: Os módulos Ecmascript nativos!

É isso mesmo, a partir da versão 8.5.x já é possível utilizar os módulos ecmascript nativamente no Node.js, obviamente ainda é experimental, para ativar basta utilizar a flag –experimental-modules como descrito na documentação oficial.

Imagino que várias questões estão vindo na cabeça de vocês agora, como: Qual a diferença entre import e require? Muda muita coisa? É verdade que a extensão tem que ser .mjs?

Começando do começo: Os módulos CommonJS

Quando o Node.js foi criado, a proposta de módulos Ecmascript ainda não existia. Os desenvolvedores envolvidos no projeto decidiram utilizar módulos CommonJS (o clássico require), que era mantido pela organização criada pelo Mozilla para padronizar o ecossistema javascript fora dos browsers. Essa organização não tem ligação com o TC39 que é responsável pelo Ecmascript.

O CommonJS deixou de ser mantido. Os responsáveis pelo Node.js e npm assumiram a especificação dos módulos, possibilitando a criação de um grande ecossistema javascript.

Com o sucesso dos módulos no ecossistema Node.js, os desenvolvedores aproveitaram para trazer os módulos para o mundo dos browsers com o Browserify e o Webpack, melhorando muito a modularização no frontend. Como resultado, o padrão de módulos CommonJS + Node/npm se espalhou por todo o ecossistema javascript, tanto no servidor, quanto no cliente, e está crescendo cada vez mais. O problema é que esse padrão não possui um comitê oficial ativo e já se tornou obsoleto.

Com os módulos Ecmascript, teremos uma nova especificação mantida pelo comitê ativo do TC39 que vai tirar proveito das últimas funcionalidades do Ecmascript para solucionar os problemas mais comuns da modularização no mundo javascript.

E esse papo de .mjs?

Sim! Todo o módulo vai ter que ter a extensão .mjs ao invés de .js. Vamos pensar no typescript, por exemplo, que utiliza a extensão .ts ou o React com jsx, que utiliza .jsx. Ambos possuem extensões customizadas, pois precisam diferir o javascript convencional e o customizado por eles. Ambos poderiam fazer uma detecção via parsing, mas isso é complexo. Basicamente é necessário parsear uma sintaxe abstrata e definir se é ou não typescript, por exemplo.

Agora imagine com módulos, a opção de descobrir o tipo de módulo via parse foi discutida, e muito. Em resumo, haveria casos onde os arquivos seriam CJS e ESM ao mesmo tempo. Import(), por exemplo, é similar tanto no CJS, quanto no ESM. Esses são alguns dos exemplos que praticamente invalidam o parsing.

Outras alternativas foram discutidas, como especificar quais arquivos são módulos ecmascript via package.json. Não preciso explicar porque foi inviabilizada né?

O que o ESM traz de novo e por que devo utilizar?

Além de ser uma especificação oficial, ele foi pensado para ser otimizado para o compilador e ter uma boa usabilidade no desenvolvimento. Algumas das principais novidades são:

Sintaxe é amigável para as tarefas do dia a dia, como: adquirir módulos, exportar módulos, requisitar alguns ou todos módulos de um arquivo.

Estático por padrão

Os módulos CommonJS possuem um formato dinâmico, tanto o que é importando quanto o que é exportado pode ser alterado em tempo de execução (runtime). Uma das principais motivações para o ES6 adicionar o próprio formato de módulos foi possibilitar um formato estático, que traz vários benefícios conforme vamos ver em detalhes a seguir.

No modelo CJS de módulos era possível fazer o seguinte:

var module;
  if (condition) {
    module = require('lib.js');
  } else {
    module = require('other_lib.js');
  }

No exemplo acima, utilizando o CJS, o require é feito com um condicional, o que significa que esse condicional vai ter que ser resolvido em tempo de execução. O mesmo pode ser feito com o exports para exportar um módulo:

if (condition) {
    exports.test = {};
}

O ESM é menos flexível nesse ponto. Só é possível importar e exportar no nível principal do arquivo e não é possível ter condicionais. Os seguinte códigos são inválidos:

var module;
  if (condition) {
    module = import lib from 'lib.js';
  } else {
    module = import lib from 'other_lib.js';
  }

O import deve ser no nível root do arquivo, como abaixo:

import lib from 'lib.js';

O resultado de forçar o desenvolvedor a modularizar de forma estática, traz benefícios como:

Eliminação de código não utilizado durante o build

Todo o módulo exportado é read-only

O objeto público não pode ser sobrescrito, mesmo que o próprio módulo altere o exports.

Bundling compacto

O Rollup trouxe o bundling onde é possível juntar pequenos pedaços de código em um arquivo só.

Como os módulos ES são estáticos e o module exports é read-only, é possível fazer o seguinte tipo de compactação de código:

// lib.js
export function foo() {}
export function bar() {}

// main.js
import {foo} from './lib.js';
console.log(foo());

O Rollup consegue transformar o código acima em algo similar ao seguinte:

function foo() {}
console.log(foo());

Lookup rápido de imports

No CJS, quando importamos um módulo recebemos sempre um objeto. Por ser dinâmico, a cada vez que acessamos uma propriedade é necessário fazer lookup para verificar se a propriedade existe ou não no módulo, essa procura é lenta. Como os módulos ES são estáticos, não é necessário fazer lookup, pois o acesso às propriedades é direto.

Checagem de variáveis

Com uma estrutura estática é fácil de saber quais variáveis estão visíveis em qualquer local dentro do módulo, poupando trabalho de ferramentas como jsHint, e esse trabalho sendo realizado pela própria engine do javascript.

Suporte a dependencias cíclicas por padrão.

O parser de módulos é totalmente separado dos scripts

Isso garante que não haverá problemas com coisas como top-level await (que está sendo considerado)

Módulos ES são sempre carregados assincronamente

O que é totalmente relacionado a maneira na qual a web funciona, e também o protocolo http2.

Exports nomeados

Compatibilidade entre Web e Node.js sem precisar de uma ferramenta de build.

Curiosidades sobre require() e ESM

  • Imports dinâmicos devem ser liberados logo, mas ainda não são suportados no ESM dado o modelo estático.
  • Não é possível fazer require() dentro de um ESM, isso porque require é síncrono e ESM é assíncrono.

Espero que esse artigo tenha respondido algumas perguntas. Agora é começar a testar os módulos no Node.js. Quem quiser utilizar sem a flag de experimental, é possível utilizar o std/esm sobre o qual já escrevi por aqui.

Você pode ler mais sobre os design goals dos módulos Ecmascript aqui.

Referências: