Vue.js 3前端开发实践指南

本文最后更新于 1 分钟前,文中所描述的信息可能已发生改变。

Vue.js 是当今最流行的前端框架之一,特别是随着 Vue 3 的发布,它带来了更好的性能、更小的体积和更好的 TypeScript 支持。本文将深入探讨 Vue.js 3 的开发实践,包括组件设计、状态管理、性能优化和最佳实践。

Vue 3 的新特性

Vue 3 相比 Vue 2 有许多重大改进:

  1. Composition API:提供更好的代码组织和逻辑复用
  2. 更好的 TypeScript 支持:从底层重写,提供更好的类型推断
  3. 更小的包体积:核心代码体积减少约 41%
  4. 更快的渲染性能:虚拟 DOM 重新实现,渲染速度提升最高可达 100%
  5. Fragments:允许组件返回多个根节点
  6. Teleport:可以将组件的一部分传送到 DOM 的其他部分
  7. Suspense:处理异步组件加载状态

环境搭建

使用 Vite 创建项目

Vite 是 Vue 团队开发的下一代前端构建工具,它显著提升了开发体验:

bash
# 安装
npm create vite@latest my-vue-app -- --template vue

# 或使用 yarn
yarn create vite my-vue-app --template vue

# 进入项目目录
cd my-vue-app

# 安装依赖
npm install
# 或
yarn

# 启动开发服务器
npm run dev
# 或
yarn dev

项目目录结构

推荐的目录结构:

src/
├── assets/           # 静态资源
├── components/       # 通用组件
│   └── common/       # 公共UI组件
│   └── layout/       # 布局组件
├── composables/      # 组合式函数
├── views/            # 页面组件
├── router/           # 路由配置
├── stores/           # Pinia 状态存储
├── services/         # API 服务
├── utils/            # 工具函数
├── App.vue           # 根组件
└── main.js           # 入口文件

Composition API 实践

基本用法

在 Vue 3 中,Composition API 是组织组件逻辑的新方式:

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

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

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

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

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

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

逻辑复用与组合式函数

组合式函数(Composables)是 Vue 3 中复用逻辑的主要方式:

js
// useCounter.js
import { ref, computed } from 'vue'

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

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  return {
    count,
    doubleCount,
    increment,
    decrement
  }
}

在组件中使用:

vue
<script setup>
import { useCounter } from '@/composables/useCounter'

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

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

响应式工具

Vue 3 提供了丰富的响应式 API:

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

// ref 用于基本类型
const message = ref('Hello')

// reactive 用于对象
const user = reactive({
  name: 'John',
  age: 30
})

// computed 创建计算属性
const greeting = computed(() => `${message.value}, ${user.name}!`)

// watch 监听特定数据变化
watch(message, (newValue, oldValue) => {
  console.log(`message changed from "${oldValue}" to "${newValue}"`)
}, { immediate: true })

// watchEffect 自动收集依赖并监听
watchEffect(() => {
  console.log(`User ${user.name} is ${user.age} years old`)
})

// 更新方法
function updateUser() {
  user.name = 'Jane'
  user.age = 25
  message.value = 'Welcome'
}
</script>

<template>
  <div>
    <p>{{ greeting }}</p>
    <button @click="updateUser">Update User</button>
  </div>
</template>

组件设计模式

Props 与事件

父子组件通信的基本方式:

vue
<!-- 子组件: ChildComponent.vue -->
<script setup>
defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})

const emit = defineEmits(['increment', 'update:count'])

function handleClick() {
  emit('increment')
  emit('update:count', props.count + 1) // 用于 v-model 绑定
}
</script>

<template>
  <div>
    <h2>{{ title }}</h2>
    <p>Count: {{ count }}</p>
    <button @click="handleClick">Increment</button>
  </div>
</template>

父组件使用:

vue
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const counter = ref(0)

function handleIncrement() {
  console.log('Incremented!')
}
</script>

<template>
  <div>
    <ChildComponent
      title="My Counter"
      :count="counter"
      @increment="handleIncrement"
      @update:count="counter = $event"
    />
    
    <!-- 或使用 v-model 简化 -->
    <ChildComponent
      title="My Counter with v-model"
      v-model:count="counter"
      @increment="handleIncrement"
    />
  </div>
</template>

插槽(Slots)

使用插槽实现内容分发:

vue
<!-- 基础插槽:CardComponent.vue -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header">默认标题</slot>
    </div>
    <div class="card-body">
      <slot>默认内容</slot>
    </div>
    <div class="card-footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

