app/ 目录
app/ 是 Nuxt 4 引入的核心应用目录,包含了所有前端代码。这是 Nuxt 4 与 Nuxt 3 最大的结构变化之一。
为什么 Nuxt 4 要引入 app/ 目录?
在 Nuxt 3 中,pages/、components/ 等目录直接放在项目根目录,和 server/、node_modules/、配置文件等混在一起。这带来了几个问题:
| 问题 | 具体表现 |
|---|---|
| 文件监听慢 | Vite 监听整个项目目录,包括 node_modules/、.git/ 等,浪费性能 |
| 上下文不清 | 打开编辑器,前端代码、后端代码、配置文件混在一起,找不到重点 |
| 类型混乱 | 前端代码可能误用服务端 API,服务端代码可能误用客户端 API |
Nuxt 4 将应用代码放入 app/ 的好处:
- 文件监听更快:Vite 只监听
app/目录,不监听node_modules/、.git/等 - IDE 上下文更清晰:
app/下全是前端代码,server/下全是后端代码,一目了然 - 类型隔离更准确:
app/和server/使用不同的 TypeScript 项目,互不干扰
Nuxt 3 → Nuxt 4 迁移
如果你用的是旧结构(pages/、components/ 在根目录),Nuxt 4 仍然兼容。但推荐迁移到 app/ 结构,获得更好的开发体验。
目录结构
app/
├── assets/ # 构建处理的资源(CSS、图片等)
├── components/ # Vue 组件(自动导入)
├── composables/ # 组合式函数(自动导入)
├── layouts/ # 布局组件
├── middleware/ # 路由中间件
├── pages/ # 页面(自动生成路由)
├── plugins/ # 插件
├── utils/ # 工具函数(自动导入)
├── app.vue # 根组件
├── app.config.ts # 应用配置
└── error.vue # 错误页面如何记忆这个结构?
按照"从前到后"的逻辑:
- 用户看到的 →
pages/(页面)、layouts/(布局)、components/(组件) - 用户交互的 →
middleware/(路由守卫)、composables/(业务逻辑) - 支撑运行的 →
plugins/(插件)、utils/(工具)、assets/(资源) - 入口文件 →
app.vue(根组件)、error.vue(错误页)、app.config.ts(配置)
子目录详解
app/assets/
存放需要构建工具处理的静态资源:
app/assets/
├── css/
│ ├── main.css # 全局样式
│ └── variables.css # CSS 变量
├── images/
│ └── logo.png # 会被 Vite 处理(压缩、哈希文件名)
└── fonts/
└── custom-font.woff2在代码中引用:
/* app/assets/css/main.css */
@import './variables.css';<style>
.logo {
/* ~ 指向项目根目录,所以 ~/app/assets/ 可以简写为 ~/assets/ */
background-image: url('~/assets/images/logo.png');
}
</style>assets/ vs public/ 的区别
| 特性 | assets/ | public/ |
|---|---|---|
| 构建处理 | ✅ Vite 处理(压缩、哈希、优化) | ❌ 原样复制 |
| 引用方式 | ~/assets/xxx(需要构建工具解析) | /xxx(绝对路径) |
| 文件名变化 | ✅ 自动加哈希(如 logo.3a4b.png) | ❌ 保持原名 |
| Tree-shaking | ✅ 未使用的不会打包 | ❌ 全部打包 |
| 适合放 | CSS、需要优化的图片 | favicon、robots.txt |
app/components/
Vue 组件自动注册目录,组件会按文件名自动导入:
app/components/
├── AppHeader.vue → <AppHeader> // 大驼峰,直接使用
├── AppFooter.vue → <AppFooter>
├── Button.vue → <Button>
└── user/
├── UserCard.vue → <UserCard> // 子目录名作为前缀
└── UserAvatar.vue → <UserAvatar>自动导入的好处
以前用 Vue,每个组件都要写 import AppHeader from './components/AppHeader.vue',然后在 components: { AppHeader } 中注册。Nuxt 省掉了这些重复步骤。
INFO
️ 但也要注意命名冲突:如果你装了 UI 库(如 Element Plus) 它也有 <Button> 组件,可能和你的 Button.vue 冲突。解决方案是给组件加前缀(如 BaseButton.vue)
app/composables/
组合式函数自动导入目录:
// app/composables/useCounter.ts
export const useCounter = () => {
// 为什么用 useState 而不是 ref?
// 因为 useState 是 SSR 安全的,ref 在 SSR 时可能跨请求共享
const count = useState('counter', () => 0)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}在任何组件中直接使用,无需 import:
<script setup>
const { count, increment } = useCounter()
</script>composables/ 和 utils/ 的区别
| 目录 | 命名规则 | 典型内容 | 是否响应式 |
|---|---|---|---|
composables/ | use 前缀 | 状态管理、数据获取、业务逻辑 | 通常是 |
utils/ | 无特殊规则 | 格式化、计算、纯函数 | 通常不是 |
简单判断
如果函数返回响应式数据(ref/computed),放 composables/;如果是纯输入输出的工具函数,放 utils/。
app/layouts/
布局组件目录——共享导航栏、页脚等公共 UI 的外壳:
app/layouts/
├── default.vue # 默认布局(所有页面都使用,除非指定其他布局)
├── admin.vue # 管理后台布局
└── blank.vue # 空白布局(登录、注册等不需要导航的页面)布局 vs 组件的区别
布局包裹整个页面,组件嵌在页面内部。你可以把布局理解为"页面模板"——同样的内容,换一个布局就像换了一个"皮肤"。
app/middleware/
路由中间件目录——在页面跳转前执行的检查逻辑:
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const isAuthenticated = useState('isAuthenticated')
if (!isAuthenticated.value) {
return navigateTo('/login') // 未登录?跳转到登录页
}
})典型场景
用户没登录就不让看某些页面;管理员才能访问后台;跳转前显示确认对话框。
app/pages/
页面目录,基于文件自动生成路由:
app/pages/
├── index.vue → /
├── about.vue → /about
├── blog/
│ ├── index.vue → /blog
│ └── [id].vue → /blog/:id
└── user/
└── [name].vue → /user/:name这是 Nuxt 最核心的约定
创建一个文件 = 创建一个路由。不需要写路由配置,不需要注册组件,一切自动化。
app/plugins/
插件目录,Nuxt 自动注册——应用启动时执行一次的初始化代码:
// app/plugins/my-plugin.ts
export default defineNuxtPlugin((nuxtApp) => {
// 在这里做全局初始化:注册指令、注入全局方法、监听事件等
})什么时候用插件?
当你需要做一些"全局性"的事情:
- 注册全局 Vue 指令(如
v-focus) - 注入全局方法(如
$formatCurrency) - 监听路由变化(如统计页面访问)
- 初始化第三方库(如 Google Analytics)
app/utils/
工具函数自动导入目录:
// app/utils/formatDate.ts
export const formatDate = (date: Date) => {
return date.toLocaleDateString('zh-CN')
}utils/ 下的函数会在客户端和服务端都可用
(因为它在 app/ 下)。如果你只想在服务端使用的工具,放在 server/utils/ 下。
app.vue
应用根组件,必须包含 <NuxtPage />:
<template>
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template><NuxtPage /> 是什么?
它是一个"占位符",告诉 Nuxt "当前路由对应的页面组件在这里渲染"。类似于 Vue Router 的 <RouterView />。
<NuxtRouteAnnouncer /> 是什么?
无障碍组件,在路由切换时通过屏幕阅读器播报页面变化。视觉上不可见,建议始终保留。
INFO
️ 常见新手错误:在 app.vue 中写业务内容(如导航栏) 导航栏应该放在布局(layouts/default.vue)中,app.vue 只做容器
app.config.ts
应用级配置,构建时确定:
export default defineAppConfig({
theme: {
primaryColor: '#00DC82',
},
})TIP
与 nuxt.config.ts 中的 runtimeConfig 不同 app.config.ts 适合放不变的 UI 配置(如主题色),runtimeConfig 适合放环境相关的配置(如 API 地址)
error.vue
自定义错误页面:
<script setup lang="ts">
const props = defineProps<{
error: { statusCode: number; statusMessage: string; message: string }
}>()
</script>
<template>
<div>
<h1>{{ error.statusCode }}</h1>
<p>{{ error.message }}</p>
<NuxtLink to="/">返回首页</NuxtLink>
</div>
</template>INFO
️ error.vue 的特殊性
- 它不是普通页面,不受布局包裹
- 不受路由中间件保护
- 某些组合式函数在错误页面中可能不可用
- 需要自带完整的 HTML 结构
一张图看懂 app/ 的各目录如何协作
用户访问 /about
│
▼
app.vue(根组件)
│
├── NuxtLayout → layouts/default.vue(布局:导航栏 + 页脚)
│ │
│ ├── <AppHeader /> → components/AppHeader.vue(导航栏组件)
│ │
│ ├── <slot /> → pages/about.vue(页面内容)
│ │ │
│ │ └── composables/useAuth.ts(使用组合式函数)
│ │ └── utils/formatDate.ts(使用工具函数)
│ │
│ └── <AppFooter /> → components/AppFooter.vue(页脚组件)
│
└── middleware/auth.ts(路由守卫,检查登录状态)