vue-nuxt-best-practices

📁 king-gd/vue-nuxt-best-practices 📅 8 days ago
2
总安装量
2
周安装量
#64205
全站排名
安装命令
npx skills add https://github.com/king-gd/vue-nuxt-best-practices --skill vue-nuxt-best-practices

Agent 安装分布

amp 2
gemini-cli 2
github-copilot 2
codex 2
kimi-cli 2
cursor 2

Skill 文档

Vue3 & Nuxt4 Best Practices

You are an expert Vue3 and Nuxt4 developer. When writing, reviewing, or refactoring code, always apply these best practices.

Priority Levels

  • CRITICAL: Must follow, violations cause bugs or major performance issues
  • HIGH: Strongly recommended, significant impact on performance/maintainability
  • MEDIUM: Recommended for better code quality
  • LOW: Nice to have, advanced optimizations

1. SSR & Hydration (Critical)

Avoid Hydration Mismatch

Server and client renders must produce identical HTML.

<!-- ❌ BAD: Different values on server vs client -->
<template>
  <div>{{ Date.now() }}</div>
  <div>{{ Math.random() }}</div>
  <div>{{ window.innerWidth }}</div>
</template>

<!-- ✅ GOOD: Use ClientOnly or onMounted -->
<template>
  <ClientOnly>
    <div>{{ currentTime }}</div>
    <template #fallback>Loading...</template>
  </ClientOnly>
</template>

<script setup lang="ts">
const currentTime = ref<number | null>(null)
onMounted(() => {
  currentTime.value = Date.now()
})
</script>

Use import.meta.client for Environment Checks

// ✅ GOOD: Nuxt 3/4 recommended way
if (import.meta.client) {
  // Client-only code
  initAnalytics()
}

if (import.meta.server) {
  // Server-only code
}

// ❌ BAD: Deprecated
if (process.client) { }

Use onMounted for Browser APIs

// ❌ BAD: Crashes on server
const width = window.innerWidth

// ✅ GOOD: Safe for SSR
const width = ref(0)
onMounted(() => {
  width.value = window.innerWidth
})

// ✅ BETTER: Use VueUse
import { useWindowSize } from '@vueuse/core'
const { width } = useWindowSize()

2. Data Fetching (Critical)

Use useFetch Instead of $fetch

// ❌ BAD: Fetches twice (server + client)
const data = await $fetch('/api/users')

// ✅ GOOD: SSR-aware, deduped, cached
const { data, pending, error, refresh } = await useFetch('/api/users')

Parallelize Independent Requests

// ❌ BAD: Sequential (slow)
const { data: user } = await useFetch('/api/user')
const { data: posts } = await useFetch('/api/posts')
const { data: comments } = await useFetch('/api/comments')

// ✅ GOOD: Parallel (fast)
const [{ data: user }, { data: posts }, { data: comments }] = await Promise.all([
  useFetch('/api/user'),
  useFetch('/api/posts'),
  useFetch('/api/comments')
])

Use Lazy for Non-Critical Data

// Critical data: blocks SSR
const { data: mainContent } = await useFetch('/api/content')

// Non-critical: loads after hydration
const { data: recommendations, pending } = useLazyFetch('/api/recommend')
const { data: comments } = useFetch('/api/comments', { lazy: true })

Set Unique Keys to Avoid Duplicate Requests

const { data } = await useFetch(`/api/article/${id}`, {
  key: `article-${id}`
})

3. Reactivity (High)

Use shallowRef for Large Objects

// ❌ BAD: Deep reactivity overhead for 10000 items
const tableData = ref<User[]>([])

// ✅ GOOD: Only .value is reactive
const tableData = shallowRef<User[]>([])

// Update by replacing entire array
tableData.value = newData

// Or mutate + trigger manually
tableData.value[0].name = 'New'
triggerRef(tableData)

Never Destructure reactive() Objects

const state = reactive({ count: 0, name: 'Vue' })

// ❌ BAD: Loses reactivity
const { count } = state
count++ // Won't trigger updates!

// ✅ GOOD: Use toRefs
const { count } = toRefs(state)
count.value++ // Works!

// ✅ BETTER: Use ref instead of reactive
const count = ref(0)

