Vue.js 3 框架深入教程 — 前端开发首选

前置要求:HTML/CSS/JavaScript基础
学习时长:约3-5天(每天4小时)
Vue版本:3.4+(Composition API)
适用场景:单页应用、管理后台、移动端H5、小程序

一、Vue 3 核心概念

Vue 3 采用 Composition API,代码组织更灵活,逻辑复用更方便。

概念说明
响应式数据变化自动更新视图
组件化UI拆分为独立可复用的组件
Composition API使用函数组织逻辑(推荐)
Options API使用选项组织逻辑(传统)

二、项目创建

# 使用Vite创建(推荐)
npm create vite@latest my-vue-app -- --template vue

# 进入项目
cd my-vue-app
npm install
npm run dev

# 项目结构
my-vue-app/
├── public/
├── src/
│   ├── assets/           # 静态资源
│   ├── components/       # 组件
│   ├── composables/      # 组合式函数
│   ├── router/           # 路由
│   ├── stores/           # 状态管理
│   ├── views/            # 页面
│   ├── App.vue           # 根组件
│   └── main.js           # 入口文件
├── index.html
├── vite.config.js
└── package.json


三、Composition API

3.1 基本用法

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>计数:{{ count }}</p>
    <p>双倍:{{ doubleCount }}</p>
    <button @click="increment">+1</button>
    <button @click="reset">重置</button>
  </div>
</template>

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

// 响应式数据
const title = ref('Vue 3 计数器')
const count = ref(0)

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

// 方法
const increment = () => {
  count.value++
}

const reset = () => {
  count.value = 0
}

// 监听
watch(count, (newVal, oldVal) => {
  console.log(`计数从 ${oldVal} 变为 ${newVal}`)
})

// 生命周期
onMounted(() => {
  console.log('组件已挂载')
})
</script>

<style scoped>
button {
  margin: 0 8px;
  padding: 8px 16px;
}
</style>

3.2 响应式对象

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

// ref:基本类型
const name = ref('张三')
const age = ref(25)

// reactive:对象/数组
const user = reactive({
  name: '张三',
  age: 25,
  skills: ['Vue', 'React']
})

// 修改
user.name = '李四'
user.skills.push('TypeScript')

// 解构(需要toRefs保持响应性)
const { name: userName, age: userAge } = toRefs(user)

// 计算属性
const userInfo = computed(() => `${user.name},${user.age}岁`)

// 只读
const readOnlyUser = readonly(user)
</script>

3.3 组合式函数(Composables)

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

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  const doubleCount = computed(() => count.value * 2)
  
  return {
    count,
    increment,
    decrement,
    reset,
    doubleCount
  }
}

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

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(true)
  
  const fetchData = async () => {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(url.value || url)
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  if (typeof url === 'object' && url.value !== undefined) {
    watchEffect(fetchData)
  } else {
    fetchData()
  }
  
  return { data, error, loading, refetch: fetchData }
}

<!-- 使用组合式函数 -->
<script setup>
import { useCounter } from '@/composables/useCounter'
import { useFetch } from '@/composables/useFetch'

const { count, increment, decrement, reset } = useCounter(0)
const { data: users, loading, error } = useFetch('/api/users')
</script>


四、组件通信

4.1 Props 和 Emits

<!-- 父组件 Parent.vue -->
<template>
  <ChildComponent 
    :title="parentTitle"
    :count="count"
    @update="handleUpdate"
    @increment="count++"
  />
</template>

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

const parentTitle = ref('来自父组件')
const count = ref(0)

const handleUpdate = (value) => {
  console.log('子组件传来的值:', value)
}
</script>

<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <p>计数:{{ count }}</p>
    <button @click="sendToParent">通知父组件</button>
    <button @click="$emit('increment')">增加</button>
  </div>
</template>

<script setup>
// 定义Props
const props = defineProps({
  title: {
    type: String,
    default: ''
  },
  count: {
    type: Number,
    required: true
  }
})

