mcpField
V
vuejs-development

vuejs-development

github.com/manutej开发62510 次安装40 周前更新
代码仓库

关于 vuejs-development

面向 Vue.js 开发者的技能指南,涵盖 Vue 3 的响应式系统、Composition API 和组件开发,并提供状态管理、路由、测试与性能优化模式。

主要功能

  1. 01响应式状态与计算属性
  2. 02Composition API 与生命周期钩子
  3. 03单文件组件和模板指令
  4. 04组件通信与状态管理
  5. 05路由、TypeScript 与测试
  6. 06性能优化和开发实践

适用场景

  1. 01构建响应式单页应用
  2. 02将 Options API 迁移到 Composition API
  3. 03编写可复用组件与 Composables
  4. 04测试并优化 Vue 应用

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/manutej/luxor-claude-marketplace --skill vuejs-development

安装后在 Claude Code 中输入/vuejs-development确认已加载。

附带文件EXAMPLES.mdREADME.md

详细介绍

Vue.js 开发技能

此技能基于 Vue.js 官方文档,为使用 Composition API、响应式系统、单文件组件、指令和生命周期钩子构建现代 Vue.js 应用提供全面指导。

何时使用此技能

在以下情况下使用此技能:

  • 使用 Vue.js 构建单页应用(SPA)
  • 使用 Vue 创建渐进式 Web 应用(PWA)
  • 使用响应式数据开发交互式用户界面
  • 构建基于组件的架构
  • 实现表单、数据获取和状态管理
  • 创建可复用的 UI 组件和库
  • 从 Options API 迁移到 Composition API
  • 优化 Vue 应用的性能
  • 构建无障碍且易于维护的 Web 应用
  • 集成 TypeScript 以实现类型安全的开发

核心概念

响应式系统

Vue 的响应式系统是其核心机制,可跟踪依赖项,并在数据发生变化时自动更新 DOM。

使用 ref() 创建响应式状态:

import { ref } from 'vue'

// ref creates a reactive reference to a value
const count = ref(0)

// Access value with .value
console.log(count.value) // 0

// Modify value
count.value++
console.log(count.value) // 1

// In templates, .value is automatically unwrapped
// <template>{{ count }}</template>

使用 reactive() 创建响应式对象:

import { reactive } from 'vue'

// reactive creates a reactive proxy of an object
const state = reactive({
  name: 'Vue',
  version: 3,
  features: ['Composition API', 'Teleport', 'Suspense']
})

// Access and modify properties directly
console.log(state.name) // 'Vue'
state.name = 'Vue.js'

// Nested objects are also reactive
state.features.push('Fragments')

何时使用 ref(),何时使用 reactive():

// Use ref() for:
// - Primitive values (string, number, boolean)
// - Single values that need reactivity
const count = ref(0)
const message = ref('Hello')
const isActive = ref(true)

// Use reactive() for:
// - Objects with multiple properties
// - Complex data structures
const user = reactive({
  id: 1,
  name: 'John',
  email: '[email protected]',
  preferences: {
    theme: 'dark',
    notifications: true
  }
})

计算属性:

import { ref, computed } from 'vue'

const count = ref(0)

// Computed property automatically tracks dependencies
const doubled = computed(() => count.value * 2)

console.log(doubled.value) // 0
count.value = 5
console.log(doubled.value) // 10

// Writable computed
const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})

fullName.value = 'Jane Smith'
console.log(firstName.value) // 'Jane'
console.log(lastName.value) // 'Smith'

侦听器:

import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const message = ref('Hello')

// Watch a single ref
watch(count, (newValue, oldValue) => {
  console.log(`Count changed from ${oldValue} to ${newValue}`)
})

// Watch multiple sources
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
  console.log(`Count: ${newCount}, Message: ${newMessage}`)
})

// Watch reactive object property
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (newValue, oldValue) => {
    console.log(`State count changed from ${oldValue} to ${newValue}`)
  }
)

// watchEffect automatically tracks dependencies
watchEffect(() => {
  console.log(`Count is ${count.value}`)
  // Automatically re-runs when count changes
})

// Immediate execution
watch(count, (newValue) => {
  console.log(`Count is now ${newValue}`)
}, { immediate: true })

// Deep watching
const user = reactive({ profile: { name: 'John' } })
watch(user, (newValue) => {
  console.log('User changed:', newValue)
}, { deep: true })
Composition API

Composition API 提供了一组基于函数的 API,用于组织组件逻辑。

使用

<script setup>
import { ref, computed, onMounted } from 'vue'

// Props
const props = defineProps({
  title: String,
  count: {
    type: Number,
    default: 0
  }
})

// Emits
const emit = defineEmits(['update', 'delete'])

