Skip to content

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/ 的好处:

  1. 文件监听更快:Vite 只监听 app/ 目录,不监听 node_modules/.git/
  2. IDE 上下文更清晰app/ 下全是前端代码,server/ 下全是后端代码,一目了然
  3. 类型隔离更准确app/server/ 使用不同的 TypeScript 项目,互不干扰

Nuxt 3 → Nuxt 4 迁移

如果你用的是旧结构(pages/components/ 在根目录),Nuxt 4 仍然兼容。但推荐迁移到 app/ 结构,获得更好的开发体验。

目录结构

text
app/
├── assets/        # 构建处理的资源(CSS、图片等)
├── components/    # Vue 组件(自动导入)
├── composables/   # 组合式函数(自动导入)
├── layouts/       # 布局组件
├── middleware/     # 路由中间件
├── pages/          # 页面(自动生成路由)
├── plugins/        # 插件
├── utils/          # 工具函数(自动导入)
├── app.vue        # 根组件
├── app.config.ts  # 应用配置
└── error.vue      # 错误页面

如何记忆这个结构?

按照"从前到后"的逻辑:

  1. 用户看到的 → pages/(页面)、layouts/(布局)、components/(组件)
  2. 用户交互的 → middleware/(路由守卫)、composables/(业务逻辑)
  3. 支撑运行的 → plugins/(插件)、utils/(工具)、assets/(资源)
  4. 入口文件 → app.vue(根组件)、error.vue(错误页)、app.config.ts(配置)

子目录详解

app/assets/

存放需要构建工具处理的静态资源:

text
app/assets/
├── css/
│   ├── main.css         # 全局样式
│   └── variables.css    # CSS 变量
├── images/
│   └── logo.png         # 会被 Vite 处理(压缩、哈希文件名)
└── fonts/
    └── custom-font.woff2

在代码中引用:

css
/* app/assets/css/main.css */
@import './variables.css';
vue
<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 组件自动注册目录,组件会按文件名自动导入:

text
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/

组合式函数自动导入目录:

ts
// 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:

vue
<script setup>
const { count, increment } = useCounter()
</script>

composables/ 和 utils/ 的区别

目录命名规则典型内容是否响应式
composables/use 前缀状态管理、数据获取、业务逻辑通常是
utils/无特殊规则格式化、计算、纯函数通常不是

简单判断

如果函数返回响应式数据(ref/computed),放 composables/;如果是纯输入输出的工具函数,放 utils/

app/layouts/

布局组件目录——共享导航栏、页脚等公共 UI 的外壳:

text
app/layouts/
├── default.vue     # 默认布局(所有页面都使用,除非指定其他布局)
├── admin.vue       # 管理后台布局
└── blank.vue       # 空白布局(登录、注册等不需要导航的页面)

布局 vs 组件的区别

布局包裹整个页面,组件嵌在页面内部。你可以把布局理解为"页面模板"——同样的内容,换一个布局就像换了一个"皮肤"。

app/middleware/

路由中间件目录——在页面跳转前执行的检查逻辑:

ts
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const isAuthenticated = useState('isAuthenticated')
  if (!isAuthenticated.value) {
    return navigateTo('/login')  // 未登录?跳转到登录页
  }
})

典型场景

用户没登录就不让看某些页面;管理员才能访问后台;跳转前显示确认对话框。

app/pages/

页面目录,基于文件自动生成路由:

text
app/pages/
├── index.vue           → /
├── about.vue           → /about
├── blog/
│   ├── index.vue       → /blog
│   └── [id].vue        → /blog/:id
└── user/
    └── [name].vue      → /user/:name

这是 Nuxt 最核心的约定

创建一个文件 = 创建一个路由。不需要写路由配置,不需要注册组件,一切自动化。

app/plugins/

插件目录,Nuxt 自动注册——应用启动时执行一次的初始化代码:

ts
// app/plugins/my-plugin.ts
export default defineNuxtPlugin((nuxtApp) => {
  // 在这里做全局初始化:注册指令、注入全局方法、监听事件等
})

什么时候用插件?

当你需要做一些"全局性"的事情:

  • 注册全局 Vue 指令(如 v-focus
  • 注入全局方法(如 $formatCurrency
  • 监听路由变化(如统计页面访问)
  • 初始化第三方库(如 Google Analytics)

app/utils/

工具函数自动导入目录:

ts
// app/utils/formatDate.ts
export const formatDate = (date: Date) => {
  return date.toLocaleDateString('zh-CN')
}

utils/ 下的函数会在客户端和服务端都可用

(因为它在 app/ 下)。如果你只想在服务端使用的工具,放在 server/utils/ 下。

app.vue

应用根组件,必须包含 <NuxtPage />

vue
<template>
  <NuxtRouteAnnouncer />
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

<NuxtPage /> 是什么?

它是一个"占位符",告诉 Nuxt "当前路由对应的页面组件在这里渲染"。类似于 Vue Router 的 <RouterView />

<NuxtRouteAnnouncer /> 是什么?

无障碍组件,在路由切换时通过屏幕阅读器播报页面变化。视觉上不可见,建议始终保留。

INFO

常见新手错误:在 app.vue 中写业务内容(如导航栏) 导航栏应该放在布局(layouts/default.vue)中,app.vue 只做容器

app.config.ts

应用级配置,构建时确定:

ts
export default defineAppConfig({
  theme: {
    primaryColor: '#00DC82',
  },
})

TIP

nuxt.config.ts 中的 runtimeConfig 不同 app.config.ts 适合放不变的 UI 配置(如主题色),runtimeConfig 适合放环境相关的配置(如 API 地址)

error.vue

自定义错误页面:

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/ 的各目录如何协作

text
用户访问 /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(路由守卫,检查登录状态)

基于 Nuxt 4 官方文档整理编写