← 返回主页

第15课: 项目实战与最佳实践

项目结构最佳实践

my-vue-app/
├── public/              # 静态资源
│   └── favicon.ico
├── src/
│   ├── assets/          # 资源文件(图片、样式等)
│   ├── components/      # 通用组件
│   │   ├── common/      # 基础组件
│   │   └── business/    # 业务组件
│   ├── composables/     # 组合式函数
│   ├── directives/      # 自定义指令
│   ├── layouts/         # 布局组件
│   ├── router/          # 路由配置
│   ├── stores/          # Pinia状态管理
│   ├── types/           # TypeScript类型定义
│   ├── utils/           # 工具函数
│   ├── views/           # 页面组件
│   ├── App.vue          # 根组件
│   └── main.ts          # 入口文件
├── .env                 # 环境变量
├── .gitignore
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts

组件设计原则

代码规范

组件顺序

<script setup lang="ts">
// 1. 导入
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import MyComponent from '@/components/MyComponent.vue'

// 2. Props和Emits
interface Props {
  title: string
}
const props = defineProps<Props>()
const emit = defineEmits<{ submit: [value: string] }>()

// 3. 响应式数据
const count = ref(0)
const state = reactive({ name: '' })

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

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

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

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

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

性能优化

1. 路由懒加载

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

2. 组件懒加载

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

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

3. v-memo优化

<template>
  <div v-for="item in list" :key="item.id" v-memo="[item.id, item.name]">
    {{ item.name }}
  </div>
</template>

4. 虚拟滚动

// 使用vue-virtual-scroller
<template>
  <RecycleScroller
    :items="items"
    :item-size="50"
    key-field="id"
  >
    <template #default="{ item }">
      <div>{{ item.name }}</div>
    </template>
  </RecycleScroller>
</template>

5. 防抖和节流

<script setup>
import { ref } from 'vue'
import { useDebounceFn, useThrottleFn } from '@vueuse/core'

const searchText = ref('')

const debouncedSearch = useDebounceFn(() => {
  console.log('搜索:', searchText.value)
}, 500)

const throttledScroll = useThrottleFn(() => {
  console.log('滚动')
}, 200)
</script>

状态管理最佳实践

// stores/modules/user.ts
import { defineStore } from 'pinia'

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

  // Getters
  const isLoggedIn = computed(() => !!token.value)

  // Actions
  async function login(credentials: LoginCredentials) {
    const response = await api.login(credentials)
    user.value = response.user
    token.value = response.token
  }

  function logout() {
    user.value = null
    token.value = null
  }

  return { user, token, isLoggedIn, login, logout }
})

API封装

// utils/request.ts
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig } from 'axios'

const request: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 10000
})

// 请求拦截器
request.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => Promise.reject(error)
)

// 响应拦截器
request.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.response?.status === 401) {
      // 跳转到登录页
    }
    return Promise.reject(error)
  }
)

export default request
// api/user.ts
import request from '@/utils/request'
import type { User, LoginCredentials } from '@/types'

export const userApi = {
  login(credentials: LoginCredentials) {
    return request.post<{ user: User; token: string }>('/login', credentials)
  },

  getProfile() {
    return request.get<User>('/user/profile')
  },

  updateProfile(data: Partial<User>) {
    return request.put<User>('/user/profile', data)
  }
}

错误处理

// composables/useErrorHandler.ts
import { ref } from 'vue'

export function useErrorHandler() {
  const error = ref<Error | null>(null)

  function handleError(e: unknown) {
    if (e instanceof Error) {
      error.value = e
      console.error('错误:', e.message)
    } else {
      error.value = new Error('未知错误')
    }
  }

  function clearError() {
    error.value = null
  }

  return { error, handleError, clearError }
}

环境变量管理

# .env.development
VITE_API_URL=http://localhost:3000/api
VITE_APP_TITLE=开发环境