// Reactive state
const localCount = ref(props.count)
const message = ref('Hello Vue!')

// Computed
const doubledCount = computed(() => localCount.value * 2)

// Methods
function increment() {
  localCount.value++
  emit('update', localCount.value)
}

// Lifecycle
onMounted(() => {
  console.log('Component mounted')
})
</script>

<template>
  <div>
    <h2>{{ title }}</h2>
    <p>{{ message }}</p>
    <p>Count: {{ localCount }}</p>
    <p>Doubled: {{ doubledCount }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

不使用

<script>
import { ref, computed, onMounted } from 'vue'

export default {
  name: 'MyComponent',
  props: {
    title: String,
    count: {
      type: Number,
      default: 0
    }
  },
  emits: ['update', 'delete'],
  setup(props, { emit }) {
    const localCount = ref(props.count)
    const message = ref('Hello Vue!')

    const doubledCount = computed(() => localCount.value * 2)

    function increment() {
      localCount.value++
      emit('update', localCount.value)
    }

    onMounted(() => {
      console.log('Component mounted')
    })

    return {
      localCount,
      message,
      doubledCount,
      increment
    }
  }
}
</script>
单文件组件

单文件组件(.vue)将模板、脚本和样式组合在一个文件中。

完整的 SFC 示例:

<script setup>
import { ref, computed } from 'vue'

const tasks = ref([
  { id: 1, text: 'Learn Vue', completed: false },
  { id: 2, text: 'Build app', completed: false }
])

const newTaskText = ref('')

const completedCount = computed(() =>
  tasks.value.filter(t => t.completed).length
)

const remainingCount = computed(() =>
  tasks.value.filter(t => !t.completed).length
)

function addTask() {
  if (newTaskText.value.trim()) {
    tasks.value.push({
      id: Date.now(),
      text: newTaskText.value,
      completed: false
    })
    newTaskText.value = ''
  }
}

function toggleTask(id) {
  const task = tasks.value.find(t => t.id === id)
  if (task) task.completed = !task.completed
}

function removeTask(id) {
  tasks.value = tasks.value.filter(t => t.id !== id)
}
</script>

<template>
  <div class="todo-app">
    <h1>Todo List</h1>

    <div class="add-task">
      <input
        v-model="newTaskText"
        @keyup.enter="addTask"
        placeholder="Add new task"
      >
      <button @click="addTask">Add</button>
    </div>

    <ul class="task-list">
      <li
        v-for="task in tasks"
        :key="task.id"
        :class="{ completed: task.completed }"
      >
        <input
          type="checkbox"
          :checked="task.completed"
          @change="toggleTask(task.id)"
        >
        <span>{{ task.text }}</span>
        <button @click="removeTask(task.id)">Delete</button>
      </li>
    </ul>

    <div class="stats">
      <p>Completed: {{ completedCount }}</p>
      <p>Remaining: {{ remainingCount }}</p>
    </div>
  </div>
</template>

<style scoped>
.todo-app {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.add-task {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.add-task input {
  flex: 1;
  padding: 8px;
  font-size: 14px;
}

.task-list {
  list-style: none;
  padding: 0;
}

.task-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.task-list li.completed span {
  text-decoration: line-through;
  opacity: 0.6;
}

.stats {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 2px solid #eee;
}

.stats p {
  margin: 5px 0;
}
</style>
模板语法和指令

Vue 使用基于 HTML 的模板语法,并提供特殊指令。

文本插值:

<template>
  <div>
    
    <p>{{ message }}</p>

    
    <p>{{ count + 1 }}</p>
    <p>{{ ok ? 'YES' : 'NO' }}</p>
    <p>{{ message.split('').reverse().join('') }}</p>

    
    <p>{{ formatDate(timestamp) }}</p>
  </div>
</template>

v-bind - 属性绑定:

<template>
  
  <img v-bind:src="imageUrl" v-bind:alt="imageAlt">

  
  <img :src="imageUrl" :alt="imageAlt">

  
  <button :[attributeName]="value">Click</button>

  
  <div v-bind="objectOfAttrs"></div>

  
  <div :class="{ active: isActive, 'text-danger': hasError }"></div>
  <div :class="[activeClass, errorClass]"></div>
  <div :class="[isActive ? activeClass : '', errorClass]"></div>

  
  <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
  <div :style="[baseStyles, overridingStyles]"></div>
</template>

<script setup>
import { ref, reactive } from 'vue'

const imageUrl = ref('/path/to/image.jpg')
const imageAlt = ref('Description')
const isActive = ref(true)
const hasError = ref(false)
const activeClass = ref('active')
const errorClass = ref('text-danger')
const activeColor = ref('red')
const fontSize = ref(14)

const objectOfAttrs = reactive({
  id: 'container',
  class: 'wrapper'
})
</script>

v-on - 事件处理:

<template>
  
  <button v-on:click="handleClick">Click me</button>

  
  <button @click="handleClick">Click me</button>

  
  <button @click="count++">Increment</button>

  
  <button @click="handleClick('hello', $event)">Click</button>

  
  <form @submit.prevent="onSubmit">Submit</form>
  <button @click.stop="handleClick">Stop Propagation</button>
  <div @click.self="handleClick">Only Self</div>
  <button @click.once="handleClick">Once</button>

  
  <input @keyup.enter="submit">
  <input @keyup.esc="cancel">
  <input @keyup.ctrl.s="save">

  
  <button @click.left="handleLeft">Left Click</button>
  <button @click.right="handleRight">Right Click</button>
  <button @click.middle="handleMiddle">Middle Click</button>
</template>

<script setup>
function handleClick(message, event) {
  console.log(message, event)
}

function onSubmit() {
  console.log('Form submitted')
}
</script>

v-model - 双向绑定:

<template>
  
  <input v-model="text">
  <p>{{ text }}</p>

  
  <textarea v-model="message"></textarea>

  
  <input type="checkbox" v-model="checked">

  
  <input type="checkbox" value="Vue" v-model="checkedNames">
  <input type="checkbox" value="React" v-model="checkedNames">
  <input type="checkbox" value="Angular" v-model="checkedNames">
  <p>{{ checkedNames }}</p>

  
  <input type="radio" value="One" v-model="picked">
  <input type="radio" value="Two" v-model="picked">

  
  <select v-model="selected">
    <option disabled value="">Please select</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>

  
  <select v-model="multiSelected" multiple>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>

  
  <input v-model.lazy="text">        
  <input v-model.number="age">       
  <input v-model.trim="message">     

  
  <CustomInput v-model="searchText" />
</template>

<script setup>
import { ref } from 'vue'

const text = ref('')
const message = ref('')
const checked = ref(false)
const checkedNames = ref([])
const picked = ref('')
const selected = ref('')
const multiSelected = ref([])
const age = ref(0)
const searchText = ref('')
</script>

v-if、v-else-if、v-else - 条件渲染:

<template>
  <div v-if="type === 'A'">
    Type A
  </div>
  <div v-else-if="type === 'B'">
    Type B
  </div>
  <div v-else-if="type === 'C'">
    Type C
  </div>
  <div v-else>
    Not A, B, or C
  </div>

  
  <template v-if="ok">
    <h1>Title</h1>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
  </template>
</template>

<script setup>
import { ref } from 'vue'
const type = ref('A')
const ok = ref(true)
</script>

v-show - 切换显示状态:

<template>
  
  <h1 v-show="isVisible">Hello!</h1>

  
</template>

<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>

v-for - 列表渲染:

<template>
  
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.text }}
    </li>
  </ul>

  
  <ul>
    <li v-for="(item, index) in items" :key="item.id">
      {{ index }}: {{ item.text }}
    </li>
  </ul>

  
  <ul>
    <li v-for="(value, key) in user" :key="key">
      {{ key }}: {{ value }}
    </li>
  </ul>

  
  <ul>
    <li v-for="(value, key, index) in user" :key="key">
      {{ index }}. {{ key }}: {{ value }}
    </li>
  </ul>

  
  <span v-for="n in 10" :key="n">{{ n }}</span>

  
  
  <ul>
    <li v-for="item in activeItems" :key="item.id">
      {{ item.text }}
    </li>
  </ul>

  
  <template v-for="item in items" :key="item.id">
    <li>{{ item.text }}</li>
    <li class="divider"></li>
  </template>
</template>

<script setup>
import { ref, computed } from 'vue'

const items = ref([
  { id: 1, text: 'Learn Vue', active: true },
  { id: 2, text: 'Build app', active: false },
  { id: 3, text: 'Deploy', active: true }
])

const user = ref({
  name: 'John',
  age: 30,
  email: '[email protected]'
})

const activeItems = computed(() =>
  items.value.filter(item => item.active)
)
</script>
生命周期钩子

生命周期钩子可让你在组件生命周期的特定阶段运行代码。

Composition API 中的生命周期钩子:

<script setup>
import {
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured,
  onActivated,
  onDeactivated
} from 'vue'

const count = ref(0)

// Before component is mounted
onBeforeMount(() => {
  console.log('Before mount')
})

// After component is mounted (DOM available)
onMounted(() => {
  console.log('Mounted')
  // Good for: API calls, DOM manipulation, timers
  fetchData()
  setupEventListeners()
})

// Before component updates (reactive data changed)
onBeforeUpdate(() => {
  console.log('Before update')
})

// After component updates
onUpdated(() => {
  console.log('Updated')
  // Good for: DOM operations after data changes
})

// Before component is unmounted
onBeforeUnmount(() => {
  console.log('Before unmount')
  // Good for: Cleanup
})

// After component is unmounted
onUnmounted(() => {
  console.log('Unmounted')
  // Good for: Cleanup timers, event listeners
  clearInterval(interval)
  removeEventListeners()
})

// Error handling
onErrorCaptured((err, instance, info) => {
  console.error('Error captured:', err, info)
  return false // Prevent error from propagating
})

// For components in <keep-alive>
onActivated(() => {
  console.log('Component activated')
})

onDeactivated(() => {
  console.log('Component deactivated')
})
</script>

生命周期图:

Creation Phase:
  setup() → onBeforeMount() → onMounted()

Update Phase (when reactive data changes):
  onBeforeUpdate() → onUpdated()

Destruction Phase:
  onBeforeUnmount() → onUnmounted()

组件通信

Props(父组件传递给子组件)

<script setup>
// Define props with types
const props = defineProps({
  name: String,
  age: Number,
  email: String,
  isActive: {
    type: Boolean,
    default: true
  },
  roles: {
    type: Array,
    default: () => []
  },
  profile: {
    type: Object,
    required: true,
    validator: (value) => {
      return value.id && value.name
    }
  }
})

// Props are reactive and can be used in computed
import { computed } from 'vue'

const displayName = computed(() =>
  `${props.name} (${props.age})`
)
</script>

<template>
  <div class="user-card">
    <h3>{{ displayName }}</h3>
    <p>{{ email }}</p>
    <span v-if="isActive">Active</span>
    <ul>
      <li v-for="role in roles" :key="role">{{ role }}</li>
    </ul>
  </div>
</template>


<script setup>
import UserCard from './UserCard.vue'
import { reactive } from 'vue'

const user = reactive({
  name: 'John Doe',
  age: 30,
  email: '[email protected]',
  isActive: true,
  roles: ['admin', 'editor'],
  profile: {
    id: 1,
    name: 'John'
  }
})
</script>

<template>
  <UserCard
    :name="user.name"
    :age="user.age"
    :email="user.email"
    :is-active="user.isActive"
    :roles="user.roles"
    :profile="user.profile"
  />

  
  <UserCard v-bind="user" />
</template>
Emits(子组件传递给父组件)

<script setup>
const props = defineProps({
  todo: {
    type: Object,
    required: true
  }
})

// Define emits
const emit = defineEmits(['toggle', 'delete', 'update'])

// Or with validation
const emit = defineEmits({
  toggle: (id) => {
    if (typeof id === 'number') {
      return true
    } else {
      console.warn('Invalid toggle event payload')
      return false
    }
  },
  delete: (id) => typeof id === 'number',
  update: (id, text) => {
    return typeof id === 'number' && typeof text === 'string'
  }
})

function handleToggle() {
  emit('toggle', props.todo.id)
}

function handleDelete() {
  emit('delete', props.todo.id)
}

function handleUpdate(newText) {
  emit('update', props.todo.id, newText)
}
</script>

<template>
  <div class="todo-item">
    <input
      type="checkbox"
      :checked="todo.completed"
      @change="handleToggle"
    >
    <span>{{ todo.text }}</span>
    <button @click="handleDelete">Delete</button>
  </div>
</template>


<script setup>
import TodoItem from './TodoItem.vue'
import { ref } from 'vue'

const todos = ref([
  { id: 1, text: 'Learn Vue', completed: false },
  { id: 2, text: 'Build app', completed: false }
])

function toggleTodo(id) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.completed = !todo.completed
}

