Bombax's Knowledge Document Notes Bombax's Knowledge Document Notes
首页
  • 前置

    • 尚硅谷Java学习
    • 基础软件安装与配置
  • 核心

    • Java从入门到精通(JDK17版)
    • MySQL从入门到高级-基础篇
    • MySQL从入门到高级-高级篇
    • JDBC 核心技术(JDK21版)
    • JavaWeb 技术
  • 学习笔记

    • POJO 概念
  • Spring Cloud

    • SpringCloud
    • SpringCloud-Alibaba
  • 持久层框架

    • MyBatis
    • MyBatis-Plus
  • 相关知识

    • Mybatis 代码生成工具比较
  • 安全框架

    • 安全框架之 Spring Security
    • 安全框架之 Shiro
  • 定时任务框架

    • 定时任务框架之 Quartz
    • 定时任务框架之 XXL-JOB
  • Java 日志热门框架
  • Git 常用命令
  • Swagger API 文档生成工具
  • Motan RPC (opens new window)
  • Lombok Tutorial (opens new window)
  • Lombok Features (opens new window)
  • FastJSON2 (opens new window)
  • Spring Framework 5 中文文档 (opens new window)
  • XStream (opens new window)
  • fluent-validator 业务逻辑验证框架 (opens new window)
  • ehcache java 缓存框架 (opens new window)
  • jetcache java 缓存框架 (opens new window)
  • caffeine 缓存框架 (opens new window)
  • Spring Cache (opens new window)
  • 主流缓存框架调研 (opens new window)
  • redisson 官方中文文档 (opens new window)
  • LiquiBase 中文学习指南 (opens new window)
  • LiquiBase 官方文档 (opens new window)
  • 分类
  • 归档
GitHub (opens new window)

bombax

小小程序猿
首页
  • 前置

    • 尚硅谷Java学习
    • 基础软件安装与配置
  • 核心

    • Java从入门到精通(JDK17版)
    • MySQL从入门到高级-基础篇
    • MySQL从入门到高级-高级篇
    • JDBC 核心技术(JDK21版)
    • JavaWeb 技术
  • 学习笔记

    • POJO 概念
  • Spring Cloud

    • SpringCloud
    • SpringCloud-Alibaba
  • 持久层框架

    • MyBatis
    • MyBatis-Plus
  • 相关知识

    • Mybatis 代码生成工具比较
  • 安全框架

    • 安全框架之 Spring Security
    • 安全框架之 Shiro
  • 定时任务框架

    • 定时任务框架之 Quartz
    • 定时任务框架之 XXL-JOB
  • Java 日志热门框架
  • Git 常用命令
  • Swagger API 文档生成工具
  • Motan RPC (opens new window)
  • Lombok Tutorial (opens new window)
  • Lombok Features (opens new window)
  • FastJSON2 (opens new window)
  • Spring Framework 5 中文文档 (opens new window)
  • XStream (opens new window)
  • fluent-validator 业务逻辑验证框架 (opens new window)
  • ehcache java 缓存框架 (opens new window)
  • jetcache java 缓存框架 (opens new window)
  • caffeine 缓存框架 (opens new window)
  • Spring Cache (opens new window)
  • 主流缓存框架调研 (opens new window)
  • redisson 官方中文文档 (opens new window)
  • LiquiBase 中文学习指南 (opens new window)
  • LiquiBase 官方文档 (opens new window)
  • 分类
  • 归档
GitHub (opens new window)
  • 前置

  • 核心

    • Java从入门到精通(JDK17版)

    • MySQL从入门到高级-基础篇

    • MySQL从入门到高级-高级篇

    • JDBC 核心技术(JDK21版)
    • JavaWeb技术

      • 第一章 概述
      • 第二章 HTML&CSS
      • 第三章 JavaScript
      • 第四章 XML、Tomcat、HTTP
      • 第五章 Servlet
      • 第六章 会话、过滤器、监听器
      • 第七章 前端工程化-上
      • 第八章 前端工程化-中
        • 五、Vue3 通过 Vite 实现工程化
          • 5.1 Vite 核心概念
          • 5.1.1 什么是 Vite
          • 5.2 Vite 创建 Vue3 工程化项目
          • 5.2.1 Vite+Vue3 项目的创建、启动、停止
          • 5.2.2 Vite+Vue3 项目的目录结构
          • 5.2.3 Vite+Vue3 项目组件(SFC 入门)
          • 5.2.4 Vite+Vue3 响应式入门和 setup 函数
          • 5.2.5 Vite+Vue3 关于样式的导入方式
        • 六、Vue3 视图渲染技术
          • 6.1 模版语法
          • 6.1.1 插值表达式和文本渲染
          • 6.1.2 Attribute 属性渲染
          • 6.1.3 事件的绑定
          • 6.2 响应式基础
          • 6.2.1 响应式需求案例
          • 6.2.2 响应式实现关键字 ref
          • 6.2.3 响应式实现关键字 reactive
          • 6.2.4 扩展响应式关键字 toRefs 和 toRef
          • 6.3 条件和列表渲染
          • 6.3.1 条件渲染
          • 6.3.2 列表渲染
          • 6.4 双向绑定
          • 6.5 属性计算
          • 6.6 数据监听器
          • 6.7 Vue 生命周期
          • 6.7.1 生命周期简介
          • 6.7.2 生命周期案例
          • 6.8 Vue 组件
          • 6.8.1 组件基础
          • 6.8.2 组件化入门案例
          • 6.8.3 组件之间传递数据
          • 6.8.3.1 父传子(Props)
          • 6.8.3.2 子传父(Emits)
          • 6.8.3.3 兄弟传参
        • 七、Vue3 路由机制 Router
          • 7.1 路由简介
          • 7.2 路由入门案例
          • 7.3 路由重定向
          • 7.4 编程式路由(useRouter)
          • 7.5 路由传参(useRoute)
          • 7.6 路由守卫
      • 第九章 前端工程化-下
  • 学习笔记

  • Java基础
  • 核心
  • JavaWeb技术
bombax
2025-12-11
目录

第八章 前端工程化-中

# 第八章 前端工程化-中

# 五、Vue3 通过 Vite 实现工程化

# 5.1 Vite 核心概念

# 5.1.1 什么是 Vite

1684488405011.webp

Vite(法语"快速"的意思,发音 /vit/,类似"veet")是新一代前端构建工具,由 Vue.js 作者尤雨溪创建。它利用浏览器原生 ES 模块和现代编译工具,提供极速的开发体验。官网:https://cn.vitejs.dev/ (opens new window)

Vite 的核心优势:

  1. 极速的冷启动:Vite 无需打包即可启动开发服务器,传统构建工具(如 Webpack)需要将整个应用打包后才能启动,而 Vite 直接利用浏览器的原生 ES 模块支持,实现按需编译。
  2. 即时的模块热更新(HMR):当修改源代码时,Vite 只会精确更新修改的模块,而不是重新编译整个应用,更新速度极快且不会丢失应用状态。
  3. 真正的按需编译:只编译当前页面实际使用到的代码,大大减少了编译时间和资源消耗。

Vite vs Webpack 对比:

特性 Vite Webpack
启动速度 毫秒级(无需打包) 秒级甚至分钟级(需要完整打包)
热更新速度 毫秒级(精确模块更新) 秒级(重新编译相关模块)
构建方式 开发环境使用 ES Module,生产环境使用 Rollup 统一使用 bundle 打包
配置复杂度 开箱即用,配置简单 需要较多配置
适用场景 现代浏览器项目、快速开发 需要兼容老旧浏览器、复杂配置需求

使用场景:

  • ✅ 适合:Vue3/React 现代框架项目、需要快速开发迭代的项目、中小型应用
  • ⚠️ 需谨慎:需要兼容 IE11 等老旧浏览器的项目(需额外配置)

注意

虽然 Vite 在开发环境极快,但生产环境仍需要打包构建,此时使用 Rollup 进行优化打包。

# 5.2 Vite 创建 Vue3 工程化项目

# 5.2.1 Vite+Vue3 项目的创建、启动、停止

1. 使用命令行创建工程

  • 在磁盘的合适位置上,创建一个空目录用于存储多个前端项目
  • 用 VS Code 打开该目录
  • 在 VS Code 中打开命令行运行如下命令
npm create vite@latest
1
  • 第一次使用 vite 时会提示下载 vite,输入 y 回车即可,下次使用 vite 就不会出现了

1687769339457

注意

选择 vue+JavaScript 选项即可

2. 安装项目所需依赖

  • cd 进入刚刚创建的项目目录
  • npm install 命令安装基础依赖
cd ./vue3-demo1
npm install
1
2

3. 启动项目

  • 查看项目下的 package.json
{
  "name": "vue3-demo1",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "bootstrap": "^5.2.3",
    "sass": "^1.62.1",
    "vue": "^3.2.47"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.1.0",
    "vite": "^4.3.2"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
npm run dev
1

image_PHNwnXnsWv.webp

5. 停止项目

  • 命令行上按 Ctrl+C 组合键停止开发服务器

6. 常见问题处理

  • 端口被占用:如果 5173 端口被占用,Vite 会自动尝试下一个可用端口(5174、5175...)
  • 依赖安装失败:尝试删除 node_modules 文件夹和 package-lock.json,重新执行 npm install
  • 启动缓慢:首次启动会进行依赖预构建,后续启动会很快

# 5.2.2 Vite+Vue3 项目的目录结构

1. 下面是 Vite 项目结构和入口的详细说明:

1684489112904

  • public/ 目录:用于存放一些公共资源,如 HTML 文件、图像、字体等,这些资源会被直接复制到构建出的目标目录中。
  • src/ 目录:存放项目的源代码,包括 JavaScript、CSS、Vue 组件、图像和字体等资源。在开发过程中,这些文件会被 Vite 实时编译和处理,并在浏览器中进行实时预览和调试。以下是 src 内部划分建议:
    1. assets/ 目录:用于存放一些项目中用到的静态资源,如图片、字体、样式文件等。
    2. components/ 目录:用于存放组件相关的文件。组件是代码复用的一种方式,用于抽象出一个可复用的 UI 部件,方便在不同的场景中进行重复使用。
    3. layouts/ 目录:用于存放布局组件的文件。布局组件通常负责整个应用程序的整体布局,如头部、底部、导航菜单等。
    4. pages/ 目录:用于存放页面级别的组件文件,通常是路由对应的组件文件。在这个目录下,可以创建对应的文件夹,用于存储不同的页面组件。
    5. plugins/ 目录:用于存放 Vite 插件相关的文件,可以按需加载不同的插件来实现不同的功能,如自动化测试、代码压缩等。
    6. router/ 目录:用于存放 Vue.js 的路由配置文件,负责管理视图和 URL 之间的映射关系,方便实现页面之间的跳转和数据传递。
    7. store/ 目录:用于存放 Vuex 状态管理相关的文件,负责管理应用程序中的数据和状态,方便统一管理和共享数据,提高开发效率。
    8. utils/ 目录:用于存放一些通用的工具函数,如日期处理函数、字符串操作函数等。
  • vite.config.js 文件:Vite 的配置文件,可以通过该文件配置项目的参数、插件、打包优化等。该文件可以使用 CommonJS 或 ES6 模块的语法进行配置。
  • package.json 文件:标准的 Node.js 项目配置文件,包含了项目的基本信息和依赖关系。其中可以通过 scripts 字段定义几个命令,如 dev、build、serve 等,用于启动开发、构建和启动本地服务器等操作。
  • Vite 项目的入口为 src/main.js 文件,这是 Vue.js 应用程序的启动文件,也是整个前端应用程序的入口文件。在该文件中,通常会引入 Vue.js 及其相关插件和组件,同时会创建 Vue 实例,挂载到 HTML 页面上指定的 DOM 元素中。

2. Vite 的运行脚本说明

  • 在安装了 Vite 的项目中,可以在 npm scripts 中使用 vite 可执行文件,或者直接使用 npx vite 运行它。下面是通过脚手架创建的 Vite 项目中默认的 npm scripts:(package.json)
{
  "scripts": {
    "dev": "vite", // 启动开发服务器,别名:`vite dev`,`vite serve`
    "build": "vite build", // 为生产环境构建产物
    "preview": "vite preview" // 本地预览生产构建产物
  }
}
1
2
3
4
5
6
7

3. Vite 配置文件详解 (vite.config.js)

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  // 插件配置
  plugins: [vue()],
  
  // 开发服务器配置
  server: {
    port: 3000,              // 自定义端口号
    open: true,              // 启动时自动打开浏览器
    cors: true,              // 允许跨域
    proxy: {                 // 配置代理解决跨域问题
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  
  // 路径别名配置
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils')
    }
  },
  
  // 构建配置
  build: {
    outDir: 'dist',          // 打包输出目录
    assetsDir: 'assets',     // 静态资源目录
    sourcemap: false,        // 是否生成 source map
    minify: 'terser',        // 压缩方式:'terser' | 'esbuild'
    chunkSizeWarningLimit: 500 // chunk 大小警告的限制(KB)
  }
})
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

常用配置项说明:

配置项 说明 常用值
server.port 开发服务器端口 3000, 5173, 8080
server.open 启动时是否自动打开浏览器 true, false
server.proxy 配置代理,解决开发环境跨域 对象配置
resolve.alias 路径别名,简化导入路径 {'@': '/src'}
build.outDir 打包输出目录 'dist', 'build'
base 公共基础路径 '/', '/my-app/'

4. 环境变量配置

创建环境变量文件:

在项目根目录创建以下文件:

  • .env - 所有环境都会加载
  • .env.development - 开发环境加载
  • .env.production - 生产环境加载

