Vue3与Nuxt3专家Skill vue-nuxt-expert

此技能专注于使用Vue 3和Nuxt 3进行前端开发,构建类型安全、高性能的Web应用程序。涵盖Vue 3 Composition API、Nuxt 3服务器端渲染(SSR)、静态站点生成(SSG)、Pinia状态管理、性能优化和客户端安全。适用于开发现代、响应式的前端项目。关键词:Vue 3, Nuxt 3, 前端开发, Composition API, TypeScript, 性能优化, 网络安全。

前端开发 0 次安装 0 次浏览 更新于 3/15/2026

Vue 3 与 Nuxt 3 专家

第1部分:概述

风险级别: 中等

专业领域:

  • Vue 3.4+ 使用 Composition API 和 TypeScript
  • Nuxt 3.10+ 服务器端渲染(SSR)和静态站点生成(SSG)
  • 使用 Pinia 和 composables 进行状态管理
  • 性能优化和核心Web指标
  • 客户端安全(XSS, CSRF, 注入攻击)
  • 现代构建工具(Vite, Nitro)

目标用户: 构建现代、高性能、类型安全Web应用的前端工程师

关键焦点: 类型安全的组件架构、可组合逻辑、SSR/SSG模式、客户端安全


第2部分:核心原则

  1. 测试驱动开发优先 - 使用Vitest和Vue Test Utils在实现前编写测试
  2. 性能意识 - 优化响应性,使用computed而非methods,实现懒加载
  3. 类型安全 - 使用TypeScript严格模式,适当的组件和composable类型
  4. 可组合优先 - 将可重用逻辑提取到composables中以实现最大可重用性
  5. 安全意识 - 防止XSS,验证输入,配置CSP头部
  6. SSR兼容 - 始终考虑服务器端渲染的影响

第3部分:实现工作流(TDD)

步骤1: 首先编写失败测试

// tests/components/UserCard.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import UserCard from '~/components/UserCard.vue'

describe('UserCard', () => {
  it('displays user name and email', () => {
    const wrapper = mount(UserCard, {
      props: {
        user: {
          id: '1',
          name: 'John Doe',
          email: 'john@example.com'
        }
      },
      global: {
        plugins: [createTestingPinia()]
      }
    })

    expect(wrapper.text()).toContain('John Doe')
    expect(wrapper.text()).toContain('john@example.com')
  })

  it('emits select event when clicked', async () => {
    const wrapper = mount(UserCard, {
      props: {
        user: { id: '1', name: 'John', email: 'john@test.com' }
      }
    })

    await wrapper.trigger('click')
    expect(wrapper.emitted('select')).toBeTruthy()
    expect(wrapper.emitted('select')[0]).toEqual(['1'])
  })

  it('shows loading state', () => {
    const wrapper = mount(UserCard, {
      props: {
        user: null,
        loading: true
      }
    })

    expect(wrapper.find('[data-testid="loading-skeleton"]').exists()).toBe(true)
  })
})

步骤2: 编写Composable测试

// tests/composables/useAsyncData.test.ts
import { describe, it, expect, vi } from 'vitest'
import { useAsyncData } from '~/composables/useAsyncData'

describe('useAsyncData', () => {
  it('fetches data successfully', async () => {
    const mockData = { id: 1, name: 'Test' }
    const fetcher = vi.fn().mockResolvedValue(mockData)

    const { data, loading, error, execute } = useAsyncData(fetcher, {
      immediate: false
    })

    expect(data.value).toBeNull()
    expect(loading.value).toBe(false)

    await execute()

    expect(fetcher).toHaveBeenCalledOnce()
    expect(data.value).toEqual(mockData)
    expect(error.value).toBeNull()
  })

  it('handles errors', async () => {
    const mockError = new Error('Network error')
    const fetcher = vi.fn().mockRejectedValue(mockError)
    const onError = vi.fn()

    const { data, error, execute } = useAsyncData(fetcher, {
      immediate: false,
      onError
    })

    await execute()

    expect(error.value).toBe(mockError)
    expect(data.value).toBeNull()
    expect(onError).toHaveBeenCalledWith(mockError)
  })

  it('transforms data', async () => {
    const fetcher = vi.fn().mockResolvedValue({ users: [{ id: 1 }] })
    const transform = (data: any) => data.users

    const { data, execute } = useAsyncData(fetcher, {
      immediate: false,
      transform
    })

    await execute()

    expect(data.value).toEqual([{ id: 1 }])
  })
})

步骤3: 编写Pinia Store测试

// tests/stores/user.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '~/stores/user'

// Mock $fetch
vi.stubGlobal('$fetch', vi.fn())

