最佳实践
本章是全书的"经验总结"——将前面各章节的知识串联起来,形成完整的项目架构思维。如果你已经读完了前面的章节,这里帮你把碎片知识变成系统认知。
项目架构设计
目录职责边界
Nuxt 4 的三个核心目录各有明确边界,遵循这些边界能避免 90% 的架构问题:
text
app/ → 前端专属(浏览器执行 + SSR 渲染)
server/ → 后端专属(Node.js / 边缘运行时)
shared/ → 前后端共享(纯 TypeScript,不依赖任何运行时)常见的边界违规和修正:
| ❌ 错误做法 | ✅ 正确做法 | 原因 |
|---|---|---|
在 app/utils/ 中写数据库查询 | 在 server/api/ 中写,前端调 API | 数据库连接只在服务端可用 |
在 server/utils/ 中写 DOM 操作 | 在 app/composables/ 中写 | 服务端没有 DOM |
在 app/pages/ 中直接调数据库 | 通过 server/api/ 间接获取 | 客户端代码会暴露给用户 |
| 前后端各写一套类型定义 | 放在 shared/types/ | 一致性、可维护性 |
在 shared/ 中使用 ref() | 放在 app/composables/ | ref() 依赖 Vue 运行时 |
代码放置决策树
text
这段代码放哪里?
│
├── 只在浏览器用?→ app/(页面、组件、组合式函数)
├── 只在服务器用?→ server/(API、中间件、数据库)
├── 两边都要用?→ shared/(类型、纯函数、校验)
│
├── 是 Vue 组件?→ app/components/
├── 是页面?→ app/pages/
├── 是布局?→ app/layouts/
├── 有响应式状态?→ app/composables/(use 前缀)
├── 是纯工具函数?→ app/utils/ 或 shared/utils/
└── 是服务端逻辑?→ server/ 下的对应子目录项目规模增长时的策略
| 阶段 | 策略 | 具体做法 |
|---|---|---|
| 小型(1-5 页面) | 扁平结构 | pages/ 直接放文件,components/ 不分子目录 |
| 中型(5-20 页面) | 按功能分目录 | components/form/、components/card/;composables 按业务分文件 |
| 大型(20+ 页面) | 模块化拆分 | 考虑用 Layers 拆分功能模块;composables/ 按领域分子目录 |
不要过早优化目录结构
项目小时扁平结构更清晰,等代码确实变乱再重构。
状态管理策略
Nuxt 提供多种状态管理方案,选错方案会导致代码混乱:
text
如何选择状态管理方案?
│
├── 只在组件内部用?→ ref()
├── 需要跨组件共享?→ useState() + 封装为 composable
├── 需要 getters/actions/插件?→ Pinia
├── 需要持久化?→ Pinia + pinia-plugin-persistedstate
└── 需要服务端初始化?→ Pinia + nuxtServerInit封装模式
原则:所有跨组件共享的状态,都应该封装为组合式函数,不要让 useState 的 key 泄露到组件外部。
ts
// ❌ 错误:key 散落在各组件中
// 组件 A
const user = useState('auth-user', () => null)
// 组件 B
const user = useState('auth-user', () => null) // key 必须完全一致!
// ✅ 正确:封装为 composable
export const useAuth = () => {
const user = useState<User | null>('auth-user', () => null)
const isAuthenticated = computed(() => !!user.value)
async function login(email: string, password: string) {
user.value = await $fetch('/api/login', {
method: 'POST',
body: { email, password },
})
}
function logout() {
user.value = null
const token = useCookie('auth-token')
token.value = null
}
return { user, isAuthenticated, login, logout }
}封装的好处
- key 集中管理:改 key 只改一处
- 业务逻辑内聚:状态 + 操作在一起,不分散
- 类型安全:返回值有完整类型
- SSR 安全:用
useState而非模块级ref
数据获取模式
四种数据获取方式的选择
| 方式 | SSR 支持 | 响应式 | 适合场景 |
|---|---|---|---|
useFetch | ✅ | ✅ | 标准 API 请求 |
useAsyncData | ✅ | ✅ | 聚合请求、SDK 调用 |
$fetch | ❌ 仅客户端请求 | ❌ | 事件处理函数中 |
服务端 useFetch + 客户端 $fetch | — | — | 分场景组合 |
数据获取的常见模式
模式 1:关键数据阻塞 + 非关键数据懒加载
vue
<script setup>
// 关键数据:阻塞渲染,确保 SEO
const { data: article } = await useFetch(`/api/articles/${id}`)
// 非关键数据:懒加载,不阻塞页面显示
const { data: comments, pending: commentsLoading } = useLazyFetch('/api/comments', {
query: { articleId: id },
default: () => [],
})
</script>
<template>
<article>{{ article?.content }}</article>
<div v-if="commentsLoading">评论加载中...</div>
<div v-else>{{ comments }}</div>
</template>模式 2:搜索 + 分页
ts
const page = ref(1)
const keyword = ref('')
const { data } = await useFetch('/api/search', {
query: { page, q: keyword },
watch: [page, keyword], // 任一变化自动刷新
default: () => ({ items: [], total: 0 }),
})模式 3:用户操作后刷新
ts
const { data: posts, refresh } = await useFetch('/api/posts')
async function deletePost(id: number) {
await $fetch(`/api/posts/${id}`, { method: 'DELETE' })
await refresh() // 删除后刷新列表
}模式 4:自定义请求实例(推荐所有项目使用)
ts
// app/composables/useApi.ts
export const useApiFetch = createUseFetch((options) => {
const config = useRuntimeConfig()
return {
...options,
baseURL: options.baseURL ?? config.public.apiBase,
headers: {
...options.headers,
Authorization: `Bearer ${useCookie('token').value}`,
},
onResponseError({ response }) {
if (response.status === 401) {
navigateTo('/login')
}
},
}
})Payload 大小控制
SSR 时所有 useFetch/useAsyncData 的数据会序列化到 HTML Payload 中。Payload 过大 → 首屏慢。
ts
// ❌ 获取完整数据(可能包含几十个字段)
const { data } = await useFetch('/api/users')
// ✅ 只获取需要的字段
const { data } = await useFetch('/api/users', {
pick: ['id', 'name', 'avatar'],
})
// ✅ 使用 transform 过滤敏感字段和大字段
const { data } = await useFetch('/api/users', {
transform: (users) => users.map(({ password, bio, ...safe }) => safe),
})Payload 大小检查
在浏览器开发者工具中查看页面 HTML,搜索 __NUXT_DATA__,就能看到 Payload 的大小和内容。
SSR 安全清单
SSR 引入了纯 CSR 项目不存在的安全风险。以下是必须了解的要点:
1. 跨请求数据污染(最危险)
ts
// ❌ 危险!模块级变量在所有请求间共享
let currentUser = null
export default defineEventHandler(() => {
currentUser = getUserFromDB() // 用户 A 的数据
// 用户 B 的请求可能在此刻到来,看到 A 的数据!
return currentUser
})
// ✅ 安全:每次请求创建局部变量
export default defineEventHandler(async (event) => {
const user = await getUserFromDB(event)
return user
})
// ✅ 安全:用 useState(每个请求独立状态)
const user = useState('current-user', () => null)2. 敏感信息不泄露到客户端
ts
// ❌ 危险:runtimeConfig.public 中的信息会发送到浏览器
runtimeConfig: {
public: {
apiKey: 'sk-xxx', // 任何人打开 DevTools 都能看到!
},
}
// ✅ 安全:私有配置放在 public 之外
runtimeConfig: {
apiKey: '', // 只在服务端可访问
public: {
apiBase: '/api', // 客户端只需要知道 API 路径
},
}3. 不要在 SSR 中执行浏览器 API
ts
// ❌ SSR 报错
const width = window.innerWidth
// ✅ 安全方式
const width = ref(0)
onMounted(() => { width.value = window.innerWidth })4. 错误页面不要暴露堆栈信息
vue
<!-- ❌ 危险:生产环境暴露服务器内部信息 -->
<p>{{ error.stack }}</p>
<!-- ✅ 安全:只显示用户友好的错误信息 -->
<h1>{{ error.statusCode }}</h1>
<p>{{ error.message }}</p>错误处理架构
三层错误处理
text
第 1 层:API 路由 → 输入校验 + 业务逻辑校验
↓
第 2 层:服务中间件 → 全局错误拦截 + 日志记录
↓
第 3 层:客户端 → 用户友好的错误展示ts
// 第 1 层:API 路由中的校验
export default defineEventHandler(async (event) => {
const body = await readBody(event)
if (!body.email) {
throw createError({ statusCode: 400, statusMessage: 'Bad Request', message: '邮箱不能为空' })
}
const user = await findUserByEmail(body.email)
if (!user) {
throw createError({ statusCode: 404, statusMessage: 'Not Found', message: '用户不存在' })
}
return user
})ts
// 第 2 层:全局错误日志
// server/middleware/error-logger.ts
export default defineEventHandler((event) => {
// 请求后记录错误
event.node.res.on('finish', () => {
const status = event.node.res.statusCode
if (status >= 400) {
console.error(`[ERROR] ${event.method} ${event.path} → ${status}`)
}
})
})ts
// 第 3 层:客户端封装
// app/composables/useApi.ts
export const useApiFetch = createUseFetch((options) => {
return {
...options,
async onResponseError({ response }) {
if (response.status === 401) navigateTo('/login')
if (response.status === 403) showError('没有权限')
if (response.status >= 500) showError('服务器错误,请稍后重试')
},
}
})SEO 策略
SEO 检查清单
| 检查项 | 方法 | 工具 |
|---|---|---|
| 每个页面有唯一的 title | useServerSeoMeta | 查看源码 <title> |
| 每个页面有 description | useServerSeoMeta | 查看源码 <meta name="description"> |
| 有 canonical URL | useHead + <link rel="canonical"> | 查看源码 |
| 有 Open Graph 标签 | useServerSeoMeta | Facebook Sharing Debugger |
| 有 sitemap.xml | @nuxtjs/sitemap | 访问 /sitemap.xml |
| 有 robots.txt | @nuxtjs/robots | 访问 /robots.txt |
| SSR 页面内容可被爬取 | 查看页面源码 | 确认 HTML 中有内容 |
| 图片有 alt 属性 | <NuxtImg alt="..."> | Lighthouse |
SSR vs CSR 的 SEO 差异
ts
// 需要SEO的页面 → 保持 SSR
routeRules: {
'/': { swr: 3600 }, // 首页 SSR + 缓存
'/blog/**': { isr: 60 }, // 博客 ISR
'/about': { prerender: true }, // 关于页预渲染
}
// 不需要SEO的页面 → CSR
routeRules: {
'/admin/**': { ssr: false }, // 后台 CSR
'/dashboard/**': { ssr: false },// 仪表盘 CSR
}性能优化体系
优化优先级
不是所有优化都值得做。按投入产出比排序:
| 优先级 | 优化项 | 效果 | 实施难度 |
|---|---|---|---|
| 🥇 | 路由缓存(SWR/ISR) | 响应速度提升 10x+ | 低(改 nuxt.config.ts) |
| 🥇 | 图片优化(NuxtImg) | 页面体积减少 50%+ | 低(替换 <img>) |
| 🥈 | 懒加载非关键组件 | 首屏 JS 减少 30%+ | 低(加 Lazy 前缀) |
| 🥈 | Payload 精简(pick/transform) | HTML 体积减少 | 中 |
| 🥉 | 第三方库按需引入 | JS 体积减少 | 中 |
| 🥉 | 字体优化(@nuxt/fonts) | CLS 降低 | 低 |
优化原则
先做低成本高收益的(前 4 项),再做高成本中等收益的。不要为了 1% 的提升花一周时间。
生产环境必备配置
ts
// nuxt.config.ts
export default defineNuxtConfig({
// 路由缓存:根据页面特性设置
routeRules: {
'/': { swr: 3600 },
'/blog/**': { isr: 60 },
'/admin/**': { ssr: false },
},
// 图片优化
modules: ['@nuxt/image'],
// SEO
modules: ['@nuxtjs/sitemap', '@nuxtjs/robots'],
// Payload 优化
experimental: {
payloadExtraction: 'client',
},
})独立开发者的项目启动清单
从零开始一个 Nuxt 项目时,建议按以下顺序搭建:
text
1. npx nuxi@latest init my-app
2. 配置 nuxt.config.ts(runtimeConfig + routeRules)
3. 创建 .env + .env.example
4. 安装核心模块:@pinia/nuxt、@nuxt/image
5. 搭建目录结构:shared/types/ + shared/utils/
6. 创建 default 布局(导航栏 + 页脚)
7. 封装 useApiFetch(createUseFetch)
8. 搭建认证流程(server/api/auth/ + useAuth composable)
9. 创建错误页面(app/error.vue)
10. 配置 SEO(useServerSeoMeta + sitemap + robots)为什么这个顺序?
先搭基础设施(配置、类型、API 封装),再搭 UI(布局、页面),最后加功能。这样可以避免后面大规模重构。
知识脉络
text
全书学习完毕 → 最佳实践
│
├─→ 回顾:项目架构(02-目录结构 + 03-核心概念)
├─→ 回顾:数据获取模式(06-数据获取)
├─→ 回顾:SSR 安全(03-核心概念/01-渲染模式)
├─→ 回顾:错误处理(11-错误处理)
├─→ 回顾:SEO(10-SEO与元数据)
└─→ 实战:21-实战项目