环境变量文件示例:

# .env.development
VITE_APP_TITLE=我的应用-开发环境
VITE_API_BASE_URL=http://localhost:8080/api
VITE_APP_PORT=3000
1
2
3
4
# .env.production
VITE_APP_TITLE=我的应用
VITE_API_BASE_URL=https://api.example.com
1
2
3

在代码中使用环境变量:

// 获取环境变量
console.log(import.meta.env.VITE_APP_TITLE)
console.log(import.meta.env.VITE_API_BASE_URL)

// 判断当前环境
if (import.meta.env.DEV) {
  console.log('开发环境')
}

if (import.meta.env.PROD) {
  console.log('生产环境')
}
1
2
3
4
5
6
7
8
9
10
11
12

注意

  1. 环境变量必须以 VITE_ 开头才能在客户端代码中访问
  2. 修改环境变量文件后需要重启开发服务器
  3. 不要在环境变量中存储敏感信息(如密钥、密码等)

# 5.2.3 Vite+Vue3 项目组件(SFC 入门)

什么是 Vue 的组件?

  • 一个页面作为整体,是由多个部分组成的,每个部分在这里就可以理解为一个组件
  • 每个 .vue 文件就可以理解为一个组件,多个 .vue 文件可以构成一个整体页面
  • 组件化给我们带来的核心优势:
    • 代码复用:相同的 UI 结构可以在不同页面中重复使用
    • 独立维护:每个组件职责单一,修改不会影响其他组件
    • 团队协作:不同开发者可以并行开发不同组件
    • 逻辑封装:将相关的 HTML、CSS、JavaScript 封装在一起

什么是 .vue 文件?

  • 传统的页面由 .html 文件、.css 文件和 .js 文件三个文件组成(多文件组件)
  • Vue 将这三个文件合并成一个 .vue 文件(Single-File Component,简称 SFC,单文件组件)
  • .vue 文件对 JS/CSS/HTML 统一封装,这是 Vue 中的核心概念,该文件由三个部分组成:
    • <template> 标签:代表组件的 HTML 结构部分,替代传统的 .html 文件
    • <script> 标签:代表组件的 JavaScript 逻辑代码,替代传统的 .js 文件
    • <style> 标签:代表组件的 CSS 样式代码,替代传统的 .css 文件

单文件组件(SFC)的优势:

  1. 更好的组织:相关的代码集中在一个文件中,便于查找和维护
  2. 作用域隔离:<style scoped> 可以让样式只作用于当前组件
  3. 编译优化:Vue 编译器可以对 SFC 进行更好的优化
  4. 类型支持:配合 TypeScript 可以获得更好的类型检查

工程化 Vue 项目如何组织这些组件?

  • index.html 是项目的入口,其中 <div id='app'></div> 是用于挂载所有组件的根元素
  • index.html 中的 <script> 标签引入了 main.js 文件,具体的挂载过程在 main.js 中执行
  • main.js 是 Vue 工程中非常重要的文件,它决定项目使用哪些依赖、插件,以及导入的根组件
  • App.vue 是 Vue 中的根组件,所有其他组件都要通过该组件进行导入,该组件通过路由可以控制页面的切换

组件的层级关系:

index.html(HTML 入口)
    ↓
main.js(JavaScript 入口)
    ↓
App.vue(根组件)
    ↓
Header.vue / Content.vue / Footer.vue(子组件)
    ↓
Button.vue / Input.vue(更细粒度的组件)
1
2
3
4
5
6
7
8
9

# 5.2.4 Vite+Vue3 响应式入门和 setup 函数

1. 使用 vite 创建一个 vue+JavaScript 项目

npm create vite
npm install 
npm run dev
1
2
3
  • App.vue
<script>
    //存储vue页面逻辑js代码
</script>

<template>
    <!-- 页面的样式的是html代码-->
</template>

<style scoped>
    /** 存储的是css代码! <style scoped> 是 Vue.js 单文件组件中用于设置组件样式的一种方式。
    它的含义是将样式局限在当前组件中,不对全局样式造成影响。 */
</style>
1
2
3
4
5
6
7
8
9
10
11
12

2. vue3 响应式数据入门

<script type="module">
    //存储vue页面逻辑js代码
    import {ref} from 'vue'
    export default{
        setup(){
            //非响应式数据: 修改后VUE不会更新DOM
            //响应式数据:   修改后VUE会更新DOM
            //VUE2中数据默认是响应式的
            //VUE3中数据要经过ref或者reactive处理后才是响应式的
            //ref是VUE3框架提供的一个函数,需要导入
            //let counter = 1
            //ref处理的响应式数据在js编码修改的时候需要通过.value操作
            //ref响应式数据在绑定到html上时不需要.value

            let counter = ref(1)
            function increase(){
                // 通过.value修改响应式数据
                counter.value++
            }
            function decrease(){
                counter.value--
            }
            return {
                counter,
                increase,
                decrease
            }
        }
    }
</script>
<template>
    <div>
      <button @click="decrease()">-</button>
      {{ counter }}
      <button @click="increase()">+</button>
    </div>
    
</template>

<style scoped>
    button{
        border: 1px solid red;
    }
</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

3. vue3 setup 函数和语法糖

  • 位置:src/App.vue
<script type="module" setup>
   
/* <script type="module" setup> 通过setup关键字
可以省略 export default {setup(){   return{}}}这些冗余的语法结构 */
    import {ref} from 'vue'
    // 定义响应式数据
    let counter = ref(1)
    // 定义函数
    function increase(){
        counter.value++
    }
    function decrease(){
        counter.value--
    }
    
</script>
<template>
    <div>
      <button @click="decrease()">-</button>
      {{ counter }}
      <button @click="increase()">+</button>
    </div>
    
</template>

<style scoped>
    button{
        border: 1px solid red;
    }
</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

# 5.2.5 Vite+Vue3 关于样式的导入方式

  1. 全局引入 main.js
    import './style/reset.css' //书写引入的资源的相对路径即可!
    
    1
  2. vue 文件 script 代码引入
    import './style/reset.css'
    
    1
  3. Vue 文件 style 代码引入
    @import './style/reset.css'
    
    1

# 六、Vue3 视图渲染技术

# 6.1 模版语法

Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。

# 6.1.1 插值表达式和文本渲染

插值表达式:最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法,即双大括号 {{}}

  • 插值表达式是将数据渲染到元素的指定位置的手段之一
  • 插值表达式不绝对依赖标签,其位置相对自由
  • 插值表达式中支持 JavaScript 的运算表达式
  • 插值表达式中也支持函数的调用

使用注意事项:

  1. 仅支持单个表达式:
    <!-- 正确 -->
    {{ number + 1 }}
    {{ ok ? 'YES' : 'NO' }}
    {{ message.split('').reverse().join('') }}
    
    <!-- 错误:这是语句,不是表达式 -->
    {{ var a = 1 }}
    
    <!-- 错误:条件控制不支持,请使用三元表达式 -->
    {{ if (ok) { return message } }}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  2. 避免复杂逻辑:插值表达式应该简单明了,复杂逻辑应该使用计算属性或方法
  3. 性能考虑:避免在插值表达式中调用复杂函数,每次重渲染都会执行
    <script setup type="module">
      let msg ="hello vue3"
      let getMsg= ()=>{
        return 'hello vue3 message'
      }
      let age = 19
      let bee = '蜜 蜂'
      // 购物车
      const carts = [{name:'可乐',price:3,number:10},{name:'薯片',price:6,number:8}];
      //计算购物车总金额
      function compute(){
          let count = 0;
          for(let index in carts){
              count += carts[index].price*carts[index].number;
          }
          return count;
      }
    </script>
    
    <template>
      <div>
        <h1>{{ msg }}</h1>
        msg的值为: {{ msg }} <br>
        getMsg返回的值为:{{ getMsg() }}  <br>
        是否成年: {{ age>=18?'true':'false' }} <br>
        反转: {{ bee.split(' ').reverse().join('-') }} <br>
        购物车总金额: {{ compute() }} <br/>
        购物车总金额: {{carts[0].price*carts[0].number + carts[1].price*carts[1].number}} <br>
      </div>
    </template>
    
    <style scoped>
    
    </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

为了渲染双标签中的文本,我们也可以选择使用 v-text 和 v-html 指令

  • v-*** 这种写法的方式使用的是 Vue 的指令
  • v-*** 的指令必须依赖元素,并且要写在元素的开始标签中
  • v-*** 指令支持 ES6 中的字符串模板
  • 插值表达式中支持 JavaScript 的运算表达式
  • 插值表达式中也支持函数的调用

v-text vs v-html 对比:

特性 v-text v-html
HTML 解析 不解析,当作纯文本 解析 HTML 标签
XSS 攻击风险 无风险 有风险,不要用于用户输入
使用场景 显示纯文本 显示带样式的内容
性能 较快 较慢(需解析 HTML)

安全警告

  • ❗ 绝不要对用户提供的内容使用 v-html,否则容易导致 XSS 攻击
  • ✅ v-text 是安全的,可以放心使用
  • ✅ 如果必须使用 v-html,请确保内容来源可信(如后端过滤后的内容)
  • v-text 可以将数据渲染成双标签中间的文本,但是不识别 HTML 元素结构的文本
  • v-html 可以将数据渲染成双标签中间的文本,识别 HTML 元素结构的文本
<script setup type="module">
  let msg ='hello vue3'
  let getMsg= ()=>{
    return msg
  }
  let age = 19
  let bee = '蜜 蜂'
  let redMsg ='<font color=\'red\'>msg</font>'
  let greenMsg =`<font color=\'green\'>${msg}</font>`
</script>

<template>
  <div>
    <span v-text='msg'></span> <br>
    <span v-text='redMsg'></span> <br>
    <span v-text='getMsg()'></span> <br>
    <span v-text='age>18?"成年":"未成年"'></span> <br>
    <span v-text='bee.split(" ").reverse().join("-")'></span> <br>
    <span v-html='msg'></span> <br>
    <span v-html='redMsg'></span> <br>
    <span v-html='greenMsg'></span> <br>
    <span v-html="`<font color='green'>${msg}</font>`"></span> <br>
  </div>
</template>

<style scoped>

</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

# 6.1.2 Attribute 属性渲染

想要渲染一个元素的 attribute,应该使用 v-bind 指令

  • 由于插值表达式不能直接放在标签的属性中,所以要渲染元素的属性就应该使用 v-bind
  • v-bind 可以用于渲染任何元素的属性,语法为 v-bind:属性名='数据名',可以简写为 :属性名='数据名'

v-bind 的特殊用法:

  1. 绑定布尔属性:
    <!-- disabled 属性会根据 isDisabled 的真假值决定是否存在 -->
    <button :disabled="isDisabled">提交</button>
    
    1
    2
  2. 绑定多个属性:
    <script setup>
    import { reactive } from 'vue'
    const attrs = reactive({
      id: 'my-input',
      class: 'input-box',
      placeholder: '请输入'
    })
    </script>
    
    <template>
      <!-- 使用 v-bind 不带参数,绑定整个对象 -->
      <input v-bind="attrs">
    </template>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  3. 动态属性名:
    <script setup>
    import { ref } from 'vue'
    const attributeName = ref('href')
    const url = ref('https://www.example.com')
    </script>
    
    <template>
      <!-- 使用方括号动态指定属性名 -->
      <a :[attributeName]="url">链接</a>
    </template>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

注意

  1. 简写语法 : 是开发中最常用的方式
  2. 属性值为 null 或 undefined 时,该属性不会被渲染
  3. class 和 style 有特殊的增强用法,支持对象和数组语法
<script setup type="module">
  const data = {
    name:'尚硅谷',
    url:"http://www.atguigu.com",
    logo:"http://www.atguigu.com/images/index_new/logo.png"
  }
</script>

<template>
  <div>
    <a 
      v-bind:href='data.url' 
      target="_self">
      <img 
        :src="data.logo" 
        :title="data.name">
      <br>
      <input type="button" 
             :value="`点击访问${data.name}`">
    </a>
  </div>
</template>

<style scoped>
</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

# 6.1.3 事件的绑定

我们可以使用 v-on 来监听 DOM 事件,并在事件触发时执行对应的 Vue 的 JavaScript 代码。

  • 用法:v-on:click="handler" 或简写为 @click="handler"
  • Vue 中的事件名 = 原生事件名去掉 on 前缀,如:onClick --> click
  • handler 的值可以是方法事件处理器,也可以是内联事件处理器

事件修饰符分类:

  1. 事件修饰符:
    • .stop:阻止事件冒泡(等同于 event.stopPropagation())
    • .prevent:阻止默认事件(等同于 event.preventDefault())[重点]
    • .once:只触发一次事件 [重点]
    • .capture:使用事件捕获模式而不是冒泡模式
    • .self:只在事件发送者自身触发时才触发事件
  2. 按键修饰符:
    <!-- 只在 Enter 键被按下时触发 -->
    <input @keyup.enter="submit">
    
    <!-- 常用按键修饰符:.enter .tab .delete .esc .space .up .down .left .right -->
    <input @keyup.esc="clearInput">
    
    <!-- 组合按键:Ctrl + Enter -->
    <input @keyup.ctrl.enter="submit">
    
    1
    2
    3
    4
    5
    6
    7
    8
  3. 系统修饰符:
    <!-- Ctrl 键被按下时点击 -->
    <button @click.ctrl="handleCtrlClick">点击</button>
    
    <!-- 系统修饰符:.ctrl .alt .shift .meta(Mac 的 Command 键) -->
    
    1
    2
    3
    4
  4. 鼠标修饰符:
    <!-- 只在鼠标左键点击时触发 -->
    <button @click.left="handleLeftClick">左键</button>
    
    <!-- 鼠标修饰符:.left .right .middle -->
    
    1
    2
    3
    4