describe('useUserStore', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    vi.clearAllMocks()
  })

  it('logs in user successfully', async () => {
    const mockResponse = {
      user: { id: '1', email: 'test@test.com', name: 'Test', roles: [] },
      token: 'mock-token'
    }
    vi.mocked($fetch).mockResolvedValue(mockResponse)

    const store = useUserStore()
    await store.login('test@test.com', 'password')

    expect($fetch).toHaveBeenCalledWith('/api/auth/login', {
      method: 'POST',
      body: { email: 'test@test.com', password: 'password' }
    })
    expect(store.currentUser).toEqual(mockResponse.user)
    expect(store.isAuthenticated).toBe(true)
  })

  it('checks user roles correctly', async () => {
    const store = useUserStore()
    store.currentUser = {
      id: '1',
      email: 'admin@test.com',
      name: 'Admin',
      roles: ['admin', 'user']
    }

    expect(store.hasRole('admin')).toBe(true)
    expect(store.hasRole('superadmin')).toBe(false)
  })

  it('clears state on logout', async () => {
    vi.mocked($fetch).mockResolvedValue({})

    const store = useUserStore()
    store.currentUser = { id: '1', email: 'test@test.com', name: 'Test', roles: [] }
    store.token = 'token'

    await store.logout()

    expect(store.currentUser).toBeNull()
    expect(store.token).toBeNull()
    expect(store.isAuthenticated).toBe(false)
  })
})

步骤4: 实现最小代码以通过测试

<!-- components/UserCard.vue -->
<script setup lang="ts">
interface User {
  id: string
  name: string
  email: string
}

const props = defineProps<{
  user: User | null
  loading?: boolean
}>()

const emit = defineEmits<{
  select: [id: string]
}>()

const handleClick = () => {
  if (props.user) {
    emit('select', props.user.id)
  }
}
</script>

<template>
  <div @click="handleClick" class="user-card">
    <div v-if="loading" data-testid="loading-skeleton" class="skeleton">
      Loading...
    </div>
    <template v-else-if="user">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </template>
  </div>
</template>

步骤5: 运行完整验证

# 运行所有测试
npm run test

# 运行测试并覆盖
npm run test:coverage

# 运行特定测试文件
npm run test tests/components/UserCard.test.ts

# 类型检查
npm run typecheck

# 代码检查
npm run lint

# 构建以确保无错误
npm run build

第4部分:性能模式

模式1: 使用Computed而非Methods

差 - 每次渲染调用方法:

<script setup lang="ts">
const items = ref([...])

// ❌ 差: 每次渲染都重新计算
const getFilteredItems = () => {
  return items.value.filter(item => item.active)
}
</script>

<template>
  <div v-for="item in getFilteredItems()" :key="item.id">
    {{ item.name }}
  </div>
</template>

好 - Computed缓存结果:

<script setup lang="ts">
const items = ref([...])

// ✅ 好: 仅在items变化时重新计算
const filteredItems = computed(() => {
  return items.value.filter(item => item.active)
})
</script>

<template>
  <div v-for="item in filteredItems" :key="item.id">
    {{ item.name }}
  </div>
</template>

模式2: 对大型对象使用shallowRef

差 - 对大型对象的深度响应性:

// ❌ 差: 为整个对象创建深度响应代理
const largeDataset = ref<DataItem[]>([])

// 每个嵌套属性都变为响应式
largeDataset.value = await fetchLargeDataset()

好 - 不需要深度跟踪时使用浅响应性:

// ✅ 好: 只跟踪引用,而非嵌套属性
const largeDataset = shallowRef<DataItem[]>([])

// 手动触发更新
largeDataset.value = await fetchLargeDataset()

// 就地突变时使用triggerRef
largeDataset.value.push(newItem)
triggerRef(largeDataset)

模式3: 对昂贵列表使用v-memo

差 - 任何变化时重新渲染所有项:

<template>
  <!-- ❌ 差: 任何变化时所有项重新渲染 -->
  <div v-for="item in items" :key="item.id">
    <ExpensiveComponent :data="item" />
  </div>
</template>

好 - 对未变化项进行记忆化:

<template>
  <!-- ✅ 好: 仅在item.id或item.updated变化时重新渲染 -->
  <div
    v-for="item in items"
    :key="item.id"
    v-memo="[item.id, item.updated]"
  >
    <ExpensiveComponent :data="item" />
  </div>
</template>

模式4: 懒加载组件

差 - 所有组件预先加载:

<script setup lang="ts">
// ❌ 差: 即使未显示也导入
import HeavyChart from '~/components/HeavyChart.vue'
import AdminPanel from '~/components/AdminPanel.vue'
import DataTable from '~/components/DataTable.vue'
</script>

好 - 按需加载组件:

<script setup lang="ts">
// ✅ 好: 仅渲染时加载
const HeavyChart = defineAsyncComponent(() =>
  import('~/components/HeavyChart.vue')
)

const AdminPanel = defineAsyncComponent({
  loader: () => import('~/components/AdminPanel.vue'),
  loadingComponent: LoadingSpinner,
  delay: 200,
  timeout: 5000
})

// 使用Nuxt lazy前缀
// components/lazy/DataTable.vue自动变为懒加载
</script>

<template>
  <HeavyChart v-if="showChart" />
  <AdminPanel v-if="isAdmin" />
  <LazyDataTable v-if="showTable" />
</template>

模式5: 大型列表的虚拟滚动

差 - 一次性渲染所有项:

<template>
  <!-- ❌ 差: 渲染10,000个DOM节点 -->
  <div v-for="item in tenThousandItems" :key="item.id">
    {{ item.name }}
  </div>
</template>

好 - 仅渲染可见项:

<script setup lang="ts">
import { useVirtualList } from '@vueuse/core'

const items = ref(generateLargeList(10000))

const { list, containerProps, wrapperProps } = useVirtualList(items, {
  itemHeight: 50,
  overscan: 5
})
</script>

<template>
  <!-- ✅ 好: 仅渲染约20个可见项 -->
  <div v-bind="containerProps" class="h-[400px] overflow-auto">
    <div v-bind="wrapperProps">
      <div v-for="{ data, index } in list" :key="index" class="h-[50px]">
        {{ data.name }}
      </div>
    </div>
  </div>
</template>

模式6: 优化观察器

差 - 不必要地观察整个对象:

// ❌ 差: 任何属性变化触发
watch(form, () => {
  validateForm()
}, { deep: true })

好 - 观察特定属性:

// ✅ 好: 仅在email变化触发
watch(() => form.email, (newEmail) => {
  validateEmail(newEmail)
})

// ✅ 好: 观察多个特定属性
watch(
  [() => form.email, () => form.password],
  ([email, password]) => {
    validateCredentials(email, password)
  }
)

模式7: 防抖昂贵操作

差 - 每次按键运行:

<script setup lang="ts">
const searchQuery = ref('')

// ❌ 差: 每次按键API调用
watch(searchQuery, async (query) => {
  results.value = await searchAPI(query)
})
</script>

好 - 防抖操作:

<script setup lang="ts">
import { useDebounceFn } from '@vueuse/core'

const searchQuery = ref('')

// ✅ 好: 等待用户停止输入
const debouncedSearch = useDebounceFn(async (query: string) => {
  results.value = await searchAPI(query)
}, 300)

watch(searchQuery, (query) => {
  debouncedSearch(query)
})
</script>

第5部分:核心职责

1. 组件架构与Composition API

  • 使用script setup语法设计可扩展组件层次
  • 遵循Vue 3最佳实践创建可重用composables
  • 为组件和composables实现适当的TypeScript类型
  • 使用ref、reactive、computed和watch管理响应性
  • 通过适当的key使用和v-memo优化组件渲染

2. Nuxt 3应用开发

  • 为SSR、SSG或混合渲染配置Nuxt 3应用
  • 实现文件路由,包括动态路由和中间件
  • 使用Nitro创建服务器路由和API端点
  • 优化包大小和代码分割
  • 配置自动导入和模块层架构

3. 状态管理

  • 设计支持TypeScript的Pinia存储
  • 实现状态持久化和水合策略
  • 创建跨组件逻辑的共享composables
  • 管理全局状态与局部组件状态
  • 处理异步状态和加载模式

4. 性能优化

  • 实现路由和组件的懒加载
  • 使用Nuxt Image模块优化图像
  • 配置缓存策略(客户端、服务器、CDN)
  • 监控和改进核心Web指标
  • 实现大型列表的虚拟滚动

5. 类型安全与开发者体验

  • 配置TypeScript严格模式
  • 为Nuxt自动导入生成类型
  • 类型化API响应和存储状态
  • 为Vue/Nuxt设置ESLint和Prettier
  • 实现适当的错误处理和边界

6. 客户端安全

  • 通过适当的模板净化防止XSS
  • 配置内容安全策略(CSP)
  • 验证和净化用户输入
  • 实现安全认证流程
  • 防止CSRF攻击

第6部分:前7大实现模式

模式1: 可组合优先架构

使用composables提取和跨组件重用逻辑:

// composables/useAsyncData.ts
import { ref, type Ref } from 'vue'

export interface UseAsyncDataOptions<T> {
  immediate?: boolean
  onError?: (error: Error) => void
  transform?: (data: any) => T
}

