Nuxt4 目录结构深度解析

Nuxt4 的"约定优于配置"哲学完全体现在目录结构中——每个目录都是一种约定,每个文件位置都决定了它的行为。本章逐目录深入解析,帮你建立"看到目录就知道该写什么、怎么写"的直觉。

1. 全局视角:Nuxt4 完整目录树

1.1 AI 视频项目完整目录结构

ai-video-app/
├── app/                         # 🟢 前端运行时(浏览器 + SSR 水合)
│   ├── app.vue                  #   应用入口组件
│   ├── error.vue                #   全局错误页面
│   ├── pages/                   #   页面路由(自动生成路由表)
│   ├── components/              #   组件(自动注册)
│   ├── composables/             #   Composables(自动导入)
│   ├── layouts/                 #   布局组件(自动注册)
│   ├── middleware/              #   路由中间件(自动注册)
│   ├── plugins/                 #   插件(自动注册)
│   ├── assets/                  #   需构建处理的资源(CSS/图片/字体)
│   └── utils/                   #   工具函数(自动导入)
├── server/                      # 🔵 服务端运行时(Nitro 引擎)
│   ├── api/                     #   API 路由(/api/ 前缀)
│   ├── routes/                  #   服务端路由(无前缀)
│   ├── middleware/              #   服务端中间件(全局拦截)
│   ├── plugins/                 #   Nitro 插件(服务端初始化)
│   ├── utils/                   #   服务端工具(自动导入)
│   └── tasks/                   #   定时任务
├── shared/                      # 🟡 跨端共享(app/ 和 server/ 都可引用)
│   ├── types/                   #   共享类型定义
│   └── utils/                   #   共享工具函数
├── public/                      # 📁 静态文件(原样复制,不经构建处理)
│   ├── favicon.ico
│   ├── robots.txt
│   └── og-image.png
├── .nuxt/                       # ⚙️ 自动生成(gitignore,勿手动修改)
├── .output/                     # 📦 构建产物(gitignore)
├── nuxt.config.ts               # Nuxt 核心配置
├── app.config.ts                # 应用运行时配置
├── tsconfig.json                # TypeScript 配置(自动生成)
└── package.json

1.2 Nuxt3 → Nuxt4 目录对比

Nuxt 3Nuxt 4变化说明
pages/app/pages/移入 app/ 目录
components/app/components/移入 app/ 目录
composables/app/composables/移入 app/ 目录
layouts/app/layouts/移入 app/ 目录
middleware/app/middleware/移入 app/ 目录
plugins/app/plugins/移入 app/ 目录
app.vueapp/app.vue移入 app/ 目录
shared/新增:跨端共享目录
server/server/不变
public/public/不变

核心变化:前端代码全部收入 app/,实现 app(前端)/ server(后端)/ shared(共享)的三层架构分离。

2. app/ 目录:前端分层设计

2.1 pages/ — 文件系统路由

app/pages/ 下的每个 .vue 文件自动生成对应路由:

app/pages/
├── index.vue                    # → /
├── about.vue                    # → /about
├── videos/
│   ├── index.vue                # → /videos
│   ├── [id].vue                 # → /videos/:id (动态路由)
│   └── [id]/
│       └── edit.vue             # → /videos/:id/edit (嵌套动态路由)
├── studio/
│   └── [...slug].vue            # → /studio/*   (catch-all 路由)
├── (auth)/                      # → 路由分组,不产生 URL 前缀
│   ├── login.vue                # → /login
│   └── register.vue             # → /register
└── admin/
    └── index.vue                # → /admin

命名约定

  • index.vue → 目录默认页
  • [param].vue → 动态参数
  • [[param]].vue → 可选参数
  • [...slug].vue → 捕获所有
  • (group)/ → 分组(不影响 URL)

2.2 components/ — 自动注册组件

app/components/
├── AppHeader.vue                # <AppHeader />
├── AppFooter.vue                # <AppFooter />
├── video/
│   ├── VideoCard.vue            # <VideoVideoCard /> 或 <VideoCard />
│   ├── VideoPlayer.vue          # <VideoVideoPlayer />
│   └── VideoList.vue            # <VideoVideoList />
├── ui/
│   ├── UiButton.vue             # <UiButton />
│   └── UiModal.vue              # <UiModal />
└── global/                      # global/ 下的组件全局注册
    └── NuxtLogo.vue             # 在所有组件中可用,包括 server components

命名规则:组件名 = 路径前缀 + 文件名。例如 video/VideoCard.vue&lt;VideoVideoCard /&gt;

要去掉路径前缀,在 nuxt.config.ts 中配置:

export default defineNuxtConfig({
  components: [
    { path: '~/components/video', pathPrefix: false },
    // VideoCard.vue → <VideoCard /> 而不是 <VideoVideoCard />
  ]
})