function deleteTodo(id) {
  todos.value = todos.value.filter(t => t.id !== id)
}

function updateTodo(id, text) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.text = text
}
</script>

<template>
  <div>
    <TodoItem
      v-for="todo in todos"
      :key="todo.id"
      :todo="todo"
      @toggle="toggleTodo"
      @delete="deleteTodo"
      @update="updateTodo"
    />
  </div>
</template>
Provide/Inject(祖先组件传递给后代组件)

<script setup>
import { provide, ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const theme = ref('dark')
const userSettings = ref({
  fontSize: 14,
  language: 'en'
})

// Provide to all descendants
provide('theme', theme)
provide('userSettings', userSettings)

// Provide with readonly to prevent modifications
import { readonly } from 'vue'
provide('theme', readonly(theme))

// Provide functions
function updateTheme(newTheme) {
  theme.value = newTheme
}
provide('updateTheme', updateTheme)
</script>

<template>
  <div>
    <ChildComponent />
  </div>
</template>


<script setup>
import { inject } from 'vue'

// Inject provided values
const theme = inject('theme')
const userSettings = inject('userSettings')
const updateTheme = inject('updateTheme')

// With default value
const locale = inject('locale', 'en')

// With factory function for default
const settings = inject('settings', () => ({ mode: 'light' }))
</script>

<template>
  <div :class="`theme-${theme}`">
    <p>Font size: {{ userSettings.fontSize }}</p>
    <button @click="updateTheme('light')">Light Theme</button>
    <button @click="updateTheme('dark')">Dark Theme</button>
  </div>
</template>
Slots(分发父组件内容)

<script setup>
const props = defineProps({
  title: String
})
</script>

<template>
  <div class="card">
    
    <header>
      <slot name="header">
        <h2>{{ title }}</h2>
      </slot>
    </header>

    
    <main>
      <slot>
        <p>Default content</p>
      </slot>
    </main>

    
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>


<template>
  <Card title="My Card">
    <template #header>
      <h1>Custom Header</h1>
    </template>

    <p>Main content goes here</p>

    <template #footer>
      <button>Action</button>
    </template>
  </Card>
</template>



<script setup>
import { ref } from 'vue'

const todos = ref([
  { id: 1, text: 'Learn Vue', completed: false },
  { id: 2, text: 'Build app', completed: true }
])
</script>

<template>
  <ul>
    <li v-for="todo in todos" :key="todo.id">
      
      <slot :todo="todo" :index="todo.id"></slot>
    </li>
  </ul>
</template>


<template>
  <TodoList>
    
    <template #default="{ todo, index }">
      <span :class="{ completed: todo.completed }">
        {{ index }}. {{ todo.text }}
      </span>
    </template>
  </TodoList>

  
  <TodoList v-slot="{ todo }">
    <span>{{ todo.text }}</span>
  </TodoList>
</template>

状态管理模式

组件本地状态
<script setup>
import { ref, reactive } from 'vue'

// Simple counter state
const count = ref(0)

function increment() {
  count.value++
}

// Form state
const formData = reactive({
  name: '',
  email: '',
  message: ''
})

function submitForm() {
  console.log('Submitting:', formData)
}

function resetForm() {
  formData.name = ''
  formData.email = ''
  formData.message = ''
}
</script>
Composables(可复用的状态逻辑)
// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)

  const doubled = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return {
    count,
    doubled,
    increment,
    decrement,
    reset
  }
}

