Nuxt 4 学习文档

0 阅读18分钟

Nuxt 4 学习文档(案例驱动)

目标:以大功能点为章节、每个知识点配套详细案例与代码,帮助具备 Vue 3 基础的工程师系统掌握 Nuxt 4(思路兼容 Nuxt 3)。

目录

    1. 项目初始化与目录约定
    1. 路由与导航
    1. 数据获取与渲染模式
    1. 组件与布局
    1. 状态管理(Pinia)
    1. 组合式 API 与可复用逻辑
    1. 插件与模块生态
    1. 服务端开发(Nitro)
    1. 运行时配置与环境变量
    1. SEO 与元信息
    1. 内容系统(Nuxt Content)
    1. 国际化(i18n)
    1. 静态资源与图片优化
    1. 样式与构建工具
    1. 安全与权限
    1. 测试与质量保障
    1. 部署与运维
    1. 性能优化
    1. 开发者工具与调试

1. 项目初始化与目录约定

1.1 使用 nuxi 创建项目与启动开发

知识点:使用官方脚手架创建 Nuxt 4 项目,了解基础命令。

案例:从零创建 nuxt4-app 并运行

步骤:

  1. 安装最新 nuxi(或使用 npx 直接调用)
  2. 创建项目并选择 TypeScript
  3. 启动开发服务器,访问本地地址
# 使用 npx(无需全局安装)
npx nuxi@latest init nuxt4-app
cd nuxt4-app
# 推荐使用 pnpm,也可用 npm 或 yarn
pnpm install
pnpm dev
# 终端输出本地预览地址,例如 http://localhost:3000

项目创建后,默认文件结构示例:

nuxt4-app/
├─ app.vue
├─ nuxt.config.ts
├─ pages/
├─ components/
├─ composables/
├─ server/
├─ plugins/
├─ middleware/
├─ assets/
├─ public/
└─ package.json

验证:

  • 打开浏览器访问本地地址,看到默认欢迎页

常见坑:

  • Node 版本过低导致依赖安装失败;建议 Node 18+。

1.2 nuxt.config 基础与类型提示

知识点:掌握 nuxt.config.ts 的基本配置项与类型提示。

案例:配置站点标题与图标

nuxt.config.ts 中设置 app.head

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      title: 'Nuxt4 学习文档示例站',
      meta: [{ name: 'description', content: '案例驱动的 Nuxt4 学习文档' }],
      link: [{ rel: 'icon', type: 'image/png', href: '/favicon.png' }]
    }
  },
  typescript: {
    strict: true
  }
})

验证:

  • 启动项目后查看页面标题与 Favicon 是否生效。

最佳实践:

  • 使用 TypeScript,开启 typescript.strict 获得更好类型提示。

1.3 约定式目录与最小博客骨架

知识点:理解 Nuxt 的约定式目录与页面、组件、服务端的组织方式。

案例:搭建最小博客骨架(首页/文章页)

创建首页与文章详情页:

<!-- pages/index.vue -->
<template>
  <section class="container">
    <h1>我的博客</h1>
    <ul>
      <li v-for="post in posts" :key="post.id">
        <NuxtLink :to="`/posts/${post.id}`">{{ post.title }}</NuxtLink>
      </li>
    </ul>
  </section>
</template>

<script setup lang="ts">
const posts = [
  { id: 1, title: 'Nuxt4 入门与目录约定' },
  { id: 2, title: '路由与导航详解' }
]
</script>

<style scoped>
.container { max-width: 720px; margin: 40px auto; }
</style>
<!-- pages/posts/[id].vue -->
<template>
  <article class="container">
    <NuxtLink to="/">← 返回首页</NuxtLink>
    <h1>{{ post?.title }}</h1>
    <p>文章 ID:{{ id }}</p>
    <p>这里是文章内容示例……</p>
  </article>
</template>

<script setup lang="ts">
const route = useRoute()
const id = computed(() => route.params.id)
const post = computed(() => {
  const map: Record<string, { title: string }> = {
    '1': { title: 'Nuxt4 入门与目录约定' },
    '2': { title: '路由与导航详解' }
  }
  return map[id.value as string]
})
</script>

<style scoped>
.container { max-width: 720px; margin: 40px auto; }
</style>

添加基础组件与样式:

<!-- components/BaseHeader.vue -->
<template>
  <header class="header">
    <NuxtLink to="/" class="logo">Nuxt4 Docs</NuxtLink>
    <nav>
      <NuxtLink to="/" class="nav">首页</NuxtLink>
      <NuxtLink to="/about" class="nav">关于</NuxtLink>
    </nav>
  </header>
</template>

