Nuxt4 高频开发技巧 Top 30

本书前面的章节按模块讲解了 Nuxt4 的各个方面。但真实开发中,很多实用技巧散落在各处,不容易系统性地记住。本章把日常开发中最高频、最实用的 30 个技巧整理成速查手册——每个技巧都是"知道了就能立刻用,不知道就可能踩坑"的实战知识。按使用频率排序,前 10 个几乎每天都会用到。

1. Auto-import 与模块系统

技巧 1:不要手动 import Nuxt 内置 API

Nuxt 自动导入了 Vue 和 Nuxt 的核心 API,手动 import 不仅多余,还可能导致 SSR 问题。

<!-- ❌ 不要这样 -->
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useFetch, useRoute, navigateTo } from '#imports'
</script>
 
<!-- ✅ 直接使用 -->
<script setup lang="ts">
const count = ref(0)
const double = computed(() => count.value * 2)
const route = useRoute()
const { data } = await useFetch('/api/videos')
</script>

自动导入包括:refcomputedwatchonMounteduseFetchuseRouteuseRouternavigateTouseStateuseHeaduseSeoMeta 等。

技巧 2:自定义 composable 自动导入

composables/ 目录下的文件自动导入,但有命名约定:

composables/
├── useVideo.ts          ✅ 默认导出自动导入
├── useAuth.ts           ✅ 默认导出自动导入
├── utils/               ❌ 子目录默认不导入
└── index.ts             ✅ 命名导出自动导入

需要导入子目录时,在 nuxt.config.ts 中配置:

export default defineNuxtConfig({
  imports: {
    dirs: ['composables/**'],
  },
})

技巧 3:第三方库自动导入

export default defineNuxtConfig({
  imports: {
    presets: [
      { from: 'date-fns', imports: ['format', 'parseISO', 'differenceInDays'] },
      { from: 'lodash-es', imports: ['debounce', 'throttle', 'groupBy'] },
    ],
  },
})

技巧 4:检查哪些函数被自动导入

# 生成 .nuxt/types/imports.d.ts 查看所有自动导入
npx nuxi prepare
cat .nuxt/types/imports.d.ts

技巧 5:组件自动导入与路径别名

components/ 目录下的组件自动按路径层级注册:

components/
├── VideoCard.vue        → <VideoCard />
├── video/
│   ├── Player.vue       → <VideoPlayer />
│   └── Controls.vue     → <VideoControls />
└── admin/
    └── Sidebar.vue      → <AdminSidebar />

目录名自动变成组件名前缀。如果不想要前缀:

export default defineNuxtConfig({
  components: [
    { path: '~/components/admin', prefix: '' },
  ],
})

2. 数据获取

技巧 6:useFetch vs useAsyncData vs $fetch

APISSR去重场景
useFetch页面/组件数据加载(最常用)
useAsyncData需要自定义获取逻辑
$fetch事件处理中的 API 调用
<script setup lang="ts">
// 页面加载数据 → useFetch(SSR + 去重)
const { data: videos } = await useFetch('/api/videos')
 
// 按钮点击 → $fetch(无需 SSR)
async function handleLike(id: string) {
  await $fetch(`/api/videos/${id}/like`, { method: 'POST' })
}
</script>

技巧 7:useFetch 的 transform 减少传输量

const { data: titles } = await useFetch('/api/videos', {
  transform: (response) => response.items.map(v => ({
    id: v.id,
    title: v.title,
  })),
})
// SSR → CSR 传输的数据量大幅减少(payload 里只有 id + title)

技巧 8:useFetch 的 watch 自动重新请求

<script setup lang="ts">
const page = ref(1)
const category = ref('all')
 
const { data } = await useFetch('/api/videos', {
  query: { page, category },
  watch: [page, category],
})
// page 或 category 变化时自动重新请求,无需手动 watch
</script>

技巧 9:useAsyncData 的 getCachedData 缓存

const { data } = await useAsyncData('video-list', () => $fetch('/api/videos'), {
  getCachedData: (key, nuxtApp) => {
    return nuxtApp.payload.data[key] || nuxtApp.static.data[key]
  },
})
// 页面导航回来时优先用缓存,不重新请求

技巧 10:$fetch 在服务端直接调用无网络开销

// 服务端渲染时,$fetch('/api/videos') 不会发起 HTTP 请求
// 而是直接调用 server/api/videos.get.ts 的处理函数
// 零网络开销,就像函数调用一样快

3. 组件技巧

