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 3 | Nuxt 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.vue | app/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 → <VideoVideoCard />。
要去掉路径前缀,在 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 后缀
执行规则:
.global.ts后缀的中间件在每次路由切换时自动执行- 无
.global后缀的中间件需要在页面中通过definePageMeta显式引用 - 执行顺序:全局中间件(按文件名排序) → 页面指定的中间件
// 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 prepare 或 nuxt 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/vsassets/:构建处理 vs 原样复制,按需选择nuxt.config.ts:所有配置的枢纽,按模块/渲染/服务端/构建/应用分组.nuxt/:自动生成的类型声明,遇到类型问题先跑nuxt prepare
下一章,我们将深入 TypeScript 在 Nuxt4 中的全量实践,掌握零配置类型安全的精髓。