编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

    • README
    • HTML工具手册

    • TypeScript入门

    • Vue高级进阶

      • 基础入门
        • 1.1 Vue快速介绍
          • 1.1.1 Vue是什么
          • 1.1.2 Vue的特点
          • 1.1.3 Vue的版本
          • 1.1.4 Vue的生态
          • 1.1.5 Vue的应用领域
          • 1.1.6 综合案例与思考
        • 1.2 环境搭建
          • 1.2.1 安装Node.js
          • 1.2.2 使用Vite创建项目
          • 1.2.3 项目目录结构
          • 1.2.4 开发工具推荐
          • 1.2.5 综合案例与思考
        • 1.3 HelloWorld
          • 1.3.1 第一个Vue应用
          • 1.3.2 代码解读
          • 1.3.3 挂载应用
          • 1.3.4 综合案例与思考
        • 1.4 模板语法
          • 1.4.1 插值表达式
          • 1.4.2 原始HTML
          • 1.4.3 属性绑定
          • 1.4.4 JavaScript表达式
          • 1.4.5 综合案例与思考
        • 1.5 常用指令
          • 1.5.1 v-if条件渲染
          • 1.5.2 v-show显示隐藏
          • 1.5.3 v-for列表渲染
          • 1.5.4 v-model双向绑定
          • 1.5.5 v-bind和v-on
          • 1.5.6 v-if和v-show区别
          • 1.5.7 综合案例与思考
        • 1.6 事件处理
          • 1.6.1 监听事件
          • 1.6.2 事件方法
          • 1.6.3 事件修饰符
          • 1.6.4 按键修饰符
          • 1.6.5 综合案例与思考
        • 1.7 样式绑定
          • 1.7.1 class绑定
          • 1.7.2 style绑定
          • 1.7.3 数组语法
          • 1.7.4 scoped样式
          • 1.7.5 综合案例与思考
      • 组件开发
      • 响应式系统
      • 路由管理
      • 状态管理
      • 组合式API
      • 工程化实战
  • Linux应用开发

  • Apps
  • Web开发和进阶
  • Vue高级进阶
杨充
2026-04-23
目录

基础入门

# 01.基础快速入门

# 目录介绍

  • 1.1 Vue快速介绍
    • 1.1.1 Vue是什么
    • 1.1.2 Vue的特点
    • 1.1.3 Vue的版本
    • 1.1.4 Vue的生态
    • 1.1.5 Vue的应用领域
    • 1.1.6 综合案例与思考
  • 1.2 环境搭建
    • 1.2.1 安装Node.js
    • 1.2.2 使用Vite创建项目
    • 1.2.3 项目目录结构
    • 1.2.4 开发工具推荐
    • 1.2.5 综合案例与思考
  • 1.3 HelloWorld
    • 1.3.1 第一个Vue应用
    • 1.3.2 代码解读
    • 1.3.3 挂载应用
    • 1.3.4 综合案例与思考
  • 1.4 模板语法
    • 1.4.1 插值表达式
    • 1.4.2 原始HTML
    • 1.4.3 属性绑定
    • 1.4.4 JavaScript表达式
    • 1.4.5 综合案例与思考
  • 1.5 常用指令
    • 1.5.1 v-if条件渲染
    • 1.5.2 v-show显示隐藏
    • 1.5.3 v-for列表渲染
    • 1.5.4 v-model双向绑定
    • 1.5.5 v-bind和v-on
    • 1.5.6 v-if和v-show区别
    • 1.5.7 综合案例与思考
  • 1.6 事件处理
    • 1.6.1 监听事件
    • 1.6.2 事件方法
    • 1.6.3 事件修饰符
    • 1.6.4 按键修饰符
    • 1.6.5 综合案例与思考
  • 1.7 样式绑定
    • 1.7.1 class绑定
    • 1.7.2 style绑定
    • 1.7.3 数组语法
    • 1.7.4 scoped样式
    • 1.7.5 综合案例与思考

# 1.1 Vue快速介绍

# 1.1.1 Vue是什么

Vue.js(读音类似 view)是一款用于构建用户界面的渐进式 JavaScript 框架。它由尤雨溪(Evan You)于2014年创建,是目前最流行的前端框架之一。

"渐进式"意味着你可以根据需要逐步采用 Vue 的功能:可以只用它做一个页面的一小部分交互,也可以用它构建一个完整的单页应用(SPA)。

渐进式的含义:
小型项目 → 只用Vue核心(模板+响应式)
中型项目 → 加上Vue Router(路由管理)
大型项目 → 再加Pinia(状态管理)+ 工程化工具链
1
2
3
4