2.3 composables/ — 自动导入的 Composable

app/composables/
├── useAuth.ts                   # 自动导入:useAuth()
├── useVideoPlayer.ts            # 自动导入:useVideoPlayer()
├── useAIGenerate.ts             # 自动导入:useAIGenerate()
└── useTheme.ts                  # 自动导入:useTheme()

自动导入规则

  • 只扫描 composables/顶层文件
  • 文件中的命名导出默认导出都会被自动导入
  • 子目录中的文件不会被自动扫描(除非在 nuxt.config.ts 中配置)
// app/composables/useAuth.ts
export function useAuth() {
  const user = useState<User | null>('user', () => null)
  const isLoggedIn = computed(() => !!user.value)
 
  async function login(credentials: LoginInput) {
    const data = await $fetch('/api/auth/login', {
      method: 'POST',
      body: credentials,
    })
    user.value = data.user
  }
 
  function logout() {
    user.value = null
    navigateTo('/login')
  }
 
  return { user, isLoggedIn, login, logout }
}

在任何页面或组件中直接使用,无需 import:

<script setup lang="ts">
const { user, isLoggedIn, logout } = useAuth()
</script>

2.4 layouts/ — 布局组件

app/layouts/
├── default.vue                  # 默认布局
├── admin.vue                    # 管理后台布局
├── auth.vue                     # 登录/注册页布局(无导航栏)
└── studio.vue                   # 创作工作台布局
<!-- app/layouts/default.vue -->
<template>
  <div class="min-h-screen flex flex-col">
    <AppHeader />
    <main class="flex-1">
      <slot />  <!-- 页面内容插入此处 -->
    </main>
    <AppFooter />
  </div>
</template>

页面中指定布局:

<script setup lang="ts">
definePageMeta({
  layout: 'admin',  // 使用 admin 布局
})
</script>

2.5 middleware/ — 路由中间件

app/middleware/
├── auth.ts                      # 命名中间件 — 页面显式引用
├── admin.ts                     # 命名中间件 — 管理员权限检查
└── analytics.global.ts          # 全局中间件 — .global 后缀

执行规则

  1. .global.ts 后缀的中间件在每次路由切换时自动执行
  2. .global 后缀的中间件需要在页面中通过 definePageMeta 显式引用
  3. 执行顺序:全局中间件(按文件名排序) → 页面指定的中间件
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { isLoggedIn } = useAuth()
  if (!isLoggedIn.value) {
    return navigateTo('/login')
  }
})

2.6 plugins/ — 插件系统

app/plugins/
├── 01.analytics.client.ts       # 仅客户端执行
├── 02.sentry.ts                 # 前后端都执行
└── 03.toast.client.ts           # 仅客户端执行