<style scoped>
.header { display:flex; align-items:center; gap:16px; padding:16px; border-bottom:1px solid #eee; }
.logo { font-weight:bold; }
.nav { margin-right: 8px; }
</style>

将组件挂到应用根:

<!-- app.vue -->
<template>
  <BaseHeader />
  <NuxtPage />
</template>

验证:

  • 首页展示文章列表,可点击进入详情页
  • 顶部导航与样式正常

最佳实践:

  • pages 用于路由页面,components 存放可复用视图组件
  • 将公共结构放入 app.vue 或布局(layouts)

下一章将从“路由与导航”开始,深入讲解动态/嵌套路由与中间件,配合完整示例持续扩展本示例站点。


2. 路由与导航

2.1 约定式路由:动态/可选/捕获/嵌套

知识点:Nuxt 的 pages 目录根据文件命名自动生成路由。

案例:动态、可选与捕获路由

pages/
├─ users/
│  ├─ [id].vue         # 动态参数 /users/123
│  ├─ [[tab]].vue      # 可选参数 /users 或 /users/profile
│  └─ [...slug].vue    # 捕获所有 /users/a/b/c
└─ users/[id]/settings.vue  # 动态 + 嵌套路由 /users/123/settings

示例页面:

<!-- pages/users/[id].vue -->
<template>
  <section class="container">
    <h2>用户:{{ id }}</h2>
    <NuxtLink :to="`/users/${id}/settings`">进入设置</NuxtLink>
  </section>
</template>
<script setup lang="ts">
const route = useRoute()
const id = computed(() => route.params.id)
</script>
<!-- pages/users/[id]/settings.vue -->
<template>
  <section class="container">
    <NuxtLink :to="`/users/${id}`">← 返回</NuxtLink>
    <h3>设置中心</h3>
    <p>这里是用户 {{ id }} 的设置页</p>
  </section>
</template>
<script setup lang="ts">
const route = useRoute()
const id = computed(() => route.params.id)
</script>

验证:

  • 访问 /users/1/users/1/settings,路由与参数正常

2.2 页面导航与编程式跳转

知识点:使用 <NuxtLink>useRouter() 进行导航。

案例:分页列表导航与编程式跳转

<!-- pages/list.vue -->
<template>
  <section class="container">
    <h2>文章列表 - 第 {{ page }} 页</h2>
    <nav class="pager">
      <NuxtLink :to="`/list?page=${Number(page)-1}`" v-if="Number(page)>1">上一页</NuxtLink>
      <NuxtLink :to="`/list?page=${Number(page)+1}`">下一页</NuxtLink>
      <button @click="goDetail(42)">编程式跳转到文章 42</button>
    </nav>
  </section>
</template>
<script setup lang="ts">
const route = useRoute()
const router = useRouter()
const page = computed(() => route.query.page ?? '1')
function goDetail(id: number) {
  router.push(`/posts/${id}`)
}
</script>
<style scoped>
.pager { display:flex; gap:12px; align-items:center; }
</style>

验证:

  • 切换分页链接正常
  • 点击按钮编程式跳转到 /posts/42

2.3 路由中间件与重定向

知识点:路由中间件在进入页面前执行,可用于权限校验或重定向。

案例:需要登录的受保护页面与 302 重定向

创建路由中间件:

// middleware/auth.global.ts  全局中间件(文件名以 .global)
export default defineNuxtRouteMiddleware((to, from) => {
  const isLoggedIn = useCookie('logged_in').value === '1'
  if (!isLoggedIn && to.path.startsWith('/admin')) {
    return navigateTo('/login', { redirectCode: 302 })
  }
})

受保护路由示例:

<!-- pages/admin/index.vue -->
<template>
  <section class="container">
    <h2>后台管理</h2>
    <p>只有登录用户可访问</p>
  </section>
</template>

登录页简单实现:

<!-- pages/login.vue -->
<template>
  <section class="container">
    <h2>登录</h2>
    <button @click="login">点击登录并跳转后台</button>
  </section>
</template>
<script setup lang="ts">
function login() {
  const cookie = useCookie('logged_in')
  cookie.value = '1'
  navigateTo('/admin')
}
</script>

验证:

  • 未登录访问 /admin 自动重定向到 /login
  • 登录后访问 /admin 正常进入

最佳实践:

  • 使用 middleware/*.global.ts 处理全局策略
  • 需要仅针对某页面的策略可在页面 definePageMeta({ middleware: 'xxx' })

3. 数据获取与渲染模式

3.1 useFetch / useAsyncData / $fetch

知识点:数据获取的三种常用方式与差异。

案例:SSR 获取文章列表 + 客户端增量刷新

服务端接口(模拟):

// server/api/posts.get.ts
export default defineEventHandler(async (event) => {
  // 模拟数据库查询
  return [
    { id: 1, title: 'Nuxt4 入门与目录约定' },
    { id: 2, title: '路由与导航详解' },
    { id: 3, title: '数据获取与渲染模式' }
  ]
})

页面使用 useAsyncData

<!-- pages/fetch.vue -->
<template>
  <section class="container">
    <h2>文章列表(SSR 首屏)</h2>
    <ul v-if="data">
      <li v-for="p in data" :key="p.id">
        <NuxtLink :to="`/posts/${p.id}`">{{ p.title }}</NuxtLink>
      </li>
    </ul>
    <p v-else>加载中...</p>
    <button @click="refresh">客户端刷新</button>
  </section>
</template>
<script setup lang="ts">
const { data, pending, error, refresh } = await useAsyncData('posts', () => $fetch('/api/posts'))
</script>

在某组件中使用 useFetch(自动处理 SSR/CSR):

<!-- components/PostCounter.vue -->
<template>
  <div>当前文章总数:{{ count ?? '-' }}</div>
</template>
<script setup lang="ts">
const { data } = await useFetch('/api/posts')
const count = computed(() => data.value?.length)
</script>

验证:

  • 首次访问 pages/fetch.vue SSR 渲染列表
  • 点击“客户端刷新”会重新请求数据并更新视图

3.2 渲染模式:SSR、CSR、混合、预渲染

知识点:理解不同渲染模式的取舍与 Nuxt 支持。

案例:对比同页面在不同模式下的表现

<!-- pages/modes.vue -->
<template>
  <section class="container">
    <h2>渲染模式实验</h2>
    <p>当前时间(服务端或客户端):{{ now }}</p>
  </section>
</template>
<script setup lang="ts">
const now = ref<string>('')
if (process.server) {
  now.value = `SSR: ${new Date().toISOString()}`
} else {
  now.value = `CSR: ${new Date().toISOString()}`
}
</script>

注:

  • 预渲染(静态生成)可通过 nuxi build + nuxi generate(视具体版本命令)生成静态 HTML
  • 混合渲染常见于部分页面 SSR、部分纯客户端

3.3 缓存与错误处理

知识点:给数据获取设置缓存键、处理错误与加载态。

案例:带缓存键的 useAsyncData 与骨架屏

<!-- pages/cache.vue -->
<template>
  <section class="container">
    <h2>缓存示例</h2>
    <div v-if="pending" class="skeleton">加载中(骨架屏)...</div>
    <ul v-else-if="data">
      <li v-for="p in data" :key="p.id">{{ p.title }}</li>
    </ul>
    <p v-else-if="error">发生错误:{{ error.message }}</p>
    <button @click="refresh">重新拉取</button>
  </section>
</template>
<script setup lang="ts">
const { data, pending, error, refresh } = await useAsyncData(
  // 缓存键
  'posts-cache',
  // 获取函数
  () => $fetch('/api/posts'),
  // 可选配置
  { default: () => [], server: true, lazy: false }
)
</script>
<style scoped>
.skeleton { height: 120px; background: #f5f5f5; animation: pulse 1.2s infinite; }
@keyframes pulse { 0%{opacity:.6} 50%{opacity:1} 100%{opacity:.6} }
</style>

最佳实践:

  • useAsyncData 设置合理的 key 以启用缓存与避免重复请求
  • 统一处理 pending/error,提供良好的用户体验

4. 组件与布局

4.1 自动导入组件与目录组织

知识点:Nuxt 自动导入 components/ 下的组件,无需手动注册。

案例:建立 BaseButton 并在多页面复用

<!-- components/BaseButton.vue -->
<template>
  <button class="btn" :class="variant">
    <slot />
  </button>
</template>
<script setup lang="ts">
defineProps<{ variant?: 'primary' | 'secondary' }>()
</script>
<style scoped>
.btn { padding:8px 12px; border-radius:6px; }
.primary { background:#0ea5e9; color:#fff; }
.secondary { background:#eee; color:#333; }
</style>

在任意页面直接使用:

<!-- pages/about.vue -->
<template>
  <section class="container">
    <h2>关于页面</h2>
    <BaseButton variant="primary">立即体验</BaseButton>
  </section>
</template>

4.2 布局(layouts)与错误页(error.vue)

知识点:使用布局统一页面框架与导航;使用错误页统一异常展示。

案例:默认布局与自定义错误页

<!-- layouts/default.vue -->
<template>
  <div>
    <BaseHeader />
    <main class="main">
      <slot />
    </main>
    <footer class="footer">© 2026 Nuxt4 Docs</footer>
  </div>
</template>
<style scoped>
.main { max-width: 960px; margin: 24px auto; min-height: 60vh; }
.footer { border-top: 1px solid #eee; padding: 16px; text-align: center; color:#666; }
</style>

错误页:

<!-- error.vue -->
<template>
  <section class="container">
    <h2>发生错误</h2>
    <p>{{ error.message }}</p>
    <NuxtLink to="/">返回首页</NuxtLink>
  </section>
</template>
<script setup lang="ts">
const props = defineProps<{ error: { message: string } }>()
const error = toRef(props, 'error')
</script>

验证:

  • 页面自动套用默认布局
  • 抛出错误时统一由 error.vue 捕获展示

4.3 插槽与跨布局状态

知识点:通过插槽构建可扩展布局;使用 useState 保持跨页面/布局状态。

案例:跨页公告栏与可插槽的主布局

// composables/useBanner.ts
export const useBanner = () => useState<string>('global-banner', () => '')
<!-- layouts/default.vue(片段,加入公告栏插槽) -->
<template>
  <div>
    <BaseHeader />
    <div v-if="banner" class="banner">{{ banner }}</div>
    <main class="main"><slot /></main>
    <footer class="footer">© 2026 Nuxt4 Docs</footer>
  </div>
</template>
<script setup lang="ts">
const banner = useBanner()
</script>
<style scoped>
.banner { background:#fff0c2; padding:8px 12px; border-bottom:1px solid #ffe28a; }
</style>

在某页面设定公告:

<!-- pages/announcement.vue -->
<template>
  <section class="container">
    <h2>设置公告</h2>
    <BaseButton variant="secondary" @click="setBanner">显示公告</BaseButton>
    <BaseButton variant="secondary" @click="clearBanner">清除公告</BaseButton>
  </section>
</template>
<script setup lang="ts">
const banner = useBanner()
function setBanner() { banner.value = '这是一个跨页公告,所有页面顶部可见。' }
function clearBanner() { banner.value = '' }
</script>

最佳实践:

  • 将全局 UI 与状态放入布局与 composables
  • 使用命名 useState 以共享跨页面状态

5. 状态管理(Pinia)

5.1 安装与集成 Pinia

知识点:在 Nuxt 中使用 Pinia 进行状态管理。

案例:通过模块集成 @pinia/nuxt

pnpm add @pinia/nuxt pinia

nuxt.config.ts 中开启模块:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt'],
  pinia: {
    autoImports: ['defineStore', 'storeToRefs']
  }
})

5.2 购物车 Store 的增删改查

知识点:定义 Store、派发动作、从组件读取状态。

案例:stores/cart.ts 与页面使用

// stores/cart.ts
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as { id: number; title: string; qty: number; price: number }[]
  }),
  getters: {
    total: (s) => s.items.reduce((sum, i) => sum + i.qty * i.price, 0)
  },
  actions: {
    add(item: { id: number; title: string; price: number }) {
      const found = this.items.find(i => i.id === item.id)
      if (found) found.qty += 1
      else this.items.push({ ...item, qty: 1 })
    },
    remove(id: number) {
      this.items = this.items.filter(i => i.id !== id)
    },
    clear() { this.items = [] }
  }
})

在页面中使用:

<!-- pages/shop.vue -->
<template>
  <section class="container">
    <h2>商品列表</h2>
    <ul>
      <li v-for="p in products" :key="p.id">
        {{ p.title }} - ¥{{ p.price }}
        <BaseButton variant="primary" @click="add(p)">加入购物车</BaseButton>
      </li>
    </ul>
    <h3>购物车(总计:¥{{ total }})</h3>
    <ul>
      <li v-for="i in items" :key="i.id">
        {{ i.title }} × {{ i.qty }} = ¥{{ i.qty * i.price }}
        <BaseButton variant="secondary" @click="remove(i.id)">移除</BaseButton>
      </li>
    </ul>
    <BaseButton variant="secondary" @click="clear">清空</BaseButton>
  </section>
</template>
<script setup lang="ts">
const products = [
  { id: 1, title: '书籍 A', price: 30 },
  { id: 2, title: '书籍 B', price: 50 }
]
const cart = useCartStore()
const { items, total } = storeToRefs(cart)
const { add, remove, clear } = cart
</script>

验证:

  • 加入/移除商品;总价实时更新

5.3 服务端初始化与持久化

知识点:在 SSR 中恢复状态;在客户端持久化(cookies/localStorage)。

案例:登录态在 SSR/CSR 的保持与恢复

// middleware/auth.global.ts(片段)
export default defineNuxtRouteMiddleware((to) => {
  const logged = useCookie('logged_in').value === '1'
  if (!logged && to.path.startsWith('/admin')) return navigateTo('/login')
})

在页面挂载时将购物车持久化:

// plugins/persist.client.ts
export default defineNuxtPlugin(() => {
  const cart = useCartStore()
  const saved = localStorage.getItem('cart')
  if (saved) cart.items = JSON.parse(saved)
  watch(() => cart.items, (val) => {
    localStorage.setItem('cart', JSON.stringify(val))
  }, { deep: true })
})

最佳实践:

  • 通过 .client.ts 插件确保仅在客户端访问 localStorage
  • SSR 依赖 cookies 传递会话;敏感信息使用 HTTPOnly Cookie(见安全章节)

6. 组合式 API 与可复用逻辑

6.1 auto-import composables 与目录组织

知识点:Nuxt 会自动导入 composables/ 下的函数。

案例:封装分页与搜索逻辑

// composables/usePagination.ts
export function usePagination(initial = 1) {
  const page = useState<number>('page', () => initial)
  function next() { page.value += 1 }
  function prev() { page.value = Math.max(1, page.value - 1) }
  return { page, next, prev }
}
// composables/useSearch.ts
export function useSearch() {
  const q = useState<string>('q', () => '')
  const set = (val: string) => { q.value = val }
  return { q, set }
}

在页面中使用:

<!-- pages/search.vue -->
<template>
  <section class="container">
    <h2>搜索与分页</h2>
    <input v-model="q" placeholder="输入关键字" />
    <div class="pager">
      <BaseButton variant="secondary" @click="prev">上一页</BaseButton>
      <span>第 {{ page }} 页</span>
      <BaseButton variant="secondary" @click="next">下一页</BaseButton>
    </div>
    <p>当前搜索:{{ q }}</p>
  </section>
</template>
<script setup lang="ts">
const { q, set } = useSearch()
const { page, next, prev } = usePagination()
</script>
<style scoped>
.pager { display:flex; gap:12px; align-items:center; margin-top:12px; }
</style>

6.2 类型安全的自定义组合式

知识点:在组合式中使用 TypeScript 声明输入/输出。

案例:表单校验 composable

// composables/useForm.ts
export interface LoginForm {
  username: string
  password: string
}

export function useForm() {
  const form = reactive<LoginForm>({ username: '', password: '' })
  const errors = reactive<{ username?: string; password?: string }>({})

  function validate(): boolean {
    errors.username = form.username ? undefined : '用户名必填'
    errors.password = form.password.length >= 6 ? undefined : '密码至少 6 位'
    return !errors.username && !errors.password
  }

  return { form, errors, validate }
}

在登录页使用:

<!-- pages/login.vue(片段,表单校验) -->
<script setup lang="ts">
const { form, errors, validate } = useForm()
async function onSubmit() {
  if (!validate()) return
  // 调用后端登录 API
}
</script>

最佳实践:

  • 将可复用业务逻辑沉淀为 composables,统一类型与校验
  • 通过 useState 或传参控制状态作用域与持久化策略

7. 插件与模块生态

7.1 Plugins:注入客户端/服务端能力

知识点:通过插件向应用注入全局对象或方法(nuxtApp.provide)。

案例:注册 axios 插件与请求拦截器,注入 $api

pnpm add axios
// plugins/api.ts
import axios from 'axios'

export default defineNuxtPlugin((nuxtApp) => {
  const instance = axios.create({
    baseURL: useRuntimeConfig().public.apiBase || 'https://api.example.com'
  })
  // 请求拦截器
  instance.interceptors.request.use((config) => {
    const token = useCookie('token').value
    if (token) config.headers.Authorization = `Bearer ${token}`
    return config
  })
  // 响应拦截器
  instance.interceptors.response.use(
    (resp) => resp,
    (err) => {
      // 统一错误处理
      console.error('API Error:', err.message)
      return Promise.reject(err)
    }
  )
  nuxtApp.provide('api', instance)
})

在组件中使用:

<!-- pages/api-demo.vue -->
<template>
  <section class="container">
    <h2>Axios 插件示例</h2>
    <p v-if="error">请求失败:{{ error }}</p>
    <ul v-else>
      <li v-for="u in users" :key="u.id">{{ u.name }}</li>
    </ul>
  </section>
</template>
<script setup lang="ts">
const { $api } = useNuxtApp()
const users = ref<{ id:number; name:string }[]>([])
const error = ref<string>('')
try {
  const resp = await $api.get('/users')
  users.value = resp.data
} catch (e: any) {
  error.value = e.message
}
</script>

7.2 Modules:官方与第三方模块

知识点:通过模块扩展 Nuxt 能力,如内容、图片、国际化等。

案例:接入 @nuxt/image 优化产品图

pnpm add @nuxt/image
// nuxt.config.ts(片段)
export default defineNuxtConfig({
  modules: ['@nuxt/image'],
  image: {
    // 可根据实际 CDN 或静态资源配置
    domains: ['images.example.com']
  }
})

使用图片组件:

<!-- pages/image-demo.vue -->
<template>
  <section class="container">
    <h2>图片优化示例</h2>
    <NuxtImg src="https://images.example.com/product.jpg" width="600" height="400" format="webp" />
  </section>
</template>

最佳实践:

  • 将通用功能封装为插件,便于在任意组件使用
  • 优先使用官方模块(content/image/i18n/devtools 等),提升开发效率与质量

8. 服务端开发(Nitro)

8.1 server/api 路由与事件处理器

知识点:在 server/api 下新增文件即为 API 路由,使用事件处理器读取请求。

案例:RESTful 文章 API

// server/api/posts.get.ts
export default defineEventHandler(() => {
  return [{ id: 1, title: '文章 A' }, { id: 2, title: '文章 B' }]
})
// server/api/posts/[id].get.ts
export default defineEventHandler((event) => {
  const id = getRouterParam(event, 'id')
  return { id, title: `文章 ${id}` }
})
// server/api/posts.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event) // { title: string }
  return { id: Date.now(), ...body }
})

客户端调用:

// composables/usePosts.ts
export function usePosts() {
  const list = () => $fetch('/api/posts')
  const detail = (id: number) => $fetch(`/api/posts/${id}`)
  const create = (title: string) => $fetch('/api/posts', { method: 'POST', body: { title } })
  return { list, detail, create }
}

8.2 server/routes 与中间件(认证/限速)

知识点:自定义服务端路由与中间件,适合非 API 的服务端响应或特殊处理。

案例:简单限流中间件与认证校验

// server/middleware/rate-limit.ts
const hits = new Map<string, { count: number; ts: number }>()
export default defineEventHandler((event) => {
  const ip = getHeader(event, 'x-forwarded-for') || event.node.req.socket.remoteAddress || 'unknown'
  const record = hits.get(ip) || { count: 0, ts: Date.now() }
  const now = Date.now()
  if (now - record.ts > 60_000) { record.count = 0; record.ts = now }
  record.count += 1
  hits.set(ip, record)
  if (record.count > 60) { // 每分钟 60 次
    throw createError({ statusCode: 429, statusMessage: 'Too Many Requests' })
  }
})
// server/routes/secure.get.ts
export default defineEventHandler((event) => {
  const token = getCookie(event, 'token')
  if (!token) throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
  return { ok: true }
})

8.3 JWT 登录与 RBAC 权限

知识点:基于 JWT 的登录态与角色权限控制。

案例:登录颁发令牌与角色检查

// server/api/auth/login.post.ts
import jwt from 'jsonwebtoken'

export default defineEventHandler(async (event) => {
  const { username, password } = await readBody(event)
  if (username !== 'admin' || password !== '123456') {
    throw createError({ statusCode: 401, statusMessage: 'Bad credentials' })
  }
  const token = jwt.sign({ sub: username, role: 'admin' }, process.env.JWT_SECRET!, { expiresIn: '1h' })
  setCookie(event, 'token', token, { httpOnly: true, secure: true, sameSite: 'lax', path: '/' })
  return { ok: true }
})
// server/middleware/rbac.ts
import jwt from 'jsonwebtoken'
export default defineEventHandler((event) => {
  const token = getCookie(event, 'token')
  if (!token) throw createError({ statusCode: 401 })
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET!) as { role: string }
    if (payload.role !== 'admin') throw createError({ statusCode: 403, statusMessage: 'Forbidden' })
  } catch {
    throw createError({ statusCode: 401 })
  }
})

说明:

  • 切勿将 JWT_SECRET 等密钥硬编码到代码(见运行时配置章节)

8.4 文件上传(multipart)

知识点:处理表单文件上传。

案例:接收图片并保存到临时目录

// server/api/upload.post.ts
import { readMultipartFormData } from 'h3'
import { promises as fs } from 'node:fs'
import { join } from 'node:path'

export default defineEventHandler(async (event) => {
  const parts = await readMultipartFormData(event)
  const file = parts?.find(p => p.type === 'file')
  if (!file) throw createError({ statusCode: 400, statusMessage: 'No file' })
  const tmp = join('/tmp', file.filename || `upload-${Date.now()}`)
  await fs.writeFile(tmp, file.data)
  return { ok: true, path: tmp }
})

最佳实践:

  • 使用中间件统一认证与限速
  • 密钥通过运行时配置与环境变量管理

9. 运行时配置与环境变量

9.1 runtimeConfig(public/private)与多环境切换

知识点:在 nuxt.config.ts 声明运行时配置,区分私有与公开字段。

案例:配置 API 域名与密钥(私有)

// nuxt.config.ts(片段)
export default defineNuxtConfig({
  runtimeConfig: {
    // 仅服务器可读
    secretKey: process.env.SECRET_KEY,
    // 客户端也可读
    public: {
      apiBase: process.env.PUBLIC_API_BASE || 'http://localhost:3000'
    }
  }
})

在客户端/服务端读取:

// composables/useApiBase.ts
export function useApiBase() {
  const { public: { apiBase } } = useRuntimeConfig()
  return apiBase
}
// server/utils/keys.ts
export function getSecret() {
  const { secretKey } = useRuntimeConfig()
  if (!secretKey) throw new Error('SECRET_KEY 未配置')
  return secretKey
}

9.2 .env 管理与类型安全校验

知识点:通过 .env 配置环境变量并进行类型校验。

案例:使用 zod 校验环境变量

pnpm add zod
// server/plugins/env-check.ts
import { z } from 'zod'

const EnvSchema = z.object({
  SECRET_KEY: z.string().min(16),
  PUBLIC_API_BASE: z.string().url()
})

export default defineNitroPlugin(() => {
  const parsed = EnvSchema.safeParse(process.env)
  if (!parsed.success) {
    console.error('环境变量校验失败:', parsed.error.format())
    // 在生产环境中建议直接退出或抛错
  }
})

最佳实践:

  • 私有配置放在 runtimeConfig 顶层,公开配置放 runtimeConfig.public
  • 使用 schema 校验环境变量,避免因缺失或格式错误导致线上事故

10. SEO 与元信息

10.1 useHead/useSeoMeta 与 OG/Meta 标签

知识点:在页面层面设置标题、描述、OG 等信息。

案例:文章详情页设置 SEO 与社交分享卡片

<!-- pages/posts/[id].vue(片段:SEO) -->
<script setup lang="ts">
const route = useRoute()
const id = route.params.id as string
const title = `文章 ${id} 的标题`
const description = `这是文章 ${id} 的摘要描述。`
useSeoMeta({
  title,
  description,
  ogTitle: title,
  ogDescription: description,
  ogType: 'article',
  ogUrl: `https://example.com/posts/${id}`,
  ogImage: 'https://images.example.com/og-default.jpg',
  twitterCard: 'summary_large_image'
})
</script>

10.2 sitemap/robots 与 canonical

知识点:为搜索引擎提供索引提示与规范化链接。

案例:配置 canonical 与 robots

<!-- pages/index.vue(片段) -->
<script setup lang="ts">
useHead({
  link: [{ rel: 'canonical', href: 'https://example.com/' }],
  meta: [{ name: 'robots', content: 'index,follow' }]
})
</script>

注:

  • sitemap 可使用社区模块或自行在构建阶段生成

10.3 结构化数据(JSON-LD)

知识点:通过 JSON-LD 增强搜索展示(如文章、产品)。

案例:BlogPosting 注入

<!-- pages/posts/[id].vue(片段:JSON-LD) -->
<script setup lang="ts">
const id = useRoute().params.id as string
const jsonLd = {
  '@context': 'https://schema.org',
  '@type': 'BlogPosting',
  'headline': `文章 ${id} 的标题`,
  'datePublished': new Date().toISOString(),
  'author': { '@type': 'Person', 'name': '作者姓名' }
}
useHead({ script: [{ type: 'application/ld+json', children: JSON.stringify(jsonLd) }] })
</script>

最佳实践:

  • 为关键页面设置 title/description/og 完整信息
  • 使用 canonical 避免重复内容带来的权重分散
  • 合理注入结构化数据提升搜索结果展示质量

11. 内容系统(Nuxt Content)

11.1 安装与基本使用

知识点:通过 Content 模块在 Nuxt 中渲染 Markdown/MDX 内容。

案例:安装并渲染 Markdown 文档

pnpm add @nuxt/content
// nuxt.config.ts(片段)
export default defineNuxtConfig({
  modules: ['@nuxt/content']
})

创建内容文件:

content/
└─ guide/
   └─ intro.md
<!-- content/guide/intro.md -->
# 入门指南

欢迎使用 Nuxt Content,这里是第一篇文档。

渲染页面:

<!-- pages/guide.vue -->
<template>
  <section class="container">
    <h2>文档</h2>
    <ContentDoc path="/guide/intro" />
  </section>
</template>

11.2 目录驱动与搜索/高亮

知识点:根据目录结构生成导航,支持代码高亮与搜索。

案例:生成侧边目录与正文

<!-- pages/docs.vue -->
<template>
  <div class="layout">
    <aside class="sidebar">
      <ContentNavigation v-slot="{ navigation }">
        <ul>
          <li v-for="item in navigation" :key="item._path">
            <NuxtLink :to="item._path">{{ item.title }}</NuxtLink>
          </li>
        </ul>
      </ContentNavigation>
    </aside>
    <main class="main">
      <ContentDoc />
    </main>
  </div>
</template>
<style scoped>
.layout { display:flex; }
.sidebar { width: 240px; border-right: 1px solid #eee; padding: 12px; }
.main { flex:1; padding: 16px; }
</style>

11.3 在 Markdown 中嵌入交互式组件

知识点:在 Content 渲染的 MD 中插入自定义 Vue 组件。

案例:嵌入 Demo 组件

<!-- components/DemoCounter.vue -->
<template>
  <div>
    <p>计数:{{ n }}</p>
    <BaseButton variant="primary" @click="n++">+</BaseButton>
  </div>
</template>
<script setup lang="ts">
const n = ref(0)
</script>
<!-- content/demo.md -->
# 交互式 Demo

这是一个嵌入组件的例子:

::DemoCounter
::

渲染:

<!-- pages/demo.vue -->
<template>
  <ContentDoc path="/demo" />
</template>

最佳实践:

  • 通过 Content 快速搭建文档站与博客系统
  • 使用目录导航与组件插入提升可读性与交互性

12. 国际化(i18n)

12.1 安装与路由策略

知识点:使用 i18n 模块实现多语言与路由前缀策略。

案例:中英文站点的路径与切换

pnpm add @nuxtjs/i18n
// nuxt.config.ts(片段)
export default defineNuxtConfig({
  modules: ['@nuxtjs/i18n'],
  i18n: {
    locales: [
      { code: 'zh', name: '中文', file: 'zh.json' },
      { code: 'en', name: 'English', file: 'en.json' }
    ],
    defaultLocale: 'zh',
    lazy: true,
    langDir: 'locales',
    strategy: 'prefix', // /zh, /en
  }
})

语言文件:

locales/
├─ zh.json
└─ en.json
// locales/zh.json
{ "home": "首页", "welcome": "欢迎使用 Nuxt4" }
// locales/en.json
{ "home": "Home", "welcome": "Welcome to Nuxt4" }

在页面中使用:

<!-- pages/i18n.vue -->
<template>
  <section class="container">
    <h2>{{ t('welcome') }}</h2>
    <NuxtLink to="/zh">中文</NuxtLink>
    <NuxtLink to="/en">English</NuxtLink>
  </section>
</template>
<script setup lang="ts">
const { t } = useI18n()
</script>

12.2 服务端翻译加载与 SEO

知识点:在 SSR 中加载翻译并设置 hreflang。

案例:设置多语言的 hreflang 链接

<!-- app.vue(片段) -->
<script setup lang="ts">
useHead({
  link: [
    { rel: 'alternate', href: 'https://example.com/zh', hreflang: 'zh' },
    { rel: 'alternate', href: 'https://example.com/en', hreflang: 'en' }
  ]
})
</script>

最佳实践:

  • 为多语言配置路由前缀与默认语言,避免重复内容冲突
  • 设置 hreflang 提示搜索引擎不同语言版本

13. 静态资源与图片优化

13.1 assets 与 public 的区别

知识点assets/ 走构建管线(可被处理/打包),public/ 原样公开。

案例:组织静态文件与图标

assets/
└─ styles/
   └─ main.css
public/
└─ favicon.png

app.vue 引入样式与图标:

<!-- app.vue(片段) -->
<script setup>
import '~/assets/styles/main.css'
</script>

13.2 Nuxt Image 的懒加载与裁剪

知识点:通过 NuxtImg 进行图片优化与懒加载。

案例:不同视口下自适应图片与格式转换

<!-- pages/image-advanced.vue -->
<template>
  <section class="container">
    <h2>图片优化</h2>
    <NuxtImg
      src="https://images.example.com/product.jpg"
      sizes="sm:320px md:640px lg:960px"
      format="webp"
      class="img"
    />
  </section>
</template>
<style scoped>
.img { width: 100%; border-radius: 8px; }
</style>

13.3 图片 CDN 集成

知识点:将图片托管到 CDN 并在 Nuxt 中统一配置。

案例:在 nuxt.config.ts 中设置域名

// nuxt.config.ts(片段)
export default defineNuxtConfig({
  image: {
    domains: ['images.example.com'],
    format: ['webp', 'png', 'jpg']
  }
})

最佳实践:

  • 体积大的静态资产放 CDN
  • 使用 Nuxt Image 统一图片优化策略(懒加载、裁剪、格式转换)

14. 样式与构建工具

14.1 Tailwind/UnoCSS 集成与暗色模式

知识点:快速集成现代样式方案与暗色模式。

案例:接入 Tailwind 并实现暗色模式切换

pnpm add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
module.exports = {
  darkMode: 'class',
  content: ['components/**/*.{vue,js}', 'pages/**/*.{vue,js}', 'app.vue'],
  theme: { extend: {} }
}
/* assets/styles/main.css(新增 Tailwind 指令) */
@tailwind base;
@tailwind components;
@tailwind utilities;
<!-- components/DarkToggle.vue -->
<template>
  <BaseButton variant="secondary" @click="toggle">
    切换暗色模式
  </BaseButton>