使用组件:

vue
<template>
  <CardComponent>
    <template #header>
      <h3>自定义标题</h3>
    </template>
    
    <p>这是主要内容</p>
    
    <template #footer>
      <button>确认</button>
      <button>取消</button>
    </template>
  </CardComponent>
</template>

动态组件

使用 <component> 动态切换组件:

vue
<script setup>
import { ref, shallowRef } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'
import TabC from './TabC.vue'

// 使用 shallowRef 可提高性能,因为组件对象不需要深层响应
const currentTab = shallowRef(TabA)
const tabs = {
  TabA,
  TabB,
  TabC
}

function switchTab(tab) {
  currentTab.value = tabs[tab]
}
</script>

<template>
  <div>
    <button @click="switchTab('TabA')">Tab A</button>
    <button @click="switchTab('TabB')">Tab B</button>
    <button @click="switchTab('TabC')">Tab C</button>
    
    <component :is="currentTab" />
  </div>
</template>

状态管理

Pinia

Pinia 是 Vue 官方推荐的状态管理库,替代了 Vuex:

安装:

bash
npm install pinia
# 或
yarn add pinia

配置:

js
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

创建 store:

js
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  // 状态
  state: () => ({
    count: 0,
    name: 'Counter'
  }),
  
  // 类似计算属性
  getters: {
    doubleCount: (state) => state.count * 2,
    // 使用this访问其他getter
    formattedCounter() {
      return `${this.name}: ${this.count}`
    }
  },
  
  // 修改状态的方法
  actions: {
    increment() {
      this.count++
    },
    async fetchInitialCount() {
      const data = await fetch('/api/counter')
      const result = await data.json()
      this.count = result.count
    }
  }
})

使用 Composition API 风格定义 store:

js
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // 状态
  const count = ref(0)
  const name = ref('Counter')
  
  // getters
  const doubleCount = computed(() => count.value * 2)
  
  // actions
  function increment() {
    count.value++
  }
  
  async function fetchInitialCount() {
    const data = await fetch('/api/counter')
    const result = await data.json()
    count.value = result.count
  }
  
  return { count, name, doubleCount, increment, fetchInitialCount }
})

在组件中使用:

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

const store = useCounterStore()

// 解构时保持响应性
const { count, doubleCount } = storeToRefs(store)

// 方法可以直接解构
const { increment } = store
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">+</button>
    <button @click="store.count++">+ (直接修改)</button>
  </div>
</template>

路由管理

Vue Router

Vue Router 是官方的路由管理器:

安装:

bash
npm install vue-router@4
# 或
yarn add vue-router@4

基本配置:

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

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/users/:id',
    name: 'UserDetail',
    // 路由级代码分割
    component: () => import('@/views/UserDetail.vue'),
    // 路由元信息
    meta: { 
      requiresAuth: true 
    }
  },
  // 404 路由
  { 
    path: '/:pathMatch(.*)*', 
    name: 'NotFound', 
    component: NotFound 
  }
]

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

// 全局前置守卫
router.beforeEach((to, from) => {
  // 检查是否需要认证
  if (to.meta.requiresAuth && !isAuthenticated()) {
    // 重定向到登录页
    return { name: 'Login', query: { redirect: to.fullPath } }
  }
})

export default router

main.js 中注册:

js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')

使用路由:

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

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

// 获取当前路由参数
const userId = route.params.id

// 编程式导航
function navigateToHome() {
  router.push('/')
}

function goToUser(id) {
  router.push({
    name: 'UserDetail',
    params: { id }
  })
}
</script>

<template>
  <div>
    <h1>用户详情</h1>
    <p>用户ID: {{ userId }}</p>
    <button @click="navigateToHome">返回首页</button>
    <button @click="goToUser(123)">查看用户123</button>
    
    <!-- 声明式导航 -->
    <router-link to="/">首页</router-link>
    <router-link :to="{ name: 'About' }">关于</router-link>
    
    <!-- 嵌套路由出口 -->
    <router-view />
  </div>
</template>

性能优化

虚拟列表

处理大量数据时,使用虚拟列表:

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

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

// 可视区域配置
const visibleItems = ref([])
const itemHeight = 40
const containerHeight = 400
const visibleCount = Math.ceil(containerHeight / itemHeight) + 2 // 额外渲染一些项防止滚动时出现空白
let startIndex = 0

