Vue.js快速入门
本文面向具有 JavaScript 基础的开发者,快速介绍 Vue.js 3.x 的核心概念和开发方式。如果你已经熟悉 JavaScript 和基本的前端开发,这篇文章将帮助你快速上手 Vue 3。
# 一、Vue.js 简介
# 什么是 Vue.js
Vue.js(读音 /vjuː/,类似于 view)是一款用于构建用户界面的渐进式 JavaScript 框架。由尤雨溪(Evan You)于 2014 年创建,现已成为最受欢迎的前端框架之一。
Vue.js 与 JavaScript 的关系:
- JavaScript 是一门编程语言,提供基础语法和 API
- Vue.js 是基于 JavaScript 构建的框架,提供响应式数据绑定、组件系统等高级特性
- 如果你还不熟悉 JavaScript,建议先阅读 JavaScript极简入门
# 核心特性
- 渐进式框架:可以只用 Vue 的核心库,也可以结合路由、状态管理等构建完整应用
- 响应式数据绑定:数据变化自动更新视图
- 组件化开发:将 UI 拆分为可复用的独立组件
- 声明式渲染:用模板语法描述视图,框架负责 DOM 更新
- 虚拟 DOM:高效的 DOM 更新机制
- 优秀的性能:Vue 3 在性能上有显著提升
# Vue 3 vs Vue 2
Vue 3 是当前推荐版本,主要改进包括:
- Composition API:更好的逻辑复用和代码组织
- 更好的 TypeScript 支持:用 TypeScript 重写
- 更小的体积:Tree-shaking 支持
- 更快的性能:虚拟 DOM 重写,速度提升 1.3-2 倍
<script setup>:更简洁的单文件组件语法
如需了解详细的版本演进,请参考 Vue.js历代版本新特性。
# 适用场景
- 单页应用(SPA):完整的前端应用
- 组件库:可复用的 UI 组件
- 渐进式增强:在现有项目中逐步引入 Vue
- 移动端应用:结合 Ionic、Quasar 等框架
- 桌面应用:结合 Electron
- 服务端渲染(SSR):结合 Nuxt.js
# 二、快速开始
# 安装 Node.js
Vue 开发需要 Node.js 环境(推荐 18.x 或更高版本)。访问 Node.js 官网 (opens new window) 下载安装,或参考 Node.js快速入门。
验证安装:
node -v
npm -v
# 创建 Vue 项目
使用官方脚手架 create-vue 创建项目:
# 创建新项目
npm create vue@latest
# 按照提示选择配置
✔ Project name: my-vue-app
✔ Add TypeScript? Yes
✔ Add JSX Support? No
✔ Add Vue Router for Single Page Application development? Yes
✔ Add Pinia for state management? Yes
✔ Add Vitest for Unit Testing? No
✔ Add an End-to-End Testing Solution? No
✔ Add ESLint for code quality? Yes
✔ Add Prettier for code formatting? Yes
# 进入项目目录
cd my-vue-app
# 安装依赖
npm install
# 启动开发服务器
npm run dev
打开浏览器访问 http://localhost:5173,你将看到 Vue 的欢迎页面。
# 项目结构
my-vue-app/
├── public/ # 静态资源
├── src/
│ ├── assets/ # 资源文件(图片、样式等)
│ ├── components/ # 组件
│ ├── router/ # 路由配置
│ ├── stores/ # Pinia 状态管理
│ ├── views/ # 页面组件
│ ├── App.vue # 根组件
│ └── main.ts # 应用入口
├── index.html # HTML 模板
├── package.json # 项目配置
├── vite.config.ts # Vite 配置
└── tsconfig.json # TypeScript 配置
# 使用 CDN(快速体验)
如果只是想快速体验 Vue,可以直接在 HTML 中引入 CDN:
<!DOCTYPE html>
<html>
<head>
<title>Vue 3 快速体验</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<h1>{{ message }}</h1>
<button @click="count++">点击次数: {{ count }}</button>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const message = ref('Hello Vue 3!')
const count = ref(0)
return {
message,
count
}
}
}).mount('#app')
</script>
</body>
</html>
# 三、响应式基础
# ref 和 reactive
Vue 3 提供了两种创建响应式数据的方式:
<script setup>
import { ref, reactive } from 'vue'
// ref:适合基本类型和单个值
const count = ref(0)
const message = ref('Hello')
// 访问和修改 ref 的值需要 .value
console.log(count.value) // 0
count.value++
// reactive:适合对象和数组
const user = reactive({
name: 'Alice',
age: 25
})
// 直接访问和修改属性
console.log(user.name) // Alice
user.age = 26
</script>
<template>
<!-- 模板中自动解包,不需要 .value -->
<p>{{ count }}</p>
<p>{{ message }}</p>
<p>{{ user.name }} - {{ user.age }}</p>
</template>
最佳实践:
- 基本类型(
number、string、boolean)使用ref - 对象和数组使用
reactive或ref - 统一使用
ref更简单(可以适用所有场景)
# computed 计算属性
计算属性是基于响应式依赖进行缓存的:
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
// 只读计算属性
const double = computed(() => count.value * 2)
// 可写计算属性
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(value) {
[firstName.value, lastName.value] = value.split(' ')
}
})
</script>
<template>
<p>Count: {{ count }}</p>
<p>Double: {{ double }}</p>
</template>
computed vs 普通函数:
computed会缓存结果,只有依赖变化时才重新计算- 普通函数每次渲染都会执行
# watch 和 watchEffect
监听响应式数据的变化:
<script setup>
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
const user = reactive({ name: 'Alice', age: 25 })
// watch:需要明确指定监听的数据源
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变为 ${newValue}`)
})
// 监听多个数据源
watch([count, () => user.age], ([newCount, newAge], [oldCount, oldAge]) => {
console.log(`count: ${oldCount} -> ${newCount}`)
console.log(`age: ${oldAge} -> ${newAge}`)
})
// 深度监听对象
watch(user, (newValue, oldValue) => {
console.log('user 发生变化')
}, { deep: true })
// watchEffect:自动追踪依赖
watchEffect(() => {
console.log(`count 是 ${count.value}`)
console.log(`user.age 是 ${user.age}`)
})
</script>
watch vs watchEffect:
watch明确指定监听源,可以访问旧值watchEffect自动追踪依赖,更简洁
# 四、模板语法
# 文本插值
使用双大括号 进行文本插值:
<template>
<p>{{ message }}</p>
<p>{{ count * 2 }}</p>
<p>{{ ok ? 'YES' : 'NO' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
</template>
插值内容会自动转义,防止 XSS 攻击。
# 指令
指令是带有 v- 前缀的特殊属性:
<template>
<!-- v-bind:绑定属性(缩写 :) -->
<img v-bind:src="imageUrl" />
<img :src="imageUrl" />
<div :class="{ active: isActive }"></div>
<div :style="{ color: textColor, fontSize: fontSize + 'px' }"></div>
<!-- v-on:绑定事件(缩写 @) -->
<button v-on:click="handleClick">点击</button>
<button @click="handleClick">点击</button>
<button @click="count++">增加</button>
<input @keyup.enter="submit" />
<!-- v-model:双向绑定 -->
<input v-model="message" />
<textarea v-model="text"></textarea>
<input type="checkbox" v-model="checked" />
<select v-model="selected">
<option>选项1</option>
<option>选项2</option>
</select>
<!-- v-if / v-else-if / v-else:条件渲染 -->
<div v-if="score >= 90">优秀</div>
<div v-else-if="score >= 60">及格</div>
<div v-else>不及格</div>
<!-- v-show:基于 CSS 的显示/隐藏 -->
<div v-show="isVisible">显示内容</div>
<!-- v-for:列表渲染 -->
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
<!-- v-html:渲染 HTML(谨慎使用,防止 XSS) -->
<div v-html="htmlContent"></div>
<!-- v-once:只渲染一次 -->
<span v-once>{{ message }}</span>
</template>
<script setup>
import { ref } from 'vue'
const imageUrl = ref('https://example.com/image.jpg')
const isActive = ref(true)
const message = ref('Hello')
const items = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
])
function handleClick() {
console.log('clicked')
}
</script>
v-if vs v-show:
v-if是真正的条件渲染,会销毁和重建元素v-show只是切换displayCSS 属性- 频繁切换用
v-show,运行时条件很少改变用v-if
# 动态参数
指令参数可以是动态的:
<template>
<a v-bind:[attributeName]="url">链接</a>
<button v-on:[eventName]="handler">按钮</button>
</template>
<script setup>
import { ref } from 'vue'
const attributeName = ref('href')
const eventName = ref('click')
const url = ref('https://vuejs.org')
function handler() {
console.log('事件触发')
}
</script>
# 五、组件基础
# 定义组件
单文件组件(SFC)是 Vue 的推荐开发方式:
<!-- MyButton.vue -->
<script setup>
import { ref } from 'vue'
// 定义 props
const props = defineProps({
text: String,
type: {
type: String,
default: 'primary'
}
})
// 定义 emits
const emit = defineEmits(['click', 'submit'])
const count = ref(0)
function handleClick() {
count.value++
emit('click', count.value)
}
</script>
<template>
<button
:class="`btn btn-${type}`"
@click="handleClick"
>
{{ text }} ({{ count }})
</button>
</template>
<style scoped>
.btn {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background: #42b983;
color: white;
}
</style>
# 使用组件
<!-- App.vue -->
<script setup>
import MyButton from './components/MyButton.vue'
function onButtonClick(count) {
console.log('按钮被点击', count)
}
</script>
<template>
<div>
<MyButton text="点击我" type="primary" @click="onButtonClick" />
<MyButton text="提交" type="success" @click="onButtonClick" />
</div>
</template>
# Props 传递
父组件向子组件传递数据:
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import UserCard from './components/UserCard.vue'
const user = ref({
name: 'Alice',
age: 25,
email: 'alice@example.com'
})
</script>
<template>
<UserCard :user="user" :show-email="true" />
</template>
<!-- UserCard.vue 子组件 -->
<script setup>
// 基础用法
const props = defineProps({
user: {
type: Object,
required: true
},
showEmail: {
type: Boolean,
default: false
}
})
// TypeScript 类型声明(推荐)
const props = defineProps<{
user: {
name: string
age: number
email: string
}
showEmail?: boolean
}>()
</script>
<template>
<div class="user-card">
<h3>{{ user.name }}</h3>
<p>年龄: {{ user.age }}</p>
<p v-if="showEmail">邮箱: {{ user.email }}</p>
</div>
</template>
Props 注意事项:
- Props 是单向数据流(父→子),不应在子组件中直接修改
- 需要修改时,可以使用
computed或emit事件通知父组件
# Emits 事件
子组件向父组件发送事件:
<!-- 子组件 Counter.vue -->
<script setup>
import { ref } from 'vue'
// 声明事件
const emit = defineEmits(['update', 'reset'])
// TypeScript 类型声明
const emit = defineEmits<{
update: [value: number]
reset: []
}>()
const count = ref(0)
function increment() {
count.value++
emit('update', count.value)
}
function reset() {
count.value = 0
emit('reset')
}
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
<button @click="reset">重置</button>
</div>
</template>
<!-- 父组件 -->
<script setup>
import Counter from './components/Counter.vue'
function onUpdate(value) {
console.log('计数器更新:', value)
}
function onReset() {
console.log('计数器重置')
}
</script>
<template>
<Counter @update="onUpdate" @reset="onReset" />
</template>
# v-model 双向绑定
v-model 是 props + emit 的语法糖:
<!-- 子组件 CustomInput.vue -->
<script setup>
// v-model 默认使用 modelValue 属性和 update:modelValue 事件
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
function handleInput(event: Event) {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)
}
</script>
<template>
<input :value="modelValue" @input="handleInput" />
</template>
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import CustomInput from './components/CustomInput.vue'
const text = ref('')
</script>
<template>
<CustomInput v-model="text" />
<p>输入内容: {{ text }}</p>
</template>
多个 v-model:
<!-- 子组件 -->
<script setup>
defineProps<{
firstName: string
lastName: string
}>()
const emit = defineEmits<{
'update:firstName': [value: string]
'update:lastName': [value: string]
}>()
</script>
<template>
<input
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>
<!-- 父组件 -->
<template>
<UserForm
v-model:first-name="firstName"
v-model:last-name="lastName"
/>
</template>
# 插槽(Slots)
插槽允许父组件向子组件传递模板内容:
<!-- 子组件 Card.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>
<!-- 父组件 -->
<template>
<Card>
<template #header>
<h3>标题</h3>
</template>
<p>这是卡片内容</p>
<template #footer>
<button>确定</button>
</template>
</Card>
</template>
作用域插槽:子组件向插槽传递数据
<!-- 子组件 List.vue -->
<script setup>
defineProps<{
items: Array<{ id: number; name: string }>
}>()
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">
<!-- 传递数据给插槽 -->
<slot :item="item" :index="item.id"></slot>
</li>
</ul>
</template>
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import List from './components/List.vue'
const items = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
])
</script>
<template>
<List :items="items">
<template #default="{ item, index }">
<strong>{{ index }}</strong>: {{ item.name }}
</template>
</List>
</template>
# 六、生命周期
Vue 组件的生命周期钩子:
<script setup>
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'
const count = ref(0)
// 组件挂载后执行(最常用)
onMounted(() => {
console.log('组件已挂载')
// 适合:发起 API 请求、访问 DOM、初始化第三方库
})
// 组件更新后执行
onUpdated(() => {
console.log('组件已更新')
})
// 组件卸载前执行
onUnmounted(() => {
console.log('组件即将卸载')
// 适合:清理定时器、取消请求、移除事件监听
})
// 其他生命周期
onBeforeMount(() => {
console.log('挂载之前')
})
onBeforeUpdate(() => {
console.log('更新之前')
})
onBeforeUnmount(() => {
console.log('卸载之前')
})
</script>
生命周期流程:
onBeforeMount- 挂载前onMounted- 挂载后(可以访问 DOM)onBeforeUpdate- 数据变化,更新前onUpdated- 更新后onBeforeUnmount- 卸载前onUnmounted- 卸载后
# 七、组合式 API 进阶
# 组合式函数(Composables)
将可复用的逻辑提取为组合式函数(类似 React Hooks):
// composables/useCounter.js
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return {
count,
increment,
decrement,
reset
}
}
<!-- 使用组合式函数 -->
<script setup>
import { useCounter } from './composables/useCounter'
const { count, increment, decrement, reset } = useCounter(10)
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">重置</button>
</div>
</template>
常见的组合式函数:
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
// composables/useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
async function fetchData() {
loading.value = true
error.value = null
try {
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
fetchData()
return { data, error, loading }
}
# Provide / Inject
跨层级组件通信(类似 React Context):
<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
// 提供数据
provide('theme', theme)
// 提供方法
provide('toggleTheme', () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
})
</script>
<!-- 后代组件(可以跨多层) -->
<script setup>
import { inject } from 'vue'
// 注入数据
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
</script>
<template>
<div :class="theme">
<p>当前主题: {{ theme }}</p>
<button @click="toggleTheme">切换主题</button>
</div>
</template>
TypeScript 类型安全:
// types.ts
import type { InjectionKey, Ref } from 'vue'
export const themeKey: InjectionKey<Ref<string>> = Symbol()
<script setup lang="ts">
import { provide, inject } from 'vue'
import { themeKey } from './types'
// 提供
const theme = ref('dark')
provide(themeKey, theme)
// 注入
const theme = inject(themeKey)
</script>
# defineExpose
<script setup> 的组件默认是封闭的,使用 defineExpose 暴露属性和方法:
<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
// 显式暴露
defineExpose({
count,
increment
})
</script>
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = ref(null)
function callChild() {
childRef.value.increment()
console.log(childRef.value.count)
}
</script>
<template>
<ChildComponent ref="childRef" />
<button @click="callChild">调用子组件方法</button>
</template>
# 八、路由(Vue Router)
# 安装和配置
npm install vue-router@4
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: About
},
{
// 动态路由
path: '/user/:id',
name: 'user',
component: () => import('../views/User.vue')
},
{
// 404
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('../views/NotFound.vue')
}
]
})
export default router
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
# 使用路由
<!-- App.vue -->
<template>
<nav>
<!-- 声明式导航 -->
<RouterLink to="/">首页</RouterLink>
<RouterLink to="/about">关于</RouterLink>
<RouterLink :to="{ name: 'user', params: { id: 123 }}">用户</RouterLink>
</nav>
<!-- 路由视图 -->
<RouterView />
</template>
<!-- views/User.vue -->
<script setup>
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// 获取路由参数
const userId = route.params.id
// 编程式导航
function goHome() {
router.push('/')
}
function goBack() {
router.back()
}
</script>
<template>
<div>
<h1>用户 ID: {{ userId }}</h1>
<button @click="goHome">返回首页</button>
<button @click="goBack">后退</button>
</div>
</template>
# 路由守卫
// router/index.ts
router.beforeEach((to, from, next) => {
// 全局前置守卫
console.log('导航到:', to.path)
// 检查权限
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login')
} else {
next()
}
})
router.afterEach((to, from) => {
// 全局后置守卫
document.title = to.meta.title || 'My App'
})
// 路由独享守卫
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
if (isAdmin()) {
next()
} else {
next('/403')
}
}
}
<!-- 组件内守卫 -->
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
return confirm('有未保存的更改,确定离开吗?')
}
})
</script>
# 九、状态管理(Pinia)
# 安装和配置
npm install pinia
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
# 定义 Store
// stores/counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 组合式 API 风格(推荐)
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0)
const name = ref('Counter')
// getters
const doubleCount = computed(() => count.value * 2)
// actions
function increment() {
count.value++
}
function incrementBy(amount: number) {
count.value += amount
}
async function fetchCount() {
const response = await fetch('/api/count')
count.value = await response.json()
}
return {
count,
name,
doubleCount,
increment,
incrementBy,
fetchCount
}
})
// 选项式 API 风格
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Counter'
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
incrementBy(amount: number) {
this.count += amount
}
}
})
# 使用 Store
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
// 直接访问(会失去响应性)
const count = counterStore.count
// 使用 storeToRefs 保持响应性
const { count, doubleCount } = storeToRefs(counterStore)
// 直接使用 actions(不需要 storeToRefs)
const { increment, incrementBy } = counterStore
// 或者
counterStore.increment()
counterStore.incrementBy(5)
// 批量更新
counterStore.$patch({
count: 10,
name: 'New Counter'
})
// 重置 store
counterStore.$reset()
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">+1</button>
<button @click="incrementBy(5)">+5</button>
</div>
</template>
# Store 之间的通信
// stores/user.ts
import { defineStore } from 'pinia'
import { useCartStore } from './cart'
export const useUserStore = defineStore('user', () => {
const cartStore = useCartStore()
function clearUserData() {
// 调用其他 store 的方法
cartStore.clear()
}
return {
clearUserData
}
})
# 十、常见实战场景
# HTTP 请求
推荐使用 axios 或浏览器原生 fetch:
npm install axios
// api/index.ts
import axios from 'axios'
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
if (error.response?.status === 401) {
// 跳转登录页
}
return Promise.reject(error)
}
)
export default api
<script setup>
import { ref, onMounted } from 'vue'
import api from '@/api'
const users = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchUsers() {
loading.value = true
error.value = null
try {
users.value = await api.get('/users')
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
onMounted(() => {
fetchUsers()
})
</script>
<template>
<div>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</div>
</template>
封装为组合式函数:
// composables/useRequest.ts
import { ref } from 'vue'
export function useRequest<T>(requestFn: () => Promise<T>) {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
async function execute() {
loading.value = true
error.value = null
try {
data.value = await requestFn()
} catch (err) {
error.value = err as Error
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
execute
}
}
<script setup>
import { useRequest } from '@/composables/useRequest'
import api from '@/api'
const { data: users, loading, error, execute } = useRequest(() => api.get('/users'))
onMounted(() => {
execute()
})
</script>
# 表单处理
<script setup>
import { reactive, ref } from 'vue'
const form = reactive({
username: '',
email: '',
password: '',
gender: '',
hobbies: [],
agree: false
})
const errors = reactive({
username: '',
email: '',
password: ''
})
function validateForm() {
let valid = true
if (form.username.length < 3) {
errors.username = '用户名至少3个字符'
valid = false
} else {
errors.username = ''
}
if (!/\S+@\S+\.\S+/.test(form.email)) {
errors.email = '邮箱格式不正确'
valid = false
} else {
errors.email = ''
}
if (form.password.length < 6) {
errors.password = '密码至少6个字符'
valid = false
} else {
errors.password = ''
}
return valid
}
async function handleSubmit() {
if (!validateForm()) {
return
}
try {
const response = await api.post('/register', form)
console.log('注册成功', response)
} catch (error) {
console.error('注册失败', error)
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<div>
<label>用户名</label>
<input v-model="form.username" type="text" />
<span class="error">{{ errors.username }}</span>
</div>
<div>
<label>邮箱</label>
<input v-model="form.email" type="email" />
<span class="error">{{ errors.email }}</span>
</div>
<div>
<label>密码</label>
<input v-model="form.password" type="password" />
<span class="error">{{ errors.password }}</span>
</div>
<div>
<label>性别</label>
<label><input v-model="form.gender" type="radio" value="male" /> 男</label>
<label><input v-model="form.gender" type="radio" value="female" /> 女</label>
</div>
<div>
<label>爱好</label>
<label><input v-model="form.hobbies" type="checkbox" value="reading" /> 阅读</label>
<label><input v-model="form.hobbies" type="checkbox" value="sports" /> 运动</label>
<label><input v-model="form.hobbies" type="checkbox" value="music" /> 音乐</label>
</div>
<div>
<label>
<input v-model="form.agree" type="checkbox" />
我同意服务条款
</label>
</div>
<button type="submit">提交</button>
</form>
</template>
<style scoped>
.error {
color: red;
font-size: 12px;
}
</style>
# 条件渲染与列表
<script setup>
import { ref, computed } from 'vue'
const todos = ref([
{ id: 1, text: '学习 Vue 3', done: true },
{ id: 2, text: '写一个项目', done: false },
{ id: 3, text: '复习知识点', done: false }
])
const filter = ref('all') // all, active, completed
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(todo => !todo.done)
case 'completed':
return todos.value.filter(todo => todo.done)
default:
return todos.value
}
})
function addTodo(text) {
todos.value.push({
id: Date.now(),
text,
done: false
})
}
function removeTodo(id) {
const index = todos.value.findIndex(todo => todo.id === id)
todos.value.splice(index, 1)
}
function toggleTodo(id) {
const todo = todos.value.find(todo => todo.id === id)
if (todo) {
todo.done = !todo.done
}
}
</script>
<template>
<div>
<div>
<button @click="filter = 'all'">全部</button>
<button @click="filter = 'active'">未完成</button>
<button @click="filter = 'completed'">已完成</button>
</div>
<ul>
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ completed: todo.done }"
>
<input
type="checkbox"
:checked="todo.done"
@change="toggleTodo(todo.id)"
/>
<span>{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
<p v-if="filteredTodos.length === 0">暂无待办事项</p>
</div>
</template>
<style scoped>
.completed span {
text-decoration: line-through;
color: gray;
}
</style>
# 十一、TypeScript 支持
# 组件类型
<script setup lang="ts">
import { ref, computed } from 'vue'
// 基础类型
const count = ref<number>(0)
const message = ref<string>('Hello')
// 接口类型
interface User {
id: number
name: string
email: string
}
const user = ref<User>({
id: 1,
name: 'Alice',
email: 'alice@example.com'
})
// 数组类型
const users = ref<User[]>([])
// computed 类型推断
const doubleCount = computed(() => count.value * 2) // 自动推断为 number
// 函数类型
function handleClick(event: MouseEvent): void {
console.log(event.clientX, event.clientY)
}
// 异步函数
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
</script>
# Props 类型
<script setup lang="ts">
// 方式1:运行时声明
defineProps({
user: {
type: Object as PropType<User>,
required: true
},
count: {
type: Number,
default: 0
}
})
// 方式2:类型声明(推荐)
interface Props {
user: User
count?: number
}
const props = defineProps<Props>()
// 方式3:带默认值的类型声明
interface Props {
user: User
count?: number
message?: string
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
message: 'Hello'
})
</script>
# Emits 类型
<script setup lang="ts">
// 类型声明
const emit = defineEmits<{
update: [id: number]
delete: [id: number]
change: [value: string, checked: boolean]
}>()
emit('update', 123)
emit('delete', 456)
emit('change', 'test', true)
// 运行时声明
const emit = defineEmits({
update: (id: number) => {
return typeof id === 'number'
}
})
</script>
# Ref 类型
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// DOM ref 类型
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
inputRef.value?.focus()
})
// 组件 ref 类型
import MyComponent from './MyComponent.vue'
const componentRef = ref<InstanceType<typeof MyComponent> | null>(null)
function callComponentMethod() {
componentRef.value?.someMethod()
}
</script>
<template>
<input ref="inputRef" />
<MyComponent ref="componentRef" />
</template>
# 十二、性能优化
# v-once 和 v-memo
<template>
<!-- v-once:只渲染一次 -->
<div v-once>{{ expensiveComputation() }}</div>
<!-- v-memo:缓存子树,仅当依赖变化时更新 -->
<div v-memo="[user.id, user.name]">
<p>{{ user.id }}</p>
<p>{{ user.name }}</p>
<p>{{ user.email }}</p>
</div>
</template>
# 组件懒加载
// 路由懒加载
const routes = [
{
path: '/about',
component: () => import('./views/About.vue')
}
]
// 组件懒加载
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
// 带加载状态
const AsyncComponent = defineAsyncComponent({
loader: () => import('./components/HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 3000
})
# 虚拟滚动
对于大列表,使用虚拟滚动库(如 vue-virtual-scroller):
npm install vue-virtual-scroller
<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
const items = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i}`
})))
</script>
<template>
<RecycleScroller
:items="items"
:item-size="50"
key-field="id"
>
<template #default="{ item }">
<div class="item">{{ item.text }}</div>
</template>
</RecycleScroller>
</template>
# KeepAlive 缓存组件
<template>
<RouterView v-slot="{ Component }">
<!-- 缓存所有路由组件 -->
<KeepAlive>
<component :is="Component" />
</KeepAlive>
<!-- 只缓存特定组件 -->
<KeepAlive :include="['Home', 'About']">
<component :is="Component" />
</KeepAlive>
<!-- 排除特定组件 -->
<KeepAlive :exclude="['Admin']">
<component :is="Component" />
</KeepAlive>
<!-- 最多缓存 10 个组件 -->
<KeepAlive :max="10">
<component :is="Component" />
</KeepAlive>
</RouterView>
</template>
<!-- 组件内处理缓存 -->
<script setup>
import { onActivated, onDeactivated } from 'vue'
// 被缓存的组件激活时调用
onActivated(() => {
console.log('组件被激活')
})
// 被缓存的组件停用时调用
onDeactivated(() => {
console.log('组件被停用')
})
</script>
# 十三、调试技巧
# Vue Devtools
安装 Vue Devtools (opens new window) 浏览器扩展,可以:
- 查看组件树结构
- 检查组件的 props、data、computed
- 追踪事件
- 查看 Pinia store 状态
- 性能分析
# 调试 API
<script setup>
import { getCurrentInstance, onMounted } from 'vue'
// 获取当前组件实例(仅用于调试)
const instance = getCurrentInstance()
console.log(instance)
// 调试响应式数据
const count = ref(0)
console.log(count) // RefImpl 对象
// 在控制台访问 Vue 实例
onMounted(() => {
console.log('Component mounted')
// 在浏览器控制台输入 $vm0 可以访问选中的组件实例
})
</script>
# 常见错误
忘记
.valueconst count = ref(0) count++ // ❌ 错误 count.value++ // ✅ 正确解构失去响应性
const { count } = counterStore // ❌ 失去响应性 const { count } = storeToRefs(counterStore) // ✅ 保持响应性修改 props
// 子组件 const props = defineProps(['count']) props.count++ // ❌ 不应修改 props // 应该通过 emit 通知父组件 const emit = defineEmits(['update:count']) emit('update:count', props.count + 1)
# 十四、学习资源
# 官方文档
- Vue 3 官方文档 (opens new window)
- Vue 3 中文文档 (opens new window)
- Vue Router 文档 (opens new window)
- Pinia 文档 (opens new window)
# 推荐资源
- Vue Mastery (opens new window):视频教程
- Vue School (opens new window):在线课程
- Awesome Vue (opens new window):精选资源列表
- Vue.js Examples (opens new window):示例代码
# 社区
- Vue.js 官方论坛 (opens new window)
- Vue.js Discord (opens new window)
- Stack Overflow - Vue.js (opens new window)
# 开发工具
- Vite (opens new window):下一代前端构建工具
- Vue Devtools (opens new window):浏览器开发工具
- Volar (opens new window):VS Code 扩展
# 十五、下一步
恭喜你完成 Vue.js 快速入门!接下来可以:
深入学习 Vue 3
- 阅读官方文档的进阶指南
- 学习 Vue 3 的响应式原理
- 了解虚拟 DOM 和编译器
构建实际项目
- 从简单的 Todo 应用开始
- 逐步构建复杂的 SPA
- 参考开源项目学习最佳实践
学习生态系统
- Vue Router:路由管理
- Pinia:状态管理
- VueUse:组合式函数工具库
- Vite:构建工具
- Nuxt.js:全栈框架(SSR)
探索 UI 框架
- Element Plus
- Ant Design Vue
- Vuetify
- Naive UI
- PrimeVue
持续关注
- 阅读 Vue.js历代版本新特性 了解最新发展
- 关注 Vue 官方博客和 RFC
- 参与社区讨论
祝你变得更强!