</template>
<script setup lang="ts">
function toggle() {
  document.documentElement.classList.toggle('dark')
}
</script>

14.2 Vite 配置扩展(别名/预处理器)

知识点:在 Nuxt 中扩展 Vite 配置以支持别名与全局样式变量。

案例:SCSS 全局变量与路径别名

// nuxt.config.ts(片段)
export default defineNuxtConfig({
  vite: {
    resolve: {
      alias: {
        '@': '/src' // 示例,按需调整
      }
    },
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@use "@/styles/vars.scss" as *;'
        }
      }
    }
  }
})

最佳实践:

  • 使用原子化 CSS(如 Tailwind/UnoCSS)提升开发效率
  • 通过 Vite 预处理器统一全局样式变量与主题

15. 安全与权限

15.1 XSS/CSRF 基础与安全配置

知识点:防止跨站脚本与跨站请求伪造。

案例:HTTPOnly Cookie + CSRF Token 双重防护

// server/api/auth/login.post.ts(片段)
import { randomBytes } from 'node:crypto'
export default defineEventHandler(async (event) => {
  // 认证通过后:
  setCookie(event, 'token', 'jwt-token', { httpOnly: true, secure: true, sameSite: 'lax', path: '/' })
  const csrf = randomBytes(16).toString('hex')
  setCookie(event, 'csrf', csrf, { httpOnly: false, secure: true, sameSite: 'lax', path: '/' })
  return { ok: true }
})
// server/middleware/csrf.ts
export default defineEventHandler((event) => {
  if (event.method === 'GET') return
  const csrfCookie = getCookie(event, 'csrf')
  const csrfHeader = getHeader(event, 'x-csrf-token')
  if (!csrfCookie || !csrfHeader || csrfCookie !== csrfHeader) {
    throw createError({ statusCode: 403, statusMessage: 'CSRF verification failed' })
  }
})

