Conteúdo deste artigo
- Como o ESM trocou flexibilidade por “analisabilidade”
- Por que os módulos são uma decisão arquitetônica
- A regra de dependência para arquitetura limpa
- O que o gráfico do seu módulo significa arquitetonicamente
- O problema do arquivo barril
- O problema do acoplamento
- Os limites do módulo são limites da equipe
- Conclusão
- Leitura adicional
Escrever programas grandes em JavaScript sem módulos seria muito difícil. Imagine que você só tem o escopo global para trabalhar. Esta era a situação no JavaScript antes dos módulos. Os scripts anexados ao DOM eram propensos a sobrescrever uns aos outros e a conflitos de nomes de variáveis.
Com módulos JavaScript, você tem a capacidade de criar escopos privados para seu código e também declarar explicitamente quais partes de seu código devem ser acessíveis globalmente.
Módulos JavaScript não são apenas uma forma de dividir código entre arquivos, mas principalmente uma forma de projetar limites entre partes do seu sistema.
Por trás de cada tecnologia deve haver um guia para seu uso. Embora os módulos JavaScript facilitem a escrita de programas “grandes”, se não houver princípios ou sistemas para usá-los, as coisas podem facilmente se tornar difíceis de manter.
Como o ESM trocou flexibilidade por “analisabilidade”
Os dois sistemas de módulos em JavaScript são CommonJS (CJS) e Módulos ECMAScript (ESM).
O sistema de módulos CommonJS foi o primeiro sistema de módulos JavaScript. Ele foi criado para ser compatível com JavaScript do lado do servidor e, como tal, sua sintaxe (require(), module.exports, etc.) não era suportada nativamente pelos navegadores.
O mecanismo de importação para CommonJS depende da função require() e, sendo uma função, não está restrito a ser chamado no topo de um módulo; ele também pode ser chamado em uma instrução if ou até mesmo em um loop.
// CommonJS — require() is a function call, can appear anywhere
const module = require('./module')
// this is valid CommonJS — the dependency is conditional and unknowable until runtime
if (process.env.NODE_ENV === 'production') {
const logger = require('./productionLogger')
}
// the path itself can be dynamic — no static tool can resolve this
const plugin = require(`./plugins/${pluginName}`)
O mesmo não pode ser dito do ESM: a instrução import deve estar no topo. Qualquer outra coisa é considerada uma sintaxe inválida.
// ESM — import is a declaration, not a function call
import { formatDate } from './formatters'
// invalid ESM — imports must be at the top level, not conditional
if (process.env.NODE_ENV === 'production') {
import { logger } from './productionLogger' // SyntaxError
}
// the path must be a static string — no dynamic resolution
import { plugin } from `./plugins/${pluginName}` // SyntaxError: : template literals are dynamic paths
Você pode ver que o CommonJS oferece mais flexibilidade do que o ESM. Mas se o ESM foi criado depois do CommonJS, por que essa flexibilidade também não foi implementada no ESM e como isso afeta o seu código?
A resposta se resume à análise estática e ao tremor de árvores. Com CommonJS, as ferramentas estáticas não podem determinar quais módulos são necessários para a execução do seu programa, a fim de remover aqueles que não são necessários. E quando um empacotador não tem certeza se um módulo é necessário ou não, ele o inclui por padrão. Da forma como o CommonJS é definido, módulos que dependem uns dos outros só podem ser conhecidos em tempo de execução.
O ESM foi projetado para corrigir isso. Ao garantir que a posição das instruções de importação esteja restrita à parte superior do arquivo e que os caminhos sejam literais de string estática, as ferramentas estáticas podem entender melhor a estrutura das dependências no código e eliminar os módulos que não são necessários, o que, por sua vez, diminui o tamanho dos pacotes.
Por que os módulos são uma decisão arquitetônica
Esteja você ciente disso ou não, toda vez que você cria, importa ou exporta módulos, você está moldando a estrutura do seu aplicativo. Isso ocorre porque os módulos são os blocos básicos de construção de uma arquitetura de projeto, e a interação entre esses módulos é o que torna um aplicativo funcional e útil.
A organização dos módulos define limites, molda o fluxo de suas dependências e até reflete a estrutura organizacional da sua equipe. A maneira como você gerencia os módulos em seu projeto pode fazer ou quebrar seu projeto.
A regra de dependência para arquitetura limpa
Existem muitas maneiras de estruturar um projeto e não existe um método único para organizar cada projeto.
Arquitetura limpa é uma metodologia controversa e nem toda equipe deveria adotá-la. Pode até ser excesso de engenharia , especialmente projetos menores. No entanto, se você não tiver uma opção estrita para estruturar um projeto, a abordagem de arquitetura limpa pode ser um bom ponto de partida.
De acordo com regra de dependência de Robert Martin :
“Nada em um círculo interno pode saber alguma coisa sobre algo em um círculo externo.”
Robert C. Martin
Com base nesta regra, uma aplicação deve ser estruturada em diferentes camadas, onde a lógica de negócio é o núcleo da aplicação e as tecnologias para construção da aplicação estão posicionadas na camada mais externa. Os adaptadores de interface e as regras de negócios ficam no meio.