修饰符链式调用:

<!-- 阻止冒泡并阻止默认行为 -->
<a @click.stop.prevent="doThis">Link</a>

<!-- 修饰符可以串联,顺序很重要 -->
<div @click.capture.stop="doThis">...</div>
1
2
3
4
5
<script setup type="module">
  import {ref} from 'vue'
  // 响应式数据 当发生变化时,会自动更新 DOM 树
  let count=ref(0)
  let addCount= ()=>{
    count.value++
  }
  let incrCount= (event)=>{
    count.value++
    // 通过事件对象阻止组件的默认行为
    event.preventDefault();
  }
</script>

<template>
  <div>
    <h1>count的值是:{{ count }}</h1>
    <!-- 方法事件处理器 -->
    <button v-on:click="addCount()">addCount</button> <br>
    <!-- 内联事件处理器 -->
    <button @click="count++">incrCount</button> <br>
    <!-- 事件修饰符 once 只绑定事件一次 -->
    <button @click.once="count++">addOnce</button> <br>
    <!-- 事件修饰符 prevent 阻止组件的默认行为 -->
    <a href="http://www.atguigu.com" target="_blank" @click.prevent="count++">prevent</a> <br>
    <!-- 原生js方式阻止组件默认行为 (推荐) -->
    <a href="http://www.atguigu.com" target="_blank" @click="incrCount($event)">prevent</a> <br>
  </div>
</template>

<style scoped>

</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

# 6.2 响应式基础

此处的响应式是指:数据模型发生变化时,自动更新 DOM 树内容,页面上显示的内容会进行同步变化。

响应式原理简介:

  • Vue 3 使用 Proxy 对象实现响应式(Vue 2 使用 Object.defineProperty)
  • Proxy 可以拦截对象的读取、设置等操作,从而实现数据变化的自动追踪
  • 当响应式数据变化时,Vue 会自动重新渲染依赖该数据的组件

重要特性:

  1. Vue 3 的数据模型默认不是响应式的,需要通过 ref 或 reactive 进行处理
  2. 只有响应式数据的变化才会触发视图更新
  3. 非响应式数据修改后,页面不会自动更新

# 6.2.1 响应式需求案例

需求:实现 + - 按钮,实现数字加一减一

<script type="module" setup>
    let counter = 0;
    function show(){
        alert(counter);
    }
</script>

<template>
  <div>
    <button @click="counter--">-</button> 
    {{ counter }} 
    <button @click="counter++">+</button>
    <hr>
    <!-- 此案例,我们发现counter值,会改变,但是页面不改变! 默认Vue3的数据是非响应式的!-->
    <button @click="show()">显示counter值</button>
   </div>
</template> 

<style scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 6.2.2 响应式实现关键字 ref

ref 可以将一个基本类型的数据(如字符串、数字、布尔值等)转换为一个响应式对象。

ref 的特点:

  1. 适用于基本类型:主要用于包装基本数据类型(String、Number、Boolean 等)
  2. 也支持对象:虽然主要用于基本类型,但也可以包装对象和数组
  3. 访问方式:在 <script> 中需要通过 .value 访问值,在 <template> 中会自动解包
  4. 返回 RefImpl 对象:ref 返回一个包含 .value 属性的对象
<script type="module" setup>
    /* 从vue中引入ref方法 */
    import {ref} from 'vue'
    let counter = ref(0);
    function show(){
        alert(counter.value);
    }
    /* 函数中要操作ref处理过的数据,需要通过.value形式 */
    let decr = () =>{
      counter.value--;
    }
    let incr = () =>{
      counter.value++;
    }
</script>

<template>
  <div>
    <button @click="counter--">-</button> 
    <button @click="decr()">-</button> 
    {{ counter }} 
    <button @click="counter++">+</button>
    <button @click="incr()">+</button> 
    <hr>
    <button @click="show()">显示counter值</button>
   </div>
</template> 

<style scoped>

</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

在上面的例子中,我们使用 ref 包裹了一个数字,在代码中给这个数字加 1 后,视图也会跟着动态更新。需要注意的是,由于使用了 ref,因此需要在访问该对象时使用 .value 来获取其实际值。

# 6.2.3 响应式实现关键字 reactive

我们可以使用 reactive() (opens new window) 函数创建一个响应式对象或数组。

reactive 的特点:

  1. 仅适用于对象类型:只能用于对象、数组、Map、Set 等引用类型
  2. 深层响应式:会递归地将对象的所有嵌套属性转换为响应式
  3. 直接访问属性:不需要 .value,直接通过 对象.属性 访问
  4. 不能替换整个对象:直接赋值会失去响应性,需要修改属性而非替换对象

使用限制:

import { reactive } from 'vue'

let state = reactive({ count: 0 })

// ❌ 错误:这会失去响应性
state = { count: 1 }

// ✅ 正确:修改属性保持响应性
state.count = 1
1
2
3
4
5
6
7
8
9
<script type="module" setup>
    /* 从vue中引入reactive方法 */
    import {ref,reactive} from 'vue'
    let data = reactive({
      counter:0
    })
    function show(){
      alert(data.counter);
    }
    /* 函数中要操作reactive处理过的数据,需要通过 对象名.属性名的方式 */
    let decr = () =>{
      data.counter--;
    }
    let incr = () =>{
      data.counter++;
    }
</script>

<template>
  <div>
    <button @click="data.counter--">-</button> 
    <button @click="decr()">-</button> 
    {{ data.counter }} 
    <button @click="data.counter++">+</button>
    <button @click="incr()">+</button> 
    <hr>
    <button @click="show()">显示counter值</button>
   </div>
</template> 

<style scoped>

</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

ref vs reactive 深度对比

特性 ref reactive
适用数据类型 基本类型 + 对象 仅对象类型(对象、数组、Map、Set)
访问方式 JS 中需 .value,模板中自动解包 直接访问属性,无需 .value
响应式深度 浅响应(基本类型)或深响应(对象) 深层响应式
重新赋值 可以整个替换 count.value = 10 不能整个替换,只能修改属性
类型推导 TypeScript 类型推导更准确 类型推导略复杂
模板使用 自动解包,直接用 直接用
解构 解构后失去响应性(需 toRefs) 解构后失去响应性(需 toRefs)

使用场景推荐:

  1. 使用 ref 的场景: ✅

    • 基本类型数据:字符串、数字、布尔值等
    • 需要整个替换的数据
    • 单一值的状态管理
    • 与组合式 API 配合使用时的简单数据
    import { ref } from 'vue'
    
    // ✅ 适合用 ref
    const count = ref(0)
    const message = ref('Hello')
    const isLoading = ref(false)
    
    // 可以整个替换
    count.value = 100
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  2. 使用 reactive 的场景: ✅

    • 复杂对象结构:表单数据、用户信息等
    • 需要管理多个相关属性的数据
    • 不需要整个替换的对象
    • 深层嵌套的数据结构
    import { reactive } from 'vue'
    
    // ✅ 适合用 reactive
    const user = reactive({
      name: '张三',
      age: 25,
      address: {
        city: '北京',
        district: '海淀区'
      }
    })
    
    // 修改属性
    user.name = '李四'
    user.address.city = '上海'
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

最佳实践建议:

  1. 优先使用 ref:尤其是在组合式 API 中,ref 更灵活,类型推导更好
  2. 对象也可用 ref:ref 也能很好地处理对象,且支持整个替换
  3. 避免混用:在同一组件中,尽量保持一致的风格
  4. 解构问题:如需解构,使用 toRefs 或 toRef 保持响应性
import { ref, reactive, toRefs } from 'vue'

// 推荐:统一使用 ref
const count = ref(0)
const user = ref({ name: '张三', age: 25 })

// 或者:reactive 配合 toRefs 解构
const state = reactive({ count: 0, name: '张三' })
const { count, name } = toRefs(state) // 解构后仍保持响应性
1
2
3
4
5
6
7
8
9

# 6.2.4 扩展响应式关键字 toRefs 和 toRef

toRef 基于 reactive 响应式对象上的一个属性,创建一个对应的 ref 响应式数据。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

toRefs 将一个响应式对象的多个属性转换为多个 ref 数据,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() (opens new window) 创建的。

使用场景:

  1. 保持解构后的响应性:解构 reactive 对象时保持响应性
  2. 组合式 API 返回值:在组合函数中返回响应式属性
  3. 简化模板使用:避免在模板中重复写对象名

toRef 用法:

import { reactive, toRef } from 'vue'

const state = reactive({
  count: 0,
  name: 'Vue'
})

// 将 state.count 转换为 ref
const countRef = toRef(state, 'count')

// 双向同步
countRef.value++  // state.count 也会变为 1
state.count = 5   // countRef.value 也会变为 5
1
2
3
4
5
6
7
8
9
10
11
12
13

toRefs 用法:

import { reactive, toRefs } from 'vue'

const state = reactive({
  count: 0,
  name: 'Vue',
  isActive: true
})

// 将整个对象的所有属性转换为 ref
const { count, name, isActive } = toRefs(state)

// 在模板中可以直接使用,无需 state.count
console.log(count.value)  // 0
count.value++             // state.count 也会变为 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14

实际应用示例:

// 组合函数中使用 toRefs
import { reactive, toRefs } from 'vue'

function useCounter() {
  const state = reactive({
    count: 0,
    doubleCount: computed(() => state.count * 2)
  })
  
  function increment() {
    state.count++
  }
  
  // 返回时使用 toRefs,让使用者可以解构
  return {
    ...toRefs(state),
    increment
  }
}

// 组件中使用
const { count, doubleCount, increment } = useCounter()
// count 和 doubleCount 都是 ref,保持响应性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

注意

  1. toRef/toRefs 创建的 ref 与源对象保持双向同步
  2. 如果源属性不存在,toRef 会创建一个值为 undefined 的 ref
  3. toRefs 只会为源对象的根级属性创建 ref,不会递归处理

案例:响应显示 reactive 对象属性

<script type="module" setup>
    /* 从vue中引入reactive方法 */
    import {ref,reactive,toRef,toRefs} from 'vue'
    let data = reactive({
      counter:0,
      name:"test"
    })

    // 将一个reactive响应式对象中的某个属性转换成一个ref响应式对象
    let ct =toRef(data,'counter');
    // 将一个reactive响应式对象中的多个属性转换成多个ref响应式对象
    let {counter,name} = toRefs(data)

    function show(){
        alert(data.counter);
        // 获取ref的响应对象,需要通过.value属性
        alert(counter.value);
        alert(name.value)
    }
    /* 函数中要操作ref处理过的数据,需要通过.value形式 */
    let decr = () =>{
      data.counter--;
    }
    let incr = () =>{
      /* ref响应式数据,要通过.value属性访问 */
      counter.value++;
    }
</script>

<template>
  <div>
    <button @click="data.counter--">-</button> 
    <button @click="decr()">-</button> 
    {{ data.counter }} 
    &amp;
    {{ ct }} 
    <button @click="data.counter++">+</button>
    <button @click="incr()">+</button> 
    <hr>
    <button @click="show()">显示counter值</button>
   </div>
</template> 

<style scoped>

</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

# 6.3 条件和列表渲染

# 6.3.1 条件渲染

v-if 条件渲染:

  • v-if='表达式'只会在指令的表达式返回真值时才被渲染
  • 也可以使用 v-else 为 v-if 添加一个“else 区块”。
  • 一个 v-else 元素必须跟在一个 v-if 元素后面,否则它将不会被识别。
<script type="module" setup>
    import {ref} from 'vue'
    let awesome = ref(true)
</script>

<template>
  <div>
    <h1 v-if="awesome">Vue is awesome!</h1>
    <h1 v-else>Oh no 😢</h1>
    <button @click="awesome = !awesome">Toggle</button>
  </div>
</template> 

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

v-show条件渲染扩展:

  • 另一个可以用来按条件显示一个元素的指令是 v-show,其用法基本一样。
  • 不同之处在于 v-show 会在 DOM 渲染中保留该元素;v-show 仅切换了该元素上名为 display 的 CSS 属性。
  • v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。
<script type="module" setup>
    import {ref} from 'vue'
    let awesome = ref(true)
</script>

<template>
  <div>
    <h1 id="ha"  v-show="awesome">Vue is awesome!</h1>
    <h1 id="hb"  v-if="awesome">Vue is awesome!</h1>
    <h1 id="hc"  v-else>Oh no 😢</h1>
    <button @click="awesome = !awesome">Toggle</button>
  </div>
</template> 

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

v-if vs v-show 渲染机制对比:

特性 v-if v-show
渲染机制 条件为 false 时完全不渲染 始终渲染,通过 CSS display 控制
DOM 操作 条件切换时销毁/重建 DOM 无 DOM 操作,仅切换样式
初始渲染开销 条件为 false 时无开销 总是渲染,开销较高
切换开销 高(DOM 操作) 低(CSS 切换)
事件监听器 条件为 false 时销毁 保留
子组件 条件为 false 时销毁 保留
惰性 是(初始 false 不渲染) 否(总是渲染)
支持 v-else 支持 不支持

性能对比:

// 场景 1:频繁切换(如选项卡切换)
const showTab = ref('tab1')

// ✅ 推荐使用 v-show,避免频繁 DOM 操作
<div v-show="showTab === 'tab1'">Tab 1 内容</div>
<div v-show="showTab === 'tab2'">Tab 2 内容</div>