技巧 11:.client.vue 和 .server.vue 后缀

components/
├── VideoPlayer.client.vue    # 只在客户端渲染(浏览器 API 安全)
├── SkeletonCard.server.vue   # 只在服务端渲染(SSR 占位)
└── VideoCard.vue             # 同构渲染

.client.vue 组件在 SSR 时自动渲染为空,hydration 后加载。适合依赖浏览器 API 的组件(播放器、图表、地图)。

技巧 12:defineProps 的 TypeScript 泛型

<script setup lang="ts">
// ✅ TypeScript 泛型(推荐)
const props = defineProps<{
  video: Video
  showActions?: boolean
}>()
 
// ✅ 带默认值
const props = withDefaults(defineProps<{
  video: Video
  showActions?: boolean
}>(), {
  showActions: true,
})
</script>

技巧 13:v-model 多绑定

<!-- 父组件 -->
<VideoFilter v-model:category="category" v-model:sort="sortBy" />
 
<!-- 子组件 -->
<script setup lang="ts">
const category = defineModel<string>('category')
const sort = defineModel<string>('sort')
</script>

技巧 14:useTemplateRef 替代字符串 ref

<script setup lang="ts">
// ✅ Nuxt4 / Vue 3.5+ 推荐
const videoEl = useTemplateRef<HTMLVideoElement>('video')
 
onMounted(() => {
  videoEl.value?.play()
})
</script>
 
<template>
  <video ref="video" />
</template>

技巧 15:Teleport 与 ClientOnly 组合

<template>
  <ClientOnly>
    <Teleport to="body">
      <div class="modal-overlay">
        <VideoPreviewModal :video="video" @close="close" />
      </div>
    </Teleport>
  </ClientOnly>
</template>

Teleport 依赖 DOM,必须包在 ClientOnly.client.vue 中。

4. Server API 技巧

技巧 16:文件名即路由方法

server/api/
├── videos/
│   ├── index.get.ts        → GET /api/videos
│   ├── index.post.ts       → POST /api/videos
│   ├── [id].get.ts         → GET /api/videos/:id
│   ├── [id].put.ts         → PUT /api/videos/:id
│   ├── [id].delete.ts      → DELETE /api/videos/:id
│   └── [id]/
│       └── like.post.ts    → POST /api/videos/:id/like

技巧 17:Zod 校验 + 类型推导

// server/api/videos/index.post.ts
import { z } from 'zod'
 
const schema = z.object({
  title: z.string().min(1).max(100),
  description: z.string().optional(),
  categoryId: z.string().uuid(),
})
 
export default defineEventHandler(async (event) => {
  const body = await readValidatedBody(event, schema.parse)
  // body 自动推导为 { title: string, description?: string, categoryId: string }
})

技巧 18:defineCachedEventHandler 服务端缓存

// server/api/tags/popular.get.ts
export default defineCachedEventHandler(async () => {
  const tags = await db.select().from(tagsTable).orderBy(desc(tagsTable.count)).limit(20)
  return tags
}, {
  maxAge: 60 * 5,    // 缓存 5 分钟
  swr: true,          // 过期后仍返回旧数据,后台刷新
  name: 'popular-tags',
})

技巧 19:server/utils 自动导入

server/utils/ 下的函数在 API 路由中自动可用:

// server/utils/auth.ts
export function requireAuth(event: H3Event) {
  const session = await getUserSession(event)
  if (!session.user) throw createError({ statusCode: 401, message: 'Unauthorized' })
  return session.user
}
 
// server/api/videos/index.post.ts — 直接使用,无需 import
export default defineEventHandler(async (event) => {
  const user = await requireAuth(event)
  // ...
})

技巧 20:API 路由的错误处理

export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
 
  const video = await db.query.videos.findFirst({
    where: eq(videos.id, id!),
  })
 
  if (!video) {
    throw createError({
      statusCode: 404,
      message: `Video ${id} not found`,
    })
  }
 
  return video
})

5. 配置与状态

技巧 21:useRuntimeConfig 环境变量

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    dbUrl: '',                // 仅服务端(来自 NUXT_DB_URL)
    klingApiKey: '',          // 仅服务端(来自 NUXT_KLING_API_KEY)
    public: {
      appName: 'AI Video',   // 客户端可用(来自 NUXT_PUBLIC_APP_NAME)
    },
  },
})
// server 端
const config = useRuntimeConfig()
config.dbUrl  // ✅ 可访问
 