// 计算可视项
function updateVisibleItems(scrollTop = 0) {
  startIndex = Math.floor(scrollTop / itemHeight)
  const start = Math.max(0, startIndex)
  const end = Math.min(items.value.length, start + visibleCount)
  
  visibleItems.value = items.value.slice(start, end).map(item => ({
    ...item,
    style: {
      position: 'absolute',
      top: `${item.id * itemHeight}px`,
      height: `${itemHeight}px`
    }
  }))
}

// 初始化
updateVisibleItems()

// 处理滚动
function handleScroll(e) {
  updateVisibleItems(e.target.scrollTop)
}
</script>

<template>
  <div 
    class="virtual-list" 
    @scroll="handleScroll"
    :style="{
      height: `${containerHeight}px`,
      overflow: 'auto',
      position: 'relative'
    }"
  >
    <div :style="{ height: `${items.length * itemHeight}px` }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        :style="item.style"
        class="list-item"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

<style scoped>
.virtual-list {
  border: 1px solid #ccc;
}
.list-item {
  padding: 10px;
  border-bottom: 1px solid #eee;
  box-sizing: border-box;
  width: 100%;
}
</style>

组件懒加载

使用 Vue Router 的动态导入实现组件懒加载:

js
// 懒加载路由组件
const routes = [
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue')
  }
]

使用 Suspense 处理异步组件

vue
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

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

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

使用 v-memo 避免不必要的重渲染

vue
<template>
  <div v-for="item in list" :key="item.id" v-memo="[item.id, item.active]">
    <!-- 只有当 item.id 或 item.active 变化时才会重新渲染 -->
    <span>{{ item.name }}</span>
    <span>{{ item.description }}</span>
  </div>
</template>

测试

使用 Vitest 进行单元测试

安装:

bash
npm install -D vitest @vue/test-utils happy-dom

配置 vite.config.js

js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'happy-dom'
  }
})

编写测试:

js
// counter.spec.js
import { mount } from '@vue/test-utils'
import { test, expect, describe } from 'vitest'
import Counter from '../components/Counter.vue'

describe('Counter.vue', () => {
  test('increments count when button is clicked', async () => {
    const wrapper = mount(Counter, {
      props: {
        initialCount: 5
      }
    })

    // 检查初始渲染
    expect(wrapper.text()).toContain('Count: 5')
    
    // 模拟按钮点击
    await wrapper.find('button').trigger('click')
    
    // 检查更新后的状态
    expect(wrapper.text()).toContain('Count: 6')
  })
})

最佳实践

  1. 使用 TypeScript:提供更好的开发体验和类型安全
vue
<script setup lang="ts">
import { ref, computed } from 'vue'

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

const user = ref<User | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)

const fullName = computed(() => 
  user.value ? `${user.value.name} (${user.value.id})` : 'Guest'
)

async function fetchUser(id: number): Promise<void> {
  loading.value = true
  error.value = null
  
  try {
    const response = await fetch(`/api/users/${id}`)
    if (!response.ok) {
      throw new Error('Failed to fetch user')
    }
    user.value = await response.json()
  } catch (err) {
    error.value = err as Error
  } finally {
    loading.value = false
  }
}
</script>
  1. 使用组合式函数抽取和复用逻辑

  2. 使用 <script setup> 简化组件

  3. 使用 Pinia 进行状态管理

  4. 按需加载组件减小包体积

  5. 正确使用 computedwatch

    • computed 适用于派生状态
    • watch 适用于响应状态变化的副作用
  6. 避免滥用 nextTick:通常不需要手动调用

  7. 使用 CSS 自定义属性实现主题

vue
<style>
:root {
  --primary-color: #42b883;
  --secondary-color: #35495e;
  --text-color: #333;
  --background-color: #fff;
}

.dark-theme {
  --primary-color: #3eaf7c;
  --secondary-color: #273849;
  --text-color: #f0f0f0;
  --background-color: #121212;
}

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

总结

Vue 3 带来了巨大的改进,特别是 Composition API,让开发者能够更灵活地组织代码并提高复用性。

本指南介绍了 Vue 3 开发的关键方面:

  • 基本概念和新特性
  • 组件设计和通信模式
  • 状态管理和路由
  • 性能优化技巧
  • 测试方法
  • 最佳实践

通过掌握这些知识和技术,你能够构建出高效、可维护且用户友好的 Vue 应用。随着你的应用变得更加复杂,可以进一步深入探索 Vue 生态系统中的其他工具和库,如 Nuxt.js(用于服务端渲染)、Vueuse(组合式函数集合)以及各种 UI 组件库。

RESTful API设计原则与最佳实践
API接口安全加密详解:四种关键防护模式全面分析