基础入门
# 01.基础快速入门
# 目录介绍
# 1.1 Vue快速介绍
# 1.1.1 Vue是什么
Vue.js(读音类似 view)是一款用于构建用户界面的渐进式 JavaScript 框架。它由尤雨溪(Evan You)于2014年创建,是目前最流行的前端框架之一。
"渐进式"意味着你可以根据需要逐步采用 Vue 的功能:可以只用它做一个页面的一小部分交互,也可以用它构建一个完整的单页应用(SPA)。
渐进式的含义:
小型项目 → 只用Vue核心(模板+响应式)
中型项目 → 加上Vue Router(路由管理)
大型项目 → 再加Pinia(状态管理)+ 工程化工具链
2
3
4
# 1.1.2 Vue的特点
- 渐进式框架:核心库只关注视图层,可以按需引入生态工具(Router、Pinia等),不强制一步到位。
- 响应式数据绑定:数据变化时视图自动更新,无需手动操作 DOM。
- 组件化开发:将页面拆分为独立、可复用的组件,每个组件包含自己的模板、逻辑和样式。
- 虚拟DOM:通过虚拟DOM进行高效的差异比较和最小化更新,提升渲染性能。
- 单文件组件(SFC):
.vue文件将 HTML、JavaScript、CSS 写在一起,开发体验好。 - 丰富的生态:Vue Router、Pinia、Vite、Nuxt 等生态工具齐全。
# 1.1.3 Vue的版本
| 版本 | 发布时间 | 关键特性 |
|---|---|---|
| Vue 1.x | 2015年 | 首个正式版本,基础的响应式系统 |
| Vue 2.x | 2016年 | 虚拟DOM、组件化、Options API,广泛普及 |
| Vue 3.x | 2020年 | Composition API、Proxy响应式、TypeScript支持、性能提升 |
Vue 3 是当前主流版本,本教程以 Vue 3 为基础。Vue 3 相较 Vue 2 有以下重大变化:
- 响应式系统从
Object.defineProperty升级为Proxy - 新增 Composition API(组合式API),更灵活地组织逻辑
- 更好的 TypeScript 支持
- 更小的打包体积和更快的渲染性能
# 1.1.4 Vue的生态
Vue 拥有完善的官方生态:
- Vue Router:官方路由管理器,支持单页应用的页面导航
- Pinia:新一代状态管理库(替代 Vuex)
- Vite:下一代前端构建工具,极速冷启动
- Nuxt:基于 Vue 的全栈框架,支持 SSR/SSG
- VueUse:实用组合式函数工具集
- Vue Devtools:浏览器开发者工具扩展
# 1.1.5 Vue的应用领域
- 单页应用(SPA):后台管理系统、Dashboard、内容管理平台
- 移动端应用:配合 uni-app、Ionic 开发跨平台 App
- 服务端渲染(SSR):配合 Nuxt 开发 SEO 友好的网站
- 桌面应用:配合 Electron 开发桌面端应用
- 小程序:通过 uni-app 开发微信/支付宝小程序
- 低代码平台:很多低代码平台基于 Vue 构建
# 1.1.6 综合案例与思考
综合案例:感受Vue的声明式渲染
<!DOCTYPE html>
<html>
<head>
<title>Vue 初体验</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<h1>{{ title }}</h1>
<p>计数器:{{ count }}</p>
<button @click="count++">点击 +1</button>
<button @click="count = 0">重置</button>
<p v-if="count > 10">你已经点了超过10次了!</p>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const title = ref('Hello Vue 3!')
const count = ref(0)
return { title, count }
}
}).mount('#app')
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
案例知识融合:这个案例展示了Vue的核心特性——声明式渲染(插值)、响应式数据(ref)、事件处理(@click)和条件渲染(v-if)。你只需要关注数据的变化,Vue会自动帮你更新DOM,这就是"数据驱动视图"的核心思想。
思考题:
- Vue的"渐进式"和React、Angular有什么不同?为什么说Vue的上手成本更低?
- 为什么Vue 3要用Proxy替换Object.defineProperty作为响应式的底层实现?
- 声明式渲染和命令式渲染(直接操作DOM)相比,各有什么优缺点?
# 1.2 环境搭建
# 1.2.1 安装Node.js
Vue 3 的开发需要 Node.js 环境(建议 v18+)。
下载安装:访问 Node.js 官网 (opens new window) 下载 LTS 版本。
验证安装:
# 查看 Node.js 版本
node -v
# 输出示例: v20.11.0
# 查看 npm 版本
npm -v
# 输出示例: 10.2.4
2
3
4
5
6
7
推荐使用 nvm 管理 Node 版本:
# macOS/Linux 安装 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# 安装指定版本
nvm install 20
nvm use 20
# 查看已安装版本
nvm ls
2
3
4
5
6
7
8
9
# 1.2.2 使用Vite创建项目
Vite 是 Vue 官方推荐的构建工具,启动速度极快:
# 使用 npm 创建项目
npm create vue@latest
# 按提示选择配置
# ✔ Project name: my-vue-app
# ✔ Add TypeScript? Yes
# ✔ Add JSX Support? No
# ✔ Add Vue Router? Yes
# ✔ Add Pinia? Yes
# ✔ Add Vitest for Unit Testing? No
# ✔ Add ESLint for code quality? Yes
# 进入项目并安装依赖
cd my-vue-app
npm install
# 启动开发服务器
npm run dev
# 输出: Local: http://localhost:5173/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1.2.3 项目目录结构
使用 Vite 创建的 Vue 项目结构如下:
my-vue-app/
├── public/ # 静态资源(不经过构建处理)
│ └── favicon.ico
├── src/ # 源代码目录
│ ├── assets/ # 静态资源(经过构建处理)
│ ├── components/ # 公共组件
│ ├── router/ # 路由配置
│ │ └── index.ts
│ ├── stores/ # Pinia 状态管理
│ ├── views/ # 页面组件
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件
├── index.html # HTML 入口
├── package.json # 项目配置和依赖
├── tsconfig.json # TypeScript 配置
└── vite.config.ts # Vite 配置
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
核心文件说明:
index.html:应用的 HTML 入口,Vite 以此为入口解析依赖src/main.ts:JavaScript 入口,创建 Vue 应用实例src/App.vue:根组件,所有页面组件的父组件vite.config.ts:Vite 构建配置,可配置插件、代理、别名等
# 1.2.4 开发工具推荐
- VS Code:最流行的前端编辑器
- 必装插件:Vue - Official(原Volar),提供语法高亮、智能提示、类型检查
- 推荐插件:ESLint、Prettier
- Vue Devtools:浏览器扩展,可查看组件树、状态、路由、Pinia等
- Chrome/Edge:在扩展商店搜索 "Vue.js devtools" 安装
- WebStorm:JetBrains 出品的前端 IDE,原生支持 Vue
# 1.2.5 综合案例与思考
综合案例:从零搭建并理解项目入口
// src/main.ts —— 应用入口文件
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
// 1. 创建 Vue 应用实例
const app = createApp(App)
// 2. 注册插件
app.use(createPinia()) // 状态管理
app.use(router) // 路由管理
// 3. 挂载到 DOM
app.mount('#app')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
然后看一下 App.vue 这个根组件
<!-- src/App.vue —— 根组件 -->
<template>
<header>
<nav>
<RouterLink to="/">首页</RouterLink>
<RouterLink to="/about">关于</RouterLink>
</nav>
</header>
<main>
<RouterView />
</main>
</template>
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>
<style scoped>
nav a {
margin-right: 16px;
color: #42b883;
text-decoration: none;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
案例知识融合:这个案例展示了一个标准 Vue 3 项目的完整入口流程:main.ts 中通过 createApp 创建应用、注册插件、挂载到 DOM;App.vue 作为根组件使用了路由的 RouterLink 和 RouterView。理解这个流程是掌握 Vue 项目的基础。
思考题:
app.mount('#app')中的#app对应的是哪个文件中的元素?整个挂载流程是怎样的?- Vite 相比 Webpack 为什么启动速度快那么多?它的核心原理是什么?
- 为什么推荐在创建项目时就开启 TypeScript?它能带来什么好处?
# 1.3 HelloWorld
# 1.3.1 第一个Vue应用
创建一个最简单的 Vue 应用:
<!-- src/App.vue -->
<template>
<div>
<h1>{{ message }}</h1>
<p>当前时间:{{ currentTime }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const message = ref('Hello Vue 3!')
const currentTime = ref(new Date().toLocaleString())
// 每秒更新时间
setInterval(() => {
currentTime.value = new Date().toLocaleString()
}, 1000)
</script>
<style scoped>
h1 {
color: #42b883;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 1.3.2 代码解读
一个 .vue 文件(单文件组件,SFC)由三部分组成:
┌─────────────────────────────────┐
│ <template> │ ← HTML模板:定义页面结构
│ <h1>{{ message }}</h1>│
│ </template> │
├─────────────────────────────────┤
│ <script setup lang="ts"> │ ← JavaScript逻辑:定义数据和行为
│ const message = ref('Hello') │
│ </script> │
├─────────────────────────────────┤
│ <style scoped> │ ← CSS样式:定义组件样式
│ h1 { color: #42b883; } │
│ </style> │
└─────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
<template>:组件的 HTML 结构,支持 Vue 模板语法<script setup>:组件的逻辑部分,setup是 Vue 3 的语法糖,自动暴露变量到模板<style scoped>:组件的样式,scoped表示样式仅作用于当前组件
# 1.3.3 挂载应用
Vue 应用的启动流程:
index.html main.ts App.vue
┌──────────┐ ┌──────────┐ ┌──────────┐
│<div │ ← mount ← │createApp │ ← 根组件 ← │<template> │
│ id="app" │ │ (App) │ │ ... │
│</div> │ │.mount() │ │</template>│
└──────────┘ └──────────┘ └──────────┘
2
3
4
5
6
- 浏览器加载
index.html,其中有<div id="app"></div> main.ts执行createApp(App)创建应用实例.mount('#app')将 App 组件渲染到#app元素中- Vue 接管该 DOM 节点,后续所有更新由 Vue 的响应式系统驱动
# 1.3.4 综合案例与思考
综合案例:交互式 HelloWorld
<template>
<div class="hello">
<h1 :style="{ color: textColor }">{{ greeting }}</h1>
<input v-model="name" placeholder="输入你的名字" />
<select v-model="textColor">
<option value="#42b883">Vue绿</option>
<option value="#3178c6">TS蓝</option>
<option value="#e44d26">HTML红</option>
</select>
<p>你已经在这个页面停留了 {{ seconds }} 秒</p>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
const name = ref('World')
const textColor = ref('#42b883')
const seconds = ref(0)
// 计算属性:动态生成问候语
const greeting = computed(() => `Hello, ${name.value}!`)
// 生命周期:组件挂载后启动计时器
let timer: number
onMounted(() => {
timer = setInterval(() => seconds.value++, 1000)
})
onUnmounted(() => {
clearInterval(timer)
})
</script>
<style scoped>
.hello {
text-align: center;
padding: 40px;
}
input, select {
margin: 8px;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
案例知识融合:这个案例综合了模板语法()、属性绑定(:style)、双向绑定(v-model)、计算属性(computed)和生命周期钩子(onMounted/onUnmounted)。通过输入名字、切换颜色、观察计时器,可以直观感受 Vue 的响应式特性。
思考题:
ref和直接声明变量let name = 'World'有什么区别?为什么必须用ref?<script setup>和普通<script>的区别是什么?它省略了哪些步骤?- 为什么在
onUnmounted中要清除定时器?不清除会有什么问题?
# 1.4 模板语法
# 1.4.1 插值表达式
双大括号 是最基础的模板语法,用于将数据渲染到页面:
<template>
<p>消息:{{ message }}</p>
<p>数字计算:{{ 1 + 1 }}</p>
<p>调用方法:{{ message.toUpperCase() }}</p>
<p>三元表达式:{{ isActive ? '激活' : '未激活' }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const message = ref('Hello Vue')
const isActive = ref(true)
</script>
2
3
4
5
6
7
8
9
10
11
12
注意:插值表达式只能包含单个表达式,不能写语句:
<!-- 正确:表达式 -->
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<!-- 错误:语句 -->
{{ var a = 1 }} <!-- 声明语句 -->
{{ if (ok) return msg }} <!-- 流程控制 -->
2
3
4
5
6
7
8
# 1.4.2 原始HTML
v-html 指令用于渲染原始 HTML(注意XSS风险):
<template>
<!-- 插值会转义HTML标签 -->
<p>{{ rawHtml }}</p>
<!-- 输出: <span style="color:red">红色文字</span> -->
<!-- v-html 会解析HTML -->
<p v-html="rawHtml"></p>
<!-- 输出: 红色文字(红色显示) -->
</template>
<script setup lang="ts">
import { ref } from 'vue'
const rawHtml = ref('<span style="color:red">红色文字</span>')
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
安全警告:在网站上动态渲染任意 HTML 是非常危险的,容易导致 XSS 攻击。只在可信内容上使用
v-html,绝不要用在用户提交的内容上。
# 1.4.3 属性绑定
使用 v-bind(简写 :)动态绑定 HTML 属性:
<template>
<!-- 完整写法 -->
<img v-bind:src="imageUrl" />
<!-- 简写(推荐) -->
<img :src="imageUrl" />
<a :href="link" :title="linkTitle">点击跳转</a>
<!-- 动态绑定class -->
<div :class="{ active: isActive, disabled: isDisabled }">
条件class
</div>
<!-- 动态绑定style -->
<p :style="{ color: textColor, fontSize: fontSize + 'px' }">
动态样式
</p>
<!-- 绑定多个属性(Vue 3) -->
<input v-bind="inputAttrs" />
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
const imageUrl = ref('/logo.png')
const link = ref('https://vuejs.org')
const linkTitle = ref('Vue官网')
const isActive = ref(true)
const isDisabled = ref(false)
const textColor = ref('#42b883')
const fontSize = ref(16)
// 一次绑定多个属性
const inputAttrs = reactive({
type: 'text',
placeholder: '请输入',
maxlength: 20
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 1.4.4 JavaScript表达式
模板中可以使用完整的 JavaScript 表达式:
<template>
<p>{{ number + 1 }}</p>
<p>{{ ok ? 'YES' : 'NO' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
<p>{{ ['Vue', 'React', 'Angular'].join(' | ') }}</p>
<p>{{ new Date().getFullYear() }}</p>
<!-- 在指令中使用表达式 -->
<div :id="'item-' + id"></div>
<p :class="isActive ? 'active' : 'inactive'">状态文本</p>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const number = ref(10)
const ok = ref(true)
const message = ref('Hello')
const id = ref(42)
const isActive = ref(true)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
可以访问的全局对象:模板表达式中可以访问有限的全局对象列表,如 Math、Date、parseInt 等。不能访问用户自定义的全局变量。
# 1.4.5 综合案例与思考
综合案例:个人名片卡
<template>
<div class="card" :style="{ borderColor: themeColor }">
<img :src="avatar" :alt="name" class="avatar" />
<h2 :style="{ color: themeColor }">{{ name }}</h2>
<p class="title">{{ title }}</p>
<p class="bio" v-html="bioHtml"></p>
<div class="stats">
<span>文章:{{ articles }}</span>
<span>粉丝:{{ fans > 1000 ? (fans / 1000).toFixed(1) + 'k' : fans }}</span>
<span>获赞:{{ likes.toLocaleString() }}</span>
</div>
<p class="joined">加入于 {{ new Date(joinDate).toLocaleDateString('zh-CN') }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const name = ref('张三')
const title = ref('前端工程师')
const avatar = ref('https://api.dicebear.com/7.x/avataaars/svg?seed=vue')
const bioHtml = ref('热爱 <strong>Vue.js</strong> 和 <em>TypeScript</em>')
const themeColor = ref('#42b883')
const articles = ref(128)
const fans = ref(5600)
const likes = ref(23456)
const joinDate = ref('2020-03-15')
</script>
<style scoped>
.card {
max-width: 320px;
padding: 24px;
border: 2px solid;
border-radius: 12px;
text-align: center;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
}
.stats {
display: flex;
justify-content: space-around;
margin: 16px 0;
font-size: 14px;
color: #666;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
案例知识融合:这个名片卡案例综合运用了插值表达式()、属性绑定(:src、:style)、原始HTML渲染(v-html)和模板中的 JavaScript 表达式(三元判断、数字格式化、日期格式化),展示了模板语法在真实UI场景中的应用。
思考题:
v-html和的根本区别是什么?在什么场景下必须用v-html?:class的对象语法和数组语法有什么区别?各适用于什么场景?- 为什么模板中只能使用表达式而不能使用语句?这个设计有什么好处?
# 1.5 常用指令
# 1.5.1 v-if条件渲染
v-if 根据条件决定是否渲染元素(条件为假时,元素不会存在于DOM中):
<template>
<div>
<h2>用户状态</h2>
<p v-if="userType === 'admin'">欢迎管理员!你拥有所有权限。</p>
<p v-else-if="userType === 'vip'">欢迎VIP用户!享受专属特权。</p>
<p v-else>欢迎普通用户!升级VIP解锁更多功能。</p>
<select v-model="userType">
<option value="admin">管理员</option>
<option value="vip">VIP用户</option>
<option value="normal">普通用户</option>
</select>
<!-- 在 template 上使用 v-if(不会渲染额外DOM元素) -->
<template v-if="showDetails">
<h3>详细信息</h3>
<p>这是一段详细内容...</p>
<p>可以包含多个元素</p>
</template>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const userType = ref('normal')
const showDetails = ref(true)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 1.5.2 v-show显示隐藏
v-show 通过 CSS display 属性控制显示隐藏(元素始终存在于DOM中):
<template>
<button @click="isVisible = !isVisible">切换显示</button>
<p v-show="isVisible">这段文字可以切换显示/隐藏</p>
<!-- 渲染结果:<p style="display: none;">...</p> -->
</template>
<script setup lang="ts">
import { ref } from 'vue'
const isVisible = ref(true)
</script>
2
3
4
5
6
7
8
9
10
# 1.5.3 v-for列表渲染
列表渲染v-for 用于循环渲染列表数据:
<template>
<!-- 遍历数组 -->
<ul>
<li v-for="(item, index) in fruits" :key="item.id">
{{ index + 1 }}. {{ item.name }} - ¥{{ item.price }}
</li>
</ul>
<!-- 遍历对象 -->
<div v-for="(value, key, index) in userInfo" :key="key">
{{ index }}: {{ key }} = {{ value }}
</div>
<!-- 遍历数字范围 -->
<span v-for="n in 5" :key="n">{{ n }} </span>
<!-- 输出: 1 2 3 4 5 -->
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
const fruits = ref([
{ id: 1, name: '苹果', price: 5.5 },
{ id: 2, name: '香蕉', price: 3.2 },
{ id: 3, name: '橙子', price: 4.8 }
])
const userInfo = reactive({
name: '张三',
age: 28,
city: '深圳'
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
重要:
v-for必须提供:key属性,使用唯一标识(如id),不推荐使用 index 作为 key。key 帮助 Vue 高效追踪每个节点的身份,在列表变化时做最小化的DOM操作。
# 1.5.4 v-model双向绑定
v-model 实现表单元素与数据的双向绑定:
<template>
<div>
<!-- 文本输入 -->
<input v-model="text" placeholder="输入文本" />
<p>输入内容:{{ text }}</p>
<!-- 多行文本 -->
<textarea v-model="content" rows="3"></textarea>
<!-- 复选框(单个:布尔值) -->
<label>
<input type="checkbox" v-model="isAgree" /> 同意协议
</label>
<!-- 复选框(多个:数组) -->
<label v-for="lang in allLanguages" :key="lang">
<input type="checkbox" v-model="selectedLangs" :value="lang" />
{{ lang }}
</label>
<p>已选:{{ selectedLangs.join(', ') }}</p>
<!-- 单选按钮 -->
<label v-for="g in ['男', '女']" :key="g">
<input type="radio" v-model="gender" :value="g" /> {{ g }}
</label>
<!-- 下拉选择 -->
<select v-model="city">
<option disabled value="">请选择城市</option>
<option>北京</option>
<option>上海</option>
<option>深圳</option>
</select>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const text = ref('')
const content = ref('')
const isAgree = ref(false)
const allLanguages = ['Vue', 'React', 'Angular', 'Svelte']
const selectedLangs = ref<string[]>([])
const gender = ref('男')
const city = ref('')
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
v-model修饰符:
<!-- .lazy:在change事件后同步(而非input事件) -->
<input v-model.lazy="msg" />
<!-- .number:自动转为数字类型 -->
<input v-model.number="age" type="text" />
<!-- .trim:自动去除首尾空格 -->
<input v-model.trim="name" />
2
3
4
5
6
7
8
# 1.5.5 v-bind和v-on
v-bind(:)绑定属性,v-on(@)绑定事件:
<template>
<!-- v-bind 绑定属性 -->
<img :src="imgSrc" :alt="imgAlt" />
<a :href="url" :target="openNew ? '_blank' : '_self'">链接</a>
<button :disabled="isLoading">{{ isLoading ? '加载中...' : '提交' }}</button>
<!-- v-on 绑定事件 -->
<button @click="handleClick">点击</button>
<input @input="handleInput" @keyup.enter="handleSubmit" />
<div @mouseover="onHover" @mouseleave="onLeave">悬停区域</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const imgSrc = ref('/logo.png')
const imgAlt = ref('Logo')
const url = ref('https://vuejs.org')
const openNew = ref(true)
const isLoading = ref(false)
const handleClick = () => {
isLoading.value = true
setTimeout(() => { isLoading.value = false }, 2000)
}
const handleInput = (e: Event) => {
console.log((e.target as HTMLInputElement).value)
}
const handleSubmit = () => console.log('提交!')
const onHover = () => console.log('鼠标进入')
const onLeave = () => console.log('鼠标离开')
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 1.5.6 v-if和v-show区别
| 对比项 | v-if | v-show |
|---|---|---|
| 渲染方式 | 条件为假时不渲染DOM | 始终渲染,通过CSS display:none 隐藏 |
| 切换开销 | 高(需创建/销毁DOM) | 低(只切换CSS属性) |
| 初始开销 | 低(条件为假不渲染) | 高(无论如何都渲染) |
| 适用场景 | 条件很少变化 | 需要频繁切换 |
| 支持template | 是 | 否 |
| 支持v-else | 是 | 否 |
选择建议:频繁切换用 v-show,运行时条件不太变化用 v-if。
# 1.5.7 综合案例与思考
综合案例:待办事项列表
<template>
<div class="todo-app">
<h2>待办事项</h2>
<!-- 输入新任务 -->
<div class="input-row">
<input
v-model.trim="newTodo"
@keyup.enter="addTodo"
placeholder="输入待办事项,回车添加"
/>
<button @click="addTodo" :disabled="!newTodo">添加</button>
</div>
<!-- 筛选 -->
<div class="filter">
<button
v-for="f in filters"
:key="f.value"
:class="{ active: filter === f.value }"
@click="filter = f.value"
>
{{ f.label }}
</button>
</div>
<!-- 列表 -->
<ul>
<li v-for="todo in filteredTodos" :key="todo.id">
<input type="checkbox" v-model="todo.done" />
<span :class="{ done: todo.done }">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
<p v-show="todos.length > 0">
共 {{ todos.length }} 项,已完成 {{ doneCount }} 项
</p>
<p v-if="todos.length === 0" class="empty">暂无待办事项</p>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Todo {
id: number
text: string
done: boolean
}
const newTodo = ref('')
const filter = ref('all')
const todos = ref<Todo[]>([
{ id: 1, text: '学习Vue基础语法', done: true },
{ id: 2, text: '练习组件开发', done: false },
{ id: 3, text: '掌握Vue Router', done: false }
])
const filters = [
{ label: '全部', value: 'all' },
{ label: '未完成', value: 'active' },
{ label: '已完成', value: 'done' }
]
let nextId = 4
const filteredTodos = computed(() => {
if (filter.value === 'active') return todos.value.filter(t => !t.done)
if (filter.value === 'done') return todos.value.filter(t => t.done)
return todos.value
})
const doneCount = computed(() => todos.value.filter(t => t.done).length)
const addTodo = () => {
if (!newTodo.value) return
todos.value.push({ id: nextId++, text: newTodo.value, done: false })
newTodo.value = ''
}
const removeTodo = (id: number) => {
todos.value = todos.value.filter(t => t.id !== id)
}
</script>
<style scoped>
.todo-app { max-width: 480px; margin: 0 auto; }
.input-row { display: flex; gap: 8px; margin-bottom: 16px; }
.input-row input { flex: 1; padding: 8px; }
.filter { margin-bottom: 12px; }
.filter button { margin-right: 8px; padding: 4px 12px; }
.filter button.active { background: #42b883; color: white; }
.done { text-decoration: line-through; color: #999; }
.empty { color: #999; text-align: center; }
li { list-style: none; padding: 8px 0; display: flex; align-items: center; gap: 8px; }
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
案例知识融合:这个待办列表综合运用了 v-model(输入绑定、复选框绑定)、v-for(列表渲染)、v-if/v-show(条件显示)、:class(动态样式)、@click/@keyup.enter(事件处理)、computed(计算属性过滤),是 Vue 指令系统的完整实战。
思考题:
- 为什么
v-for的 key 要用todo.id而不是 index?如果用 index 会出什么问题? v-model在<input>上本质上是哪两个操作的语法糖?- 这个案例中
v-show和v-if分别用在了什么场景?能否互换?为什么?
# 1.6 事件处理
# 1.6.1 监听事件
使用 v-on(简写 @)监听 DOM 事件:
<template>
<!-- 内联处理 -->
<button @click="count++">计数:{{ count }}</button>
<!-- 方法处理 -->
<button @click="increment">+1</button>
<!-- 传参 -->
<button @click="addCount(5)">+5</button>
<!-- 同时传参和传事件对象 -->
<button @click="handleClick(10, $event)">点击</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const increment = () => { count.value++ }
const addCount = (n: number) => { count.value += n }
const handleClick = (n: number, event: MouseEvent) => {
count.value += n
console.log('点击坐标:', event.clientX, event.clientY)
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 1.6.2 事件方法
将事件处理逻辑抽取为方法,保持模板简洁:
<template>
<form @submit.prevent="handleSubmit">
<input v-model="form.name" placeholder="姓名" />
<input v-model="form.email" type="email" placeholder="邮箱" />
<button type="submit">提交</button>
</form>
<p v-if="submitted">提交成功!{{ form.name }} ({{ form.email }})</p>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
const form = reactive({ name: '', email: '' })
const submitted = ref(false)
const handleSubmit = () => {
if (!form.name || !form.email) {
alert('请填写完整信息')
return
}
submitted.value = true
console.log('提交数据:', { ...form })
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 1.6.3 事件修饰符
Vue 提供事件修饰符简化常见的事件处理逻辑:
<template>
<!-- .prevent:阻止默认行为(如表单提交刷新页面) -->
<form @submit.prevent="onSubmit">...</form>
<!-- .stop:阻止事件冒泡 -->
<div @click="outerClick">
<button @click.stop="innerClick">不冒泡</button>
</div>
<!-- .once:只触发一次 -->
<button @click.once="doOnce">只能点一次</button>
<!-- .self:仅当事件在该元素自身触发时(非子元素) -->
<div @click.self="onDivClick">
<button>点我不触发div的click</button>
</div>
<!-- .capture:使用捕获模式 -->
<div @click.capture="onCapture">...</div>
<!-- .passive:提升滚动性能 -->
<div @scroll.passive="onScroll">...</div>
<!-- 修饰符可以链式调用 -->
<a @click.stop.prevent="doSomething">链接</a>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 1.6.4 按键修饰符
监听键盘事件时使用按键修饰符:
<template>
<!-- 常用按键别名 -->
<input @keyup.enter="submit" placeholder="回车提交" />
<input @keyup.esc="cancel" placeholder="ESC取消" />
<input @keyup.tab="onTab" />
<input @keyup.delete="onDelete" />
<input @keyup.space="onSpace" />
<!-- 方向键 -->
<div @keyup.up="moveUp" @keyup.down="moveDown"
@keyup.left="moveLeft" @keyup.right="moveRight"
tabindex="0">方向键控制区</div>
<!-- 系统修饰键组合 -->
<input @keyup.ctrl.enter="ctrlEnter" placeholder="Ctrl+Enter" />
<div @click.ctrl="ctrlClick">Ctrl+点击</div>
<div @click.alt="altClick">Alt+点击</div>
<div @click.meta="metaClick">Cmd/Win+点击</div>
<!-- .exact:精确匹配修饰键 -->
<button @click.ctrl.exact="onCtrlOnly">仅Ctrl+点击</button>
</template>
<script setup lang="ts">
const submit = () => console.log('提交')
const cancel = () => console.log('取消')
const onTab = () => console.log('Tab')
const onDelete = () => console.log('Delete')
const onSpace = () => console.log('Space')
const moveUp = () => console.log('上')
const moveDown = () => console.log('下')
const moveLeft = () => console.log('左')
const moveRight = () => console.log('右')
const ctrlEnter = () => console.log('Ctrl+Enter')
const ctrlClick = () => console.log('Ctrl+点击')
const altClick = () => console.log('Alt+点击')
const metaClick = () => console.log('Meta+点击')
const onCtrlOnly = () => console.log('仅Ctrl+点击')
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 1.6.5 综合案例与思考
综合案例:键盘快捷键控制面板
<template>
<div class="panel" tabindex="0" @keyup="handleKey">
<h3>快捷键面板</h3>
<div class="box" :style="boxStyle">{{ label }}</div>
<div class="log">
<p>位置: ({{ x }}, {{ y }})</p>
<p>大小: {{ size }}px</p>
<p>最近按键: {{ lastKey }}</p>
</div>
<ul class="tips">
<li>方向键:移动方块</li>
<li>+/-:放大/缩小</li>
<li>R:重置</li>
<li>点击方块:改变颜色</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const x = ref(100)
const y = ref(100)
const size = ref(60)
const color = ref('#42b883')
const label = ref('Vue')
const lastKey = ref('-')
const colors = ['#42b883', '#3178c6', '#e44d26', '#f7df1e', '#764abc']
let colorIndex = 0
const boxStyle = computed(() => ({
left: x.value + 'px',
top: y.value + 'px',
width: size.value + 'px',
height: size.value + 'px',
backgroundColor: color.value,
lineHeight: size.value + 'px'
}))
const handleKey = (e: KeyboardEvent) => {
lastKey.value = e.key
const step = e.shiftKey ? 20 : 5
switch (e.key) {
case 'ArrowUp': y.value = Math.max(0, y.value - step); break
case 'ArrowDown': y.value += step; break
case 'ArrowLeft': x.value = Math.max(0, x.value - step); break
case 'ArrowRight': x.value += step; break
case '+': case '=': size.value = Math.min(200, size.value + 10); break
case '-': size.value = Math.max(20, size.value - 10); break
case 'r': case 'R':
x.value = 100; y.value = 100; size.value = 60; color.value = '#42b883'
break
}
}
const changeColor = () => {
colorIndex = (colorIndex + 1) % colors.length
color.value = colors[colorIndex]
}
</script>
<style scoped>
.panel { position: relative; height: 400px; border: 1px solid #ddd; outline: none; }
.box {
position: absolute;
border-radius: 8px;
color: white;
font-weight: bold;
text-align: center;
cursor: pointer;
transition: all 0.15s;
}
.log { position: absolute; bottom: 8px; left: 8px; font-size: 13px; }
.tips { position: absolute; bottom: 8px; right: 8px; font-size: 12px; color: #999; }
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
案例知识融合:这个快捷键面板综合了键盘事件监听、事件对象(e.key、e.shiftKey)、计算属性动态生成样式、以及交互反馈。通过一个可玩的交互案例,把事件处理的各种技巧串联在一起。
思考题:
.prevent和.stop分别在什么场景下必须使用?不使用会怎样?- 为什么 Vue 要在模板中处理事件,而不是像 jQuery 那样在 JS 中手动绑定?这种设计有什么优势?
.passive修饰符在什么场景下能显著提升性能?为什么?
# 1.7 样式绑定
# 1.7.1 class绑定
动态绑定 CSS class 的多种方式:
<template>
<!-- 对象语法:key 是类名,value 是布尔值 -->
<div :class="{ active: isActive, 'text-bold': isBold }">
对象语法
</div>
<!-- 绑定一个计算属性对象 -->
<div :class="classObject">计算属性对象</div>
<!-- 和普通class共存 -->
<div class="base" :class="{ active: isActive }">
静态 + 动态 class
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const isActive = ref(true)
const isBold = ref(false)
const classObject = computed(() => ({
active: isActive.value,
'text-bold': isBold.value,
'text-large': true
}))
</script>
<style>
.active { color: #42b883; }
.text-bold { font-weight: bold; }
.text-large { font-size: 20px; }
.base { padding: 8px; }
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 1.7.2 style绑定
动态绑定内联样式:
<template>
<!-- 对象语法(推荐驼峰命名) -->
<div :style="{ color: textColor, fontSize: fontSize + 'px' }">
内联样式
</div>
<!-- 绑定样式对象 -->
<div :style="styleObject">样式对象</div>
<!-- 自动添加浏览器前缀 -->
<div :style="{ display: ['-webkit-flex', 'flex'] }">
自动前缀
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
const textColor = ref('#333')
const fontSize = ref(16)
const styleObject = reactive({
color: '#42b883',
fontSize: '18px',
fontWeight: 'bold',
padding: '12px',
backgroundColor: '#f0f0f0',
borderRadius: '8px'
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 1.7.3 数组语法
使用数组语法组合多个 class 或 style:
<template>
<!-- class 数组语法 -->
<div :class="[baseClass, sizeClass]">数组语法</div>
<!-- 数组 + 条件 -->
<div :class="[baseClass, isActive ? 'active' : '']">条件数组</div>
<!-- 数组中嵌套对象 -->
<div :class="[baseClass, { active: isActive, disabled: isDisabled }]">
混合语法
</div>
<!-- style 数组语法(合并多个样式对象) -->
<div :style="[baseStyle, overrideStyle]">多样式合并</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
const baseClass = ref('box')
const sizeClass = ref('box-large')
const isActive = ref(true)
const isDisabled = ref(false)
const baseStyle = reactive({ color: '#333', fontSize: '14px' })
const overrideStyle = reactive({ color: '#42b883', fontWeight: 'bold' })
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 1.7.4 scoped样式
<style scoped> 使样式仅作用于当前组件,避免全局污染:
<template>
<div class="container">
<h2 class="title">Scoped 样式示例</h2>
<p class="desc">这个样式不会影响其他组件</p>
</div>
</template>
<style scoped>
/* scoped 会给选择器自动加上属性选择器,如 .title[data-v-7a7a37b1] */
.container {
padding: 20px;
border: 1px solid #eee;
}
.title {
color: #42b883;
}
.desc {
color: #666;
}
/* 深度选择器:穿透 scoped 影响子组件 */
:deep(.child-class) {
color: red;
}
/* 插槽内容选择器 */
:slotted(.slot-class) {
font-weight: bold;
}
/* 全局选择器 */
:global(.global-class) {
color: blue;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
scoped原理:Vue 会为组件的每个元素添加一个唯一的 data-v-xxx 属性,然后将 CSS 选择器编译为 .title[data-v-xxx],从而实现样式隔离。
# 1.7.5 综合案例与思考
综合案例:主题切换器
<template>
<div :class="['app', themeClass]" :style="customStyle">
<h2>主题切换器</h2>
<div class="theme-picker">
<button
v-for="theme in themes"
:key="theme.name"
:class="['theme-btn', { active: currentTheme === theme.name }]"
:style="{ backgroundColor: theme.primary }"
@click="currentTheme = theme.name"
>
{{ theme.label }}
</button>
</div>
<div class="controls">
<label>字体大小: {{ fontSize }}px</label>
<input type="range" v-model.number="fontSize" min="12" max="24" />
<label>圆角: {{ borderRadius }}px</label>
<input type="range" v-model.number="borderRadius" min="0" max="20" />
</div>
<div class="preview-card" :style="cardStyle">
<h3>预览卡片</h3>
<p>这是一个动态样式的预览卡片,所有样式都是响应式绑定的。</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const themes = [
{ name: 'light', label: '浅色', primary: '#42b883', bg: '#ffffff', text: '#333' },
{ name: 'dark', label: '深色', primary: '#42b883', bg: '#1a1a2e', text: '#eee' },
{ name: 'blue', label: '蓝色', primary: '#3178c6', bg: '#f0f4ff', text: '#333' }
]
const currentTheme = ref('light')
const fontSize = ref(16)
const borderRadius = ref(8)
const themeClass = computed(() => `theme-${currentTheme.value}`)
const currentThemeData = computed(() =>
themes.find(t => t.name === currentTheme.value)!
)
const customStyle = computed(() => ({
fontSize: fontSize.value + 'px',
backgroundColor: currentThemeData.value.bg,
color: currentThemeData.value.text,
transition: 'all 0.3s ease'
}))
const cardStyle = computed(() => ({
borderRadius: borderRadius.value + 'px',
borderColor: currentThemeData.value.primary,
padding: '20px',
border: '2px solid',
marginTop: '16px'
}))
</script>
<style scoped>
.app { padding: 24px; min-height: 300px; }
.theme-picker { display: flex; gap: 8px; margin: 16px 0; }
.theme-btn {
padding: 8px 16px; color: white; border: 2px solid transparent;
border-radius: 6px; cursor: pointer;
}
.theme-btn.active { border-color: #333; box-shadow: 0 0 0 2px rgba(0,0,0,0.2); }
.controls { margin: 16px 0; }
.controls label { display: block; margin: 8px 0 4px; font-size: 14px; }
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
案例知识融合:这个主题切换器综合运用了 :class 的数组语法和对象语法、:style 的对象绑定、计算属性驱动样式、scoped 样式隔离,以及 v-model 控制滑块参数。展示了 Vue 样式绑定在实际 UI 交互中的完整应用。
思考题:
:class的对象语法、数组语法、混合语法分别适合什么场景?scoped样式中的:deep()什么时候会用到?为什么需要它?- 使用 CSS 变量(CSS Custom Properties)和 Vue 的
:style绑定,哪种方式更适合做主题切换?为什么?