Nuxt: 组件与布局
最后更新:2026-08-26
Charlie 的团队有 5 名开发者,每个人写组件的方式都不一样——有的手动 import,有的全局注册,命名也不统一。Alice 发现页面加载了很多不需要的组件,拖慢了首屏。Bob 想给管理后台用不同的布局,却不知道怎么切换。
1. 你将学到
- 组件自动导入:目录扫描规则与命名约定
- Layouts 系统:default 布局与自定义布局切换
- 组件懒加载:Lazy 前缀与 Suspense
- 组件 Props/Emits 最佳实践
- MegaShop Header/Footer/ProductCard 实战
2. 一个团队的真实故事
(1) 痛点:组件管理混乱
Charlie 的 5 人团队各自为政:Alice 手动 import 每个组件,Bob 用全局注册导致包体积膨胀,新同事搞不清组件叫什么名字。一个 ProductCard 在不同文件里有 3 种引用方式。
(2) Nuxt 3 自动导入的解法
Nuxt 3 自动扫描 components/ 目录,按规则生成组件名,所有人用统一方式引用:
VUE
<!-- No import needed, just use it -->
<ProductCard :product="item" />
(3) 收益:统一规范 + 减少体积
团队统一了组件引用方式,Lazy 前缀让首屏只加载可见组件,MegaShop 首屏 JS 体积减少了 40%。
3. 组件自动导入
(1) 目录扫描与命名规则
graph TB
A[components/] --> B[Root level]
A --> C[Sub-directory]
A --> D[Nested directory]
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,可能重名冲突。推荐保持默认前缀。
(2) 命名规则对比
| 文件路径 | 默认组件名 | 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 |
▶ 示例:组件自动导入
VUE
<!-- 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)">Add to Cart</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>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:在页面中使用自动导入组件
VUE
<!-- pages/products/index.vue -->
<template>
<div>
<h1>All Products</h1>
<div class="grid">
<!-- ProductCard auto-imported -->
<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('Added:', product.name)
}
</script>
输出:
TEXT
📖 仅展示
// 执行成功
4. Layouts 布局系统
(1) 布局工作原理
graph TB
A[app.vue] --> B[NuxtLayout]
B --> C{Current Layout}
C -->|default| D[layouts/default.vue]
C -->|sidebar| E[layouts/sidebar.vue]
D --> F[NuxtPage - Page Content]
E --> F
(2) app.vue 与 Layout 的关系
VUE
<!-- app.vue - Root component with layout support -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
▶ 示例:Default 布局
VUE
<!-- layouts/default.vue -->
<template>
<div class="layout-default">
<AppHeader />
<main class="content">
<!-- Page content renders here -->
<slot />
</main>
<AppFooter />
</div>
</template>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:Sidebar 布局
VUE
<!-- 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">Products</NuxtLink>
<NuxtLink to="/admin/orders">Orders</NuxtLink>
</nav>
</aside>
<main class="content">
<slot />
</main>
</div>
</div>
</template>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:页面切换布局
VUE
<!-- pages/admin/index.vue -->
<template>
<div>
<h1>Admin Dashboard</h1>
<p>Welcome, Bob</p>
</div>
</template>
<script setup lang="ts">
// Switch to sidebar layout for this page
definePageMeta({
layout: 'sidebar'
})
</script>
输出:
TEXT
📖 仅展示
// 执行成功
(3) 布局 vs 嵌套路由对比
| 维度 | Layout | 嵌套路由 |
|---|---|---|
| 定义位置 | layouts/*.vue | pages/parent.vue |
| 内容槽 | <slot /> |
<NuxtPage /> |
| 切换方式 | definePageMeta | 自动(路由层级) |
| 数据获取 | 不推荐 | 父子各取所需 |
| 适用场景 | 页面整体布局 | 路由层级共享 UI |
5. 组件懒加载
(1) 懒加载机制
Nuxt 3 自动为每个组件注册 Lazy 版本,只在组件首次渲染时加载代码。
▶ 示例:Lazy 前缀懒加载
VUE
<template>
<div>
<!-- Eagerly loaded - included in initial bundle -->
<AppHeader />
<!-- Lazy loaded - code-split, loaded on first render -->
<LazyProductCard
v-for="p in products"
:key="p.id"
:product="p"
/>
<!-- Lazy with v-if - only loaded when condition is true -->
<LazyProductReviewModal
v-if="showReviewModal"
:product-id="selectedProductId"
@close="showReviewModal = false"
/>
</div>
</template>
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:Suspense 异步加载
VUE
<template>
<div>
<Suspense>
<template #default>
<LazyHeavyChart :data="chartData" />
</template>
<template #fallback>
<div class="loading">Loading chart...</div>
</template>
</Suspense>
</div>
</template>
输出:
TEXT
📖 仅展示
// 执行成功
(2) 懒加载策略对比
| 策略 | 写法 | 加载时机 | 适用场景 |
|---|---|---|---|
| 即时加载 | <ProductCard /> |
页面加载时 | 首屏核心组件 |
| Lazy 前缀 | <LazyProductCard /> |
首次渲染时 | 首屏下方组件 |
| v-if + Lazy | <LazyModal v-if="show" /> |
条件为 true 时 | 弹窗/抽屉 |
| Suspense | <Suspense> 包裹 |
异步就绪时 | 需 loading 状态 |
6. Props/Emits 最佳实践
▶ 示例:类型安全的 Props 与 Emits
VUE
<!-- components/ProductCard.vue -->
<script setup lang="ts">
interface Product {
id: number
name: string
price: number
image: string
inStock: boolean
}
// Type-safe props with default values
const props = withDefaults(
defineProps<{
product: Product
showStock?: boolean
currency?: string
}>(),
{
showStock: true,
currency: 'USD'
}
)
// Type-safe emits
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 ? 'In Stock' : 'Out of Stock' }}
</p>
<button @click="emit('add-to-cart', product)">Add to Cart</button>
<button @click="emit('toggle-wishlist', product.id)">Wishlist</button>
</div>
</template>
输出:
TEXT
📖 仅展示
// 执行成功
7. 综合示例:MegaShop 布局组件体系
VUE
<!-- components/AppHeader.vue -->
<template>
<header class="app-header">
<NuxtLink to="/" class="logo">MegaShop</NuxtLink>
<nav>
<NuxtLink to="/products">Products</NuxtLink>
<NuxtLink to="/categories">Categories</NuxtLink>
</nav>
<div class="user-area">
<NuxtLink to="/cart">Cart ({{ cartCount }})</NuxtLink>
<NuxtLink v-if="isLoggedIn" to="/admin">Admin</NuxtLink>
<NuxtLink v-else to="/login">Sign In</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>
VUE
<!-- components/AppFooter.vue -->
<template>
<footer class="app-footer">
<p>© 2026 MegaShop. All rights reserved.</p>
<nav>
<NuxtLink to="/about">About</NuxtLink>
<NuxtLink to="/contact">Contact</NuxtLink>
<NuxtLink to="/privacy">Privacy Policy</NuxtLink>
</nav>
</footer>
</template>
VUE
<!-- layouts/default.vue -->
<template>
<div class="layout-default">
<AppHeader />
<main>
<slot />
</main>
<AppFooter />
</div>
</template>
❓ 常见问题
Q 为什么我的组件自动导入不生效?
A 检查文件是否在 components/ 目录下、文件名是否以大写开头、是否在 nuxt.config.ts 的 components 配置中排除了该路径。重启 dev server 试试。
Q Lazy 前缀会让组件延迟渲染吗?
A 不会。Lazy 只延迟加载组件的 JS 代码,组件一旦渲染就正常工作。它做的是 code-splitting,不是延迟渲染。
Q Layout 能不能获取数据?
A 技术上可以,但不推荐。Layout 是 UI 框架,数据获取应在页面或 Composable 中进行。Layout 只负责布局结构。
Q 一个页面能用多个 Layout 吗?
A 不能。每个页面只能通过 definePageMeta 指定一个 layout。如果需要不同区域的不同布局,用组件组合实现。
Q definePageMeta 必须在 script setup 的顶层吗?
A 是的。definePageMeta 是编译器宏,必须在
<script setup> 顶层直接调用,不能放在条件语句或函数内。Q 组件放在 components/ 子目录太深会影响性能吗?
A 不会影响运行时性能,因为 Nuxt 3 只打包实际使用的组件。但目录层级过深会增加命名复杂度,建议不超过 2 层。
📖 小节
- Nuxt 3 自动导入组件:按目录路径生成组件名,无需手动 import
- Layouts 系统通过 definePageMeta 切换页面布局,slot 承载页面内容
- Lazy 前缀实现组件懒加载,减少首屏 JS 体积
- Props/Emits 使用 TypeScript 泛型实现类型安全
- MegaShop 用 default + sidebar 两种布局覆盖前台与管理后台
📝 作业
- 基础题(难度⭐):创建 AppHeader 和 AppFooter 组件,在 default 布局中使用
- 进阶题(难度⭐⭐):创建 sidebar 布局,在 admin 页面通过 definePageMeta 切换,对比两种布局效果
- 挑战题(难度⭐⭐⭐):用 Lazy 前缀 + v-if 实现一个商品详情弹窗组件,只有点击时才加载,并用 Suspense 展示 loading 状态
---|