// 场景 2:很少改变(如权限控制)
const isAdmin = ref(false)

// ✅ 推荐使用 v-if,不渲染不需要的 DOM
<div v-if="isAdmin">管理员功能</div>
1
2
3
4
5
6
7
8
9
10
11
12

选择建议:

  1. 使用 v-if 的场景:
    • 条件很少改变(如用户权限、配置项)
    • 初始条件为 false 的情况占多数
    • 需要配合 v-else、v-else-if 使用
    • 节省初始渲染开销(懒加载)
  2. 使用 v-show 的场景:
    • 需要频繁切换显示/隐藏(如选项卡、折叠面板)
    • 元素初始就需要渲染
    • 切换频率高的场景
  3. 性能优化建议:
    • 大型列表条件渲染优先用 v-if,减少初始 DOM 数量
    • 复杂组件的显示/隐藏优先用 v-show,避免重复初始化
    • 不要在 v-for 上使用 v-if(优先级问题)

小结:

  • v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
  • v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
  • 相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。
  • 总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

# 6.3.2 列表渲染

我们可以使用 v-for 指令基于一个数组来渲染一个列表。

  • v-for 指令的值需要使用 item in items 形式的特殊语法,其中 items 是源数据的数组,而 item 是迭代项的别名。
  • 在 v-for 块中可以完整地访问父作用域内的属性和变量。v-for 也支持使用可选的第二个参数表示当前项的位置索引。

key 属性的重要性:

Vue 默认按照“就地更新”策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。

key 的作用:

  1. 唯一标识:key 为每个节点提供唯一标识,Vue 可以追踪每个节点的身份
  2. 高效更新:帮助 Vue 的虚拟 DOM 算法识别哪些元素发生了变化
  3. 保持状态:在列表重排序时保持组件状态和 DOM 状态

key 的使用规范:

<!-- ✅ 正确:使用唯一 ID 作为 key -->
<div v-for="item in items" :key="item.id">
  {{ item.name }}
</div>

<!-- ❌ 错误:不要使用索引作为 key(除非列表不会变化) -->
<div v-for="(item, index) in items" :key="index">
  {{ item.name }}
</div>

<!-- ❌ 错误:不要使用不稳定的值作为 key -->
<div v-for="item in items" :key="Math.random()">
  {{ item.name }}
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

key 的选择原则:

  1. 优先使用数据的唯一 ID:如数据库主键、唯一编号等
  2. 不要使用索引:除非列表是静态的且永不会改变
  3. 不要使用随机数:会导致每次都重新渲染
  4. 保持稳定性:key 值在多次渲染中应该保持一致

不使用 key 的问题示例:

// 问题场景:列表中有输入框,删除中间项时
const items = ref([
  { id: 1, name: '项目1' },
  { id: 2, name: '项目2' },
  { id: 3, name: '项目3' }
])

// 没有 key:删除项目2后,项目3的输入框内容可能会出现在项目2的位置
// 有 key:每个项目的输入框状态会正确保持
1
2
3
4
5
6
7
8
9

性能优化建议:

<!-- ✅ 推荐:使用唯一 ID -->
<li v-for="item in items" :key="item.id">
  {{ item.name }}
</li>

<!-- ⚠️ 可接受:静态列表且不会改变顺序 -->
<li v-for="(color, index) in colors" :key="index">
  {{ color }}
</li>

<!-- ✅ 复杂列表:组合多个属性作为 key -->
<li v-for="item in items" :key="`${item.category}-${item.id}`">
  {{ item.name }}
</li>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script type="module" setup>
    import {ref,reactive} from 'vue'
    let parentMessage= ref('产品')
    let items =reactive([
      {
        id:'item1',
        message:"薯片"
      },
      {
        id:'item2',
        message:"可乐"
      }
    ])
</script>

<template>
  <div>
    <ul>
      <!-- :key不写也可以 -->
      <li v-for='item in items' :key='item.id'>
        {{ item.message }}
      </li>
    </ul>

    <ul>
      <!-- index表示索引,当然不是非得使用index这个单词 -->
      <li v-for="(item, index) in items" :key="index">
        {{ parentMessage }} - {{ index }} - {{ item.message }}
      </li>
    </ul>
  </div>
</template> 

<style scoped>
</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
  • 案例:实现购物车显示和删除购物项
<script type="module" setup>
    //引入模块
    import { reactive} from 'vue'
    //准备购物车数据,设置成响应数据
    const carts = reactive([{name:'可乐',price:3,number:10},{name:'薯片',price:6,number:8}])

    //计算购物车总金额
    function compute(){
      let count = 0;
      for(let index in carts){
        count += carts[index].price*carts[index].number;
      }
      return count;
    }
    //删除购物项方法
    function removeCart(index){
      carts.splice(index,1);
    }
</script>

<template>
    <div>
        <table>
           <thead>
               <tr>
                  <th>序号</th>
                  <th>商品名</th>
                  <th>价格</th>
                  <th>数量</th>
                  <th>小计</th>
                  <th>操作</th>
               </tr>
           </thead>
           <tbody v-if="carts.length > 0">
               <!-- 有数据显示-->
               <tr v-for="cart,index in carts" :key="index">
                  <th>{{ index+1 }}</th>
                  <th>{{ cart.name }}</th>
                  <th>{{ cart.price + '元' }}</th>
                  <th>{{ cart.number }}</th>
                  <th>{{ cart.price*cart.number  + '元'}}</th>
                  <th> <button @click="removeCart(index)">删除</button> </th>
               </tr>
           </tbody>
           <tbody v-else>
               <!-- 没有数据显示-->
               <tr>
                  <td colspan="6">购物车没有数据!</td>
               </tr>
           </tbody>
        </table>
        购物车总金额: {{ compute() }} 元
    </div>
</template> 

<style scoped>
</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

# 6.4 双向绑定

单向绑定和双向绑定:

  • 单向绑定:响应式数据的变化会更新 DOM 树,但是 DOM 树上用户的操作造成的数据改变不会同步更新到响应式数据
  • 双向绑定:响应式数据的变化会更新 DOM 树,但是 DOM 树上用户的操作造成的数据改变会同步更新到响应式数据
    • 用户通过表单标签才能够输入数据,所以双向绑定都是应用到表单标签上的,其他标签不行
    • v-model 专门用于双向绑定表单标签的 value 属性,语法为 v-model:value='',可以简写为 v-model=''(目前已不可使用全称)
    • v-model 还可以用于各种不同类型的输入,<textarea>、<select> 元素。

v-model 的原理:

v-model 实质上是一个语法糖,它背后是两个操作:

  1. v-bind 绑定 value 属性
  2. v-on 监听 input 事件更新数据
<!-- 以下两种写法等价 -->
<input v-model="message">

<input 
  :value="message" 
  @input="message = $event.target.value">
1
2
3
4
5
6

v-model 修饰符:

  1. .lazy:将 input 事件改为 change 事件,在失去焦点时才更新
    <!-- 在输入时不更新,失去焦点时才更新 -->
    <input v-model.lazy="message">
    
    1
    2
  2. .number:自动将输入转换为数字类型
    <!-- 输入会自动转换为 number 类型 -->
    <input v-model.number="age" type="number">
    
    1
    2
  3. .trim:自动去除首尾空格
    <!-- 自动去除首尾空格 -->
    <input v-model.trim="username">
    
    1
    2
  4. 修饰符组合使用:
    <!-- 同时使用多个修饰符 -->
    <input v-model.lazy.trim="message">
    <input v-model.number.lazy="age" type="number">
    
    1
    2
    3

不同表单元素的 v-model 使用:

<script setup>
import { ref } from 'vue'

const text = ref('')
const checked = ref(false)
const checkedNames = ref([])
const selected = ref('')
const multiSelected = ref([])
</script>

<template>
  <!-- 文本输入 -->
  <input v-model="text" placeholder="请输入">
  
  <!-- 多行文本 -->
  <textarea v-model="text"></textarea>
  
  <!-- 单个复选框 -->
  <input type="checkbox" v-model="checked">
  
  <!-- 多个复选框(绑定到数组) -->
  <input type="checkbox" value="选项1" v-model="checkedNames">
  <input type="checkbox" value="选项2" v-model="checkedNames">
  
  <!-- 单选按钮 -->
  <input type="radio" value="男" v-model="selected">
  <input type="radio" value="女" v-model="selected">
  
  <!-- 下拉单选 -->
  <select v-model="selected">
    <option value="">请选择</option>
    <option value="A">A</option>
    <option value="B">B</option>
  </select>
  
  <!-- 下拉多选 -->
  <select v-model="multiSelected" multiple>
    <option value="A">A</option>
    <option value="B">B</option>
    <option value="C">C</option>
  </select>
</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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

自定义组件的 v-model:

在 Vue 3 中,自定义组件可以通过 defineModel 或手动实现 v-model:

<!-- 子组件 CustomInput.vue -->
<script setup>
import { defineModel } from 'vue'

// Vue 3.4+ 推荐方式
const model = defineModel()

// 或者手动实现
// const props = defineProps(['modelValue'])
// const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input 
    v-model="model"
    placeholder="自定义输入组件">
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 父组件使用 -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const message = ref('')
</script>

<template>
  <CustomInput v-model="message" />
  <p>输入的内容:{{ message }}</p>
</template>
1
2
3
4
5
6
7
8
9
10
11
12

多个 v-model 绑定:

<!-- 子组件 UserForm.vue -->
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input v-model="firstName" placeholder="名">
  <input v-model="lastName" placeholder="姓">
</template>
1
2
3
4
5
6
7
8
9
10
<!-- 父组件 -->
<UserForm 
  v-model:first-name="first" 
  v-model:last-name="last" 
/>
1
2
3
4
5
<script type="module" setup>

  //引入模块
  import { reactive,ref} from 'vue' 
  let hbs = ref([]); //装爱好的值
  let user = reactive({username:null,password:null,introduce:null,pro:null})   
  function login(){
    alert(hbs.value);
    alert(JSON.stringify(user));
  }
  function clearx(){
    //user = {};// 这种写法会将数据变成非响应的,应该是user.username=""
    user.username=''
    user.password=''
    user.introduce=''
    user.pro=''
    hbs.value.splice(0,hbs.value.length);
  }
</script>

<template>
  <div>
      账号: <input type="text" placeholder="请输入账号!" v-model="user.username"> <br>
      密码: <input type="text" placeholder="请输入账号!" v-model="user.password"> <br>
      爱好: 
        吃 <input type="checkbox" name="hbs" v-model="hbs" value="吃"> 
        喝 <input type="checkbox" name="hbs" v-model="hbs" value="喝">
        玩 <input type="checkbox" name="hbs" v-model="hbs" value="玩">
        乐 <input type="checkbox" name="hbs" v-model="hbs" value="乐">
      <br>
      简介:<textarea v-model="user.introduce"></textarea>
      <br>
      籍贯:
          <select v-model="user.pro">
            <option value="1">黑</option>
            <option value="2">吉</option>
            <option value="3">辽</option>
            <option value="4">京</option>
            <option value="5">津</option>
            <option value="6">冀</option>
          </select> 
      <br>
      <button @click="login()">登录</button> 
      <button @click="clearx()">重置</button>
      <hr>
      显示爱好:{{ hbs }}
      <hr>
      显示用户信息:{{ user }}
  </div> 
</template> 

<style scoped>
</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

# 6.5 属性计算

模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。比如说,我们有这样一个包含嵌套数组的对象:

<script type="module" setup>
  //引入模块
  import { reactive,computed} from 'vue'
  const author = reactive({
    name: 'John Doe',
    books: [
      'Vue 2 - Advanced Guide',
      'Vue 3 - Basic Guide',
      'Vue 4 - The Mystery'
    ]
  })
</script>

<template>
  <div>
    <p>{{author.name}} Has published books?:</p>
    <span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
  </div>
</template> 

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 这里的模板看起来有些复杂。我们必须认真看好一会儿才能明白它的计算依赖于 author.books。更重要的是,如果在模板中需要不止一次这样的计算,我们可不想将这样的代码在模板里重复好多遍。

因此我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑。这是重构后的示例:

<script type="module" setup>
  //引入模块
  import { reactive,computed} from 'vue'
  const author = reactive({
    name: 'John Doe',
    books: [
      'Vue 2 - Advanced Guide',
      'Vue 3 - Basic Guide',
      'Vue 4 - The Mystery'
    ]
  })
  // 一个计算属性 ref
  const publishedBooksMessage = computed(() => {
    console.log("publishedBooksMessage")
    return author.books.length > 0 ? 'Yes' : 'No'
  })
  // 一个函数
  let hasBooks = ()=>{
    console.log("hasBooks")
    return author.books.length > 0?'Yes':'no'
  }
</script>

<template>
  <div>
    <p>{{author.name}} Has published books?:</p>
    <span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
    <span>{{ hasBooks() }}</span><!-- 调用方法,每个标签都会调用一次 -->
    <span>{{ hasBooks() }}</span>

    <p>{{author.name}} Has published books?:</p>
    <span>{{ publishedBooksMessage }}</span><!-- 属性计算,属性值不变时,多个个标签只会调用一次 -->
    <span>{{ publishedBooksMessage }}</span>
  </div>
</template> 

<style scoped>
</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
  • 我们在这里定义了一个计算属性 publishedBooksMessage。computed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过 publishedBooksMessage.value 访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value。
  • Vue 的计算属性会自动追踪响应式依赖。它会检测到 publishedBooksMessage 依赖于 author.books,所以当 author.books 改变时,任何依赖于 publishedBooksMessage 的绑定都会同时更新。

计算属性缓存 vs 方法

