第八章 前端工程化-中
# 第八章 前端工程化-中
# 五、Vue3 通过 Vite 实现工程化
# 5.1 Vite 核心概念
# 5.1.1 什么是 Vite
Vite(法语"快速"的意思,发音 /vit/,类似"veet")是新一代前端构建工具,由 Vue.js 作者尤雨溪创建。它利用浏览器原生 ES 模块和现代编译工具,提供极速的开发体验。官网:https://cn.vitejs.dev/ (opens new window)
Vite 的核心优势:
- 极速的冷启动:Vite 无需打包即可启动开发服务器,传统构建工具(如 Webpack)需要将整个应用打包后才能启动,而 Vite 直接利用浏览器的原生 ES 模块支持,实现按需编译。
- 即时的模块热更新(HMR):当修改源代码时,Vite 只会精确更新修改的模块,而不是重新编译整个应用,更新速度极快且不会丢失应用状态。
- 真正的按需编译:只编译当前页面实际使用到的代码,大大减少了编译时间和资源消耗。
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
- 第一次使用 vite 时会提示下载 vite,输入 y 回车即可,下次使用 vite 就不会出现了

注意
选择 vue+JavaScript 选项即可
2. 安装项目所需依赖
- cd 进入刚刚创建的项目目录
- npm install 命令安装基础依赖
cd ./vue3-demo1
npm install
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"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
npm run dev

5. 停止项目
- 命令行上按
Ctrl+C组合键停止开发服务器
6. 常见问题处理
- 端口被占用:如果 5173 端口被占用,Vite 会自动尝试下一个可用端口(5174、5175...)
- 依赖安装失败:尝试删除
node_modules文件夹和package-lock.json,重新执行npm install - 启动缓慢:首次启动会进行依赖预构建,后续启动会很快
# 5.2.2 Vite+Vue3 项目的目录结构
1. 下面是 Vite 项目结构和入口的详细说明:

public/目录:用于存放一些公共资源,如 HTML 文件、图像、字体等,这些资源会被直接复制到构建出的目标目录中。src/目录:存放项目的源代码,包括 JavaScript、CSS、Vue 组件、图像和字体等资源。在开发过程中,这些文件会被 Vite 实时编译和处理,并在浏览器中进行实时预览和调试。以下是 src 内部划分建议:assets/目录:用于存放一些项目中用到的静态资源,如图片、字体、样式文件等。components/目录:用于存放组件相关的文件。组件是代码复用的一种方式,用于抽象出一个可复用的 UI 部件,方便在不同的场景中进行重复使用。layouts/目录:用于存放布局组件的文件。布局组件通常负责整个应用程序的整体布局,如头部、底部、导航菜单等。pages/目录:用于存放页面级别的组件文件,通常是路由对应的组件文件。在这个目录下,可以创建对应的文件夹,用于存储不同的页面组件。plugins/目录:用于存放 Vite 插件相关的文件,可以按需加载不同的插件来实现不同的功能,如自动化测试、代码压缩等。router/目录:用于存放 Vue.js 的路由配置文件,负责管理视图和 URL 之间的映射关系,方便实现页面之间的跳转和数据传递。store/目录:用于存放 Vuex 状态管理相关的文件,负责管理应用程序中的数据和状态,方便统一管理和共享数据,提高开发效率。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" // 本地预览生产构建产物
}
}
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)
}
})
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
2
3
4
# .env.production
VITE_APP_TITLE=我的应用
VITE_API_BASE_URL=https://api.example.com
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('生产环境')
}
2
3
4
5
6
7
8
9
10
11
12
注意
- 环境变量必须以
VITE_开头才能在客户端代码中访问 - 修改环境变量文件后需要重启开发服务器
- 不要在环境变量中存储敏感信息(如密钥、密码等)
# 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)的优势:
- 更好的组织:相关的代码集中在一个文件中,便于查找和维护
- 作用域隔离:
<style scoped>可以让样式只作用于当前组件 - 编译优化:Vue 编译器可以对 SFC 进行更好的优化
- 类型支持:配合 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(更细粒度的组件)
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
2
3
- App.vue
<script>
//存储vue页面逻辑js代码
</script>
<template>
<!-- 页面的样式的是html代码-->
</template>
<style scoped>
/** 存储的是css代码! <style scoped> 是 Vue.js 单文件组件中用于设置组件样式的一种方式。
它的含义是将样式局限在当前组件中,不对全局样式造成影响。 */
</style>
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>
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>
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 关于样式的导入方式
- 全局引入 main.js
import './style/reset.css' //书写引入的资源的相对路径即可!1 - vue 文件 script 代码引入
import './style/reset.css'1 - 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 的运算表达式
- 插值表达式中也支持函数的调用
使用注意事项:
- 仅支持单个表达式:
<!-- 正确 --> {{ 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 - 避免复杂逻辑:插值表达式应该简单明了,复杂逻辑应该使用计算属性或方法
- 性能考虑:避免在插值表达式中调用复杂函数,每次重渲染都会执行
<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>
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 的特殊用法:
- 绑定布尔属性:
<!-- disabled 属性会根据 isDisabled 的真假值决定是否存在 --> <button :disabled="isDisabled">提交</button>1
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 - 动态属性名:
<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
注意
- 简写语法
:是开发中最常用的方式 - 属性值为
null或undefined时,该属性不会被渲染 - 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>
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 的值可以是方法事件处理器,也可以是内联事件处理器
事件修饰符分类:
- 事件修饰符:
.stop:阻止事件冒泡(等同于event.stopPropagation()).prevent:阻止默认事件(等同于event.preventDefault())[重点].once:只触发一次事件 [重点].capture:使用事件捕获模式而不是冒泡模式.self:只在事件发送者自身触发时才触发事件
- 按键修饰符:
<!-- 只在 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 - 系统修饰符:
<!-- Ctrl 键被按下时点击 --> <button @click.ctrl="handleCtrlClick">点击</button> <!-- 系统修饰符:.ctrl .alt .shift .meta(Mac 的 Command 键) -->1
2
3
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>
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>
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 会自动重新渲染依赖该数据的组件
重要特性:
- Vue 3 的数据模型默认不是响应式的,需要通过
ref或reactive进行处理 - 只有响应式数据的变化才会触发视图更新
- 非响应式数据修改后,页面不会自动更新
# 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>
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 的特点:
- 适用于基本类型:主要用于包装基本数据类型(String、Number、Boolean 等)
- 也支持对象:虽然主要用于基本类型,但也可以包装对象和数组
- 访问方式:在
<script>中需要通过.value访问值,在<template>中会自动解包 - 返回 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>
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 的特点:
- 仅适用于对象类型:只能用于对象、数组、Map、Set 等引用类型
- 深层响应式:会递归地将对象的所有嵌套属性转换为响应式
- 直接访问属性:不需要
.value,直接通过对象.属性访问 - 不能替换整个对象:直接赋值会失去响应性,需要修改属性而非替换对象
使用限制:
import { reactive } from 'vue'
let state = reactive({ count: 0 })
// ❌ 错误:这会失去响应性
state = { count: 1 }
// ✅ 正确:修改属性保持响应性
state.count = 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>
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) |
使用场景推荐:
使用
ref的场景: ✅- 基本类型数据:字符串、数字、布尔值等
- 需要整个替换的数据
- 单一值的状态管理
- 与组合式 API 配合使用时的简单数据
import { ref } from 'vue' // ✅ 适合用 ref const count = ref(0) const message = ref('Hello') const isLoading = ref(false) // 可以整个替换 count.value = 1001
2
3
4
5
6
7
8
9使用
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
最佳实践建议:
- 优先使用
ref:尤其是在组合式 API 中,ref更灵活,类型推导更好 - 对象也可用
ref:ref也能很好地处理对象,且支持整个替换 - 避免混用:在同一组件中,尽量保持一致的风格
- 解构问题:如需解构,使用
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) // 解构后仍保持响应性
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) 创建的。
使用场景:
- 保持解构后的响应性:解构 reactive 对象时保持响应性
- 组合式 API 返回值:在组合函数中返回响应式属性
- 简化模板使用:避免在模板中重复写对象名
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
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
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,保持响应性
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
注意
- toRef/toRefs 创建的 ref 与源对象保持双向同步
- 如果源属性不存在,toRef 会创建一个值为 undefined 的 ref
- 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 }}
&
{{ ct }}
<button @click="data.counter++">+</button>
<button @click="incr()">+</button>
<hr>
<button @click="show()">显示counter值</button>
</div>
</template>
<style scoped>
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 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>
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>
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>
2
3
4
5
6
7
8
9
10
11
12
选择建议:
- 使用 v-if 的场景:
- 条件很少改变(如用户权限、配置项)
- 初始条件为 false 的情况占多数
- 需要配合 v-else、v-else-if 使用
- 节省初始渲染开销(懒加载)
- 使用 v-show 的场景:
- 需要频繁切换显示/隐藏(如选项卡、折叠面板)
- 元素初始就需要渲染
- 切换频率高的场景
- 性能优化建议:
- 大型列表条件渲染优先用
v-if,减少初始 DOM 数量 - 复杂组件的显示/隐藏优先用
v-show,避免重复初始化 - 不要在
v-for上使用v-if(优先级问题)
- 大型列表条件渲染优先用
小结:
v-if是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。v-if也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。- 相比之下,
v-show简单许多,元素无论初始条件如何,始终会被渲染,只有 CSSdisplay属性会被切换。 - 总的来说,
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 的作用:
- 唯一标识:key 为每个节点提供唯一标识,Vue 可以追踪每个节点的身份
- 高效更新:帮助 Vue 的虚拟 DOM 算法识别哪些元素发生了变化
- 保持状态:在列表重排序时保持组件状态和 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>
2
3
4
5
6
7
8
9
10
11
12
13
14
key 的选择原则:
- 优先使用数据的唯一 ID:如数据库主键、唯一编号等
- 不要使用索引:除非列表是静态的且永不会改变
- 不要使用随机数:会导致每次都重新渲染
- 保持稳定性:key 值在多次渲染中应该保持一致
不使用 key 的问题示例:
// 问题场景:列表中有输入框,删除中间项时
const items = ref([
{ id: 1, name: '项目1' },
{ id: 2, name: '项目2' },
{ id: 3, name: '项目3' }
])
// 没有 key:删除项目2后,项目3的输入框内容可能会出现在项目2的位置
// 有 key:每个项目的输入框状态会正确保持
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>
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>
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>
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 实质上是一个语法糖,它背后是两个操作:
v-bind绑定 value 属性v-on监听 input 事件更新数据
<!-- 以下两种写法等价 -->
<input v-model="message">
<input
:value="message"
@input="message = $event.target.value">
2
3
4
5
6
v-model 修饰符:
.lazy:将 input 事件改为 change 事件,在失去焦点时才更新<!-- 在输入时不更新,失去焦点时才更新 --> <input v-model.lazy="message">1
2.number:自动将输入转换为数字类型<!-- 输入会自动转换为 number 类型 --> <input v-model.number="age" type="number">1
2.trim:自动去除首尾空格<!-- 自动去除首尾空格 --> <input v-model.trim="username">1
2- 修饰符组合使用:
<!-- 同时使用多个修饰符 --> <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>
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>
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>
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>
2
3
4
5
6
7
8
9
10
<!-- 父组件 -->
<UserForm
v-model:first-name="first"
v-model:last-name="last"
/>
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>
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>
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>
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 主要用于以下场景:
- 数据变化时执行操作:当数据发生变化时需要执行相应的操作
- 条件触发:监听数据变化,当满足一定条件时触发相应操作
- 异步操作:在异步操作前或操作后需要执行相应的操作
- 副作用处理:需要执行 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>
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 高级选项详解:
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
16immediate: true- 立即执行:- 创建监听器时立即执行一次回调
- 适合需要初始化数据的场景
const keyword = ref('') // 立即执行:组件加载时就会执行一次 watch(keyword, (newValue) => { console.log('搜索关键词:', newValue) // 首次加载时也会执行搜索 fetchSearchResults(newValue) }, { immediate: true })1
2
3
4
5
6
7
8flush- 回调执行时机:'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停止监听:
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监听多个数据源:
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监听 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 })
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 })
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>
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。在此过程中,它也会运行被称为 生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码!
生命周期图解:

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() - 组件卸载完成
2
3
4
5
6
7
8
9
常见应用场景:
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
16onBeforeUnmount - 清理工作:
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
22onUpdated - 访问更新后的 DOM:
import { onUpdated } from 'vue' onUpdated(() => { // ❗ 注意:避免在 onUpdated 中修改响应式数据 // 可能导致无限循环 // ✅ 适合:访问更新后的 DOM const height = element.offsetHeight adjustLayout(height) })1
2
3
4
5
6
7
8
9
10
注意事项:
不要在 onUpdated 中修改响应式数据:
// ❌ 错误:会导致无限循环 onUpdated(() => { count.value++ // 会触发新的更新 })1
2
3
4生命周期钩子中的 this:
- 在
<script setup>中不需要 this - 钩子函数自动绑定到组件实例
- 在
异步注册钩子:
// ❌ 错误:异步注册钩子无效 setTimeout(() => { onMounted(() => { // 这不会工作 }) }, 100) // ✅ 正确:同步注册 onMounted(() => { // 可以在钩子内部使用异步 setTimeout(() => { console.log('延迟执行') }, 100) })1
2
3
4
5
6
7
8
9
10
11
12
13
14多次注册同一钩子:
// ✅ 允许:会按顺序执行 onMounted(() => { console.log('第一个') }) onMounted(() => { console.log('第二个') })1
2
3
4
5
6
7
8
常见问题及解决方案:
组件卸载时未清理资源:
// ❌ 问题:定时器未清理,导致内存泄漏 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在挂载前访问 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父子组件生命周期执行顺序:
父组件 onBeforeMount ↓ 子组件 onBeforeMount 子组件 onMounted ↓ 父组件 onMounted // 更新时 父组件 onBeforeUpdate ↓ 子组件 onBeforeUpdate 子组件 onUpdated ↓ 父组件 onUpdated // 卸载时 父组件 onBeforeUnmount ↓ 子组件 onBeforeUnmount 子组件 onUnmounted ↓ 父组件 onUnmounted1
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>
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 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。简单来说,组件就是实现应用中局部功能代码和资源的集合。
在实际应用中,组件常常被组织成层层嵌套的树状结构:
这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。
组件化 vs 模块化
- 组件化:对 js/css/html 统一封装,这是 Vue 中的概念。一个组件包含了模板(HTML)、逻辑(JavaScript)和样式(CSS)。
- 模块化:对 JavaScript 代码的统一封装,这是 ES6 中的概念。
- 关系:组件化中,对 JavaScript 部分代码的处理使用 ES6 中的模块化。
组件的核心优势
- 可复用性:编写一次,多处使用
- 可维护性:功能独立,便于定位和修复问题
- 代码组织:将复杂的应用拆分为多个简单的部分
- 团队协作:不同成员可以独立开发不同的组件
组件通信机制概述
在 Vue 中,组件之间的数据传递主要有以下几种方式:
- 父传子:通过
props向下传递数据 - 子传父:通过
$emit触发自定义事件向上传递数据 - 兄弟组件:通过共同的父组件中转数据
- 跨层级通信:使用
provide/inject或状态管理工具(如 Pinia)
组件的生命周期(简介)
Vue 组件从创建到销毁会经历一系列的生命周期钩子:
| 生命周期钩子 | 触发时机 | 常见用途 |
|---|---|---|
onMounted | 组件挂载完成后 | 发送 AJAX 请求、操作 DOM |
onUpdated | 组件更新后 | 监听数据变化后的操作 |
onUnmounted | 组件卸载前 | 清理定时器、取消订阅 |
笔记
在 Vue 3 的 Composition API 中,生命周期钩子需要通过 import 引入后使用。
# 6.8.2 组件化入门案例
案例需求: 创建一个页面,包含头部和菜单以及内容显示区域,每个区域使用独立组建!

1. 准备 Vue 项目
npm create vite
cd vite项目
npm install
2
3
2. 安装相关依赖
npm install sass
npm install bootstrap
2
3. 项目结构说明
vite项目/
├── src/
│ ├── components/ # 组件目录
│ │ ├── Header.vue # 头部组件
│ │ ├── Navigator.vue # 导航菜单组件
│ │ └── Content.vue # 内容展示组件
│ ├── App.vue # 根组件(入口)
│ └── main.js # 入口文件
└── package.json
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
11Navigator.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
18Content.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>
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 />
2
全局注册示例(了解):
// main.js
import Header from './components/Header.vue'
app.component('Header', Header)
// 所有组件都可以使用 <Header />,无需 import
2
3
4
7. scoped 样式作用域详解
在 <style scoped> 中定义的样式仅对当前组件生效:
<!-- App.vue -->
<style scoped>
.header { color: red; } /* 只影响 App.vue 中的 .header */
</style>
2
3
4
原理:Vue 会为组件添加唯一的 data-v-xxx 属性,样式选择器会自动添加该属性限定作用域。
注意事项:
- 如果需要全局样式,去掉
scoped或在单独的 CSS 文件中定义 scoped样式不会影响子组件的根元素(可以通过:deep()深度选择器穿透) 8. 启动测试
npm run dev
# 6.8.3 组件之间传递数据
组件通信方式概述
Vue 组件之间的数据传递遵循 单向数据流 原则:
- 父传子(Props Down):父组件通过
props向子组件传递数据 - 子传父(Events Up):子组件通过
$emit触发事件向父组件传递数据 - 兄弟组件:通过共同的父组件作为中介进行数据中转
注意
Props 是只读的,子组件不应直接修改父组件传递的 props 值,这会破坏单向数据流。
# 6.8.3.1 父传子(Props)
核心概念
Vue 3 中父组件通过 props 向子组件传递数据,具体流程:
- 父组件:定义响应式数据,在模板中通过自定义属性(
v-bind或:简写)传递给子组件 - 子组件:使用
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>
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>
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)
}
}
})
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 注意事项
- 命名规范:
- 父组件传递:使用
kebab-case(短横线)::user-name="name" - 子组件接收:使用
camelCase(驼峰):defineProps({ userName: String })
- 父组件传递:使用
- 单向数据流:
- 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++ // ✓ 正确
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>
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>
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 // 验证失败
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
自定义事件命名规范
- 事件名使用 kebab-case(短横线形式):
<!-- 推荐 --> <Son @update-value="handleUpdate" /> <!-- 不推荐 --> <Son @updateValue="handleUpdate" />1
2
3
4
5 - 事件名不要与原生 HTML 事件名冲突(如
click、focus等) - 语义化命名:使用动词开头,如
update-user、delete-item等
# 6.8.3.3 兄弟传参
核心思路
兄弟组件之间无法直接通信,需要通过 共同的父组件 作为中介进行数据中转:
- 组件 A(发送方):通过
emit向父组件发送数据 - 父组件:接收数据并存储到自己的响应式变量中
- 组件 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>
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>
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>
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 (接收方)
2
3
4
5
适用场景:
- 简单的兄弟组件通信
- 嵌套层级不深的组件
复杂场景推荐:
- 使用 Pinia(Vue 3 官方状态管理工具)
- 使用 provide/inject(跨层级通信)
# 七、Vue3 路由机制 Router
# 7.1 路由简介
什么是路由:
- 定义:路由就是根据不同的 URL 地址展示不同的内容或页面。
- 通俗理解:路由就像是一个地图,我们要去不同的地方,需要通过不同的路线进行导航。
路由的作用:
- 无刷新切换页面:在单页应用程序(SPA)中,路由可以实现不同视图之间的无刷新切换,提升用户体验
- 权限控制:路由可以实现页面的认证和权限控制,保护用户的隐私和安全
- 历史记录管理:路由可以利用浏览器的前进与后退,帮助用户更好地回到之前访问过的页面
- 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 版本
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>
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>
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>
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>
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>
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
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: [/*...*/]
})
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")
2
3
4
5
6
7
8
9
10
11
12
13
14
启动测试
npm run dev
浏览器访问: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
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 } }
}
}
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 组件
}
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>
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>
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
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>
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>
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 || '默认标题'
})
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/123to.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() // 不需要认证,直接放行
}
})
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')
}
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
}
}
}
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 的回调函数
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. 全局后置钩子
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>
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>
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>
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
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
案例要点总结
登录验证:
- 使用
localStorage存储登录状态 - 使用
router.replace()避免用户通过后退键返回登录页
- 使用
路由守卫:
- 在
beforeEach中检查meta.requiresAuth - 未登录时重定向到
/login - 注意避免无限重定向(登录页需要放行)
- 在
退出登录:
- 清除 localStorage 中的用户信息
- 使用
router.replace('/login')跳转
安全性提示:
- 前端路由守卫只能防止普通用户误操作
- 真正的权限控制必须在后端实现
- 敏感数据不要存储在 localStorage 中
- Token 应该有过期时间