目录

Vue 最佳实践

快速入门

安装与版本

当前使用版本为 Vue 3.5.32

Vue 3 需要 Node.js 20+ 环境。

使用 create-vue 创建项目(推荐)

# npm
npm create vue@latest
cd <your-project-name>
npm install
npm run dev

# pnpm
pnpm create vue@latest
cd <your-project-name>
pnpm install
pnpm run dev

# yarn
yarn create vue@latest
cd <your-project-name>
yarn
yarn dev

使用 Vite 创建项目

# npm
npm create vite@latest my-vue-app -- --template vue-ts
cd my-vue-app
npm install
npm run dev

# pnpm
pnpm create vite my-vue-app --template vue-ts
cd my-vue-app
pnpm install
pnpm run dev

CDN 方式(无构建步骤)

<!-- 使用 ESM 构建 -->
<script type="module">
  import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
  createApp({ data() { return { message: 'Hello Vue!' } } }).mount('#app')
</script>

Vue 3 vs Vue 2 主要变化

特性 Vue 2 Vue 3
响应式 defineProperty Proxy
组件定义 Options API Composition API
TypeScript 通过装饰器 原生支持
悬悬组件 slot v-slot
全局状态 Vuex Pinia

Composition API

Vue 3 的核心特性,使用 script setup 语法:

基本结构

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

// 响应式状态
const count = ref(0)
const message = ref('Hello')

// 计算属性
const doubled = computed(() => count.value * 2)

// 方法
function increment() {
  count.value++
}

// 生命周期
onMounted(() => {
  console.log('Component mounted')
})
</script>

<template>
  <div>
    <p>{{ message }} {{ count }}</p>
    <p>Doubled: {{ doubled }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

reactive

import { reactive } from 'vue'

// 对象响应式
const state = reactive({
  count: 0,
  user: {
    name: 'Alice',
    age: 30
  }
})

// 修改
state.count++
state.user.name = 'Bob'

解构与响应式

import { reactive, toRefs } from 'vue'

const state = reactive({
  count: 0,
  message: 'Hello'
})

// 解构后保持响应式
const { count, message } = toRefs(state)

TypeScript 集成

定义 Props

import { defineProps, PropType } from 'vue'

interface User {
  id: number
  name: string
  email: string
}

const props = defineProps<{
  title: string
  users: User[]
  onSave: (user: User) => void
}>()

定义 Emits

const emit = defineEmits<{
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
}>()

// 使用
emit('update', 'new value')
emit('delete', 1)

模板引用

import { ref, onMounted } from 'vue'

const inputRef = ref<HTMLInputElement | null>(null)

onMounted(() => {
  inputRef.value?.focus()
})

组件结构

单文件组件结构

<script setup lang="ts">
// 1. 导入
import { ref } from 'vue'
import BaseButton from '@/components/BaseButton.vue'

// 2. Props 和 Emits
const props = defineProps<{ title: string }>()
const emit = defineEmits<{ (e: 'submit'): void }>()

// 3. 响应式状态
const loading = ref(false)

// 4. 计算属性
const isValid = computed(() => props.title.length > 0)

// 5. 方法
async function handleSubmit() {
  loading.value = true
  try {
    emit('submit')
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <!-- 模板 -->
</template>

<style scoped>
/* 组件样式 */
</style>

Pinia 状态管理

安装

npm install pinia

创建 Store

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

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

  // 计算属性
  const userCount = computed(() => users.value.length)
  const isLoggedIn = computed(() => currentUser.value !== null)

  // 方法
  async function fetchUsers() {
    const response = await api.get('/users')
    users.value = response.data
  }

  async function login(credentials: Credentials) {
    const response = await api.post('/login', credentials)
    currentUser.value = response.data
  }

  function logout() {
    currentUser.value = null
  }

  return {
    users,
    currentUser,
    userCount,
    isLoggedIn,
    fetchUsers,
    login,
    logout
  }
})

组件中使用

<script setup lang="ts">
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// 使用
console.log(userStore.userCount)
userStore.login({ email: '[email protected]', password: '123' })
</script>

Vue Router

配置

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/users/:id',
    name: 'UserDetail',
    component: () => import('@/views/UserDetail.vue'),
    props: true
  },
  {
    path: '/admin',
    component: () => import('@/views/Admin.vue'),
    meta: { requiresAuth: true }
  }
]

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

// 导航守卫
router.beforeEach((to) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return '/login'
  }
})

export default router

组件中使用

<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'

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

// 获取参数
const userId = route.params.id

// 导航
router.push('/dashboard')
</script>

组合式函数 (Composables)

封装可复用的逻辑:

创建

// composables/useFetch.ts
import { ref, onMounted } from 'vue'

export function useFetch<T>(url: string) {
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)

  async function fetchData() {
    loading.value = true
    error.value = null
    try {
      const response = await fetch(url)
      data.value = await response.json()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  onMounted(fetchData)

  return { data, loading, error, refetch: fetchData }
}

使用

<script setup lang="ts">
import { useFetch } from '@/composables/useFetch'

interface User {
  id: number
  name: string
}

const { data: users, loading, error, refetch } = useFetch<User[]>('/api/users')
</script>

性能优化

懒加载组件

<!-- 静态导入 -->
<script setup>
import HeavyComponent from './HeavyComponent.vue'
</script>

<!-- 动态导入懒加载 -->
<script setup>
import { defineAsyncComponent } from 'vue'

const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)
</script>

v-memo

<template>
  <div v-for="item in list" :key="item.id" v-memo="[item.id === selectedId]">
    <!-- 只有 selectedId 变化时更新 -->
  </div>
</template>

KeepAlive

<template>
  <keep-alive include="UserList,ProductList">
    <router-view />
  </keep-alive>
</template>

样式指南

Scoped CSS

<style scoped>
.button {
  padding: 8px 16px;
  border-radius: 4px;
}

/* 深度选择器 */
:deep(.el-input) {
  border: none;
}
</style>

CSS 变量

<style scoped>
:root {
  --primary-color: #42b983;
  --button-padding: 8px 16px;
}

.button {
  background: var(--primary-color);
  padding: var(--button-padding);
}
</style>

测试

Vitest

npm install -D vitest @vue/test-utils jsdom

组件测试

// components/Button.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './Button.vue'

describe('Button', () => {
  it('renders with correct text', () => {
    const wrapper = mount(Button, {
      props: { label: 'Click me' }
    })
    expect(wrapper.text()).toContain('Click me')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button, {
      props: { label: 'Click me' }
    })
    await wrapper.trigger('click')
    expect(wrapper.emitted()).toHaveProperty('click')
  })
})

相关资源