前置要求: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友好 |
| Vuetify | Material风格 | Google设计语言 |
| PrimeVue | 通用 | 组件丰富、主题多 |
# 安装Element Plus
npm install element-plus
# main.js中引入
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)
学习建议
- 先掌握Composition API,这是Vue 3的核心
- 理解响应式原理,ref和reactive的区别
- 学会组件通信,props/emit/provide/inject
- 掌握Vue Router,单页应用必备
- 学习Pinia,状态管理是大型应用的基础
下一步学习
- TypeScript — 类型安全
- Nuxt.js — SSR框架
- uni-app — 跨端开发