// 定义Emits
const emit = defineEmits(['update', 'increment'])

const sendToParent = () => {
  emit('update', { message: '来自子组件', timestamp: Date.now() })
}
</script>

4.2 Provide / Inject

<!-- 祖先组件 GrandParent.vue -->
<script setup>
import { ref, provide } from 'vue'

const theme = ref('dark')
const user = ref({ name: '张三', role: 'admin' })

// 提供数据
provide('theme', theme)
provide('user', user)
provide('appName', 'My App')
</script>

<!-- 后代组件 GrandChild.vue -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme')         // 响应式
const user = inject('user')           // 响应式
const appName = inject('appName')     // 静态
const missing = inject('missing', '默认值')  // 带默认值
</script>

4.3 Pinia 状态管理

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

export const useUserStore = defineStore('user', () => {
  // State
  const userInfo = ref(null)
  const token = ref(localStorage.getItem('token') || '')
  
  // Getters
  const isLoggedIn = computed(() => !!token.value)
  const displayName = computed(() => 
    userInfo.value?.nickname || userInfo.value?.username || '未登录'
  )
  
  // Actions
  async function login(credentials) {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(credentials)
    })
    const data = await response.json()
    
    token.value = data.token
    userInfo.value = data.user
    localStorage.setItem('token', data.token)
  }
  
  async function fetchUser() {
    const response = await fetch('/api/user', {
      headers: { Authorization: `Bearer ${token.value}` }
    })
    userInfo.value = await response.json()
  }
  
  function logout() {
    token.value = ''
    userInfo.value = null
    localStorage.removeItem('token')
  }
  
  return {
    userInfo, token,
    isLoggedIn, displayName,
    login, fetchUser, logout
  }
})

<!-- 在组件中使用 -->
<script setup>
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// 访问状态
console.log(userStore.isLoggedIn)
console.log(userStore.displayName)

// 调用action
await userStore.login({ username: 'admin', password: '123456' })
userStore.logout()
</script>


五、Vue Router

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { guest: true }
  },
  {
    path: '/user/:id',
    name: 'UserDetail',
    component: () => import('@/views/UserDetail.vue'),
    props: true  // 将路由参数作为props
  },
  {
    path: '/admin',
    component: () => import('@/views/Admin.vue'),
    meta: { requiresAuth: true },
    children: [
      {
        path: '',
        name: 'AdminDashboard',
        component: () => import('@/views/admin/Dashboard.vue')
      },
      {
        path: 'users',
        name: 'AdminUsers',
        component: () => import('@/views/admin/Users.vue')
      },
      {
        path: 'posts',
        name: 'AdminPosts',
        component: () => import('@/views/admin/Posts.vue')
      }
    ]
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/404.vue')
  }
]

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

// 路由守卫
router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token')
  
  if (to.meta.requiresAuth && !token) {
    next({ name: 'Login', query: { redirect: to.fullPath } })
  } else if (to.meta.guest && token) {
    next({ name: 'Home' })
  } else {
    next()
  }
})

export default router

<!-- 使用路由 -->
<template>
  <nav>
    <router-link to="/">首页</router-link>
    <router-link to="/admin">后台</router-link>
  </nav>
  
  <router-view />
</template>

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

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

// 获取路由参数
console.log(route.params.id)
console.log(route.query.page)

// 编程式导航
router.push('/login')
router.push({ name: 'UserDetail', params: { id: 123 } })
router.replace('/home')
router.go(-1)  // 后退
</script>


六、表单处理

<template>
  <form @submit.prevent="handleSubmit">
    <div>
      <label>用户名:</label>
      <input v-model="form.username" type="text" />
      <span class="error" v-if="errors.username">{{ errors.username }}</span>
    </div>
    
    <div>
      <label>邮箱:</label>
      <input v-model="form.email" type="email" />
      <span class="error" v-if="errors.email">{{ errors.email }}</span>
    </div>
    
    <div>
      <label>角色:</label>
      <select v-model="form.role">
        <option value="">请选择</option>
        <option value="admin">管理员</option>
        <option value="user">普通用户</option>
      </select>
    </div>
    
    <div>
      <label>
        <input v-model="form.isActive" type="checkbox" />
        启用
      </label>
    </div>
    
    <button type="submit" :disabled="loading">
      {{ loading ? '提交中...' : '提交' }}
    </button>
  </form>