export function useAsyncData<T>(
  fetcher: () => Promise<T>,
  options: UseAsyncDataOptions<T> = {}
) {
  const { immediate = true, onError, transform } = options

  const data: Ref<T | null> = ref(null)
  const error: Ref<Error | null> = ref(null)
  const loading = ref(false)

  const execute = async () => {
    loading.value = true
    error.value = null

    try {
      const result = await fetcher()
      data.value = transform ? transform(result) : result
    } catch (e) {
      error.value = e as Error
      onError?.(e as Error)
    } finally {
      loading.value = false
    }
  }

  if (immediate) execute()

  return { data, error, loading, execute }
}

用法:

<script setup lang="ts">
import { useAsyncData } from '~/composables/useAsyncData'

interface User {
  id: string
  name: string
}

const { data: user, loading, error } = useAsyncData<User>(
  () => $fetch('/api/user/me'),
  { immediate: true }
)
</script>

模式2: 类型安全的Pinia存储

使用组合API创建强类型存储:

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export interface User {
  id: string
  email: string
  name: string
  roles: string[]
}

export const useUserStore = defineStore('user', () => {
  // 状态
  const currentUser = ref<User | null>(null)
  const token = ref<string | null>(null)

  // 获取器
  const isAuthenticated = computed(() => !!currentUser.value)
  const hasRole = computed(() => (role: string) =>
    currentUser.value?.roles.includes(role) ?? false
  )

  // 操作
  async function login(email: string, password: string) {
    const response = await $fetch<{ user: User; token: string }>('/api/auth/login', {
      method: 'POST',
      body: { email, password }
    })

    currentUser.value = response.user
    token.value = response.token

    // 持久化token
    if (process.client) {
      localStorage.setItem('auth_token', response.token)
    }
  }

  async function logout() {
    await $fetch('/api/auth/logout', { method: 'POST' })
    currentUser.value = null
    token.value = null

    if (process.client) {
      localStorage.removeItem('auth_token')
    }
  }

  async function fetchCurrentUser() {
    if (!token.value) return

    try {
      const user = await $fetch<User>('/api/user/me', {
        headers: { Authorization: `Bearer ${token.value}` }
      })
      currentUser.value = user
    } catch (error) {
      // Token无效,清除认证状态
      await logout()
    }
  }

  return {
    currentUser,
    token,
    isAuthenticated,
    hasRole,
    login,
    logout,
    fetchCurrentUser
  }
})

模式3: Nuxt 3中间件和路由守卫

实现认证和授权中间件:

// middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const userStore = useUserStore()
  const publicRoutes = ['/login', '/register', '/forgot-password']

  // 允许公共路由
  if (publicRoutes.includes(to.path)) {
    return
  }

  // 如果未认证,重定向到登录
  if (!userStore.isAuthenticated) {
    return navigateTo('/login', { redirectCode: 401 })
  }

  // 检查基于角色的访问
  if (to.meta.requiresAdmin && !userStore.hasRole('admin')) {
    return abortNavigation({
      statusCode: 403,
      message: '访问被拒绝'
    })
  }
})

带元数据的页面:

<script setup lang="ts">
definePageMeta({
  requiresAdmin: true,
  layout: 'admin'
})

const users = await useFetch('/api/admin/users')
</script>

模式4: 带验证的服务器API路由

使用输入验证创建类型安全API端点:

// server/api/users/[id].post.ts
import { z } from 'zod'
import { createError } from 'h3'

const updateUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().int().min(18).max(120).optional()
})

export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')

  if (!id) {
    throw createError({
      statusCode: 400,
      message: '用户ID必填'
    })
  }

  // 验证请求体
  const body = await readBody(event)
  const result = updateUserSchema.safeParse(body)

  if (!result.success) {
    throw createError({
      statusCode: 400,
      message: '请求数据无效',
      data: result.error.format()
    })
  }

  // 检查认证
  const session = await requireUserSession(event)

  // 检查授权(用户只能更新自己,除非管理员)
  if (session.user.id !== id && !session.user.roles.includes('admin')) {
    throw createError({
      statusCode: 403,
      message: '无权更新此用户'
    })
  }

  // 在数据库中更新用户
  const updatedUser = await db.users.update(id, result.data)

  return updatedUser
})

模式5: 优化组件加载

实现策略性代码分割和懒加载:

<script setup lang="ts">
// 懒加载重型组件
const HeavyChart = defineAsyncComponent(() =>
  import('~/components/HeavyChart.vue')
)

const AdminPanel = defineAsyncComponent({
  loader: () => import('~/components/AdminPanel.vue'),
  loadingComponent: () => h('div', '加载中...'),
  delay: 200,
  timeout: 3000
})

const showChart = ref(false)
const userStore = useUserStore()

// 仅在需要时加载
const loadChart = () => {
  showChart.value = true
}
</script>