// Usage in component
<script setup>
import { useCounter } from '@/composables/useCounter'

const { count, doubled, increment, decrement, reset } = useCounter(10)
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Doubled: {{ doubled }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">Reset</button>
  </div>
</template>

鼠标位置 Composable:

// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => {
    window.addEventListener('mousemove', update)
  })

  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })

  return { x, y }
}

// Usage
<script setup>
import { useMouse } from '@/composables/useMouse'

const { x, y } = useMouse()
</script>

<template>
  <p>Mouse position: {{ x }}, {{ y }}</p>
</template>

获取数据的 Composable:

// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  watchEffect(async () => {
    loading.value = true
    data.value = null
    error.value = null

    const urlValue = toValue(url)

    try {
      const response = await fetch(urlValue)
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
      data.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  })

  return { data, error, loading }
}

// Usage
<script setup>
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'

const userId = ref(1)
const url = computed(() => `https://api.example.com/users/${userId.value}`)

const { data: user, error, loading } = useFetch(url)
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <div v-else-if="user">{{ user.name }}</div>
</template>
使用 Pinia 管理全局状态
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// Option 1: Setup Stores (Composition API style)
export const useCounterStore = defineStore('counter', () => {
  // State
  const count = ref(0)
  const name = ref('Counter')

  // Getters
  const doubleCount = computed(() => count.value * 2)

  // Actions
  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  async function incrementAsync() {
    await new Promise(resolve => setTimeout(resolve, 1000))
    count.value++
  }

  return {
    count,
    name,
    doubleCount,
    increment,
    decrement,
    incrementAsync
  }
})