客户端在提交时带上 CSRF 头:

// plugins/api.ts(片段)
instance.interceptors.request.use((config) => {
  const csrf = useCookie('csrf').value
  if (csrf) config.headers['x-csrf-token'] = csrf
  return config
})

15.2 角色权限中间件与路由保护

知识点:前端路由守卫结合服务端 RBAC。

案例:页面级路由守卫

// middleware/admin.ts
export default defineNuxtRouteMiddleware(() => {
  const role = useCookie('role').value
  if (role !== 'admin') return navigateTo('/', { redirectCode: 302 })
})
<!-- pages/admin/settings.vue -->
<script setup lang="ts">
definePageMeta({ middleware: 'admin' })
</script>

最佳实践:

  • 敏感信息只放 HTTPOnly Cookie
  • 写操作强制 CSRF 校验;前后端共同防护
  • 前端路由守卫仅用于提升体验,真正授权在服务端校验

16. 测试与质量保障

16.1 单元测试(Vitest)

知识点:为 Store/组件编写单元测试。

案例:测试购物车 Store

pnpm add -D vitest
// tests/cart.test.ts
import { describe, it, expect } from 'vitest'
import { useCartStore } from '../stores/cart'

describe('cart store', () => {
  it('add and remove items', () => {
    const cart = useCartStore()
    cart.clear()
    cart.add({ id: 1, title: 'A', price: 10 })
    cart.add({ id: 1, title: 'A', price: 10 })
    expect(cart.items[0].qty).toBe(2)
    cart.remove(1)
    expect(cart.items.length).toBe(0)
  })
})