# 1.1.2 Vue的特点

  1. 渐进式框架:核心库只关注视图层,可以按需引入生态工具(Router、Pinia等),不强制一步到位。
  2. 响应式数据绑定:数据变化时视图自动更新,无需手动操作 DOM。
  3. 组件化开发:将页面拆分为独立、可复用的组件,每个组件包含自己的模板、逻辑和样式。
  4. 虚拟DOM:通过虚拟DOM进行高效的差异比较和最小化更新,提升渲染性能。
  5. 单文件组件(SFC):.vue 文件将 HTML、JavaScript、CSS 写在一起,开发体验好。
  6. 丰富的生态: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>
1
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,这就是"数据驱动视图"的核心思想。

思考题:

  1. Vue的"渐进式"和React、Angular有什么不同?为什么说Vue的上手成本更低?
  2. 为什么Vue 3要用Proxy替换Object.defineProperty作为响应式的底层实现?
  3. 声明式渲染和命令式渲染(直接操作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
1
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
1
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/
1
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 配置
1
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')
1
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>
1
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 项目的基础。

思考题:

  1. app.mount('#app') 中的 #app 对应的是哪个文件中的元素?整个挂载流程是怎样的?
  2. Vite 相比 Webpack 为什么启动速度快那么多?它的核心原理是什么?
  3. 为什么推荐在创建项目时就开启 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>
1
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)由三部分组成:

┌─────────────────────────────────┐
│ &lt;template&gt;                 │  ← HTML模板:定义页面结构
│   &lt;h1&gt;{{ message }}&lt;/h1&gt;│
│ &lt;/template&gt;                │
├─────────────────────────────────┤
│ &lt;script setup lang="ts"&gt;   │  ← JavaScript逻辑:定义数据和行为
│   const message = ref('Hello')  │
│ &lt;/script&gt;                  │
├─────────────────────────────────┤
│ &lt;style scoped&gt;             │  ← CSS样式:定义组件样式
│   h1 { color: #42b883; }       │
│ &lt;/style&gt;                   │
└─────────────────────────────────┘
1
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
┌──────────┐                ┌──────────┐               ┌──────────┐
│&lt;div       │  ← mount ←   │createApp  │  ← 根组件 ←  │&lt;template&gt; │
│  id="app" │               │  (App)    │               │  ...      │
│&lt;/div&gt;     │               │.mount()   │               │&lt;/template&gt;│
└──────────┘                └──────────┘               └──────────┘
1
2
3
4
5
6
  1. 浏览器加载 index.html,其中有 <div id="app"></div>
  2. main.ts 执行 createApp(App) 创建应用实例
  3. .mount('#app') 将 App 组件渲染到 #app 元素中
  4. 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>
1
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 的响应式特性。

思考题:

  1. ref 和直接声明变量 let name = 'World' 有什么区别?为什么必须用 ref?
  2. <script setup> 和普通 <script> 的区别是什么?它省略了哪些步骤?
  3. 为什么在 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>
1
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 }} <!-- 流程控制 -->
1
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>
1
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>
1
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>
1
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>
1
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场景中的应用。

思考题:

  1. v-html 和 的根本区别是什么?在什么场景下必须用 v-html?
  2. :class 的对象语法和数组语法有什么区别?各适用于什么场景?
  3. 为什么模板中只能使用表达式而不能使用语句?这个设计有什么好处?

# 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>
1
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>
1
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>
1
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>
1
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" />
1
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>
1
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>
1
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 指令系统的完整实战。

思考题:

  1. 为什么 v-for 的 key 要用 todo.id 而不是 index?如果用 index 会出什么问题?
  2. v-model 在 <input> 上本质上是哪两个操作的语法糖?
  3. 这个案例中 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>
1
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>
1
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>
1
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>
1
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>
1
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)、计算属性动态生成样式、以及交互反馈。通过一个可玩的交互案例,把事件处理的各种技巧串联在一起。

思考题:

  1. .prevent 和 .stop 分别在什么场景下必须使用?不使用会怎样?
  2. 为什么 Vue 要在模板中处理事件,而不是像 jQuery 那样在 JS 中手动绑定?这种设计有什么优势?
  3. .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>
1
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>
1
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>
1
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>
1
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>
1
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 交互中的完整应用。

思考题:

  1. :class 的对象语法、数组语法、混合语法分别适合什么场景?
  2. scoped 样式中的 :deep() 什么时候会用到?为什么需要它?
  3. 使用 CSS 变量(CSS Custom Properties)和 Vue 的 :style 绑定,哪种方式更适合做主题切换?为什么?
上次更新: 2026/06/10, 11:13:41
实用工具
组件开发

← 实用工具 组件开发→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式