Nuxt 4 详细开发教程

6 阅读4分钟

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:refcomputedwatchonMounted
  • Nuxt 内置:useRouteuseRouteruseFetchuseStatenavigateTo
<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>

要点:

  • 服务端渲染时取数据,把结果序列化传给客户端,客户端不再重复请求。
  • 返回的 datapending 都是响应式 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. 学习路线小结

  1. 接受自动导入——少写 import,熟悉哪些东西是自动可用的。
  2. 数据获取牢记 useFetch/useAsyncData(setup 取数据)vs $fetch(事件里发请求) 的区别,避免重复请求。
  3. 全局状态一律用 useState,别用裸 ref(SSR 串数据)。
  4. 善用 server/ + Nitro 写全栈,routeRules 按页面定制渲染策略。
  5. 需要功能先找官方模块,通常已经有现成方案。


生产级补充(纯前端场景:后端是独立服务)

适用场景:后端由独立服务(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. 安全

做法
安全响应头 / CSPnuxt-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 15Nuxt 4
底层框架ReactVue 3
路由App Router 文件约定pages/ 文件约定
渲染模型Server/Client Component 显式区分同构(universal),无显式区分
数据获取Server Component 直接 await + fetch 缓存useFetch / useAsyncData
变更Server Actionsserver/ API + $fetch
自动导入无(需手动 import)有(招牌特性)
服务端Route HandlersNitro 引擎
部署Vercel 最佳Nitro 多平台自适应
模块生态较少,靠社区库官方模块丰富