0. 写在前面:给 Vue 开发者的定位
Nuxt 之于 Vue,等同于 Next 之于 React:补齐路由、SSR、数据获取、工程化。如果你会 Vue 3 + Composition API,Nuxt 的学习曲线很平缓。
Nuxt 的核心哲学是约定优于配置和自动导入:你几乎不用写 import,组件、composables、Vue API 都被自动注入。这是它和 Next 体感上最大的不同。
注意:Nuxt 的渲染模型与 Next 的 Server/Client Component 不同。Nuxt 默认是"同构(universal)"——同一份组件代码先在服务端渲染出 HTML,再到客户端 hydration 激活,没有 Next 那种显式的服务端/客户端组件之分。这是两个框架最根本的差异。
1. 环境与项目初始化
npm create nuxt@latest my-app
cd my-app
npm install
npm run dev # http://localhost:3000
Nuxt 4 的默认目录结构(注意:Nuxt 4 把源码挪进了 app/ 目录):
my-app/
├─ app/ # Nuxt 4 新增的源码根目录
│ ├─ app.vue # 应用入口(根组件)
│ ├─ pages/ # 文件路由
│ ├─ components/ # 自动导入的组件
│ ├─ composables/ # 自动导入的组合式函数
│ ├─ layouts/ # 布局
│ ├─ middleware/ # 路由中间件
│ └─ plugins/ # 插件
├─ server/ # 服务端代码(API、中间件)—— 仍在根目录
├─ public/ # 静态资源
├─ nuxt.config.ts # 配置
└─ package.json
Nuxt 3 是把
pages/、components/等直接放在项目根。Nuxt 4 默认收进app/,让源码和配置分离。这是 Nuxt 4 的主要结构变更。
2. 入口与路由
2.1 app.vue 与 pages
最简单的应用只要一个 app.vue。一旦你创建了 pages/ 目录,需要在 app.vue 里放 <NuxtPage /> 作为路由出口:
<!-- app/app.vue -->
<template>
<div>
<NuxtLayout>
<NuxtPage /> <!-- 当前路由页面渲染在这里 -->
</NuxtLayout>
</div>
</template>
2.2 文件路由映射
app/pages/
├─ index.vue → /
├─ about.vue → /about
├─ blog/
│ ├─ index.vue → /blog
│ └─ [slug].vue → /blog/:slug (动态)
└─ shop/
└─ [...slug].vue → /shop/a/b/c (捕获所有)
2.3 动态参数
<!-- app/pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute() // useRoute 自动导入,无需 import
const slug = route.params.slug
</script>
<template>
<h1>文章:{{ slug }}</h1>
</template>
2.4 导航
<template>
<!-- 声明式:NuxtLink 自动预取 -->
<NuxtLink to="/blog/hello">去文章</NuxtLink>
</template>
<script setup>
// 编程式
const router = useRouter()
function go() {
router.push('/dashboard')
// 或用 Nuxt 的快捷方法:navigateTo('/dashboard')
}
</script>
3. 自动导入(Nuxt 的招牌特性)
Nuxt 会自动导入这些,你直接用即可:
app/components/下的组件 → 模板里直接<MyButton />app/composables/下的函数 → 直接调用- Vue API:
ref、computed、watch、onMounted等 - Nuxt 内置:
useRoute、useRouter、useFetch、useState、navigateTo…
<script setup lang="ts">
// 注意:下面没有任何 import,全靠自动导入
const count = ref(0)
const double = computed(() => count.value * 2)
</script>
嵌套组件名按目录拼接:components/base/Button.vue → <BaseButton />。
4. 布局
<!-- app/layouts/default.vue -->
<template>
<div>
<header>全站导航</header>
<slot /> <!-- 页面内容插入这里 -->
</div>
</template>
页面指定使用哪个布局:
<!-- app/pages/admin.vue -->
<script setup>
definePageMeta({ layout: 'admin' }) // 使用 layouts/admin.vue
</script>
5. 数据获取(核心)
Nuxt 提供两个组合式函数,专为 SSR 设计,会自动避免"服务端取一次、客户端 hydration 又取一次"的重复请求。
5.1 useFetch —— 最常用
<script setup lang="ts">
const { data, pending, error, refresh } = await useFetch('/api/posts')
</script>
<template>
<div v-if="pending">加载中…</div>
<div v-else-if="error">出错了</div>
<ul v-else>
<li v-for="p in data" :key="p.id">{{ p.title }}</li>
</ul>
</template>
要点:
- 服务端渲染时取数据,把结果序列化传给客户端,客户端不再重复请求。
- 返回的
data、pending都是响应式ref。 - 带参数请求,参数变化会自动重新请求:
const id = ref(1)
const { data } = await useFetch(() => `/api/user/${id.value}`)
// id.value 改变时自动重新 fetch
5.2 useAsyncData —— 包裹任意异步逻辑
当数据来源不是简单的一个 URL(比如调用 SDK、组合多个请求)时用它:
<script setup lang="ts">
const { data } = await useAsyncData('posts', () => {
return $fetch('/api/posts') // $fetch 是 Nuxt 封装的请求工具
})
</script>
第一个参数 'posts' 是缓存 key,Nuxt 用它去重和缓存。
5.3 useFetch vs $fetch vs useAsyncData
| 工具 | 用途 |
|---|---|
useFetch | 组件 setup 中取数据,自动 SSR 去重(首选) |
useAsyncData | 包裹复杂异步逻辑,同样 SSR 去重 |
$fetch | 纯粹发请求(如事件处理),不做 SSR 去重 |
陷阱:不要在 setup 顶层直接用 $fetch 取要渲染的数据——会导致服务端和客户端各请求一次。setup 取数据用 useFetch/useAsyncData;按钮点击等事件里用 $fetch。
6. 服务端引擎 Nitro 与 API 路由
Nuxt 内置服务端引擎 Nitro。在 server/ 目录写后端代码,与前端同一个项目、同一次部署。
6.1 API 路由
// server/api/posts.get.ts —— 文件名后缀指定 HTTP 方法
export default defineEventHandler(async (event) => {
const posts = await db.post.findMany()
return posts // 直接 return,自动 JSON 序列化
})
// server/api/posts.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event) // 读请求体
const created = await db.post.create({ data: body })
return created
})
动态参数:server/api/user/[id].get.ts
export default defineEventHandler((event) => {
const id = getRouterParam(event, 'id')
return getUser(id)
})
前端用 useFetch('/api/posts') 即可调用,类型还能端到端推断。
6.2 服务端中间件
// server/middleware/auth.ts —— 每个请求都会经过
export default defineEventHandler((event) => {
const token = getCookie(event, 'token')
if (!token) {
// 鉴权逻辑
}
})
7. 状态管理:useState
Nuxt 提供 SSR 友好的全局状态(跨组件共享,且能从服务端传到客户端):
// app/composables/useCounter.ts
export const useCounter = () => useState<number>('counter', () => 0)
<script setup>
const counter = useCounter()
</script>
<template>
<button @click="counter++">{{ counter }}</button>
</template>
不要用普通的模块级
ref做全局状态——在 SSR 下会跨请求串数据(用户 A 的状态泄漏给用户 B)。一定用useState。复杂场景可上 Pinia(@pinia/nuxt)。
8. 路由中间件(鉴权)
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const user = useState('user')
if (!user.value) {
return navigateTo('/login') // 重定向
}
})
页面启用:
<script setup>
definePageMeta({ middleware: 'auth' })
</script>
全局中间件:文件名加 .global 后缀(如 auth.global.ts),所有路由自动生效。
9. SEO 与 Meta
<script setup lang="ts">
useHead({
title: '我的网站',
meta: [{ name: 'description', content: '用 Nuxt 构建的站点' }],
})
// 语义化的 SEO 专用 API
useSeoMeta({
title: '文章标题',
ogTitle: '文章标题',
ogImage: '/cover.jpg',
twitterCard: 'summary_large_image',
})
</script>
10. 渲染模式
在 nuxt.config.ts 里可以全局或按路由配置渲染策略:
export default defineNuxtConfig({
// 全局默认 SSR
ssr: true,
// 按路由细分(Nuxt 的路由规则)
routeRules: {
'/': { prerender: true }, // 静态预渲染(SSG)
'/blog/**': { isr: 3600 }, // ISR,每小时再生成
'/admin/**': { ssr: false }, // 纯客户端渲染(SPA)
'/api/**': { cors: true }, // 给 API 加 CORS
'/old': { redirect: '/new' }, // 重定向
},
})
这套 routeRules 是 Nuxt 很强的能力:同一个应用里不同页面可以用不同渲染策略,无需拆项目。
11. 配置与模块生态
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@nuxt/image', // 图片优化(对标 next/image)
'@pinia/nuxt', // 状态管理
'@nuxtjs/tailwindcss',
'@nuxt/content', // Markdown 内容站(写博客极方便)
],
runtimeConfig: {
apiSecret: '', // 仅服务端可见
public: { apiBase: '/api' }, // 客户端也可见
},
})
读取运行时配置:
const config = useRuntimeConfig()
config.public.apiBase // 客户端 OK
config.apiSecret // 仅在 server/ 中可读
Nuxt 的模块生态是它相对 Next 的一大优势:很多功能(图片、PWA、i18n、内容、认证)装个模块就好,开箱即用。
12. 构建与部署
npm run build # SSR 构建,产出 .output/(Nitro 服务)
npm run preview # 本地预览生产构建
npm run generate # 全静态生成(SSG),产出可托管的静态文件
Nitro 的杀手锏是部署目标自适应:同一份代码,通过预设(preset)可部署到 Node、Vercel、Netlify、Cloudflare Workers、Deno 等,通常零改动:
export default defineNuxtConfig({
nitro: { preset: 'cloudflare-pages' } // 切换部署目标
})
13. 学习路线小结
- 接受自动导入——少写 import,熟悉哪些东西是自动可用的。
- 数据获取牢记
useFetch/useAsyncData(setup 取数据)vs$fetch(事件里发请求) 的区别,避免重复请求。 - 全局状态一律用
useState,别用裸ref(SSR 串数据)。 - 善用
server/+ Nitro 写全栈,routeRules按页面定制渲染策略。 - 需要功能先找官方模块,通常已经有现成方案。
生产级补充(纯前端场景:后端是独立服务)
适用场景:后端由独立服务(Java / Go / Node 等)提供,Nuxt 只负责页面渲染和调用现成 API。 因此本节不涉及数据库、服务端认证实现,聚焦前端工程化、API 对接、安全、测试、部署。
14. 对接独立后端 API
14.1 配置 API 基地址
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
apiToken: '', // 仅服务端可见(密钥放这)
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'https://api.example.com',
},
},
})
const config = useRuntimeConfig()
const { data } = await useFetch('/products', { baseURL: config.public.apiBase })
14.2 用 Nitro 做代理 / BFF
即使后端独立,也建议让 Nuxt 的 server/ 充当 BFF 去调真正的后端,而不是浏览器直连。好处:隐藏后端地址、统一加 token、规避跨域。
// server/api/products.get.ts
export default defineEventHandler(async () => {
const config = useRuntimeConfig()
return await $fetch('/products', {
baseURL: config.public.apiBase,
headers: { Authorization: `Bearer ${config.apiToken}` }, // 密钥不进浏览器
})
})
最简单的整段透传可用 Nitro 的 routeRules proxy:
// nuxt.config.ts
routeRules: {
'/backend/**': { proxy: 'https://api.example.com/**' },
}
14.3 封装一个带拦截器的 $fetch
// app/composables/useApi.ts
export const useApi = () => {
const config = useRuntimeConfig()
return $fetch.create({
baseURL: config.public.apiBase,
onRequest({ options }) {
const token = useCookie('token').value
if (token) options.headers.set('Authorization', `Bearer ${token}`)
},
onResponseError({ response }) {
if (response.status === 401) navigateTo('/login')
},
})
}
15. 环境变量与配置
.env # 通过 NUXT_ 前缀映射到 runtimeConfig
NUXT_API_TOKEN=xxx → runtimeConfig.apiToken(仅服务端)
NUXT_PUBLIC_API_BASE=https://… → runtimeConfig.public.apiBase(客户端可见)
铁律:密钥只放 runtimeConfig 顶层(不带 public),它绝不会进客户端 bundle。public 下的值会暴露给浏览器,只放非敏感配置。
16. 错误处理与监控
16.1 全局错误页
<!-- app/error.vue —— 放在 app 根,捕获致命错误 -->
<script setup lang="ts">
const props = defineProps<{ error: { statusCode: number; message: string } }>()
const handleError = () => clearError({ redirect: '/' })
</script>
<template>
<div>
<h1>{{ error.statusCode }}</h1>
<p>{{ error.message }}</p>
<button @click="handleError">返回首页</button>
</div>
</template>
主动抛错:throw createError({ statusCode: 404, message: '找不到', fatal: true })
16.2 接入 Sentry
用官方模块 @sentry/nuxt,在 sentry.client.config.ts 初始化,自动捕获前端报错与性能数据。
17. 安全
| 项 | 做法 |
|---|---|
| 安全响应头 / CSP | 装 nuxt-security 模块,一键配 CSP、CORS、各类安全头、限流 |
| XSS | 避免 v-html;必须用时先净化 |
| token 存储 | 用 useCookie('token', { httpOnly: true, secure: true, sameSite: 'lax' }),别存 localStorage |
| 依赖漏洞 | CI 跑 npm audit / Dependabot |
// nuxt.config.ts
modules: ['nuxt-security'],
security: {
headers: {
contentSecurityPolicy: { 'default-src': ["'self'"] },
xFrameOptions: 'DENY',
},
rateLimiter: { tokensPerInterval: 150, interval: 60000 },
}
18. 测试
# 官方测试工具,封装好 Nuxt 运行时
npm i -D @nuxt/test-utils vitest @vue/test-utils happy-dom
# E2E
npm i -D @playwright/test
组件测试:
// counter.nuxt.test.ts
import { mountSuspended } from '@nuxt/test-utils/runtime'
import { it, expect } from 'vitest'
import Counter from '~/components/Counter.vue'
it('点击递增', async () => {
const wrapper = await mountSuspended(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('1')
})
E2E 用 Playwright 测真实页面流程,生产项目硬要求。
19. 性能与可观测性
-
懒加载组件:组件名加
Lazy前缀即按需加载——<LazyHeavyChart />。 -
Nitro 缓存:
defineCachedEventHandler缓存服务端响应,降后端压力。export default defineCachedEventHandler(async () => { return await $fetch('/heavy-data') }, { maxAge: 60 }) -
图片:用
@nuxt/image的<NuxtImg>/<NuxtPicture>,自动优化与响应式。 -
payload 体积:留意
useState/useAsyncData序列化进 HTML 的数据量,别把大对象塞进去。 -
Web Vitals:可用
nuxt-vitals或在插件里上报 LCP/CLS/INP。
20. 国际化(i18n)
用官方模块 @nuxtjs/i18n:
modules: ['@nuxtjs/i18n'],
i18n: {
locales: ['zh', 'en'],
defaultLocale: 'zh',
}
组件里用 const { t } = useI18n(),模板 {{ $t('home.title') }},自动处理 locale 路由前缀与切换。
21. CI/CD 与部署
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run build
部署:npm run build 产出 .output/(Nitro 服务),通过 nitro.preset 自适应目标平台(Node/Vercel/Cloudflare 等);纯静态站点用 npm run generate。
22. 纯前端生产清单(上线前自查)
- 密钥只在
runtimeConfig顶层(不带public),不进浏览器 - 所有外部 API 调用有错误处理与超时,封装统一拦截器
-
error.vue已配,Sentry 已接 - nuxt-security 配好 CSP 等安全头
- 单测 + 关键流程 E2E 覆盖
- 重组件
Lazy懒加载,payload 体积可控 - 图片用
<NuxtImg> - CI 跑 lint / test / build,绿了才合并
附:Next.js 与 Nuxt 横向对比
| 维度 | Next.js 15 | Nuxt 4 |
|---|---|---|
| 底层框架 | React | Vue 3 |
| 路由 | App Router 文件约定 | pages/ 文件约定 |
| 渲染模型 | Server/Client Component 显式区分 | 同构(universal),无显式区分 |
| 数据获取 | Server Component 直接 await + fetch 缓存 | useFetch / useAsyncData |
| 变更 | Server Actions | server/ API + $fetch |
| 自动导入 | 无(需手动 import) | 有(招牌特性) |
| 服务端 | Route Handlers | Nitro 引擎 |
| 部署 | Vercel 最佳 | Nitro 多平台自适应 |
| 模块生态 | 较少,靠社区库 | 官方模块丰富 |