// Option 2: Options Stores
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter'
  }),

  getters: {
    doubleCount: (state) => state.count * 2,
    doublePlusOne() {
      return this.doubleCount + 1
    }
  },

  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.count++
    }
  }
})

// Usage in component
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const counterStore = useCounterStore()

// Extract reactive state (preserves reactivity)
const { count, name, doubleCount } = storeToRefs(counterStore)

// Actions can be destructured directly
const { increment, decrement } = counterStore
</script>

<template>
  <div>
    <p>{{ name }}: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

使用 Vue Router 进行路由管理

路由设置:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/about',
    name: 'about',
    component: About
  },
  {
    path: '/user/:id',
    name: 'user',
    component: () => import('@/views/User.vue'), // Lazy loading
    props: true // Pass route params as props
  },
  {
    path: '/posts',
    name: 'posts',
    component: () => import('@/views/Posts.vue'),
    children: [
      {
        path: ':id',
        name: 'post-detail',
        component: () => import('@/views/PostDetail.vue')
      }
    ]
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: () => import('@/views/NotFound.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// Navigation guards
router.beforeEach((to, from, next) => {
  // Check authentication, etc.
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login')
  } else {
    next()
  }
})

export default router

在组件中使用路由:

<script setup>
import { useRouter, useRoute } from 'vue-router'
import { computed } from 'vue'

const router = useRouter()
const route = useRoute()

// Access route params
const userId = computed(() => route.params.id)

// Access query params
const searchQuery = computed(() => route.query.q)

// Programmatic navigation
function goToHome() {
  router.push('/')
}

function goToUser(id) {
  router.push({ name: 'user', params: { id } })
}

function goBack() {
  router.back()
}

function goToSearch(query) {
  router.push({ path: '/search', query: { q: query } })
}
</script>

<template>
  <div>
    
    <router-link to="/">Home</router-link>
    <router-link :to="{ name: 'about' }">About</router-link>
    <router-link :to="`/user/${userId}`">User Profile</router-link>

    
    <router-link
      to="/dashboard"
      active-class="active"
      exact-active-class="exact-active"
    >
      Dashboard
    </router-link>

    
    <button @click="goToHome">Go Home</button>
    <button @click="goToUser(123)">View User 123</button>
    <button @click="goBack">Go Back</button>

    
    <router-view />

    
    <router-view name="sidebar" />
    <router-view name="main" />
  </div>
</template>

高级功能

Teleport

将内容移动到 DOM 中的其他位置。

<script setup>
import { ref } from 'vue'

const showModal = ref(false)
</script>

<template>
  <div class="app">
    <h1>My App</h1>
    <button @click="showModal = true">Open Modal</button>

    
    <Teleport to="body">
      <div v-if="showModal" class="modal">
        <div class="modal-content">
          <h2>Modal Title</h2>
          <p>Modal content</p>
          <button @click="showModal = false">Close</button>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<style>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 8px;
}
</style>
Suspense

