Nuxt 4 学习文档(案例驱动)
目标:以大功能点为章节、每个知识点配套详细案例与代码,帮助具备 Vue 3 基础的工程师系统掌握 Nuxt 4(思路兼容 Nuxt 3)。
目录
-
- 项目初始化与目录约定
-
- 路由与导航
-
- 数据获取与渲染模式
-
- 组件与布局
-
- 状态管理(Pinia)
-
- 组合式 API 与可复用逻辑
-
- 插件与模块生态
-
- 服务端开发(Nitro)
-
- 运行时配置与环境变量
-
- SEO 与元信息
-
- 内容系统(Nuxt Content)
-
- 国际化(i18n)
-
- 静态资源与图片优化
-
- 样式与构建工具
-
- 安全与权限
-
- 测试与质量保障
-
- 部署与运维
-
- 性能优化
-
- 开发者工具与调试
1. 项目初始化与目录约定
1.1 使用 nuxi 创建项目与启动开发
知识点:使用官方脚手架创建 Nuxt 4 项目,了解基础命令。
案例:从零创建 nuxt4-app 并运行
步骤:
- 安装最新 nuxi(或使用 npx 直接调用)
- 创建项目并选择 TypeScript
- 启动开发服务器,访问本地地址
bash
# 使用 npx(无需全局安装)
npx nuxi@latest init nuxt4-app
cd nuxt4-app
# 推荐使用 pnpm,也可用 npm 或 yarn
pnpm install
pnpm dev
# 终端输出本地预览地址,例如 http://localhost:3000
项目创建后,默认文件结构示例:
arduino
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:
ts
// 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 的约定式目录与页面、组件、服务端的组织方式。
案例:搭建最小博客骨架(首页/文章页)
创建首页与文章详情页:
vue
<!-- 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>
vue
<!-- 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>
添加基础组件与样式:
vue
<!-- 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>
将组件挂到应用根:
vue
<!-- app.vue -->
<template>
<BaseHeader />
<NuxtPage />
</template>
验证:
- 首页展示文章列表,可点击进入详情页
- 顶部导航与样式正常
最佳实践:
- pages 用于路由页面,components 存放可复用视图组件
- 将公共结构放入 app.vue 或布局(layouts)
下一章将从"路由与导航"开始,深入讲解动态/嵌套路由与中间件,配合完整示例持续扩展本示例站点。
2. 路由与导航
2.1 约定式路由:动态/可选/捕获/嵌套
知识点:Nuxt 的 pages 目录根据文件命名自动生成路由。
案例:动态、可选与捕获路由
bash
pages/
├─ users/
│ ├─ [id].vue # 动态参数 /users/123
│ ├─ [[tab]].vue # 可选参数 /users 或 /users/profile
│ └─ [...slug].vue # 捕获所有 /users/a/b/c
└─ users/[id]/settings.vue # 动态 + 嵌套路由 /users/123/settings
示例页面:
vue
<!-- 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>
vue
<!-- 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() 进行导航。
案例:分页列表导航与编程式跳转
vue
<!-- 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 重定向
创建路由中间件:
ts
// 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 })
}
})
受保护路由示例:
vue
<!-- pages/admin/index.vue -->
<template>
<section class="container">
<h2>后台管理</h2>
<p>只有登录用户可访问</p>
</section>
</template>
登录页简单实现:
vue
<!-- 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 获取文章列表 + 客户端增量刷新
服务端接口(模拟):
ts
// server/api/posts.get.ts
export default defineEventHandler(async (event) => {
// 模拟数据库查询
return [
{ id: 1, title: 'Nuxt4 入门与目录约定' },
{ id: 2, title: '路由与导航详解' },
{ id: 3, title: '数据获取与渲染模式' }
]
})
页面使用 useAsyncData:
vue
<!-- 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):
vue
<!-- 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.vueSSR 渲染列表 - 点击"客户端刷新"会重新请求数据并更新视图
3.2 渲染模式:SSR、CSR、混合、预渲染
知识点:理解不同渲染模式的取舍与 Nuxt 支持。
案例:对比同页面在不同模式下的表现
vue
<!-- 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 与骨架屏
vue
<!-- 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 并在多页面复用
vue
<!-- 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>
在任意页面直接使用:
vue
<!-- pages/about.vue -->
<template>
<section class="container">
<h2>关于页面</h2>
<BaseButton variant="primary">立即体验</BaseButton>
</section>
</template>
4.2 布局(layouts)与错误页(error.vue)
知识点:使用布局统一页面框架与导航;使用错误页统一异常展示。
案例:默认布局与自定义错误页
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>
错误页:
vue
<!-- 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 保持跨页面/布局状态。
案例:跨页公告栏与可插槽的主布局
ts
// composables/useBanner.ts
export const useBanner = () => useState<string>('global-banner', () => '')
vue
<!-- 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>
在某页面设定公告:
vue
<!-- 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
bash
pnpm add @pinia/nuxt pinia
在 nuxt.config.ts 中开启模块:
ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
pinia: {
autoImports: ['defineStore', 'storeToRefs']
}
})
5.2 购物车 Store 的增删改查
知识点:定义 Store、派发动作、从组件读取状态。
案例:stores/cart.ts 与页面使用
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 = [] }
}
})
在页面中使用:
vue
<!-- 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 的保持与恢复
ts
// middleware/auth.global.ts(片段)
export default defineNuxtRouteMiddleware((to) => {
const logged = useCookie('logged_in').value === '1'
if (!logged && to.path.startsWith('/admin')) return navigateTo('/login')
})
在页面挂载时将购物车持久化:
ts
// 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/ 下的函数。
案例:封装分页与搜索逻辑
ts
// 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 }
}
ts
// composables/useSearch.ts
export function useSearch() {
const q = useState<string>('q', () => '')
const set = (val: string) => { q.value = val }
return { q, set }
}
在页面中使用:
vue
<!-- 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
ts
// 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 }
}
在登录页使用:
vue
<!-- 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
bash
pnpm add axios
ts
// 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)
})
在组件中使用:
vue
<!-- 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 优化产品图
bash
pnpm add @nuxt/image
ts
// nuxt.config.ts(片段)
export default defineNuxtConfig({
modules: ['@nuxt/image'],
image: {
// 可根据实际 CDN 或静态资源配置
domains: ['images.example.com']
}
})
使用图片组件:
vue
<!-- 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
ts
// server/api/posts.get.ts
export default defineEventHandler(() => {
return [{ id: 1, title: '文章 A' }, { id: 2, title: '文章 B' }]
})
ts
// server/api/posts/[id].get.ts
export default defineEventHandler((event) => {
const id = getRouterParam(event, 'id')
return { id, title: `文章 ${id}` }
})
ts
// server/api/posts.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event) // { title: string }
return { id: Date.now(), ...body }
})
客户端调用:
ts
// 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 的服务端响应或特殊处理。
案例:简单限流中间件与认证校验
ts
// 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' })
}
})
ts
// 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 的登录态与角色权限控制。
案例:登录颁发令牌与角色检查
ts
// 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 }
})
ts
// 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)
知识点:处理表单文件上传。
案例:接收图片并保存到临时目录
ts
// 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 域名与密钥(私有)
ts
// nuxt.config.ts(片段)
export default defineNuxtConfig({
runtimeConfig: {
// 仅服务器可读
secretKey: process.env.SECRET_KEY,
// 客户端也可读
public: {
apiBase: process.env.PUBLIC_API_BASE || 'http://localhost:3000'
}
}
})
在客户端/服务端读取:
ts
// composables/useApiBase.ts
export function useApiBase() {
const { public: { apiBase } } = useRuntimeConfig()
return apiBase
}
ts
// server/utils/keys.ts
export function getSecret() {
const { secretKey } = useRuntimeConfig()
if (!secretKey) throw new Error('SECRET_KEY 未配置')
return secretKey
}
9.2 .env 管理与类型安全校验
知识点 :通过 .env 配置环境变量并进行类型校验。
案例:使用 zod 校验环境变量
bash
pnpm add zod
ts
// 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 与社交分享卡片
vue
<!-- 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
vue
<!-- 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 注入
vue
<!-- 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 文档
bash
pnpm add @nuxt/content
ts
// nuxt.config.ts(片段)
export default defineNuxtConfig({
modules: ['@nuxt/content']
})
创建内容文件:
css
content/
└─ guide/
└─ intro.md
md
<!-- content/guide/intro.md -->
# 入门指南
欢迎使用 Nuxt Content,这里是第一篇文档。
渲染页面:
vue
<!-- pages/guide.vue -->
<template>
<section class="container">
<h2>文档</h2>
<ContentDoc path="/guide/intro" />
</section>
</template>
11.2 目录驱动与搜索/高亮
知识点:根据目录结构生成导航,支持代码高亮与搜索。
案例:生成侧边目录与正文
vue
<!-- 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 组件
vue
<!-- 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>
md
<!-- content/demo.md -->
# 交互式 Demo
这是一个嵌入组件的例子:
::DemoCounter
::
渲染:
vue
<!-- pages/demo.vue -->
<template>
<ContentDoc path="/demo" />
</template>
最佳实践:
- 通过 Content 快速搭建文档站与博客系统
- 使用目录导航与组件插入提升可读性与交互性
12. 国际化(i18n)
12.1 安装与路由策略
知识点:使用 i18n 模块实现多语言与路由前缀策略。
案例:中英文站点的路径与切换
bash
pnpm add @nuxtjs/i18n
ts
// 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
json
// locales/zh.json
{ "home": "首页", "welcome": "欢迎使用 Nuxt4" }
json
// locales/en.json
{ "home": "Home", "welcome": "Welcome to Nuxt4" }
在页面中使用:
vue
<!-- 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 链接
vue
<!-- 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/ 原样公开。
案例:组织静态文件与图标
arduino
assets/
└─ styles/
└─ main.css
public/
└─ favicon.png
在 app.vue 引入样式与图标:
vue
<!-- app.vue(片段) -->
<script setup>
import '~/assets/styles/main.css'
</script>
13.2 Nuxt Image 的懒加载与裁剪
知识点 :通过 NuxtImg 进行图片优化与懒加载。
案例:不同视口下自适应图片与格式转换
vue
<!-- 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 中设置域名
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 并实现暗色模式切换
bash
pnpm add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
js
// tailwind.config.js
module.exports = {
darkMode: 'class',
content: ['components/**/*.{vue,js}', 'pages/**/*.{vue,js}', 'app.vue'],
theme: { extend: {} }
}
css
/* assets/styles/main.css(新增 Tailwind 指令) */
@tailwind base;
@tailwind components;
@tailwind utilities;
vue
<!-- 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 全局变量与路径别名
ts
// 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 双重防护
ts
// 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 }
})
ts
// 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 头:
ts
// 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。
案例:页面级路由守卫
ts
// middleware/admin.ts
export default defineNuxtRouteMiddleware(() => {
const role = useCookie('role').value
if (role !== 'admin') return navigateTo('/', { redirectCode: 302 })
})
vue
<!-- pages/admin/settings.vue -->
<script setup lang="ts">
definePageMeta({ middleware: 'admin' })
</script>
最佳实践:
- 敏感信息只放 HTTPOnly Cookie
- 写操作强制 CSRF 校验;前后端共同防护
- 前端路由守卫仅用于提升体验,真正授权在服务端校验
16. 测试与质量保障
16.1 单元测试(Vitest)
知识点:为 Store/组件编写单元测试。
案例:测试购物车 Store
bash
pnpm add -D vitest
ts
// 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 测试覆盖登录与路由守卫。
案例:登录流程与访问受保护路由
bash
pnpm add -D @playwright/test
ts
// 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 步骤(伪代码)
yaml
# .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_KEY 与 PUBLIC_API_BASE
- 平台侧设置环境变量,避免写入代码仓库
- 使用
runtimeConfig获取并对敏感值仅在服务器端使用
17.3 日志与监控(Sentry)
知识点:集成错误上报与性能监控。
案例:服务端错误上报(示意)
ts
// server/plugins/sentry.ts(示意)
import * as Sentry from '@sentry/node'
export default defineNitroPlugin(() => {
Sentry.init({ dsn: process.env.SENTRY_DSN })
})
在 API 中捕获并上报:
ts
// 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
知识点:为静态与动态资源设置合理缓存策略。
案例:为图片与静态文件设置长期缓存
ts
// 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
ts
// nuxt.config.ts(片段)
export default defineNuxtConfig({
app: {
// 在视口可见的链接上自动进行预取
pageTransition: { name: 'page', mode: 'out-in' }
},
experimental: {
// 不同 Nuxt 版本选项可能不同,此处为示意
}
})
在页面中使用 <NuxtLink> 默认即可享受预取(可在 DevTools 中观察网络请求)。
18.2 useLazyAsyncData 与请求优化
知识点:惰性数据获取、去抖/节流减少不必要请求。
案例:搜索建议的去抖优化
vue
<!-- 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 压缩提升加载性能。
案例:服务端开启压缩(示意)
ts
// 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 发现问题
- 优先移除不必要依赖、减少全局引入,采用按需与懒加载