组件
app/components/ 目录下的 Vue 组件会自动注册,无需手动 import。这是 Nuxt 和纯 Vue 项目最大的区别之一——你创建组件文件,Nuxt 帮你完成注册和导入。
自动注册
app/components/
├── AppHeader.vue → <AppHeader>
├── AppFooter.vue → <AppFooter>
├── Button.vue → <Button>
├── Card/
│ ├── Card.vue → <Card>
│ ├── CardHeader.vue → <CardHeader>
│ └── CardBody.vue → <CardBody>
└── user/
├── UserCard.vue → <UserCard>
└── UserAvatar.vue → <UserAvatar><template>
<AppHeader />
<Card>
<CardHeader>标题</CardHeader>
<CardBody>内容</CardBody>
</Card>
<UserCard :user="user" />
</template>
<!-- 不需要写这些:
<script setup>
import AppHeader from '~/components/AppHeader.vue'
import Card from '~/components/Card/Card.vue'
import CardHeader from '~/components/Card/CardHeader.vue'
import CardBody from '~/components/Card/CardBody.vue'
import UserCard from '~/components/user/UserCard.vue'
</script>
-->自动注册的本质
Nuxt 在构建时扫描 components/ 目录,为每个组件生成注册代码。你在模板中写 <AppHeader />,Nuxt 确保对应的组件已经被导入和注册。
INFO
️ 新创建的组件如果 IDE 不识别 运行 nuxt prepare 重新生成类型声明,或重启开发服务器
命名规则
| 文件路径 | 组件名 | 说明 |
|---|---|---|
Button.vue | <Button> | 文件名即组件名 |
BaseButton.vue | <BaseButton> | 前缀保留 |
user/UserCard.vue | <UserCard> | 目录名首字母大写作为前缀 |
blog/PostItem.vue | <BlogPostItem> | 嵌套目录全保留 |
Icon/Home.vue | <IconHome> | 仅当同级存在 Icon.vue 时加前缀 |
命名去重
当同级存在同名文件和目录时,重复部分会被去掉:
components/
├── Card.vue → <Card>
├── Card/
│ ├── CardHeader.vue → <CardHeader>(不是 <CardCardHeader>)
│ └── CardBody.vue → <CardBody>(不是 <CardCardBody>)去重规则
如果组件名和父目录名有重复部分,重复的前缀被去掉。这是因为 Card/CardHeader.vue 在逻辑上属于 Card 的子组件,不需要再加 Card 前缀。
命名冲突
components/
├── Button.vue → <Button>
└── form/
└── Button.vue → <FormButton> ← 自动加目录名前缀INFO
️ 如果安装了 UI 库(如 Element Plus、Naive UI) 它们的组件名可能和你的冲突。比如 Element Plus 也有 <Button> 组件
解决方案
- 给自己的组件加前缀:
BaseButton.vue、AppButton.vue - 或者通过
nuxt.config.ts配置全局前缀:
export default defineNuxtConfig({
components: {
dirs: [{
path: '~/components',
pathPrefix: false, // 不加目录前缀
prefix: 'My', // 所有组件加 My 前缀:<MyButton>
}],
},
})最佳实践
- 基础组件加
Base前缀:BaseButton、BaseInput - 单例组件加
App前缀:AppHeader、AppFooter - 按功能分目录:
form/、card/、table/ - 避免和 UI 库组件重名
懒加载组件
组件名前加 Lazy 前缀即可懒加载——组件代码单独打包,需要时才下载:
<template>
<!-- 懒加载:组件需要时才下载,减少首屏体积 -->
<LazyHeavyChart v-if="showChart" />
<!-- 普通加载:打包进首屏 JS -->
<Button>点击</Button>
</template>懒加载的工作原理
- 普通
<HeavyChart />:组件代码打包进页面 JS,页面加载时一起下载 <LazyHeavyChart />:组件代码单独打包为独立 chunk,v-if为 true 时才下载- 懒加载本质上是 Vue 的
defineAsyncComponent+import()
| 场景 | 是否用 Lazy | 原因 |
|---|---|---|
| 地图组件(50KB+) | ✅ 用 | 体积大,只在特定页面用 |
| 富文本编辑器 | ✅ 用 | 体积大,只在编辑页面用 |
| 弹窗/抽屉 | ✅ 用 | 条件渲染,初始不需要 |
| 导航栏/页脚 | ❌ 不用 | 每个页面都用,懒加载反而多一次请求 |
| 小组件(< 5KB) | ❌ 不用 | 体积小,懒加载的请求开销比节省的体积还大 |
INFO
️ 懒加载的 SSR 行为:在 SSR 时 懒加载组件仍然会在服务端渲染(为了 SEO 和首屏内容完整)。只是客户端的下载被延迟了。如果你确实不想在 SSR 时渲染,用 <ClientOnly> 包裹
<ClientOnly> — 仅客户端渲染
<ClientOnly> 让内容只在客户端渲染,SSR 时跳过或显示 fallback:
<template>
<ClientOnly>
<!-- 仅在客户端渲染 -->
<MapComponent />
<!-- SSR 时显示的 fallback -->
<template #fallback>
<div class="map-placeholder">地图加载中...</div>
</template>
</ClientOnly>
</template>为什么需要 ClientOnly?
在 Nuxt SSR 中,组件代码可能在服务端执行。如果组件依赖浏览器 API,就会报错:
<!-- ❌ SSR 报错:window is not defined -->
<script setup>
const width = window.innerWidth // 服务端没有 window!
</script>三种解决方案的对比:
| 方案 | 代码 | 适用场景 |
|---|---|---|
<ClientOnly> | 模板级别控制 | 整个组件不能 SSR |
onMounted | 逻辑级别控制 | 只是个别代码不能在 SSR 执行 |
process.client | 条件判断 | 需要在 <script setup> 中判断 |
<!-- 方案 1:整个组件只在客户端渲染 -->
<ClientOnly>
<MapComponent />
</ClientOnly>
<!-- 方案 2:组件仍 SSR,但浏览器相关代码放在 onMounted 中 -->
<script setup>
const width = ref(0)
onMounted(() => {
width.value = window.innerWidth // ✅ onMounted 只在客户端执行
})
</script>
<!-- 方案 3:条件判断 -->
<script setup>
if (process.client) {
console.log(window.innerWidth) // ✅ 只在客户端执行
}
</script><ClientOnly> 对 SEO 的影响
被 <ClientOnly> 包裹的内容不会出现在 SSR 的 HTML 中,搜索引擎爬虫看不到。因此:
- ✅ 地图、图表等非文字内容 → 用
<ClientOnly>没问题 - ❌ 关键文字内容、SEO 关键词 → 不要用
<ClientOnly>,应该确保 SSR 渲染
典型需要 ClientOnly 的组件
| 组件类型 | 依赖的浏览器 API | 是否有 fallback |
|---|---|---|
| 地图(高德/百度/Google Maps) | window、DOM | ✅ 静态地图图片 |
| 图表(ECharts/Chart.js) | Canvas、SVG | ✅ 加载占位符 |
| 富文本编辑器 | document.execCommand | ✅ 只读预览 |
| 第三方支付按钮 | window.Alipay 等 | ✅ "加载中"提示 |
| 社交分享按钮 | window.FB 等 | ✅ 静态链接 |
<DevOnly> — 仅开发环境渲染
<DevOnly> 只在开发环境(npm run dev)渲染,生产构建时完全移除:
<template>
<DevOnly>
<DebugPanel /> <!-- 生产环境不会包含,甚至不会出现在 JS 包中 -->
</DevOnly>
</template><DevOnly> vs if (process.dev)
| 方案 | 模板中 | 生产构建结果 |
|---|---|---|
<DevOnly> | ✅ 可以在模板中使用 | 组件代码完全移除,不占体积 |
v-if="isDev" | ✅ 可用 | 组件代码仍包含在 JS 中,只是不渲染 |
process.dev | ❌ 只能在 script 中 | — |
推荐
模板中用 <DevOnly>,script 中用 process.dev。
适合场景
- 调试面板(显示当前路由、状态、性能数据)
- 开发辅助工具(设计稿对比、组件文档)
- Mock 数据切换
- 环境信息展示(开发环境标识)
动态组件
使用 Vue 的 <component :is=""> 动态切换组件。在 Nuxt 中需要用 resolveComponent 解析自动注册的组件:
<script setup>
const currentTab = ref('home')
// resolveComponent:解析自动注册的组件名
const tabs = {
home: resolveComponent('HomeTab'),
profile: resolveComponent('ProfileTab'),
settings: resolveComponent('SettingsTab'),
}
</script>
<template>
<component :is="tabs[currentTab]" />
</template>为什么用 resolveComponent?
在模板中写 <HomeTab /> 时,Vue 编译器自动处理组件解析。但在 <script> 中动态引用时,你需要手动调用 resolveComponent 来找到自动注册的组件。
INFO
️ SSR 注意事项:动态组件在 SSR 时会被渲染 如果某个 tab 的组件依赖浏览器 API,用 defineAsyncComponent + <ClientOnly> 配合
<ClientOnly>
<component :is="tabs[currentTab]" />
</ClientOnly>组件中的 SSR 注意事项
1. 不要在 <script setup> 顶层使用浏览器 API
<!-- ❌ 错误:SSR 时报错 -->
<script setup>
const width = window.innerWidth // ReferenceError: window is not defined
</script>
<!-- ✅ 正确:放在 onMounted 中 -->
<script setup>
const width = ref(0)
onMounted(() => {
width.value = window.innerWidth
})
</script>2. 避免服务端和客户端渲染不一致
<!-- ❌ Hydration 不匹配:服务端和客户端时间不同 -->
<template>
<p>当前时间:{{ new Date().toLocaleString() }}</p>
</template>
<!-- ✅ 只在客户端渲染时间 -->
<template>
<p v-if="mounted">当前时间:{{ currentTime }}</p>
<p v-else>加载中...</p>
</template>
<script setup>
const mounted = ref(false)
const currentTime = ref('')
onMounted(() => {
mounted.value = true
currentTime.value = new Date().toLocaleString()
})
</script>3. 组件中使用 useFetch 的注意事项
<script setup>
// ✅ 正确:useFetch 在 SSR 时获取数据,客户端复用
const { data } = await useFetch('/api/user')
// ❌ 错误:在 onMounted 中用 useFetch(SSR 时不执行,首屏无数据)
onMounted(async () => {
const { data } = await useFetch('/api/user') // SSR 时跳过!
})
</script>规则
需要 SSR 渲染的数据获取,必须在 <script setup> 顶层使用 useFetch/useAsyncData,不要放在 onMounted 中。
4. useId() 生成 SSR 安全的唯一 ID
<script setup>
// ✅ SSR 和客户端生成相同的 ID,不会 Hydration 不匹配
const inputId = useId()
</script>
<template>
<label :for="inputId">用户名</label>
<input :id="inputId" type="text" />
</template>为什么不用 Math.random() 或 Date.now()?
这些在服务端和客户端会生成不同的值,导致 Hydration 不匹配。useId() 是 Vue 3.5+ 提供的 API,确保两端生成相同的 ID。
组件目录配置
自定义扫描目录
// nuxt.config.ts
export default defineNuxtConfig({
components: [
// 默认目录
'~/components',
// 额外目录
{
path: '~/design-system/components', // 扫描这个目录
prefix: 'Ds', // 组件加 Ds 前缀:<DsButton>
},
],
})禁用自动导入某个目录
export default defineNuxtConfig({
components: [
{
path: '~/components/legacy',
enabled: false, // 不自动导入这个目录的组件
},
],
})什么时候需要自定义?
- 使用设计系统时,给组件加统一前缀避免冲突
- 有一些旧组件不想自动注册,但也不想删掉
- 组件分散在多个目录中(如 monorepo)
常见问题
新建的组件不识别
- 重启开发服务器(
Ctrl+C再npm run dev) - 运行
nuxt prepare重新生成类型 - 检查文件是否在
app/components/目录下
组件和 UI 库名字冲突
给组件加前缀,或通过 nuxt.config.ts 配置 prefix。详见上方"命名冲突"一节。
<ClientOnly> 内的组件样式闪烁
SSR 时 <ClientOnly> 内的内容不渲染,客户端 Hydration 后才渲染,可能导致布局跳动。解决方案:
<ClientOnly>
<MapComponent />
<template #fallback>
<!-- 用和实际组件相同尺寸的占位符,避免布局跳动 -->
<div style="height: 400px; background: #f5f5f5;">地图加载中...</div>
</template>
</ClientOnly>知识脉络
布局 → 你在这里:组件
│
├─→ 下一步:页面过渡
│
├─→ 深入了解:自动导入(03-核心概念/03-自动导入)
│
└─→ 深入了解:ClientOnly 内置组件(14-内置组件速查/03-条件渲染)