<template>
  <div>
    <button @click="loadChart">显示图表</button>

    <!-- 仅在showChart为true时加载组件 -->
    <HeavyChart v-if="showChart" :data="chartData" />

    <!-- 仅对管理员显示管理面板 -->
    <AdminPanel v-if="userStore.hasRole('admin')" />
  </div>
</template>

Nuxt配置以优化分割:

// nuxt.config.ts
export default defineNuxtConfig({
  vite: {
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            'vendor-vue': ['vue', 'vue-router', 'pinia'],
            'vendor-ui': ['@headlessui/vue', '@heroicons/vue'],
          }
        }
      }
    }
  },

  experimental: {
    payloadExtraction: true, // 提取有效负载以更好缓存
    componentIslands: true    // 部分水合岛架构
  }
})

模式6: VueUse集成用于常见逻辑

利用VueUse composables实现健壮功能:

<script setup lang="ts">
import { useLocalStorage, useMediaQuery, useIntersectionObserver } from '@vueuse/core'
import { ref, watch } from 'vue'

// 持久化深色模式
const isDark = useLocalStorage('dark-mode', false)

// 响应式断点
const isMobile = useMediaQuery('(max-width: 768px)')
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)')
const isDesktop = useMediaQuery('(min-width: 1025px)')

// 带交叉观察器的无限滚动
const target = ref<HTMLElement | null>(null)
const isVisible = ref(false)

useIntersectionObserver(
  target,
  ([{ isIntersecting }]) => {
    isVisible.value = isIntersecting
  },
  { threshold: 0.5 }
)

// 当目标可见时加载更多
watch(isVisible, (visible) => {
  if (visible && !loading.value) {
    loadMore()
  }
})

const loadMore = async () => {
  // 加载更多项
}
</script>

<template>
  <div :class="{ dark: isDark }">
    <button @click="isDark = !isDark">
      切换{{ isDark ? '浅色' : '深色' }}模式
    </button>

    <div v-if="isMobile">移动视图</div>
    <div v-else-if="isTablet">平板视图</div>
    <div v-else>桌面视图</div>

    <!-- 项列表 -->
    <div v-for="item in items" :key="item.id">
      {{ item.name }}
    </div>

    <!-- 无限滚动的交叉观察器目标 -->
    <div ref="target" class="loading-trigger">
      <span v-if="isVisible">加载更多...</span>
    </div>
  </div>
</template>

模式7: SSR安全的数据获取

正确处理SSR/SSG的数据获取:

<script setup lang="ts">
// ✅ 正确: 使用Nuxt数据获取composables
// 这些在服务器和客户端都工作,带自动水合

// 基本获取
const { data: posts } = await useFetch('/api/posts', {
  key: 'posts-list',
  transform: (data) => data.posts,
  getCachedData: (key) => useNuxtApp().static.data[key]
})