16.2 端到端测试(Playwright)

知识点:编写 E2E 测试覆盖登录与路由守卫。

案例:登录流程与访问受保护路由

pnpm add -D @playwright/test
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test'

test('redirect to login when not authenticated', async ({ page }) => {
  await page.goto('http://localhost:3000/admin')
  await expect(page).toHaveURL(/login/)
})

test('login then access admin', async ({ page }) => {
  await page.goto('http://localhost:3000/login')
  await page.getByRole('button', { name: '点击登录并跳转后台' }).click()
  await expect(page).toHaveURL(/admin/)
})

16.3 Lint/TypeScript 与 CI

知识点:在 CI 中集成 Lint、类型检查与测试。

案例:简单 CI 步骤(伪代码)

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with: { node-version: 18 }
      - run: pnpm install
      - run: pnpm typecheck
      - run: pnpm lint
      - run: pnpm test

最佳实践:

  • 单元测试聚焦纯逻辑;E2E 测试覆盖关键业务流
  • 在 CI 强制类型检查与测试,保障主干稳定

17. 部署与运维

17.1 目标平台与构建

知识点:Nuxt 可部署至 Node、Edge、Serverless,也可静态导出。

案例:不同平台部署要点

  • Node:传统服务器,pnpm build 后运行 Nitro 服务器
  • Edge/Workers:要求无 Node 专属 API,注意 Bundling 与 KV 存储
  • Serverless:每个 API 作为函数,需冷启动优化
  • 静态导出:内容/纯前端页面预渲染为静态 HTML