命名约定

  • .client.ts → 仅在客户端执行
  • .server.ts → 仅在服务端执行
  • 无后缀 → 两端都执行
  • 数字前缀控制执行顺序(01.02.03.

2.7 assets/ vs utils/

app/assets/                      # 需要 Vite 构建处理的资源
├── css/
│   └── main.css                 # 全局 CSS(Tailwind @import)
├── images/
│   └── logo.svg                 # 会被 hash + 压缩
└── fonts/
    └── inter.woff2              # 字体文件

app/utils/                       # 工具函数(自动导入)
├── formatDate.ts                # 自动导入:formatDate()
├── formatDuration.ts            # 自动导入:formatDuration()
└── validators.ts                # 自动导入:命名导出的函数

utils/composables/ 的区别:utils/ 用于纯函数工具,composables/ 用于使用 Vue 响应式 API 的组合函数。

3. server/ 目录:后端职责边界

3.1 整体结构

server/
├── api/                         # API 路由(自动添加 /api/ 前缀)
│   ├── auth/
│   │   ├── login.post.ts        # POST /api/auth/login
│   │   └── me.get.ts            # GET  /api/auth/me
│   └── videos/
│       ├── index.get.ts         # GET  /api/videos
│       ├── index.post.ts        # POST /api/videos
│       └── [id].get.ts          # GET  /api/videos/:id
├── routes/                      # 服务端路由(无前缀)
│   └── sitemap.xml.ts           # GET  /sitemap.xml
├── middleware/                   # 服务端中间件(请求级拦截)
│   ├── 01.cors.ts               # CORS 跨域处理
│   ├── 02.logger.ts             # 请求日志
│   └── 03.auth.ts               # Token 验证
├── plugins/                     # Nitro 插件(服务端启动时初始化)
│   ├── db.ts                    # 数据库连接池
│   └── redis.ts                 # Redis 客户端初始化
├── utils/                       # 服务端工具(自动导入)
│   ├── db.ts                    # Drizzle ORM 实例
│   ├── redis.ts                 # Redis 客户端
│   └── jwt.ts                   # JWT 工具函数
└── tasks/                       # 定时任务
    └── cleanup-expired.ts       # 清理过期数据

3.2 api/ vs routes/ 的关键区别

特性server/api/server/routes/
URL 前缀自动添加 /api/无前缀
典型用途JSON API 接口sitemap、RSS、webhook
HTTP 方法文件后缀指定(.get.ts.post.ts同左

3.3 server/ 与 app/ 的运行时隔离

这是初学者最常犯错的地方:server/app/ 是两个完全独立的运行时

// ❌ 错误:在 server/ 中使用 Vue API
// server/api/videos.get.ts
import { ref } from 'vue'  // 💥 错误!server/ 中没有 Vue 运行时
 
// ❌ 错误:在 app/ 中直接访问数据库
// app/pages/videos.vue
import { db } from '~/server/utils/db'  // 💥 错误!app/ 无法引用 server/
 
// ✅ 正确:app/ 通过 HTTP 请求访问 server/
// app/pages/videos.vue
const { data } = await useFetch('/api/videos')

唯一例外是 shared/ 目录——它可以同时被 app/server/ 引用。

4. public/ vs assets/:静态资源的两种处理方式

4.1 本质差异

特性public/app/assets/
构建处理❌ 原样复制到 .output/public/✅ Vite 处理(hash、压缩、转换)
URL 引用/favicon.ico构建后路径如 /_nuxt/logo.abc123.svg
缓存策略手动控制自动 hash → 强缓存(immutable)
适用场景favicon、robots.txt、OG 图片CSS、JS 引用的图片、字体

4.2 决策流程

资源是否需要构建处理(压缩/hash/格式转换)?
├── 是 → 放 assets/
│   ├── CSS/Sass 文件 → assets/css/
│   ├── 组件引用的图片 → assets/images/
│   └── 字体文件 → assets/fonts/
└── 否 → 放 public/
    ├── favicon.ico → public/
    ├── robots.txt → public/
    ├── OG 分享图 → public/  (社交平台需要固定 URL)
    └── 第三方 SDK 的静态文件 → public/

4.3 引用方式对比

<template>
  <!-- public/ 中的资源 — 直接用绝对路径 -->
  <img src="/og-image.png" />
  <link rel="icon" href="/favicon.ico" />
 
  <!-- assets/ 中的资源 — 用 ~ 别名 -->
  <img src="~/assets/images/logo.svg" />
</template>
 
<style>
/* assets/ 中的字体 */
@font-face {
  font-family: 'Inter';
  src: url('~/assets/fonts/inter.woff2') format('woff2');
}
</style>

5. shared/ 目录:跨端共享策略

5.1 设计意图

shared/ 是 Nuxt4 新增的目录,解决了一个长期痛点:前端和后端需要共享类型定义和纯函数工具。

shared/
├── types/
│   ├── video.ts                 # 视频相关类型
│   ├── user.ts                  # 用户相关类型
│   └── api.ts                   # API 请求/响应类型
└── utils/
    ├── formatDuration.ts        # 时长格式化(前后端都用)
    ├── validators.ts            # 数据校验(前后端都用)
    └── constants.ts             # 共享常量

5.2 使用示例

// shared/types/video.ts
export interface Video {
  id: string
  title: string
  description: string
  url: string
  thumbnailUrl: string
  duration: number          // 秒
  status: 'processing' | 'ready' | 'failed'
  createdAt: string
}
 
export interface CreateVideoInput {
  prompt: string
  style: 'realistic' | 'anime' | 'cinematic'
  duration: 5 | 10 | 15     // 生成时长
}
// shared/utils/formatDuration.ts
export function formatDuration(seconds: number): string {
  const mins = Math.floor(seconds / 60)
  const secs = seconds % 60
  return `${mins}:${secs.toString().padStart(2, '0')}`
}

app/server/ 中都可以直接引用:

// server/api/videos/[id].get.ts
import type { Video } from '~/shared/types/video'
 
// app/pages/videos/[id].vue
// import { formatDuration } from '~/shared/utils/formatDuration'
// 或者由于自动导入,直接使用 formatDuration()

5.3 重要约束

shared/ 中的代码必须是环境无关的纯 ESM

// ❌ 错误:引用浏览器专有 API
export function saveToLocalStorage(key: string, value: string) {
  localStorage.setItem(key, value)  // 💥 server/ 中没有 localStorage
}
 
// ❌ 错误:引用 Node.js 专有 API
import { readFileSync } from 'node:fs'  // 💥 浏览器中没有 fs
 
// ✅ 正确:纯函数 + 标准 ESM
export function slugify(text: string): string {
  return text.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, '')
}

6. nuxt.config.ts 核心配置精讲

6.1 配置分组总览

// nuxt.config.ts — AI 视频项目完整配置
export default defineNuxtConfig({
  // ── 模块 ──
  modules: [
    '@nuxt/ui',
    '@nuxt/image',
    '@nuxt/fonts',
    '@pinia/nuxt',
    '@nuxtjs/i18n',
    '@sidebase/nuxt-auth',
  ],
 
  // ── 全局 CSS ──
  css: ['~/assets/css/main.css'],
 
  // ── 运行时配置 ──
  runtimeConfig: {
    aiApiKey: '',
    redisUrl: '',
    public: {
      appName: 'AI 视频平台',
      apiBase: '/api',
    },
  },
 
  // ── 路由规则(混合渲染) ──
  routeRules: {
    '/': { prerender: true },
    '/videos': { swr: 3600 },
    '/videos/**': { ssr: true },
    '/studio/**': { ssr: false },
    '/admin/**': { ssr: false },
  },
 
  // ── Nitro 服务引擎 ──
  nitro: {
    preset: 'node-server',        // 部署目标
    storage: {
      redis: {
        driver: 'redis',
        url: process.env.NUXT_REDIS_URL,
      },
    },
    experimental: {
      websocket: true,            // 启用 WebSocket 支持
      tasks: true,                // 启用定时任务
    },
  },
 
  // ── Vite 构建配置 ──
  vite: {
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            'video-player': ['hls.js'],  // 播放器单独分包
          },
        },
      },
    },
  },
 
  // ── 应用配置 ──
  app: {
    head: {
      title: 'AI 视频平台',
      htmlAttrs: { lang: 'zh-CN' },
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      ],
      link: [
        { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      ],
    },
  },
 
  // ── 实验性特性 ──
  experimental: {
    typedPages: true,             // 路由参数类型安全
  },
 
  // ── 开发工具 ──
  devtools: { enabled: true },
 
  // ── TypeScript ──
  typescript: {
    strict: true,
    typeCheck: true,
  },
})