// 带参数
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.id}`, {
  key: `post-${route.params.id}`,
  watch: [() => route.params.id] // ID变化时重新获取
})

// 带懒加载(初始仅在客户端)
const { data: comments, pending } = await useLazyFetch(`/api/posts/${route.params.id}/comments`)

// 使用useAsyncData进行自定义异步操作
const { data: userData, refresh } = await useAsyncData(
  'user-profile',
  async () => {
    const [profile, settings] = await Promise.all([
      $fetch('/api/user/profile'),
      $fetch('/api/user/settings')
    ])
    return { profile, settings }
  },
  {
    server: true,  // 在服务器获取
    lazy: false,   // 渲染前等待数据
    default: () => ({ profile: null, settings: null })
  }
)

// ❌ 错误: 直接fetch调用会执行两次(服务器 + 客户端)
// const response = await fetch('/api/posts') // 不要这样做!
</script>

<template>
  <div>
    <article v-if="post">
      <h1>{{ post.title }}</h1>
      <p>{{ post.content }}</p>
    </article>

    <section v-if="!pending">
      <h2>评论 ({{ comments?.length || 0 }})</h2>
      <div v-for="comment in comments" :key="comment.id">
        {{ comment.text }}
      </div>
    </section>
    <div v-else>加载评论中...</div>
  </div>
</template>

查看 references/advanced-patterns.md 获取更多模式,包括插件、模块和高级composables。


第7部分:安全

风险级别: 中等 - 客户端应用易受XSS、注入和数据暴露攻击

前3大关键漏洞

1. 跨站脚本攻击(XSS)

风险: 攻击者通过用户输入注入恶意脚本,窃取数据或执行未授权操作。

预防:

<script setup lang="ts">
import DOMPurify from 'isomorphic-dompurify'

const userInput = ref('')
const sanitizedHtml = computed(() => DOMPurify.sanitize(userInput.value))

// ✅ 安全: Vue的模板绑定自动转义HTML
const displayText = ref('<script>alert("XSS")</script>')
</script>

<template>
  <!-- ✅ 安全: 自动转义 -->
  <div>{{ displayText }}</div>

  <!-- ⚠️ 危险: 仅与净化内容使用 -->
  <div v-html="sanitizedHtml"></div>

  <!-- ❌ 绝不要: 原始用户输入 -->
  <!-- <div v-html="userInput"></div> -->
</template>

2. 不安全数据暴露

风险: 敏感数据通过客户端代码、API响应或状态管理泄露。

预防:

// ✅ 服务器API路由 - 在服务器上保持秘密
// server/api/payment.post.ts
export default defineEventHandler(async (event) => {
  const apiKey = useRuntimeConfig().stripeSecretKey // 仅服务器

  const payment = await stripe.charges.create({
    amount: 1000,
    currency: 'usd',
    source: req.body.token
  }, {
    apiKey // 永不暴露给客户端
  })

  // 仅返回必要数据
  return {
    id: payment.id,
    status: payment.status,
    amount: payment.amount
  }
})

3. CSRF(跨站请求伪造)

风险: 攻击者诱使用户在认证会话中执行不需要的操作。

预防:

// nuxt.config.ts
export default defineNuxtConfig({
  // 为SSR启用CSRF保护
  security: {
    headers: {
      crossOriginEmbedderPolicy: 'require-corp',
      crossOriginOpenerPolicy: 'same-origin',
      crossOriginResourcePolicy: 'same-origin'
    }
  }
})

// API路由中间件
// server/middleware/csrf.ts
export default defineEventHandler((event) => {
  if (event.method !== 'GET' && event.method !== 'HEAD') {
    const origin = getHeader(event, 'origin')
    const host = getHeader(event, 'host')

    if (origin && !origin.includes(host)) {
      throw createError({
        statusCode: 403,
        message: 'CSRF验证失败'
      })
    }
  }
})

OWASP Top 10 映射

OWASP 类别 相关性 在Vue/Nuxt中的缓解措施
A03:2021 注入 输入验证、参数化查询、净化
A05:2021 安全配置错误 中等 CSP头部、安全默认值、环境配置
A06:2021 脆弱组件 中等 定期更新、审计依赖项、Snyk/npm审计
A07:2021 认证失败 安全会话管理、适当令牌处理
A08:2021 数据完整性失败 中等 签名有效负载、完整性检查、仅HTTPS

详细安全示例和完整OWASP覆盖,请查看 references/security-examples.md


第8部分:常见错误

错误1: 解构时失去响应性

问题:

// ❌ 错误: 失去响应性
const userStore = useUserStore()
const { currentUser } = userStore // 非响应式!

watch(currentUser, () => {
  console.log('这永远不会触发!')
})

解决方案:

// ✅ 正确: 使用storeToRefs保持响应性
import { storeToRefs } from 'pinia'

const userStore = useUserStore()
const { currentUser } = storeToRefs(userStore) // 响应式!

watch(currentUser, () => {
  console.log('这有效!')
})

// 或直接访问
watch(() => userStore.currentUser, () => {
  console.log('这也有效!')
})

错误2: 事件监听器导致内存泄漏

问题:

// ❌ 错误: 事件监听器未清理
onMounted(() => {
  window.addEventListener('resize', handleResize)
})
// 组件卸载但监听器持久!

解决方案:

// ✅ 正确: 在onUnmounted中清理
onMounted(() => {
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})

// ✅ 更好: 使用VueUse composable
import { useEventListener } from '@vueuse/core'
useEventListener(window, 'resize', handleResize) // 自动清理!

错误3: 不正确的useFetch用法

问题:

// ❌ 错误: 在事件处理器中使用useFetch
const handleClick = async () => {
  const { data } = await useFetch('/api/data') // 错误!不允许在函数中
}

// ❌ 错误: 在条件内
if (someCondition) {
  const { data } = await useFetch('/api/data') // 错误!必须在顶级
}

解决方案:

// ✅ 正确: 对程序化调用使用$fetch
const handleClick = async () => {
  const data = await $fetch('/api/data') // 在函数中工作
}

// ✅ 正确: 在组件顶级使用useFetch
const { data, refresh } = await useFetch('/api/data', {
  immediate: false
})

const handleClick = () => {
  refresh() // 触发重新获取
}

错误4: 未处理SSR/客户端差异

问题:

// ❌ 错误: 在SSR期间访问浏览器API
const windowWidth = ref(window.innerWidth) // 错误!服务器上window未定义

onMounted(() => {
  localStorage.setItem('key', 'value') // 错误!服务器上localStorage未定义
})

解决方案:

// ✅ 正确: 检查环境
const windowWidth = ref(0)

onMounted(() => {
  if (process.client) {
    windowWidth.value = window.innerWidth
  }
})

// ✅ 更好: 使用SSR安全的VueUse
import { useWindowSize, useLocalStorage } from '@vueuse/core'

const { width } = useWindowSize() // SSR安全
const stored = useLocalStorage('key', 'default') // SSR安全

错误5: 低效观察器

问题:

// ❌ 错误: 观察整个对象(任何属性变化触发)
const form = reactive({
  name: '',
  email: '',
  phone: '',
  address: ''
})

watch(form, () => {
  console.log('任何字段变化都触发!')
})

解决方案:

// ✅ 正确: 观察特定属性
watch(() => form.email, (newEmail) => {
  validateEmail(newEmail)
})

// ✅ 正确: 观察多个特定属性
watch([() => form.email, () => form.phone], ([email, phone]) => {
  validateContactInfo(email, phone)
})

// ✅ 正确: 需要时的深度观察带立即标志
watch(form, () => {
  saveFormDraft(form)
}, {
  deep: true,
  debounce: 500 // 防抖以避免过多调用
})

第9部分:测试

测试配置

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'happy-dom',
    globals: true,
    setupFiles: ['./tests/setup.ts'],
    include: ['tests/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'tests/']
    }
  },
  resolve: {
    alias: {
      '~': resolve(__dirname, './')
    }
  }
})

测试设置文件

// tests/setup.ts
import { config } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'

// 全局插件
config.global.plugins = [createTestingPinia()]

// 模拟Nuxt composables
vi.mock('#app', () => ({
  useNuxtApp: () => ({ $fetch: vi.fn() }),
  useRuntimeConfig: () => ({ public: {} }),
  useFetch: vi.fn(),
  useAsyncData: vi.fn(),
  navigateTo: vi.fn(),
  definePageMeta: vi.fn()
}))

// 全局模拟$fetch
vi.stubGlobal('$fetch', vi.fn())

组件测试模式

// tests/components/Form.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import Form from '~/components/Form.vue'

describe('Form', () => {
  it('validates required fields', async () => {
    const wrapper = mount(Form)

    await wrapper.find('form').trigger('submit')

    expect(wrapper.find('.error').text()).toContain('姓名必填')
  })

  it('submits valid data', async () => {
    const onSubmit = vi.fn()
    const wrapper = mount(Form, {
      props: { onSubmit }
    })

    await wrapper.find('input[name="name"]').setValue('John')
    await wrapper.find('input[name="email"]').setValue('john@test.com')
    await wrapper.find('form').trigger('submit')
    await flushPromises()

    expect(onSubmit).toHaveBeenCalledWith({
      name: 'John',
      email: 'john@test.com'
    })
  })

  it('shows loading state during submission', async () => {
    const wrapper = mount(Form, {
      props: {
        onSubmit: () => new Promise(r => setTimeout(r, 100))
      }
    })

    await wrapper.find('input[name="name"]').setValue('John')
    await wrapper.find('form').trigger('submit')

    expect(wrapper.find('button').attributes('disabled')).toBeDefined()
    expect(wrapper.find('.loading').exists()).toBe(true)
  })
})

测试异步操作

// tests/composables/useApi.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import { useApi } from '~/composables/useApi'

describe('useApi', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('handles concurrent requests', async () => {
    const results = ['first', 'second']
    let callCount = 0

    vi.mocked($fetch).mockImplementation(() =>
      Promise.resolve(results[callCount++])
    )

    const { data, execute } = useApi('/api/test')

    // 触发两个请求
    execute()
    execute()
    await flushPromises()

    // 应获取最新结果
    expect(data.value).toBe('second')
  })

  it('cancels pending request on new request', async () => {
    const abortSpy = vi.fn()
    vi.mocked($fetch).mockImplementation((_, opts) => {
      opts?.signal?.addEventListener('abort', abortSpy)
      return new Promise(() => {})
    })

    const { execute } = useApi('/api/test')

    execute()
    execute() // 应取消第一个

    expect(abortSpy).toHaveBeenCalled()
  })
})

第10部分:关键提醒

类型安全

  • 始终在 tsconfig.json 中启用TypeScript严格模式
  • 使用 defineProps<T>() 语法类型化所有组件属性
  • 为Nuxt自动导入生成类型:nuxt prepare
  • 使用运行时验证(Zod)用于API输入,而不仅是TypeScript

性能

  • 使用 useFetch/useAsyncData 进行数据获取(SSR兼容)
  • 实现路由懒加载:defineAsyncComponent()
  • 优化图像:使用Nuxt Image模块与适当格式(WebP, AVIF)
  • 监控包大小:nuxi analyze 并设置预算
  • 对不常变化的昂贵列表使用 v-memo

安全

  • 绝不要对未净化用户输入使用 v-html
  • nuxt.config.ts 中配置CSP头部
  • 验证所有客户端和服务器输入
  • 将秘密存储在 .env 文件中,永不客户端代码
  • 使用 httpOnly cookie处理敏感令牌

状态管理

  • 使用Pinia处理全局状态,composables处理共享逻辑
  • storeToRefs() 提取以保持响应性
  • 安全持久化认证状态(首选httpOnly cookie)
  • 登出时清除敏感状态
  • 避免属性传递:使用provide/inject或存储

SSR/SSG

  • 访问浏览器API前始终检查 process.client
  • 使用Nuxt数据获取composables,而非原始fetch
  • 配置 routeRules 用于页面级渲染策略
  • 需要时使用 <ClientOnly> 处理水合不匹配
  • 为静态资产设置适当缓存头部

开发者体验

  • 启用Nuxt DevTools进行调试
  • 使用Vue DevTools进行组件检查
  • 使用Vue/Nuxt配置设置ESLint + Prettier
  • 用Vitest + Vue Test Utils编写测试
  • 记录复杂composables和存储

第11部分:预实现检查清单

阶段1: 编码前

  • [ ] 识别需求 - 将用户故事/任务解析为特定验收标准
  • [ ] 设计组件结构 - 草绘组件层次和数据流
  • [ ] 规划composables - 识别要提取的可重用逻辑
  • [ ] 考虑SSR - 确定渲染策略(SSR/SSG/SPA)
  • [ ] 检查现有模式 - 审查代码库中类似组件/composables
  • [ ] 编写测试用例 - 为预期行为创建失败测试
  • [ ] 规划状态管理 - 决定本地与存储状态

阶段2: 实现期间

  • [ ] TDD循环 - 编写测试 -> 实现 -> 重构 -> 重复
  • [ ] 类型化所有内容 - 属性、事件、composable返回、API响应
  • [ ] 使用computed - 用于派生状态而非方法
  • [ ] 优化响应性 - 对大型对象使用shallowRef,观察特定属性
  • [ ] 处理边界情况 - 加载状态、错误、空数据
  • [ ] SSR安全 - 访问浏览器API前检查 process.client
  • [ ] 清理效果 - 使用onUnmounted或VueUse composables
  • [ ] 安全检查 - 无v-html与用户输入,验证输入

阶段3: 提交前

  • [ ] 所有测试通过 - 运行 npm run test
  • [ ] 类型检查通过 - 运行 npm run typecheck
  • [ ] 代码检查通过 - 运行 npm run lint
  • [ ] 构建成功 - 运行 npm run build
  • [ ] 手动测试 - 在浏览器中使用开发工具验证
  • [ ] 性能检查 - 无控制台警告,流畅渲染
  • [ ] 安全审查 - 无暴露秘密,输入已验证
  • [ ] 文档 - 复杂逻辑有注释/JSDoc

验证命令

# 提交前运行所有检查
npm run test && npm run typecheck && npm run lint && npm run build

# 开发期间快速验证
npm run dev  # 应启动无错误

# 完整测试套件带覆盖
npm run test:coverage

# E2E测试
npm run test:e2e

第12部分:总结

此技能提供构建现代、高性能、类型安全的Vue 3和Nuxt 3应用的专家知识。关键要点:

架构: 使用Composition API设计组件层次,将逻辑提取到composables中,使用Pinia管理状态。遵循可组合优先方法以实现最大可重用性。

Nuxt 3模式: 利用文件路由、自动导入和Nitro服务器进行全栈开发。配置渲染策略(SSR/SSG/混合)以实现最佳性能。

类型安全: 全程使用TypeScript严格模式。类型化组件、存储和API响应。结合编译时TypeScript与运行时验证(Zod)以实现健壮应用。

性能: 实现策略性代码分割、懒加载和优化数据获取与useFetch。监控核心Web指标并设置性能预算。

安全: 通过适当转义和净化防止XSS。验证所有输入。配置CSP头部。在服务器上保持秘密。实施CSRF保护。

常见陷阱: 使用storeToRefs保持响应性。清理事件监听器。使用正确数据获取API(useFetch vs $fetch)。处理SSR/客户端差异。编写高效观察器。

最佳实践:

  • 保持组件专注和可组合
  • 提取并独立测试composables
  • 使用VueUse处理常见模式
  • 配置ESLint和Prettier
  • 为关键逻辑编写测试
  • 监控性能和包大小

风险级别: 中等 - 主要关切是客户端安全(XSS, 数据暴露)和性能(包大小, SSR复杂性)。

高级模式,请查看 references/advanced-patterns.md。 详细安全示例,请查看 references/security-examples.md