17.2 环境变量注入与密钥管理

知识点:生产环境注入 .env 并在运行时读取。

案例:在平台配置面板中注入 SECRET_KEYPUBLIC_API_BASE

  • 平台侧设置环境变量,避免写入代码仓库
  • 使用 runtimeConfig 获取并对敏感值仅在服务器端使用

17.3 日志与监控(Sentry)

知识点:集成错误上报与性能监控。

案例:服务端错误上报(示意)

// server/plugins/sentry.ts(示意)
import * as Sentry from '@sentry/node'
export default defineNitroPlugin(() => {
  Sentry.init({ dsn: process.env.SENTRY_DSN })
})

在 API 中捕获并上报:

// server/api/example.get.ts(示意)
import * as Sentry from '@sentry/node'
export default defineEventHandler(() => {
  try {
    // ...
  } catch (e) {
    Sentry.captureException(e)
    throw e
  }
})

17.4 缓存策略与 headers

知识点:为静态与动态资源设置合理缓存策略。

案例:为图片与静态文件设置长期缓存

// server/middleware/cache.ts
export default defineEventHandler((event) => {
  const url = event.path
  if (url.startsWith('/_nuxt/') || url.startsWith('/public/')) {
    setHeader(event, 'Cache-Control', 'public, max-age=31536000, immutable')
  }
})