若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果!

# 6.6 数据监听器

计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。我们可以使用 watch (opens new window) 函数 (opens new window)在每次响应式状态发生变化时触发回调函数。

watch 主要用于以下场景:

  1. 数据变化时执行操作:当数据发生变化时需要执行相应的操作
  2. 条件触发:监听数据变化,当满足一定条件时触发相应操作
  3. 异步操作:在异步操作前或操作后需要执行相应的操作
  4. 副作用处理:需要执行 API 调用、手动操作 DOM 等

watch vs computed 的选择:

特性 watch computed
使用场景 副作用、异步操作 同步计算、数据转换
是否有返回值 无 有
缓存 无 有
适合场景 API 调用、DOM 操作 模板渲染、数据处理

监控响应式数据(watch):

<script type="module" setup>
  //引入模块
  import { ref,reactive,watch} from 'vue'
 
  let firstname=ref('')
  let lastname=reactive({name:''})
  let fullname=ref('')

  //监听一个ref响应式数据
  watch(firstname,(newValue,oldValue)=>{
    console.log(`${oldValue}变为${newValue}`)
    fullname.value=firstname.value+lastname.name
  })
  //监听reactive响应式数据的指定属性
  watch(()=>lastname.name,(newValue,oldValue)=>{
    console.log(`${oldValue}变为${newValue}`)
    fullname.value=firstname.value+lastname.name
  })
  //监听reactive响应式数据的所有属性(深度监视,一般不推荐)
  //deep:true 深度监视
  //immediate:true 深度监视在进入页面时立即执行一次
  watch(()=>lastname,(newValue,oldValue)=>{
    // 此时的newValue和oldValue一样,都是lastname
    console.log(newValue)
    console.log(oldValue)
    fullname.value=firstname.value+lastname.name
  },{deep:true,immediate:false})
</script>

<template>
  <div>
    全名:{{fullname}} <br>
    姓氏:<input type="text" v-model="firstname"> <br>
    名字:<input type="text" v-model="lastname.name" > <br>
  </div>
</template> 

<style scoped>
</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

watch 高级选项详解:

  1. deep: true - 深度监听:

    • 监听对象内部属性的变化
    • 会递归遍历对象的所有属性
    • 性能开销较大,谨慎使用
    const user = reactive({
      info: {
        name: '张三',
        age: 25
      }
    })
    
    // 深度监听:可以监听到 user.info.name 的变化
    watch(() => user.info, (newValue, oldValue) => {
      console.log('信息发生变化')
    }, { deep: true })
    
    // 不使用 deep:监听不到内部属性变化
    watch(() => user.info, (newValue, oldValue) => {
      console.log('不会执行') // user.info.name 变化时不会触发
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  2. immediate: true - 立即执行:

    • 创建监听器时立即执行一次回调
    • 适合需要初始化数据的场景
    const keyword = ref('')
    
    // 立即执行:组件加载时就会执行一次
    watch(keyword, (newValue) => {
      console.log('搜索关键词:', newValue)
      // 首次加载时也会执行搜索
      fetchSearchResults(newValue)
    }, { immediate: true })
    
    1
    2
    3
    4
    5
    6
    7
    8
  3. flush - 回调执行时机:

    • 'pre'(默认):组件更新前执行
    • 'post':组件更新后执行(可访问更新后的 DOM)
    • 'sync':同步执行(不推荐)
    watch(source, (newValue) => {
      // 可以访问更新后的 DOM
      const element = document.querySelector('#my-element')
      console.log(element.textContent)
    }, { flush: 'post' })
    
    1
    2
    3
    4
    5
  4. 停止监听:

    import { ref, watch } from 'vue'
    
    const count = ref(0)
    
    // watch 返回一个停止函数
    const stop = watch(count, (newValue) => {
      console.log('计数变化:', newValue)
    })
    
    // 当不再需要监听时,调用 stop 函数
    stop()
    
    // 此后 count 变化不会再触发回调
    count.value++ // 不会执行回调
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  5. 监听多个数据源:

    const firstName = ref('')
    const lastName = ref('')
    
    // 监听多个 ref
    watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
      console.log(`姓名从 ${oldFirst} ${oldLast} 变为 ${newFirst} ${newLast}`)
    })
    
    1
    2
    3
    4
    5
    6
    7
  6. 监听 getter 函数:

    const user = reactive({ name: '张三', age: 25 })
    
    // 监听 getter 函数的返回值
    watch(
      () => user.name + user.age,
      (newValue, oldValue) => {
        console.log(`组合值变化: ${oldValue} -> ${newValue}`)
      }
    )
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

watch 使用注意事项:

// ❌ 错误:监听 reactive 对象本身,newValue 和 oldValue 会是同一个对象
const state = reactive({ count: 0 })
watch(state, (newValue, oldValue) => {
  console.log(newValue === oldValue) // true
})

// ✅ 正确:监听 getter 函数
watch(() => state.count, (newValue, oldValue) => {
  console.log(newValue, oldValue) // 正常工作
})

// ✅ 正确:或者使用 deep 选项
watch(() => state, (newValue, oldValue) => {
  // 注意:newValue 和 oldValue 仍然是同一对象
}, { deep: true })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

实际应用场景:

// 场景 1:搜索防抖
const searchKeyword = ref('')
let timer = null

watch(searchKeyword, (newValue) => {
  clearTimeout(timer)
  timer = setTimeout(() => {
    fetchSearchResults(newValue)
  }, 300)
})

// 场景 2:表单验证
const email = ref('')
const emailError = ref('')

watch(email, (newValue) => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  emailError.value = emailRegex.test(newValue) ? '' : '邮箱格式不正确'
})

// 场景 3:数据同步到本地存储
const userSettings = ref({})

watch(userSettings, (newValue) => {
  localStorage.setItem('settings', JSON.stringify(newValue))
}, { deep: true })

// 场景 4:路由参数变化时重新加载数据
import { useRoute } from 'vue-router'

const route = useRoute()

watch(() => route.params.id, (newId) => {
  fetchDataById(newId)
}, { immediate: true })
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

监控响应式数据(watchEffect):

  • watchEffect 默认监听所有的响应式数据
<script type="module" setup>
  //引入模块
  import { ref,reactive,watch, watchEffect} from 'vue'
 
  let firstname=ref('')
  let lastname=reactive({name:''})
  let fullname=ref('')

  //监听所有响应式数据
  watchEffect(()=>{
    //直接在内部使用监听属性即可!不用外部声明
    //也不需要,即时回调设置!默认初始化就加载!
    console.log(firstname.value)
    console.log(lastname.name)
    fullname.value=`${firstname.value}${lastname.name}`
  })
</script>

<template>
  <div>
    全名:{{fullname}} <br>
    姓氏:<input type="text" v-model="firstname"> <br>
    名字:<input type="text" v-model="lastname.name" > <br>
  </div>
</template> 

<style scoped>
</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

watch vs. watchEffect 深度对比

特性 watch watchEffect
数据源 需要明确指定 自动追踪依赖
惰性 默认惰性(除非设置 immediate) 立即执行
访问旧值 可以访问 oldValue 不能访问
精确控制 可以精确控制监听的数据 自动追踪所有依赖
适用场景 需要访问旧值、精确控制 简单依赖追踪、初始化执行
调试难度 依赖明确,易调试 依赖隐式,调试困难
  • watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
    • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
    • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。

# 6.7 Vue 生命周期

# 6.7.1 生命周期简介

每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为 生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码!

生命周期图解:

image_elceCM4Wbp.webp

Vue 3 常见生命周期钩子:

钩子名称 触发时机 常见用途
onBeforeMount 组件挂载前 获取数据前的准备工作
onMounted 组件挂载完成 DOM 操作、发起 API 请求、初始化第三方库
onBeforeUpdate 响应式数据更新前 获取更新前的 DOM 状态
onUpdated DOM 更新完成后 访问更新后的 DOM(慎用)
onBeforeUnmount 组件卸载前 清理定时器、取消请求、移除事件监听
onUnmounted 组件卸载后 清理工作(如全局事件监听)
onActivated keep-alive 缓存组件激活时 恢复数据、重新请求
onDeactivated keep-alive 缓存组件停用时 暂停操作、保存状态
onErrorCaptured 捕获子组件错误 错误处理、日志上报

生命周期执行顺序:

1. setup() 执行
2. onBeforeMount() - 组件挂载前
3. onMounted() - 组件挂载完成
   ↓
4. onBeforeUpdate() - 数据更新前
5. onUpdated() - 数据更新后
   ↓ (重复 4-5)
6. onBeforeUnmount() - 组件卸载前
7. onUnmounted() - 组件卸载完成
1
2
3
4
5
6
7
8
9