# .env.production
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=生产环境
// 使用环境变量
const apiUrl = import.meta.env.VITE_API_URL
const appTitle = import.meta.env.VITE_APP_TITLE

测试

单元测试

// MyComponent.spec.ts
import { mount } from '@vue/test-utils'
import MyComponent from '@/components/MyComponent.vue'

describe('MyComponent', () => {
  it('renders properly', () => {
    const wrapper = mount(MyComponent, {
      props: { title: 'Hello' }
    })
    expect(wrapper.text()).toContain('Hello')
  })

  it('increments count on click', async () => {
    const wrapper = mount(MyComponent)
    await wrapper.find('button').trigger('click')
    expect(wrapper.vm.count).toBe(1)
  })
})

实战项目:待办事项应用

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

interface Todo {
  id: number
  text: string
  completed: boolean
}

export const useTodoStore = defineStore('todo', () => {
  const todos = ref<Todo[]>([])
  const filter = ref<'all' | 'active' | 'completed'>('all')

  const filteredTodos = computed(() => {
    switch (filter.value) {
      case 'active':
        return todos.value.filter(t => !t.completed)
      case 'completed':
        return todos.value.filter(t => t.completed)
      default:
        return todos.value
    }
  })

  const activeCount = computed(() => {
    return todos.value.filter(t => !t.completed).length
  })

  function addTodo(text: string) {
    todos.value.push({
      id: Date.now(),
      text,
      completed: false
    })
  }

  function removeTodo(id: number) {
    const index = todos.value.findIndex(t => t.id === id)
    if (index > -1) {
      todos.value.splice(index, 1)
    }
  }

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

  function clearCompleted() {
    todos.value = todos.value.filter(t => !t.completed)
  }

  return {
    todos,
    filter,
    filteredTodos,
    activeCount,
    addTodo,
    removeTodo,
    toggleTodo,
    clearCompleted
  }
})
<!-- TodoApp.vue -->
<template>
  <div class="todo-app">
    <h1>待办事项</h1>

    <input
      v-model="newTodo"
      @keyup.enter="addTodo"
      placeholder="添加待办事项"
    >

    <div class="filters">
      <button @click="store.filter = 'all'">全部</button>
      <button @click="store.filter = 'active'">未完成</button>
      <button @click="store.filter = 'completed'">已完成</button>
    </div>

    <ul class="todo-list">
      <li v-for="todo in store.filteredTodos" :key="todo.id">
        <input
          type="checkbox"
          :checked="todo.completed"
          @change="store.toggleTodo(todo.id)"
        >
        <span :class="{ completed: todo.completed }">
          {{ todo.text }}
        </span>
        <button @click="store.removeTodo(todo.id)">删除</button>
      </li>
    </ul>

    <div class="footer">
      <span>剩余 {{ store.activeCount }} 项</span>
      <button @click="store.clearCompleted">清除已完成</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useTodoStore } from '@/stores/todo'

const store = useTodoStore()
const newTodo = ref('')

function addTodo() {
  if (newTodo.value.trim()) {
    store.addTodo(newTodo.value)
    newTodo.value = ''
  }
}
</script>

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

.completed {
  text-decoration: line-through;
  color: #999;
}

.filters button {
  margin: 0 5px;
}
</style>

部署

构建生产版本

npm run build

预览构建结果

npm run preview

部署到Vercel

# 安装Vercel CLI
npm i -g vercel

# 部署
vercel

部署到Netlify

# 安装Netlify CLI
npm i -g netlify-cli

# 部署
netlify deploy --prod

常用工具库推荐

学习资源

总结

恭喜你完成了Vue3的学习!现在你已经掌握了:

下一步:继续实践,构建真实项目,深入学习Vue3生态系统,成为Vue3专家!

练习项目

  1. 构建一个完整的博客系统(文章列表、详情、评论)
  2. 开发一个电商网站(商品展示、购物车、订单管理)
  3. 创建一个项目管理工具(任务管理、团队协作)
  4. 实现一个社交媒体应用(用户动态、关注、点赞)