最佳实践:

  • 优先选择平台的原生集成(Vercel/Netlify/Cloudflare)简化部署流程
  • 使用监控与日志定位线上问题,配合错误上报
  • 针对不同类型资源设计差异化缓存策略

18. 性能优化

18.1 路由级代码分割与预取

知识点:Nuxt 自动按页面代码分割;可预取提升导航速度。

案例:启用 link prefetch

// nuxt.config.ts(片段)
export default defineNuxtConfig({
  app: {
    // 在视口可见的链接上自动进行预取
    pageTransition: { name: 'page', mode: 'out-in' }
  },
  experimental: {
    // 不同 Nuxt 版本选项可能不同,此处为示意
  }
})

在页面中使用 <NuxtLink> 默认即可享受预取(可在 DevTools 中观察网络请求)。


18.2 useLazyAsyncData 与请求优化

知识点:惰性数据获取、去抖/节流减少不必要请求。

案例:搜索建议的去抖优化

<!-- pages/search-optimized.vue -->
<template>
  <section class="container">
    <input v-model="q" placeholder="输入关键字(300ms 去抖)" />
    <ul>
      <li v-for="s in suggestions" :key="s">{{ s }}</li>
    </ul>
  </section>
</template>
<script setup lang="ts">
const q = ref('')
const debounced = ref('')
let timer: any
watch(q, (val) => {
  clearTimeout(timer)
  timer = setTimeout(() => debounced.value = val, 300)
})
const { data: suggestions } = await useLazyAsyncData(
  () => `sugg-${debounced.value}`,
  () => $fetch('/api/suggest', { query: { q: debounced.value } }),
  { immediate: true }
)
</script>