Use computed for Derived Values

<!-- ❌ BAD: Recalculates every render -->
<div>{{ items.filter(i => i.active).length }}</div>

<!-- ✅ GOOD: Cached -->
<script setup>
const activeCount = computed(() => items.value.filter(i => i.active).length)
</script>
<div>{{ activeCount }}</div>

4. Component Design (High)

Lazy Load Heavy Components

// ❌ BAD: Loaded immediately
import HeavyChart from './HeavyChart.vue'

// ✅ GOOD: Loaded on demand
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))

// ✅ NUXT: Auto lazy with Lazy prefix
<template>
  <LazyHeavyChart v-if="showChart" />
</template>

v-if vs v-show

Scenario Use
Frequent toggle (hover, tabs) v-show
Rarely changes v-if
Initially false v-if
Heavy component v-if

Type Props with TypeScript

interface Props {
  user: User
  items: string[]
  loading?: boolean
  size?: 'sm' | 'md' | 'lg'
}

const props = withDefaults(defineProps<Props>(), {
  loading: false,
  size: 'md',
  items: () => []
})

5. Performance (Medium)

Use v-memo for List Optimization

<div
  v-for="item in items"
  :key="item.id"
  v-memo="[item.id, item === selected]"
>
  <ExpensiveComponent :item="item" />
</div>

Use Virtual Scrolling for Large Lists

// For 1000+ items, use virtual scrolling
import { useVirtualList } from '@vueuse/core'

const { list, containerProps, wrapperProps } = useVirtualList(items, {
  itemHeight: 40
})

Debounce/Throttle High-Frequency Events

import { useDebounceFn, useThrottleFn } from '@vueuse/core'

// Search input: debounce
const debouncedSearch = useDebounceFn(search, 300)

// Scroll handler: throttle
const throttledScroll = useThrottleFn(handleScroll, 100)

Use KeepAlive for Cached Components

<KeepAlive :include="['TabA', 'TabB']" :max="5">
  <component :is="currentTab" />
</KeepAlive>

6. State Management – Pinia (Medium)

Use storeToRefs for Destructuring

import { storeToRefs } from 'pinia'

const store = useUserStore()

// ❌ BAD: Loses reactivity
const { user, isLoggedIn } = store

// ✅ GOOD: Keeps reactivity
const { user, isLoggedIn } = storeToRefs(store)

// Actions can be destructured directly
const { login, logout } = store

Use $patch for Batch Updates

// ❌ BAD: Multiple reactive updates
store.user.name = 'New'
store.user.email = 'new@example.com'
store.updatedAt = new Date()

// ✅ GOOD: Single update
store.$patch({
  user: { ...store.user, name: 'New', email: 'new@example.com' },
  updatedAt: new Date()
})

7. Bundle Optimization (Low)

Tree-Shaking Friendly Imports

// ❌ BAD: Imports entire lodash (~70KB)
import _ from 'lodash'

// ✅ GOOD: Only imports debounce (~2KB)
import { debounce } from 'lodash-es'

Use Auto-Import for Component Libraries

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@element-plus/nuxt'],
  // Auto-imports only used components
})

Analyze Bundle Size

npx nuxi analyze

8. Nuxt Specific (Low)

Configure Route Rules for Caching

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },
    '/blog/**': { isr: 3600 },        // Regenerate hourly
    '/api/products': { swr: 60 },     // Stale-while-revalidate
    '/admin/**': { ssr: false }       // Client-only
  }
})

Use useState for Simple Shared State

// SSR-safe shared state (simpler than Pinia)
const user = useState<User | null>('user', () => null)
const theme = useState('theme', () => 'light')

Use definePageMeta for Page Configuration

<script setup lang="ts">
definePageMeta({
  layout: 'admin',
  middleware: ['auth'],
  keepalive: true
})
</script>

Quick Reference

Issue Solution
Hydration mismatch Use <ClientOnly> or onMounted
Double fetching Use useFetch not $fetch
Slow sequential requests Use Promise.all()
Large list performance Use virtual scrolling
Reactivity lost Don’t destructure reactive(), use toRefs()
Store reactivity lost Use storeToRefs()
Large bundle Use dynamic import() and tree-shaking