</template>

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

const loading = ref(false)
const form = reactive({
  username: '',
  email: '',
  role: '',
  isActive: true
})
const errors = reactive({})

const validate = () => {
  errors.username = ''
  errors.email = ''
  
  if (!form.username) {
    errors.username = '用户名不能为空'
  } else if (form.username.length < 3) {
    errors.username = '用户名至少3个字符'
  }
  
  if (!form.email) {
    errors.email = '邮箱不能为空'
  } else if (!/\S+@\S+\.\S+/.test(form.email)) {
    errors.email = '邮箱格式不正确'
  }
  
  return !errors.username && !errors.email
}

const handleSubmit = async () => {
  if (!validate()) return
  
  loading.value = true
  try {
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(form)
    })
    
    if (response.ok) {
      alert('创建成功')
    }
  } finally {
    loading.value = false
  }
}
</script>


七、列表与分页

<template>
  <div>
    <!-- 搜索 -->
    <input v-model="keyword" placeholder="搜索..." @input="debounceSearch" />
    
    <!-- 加载状态 -->
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">{{ error }}</div>
    
    <!-- 列表 -->
    <div v-else>
      <div v-for="user in users" :key="user.id" class="user-card">
        <h3>{{ user.username }}</h3>
        <p>{{ user.email }}</p>
        <button @click="deleteUser(user.id)">删除</button>
      </div>
      
      <div v-if="users.length === 0">暂无数据</div>
    </div>
    
    <!-- 分页 -->
    <div class="pagination">
      <button :disabled="page <= 1" @click="page--; fetchUsers()">上一页</button>
      <span>{{ page }} / {{ totalPages }}</span>
      <button :disabled="page >= totalPages" @click="page++; fetchUsers()">下一页</button>
    </div>
  </div>
</template>

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

const users = ref([])
const loading = ref(false)
const error = ref(null)
const keyword = ref('')
const page = ref(1)
const size = ref(10)
const total = ref(0)

const totalPages = computed(() => Math.ceil(total.value / size.value))

let searchTimeout = null
const debounceSearch = () => {
  clearTimeout(searchTimeout)
  searchTimeout = setTimeout(() => {
    page.value = 1
    fetchUsers()
  }, 300)
}

const fetchUsers = async () => {
  loading.value = true
  error.value = null
  
  try {
    const params = new URLSearchParams({
      keyword: keyword.value,
      page: page.value,
      size: size.value
    })
    
    const response = await fetch(`/api/users?${params}`)
    const data = await response.json()
    
    users.value = data.items
    total.value = data.total
  } catch (err) {
    error.value = '加载失败'
  } finally {
    loading.value = false
  }
}

const deleteUser = async (id) => {
  if (!confirm('确定删除?')) return
  
  await fetch(`/api/users/${id}`, { method: 'DELETE' })
  fetchUsers()
}

onMounted(fetchUsers)
</script>


八、常用UI组件库

组件库适用场景特点
Element Plus管理后台组件最全、文档好
Ant Design Vue企业应用设计规范、组件丰富
Naive UI现代应用TypeScript友好
VuetifyMaterial风格Google设计语言
PrimeVue通用组件丰富、主题多
# 安装Element Plus
npm install element-plus

# main.js中引入
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)


学习建议

  1. 先掌握Composition API,这是Vue 3的核心
  2. 理解响应式原理,ref和reactive的区别
  3. 学会组件通信,props/emit/provide/inject
  4. 掌握Vue Router,单页应用必备
  5. 学习Pinia,状态管理是大型应用的基础

下一步学习

返回首页