// client 端
const config = useRuntimeConfig()
config.public.appName  // ✅ 可访问
config.dbUrl           // ❌ undefined(私有)

技巧 22:useState 跨组件共享状态

// composables/useCounter.ts
export function useCounter() {
  const count = useState('counter', () => 0)  // SSR 安全
  const increment = () => count.value++
  return { count, increment }
}

useStateref 的区别:SSR 时数据会从服务端序列化到客户端,hydration 不会丢失状态。

技巧 23:app.config.ts 构建时常量

// app.config.ts(构建时确定,客户端可用,不需要环境变量)
export default defineAppConfig({
  theme: {
    primaryColor: '#3b82f6',
    darkMode: true,
  },
  pagination: {
    defaultPageSize: 20,
  },
})
 
// 使用
const appConfig = useAppConfig()
appConfig.theme.primaryColor

app.config.ts vs runtimeConfig.public:前者是构建时常量(可以做 HMR),后者是运行时变量(可以通过环境变量覆盖)。

6. 路由与中间件

技巧 24:页面级中间件

<!-- pages/admin/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
  middleware: 'auth',
  layout: 'admin',
})
</script>
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { loggedIn } = useUserSession()
  if (!loggedIn.value) {
    return navigateTo('/login')
  }
})

技巧 25:路由参数类型安全

<script setup lang="ts">
const route = useRoute()
// route.params.id 是 string | string[]
 
// 用类型断言确保安全
const id = computed(() => {
  const param = route.params.id
  return Array.isArray(param) ? param[0] : param
})
</script>
<template>
  <!-- 默认:进入视口时预加载 -->
  <NuxtLink to="/video/123">视频详情</NuxtLink>
 
  <!-- 禁用预加载(列表项太多时节省带宽) -->
  <NuxtLink to="/video/123" :prefetch="false">视频详情</NuxtLink>
 
  <!-- 外部链接自动处理 -->
  <NuxtLink to="https://example.com" external>外部链接</NuxtLink>
</template>

7. SEO 与 Head

技巧 27:useSeoMeta 统一 SEO

<script setup lang="ts">
const { data: video } = await useFetch(`/api/videos/${route.params.id}`)
 
useSeoMeta({
  title: () => video.value?.title ?? 'AI 视频平台',
  description: () => video.value?.description,
  ogImage: () => video.value?.thumbnail,
  ogType: 'video.other',
  twitterCard: 'summary_large_image',
})
</script>

useSeoMetauseHead 更好——它有严格的类型提示,不会写错 meta 名称。

技巧 28:useHead 动态 title 模板

// app.vue 或 nuxt.config.ts
useHead({
  titleTemplate: (title) => title ? `${title} - AI 视频平台` : 'AI 视频平台',
})
 
// pages/video/[id].vue
useHead({ title: video.value?.title })
// 渲染结果: "视频标题 - AI 视频平台"

8. 性能与调试

技巧 29:useNuxtData 共享缓存

// 页面 A 加载了视频列表
const { data: videos } = await useFetch('/api/videos', { key: 'videos' })
 
// 页面 B 可以直接读取缓存,不重新请求
const { data: cachedVideos } = useNuxtData('videos')

技巧 30:Nuxt DevTools 调试

Shift + Alt + D(或 Shift + Option + D)打开 Nuxt DevTools:

  • Pages:查看路由和当前参数
  • Components:查看组件树和 props
  • Composables:查看 useState / useAsyncData 的值
  • Server Routes:查看所有 API 路由和测试调用
  • Modules:查看已安装模块和配置
  • Payload:查看 SSR payload 数据大小
// nuxt.config.ts
export default defineNuxtConfig({
  devtools: { enabled: true },
})

本章小结

30 个技巧按使用频率分为七大类:

  • Auto-import(1-5):不手动 import、composable 自动导入、第三方库导入、组件路径别名
  • 数据获取(6-10):useFetch/useAsyncData/$fetch 选型、transform 优化、watch 自动请求、缓存、服务端零开销调用
  • 组件(11-15):.client/.server 后缀、defineProps 泛型、v-model 多绑定、useTemplateRef、Teleport+ClientOnly
  • Server API(16-20):文件名路由、Zod 校验、服务端缓存、utils 自动导入、错误处理
  • 配置状态(21-23):runtimeConfig、useState、app.config
  • 路由中间件(24-26):页面级中间件、参数类型安全、NuxtLink 预加载
  • SEO 与调试(27-30):useSeoMeta、title 模板、useNuxtData 缓存共享、DevTools