轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JavaScript

  • TypeScript

  • Node.js

  • Vue.js

    • Vue.js快速入门
      • 一、Vue.js 简介
        • 什么是 Vue.js
        • 核心特性
        • Vue 3 vs Vue 2
        • 适用场景
      • 二、快速开始
        • 安装 Node.js
        • 创建 Vue 项目
        • 项目结构
        • 使用 CDN(快速体验)
      • 三、响应式基础
        • ref 和 reactive
        • computed 计算属性
        • watch 和 watchEffect
      • 四、模板语法
        • 文本插值
        • 指令
        • 动态参数
      • 五、组件基础
        • 定义组件
        • 使用组件
        • Props 传递
        • Emits 事件
        • v-model 双向绑定
        • 插槽(Slots)
      • 六、生命周期
      • 七、组合式 API 进阶
        • 组合式函数(Composables)
        • Provide / Inject
        • defineExpose
      • 八、路由(Vue Router)
        • 安装和配置
        • 使用路由
        • 路由守卫
      • 九、状态管理(Pinia)
        • 安装和配置
        • 定义 Store
        • 使用 Store
        • Store 之间的通信
      • 十、常见实战场景
        • HTTP 请求
        • 表单处理
        • 条件渲染与列表
      • 十一、TypeScript 支持
        • 组件类型
        • Props 类型
        • Emits 类型
        • Ref 类型
      • 十二、性能优化
        • v-once 和 v-memo
        • 组件懒加载
        • 虚拟滚动
        • KeepAlive 缓存组件
      • 十三、调试技巧
        • Vue Devtools
        • 调试 API
        • 常见错误
      • 十四、学习资源
        • 官方文档
        • 推荐资源
        • 社区
        • 开发工具
      • 十五、下一步
    • Vue.js历代版本新特性
    • Nuxt.js极简入门
  • 工程化

  • 浏览器与Web API

  • 前端
  • Vue.js
轩辕李
2025-05-11
目录

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极简入门

# 核心特性

  1. 渐进式框架:可以只用 Vue 的核心库,也可以结合路由、状态管理等构建完整应用
  2. 响应式数据绑定:数据变化自动更新视图
  3. 组件化开发:将 UI 拆分为可复用的独立组件
  4. 声明式渲染:用模板语法描述视图,框架负责 DOM 更新
  5. 虚拟 DOM:高效的 DOM 更新机制
  6. 优秀的性能: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 只是切换 display CSS 属性
  • 频繁切换用 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>

生命周期流程:

  1. onBeforeMount - 挂载前
  2. onMounted - 挂载后(可以访问 DOM)
  3. onBeforeUpdate - 数据变化,更新前
  4. onUpdated - 更新后
  5. onBeforeUnmount - 卸载前
  6. 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>

# 常见错误

  1. 忘记 .value

    const count = ref(0)
    count++ // ❌ 错误
    count.value++ // ✅ 正确
    
  2. 解构失去响应性

    const { count } = counterStore // ❌ 失去响应性
    const { count } = storeToRefs(counterStore) // ✅ 保持响应性
    
  3. 修改 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 快速入门!接下来可以:

  1. 深入学习 Vue 3

    • 阅读官方文档的进阶指南
    • 学习 Vue 3 的响应式原理
    • 了解虚拟 DOM 和编译器
  2. 构建实际项目

    • 从简单的 Todo 应用开始
    • 逐步构建复杂的 SPA
    • 参考开源项目学习最佳实践
  3. 学习生态系统

    • Vue Router:路由管理
    • Pinia:状态管理
    • VueUse:组合式函数工具库
    • Vite:构建工具
    • Nuxt.js:全栈框架(SSR)
  4. 探索 UI 框架

    • Element Plus
    • Ant Design Vue
    • Vuetify
    • Naive UI
    • PrimeVue
  5. 持续关注

    • 阅读 Vue.js历代版本新特性 了解最新发展
    • 关注 Vue 官方博客和 RFC
    • 参与社区讨论

祝你变得更强!

编辑 (opens new window)
#Vue.js#前端框架
上次更新: 2025/11/12
Node.js高级特性详解
Vue.js历代版本新特性

← Node.js高级特性详解 Vue.js历代版本新特性→

最近更新
01
AI编程时代的一些心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code 最佳实践(个人版)
08-01
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式