6.2 关键配置详解

modules — 模块注册顺序有意义,部分模块依赖前序模块的功能。

routeRules — Nuxt4 最强大的配置之一,支持通配符 **,可以为不同路由设置不同的渲染策略、缓存头、CORS 等。

nitro.storage — 配置 Nitro 的存储后端,支持 memory、fs、redis、cloudflare-kv 等多种驱动。

experimental.typedPages — 启用后,useRoute().params 的类型会根据文件名自动推导。

7. .nuxt/ 目录:理解自动生成的黑盒

7.1 目录结构

运行 nuxt preparenuxt dev 后,会生成 .nuxt/ 目录:

.nuxt/
├── types/                       # 类型声明文件
│   ├── imports.d.ts             # 自动导入的 API 类型
│   ├── components.d.ts          # 自动注册的组件类型
│   ├── layouts.d.ts             # 布局类型
│   ├── middleware.d.ts          # 中间件类型
│   ├── nitro.d.ts               # Nitro API 类型
│   └── plugins.d.ts             # 插件类型
├── tsconfig.json                # TypeScript 配置(引用 types/)
├── nuxt.d.ts                    # Nuxt 增强类型
└── app.config.mjs               # 运行时 app.config 编译产物

7.2 为什么不应该手动修改

.nuxt/ 目录是完全自动生成的:

  • 每次 nuxt dev 启动时重新生成
  • 每次 nuxt build 构建时重新生成
  • 文件变更(添加页面/组件/Composable)时自动更新

手动修改 .nuxt/ 中的文件会被下次构建覆盖,因此:

# .gitignore 中已包含
.nuxt/

7.3 nuxt prepare 的作用

当你的 IDE 报"找不到类型"时,通常需要运行:

pnpm nuxt prepare

这会重新生成 .nuxt/types/ 下的所有类型声明文件,让 IDE 正确识别自动导入的 API、组件和 Composable。

什么时候需要手动运行 nuxt prepare

  • 首次 git clone 项目后
  • 切换分支后
  • 手动清理了 .nuxt/
  • IDE 类型提示异常时

本章小结

本章深入解析了 Nuxt4 的每一层目录结构:

  • app/:前端代码的家,7 个子目录各有职责,通过文件位置和命名约定实现自动化
  • server/:Nitro 服务端代码,与 app/ 运行时隔离,通过 HTTP 通信
  • shared/:Nuxt4 新增的跨端共享层,仅放环境无关的纯 ESM 代码
  • public/ vs assets/:构建处理 vs 原样复制,按需选择
  • nuxt.config.ts:所有配置的枢纽,按模块/渲染/服务端/构建/应用分组
  • .nuxt/:自动生成的类型声明,遇到类型问题先跑 nuxt prepare

下一章,我们将深入 TypeScript 在 Nuxt4 中的全量实践,掌握零配置类型安全的精髓。