O que é um importmap: sem bundler necessário
A maioria dos frameworks JavaScript requer uma etapa de build: um bundler como Webpack, esbuild ou Vite que transforma, concatena e minifica seu JavaScript antes do navegador poder usá-lo. Rails 8 adota uma abordagem diferente: importmaps. Sem Node.js. Sem node_modules. Sem etapa de build para JavaScript.
O problema que importmaps resolvem
Antes dos importmaps, carregar JavaScript no navegador significava uma de duas coisas:
-
Tags script:
<script src="lodash.js">seguido de<script src="app.js">. Cada biblioteca precisava de sua própria tag, a ordem de carregamento importava e tudo vivia no escopo global. Conflitos eram comuns. -
Bundlers: ferramentas como Webpack que leem declarações
importno seu código, resolvem dependências, concatenam tudo em um ou poucos arquivos e geram bundles de saída com fingerprint. Isso funciona, mas acrescenta complexidade: arquivos de configuração, pipelines de build, Node.js como dependência e tempos de build longos em projetos grandes.
Importmaps dão a você a sintaxe import da opção 2 sem a etapa de build da opção 2. O navegador faz a resolução de módulos nativamente.
Como importmaps funcionam
Um importmap é um objeto JSON que diz ao navegador: "quando você ver import X from 'stimulus', busque desta URL." É um recurso nativo do navegador, e os navegadores modernos não precisam de polyfill para ele.
<script type="importmap">
{
"imports": {
"@hotwired/stimulus": "/assets/stimulus.min-4b1e420e.js",
"application": "/assets/application-edf3ab74.js"
}
}
</script>
<script type="module">
import { Application } from "@hotwired/stimulus"
// Navegador sabe buscar /assets/stimulus.min-4b1e420e.js
</script>
O bloco <script type="importmap"> mapeia nomes de módulos para URLs. Quando um <script type="module"> executa uma declaração import, o navegador procura o nome no importmap e busca o arquivo correto. Sem bundler necessário: o navegador é o resolvedor de módulos.
Como esta aplicação usa importmaps
O layout desta aplicação inclui uma chamada de helper que gera todas as tags JavaScript:
<%# app/views/layouts/application.html.erb %>
<%= javascript_importmap_tags %>
Esse único helper gera três coisas:
- Um bloco
<script type="importmap">com o mapeamento completo de módulos para URLs. - Uma tag
<link rel="modulepreload">para cada pin que não esteja marcado compreload: false(para desempenho). - Uma tag
<script type="module">que importa o ponto de entrada da aplicação.
O mapeamento vem de config/importmap.rb, que começa assim:
# config/importmap.rb
pin "application"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
Cada declaração pin mapeia um nome de módulo para um arquivo JavaScript. Quando você escreve import "application" no navegador, ele resolve para /assets/application-edf3ab74.js: a versão com fingerprint de app/javascript/application.js.
O ponto de entrada: application.js
// app/javascript/application.js
import "controllers"
import "@hotwired/turbo-rails"
import "lexxy"
import "@rails/actiontext"
Este é o ponto de partida. Quando o navegador carrega a página, ele:
- Lê o importmap para saber onde cada módulo está.
- Executa
application.jscomo um módulo. - Segue cada
import: carregandocontrollers,@hotwired/turbo-rails,lexxye@rails/actiontext. - Cada um deles pode importar mais módulos, que o navegador resolve pelo mesmo importmap.
flowchart TD
A["javascript_importmap_tags"] --> B["script type=importmap (mapeamento JSON)"]
A --> C["link rel=modulepreload (performance)"]
A --> D["script type=module (ponto de entrada)"]
D --> E["application.js"]
E --> F["controllers/index.js"]
E --> G["@hotwired/turbo-rails"]
E --> H["lexxy"]
E --> I["@rails/actiontext"]
F --> J["Todos os controllers Stimulus"]
Sem node_modules, sem package.json
Esta aplicação não tem diretório node_modules, nem package.json, nem package-lock.json. Dependências JavaScript são gerenciadas através de config/importmap.rb e uma de duas opções:
- Arquivos vendorizados em
vendor/javascript/: baixados uma vez, commitados no repositório. - URLs de CDN: buscados diretamente de um CDN em tempo de execução.
# Vendorizado: o arquivo está em vendor/javascript/
pin "codemirror", preload: false # => vendor/javascript/codemirror.js
# CDN: buscado da rede em tempo de execução
pin "mermaid", to: "https://cdn.jsdelivr.net/npm/[email protected]/dist/mermaid.esm.min.mjs", preload: false
Ambas as abordagens evitam Node.js completamente. O trade-off: você não tem tree-shaking (remoção de código não utilizado das bibliotecas). Para a maioria das aplicações Rails, multiplexação HTTP/2 e cache do navegador tornam esse trade-off aceitável.
O que este curso cobre
Nas próximas seis aulas, você vai entender cada camada do asset pipeline desta aplicação:
- A API pin (aula 2): como
pin,pin_all_frometo:mapeiam módulos para arquivos. - Propshaft (aula 3): o servidor de assets que aplica fingerprint e serve arquivos, o substituto moderno do Sprockets.
- Carregamento de controllers Stimulus (aula 4): como
eagerLoadControllersFromauto-registra controllers. - Adicionando uma dependência (aula 5): o workflow
bin/importmap pinpara novos pacotes JavaScript. - Preocupações de produção (aula 6): preloading, caching, fallback de CDN e Content Security Policy.
- Juntando tudo (aula 7): o pipeline completo de
config/importmap.rbà execução no navegador.
Continue com a aula 2
A API pin: mapeando módulos para arquivos
Trata de pin, pin_all_from, pins de CDN e vendorizados, o JSON resolvido e a ordem de busca do Propshaft.
Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.