常见应用场景:

  1. onMounted - 最常用:

    import { onMounted } from 'vue'
    
    onMounted(() => {
      // 场景 1:发起 API 请求
      fetchUserData()
      
      // 场景 2:DOM 操作
      const element = document.querySelector('.my-element')
      element.focus()
      
      // 场景 3:初始化第三方库
      const chart = new Chart(canvas, options)
      
      // 场景 4:添加事件监听
      window.addEventListener('resize', handleResize)
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  2. onBeforeUnmount - 清理工作:

    import { onBeforeUnmount } from 'vue'
    
    let timer = null
    
    onMounted(() => {
      timer = setInterval(() => {
        console.log('tick')
      }, 1000)
    })
    
    onBeforeUnmount(() => {
      // 清理定时器
      if (timer) {
        clearInterval(timer)
      }
      
      // 移除事件监听
      window.removeEventListener('resize', handleResize)
      
      // 取消未完成的请求
      abortController.abort()
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
  3. onUpdated - 访问更新后的 DOM:

    import { onUpdated } from 'vue'
    
    onUpdated(() => {
      // ❗ 注意:避免在 onUpdated 中修改响应式数据
      // 可能导致无限循环
      
      // ✅ 适合:访问更新后的 DOM
      const height = element.offsetHeight
      adjustLayout(height)
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

注意事项:

  1. 不要在 onUpdated 中修改响应式数据:

    // ❌ 错误:会导致无限循环
    onUpdated(() => {
      count.value++ // 会触发新的更新
    })
    
    1
    2
    3
    4
  2. 生命周期钩子中的 this:

    • 在 <script setup> 中不需要 this
    • 钩子函数自动绑定到组件实例
  3. 异步注册钩子:

    // ❌ 错误:异步注册钩子无效
    setTimeout(() => {
      onMounted(() => {
        // 这不会工作
      })
    }, 100)
    
    // ✅ 正确:同步注册
    onMounted(() => {
      // 可以在钩子内部使用异步
      setTimeout(() => {
        console.log('延迟执行')
      }, 100)
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  4. 多次注册同一钩子:

    // ✅ 允许:会按顺序执行
    onMounted(() => {
      console.log('第一个')
    })
    
    onMounted(() => {
      console.log('第二个')
    })
    
    1
    2
    3
    4
    5
    6
    7
    8

常见问题及解决方案:

  1. 组件卸载时未清理资源:

    // ❌ 问题:定时器未清理,导致内存泄漏
    onMounted(() => {
      setInterval(() => {
        console.log('tick')
      }, 1000)
    })
    
    // ✅ 解决:在卸载前清理
    let timer = null
    onMounted(() => {
      timer = setInterval(() => {
        console.log('tick')
      }, 1000)
    })
    onBeforeUnmount(() => {
      clearInterval(timer)
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  2. 在挂载前访问 DOM:

    // ❌ 错误:setup 中 DOM 还未创建
    const element = document.querySelector('.my-element') // null
    
    // ✅ 正确:在 onMounted 中访问
    onMounted(() => {
      const element = document.querySelector('.my-element')
    })
    
    1
    2
    3
    4
    5
    6
    7
  3. 父子组件生命周期执行顺序:

    父组件 onBeforeMount
      ↓
    子组件 onBeforeMount
    子组件 onMounted
      ↓
    父组件 onMounted
    
    // 更新时
    父组件 onBeforeUpdate
      ↓
    子组件 onBeforeUpdate
    子组件 onUpdated
      ↓
    父组件 onUpdated
    
    // 卸载时
    父组件 onBeforeUnmount
      ↓
    子组件 onBeforeUnmount
    子组件 onUnmounted
      ↓
    父组件 onUnmounted
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

# 6.7.2 生命周期案例

<script setup>

    import {ref,onUpdated,onMounted,onBeforeUpdate} from 'vue'
    
    let message =ref('hello')
   
    // 挂载完毕生命周期
    onMounted(()=>{
      console.log('-----------onMounted---------')
      let span1 =document.getElementById("span1")
      console.log(span1.innerText)
    })
    // 更新前生命周期
    onBeforeUpdate(()=>{
      console.log('-----------onBeforeUpdate---------')
      console.log(message.value)
      let span1 =document.getElementById("span1")
      console.log(span1.innerText)
    })
    // 更新完成生命周期
    onUpdated(()=>{
      console.log('-----------onUpdated---------')
      let span1 =document.getElementById("span1")
      console.log(span1.innerText)
    })
</script>

<template>
  <div>
    <span id="span1" v-text="message"></span> <br>
    <input type="text" v-model="message">
  </div>
  
</template>

<style scoped>
</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

# 6.8 Vue 组件

# 6.8.1 组件基础

什么是组件

组件(Component)是 Vue.js 最核心的功能之一,它允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。简单来说,组件就是实现应用中局部功能代码和资源的集合。

在实际应用中,组件常常被组织成层层嵌套的树状结构:

image_9dCv8raLh-.webp

这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。

组件化 vs 模块化

  • 组件化:对 js/css/html 统一封装,这是 Vue 中的概念。一个组件包含了模板(HTML)、逻辑(JavaScript)和样式(CSS)。
  • 模块化:对 JavaScript 代码的统一封装,这是 ES6 中的概念。
  • 关系:组件化中,对 JavaScript 部分代码的处理使用 ES6 中的模块化。

组件的核心优势

  1. 可复用性:编写一次,多处使用
  2. 可维护性:功能独立,便于定位和修复问题
  3. 代码组织:将复杂的应用拆分为多个简单的部分
  4. 团队协作:不同成员可以独立开发不同的组件

组件通信机制概述

在 Vue 中,组件之间的数据传递主要有以下几种方式:

  • 父传子:通过 props 向下传递数据
  • 子传父:通过 $emit 触发自定义事件向上传递数据
  • 兄弟组件:通过共同的父组件中转数据
  • 跨层级通信:使用 provide/inject 或状态管理工具(如 Pinia)

组件的生命周期(简介)

Vue 组件从创建到销毁会经历一系列的生命周期钩子:

生命周期钩子 触发时机 常见用途
onMounted 组件挂载完成后 发送 AJAX 请求、操作 DOM
onUpdated 组件更新后 监听数据变化后的操作
onUnmounted 组件卸载前 清理定时器、取消订阅

笔记

在 Vue 3 的 Composition API 中,生命周期钩子需要通过 import 引入后使用。

# 6.8.2 组件化入门案例

案例需求: 创建一个页面,包含头部和菜单以及内容显示区域,每个区域使用独立组建!

1686885192862

1. 准备 Vue 项目

npm create vite
cd vite项目
npm install
1
2
3

2. 安装相关依赖

npm install sass
npm install bootstrap
1
2

3. 项目结构说明

vite项目/
├── src/
│   ├── components/       # 组件目录
│   │   ├── Header.vue    # 头部组件
│   │   ├── Navigator.vue # 导航菜单组件
│   │   └── Content.vue   # 内容展示组件
│   ├── App.vue           # 根组件(入口)
│   └── main.js           # 入口文件
└── package.json
1
2
3
4
5
6
7
8
9

笔记

在 VS Code 中,建议安装 Volar 插件(Vue 3 官方推荐),以获得更好的 .vue 文件语法高亮和智能提示。

4. 创建子组件

在 src/components 文件夹下创建以下组件:

  • Header.vue(头部组件)

    <script setup type="module">
    </script>
    
    <template>
        <div>
            欢迎: xx <a href="#">退出登录</a>
        </div>
    </template>
    
    <style>
    </style>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • Navigator.vue(导航菜单组件)

    <script setup type="module">
    </script>
    
    <template>
        <!-- 推荐每个组件只有一个根标签 -->
        <div>
           <ul>
              <li>学员管理</li>
              <li>图书管理</li>
              <li>请假管理</li>
              <li>考试管理</li>
              <li>讲师管理</li>
           </ul>
        </div>
    </template>
    
    <style>
    </style>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
  • Content.vue(内容展示组件)

    <script setup type="module">
    </script>
    
    <template>
        <div>
            展示的主要内容!
        </div>
    </template>
    
    <style>
    </style>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

5. 配置 App.vue 入口组件

App.vue 作为根组件,负责引入并使用上述子组件:

<script setup>
    // 导入子组件
    import Header  from './components/Header.vue'
    import Navigator  from './components/Navigator.vue'
    import Content  from './components/Content.vue'
</script>

<template>
  <div>
     <!-- 使用子组件,并添加类名用于样式控制 -->
     <Header class="header"></Header>
     <Navigator class="navigator"></Navigator>
     <Content class="content"></Content>
  </div>
</template>

<style scoped>
    /* scoped 表示样式仅在当前组件生效,不会影响其他组件 */
    .header{
       height: 80px;
       border: 1px solid red;
    }

    .navigator{
      width: 15%;
      height: 800px;
      display: inline-block;
      border: 1px blue solid;
      float: left;
    }

    .content{
      width: 83%;
      height: 800px;
      display: inline-block;
      border: 1px goldenrod solid;
      float: right;
    }
</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

6. 组件注册方式说明

在上述案例中,我们使用的是 局部注册 方式:

注册方式 特点 使用场景
局部注册 在 <script setup> 中 import 后直接在模板中使用 推荐方式,按需引入,减小打包体积
全局注册 通过 app.component('组件名', 组件) 注册 频繁使用的公共组件(如图标、按钮)

局部注册示例(已使用):

import Header from './components/Header.vue'
// 直接在 <template> 中使用 <Header />
1
2

全局注册示例(了解):

// main.js
import Header from './components/Header.vue'
app.component('Header', Header)
// 所有组件都可以使用 <Header />,无需 import
1
2
3
4

7. scoped 样式作用域详解

在 <style scoped> 中定义的样式仅对当前组件生效:

<!-- App.vue -->
<style scoped>
  .header { color: red; }  /* 只影响 App.vue 中的 .header */
</style>
1
2
3
4

原理:Vue 会为组件添加唯一的 data-v-xxx 属性,样式选择器会自动添加该属性限定作用域。

注意事项:

  • 如果需要全局样式,去掉 scoped 或在单独的 CSS 文件中定义
  • scoped 样式不会影响子组件的根元素(可以通过 :deep() 深度选择器穿透) 8. 启动测试
npm run dev
1

# 6.8.3 组件之间传递数据

组件通信方式概述

Vue 组件之间的数据传递遵循 单向数据流 原则:

  • 父传子(Props Down):父组件通过 props 向子组件传递数据
  • 子传父(Events Up):子组件通过 $emit 触发事件向父组件传递数据
  • 兄弟组件:通过共同的父组件作为中介进行数据中转

注意

Props 是只读的,子组件不应直接修改父组件传递的 props 值,这会破坏单向数据流。

# 6.8.3.1 父传子(Props)

核心概念

Vue 3 中父组件通过 props 向子组件传递数据,具体流程:

  1. 父组件:定义响应式数据,在模板中通过自定义属性(v-bind 或 : 简写)传递给子组件
  2. 子组件:使用 defineProps() 声明接收的 props,在模板中直接使用

响应式特性:父组件传递的值发生变化时,子组件中的值会自动更新(单向数据流)。

  • 父组件代码:App.vue
<script setup>
  import Son from './components/Son.vue'
  import { ref } from 'vue'

  // 定义响应式数据
  let message = ref('parent data!')
  let title = ref(42)

  // 修改数据的方法
  function changeMessage() {
    message.value = '修改数据!'
    title.value++
  }
</script>

<template>
  <div>
    <h2>父组件数据:{{ message }}</h2>
    <hr>
    <!-- 通过 v-bind(简写 :)向子组件传递 props -->
    <Son :message="message" :title="title"></Son>
    <hr>
    <button @click="changeMessage">点击更新数据</button>
  </div>
</template>

<style scoped>
</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
  • 子组件代码:Son.vue
<script setup type="module">
    import { defineProps } from 'vue'
    
    // 使用 defineProps() 声明接收的 props
    defineProps({
        message: String,  // 指定类型
        title: Number
    })
</script>

<template>
    <div>
        <div>子组件接收到的 message:{{ message }}</div>
        <div>子组件接收到的 title:{{ title }}</div>
    </div>
</template>

<style>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Props 验证规则

除了基本的类型验证,还可以进行更严格的验证:

defineProps({
    // 基础类型验证
    message: String,
    
    // 多个可能的类型
    propA: [String, Number],
    
    // 必填项
    propB: {
        type: String,
        required: true
    },
    
    // 带默认值
    propC: {
        type: Number,
        default: 100
    },
    
    // 对象或数组的默认值必须从一个工厂函数返回
    propD: {
        type: Object,
        default() {
            return { msg: 'hello' }
        }
    },
    
    // 自定义验证函数
    propE: {
        validator(value) {
            return ['success', 'warning', 'danger'].includes(value)
        }
    }
})
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

常用类型:String、Number、Boolean、Array、Object、Date、Function、Symbol

Props 注意事项

  1. 命名规范:
    • 父组件传递:使用 kebab-case(短横线)::user-name="name"
    • 子组件接收:使用 camelCase(驼峰):defineProps({ userName: String })
  2. 单向数据流:
    • Props 是 只读 的,子组件不应直接修改 props
    • 如需修改,应将 props 赋值给本地 ref 或 reactive 变量
// 错误示例:直接修改 props
const props = defineProps({ count: Number })
props.count++  // ✗ 会报警告

// 正确示例:使用本地变量
const props = defineProps({ count: Number })
const localCount = ref(props.count)
localCount.value++  // ✓ 正确
1
2
3
4
5
6
7
8
# 6.8.3.2 子传父(Emits)

核心概念

子组件通过 defineEmits() 定义自定义事件,然后通过 emit 触发事件向父组件传递数据。

  • 父组件代码:App.vue
<script setup>
    import Son from './components/Son.vue'
    import { ref } from 'vue'

    let pdata = ref('')

    // 接收子组件 add 事件的处理函数
    const padd = (data) => {
        pdata.value = data
    }

    // 接收子组件 sub 事件的处理函数
    const psub = (data) => {
        pdata.value = data
    }
</script>

<template>
    <div>
        <!-- 声明@事件名应该等于子模块对应事件名!调用方法可以是当前自定义!-->
        <Son @add="padd" @sub="psub"></Son>
        <hr>
        {{ pdata }}
    </div>
</template>

<style>
</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
  • 子组件代码:Son.vue
<script setup>
    import { ref, defineEmits } from 'vue'

    // 1. 使用 defineEmits() 定义要发送给父组件的事件(可以多个)
    let emits = defineEmits(['add', 'sub'])

    let data = ref(1)

    function sendMsgToParent() {
        // 2. 触发父组件对应的事件,传递数据
        emits('add', 'add data!' + data.value)
        emits('sub', 'sub data!' + data.value)
        
        data.value++
    }
</script>

<template>
    <div>
        <button @click="sendMsgToParent">发送消息给父组件</button>
    </div>
</template>
  
<style>
</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

Emits 验证规则

和 Props 类似,也可以对 emits 进行验证:

const emits = defineEmits({
    // 无验证
    click: null,
    
    // 验证 submit 事件
    submit: ({ email, password }) => {
        if (email && password) {
            return true  // 验证通过
        } else {
            console.warn('无效的提交事件载荷!')
            return false  // 验证失败
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

自定义事件命名规范

  1. 事件名使用 kebab-case(短横线形式):
    <!-- 推荐 -->
    <Son @update-value="handleUpdate" />
    
    <!-- 不推荐 -->
    <Son @updateValue="handleUpdate" />
    
    1
    2
    3
    4
    5
  2. 事件名不要与原生 HTML 事件名冲突(如 click、focus 等)
  3. 语义化命名:使用动词开头,如 update-user、delete-item 等
# 6.8.3.3 兄弟传参

核心思路

兄弟组件之间无法直接通信,需要通过 共同的父组件 作为中介进行数据中转:

  1. 组件 A(发送方):通过 emit 向父组件发送数据
  2. 父组件:接收数据并存储到自己的响应式变量中
  3. 组件 B(接收方):通过 props 接收父组件传递的数据

  • Navigator.vue:发送数据到 App.vue
<script setup type="module">
    import { defineEmits } from 'vue'
    const emits = defineEmits(['sendMenu'])
    // 触发事件,向父容器发送数据
    function send(data) {
        emits('sendMenu', data)
    }
</script>

<template>
    <div>
       <ul>
          <li @click="send('学员管理')">学员管理</li>
          <li @click="send('图书管理')">图书管理</li>
          <li @click="send('请假管理')">请假管理</li>
          <li @click="send('考试管理')">考试管理</li>
          <li @click="send('讲师管理')">讲师管理</li>
       </ul>
    </div>
</template>

<style>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  • App.vue:作为中间人,接收 Navigator 数据并传递给 Content
<script setup>
  import Header from './components/Header.vue'
  import Navigator from './components/Navigator.vue'
  import Content from './components/Content.vue'
  import { ref } from "vue"
  
  // 定义响应式变量接收 Navigator 传递的参数
  var navigator_menu = ref('请点击菜单')

  // 接收 Navigator 组件发送的数据
  const receiver = (data) => {
    navigator_menu.value = data
  }
</script>

<template>
  <div>
      <hr>
      当前选中:{{ navigator_menu }}
      <hr>
     <Header class="header"></Header>
     <!-- 监听 Navigator 的 sendMenu 事件 -->
     <Navigator @sendMenu="receiver" class="navigator"></Navigator>
     <!-- 向 Content 传递数据 -->
     <Content class="content" :message="navigator_menu"></Content>
    </div>
</template>

<style scoped>
    .header{
       height: 80px;
       border: 1px solid red;
    }

    .navigator{
      width: 15%;
      height: 800px;
      display: inline-block;
      border: 1px blue solid;
      float: left;
    }

    .content{
      width: 83%;
      height: 800px;
      display: inline-block;
      border: 1px goldenrod solid;
      float: right;
    }
</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
  • Content.vue:接收父组件传递的数据
<script setup type="module">
    import { defineProps } from 'vue'
    
    // 接收父组件 App.vue 传递的 message
    defineProps({
        message: String
    })
</script>

<template>
    <div>
        <h3>展示的主要内容!</h3>
        <hr>
        <p>当前选中的菜单:{{ message }}</p>
    </div>
</template>

<style>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

兄弟传参总结

数据流转过程:

Navigator (发送方)
    ↓ emit('sendMenu', data)
App (父组件/中介)
    ↓ :message="navigator_menu"
Content (接收方)
1
2
3
4
5

适用场景:

  • 简单的兄弟组件通信
  • 嵌套层级不深的组件

复杂场景推荐:

  • 使用 Pinia(Vue 3 官方状态管理工具)
  • 使用 provide/inject(跨层级通信)

# 七、Vue3 路由机制 Router

# 7.1 路由简介

什么是路由:

  • 定义:路由就是根据不同的 URL 地址展示不同的内容或页面。
  • 通俗理解:路由就像是一个地图,我们要去不同的地方,需要通过不同的路线进行导航。

路由的作用:

  1. 无刷新切换页面:在单页应用程序(SPA)中,路由可以实现不同视图之间的无刷新切换,提升用户体验
  2. 权限控制:路由可以实现页面的认证和权限控制,保护用户的隐私和安全
  3. 历史记录管理:路由可以利用浏览器的前进与后退,帮助用户更好地回到之前访问过的页面
  4. URL 语义化:通过路由可以让 URL 更具语义,方便分享和收藏

Vue Router 简介:

Vue Router 是 Vue.js 的官方路由管理器,与 Vue.js 核心深度集成,使构建单页面应用变得轻而易举。

核心功能:

  • 嵌套路由映射
  • 动态路由选择
  • 模块化、基于组件的路由配置
  • 路由参数、查询、通配符
  • 细粒度的导航控制
  • 自动激活的 CSS 类

当前版本:Vue 3 配合使用 Vue Router 4

# 7.2 路由入门案例

案例需求:进入程序,显示首页!点击首页和列表可以进行页面切换。

创建项目和导入路由依赖

npm create vite              # 创建项目
cd 项目文件夹               # 进入项目文件夹
npm install                  # 安装项目需求依赖
npm install vue-router@4 --save  # 安装 vue-router 4 版本
1
2
3
4

笔记

Vue 3 必须使用 Vue Router 4,而不是 Vue Router 3。

准备页面和组件

  • components/Home.vue
<script setup>
</script>

<template>
    <div>
        <h1>Home页面</h1>
    </div>
</template>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
  • components/List.vue
<script setup>
</script>

<template>
    <div>
        <h1>List页面</h1>
    </div>
</template>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
  • components/Add.vue
<script setup>
</script>

<template>
    <div>
        <h1>Add页面</h1>
    </div>
</template>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
  • components/Update.vue
<script setup>
</script>

<template>
    <div>
        <h1>Update页面</h1>
    </div>
</template>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
  • App.vue
<script setup>
</script>

<template>
    <div>
      <h1>App页面</h1>
      <hr/>
      <!-- 路由链接:使用 router-link 组件 -->
      <router-link to="/">home页</router-link> |
      <router-link to="/list">list页</router-link> |
      <router-link to="/add">add页</router-link> |
      <router-link to="/update">update页</router-link>
      <hr/>
      
      <!-- 路由视图:组件展示位置 -->
      <h3>默认展示位置:</h3>
      <router-view></router-view>
      <h3>Home视图展示:</h3>
      <router-view name="homeView"></router-view>
      <h3>List视图展示:</h3>
      <router-view name="listView"></router-view>
      <h3>Add视图展示:</h3>
      <router-view name="addView"></router-view>
      <h3>Update视图展示:</h3>
      <router-view name="updateView"></router-view>
    </div>
</template>

<style scoped>
</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

router-link 和 router-view 说明

  • <router-link>:声明式路由链接,渲染为 <a> 标签,点击时不会刷新页面
  • <router-view>:路由视图占位符,用于展示匹配到的组件

命名视图应用场景:

  • 同一个路由需要展示多个组件
  • 布局复杂的页面(如同时显示侧边栏和主内容)

准备路由配置

  • src/routers/router.js
// 导入路由创建的相关方法
import { createRouter, createWebHashHistory } from 'vue-router'

// 导入 Vue 组件
import Home from '../components/Home.vue'
import List from '../components/List.vue'
import Add from '../components/Add.vue'
import Update from '../components/Update.vue'

// 创建路由对象,声明路由规则
const router = createRouter({
    // 使用 Hash 模式(URL 中带 # 号)
    history: createWebHashHistory(),
    routes: [
        {
            path: '/',
            // 使用多个命名视图
            components: {
                default: Home,      // 默认视图位置
                homeView: Home      // name="homeView" 的视图位置
            }
        },
        {
            path: '/list',
            components: {
                listView: List
            }
        },
        {
            path: '/add',
            components: {
                addView: Add
            }
        },
        {
            path: '/update',
            components: {
                updateView: Update
            }
        },
    ]
})

// 导出路由对象
export default router
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

路由模式对比:Hash vs History

特性 Hash 模式 History 模式
URL 形式 http://example.com/#/user http://example.com/user
原理 通过 URL 的 hash (#) 部分 基于 HTML5 History API
兼容性 兼容所有浏览器 需要 IE10+
服务器配置 无需配置 需要配置回退到 index.html
SEO 友好 较差 较好
创建方法 createWebHashHistory() createWebHistory()

使用建议:

  • Hash 模式:适用于静态部署、不需要 SEO 的场景
  • History 模式:适用于需要 SEO、URL 美观的场景
// History 模式示例
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
    history: createWebHistory(),  // 使用 History 模式
    routes: [/*...*/]
})
1
2
3
4
5
6
7

注意

使用 History 模式时,需要在服务器上配置,将所有请求都回退到 index.html,否则刷新页面会出现 404 错误。

main.js 引入 router 配置

  • 修改文件:main.js(入口文件)
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

// 导入路由模块
import router from './routers/router.js'

let app = createApp(App)

// 注册路由对象
app.use(router)

// 挂载应用
app.mount("#app")
1
2
3
4
5
6
7
8
9
10
11
12
13
14

启动测试

npm run dev
1

浏览器访问:http://localhost:5173,点击不同的路由链接,观察视图切换。

# 7.3 路由重定向

重定向的作用

路由重定向(redirect)可以将一个路由地址重定向到另一个路由地址。

应用场景:

  • 旧路由路径迁移到新路径
  • 多个路径指向同一个页面
  • 默认页面跳转

案例示例

修改案例:访问 /list 和 /showAll 都定向到 List.vue

  • router.js
import { createRouter, createWebHashHistory } from 'vue-router'

import Home from '../components/Home.vue'
import List from '../components/List.vue'
import Add from '../components/Add.vue'
import Update from '../components/Update.vue'

const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: '/',
            components: {
                default: Home,
                homeView: Home
            }
        },
        {
            path: '/list',
            components: {
                listView: List
            }
        },
        {
            // 重定向:访问 /showAll 时自动跳转到 /list
            path: '/showAll',
            redirect: '/list'
        },
        {
            path: '/add',
            components: {
                addView: Add
            }
        },
        {
            path: '/update',
            components: {
                updateView: Update
            }
        },
    ]
})

export default router
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

重定向的多种方式

// 1. 直接重定向到路径
{
    path: '/home',
    redirect: '/'
}

// 2. 重定向到命名路由
{
    path: '/home',
    redirect: { name: 'HomePage' }
}

// 3. 使用函数动态决定重定向目标
{
    path: '/search/:query',
    redirect: to => {
        // to 是目标路由对象
        return { path: '/list', query: { q: to.params.query } }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

重定向 vs 别名

特性 重定向 (redirect) 别名 (alias)
URL 变化 URL 会变为重定向后的地址 URL 不变
导航守卫 会触发 不会触发
使用场景 路径迁移、默认跳转 同一组件多个 URL

别名示例:

{
    path: '/list',
    component: List,
    alias: ['/showAll', '/display']  // 访问这些路径都显示 List 组件
}
1
2
3
4
5

# 7.4 编程式路由(useRouter)

普通路由

  • <router-link to="/list">list页</router-link>这种路由,to 中的内容目前是固定的,点击后只能切换 /list 对象组件(声明式路由)

编程式路由

  • 通过 useRouter,动态决定向那个组件切换的路由
  • 在 Vue 3 和 Vue Router 4 中,你可以使用 useRouter 来实现动态路由(编程式路由)
  • 这里的 useRouter 方法返回的是一个 router 对象,你可以用它来做如导航到新页面、返回上一页面等操作。

案例需求:通过普通按钮配合事件绑定实现路由页面跳转,不直接使用 router-link 标签

  • App.vue
<script setup type="module">
  import {useRouter} from 'vue-router'
  import {ref} from 'vue'
  //创建动态路由对象
  let router = useRouter()

  let routePath = ref('')
  let showList = ()=>{
      // 编程式路由
      // 直接push一个路径
      //router.push('/list')
      // push一个带有path属性的对象
      router.push({path:'/list'})
  }
</script>

<template>
    <div>
      <h1>App页面</h1>
      <hr/>
        <!-- 路由的链接 -->
        <router-link to="/">home页</router-link> <br>
        <router-link to="/list">list页</router-link> <br>
        <router-link to="/showAll">showAll页</router-link> <br>
        <router-link to="/add">add页</router-link> <br>
        <router-link to="/update">update页</router-link> <br>
        <!-- 动态输入路径,点击按钮,触发单击事件的函数,在函数中通过编程式路由切换页面 -->
        <button @click="showList()">showList</button> <br>
      <hr/>
      <!-- 路由链接对应视图的展示位置 -->
      <hr>
      默认展示位置:<router-view></router-view>
      <hr>
      Home视图展示:<router-view name="homeView"></router-view>
      <hr>
      List视图展示:<router-view name="listView"></router-view>
      <hr>
      Add视图展示:<router-view name="addView"></router-view>
      <hr>
      Update视图展示:<router-view name="updateView"></router-view>
    </div>
</template>

<style scoped>
</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

# 7.5 路由传参(useRoute)

传参方式概述

Vue Router 提供两种主要的传参方式:

1.路径参数(Params)

  • 参数作为 URL 路径的一部分
  • 例如:/showDetail/1,其中 1 是动态参数
  • 适用场景:资源 ID、必须参数

2. 查询参数(Query)

  • 参数以键值对形式跟在 URL 后面
  • 例如:/showDetail?id=1&language=Java
  • 适用场景:可选参数、搜索条件、分页信息

params vs query 对比

特性 Params(路径参数) Query(查询参数)
URL 形式 /user/123 /user?id=123
参数可见性 更简洁 更明确
SEO 友好 较好 一般
刷新保留 需要配置 自动保留
使用场景 资源 ID、必须参数 可选参数、筛选条件

useRoute 的使用

useRoute() 返回当前路由对象,用于获取路由信息(如参数、路径等)。

常用属性:

  • route.params:获取路径参数
  • route.query:获取查询参数
  • route.path:当前路径
  • route.name:路由名称
  • route.meta:路由元信息

案例需求:切换到 ShowDetail.vue 组件时,向该组件通过路由传递参数(分别演示 params 和 query 两种方式)。

  • 修改 App.vue 文件
<script setup type="module">
  import { useRouter } from 'vue-router'

  // 获取路由对象
  let router = useRouter()
  
  // 路径参数传递方法(使用 params)
  let showDetail = (id, language) => {
      // 方式1:字符串拼接(简单但不推荐)
      // router.push(`/showDetail/${id}/${language}`)
      
      // 方式2:使用命名路由 + params(推荐)
      router.push({ name: "showDetail", params: { id: id, language: language } })
  }
  
  // 查询参数传递方法(使用 query)
  let showDetail2 = (id, language) => {
      router.push({ path: "/showDetail2", query: { id: id, language: language } })
  }
</script>

<template>
    <div>
      <h1>App页面</h1>
      <hr/>
      <!-- 路径参数示例 -->
      <h3>路径参数(Params):</h3>
      <router-link to="/showDetail/1/JAVA">声明式:showDetail路径传参</router-link><br>
      <button @click="showDetail(1, 'JAVA')">编程式:showDetail路径传参</button>
      <hr/>
      <!-- 查询参数示例 -->
      <h3>查询参数(Query):</h3>
      <router-link :to="{ path: '/showDetail2', query: { id: 1, language: 'Java' } }">
        声明式:showDetail2查询传参
      </router-link><br>
      <button @click="showDetail2(1, 'JAVA')">编程式:showDetail2查询传参</button>
      <hr>
      <!-- 路由视图 -->
      <h3>Params 方式展示:</h3>
      <router-view name="showDetailView"></router-view>
      
      <h3>Query 方式展示:</h3>
      <router-view name="showDetailView2"></router-view>
    </div>
</template>

<style scoped>
</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
  • 修改 router.js 增加路径参数占位符
import { createRouter, createWebHashHistory } from 'vue-router'

import ShowDetail from '../components/ShowDetail.vue'
import ShowDetail2 from '../components/ShowDetail2.vue'

// 创建路由对象,声明路由规则
const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            // 路径参数:使用 :参数名 作为占位符
            path: '/showDetail/:id/:language',
            // 命名路由:在编程式导航中使用 params 时必须通过 name 跳转
            name: 'showDetail',
            components: {
                showDetailView: ShowDetail
            }
        },
        {
            // 查询参数:路径不需要占位符
            path: '/showDetail2',
            components: {
                showDetailView2: ShowDetail2
            }
        },
    ]
})

export default router
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

注意

使用 params 传参时,必须使用 命名路由(通过 name 跳转),不能使用 path。使用 query 传参则没有此限制。

  • ShowDetail.vue 通过 useRoute 获取路径参数
<script setup type="module">
    import { useRoute } from 'vue-router'
    import { onUpdated, ref } from 'vue'
    
    // 获取当前的 route 对象
    let route = useRoute()
    let languageId = ref(0)
    let languageName = ref('')
    
    // 使用生命周期钩子更新响应式数据
    onUpdated(() => {
        // 从 route.params 获取路径参数
        languageId.value = route.params.id
        languageName.value = route.params.language
    })
</script>

<template>
    <div>
        <h1>ShowDetail页面(Params方式)</h1>
        <!-- 方式1:直接在模板中使用 route.params -->
        <h3>编号 {{ route.params.id }}: {{ route.params.language }} 是世界上最好的语言</h3>
        <!-- 方式2:使用响应式变量 -->
        <h3>编号 {{ languageId }}: {{ languageName }} 是世界上最好的语言</h3>
    </div>
</template>

<style scoped>
</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
  • ShowDetail2.vue 通过 useRoute 获取查询参数
<script setup type="module">
    import { useRoute } from 'vue-router'
    import { onUpdated, ref } from 'vue'
    
    // 获取当前的 route 对象
    let route = useRoute()
    let languageId = ref(0)
    let languageName = ref('')
    
    // 使用生命周期钩子更新响应式数据
    onUpdated(() => {
        // 从 route.query 获取查询参数
        languageId.value = route.query.id
        languageName.value = route.query.language
    })
</script>

<template>
    <div>
        <h1>ShowDetail2页面(Query方式)</h1>
        <!-- 方式1:直接在模板中使用 route.query -->
        <h3>编号 {{ route.query.id }}: {{ route.query.language }} 是世界上最好的语言</h3>
        <!-- 方式2:使用响应式变量 -->
        <h3>编号 {{ languageId }}: {{ languageName }} 是世界上最好的语言</h3>
    </div>
</template>

<style scoped>
</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

# 7.6 路由守卫

什么是路由守卫

路由守卫是在路由跳转过程中执行的钩子函数,用于控制路由的访问权限、执行特定逻辑等。

常见应用场景:

  • 登录验证:未登录用户重定向到登录页
  • 权限控制:不同角色访问不同页面
  • 页面跳转确认:离开页面前提示保存
  • 数据预加载:进入页面前加载数据
  • 页面标题设置:动态设置页面标题

路由守卫的类型

Vue Router 提供三种类型的路由守卫:

1. 全局守卫(对所有路由生效)

  • beforeEach:全局前置守卫
  • beforeResolve:全局解析守卫
  • afterEach:全局后置钩子

2. 路由独享守卫(在路由配置中定义)

  • beforeEnter:进入该路由前

3. 组件内守卫(在组件内定义)

  • onBeforeRouteEnter:进入组件前
  • onBeforeRouteUpdate:路由更新时
  • onBeforeRouteLeave:离开组件前

全局守卫基本用法

守卫代码应写在 router.js 文件中:

// 全局前置守卫
router.beforeEach((to, from, next) => {
    // to: 即将要进入的目标路由对象
    // from: 当前导航正要离开的路由对象
    // next: 控制导航的函数
    
    // next() - 放行,继续导航
    // next(false) - 中断导航
    // next('/login') - 重定向到其他路由
    
    console.log('从', from.path, '到', to.path)
    
    // 示例:简单的权限控制
    if (to.path === '/admin' && !isLoggedIn()) {
        next('/login')  // 未登录重定向到登录页
    } else {
        next()  // 放行
    }
})

// 全局后置钩子(不接收 next 参数)
router.afterEach((to, from) => {
    // 可用于日志记录、页面标题设置等
    console.log(`导航完成:从 ${from.path} 到 ${to.path}`)
    document.title = to.meta.title || '默认标题'
})
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

注意

在 beforeEach 中必须调用 next(),否则路由将被阻止!注意避免无限重定向。

守卫参数详解

to(目标路由对象) 包含的常用属性:

  • to.path:目标路径,如 /user/123
  • to.params:路径参数对象,如 { id: '123' }
  • to.query:查询参数对象,如 { page: '1' }
  • to.name:路由名称
  • to.meta:路由元信息,可用于权限控制

from(来源路由对象):

  • 与 to 具有相同的属性结构
  • 表示当前正要离开的路由

next(导航控制函数) 的用法:

  • next():放行,进入下一个钩子
  • next(false):中断当前导航
  • next('/path') 或 next({ path: '/path' }):重定向到指定路由
  • next(error):传递错误给全局错误处理器

代码示例:

router.beforeEach((to, from, next) => {
    // 检查路由元信息中的权限要求
    if (to.meta.requiresAuth) {
        const token = localStorage.getItem('token')
        if (token) {
            next()  // 有 token,放行
        } else {
            next({
                path: '/login',
                query: { redirect: to.fullPath }  // 保存目标路径,登录后可跳转回来
            })
        }
    } else {
        next()  // 不需要认证,直接放行
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

路由独享守卫

在路由配置中直接定义,只对当前路由生效:

const router = createRouter({
    routes: [
        {
            path: '/admin',
            component: Admin,
            // 路由独享守卫:可以定义多个
            beforeEnter: (to, from, next) => {
                // 只在进入 /admin 路由时触发
                if (hasAdminPermission()) {
                    next()
                } else {
                    next('/403')  // 无权限,跳转到403页面
                }
            }
        },
        {
            path: '/user',
            component: User,
            // 也可以使用数组形式定义多个守卫
            beforeEnter: [checkAuth, checkPermission]
        }
    ]
})

// 可复用的守卫函数
function checkAuth(to, from, next) {
    const isLoggedIn = !!localStorage.getItem('token')
    isLoggedIn ? next() : next('/login')
}

function checkPermission(to, from, next) {
    const userRole = localStorage.getItem('role')
    userRole === 'admin' ? next() : next('/403')
}
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

路由独享守卫的特点:

  • 只在进入路由时触发,路由参数变化不会触发
  • 可以定义为单个函数或函数数组
  • 适合对特定路由进行权限控制

组件内守卫

在 Vue 3 的 Composition API 中,使用以下 API:

// 在组件中
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'

export default {
    setup() {
        const hasUnsavedChanges = ref(false)
        
        // 1. 离开组件前触发
        onBeforeRouteLeave((to, from, next) => {
            if (hasUnsavedChanges.value) {
                const answer = window.confirm('有未保存的修改,确定离开吗?')
                if (answer) {
                    next()
                } else {
                    next(false)  // 取消导航
                }
            } else {
                next()
            }
        })
        
        // 2. 路由更新时触发(同一组件,但 params 变化)
        onBeforeRouteUpdate((to, from, next) => {
            // 例如:/user/1 -> /user/2
            console.log('路由参数更新:', to.params)
            // 可以在这里重新加载数据
            loadUserData(to.params.id)
            next()
        })
        
        return {
            hasUnsavedChanges
        }
    }
}
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

组件内守卫的应用场景:

守卫 触发时机 常见用途
onBeforeRouteLeave 离开当前组件前 表单未保存提示、清理定时器
onBeforeRouteUpdate 路由参数变化但组件复用 重新加载数据、更新视图

注意事项:

  • Vue 3 不再支持 beforeRouteEnter(因为 setup 执行时组件实例还未创建)
  • 如需在进入组件前执行逻辑,应使用路由独享守卫或全局守卫
  • onBeforeRouteUpdate 仅在组件复用时触发,完全切换组件时不会触发

守卫执行顺序

完整的导航解析流程:

1. 导航触发(用户点击链接或调用 router.push)
   ↓
2. 在失活的组件里调用 onBeforeRouteLeave 守卫
   ↓
3. 调用全局的 beforeEach 守卫
   ↓
4. 在重用的组件里调用 onBeforeRouteUpdate 守卫
   ↓
5. 调用路由配置里的 beforeEnter 守卫
   ↓
6. 解析异步路由组件
   ↓
7. 在被激活的组件里调用 beforeRouteEnter 守卫(Options API)
   ↓
8. 调用全局的 beforeResolve 守卫
   ↓
9. 导航被确认
   ↓
10. 调用全局的 afterEach 钩子
   ↓
11. 触发 DOM 更新
   ↓
12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

执行顺序示例:

假设从 /home 导航到 /user/123:

// 1. Home 组件中
onBeforeRouteLeave((to, from, next) => {
    console.log('1. 离开 Home 组件')
    next()
})

// 2. router.js 中
router.beforeEach((to, from, next) => {
    console.log('2. 全局前置守卫')
    next()
})

// 3. router.js 的路由配置中
{
    path: '/user/:id',
    component: User,
    beforeEnter: (to, from, next) => {
        console.log('3. /user 路由独享守卫')
        next()
    }
}

// 4. router.js 中
router.beforeResolve((to, from, next) => {
    console.log('4. 全局解析守卫')
    next()
})

// 5. router.js 中
router.afterEach((to, from) => {
    console.log('5. 全局后置钩子')
})

// 输出顺序:
// 1. 离开 Home 组件
// 2. 全局前置守卫
// 3. /user 路由独享守卫
// 4. 全局解析守卫
// 5. 全局后置钩子
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

登录鉴权案例

需求:登录后才可以进入 home 页面,否则必须进入 login 页面。

  • 定义 Login.vue
<script setup>
    import { ref } from 'vue'
    import { useRouter } from 'vue-router'
    
    let username = ref('')
    let password = ref('')
    let router = useRouter()
    let errorMsg = ref('')
    
    let login = () => {
        // 验证用户名和密码
        if (username.value === 'root' && password.value === '123456') {
            // 登录成功,存储用户信息到 localStorage
            localStorage.setItem('username', username.value)
            
            // 跳转到首页(使用 replace 避免返回到登录页)
            router.replace({ path: '/home', query: { username: username.value } })
        } else {
            errorMsg.value = '账号或密码错误!'
        }
    }
</script>

<template>
    <div class="login-container">
        <h2>用户登录</h2>
        <div class="form-group">
            <label>账号:</label>
            <input type="text" v-model="username" placeholder="请输入账号(root)">
        </div>
        <div class="form-group">
            <label>密码:</label>
            <input type="password" v-model="password" placeholder="请输入密码(123456)">
        </div>
        <p v-if="errorMsg" class="error">{{ errorMsg }}</p>
        <button @click="login" class="btn-login">登录</button>
    </div>
</template>

<style scoped>
.login-container {
    width: 300px;
    margin: 100px auto;
    padding: 20px;
    border: 1px solid #ddd;
    border-radius: 5px;
}
.form-group {
    margin-bottom: 15px;
}
.error {
    color: red;
    font-size: 14px;
}
.btn-login {
    width: 100%;
    padding: 10px;
    background-color: #42b983;
    color: white;
    border: none;
    border-radius: 3px;
    cursor: pointer;
}
</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
  • 定义 Home.vue
<script setup>
    import { ref } from 'vue'
    import { useRoute, useRouter } from 'vue-router'

    let route = useRoute()
    let router = useRouter()
    
    // 从 localStorage 获取用户名
    let username = ref(window.localStorage.getItem('username'))

    let logout = () => {
        // 清除 localStorage 中的用户信息
        window.localStorage.removeItem('username')
        
        // 跳转到登录页(使用 replace 避免返回到 home)
        router.replace('/login')
    }
</script>

<template>
    <div>
        <h1>Home页面</h1>
        <h3>欢迎 {{ username }} 登录</h3>
        <button @click="logout">退出登录</button>
    </div>
</template>

<style scoped>
</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
  • App.vue
<script setup>
</script>

<template>
    <div>
        <!-- 路由视图展示区 -->
        <router-view></router-view>
    </div>
</template>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
  • 配置 router.js(含路由守卫)
import { createRouter, createWebHashHistory } from 'vue-router'

import Home from '../components/Home.vue'
import Login from '../components/Login.vue'

const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: '/home',
            component: Home,
            meta: { requiresAuth: true }  // 需要登录
        },
        {
            path: '/',
            redirect: '/home'  // 默认重定向到 home
        },
        {
            path: '/login',
            component: Login
        },
    ]
})

// 设置全局前置守卫
router.beforeEach((to, from, next) => {
    console.log(`从 ${from.path} 到 ${to.path}`)

    // 如果目标路由是登录页,直接放行
    if (to.path === '/login') {
        next()
        return
    }

    // 检查是否需要登录
    const username = window.localStorage.getItem('username')
    
    if (to.meta.requiresAuth && !username) {
        // 需要登录但未登录,重定向到登录页
        console.log('未登录,重定向到登录页')
        next('/login')
    } else {
        // 已登录或不需要登录,放行
        next()
    }
})

// 设置全局后置钩子
router.afterEach((to, from) => {
    console.log(`导航完成:从 ${from.path} 到 ${to.path}`)
    // 可以在这里设置页面标题
    document.title = to.meta.title || 'Vue 应用'
})

export default router
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

案例要点总结

  1. 登录验证:

    • 使用 localStorage 存储登录状态
    • 使用 router.replace() 避免用户通过后退键返回登录页
  2. 路由守卫:

    • 在 beforeEach 中检查 meta.requiresAuth
    • 未登录时重定向到 /login
    • 注意避免无限重定向(登录页需要放行)
  3. 退出登录:

    • 清除 localStorage 中的用户信息
    • 使用 router.replace('/login') 跳转

安全性提示:

  • 前端路由守卫只能防止普通用户误操作
  • 真正的权限控制必须在后端实现
  • 敏感数据不要存储在 localStorage 中
  • Token 应该有过期时间
上次更新: 2026/01/11, 22:18:14
第七章 前端工程化-上
第九章 前端工程化-下

← 第七章 前端工程化-上 第九章 前端工程化-下→

最近更新
01
第九章 前端工程化-下
12-11
02
第七章 前端工程化-上
12-04
03
第四章 XML、Tomcat、HTTP
11-04
更多文章>
Theme by Vdoing | Copyright © 2024-2026 bombax | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式