通过加载状态处理异步组件。


<script setup>
const data = await fetch('/api/data').then(r => r.json())
</script>

<template>
  <div>{{ data }}</div>
</template>


<template>
  <Suspense>
    
    <template #default>
      <AsyncComponent />
    </template>

    
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>


<script setup>
import { onErrorCaptured, ref } from 'vue'

const error = ref(null)

onErrorCaptured((err) => {
  error.value = err
  return true
})
</script>

<template>
  <div v-if="error">Error: {{ error.message }}</div>
  <Suspense v-else>
    <AsyncComponent />
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>
Transitions

为进入或离开 DOM 的元素添加动画。

<script setup>
import { ref } from 'vue'

const show = ref(true)
</script>

<template>
  <button @click="show = !show">Toggle</button>

  
  <Transition>
    <p v-if="show">Hello</p>
  </Transition>

  
  <Transition name="fade">
    <p v-if="show">Fade transition</p>
  </Transition>

  
  <Transition
    enter-active-class="animate__animated animate__fadeIn"
    leave-active-class="animate__animated animate__fadeOut"
  >
    <p v-if="show">Custom animation</p>
  </Transition>

  
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item.id">
      {{ item.text }}
    </li>
  </TransitionGroup>
</template>

<style>
/* Transition classes */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* List transitions */
.list-enter-active,
.list-leave-active {
  transition: all 0.3s ease;
}

.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

.list-move {
  transition: transform 0.3s ease;
}
</style>
自定义指令

创建用于操作 DOM 的自定义指令。

// directives/focus.js
export const vFocus = {
  mounted(el) {
    el.focus()
  }
}

// directives/click-outside.js
export const vClickOutside = {
  mounted(el, binding) {
    el.clickOutsideEvent = (event) => {
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el.clickOutsideEvent)
  },
  unmounted(el) {
    document.removeEventListener('click', el.clickOutsideEvent)
  }
}

// Usage in component
<script setup>
import { vFocus } from '@/directives/focus'
import { vClickOutside } from '@/directives/click-outside'
import { ref } from 'vue'

const show = ref(false)

function closeDropdown() {
  show.value = false
}
</script>

<template>
  
  <input v-focus type="text">

  
  <div v-click-outside="closeDropdown">
    <button @click="show = !show">Toggle</button>
    <div v-if="show">Dropdown content</div>
  </div>
</template>

性能优化