No diagrama, o primeiro bloco representa o círculo externo e o último bloco representa o círculo interno. As setas mostram qual camada depende da outra, e a direção das dependências flui em direção ao círculo interno. Isso significa que a estrutura e os drivers podem depender dos adaptadores de interface, e os adaptadores de interface podem depender da camada de casos de uso, e a camada de casos de uso pode depender das entidades. As dependências devem apontar para dentro e não para fora.
Portanto, com base nesta regra, a camada de lógica de negócios não deve saber absolutamente nada sobre as tecnologias usadas na construção do aplicativo — o que é bom porque as tecnologias são mais voláteis que a lógica de negócios, e você não quer que sua lógica de negócios seja afetada toda vez que precisar atualizar sua pilha de tecnologia. Você deve construir seu projeto em torno de sua lógica de negócios e não em torno de sua pilha de tecnologia.
Sem uma regra adequada, você provavelmente estará importando módulos livremente de qualquer lugar do seu projeto e, conforme seu projeto cresce, fica cada vez mais difícil fazer alterações. Eventualmente, você terá que refatorar seu código para manter seu projeto adequadamente no futuro.
O que o gráfico do seu módulo significa arquitetonicamente
Uma ferramenta que pode ajudá-lo a manter uma boa arquitetura do projeto é o gráfico do módulo. Um gráfico de módulo é um tipo de fluxo de dependência que mostra como diferentes módulos em um projeto dependem uns dos outros. Cada vez que você faz importações, você está moldando o gráfico de dependências do seu projeto.
Um gráfico de dependência saudável poderia ser assim:

No gráfico, você pode ver as dependências fluindo em uma direção (seguindo a regra de dependência), onde os módulos de alto nível dependem dos de baixo nível, e nunca o contrário.
Por outro lado, esta é a aparência de um insalubre:

No gráfico acima, você pode ver que // CommonJS — require() is a function call, can appear anywhere
const module = require('./module')
// this is valid CommonJS — the dependency is conditional and unknowable until runtime
if (process.env.NODE_ENV === 'production') {
const logger = require('./productionLogger')
}
// the path itself can be dynamic — no static tool can resolve this
const plugin = require(`./plugins/${pluginName}`) não é mais uma dependência de // ESM — import is a declaration, not a function call
import { formatDate } from './formatters'
// invalid ESM — imports must be at the top level, not conditional
if (process.env.NODE_ENV === 'production') {
import { logger } from './productionLogger' // SyntaxError
}
// the path must be a static string — no dynamic resolution
import { plugin } from `./plugins/${pluginName}` // SyntaxError: : template literals are dynamic paths e utils.js como encontraríamos em um gráfico saudável, mas também depende de response.js e application.js. Este nível de dependência de // CommonJS — require() is a function call, can appear anywhere
const module = require('./module')
// this is valid CommonJS — the dependency is conditional and unknowable until runtime
if (process.env.NODE_ENV === 'production') {
const logger = require('./productionLogger')
}
// the path itself can be dynamic — no static tool can resolve this
const plugin = require(`./plugins/${pluginName}`) aumenta o raio de explosão se algo der errado com ele. E também dificulta a execução de testes no módulo.
Ainda outro problema que podemos apontar com // CommonJS — require() is a function call, can appear anywhere
const module = require('./module')
// this is valid CommonJS — the dependency is conditional and unknowable until runtime
if (process.env.NODE_ENV === 'production') {
const logger = require('./productionLogger')
}
// the path itself can be dynamic — no static tool can resolve this
const plugin = require(`./plugins/${pluginName}`) é como isso depende de response.js isso vai contra o fluxo ideal para dependências. Os módulos de alto nível devem depender dos de baixo nível, e nunca o contrário.
Então, como podemos resolver esses problemas? O primeiro passo é identificar o que está causando o problema. Todos os problemas com // CommonJS — require() is a function call, can appear anywhere
const module = require('./module')
// this is valid CommonJS — the dependency is conditional and unknowable until runtime
if (process.env.NODE_ENV === 'production') {
const logger = require('./productionLogger')
}
// the path itself can be dynamic — no static tool can resolve this
const plugin = require(`./plugins/${pluginName}`) estão relacionados ao fato de que ele está fazendo muito. É aí que o Princípio de Responsabilidade Única entra em ação. Usando este princípio, // CommonJS — require() is a function call, can appear anywhere
const module = require('./module')
// this is valid CommonJS — the dependency is conditional and unknowable until runtime
if (process.env.NODE_ENV === 'production') {
const logger = require('./productionLogger')
}
// the path itself can be dynamic — no static tool can resolve this
const plugin = require(`./plugins/${pluginName}`) pode ser inspecionado para identificar tudo o que faz, então cada funcionalidade coesa identificada em // CommonJS — require() is a function call, can appear anywhere
const module = require('./module')
// this is valid CommonJS — the dependency is conditional and unknowable until runtime
if (process.env.NODE_ENV === 'production') {
const logger = require('./productionLogger')
}
// the path itself can be dynamic — no static tool can resolve this
const plugin = require(`./plugins/${pluginName}`) pode ser extraída em seu próprio módulo focado. Dessa forma, não teremos tantos módulos dependentes de // CommonJS — require() is a function call, can appear anywhere
const module = require('./module')
// this is valid CommonJS — the dependency is conditional and unknowable until runtime
if (process.env.NODE_ENV === 'production') {
const logger = require('./productionLogger')
}
// the path itself can be dynamic — no static tool can resolve this
const plugin = require(`./plugins/${pluginName}`), levando a uma aplicação mais estável.
Passando de // CommonJS — require() is a function call, can appear anywhere
const module = require('./module')
// this is valid CommonJS — the dependency is conditional and unknowable until runtime
if (process.env.NODE_ENV === 'production') {
const logger = require('./productionLogger')
}
// the path itself can be dynamic — no static tool can resolve this
const plugin = require(`./plugins/${pluginName}`), podemos ver no gráfico que agora existem duas dependências circulares:
-
utils.js→utils.js→application.js→utils.js -
// ESM — import is a declaration, not a function call→
import { formatDate } from './formatters'// invalid ESM — imports must be at the top level, not conditional
if (process.env.NODE_ENV === 'production') {
import { logger } from './productionLogger' // SyntaxError
}// the path must be a static string — no dynamic resolution
import { plugin } from `./plugins/${pluginName}` // SyntaxError: : template literals are dynamic paths// CommonJS — require() is a function call, can appear anywhere→
const module = require('./module')// this is valid CommonJS — the dependency is conditional and unknowable until runtime
if (process.env.NODE_ENV === 'production') {
const logger = require('./productionLogger')
}// the path itself can be dynamic — no static tool can resolve this
const plugin = require(`./plugins/${pluginName}`)application.js→// ESM — import is a declaration, not a function call
import { formatDate } from './formatters'// invalid ESM — imports must be at the top level, not conditional
if (process.env.NODE_ENV === 'production') {
import { logger } from './productionLogger' // SyntaxError
}// the path must be a static string — no dynamic resolution
import { plugin } from `./plugins/${pluginName}` // SyntaxError: : template literals are dynamic paths
Dependências circulares ocorrem quando dois ou mais módulos dependem direta ou indiretamente um do outro. Isso é ruim porque dificulta a reutilização de um módulo, e qualquer alteração feita em um módulo na dependência circular provavelmente afetará o restante dos módulos.
Por exemplo, na primeira dependência circular (utils.js → utils.js → application.js → utils.js), se application.js quebrar, utils.js também irá quebrar porque depende de application.js – e utils.js também irá quebrar porque depende de utils.js.
Você pode começar a verificar e gerenciar os gráficos do seu módulo com ferramentas como Madge e Dependency Cruiser . Madge permite visualizar dependências de módulos, enquanto Dependency Cruiser vai além, permitindo definir regras sobre quais camadas do seu aplicativo podem importar de quais outras camadas.
Compreender o gráfico do módulo pode ajudá-lo a otimizar os tempos de construção e corrigir problemas de arquitetura, como dependência circular e alto acoplamento.
O problema do arquivo barril
Uma maneira comum de usar o sistema de módulos JavaScript é por meio de arquivos barril. Um arquivo barril é um arquivo (geralmente denominado algo como express.js/application.js) que reexporta componentes de outros arquivos. Os arquivos Barrel fornecem uma maneira mais limpa de lidar com as importações e exportações de um projeto.
Suponha que temos os seguintes arquivos:
// auth/login.ts
export function login(email: string, password: string) {
return `Logging in ${email}`;
}
// auth/register.ts
export function register(email: string, password: string) {
return `Registering ${email}`;
}
Sem arquivos barril, a importação fica assim:
// somewhere else in the app
import { login } from '@/features/auth/login';
import { register } from '@/features/auth/register';
Observe como quanto mais módulos precisarmos em um arquivo, mais linhas de importação teremos nesse arquivo.
Usando arquivos barril, podemos fazer com que nossas importações fiquem assim:
// somewhere else in the app
import { login, register } from '@/features/auth';
E o arquivo barril que trata das exportações ficará assim:
// auth/index.ts
export * from './login';
export * from './register';
Arquivos barril fornecem uma maneira mais limpa de lidar com importações e exportações. Eles melhoram a legibilidade do código e facilitam a refatoração do código, reduzindo as linhas de importação que você precisa gerenciar. No entanto, os benefícios que eles oferecem prejudicam o desempenho (prolongando os tempos de compilação) e a agitação da árvore menos eficaz, o que, é claro, resulta em pacotes JavaScript maiores. A Atlassian, por exemplo, relatou ter alcançado compilações 75% mais rápidas e uma ligeira redução no tamanho do pacote JavaScript após a remoção dos arquivos barril do front-end do aplicativo Jira.
Para projetos pequenos, arquivos barril são ótimos. Mas para projetos maiores, eu diria que eles melhoram a legibilidade do código em detrimento do desempenho. Você também pode ler sobre os efeitos que os arquivos barril tiveram no projeto da biblioteca MSW .
O problema do acoplamento
Acoplamento descreve como os componentes do seu sistema dependem uns dos outros. Na prática, você não pode se livrar do acoplamento, pois diferentes partes do seu projeto precisam interagir para que funcionem bem. No entanto, existem dois tipos de acoplamento que você deve evitar: (1) acoplamento rígido e (2) acoplamento implícito .
Acoplamento forte ocorre quando há um alto grau de interdependência entre dois ou mais módulos em um projeto, de modo que o módulo dependente depende de alguns detalhes de implementação do módulo de dependência. Isso torna difícil (se não impossível) atualizar o módulo de dependência sem tocar no módulo dependente e, dependendo de quão fortemente acoplado está o seu projeto, a atualização de um módulo pode exigir a atualização de vários outros módulos – um fenômeno conhecido como change amplification .
O acoplamento implícito ocorre quando um módulo do seu projeto depende secretamente de outro. Padrões como singletons globais, estado mutável compartilhado e efeitos colaterais podem causar acoplamento implícito. O acoplamento implícito pode reduzir a oscilação imprecisa da árvore, o comportamento inesperado no seu código e outros problemas que são difíceis de rastrear.
Embora o acoplamento não possa ser removido de um sistema, é importante que:
- Você não está expondo os detalhes de implementação de um módulo para que outro possa depender.
- Você não está expondo os detalhes de implementação de um módulo para que outro possa depender.
- A dependência de um módulo em relação a outro é explícita.
- Padrões como estados mutáveis compartilhados e singletons globais são usados com cuidado.
Os limites do módulo são limites da equipe
Ao construir aplicativos de grande escala, diferentes módulos do aplicativo geralmente são atribuídos a equipes diferentes. Dependendo de quem possui os módulos, limites são criados, e esses limites podem ser caracterizados como um dos seguintes:
- Fraco: Onde outros podem fazer alterações no código que não foi atribuído a eles, e os responsáveis pelo código monitoram as alterações feitas por outros enquanto também mantêm o código.
- Forte: Onde a propriedade é atribuída a pessoas diferentes e ninguém tem permissão para fazer contribuições para códigos que não sejam atribuídos a elas. Se alguém precisar de uma alteração no módulo de outra pessoa, deverá entrar em contato com o proprietário desse módulo, para que os proprietários possam fazer essa alteração.
- Coletivo: Onde ninguém é dono de nada e qualquer um pode fazer alterações em qualquer parte do projeto.
Deve haver alguma forma de comunicação, independentemente do tipo de colaboração. Com a Lei de Conway, podemos inferir melhor como diferentes níveis de comunicação aliados aos diferentes tipos de propriedade podem afetar a arquitetura de software.
De acordo com a Lei de Conway :
Qualquer organização que projete um sistema (definido de forma ampla) produzirá um design cuja estrutura é uma cópia da estrutura de comunicação da organização.
Com base nisso, aqui estão algumas suposições que podemos fazer:
| Boa comunicação | Má comunicação | |
|---|---|---|
| Propriedade fraca de código | A arquitetura ainda pode surgir, mas os limites permanecem obscuros | Arquitetura fragmentada e inconsistente |
| Código Forte Propriedade | Arquitetura clara e coesa alinhada com os limites de propriedade | Módulos desconectados; incompatibilidades de integração |
| Propriedade coletiva do código | Arquitetura integrada e altamente colaborativa | Limites desfocados; deriva arquitetônica |
Aqui está algo para ter em mente sempre que você definir os limites do módulo: Módulos que mudam frequentemente juntos devem compartilhar o mesmo limite, já que a evolução compartilhada é um forte sinal de que eles representam uma única unidade coesa.
Conclusão
Estruturar um grande projeto vai além de organizar arquivos e pastas. Envolve criar limites através de módulos e acoplá-los para formar um sistema funcional. Ao ser deliberado sobre a arquitetura do seu projeto, você evita o incômodo que acompanha a refatoração e torna seu projeto mais fácil de escalar e manter.
Se você tem projetos existentes que gostaria de gerenciar e não sabe por onde começar, você pode começar instalando Madge ou Dependency Cruiser. Aponte Madge para o seu projeto e veja como o gráfico realmente se parece. Verifique se há dependências circulares e módulos com setas vindas de todos os lugares. Pergunte a si mesmo se o que você vê é a aparência que você planejou para seu projeto.
Em seguida, você pode prosseguir impondo limites, quebrando cadeias circulares, movendo módulos e extraindo utilitários. Você não precisa refatorar tudo de uma vez – você pode fazer alterações conforme avança. Além disso, se você não possui um sistema organizado para uso de módulos, você precisa começar a implementar um.
Você está deixando a estrutura do seu módulo acontecer com você ou está projetando-a?
Leitura adicional
- A estrutura de pastas perfeita para front-end escalonável (design com fatias de recursos)
- Boas arquiteturas de software são principalmente sobre limites (Federico Terzi)
Um sistema de módulo JavaScript bem projetado é sua primeira decisão de arquitetura originalmente escrito à mão e publicado com amor em CSS-Tricks . Você realmente deveria receber o boletim informativo também.
