Skip to content

最佳实践

本章是全书的"经验总结"——将前面各章节的知识串联起来,形成完整的项目架构思维。如果你已经读完了前面的章节,这里帮你把碎片知识变成系统认知。

项目架构设计

目录职责边界

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 }
}

封装的好处

  1. key 集中管理:改 key 只改一处
  2. 业务逻辑内聚:状态 + 操作在一起,不分散
  3. 类型安全:返回值有完整类型
  4. 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 检查清单

检查项方法工具
每个页面有唯一的 titleuseServerSeoMeta查看源码 <title>
每个页面有 descriptionuseServerSeoMeta查看源码 <meta name="description">
有 canonical URLuseHead + <link rel="canonical">查看源码
有 Open Graph 标签useServerSeoMetaFacebook 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-实战项目

基于 Nuxt 4 官方文档整理编写