Computed 与 Methods
<script setup>
import { ref, computed } from 'vue'

const items = ref([1, 2, 3, 4, 5])

// Computed: cached, only re-runs when dependencies change
const total = computed(() => {
  console.log('Computing total')
  return items.value.reduce((sum, n) => sum + n, 0)
})

// Method: runs on every render
function getTotal() {
  console.log('Getting total')
  return items.value.reduce((sum, n) => sum + n, 0)
}
</script>

<template>
  
  <p>{{ total }}</p>
  <p>{{ total }}</p>

  
  <p>{{ getTotal() }}</p>
  <p>{{ getTotal() }}</p>
</template>
v-once 和 v-memo
<template>
  
  <div v-once>
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
  </div>

  
  <div v-memo="[count, message]">
    <p>{{ count }}</p>
    <p>{{ message }}</p>
    
  </div>

  
  <div
    v-for="item in list"
    :key="item.id"
    v-memo="[item.selected]"
  >
    
    {{ item.name }}
  </div>
</template>
延迟加载组件
<script setup>
import { defineAsyncComponent } from 'vue'

// Lazy load component
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// With loading and error components
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./AsyncComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 3000
})
</script>

<template>
  <Suspense>
    <HeavyComponent />
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>
虚拟滚动
<script setup>
import { ref, computed } from 'vue'

const items = ref(Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  text: `Item ${i}`
})))

const containerHeight = 400
const itemHeight = 50
const visibleCount = Math.ceil(containerHeight / itemHeight)

const scrollTop = ref(0)

const startIndex = computed(() =>
  Math.floor(scrollTop.value / itemHeight)
)

const endIndex = computed(() =>
  Math.min(startIndex.value + visibleCount + 1, items.value.length)
)

const visibleItems = computed(() =>
  items.value.slice(startIndex.value, endIndex.value)
)

const offsetY = computed(() =>
  startIndex.value * itemHeight
)

const totalHeight = computed(() =>
  items.value.length * itemHeight
)

function handleScroll(event) {
  scrollTop.value = event.target.scrollTop
}
</script>

<template>
  <div
    class="virtual-scroll-container"
    :style="{ height: `${containerHeight}px`, overflow: 'auto' }"
    @scroll="handleScroll"
  >
    <div :style="{ height: `${totalHeight}px`, position: 'relative' }">
      <div :style="{ transform: `translateY(${offsetY}px)` }">
        <div
          v-for="item in visibleItems"
          :key="item.id"
          :style="{ height: `${itemHeight}px` }"
        >
          {{ item.text }}
        </div>
      </div>
    </div>
  </div>
</template>

TypeScript 集成

基础设置
// Component with TypeScript
<script setup lang="ts">
import { ref, computed, type Ref } from 'vue'

// Type annotations
const count: Ref<number> = ref(0)
const message = ref<string>('Hello')

// Interface for objects
interface User {
  id: number
  name: string
  email: string
}

const user = ref<User>({
  id: 1,
  name: 'John',
  email: '[email protected]'
})

// Props with types
interface Props {
  title: string
  count?: number
  user: User
}

const props = withDefaults(defineProps<Props>(), {
  count: 0
})

// Emits with types
interface Emits {
  (e: 'update', value: number): void
  (e: 'delete', id: number): void
}

const emit = defineEmits<Emits>()

// Computed with type inference
const doubled = computed(() => props.count * 2)

// Typed function
function updateUser(id: number, name: string): void {
  user.value.id = id
  user.value.name = name
}
</script>
使用 TypeScript 编写 Composables
// composables/useCounter.ts
import { ref, computed, type Ref, type ComputedRef } from 'vue'

interface UseCounterReturn {
  count: Ref<number>
  doubled: ComputedRef<number>
  increment: () => void
  decrement: () => void
}

export function useCounter(initialValue = 0): UseCounterReturn {
  const count = ref(initialValue)
  const doubled = computed(() => count.value * 2)

  function increment(): void {
    count.value++
  }

  function decrement(): void {
    count.value--
  }

  return {
    count,
    doubled,
    increment,
    decrement
  }
}

测试

使用 Vitest 测试组件
// Counter.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'