18.3 图片优化与 HTTP 压缩

知识点:webp/avif 与 Gzip/Brotli 压缩提升加载性能。

案例:服务端开启压缩(示意)

// server/middleware/compress.ts(示意)
export default defineEventHandler((event) => {
  // 依赖平台/运行时开启压缩,此处仅示意设置头
  setHeader(event, 'Content-Encoding', 'br')
})

最佳实践:

  • 使用 Lighthouse 评估并逐项优化(图片、脚本体积、缓存策略)
  • 结合 DevTools 与 Vite Inspect 分析依赖并按需拆分

19. 开发者工具与调试

19.1 Nuxt DevTools

知识点:使用 DevTools 查看路由、组件树、数据来源与性能。

案例:定位慢路由

  • 打开 DevTools(开发模式自动可用)
  • 进入路由面板,观察该页面数据获取时间与组件渲染耗时
  • 结合网络面板检查是否有重复请求或大型资源

19.2 Vite Inspect 与依赖分析

知识点:分析打包产物与依赖体积来源。

案例:发现大体积依赖并按需优化

  • 启用 Inspect 插件(Nuxt 内置集成或按需配置)
  • 查看页面对应 chunk,确认是否引入了不必要的第三方库
  • 通过动态导入或替换轻量库减少体积

最佳实践:

  • 在开发阶段持续使用 DevTools/Inspect 发现问题
  • 优先移除不必要依赖、减少全局引入,采用按需与懒加载