Componentes e Layouts
A equipe do Charlie consiste em cinco desenvolvedores, e cada um escreve componentes de forma diferente—alguns importam manualmente, outros registram globalmente, e não há uma convenção de nomenclatura consistente. Alice notou que a página estava carregando muitos componentes desnecessários, o que estava atrasando o carregamento da primeira tela. Bob queria usar um layout diferente para o painel de administração, mas não sabia como trocar.
1. O Que Você Vai Aprender
- Importação Automática de Componentes: Regras de Escaneamento de Diretório e Convenções de Nomenclatura
- Sistema de Layouts: Troca entre layout padrão e layouts personalizados
- Lazy Loading de Componentes: O Prefixo "Lazy" e Suspense
- Melhores Práticas para Props e Emits de Componentes
- MegaShop Header/Footer/ProductCard: Guia Prático
2. Uma História Real de uma Equipe
(1) Dor: Gerenciamento Desorganizado de Componentes
A equipe de cinco do Charlie trabalha de forma isolada: Alice importa manualmente cada componente, Bob usa registro global, o que faz o tamanho do bundle inflar, e o novo colega não consegue descobrir os nomes dos componentes. Existem três maneiras diferentes de referenciar um único ProductCard em arquivos diferentes.
(2) Solução com a Importação Automática do Nuxt 3
O Nuxt 3 escaneia automaticamente o diretório components/, gera nomes de componentes de acordo com as regras, e garante que todos referenciem de forma consistente:
<!-- Não é necessário import, apenas use -->
<ProductCard :product="item" />
(3) Benefícios: Padronização + Redução de Tamanho
A equipe padronizou a forma de referenciar componentes; o prefixo "Lazy" garante que apenas componentes visíveis sejam carregados na primeira tela, reduzindo o JavaScript da primeira tela da MegaShop em 40%.
3. Importação Automática de Componentes
(1) Escaneamento de Diretório e Convenções de Nomenclatura
graph TB
A[components/] --> B[Nível raiz]
A --> C[Subdiretório]
A --> D[Diretório aninhado]
B --> E[ProductCard.vue → ProductCard]
C --> F[product/Card.vue → ProductCard ①]
D --> G[shop/product/Card.vue → ShopProductCard]
style G fill:#ffe,stroke:#f90
pathPrefix: false, product/Card.vue → Card pode resultar em conflito de nomenclatura. Recomendamos manter o prefixo padrão.
(2) Comparação de Convenções de Nomenclatura
| Caminho do Arquivo | Nome do Componente Padrão | pathPrefix: false |
|---|---|---|
| components/AppHeader.vue | AppHeader | AppHeader |
| components/product/Card.vue | ProductCard | Card |
| components/shop/ProductCard.vue | ShopProductCard | ProductCard |
| components/admin/user/Table.vue | AdminUserTable | Table |
(1) ▶Exemplo: Importação Automática de Componente
<!-- components/ProductCard.vue -->
<template>
<div class="product-card">
<img :src="product.image" :alt="product.name" />
<h3>{{ product.name }}</h3>
<p>${{ product.price }} USD</p>
<button @click="$emit('add-to-cart', product)">Adicionar ao Carrinho</button>
</div>
</template>
<script setup lang="ts">
interface Product {
id: number
name: string
price: number
image: string
}
defineProps<{ product: Product }>()
defineEmits<{ 'add-to-cart': [product: Product] }>()
</script>
Saída:
// Execução Bem-sucedida
(2) ▶Exemplo: Usando o Componente Auto-Importado em uma Página
<!-- pages/products/index.vue -->
<template>
<div>
<h1>Todos os Produtos</h1>
<div class="grid">
<!-- ProductCard auto-importado -->
<ProductCard
v-for="p in products"
:key="p.id"
:product="p"
@add-to-cart="handleAdd"
/>
</div>
</div>
</template>
<script setup lang="ts">
const { data: products } = await useFetch('/api/products')
function handleAdd(product: any) {
console.log('Adicionado:', product.name)
}
</script>
Saída:
// Execução Bem-sucedida
4. Sistema de Layouts
(1) Como o Layout Funciona
graph TB
A[app.vue] --> B[NuxtLayout]
B --> C{Layout Atual}
C -->|default| D[layouts/default.vue]
C -->|sidebar| E[layouts/sidebar.vue]
D --> F[NuxtPage - Conteúdo da Página]
E --> F
(2) A Relação Entre app.vue e Layout
<!-- app.vue - Componente raiz com suporte a layout -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
(1) ▶Exemplo: Layout Padrão
<!-- layouts/default.vue -->
<template>
<div class="layout-default">
<AppHeader />
<main class="content">
<!-- Conteúdo da página renderiza aqui -->
<slot />
</main>
<AppFooter />
</div>
</template>
Saída:
// Execução Bem-sucedida
(2) ▶Exemplo: Layout com Sidebar
<!-- layouts/sidebar.vue -->
<template>
<div class="layout-sidebar">
<AppHeader />
<div class="main-wrapper">
<aside class="sidebar">
<nav>
<NuxtLink to="/admin">Dashboard</NuxtLink>
<NuxtLink to="/admin/products">Produtos</NuxtLink>
<NuxtLink to="/admin/orders">Pedidos</NuxtLink>
</nav>
</aside>
<main class="content">
<slot />
</main>
</div>
</div>
</template>
Saída:
// Execução Bem-sucedida
(3) ▶Exemplo: Troca de Layout por Página
<!-- pages/admin/index.vue -->
<template>
<div>
<h1>Painel de Administração</h1>
<p>Bem-vindo, Bob</p>
</div>
</template>
<script setup lang="ts">
// Trocar para layout sidebar nesta página
definePageMeta({
layout: 'sidebar'
})
</script>
Saída:
// Execução Bem-sucedida
(3) Comparação de Layout vs. Rotas Aninhadas
| Dimensão | Layout | Rotas Aninhadas |
|---|---|---|
| Local de Definição | layouts/*.vue | pages/parent.vue |
| Slot de Conteúdo | <slot /> |
<NuxtPage /> |
| Método de Troca | definePageMeta | Automático (Hierarquia de Rotas) |
| Recuperação de Dados | Não Recomendado | Cada Um Busca o Que Precisa |
| Casos de Uso | Layout Geral de Página | UI Compartilhada Entre Níveis de Rota |
5. Lazy Loading de Componentes
(1) Mecanismo de Lazy Loading
O Nuxt 3 registra automaticamente uma versão "Lazy" de cada componente, carregando o código apenas quando o componente é renderizado pela primeira vez.
(1) ▶Exemplo: Lazy loading com prefixo Lazy
<template>
<div>
<!-- Carregamento imediato - incluído no bundle inicial -->
<AppHeader />
<!-- Lazy loaded - code-split, carregado na primeira renderização -->
<LazyProductCard
v-for="p in products"
:key="p.id"
:product="p"
/>
<!-- Lazy com v-if - carregado apenas quando a condição é verdadeira -->
<LazyProductReviewModal
v-if="showReviewModal"
:product-id="selectedProductId"
@close="showReviewModal = false"
/>
</div>
</template>
Saída:
// Execução Bem-sucedida
(2) ▶Exemplo: Carregamento Assíncrono com Suspense
<template>
<div>
<Suspense>
<template #default>
<LazyHeavyChart :data="chartData" />
</template>
<template #fallback>
<div class="loading">Carregando gráfico...</div>
</template>
</Suspense>
</div>
</template>
Saída:
// Execução Bem-sucedida
(2) Comparação de Estratégias de Lazy Loading
| Estratégia | Implementação | Momento do Carregamento | Cenários Aplicáveis |
|---|---|---|---|
| Carregamento Imediato | <ProductCard /> |
Quando a página carrega | Componentes core na primeira tela |
| Prefixo Lazy | <LazyProductCard /> |
Na primeira renderização | Componentes abaixo da dobra |
| v-if + Lazy | <LazyModal v-if="show" /> |
Quando a condição é verdadeira | Pop-up/Drawer |
| Suspense | Envoltório <Suspense> |
Quando a operação assíncrona está pronta | Um estado "carregando" é necessário |
6. Melhores Práticas para Props e Emits
(1) ▶Exemplo: Props e Emits com Segurança de Tipos
<!-- components/ProductCard.vue -->
<script setup lang="ts">
interface Product {
id: number
name: string
price: number
image: string
inStock: boolean
}
// Props com segurança de tipos e valores padrão
const props = withDefaults(
defineProps<{
product: Product
showStock?: boolean
currency?: string
}>(),
{
showStock: true,
currency: 'USD'
}
)
// Emits com segurança de tipos
const emit = defineEmits<{
'add-to-cart': [product: Product]
'toggle-wishlist': [productId: number]
}>()
</script>
<template>
<div class="card">
<img :src="product.image" :alt="product.name" />
<h3>{{ product.name }}</h3>
<p>{{ product.price }} {{ currency }}</p>
<p v-if="showStock">
{{ product.inStock ? 'Em Estoque' : 'Esgotado' }}
</p>
<button @click="emit('add-to-cart', product)">Adicionar ao Carrinho</button>
<button @click="emit('toggle-wishlist', product.id)">Favoritar</button>
</div>
</template>
Saída:
// Execução Bem-sucedida
7. Exemplo Completo: Sistema de Componentes de Layout da MegaShop
<!-- components/AppHeader.vue -->
<template>
<header class="app-header">
<NuxtLink to="/" class="logo">MegaShop</NuxtLink>
<nav>
<NuxtLink to="/products">Produtos</NuxtLink>
<NuxtLink to="/categories">Categorias</NuxtLink>
</nav>
<div class="user-area">
<NuxtLink to="/cart">Carrinho ({{ cartCount }})</NuxtLink>
<NuxtLink v-if="isLoggedIn" to="/admin">Admin</NuxtLink>
<NuxtLink v-else to="/login">Entrar</NuxtLink>
</div>
</header>
</template>
<script setup lang="ts">
const cart = useState<any[]>('cart', () => [])
const cartCount = computed(() => cart.value.length)
const isLoggedIn = useState('isLoggedIn', () => false)
</script>
<!-- components/AppFooter.vue -->
<template>
<footer class="app-footer">
<p>© 2026 MegaShop. Todos os direitos reservados.</p>
<nav>
<NuxtLink to="/about">Sobre</NuxtLink>
<NuxtLink to="/contact">Contato</NuxtLink>
<NuxtLink to="/privacy">Política de Privacidade</NuxtLink>
</nav>
</footer>
</template>
<!-- layouts/default.vue -->
<template>
<div class="layout-default">
<AppHeader />
<main>
<slot />
</main>
<AppFooter />
</div>
</template>
❓Perguntas Frequentes
components/, se o nome do arquivo começa com letra maiúscula, e se o caminho não foi excluído da configuração components no nuxt.config.ts. Tente reiniciar o servidor dev.Lazy faz o componente renderizar mais tarde?Lazy apenas atrasa o carregamento do código JavaScript do componente; uma vez que o componente renderiza, ele funciona normalmente. Ele realiza code splitting, não renderização adiada.definePageMeta. Se você precisa de layouts diferentes para seções diferentes, alcance isso combinando componentes.definePageMeta precisa estar no nível superior do script setup?definePageMeta é uma macro do compilador e deve ser chamado diretamente no nível superior do <script setup>; não pode ser colocado dentro de declarações condicionais ou funções.components/ afeta a performance?📖Resumo
- Importação Automática de Componentes do Nuxt 3: Gera nomes de componentes com base em caminhos de diretório, eliminando a necessidade de imports manuais
- O sistema de Layouts usa
definePageMetapara trocar layouts de página, eslotcontém o conteúdo da página - O prefixo "Lazy" habilita lazy loading de componentes, reduzindo o tamanho do JavaScript acima da dobra
- Props/Emits: Implementando Segurança de Tipos Usando Generics do TypeScript
- MegaShop usa dois layouts—"default" e "sidebar"—para o front-end e o painel de administração
📝Exercícios
- Exercício Básico (Dificuldade: ⭐): Crie os componentes AppHeader e AppFooter e use-os no layout padrão.
- Exercício Avançado (Dificuldade: ⭐⭐): Crie um layout com sidebar e alterne entre os dois layouts na página admin usando
definePageMeta. Compare os resultados dos dois layouts. - Desafio (Dificuldade: ⭐⭐⭐): Use o prefixo Lazy e v-if para implementar um componente de popup de detalhes do produto que carrega apenas quando clicado, e use Suspense para exibir o estado de carregamento.
---|