describe('Counter', () => {
  it('renders initial count', () => {
    const wrapper = mount(Counter, {
      props: {
        initialCount: 5
      }
    })

    expect(wrapper.text()).toContain('5')
  })

  it('increments count when button clicked', async () => {
    const wrapper = mount(Counter)

    await wrapper.find('button.increment').trigger('click')

    expect(wrapper.vm.count).toBe(1)
    expect(wrapper.text()).toContain('1')
  })

  it('emits update event', async () => {
    const wrapper = mount(Counter)

    await wrapper.find('button.increment').trigger('click')

    expect(wrapper.emitted()).toHaveProperty('update')
    expect(wrapper.emitted('update')[0]).toEqual([1])
  })
})
测试 Composable
// useCounter.test.js
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('initializes with default value', () => {
    const { count } = useCounter()
    expect(count.value).toBe(0)
  })

  it('initializes with custom value', () => {
    const { count } = useCounter(10)
    expect(count.value).toBe(10)
  })

  it('increments count', () => {
    const { count, increment } = useCounter()
    increment()
    expect(count.value).toBe(1)
  })

  it('computes doubled value', () => {
    const { count, doubled, increment } = useCounter()
    expect(doubled.value).toBe(0)
    increment()
    expect(doubled.value).toBe(2)
  })
})

最佳实践

1. 在复杂逻辑中使用 Composition API

Composition API 能更好地组织和复用代码。

<script setup>
// Good: Organized by feature
import { useUser } from '@/composables/useUser'
import { useProducts } from '@/composables/useProducts'
import { useCart } from '@/composables/useCart'

const { user, login, logout } = useUser()
const { products, fetchProducts } = useProducts()
const { cart, addToCart, removeFromCart } = useCart()
</script>
2. 保持组件小巧且专注

将大型组件拆分为更小、可复用的部分。


<template>
  <div>
    <UserHeader :user="user" />
    <UserProfile :user="user" />
    <UserPosts :posts="posts" />
  </div>
</template>


<template>
  <div>
    
  </div>
</template>
3. 使用 Computed 派生状态

不要在模板或方法中计算值。

<script setup>
import { ref, computed } from 'vue'

const items = ref([...])

// Good: Computed property
const activeItems = computed(() =>
  items.value.filter(item => item.active)
)

// Bad: Method called in template
function getActiveItems() {
  return items.value.filter(item => item.active)
}
</script>

<template>
  
  <div v-for="item in activeItems" :key="item.id">

  
  <div v-for="item in getActiveItems()" :key="item.id">
</template>
4. 始终在 v-for 中使用键

键可帮助 Vue 识别发生变化的项目。


<div v-for="item in items" :key="item.id">


<div v-for="item in items">


<div v-for="(item, index) in items" :key="index">
5. 避免同时使用 v-if 和 v-for

改用计算属性筛选列表。

<script setup>
import { computed } from 'vue'

// Good: Filter with computed
const activeItems = computed(() =>
  items.value.filter(item => item.active)
)
</script>

<template>
  
  <div v-for="item in activeItems" :key="item.id">

  
  <div v-for="item in items" :key="item.id" v-if="item.active">
</template>
6. 验证 Prop

在生产组件中始终验证 props。

<script setup>
// Good: Validated props
defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0,
    validator: (value) => value >= 0
  },
  status: {
    type: String,
    validator: (value) => ['draft', 'published', 'archived'].includes(value)
  }
})
</script>
7. 谨慎使用 Provide/Inject

Provide/Inject 适用于深层组件树,不应取代 props。


<script setup>
provide('theme', theme)
provide('i18n', i18n)
</script>



8. 在 onUnmounted 中清理

始终清理副作用,以防止内存泄漏。

<script setup>
import { onMounted, onUnmounted } from 'vue'

let interval

onMounted(() => {
  interval = setInterval(() => {
    // Do something
  }, 1000)
})

onUnmounted(() => {
  clearInterval(interval)
})
</script>
9. 使用作用域样式

使用作用域样式,防止样式泄漏。

<style scoped>
/* Styles only apply to this component */
.button {
  background: blue;
}
</style>


<style scoped>
.parent :deep(.child) {
  color: red;
}
</style>
10. 懒加载路由

通过基于路由的代码拆分缩短初始加载时间。

const routes = [
  {
    path: '/',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue')
  }
]

总结

这项 Vue.js 开发技能涵盖:

  1. 响应式系统:ref()、reactive()、computed()、watch()
  2. 组合式 API:
  3. 单文件组件:模板、脚本和样式的组织
  4. 指令:v-if、v-for、v-model、v-bind、v-on
  5. 组件通信:Props、emits、provide/inject、插槽
  6. 状态管理:本地状态、组合式函数、Pinia
  7. 路由:Vue Router 导航和守卫
  8. 高级功能:Teleport、Suspense、Transitions、自定义指令
  9. 性能:计算属性与方法的比较、v-memo、懒加载、虚拟滚动
  10. TypeScript:类型安全的 props、emits、组合式函数
  11. 测试:组件和组合式函数测试
  12. 最佳实践:现代 Vue 3 模式和优化技巧

这些模式和示例基于 Vue.js 官方文档(信任评分:9.7),体现了使用组合式 API 和

评论

支持 Markdown