Vue3全家桶教程
@description:系统学习 Vue.js 渐进式 JavaScript 框架 接触到的第一个框架
@author:liuwy
@date:2023.4.30
Ⅰ Vue基础入门
目标 | 目录 |
---|---|
了解Vue | 一、Vue简介 |
能够知道 Vue 的基本使用步骤 | 二、Vue的基本使用 |
掌握六种指令与过滤器的使用 | 三、Vue的指令和过滤器 |
案例实战——提高熟练程度 | 四、第一个案例——品牌列表案例 |
对Vue基础入门的总结 | 五、Vue基础入门总结 |
一、Vue简介
1.Vue数据驱动
1.1 数据驱动
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fLFmacXc-1685004795657)(./assets/Snipaste_2023-05-02_19-53-50.png)]
使用了vue的页面中,vue会监听数据的变化,从而自动重新渲染页面结构。
好处:当页面数据发生变化时,页面会自动渲染!
注意:数据驱动视图是单向的数据绑定
1.2 双向数据绑定
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YYEbu4wL-1685004795658)(.\assets\03_vue基础入门_1-79_1476395055.bmp)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DIzsaOAx-1685004795659)(.\assets\03_vue基础入门_1-79_1476395056.jpg)]
在填写表单时,双向数据绑定可以辅助开发者在不操作 DOM 的前提下,自动把用户填写的内容同步到数据源中。
好处:开发者不再需要手动操作 DOM 元素,来获取表单元素最新的值!
1.3 MVVM
MVVM 是 vue 实现数据驱动视图和双向数据绑定的核心原理。它把每个 HTML 页面都拆分成了如下三个部分:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xsi9yu0O-1685004795659)(.\assets\03_vue基础入门_1-79_1476395060.bmp)]
在 MVVM 概念中:
- View 表示当前页面所渲染的 DOM 结构。
- Model 表示当前页面渲染时所依赖的数据源。
- ViewModel 表示 vue 的实例,它是 MVVM 的核心。
1.4 MVVM的工作原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jziKIvuS-1685004795660)(.\assets\03_vue基础入门_1-79_1476395056.jpg)]
ViewModel 作为 MVVM 的核心,是它把当前页面的数据源(Model)和页面的结构(View)连接在了一起。
当数据源发生变化时,会被 ViewModel 监听到,VM 会根据最新的数据源自动更新页面的结构
当表单元素的值发生变化时,也会被 VM 监听到,VM 会把变化过后最新的值自动同步到 Model 数据源中
2.Vue的版本
当前,vue 共有 3 个大版本,其中:
- 2.x 版本的 vue 是目前企业级项目开发中的主流版本
- 3.x 版本的 vue 于 2020-09-19 发布,生态还不完善,尚未在企业级项目开发中普及和推广
- 1.x 版本的 vue 几乎被淘汰,不再建议学习与使用
vue3.x 和 vue2.x 版本的对比
vue2.x 中绝大多数的 API 与特性,在 vue3.x 中同样支持。同时,vue3.x 中还新增了 3.x 所特有的功能、并废弃了某些 2.x 中的旧功能:
新增的功能例如:
组合式 API、多根节点组件、更好的 TypeScript 支持等
废弃的旧功能如下:
过滤器、不再支持 o n , on, on,off 和 $once 实例方法等
二、Vue的基本使用
1.基本使用步骤
① 导入 vue.js 的 script 脚本文件
② 在页面中声明一个将要被 vue 所控制的 DOM 区域
③ 创建 vm 实例对象(vue 实例对象)
2.基本代码与 MVVM 的对应关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jDGmidIu-1685004795660)(.\assets\03_vue基础入门_1-79_1476395148.bmp)]
3.安装 Vue-devtools 调试工具
收藏猫chrome插件资源,下载安装:https://chrome.pictureknow.com/
扩展迷chrome插件资源,下载安装:https://www.extfans.com/
三、Vue的指令和过滤器
1.指令的概念
**指令(Directives)**是 vue 为开发者提供的模板语法,用于辅助开发者渲染页面的基本结构。
vue 中的指令按照不同的用途可以分为如下 6 大类:
① 内容渲染指令
② 属性绑定指令
③ 事件绑定指令
④ 双向绑定指令
⑤ 条件渲染指令
⑥ 列表渲染指令
注意:指令是 vue 开发中最基础、最常用、最简单的知识点。
1.1 内容渲染指令
内容渲染指令用来辅助开发者渲染 DOM 元素的文本内容。常用的内容渲染指令有如下 3 个:
- v-text
- {{ }}
- v-html
v-text
<div id="app">
<p v-text="username"></p>
<p v-text="gender">性别</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
username: 'zs',
gender: '男'
}
})
</script>
注意:v-text 指令会覆盖元素内默认的值。
{{}}
vue 提供的 {{ }} 语法,专门用来解决 v-text 会覆盖默认文本内容的问题。这种 {{ }} 语法的专业名称是插值表达式(英文名为:Mustache)。
<div id="app">
<p>姓名:{{username}}</p>
<p>性别:{{gender}}</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
username: 'zs',
gender: '男'
}
})
</script>
注意:相对于 v-text 指令来说,插值表达式在开发中更常用一些!因为它不会覆盖元素中默认的文本内容。
v-html
v-text 指令和插值表达式只能渲染纯文本内容。如果要把包含 HTML 标签的字符串渲染为页面的 HTML 元素,则需要用到 v-html 这个指令:
<div id="app">
<p v-text="desc"></p>
<p>{{desc}}</p>
<p v-html="desc"></p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
username: 'zs',
gender: '男',
desc: '<i>abc</i>'
}
})
</script>
1.2 属性绑定指令
v-bind
如果需要为元素的属性动态绑定属性值,则需要用到 v-bind 属性绑定指令。用法示例如下:
<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
<input type="text" v-bind:placeholder="inputValue">
<hr>
<img v-bind:src="imgSrc" alt="">
</div>
<!-- 导入 vue 脚本文件 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
// 创建 VM 实例对象
const vm = new Vue({
// 指定当前 VM 要控制的区域
el: '#app',
// 数据源
data: {
// 文本框的占位符内容
inputValue: '请输入内容',
// 图片的 src 地址
imgSrc: './images/logo.png',
},
})
</script>
属性绑定指令的简写形式
由于 v-bind 指令在开发中使用频率非常高,vue 官方为其提供了简写形式(简写为英文的 : )。
使用 Javascript 表达式
在 vue 提供的模板渲染语法中,除了支持绑定简单的数据值之外,还支持 Javascript 表达式的运算,例如:
<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
<p>{{number + 1}}</p>
<p>{{ok ? 'True' : 'False'}}</p>
<p>{{message.split('').reverse().join('')}}</p>
<p :id="'list-' + id">xxx</p>
<p>{{user.name}}</p>
</div>
<!-- 导入 vue 脚本文件 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
// 创建 VM 实例对象
const vm = new Vue({
// 指定当前 VM 要控制的区域
el: '#app',
// 数据源
data: {
// 数值
number: 9,
// 布尔值
ok: false,
// 字符串
message: 'ABC',
// id 值
id: 3,
// 用户的信息对象
user: {
name: 'zs',
},
},
})
</script>
1.3 事件绑定指令
v-on
vue 提供了 v-on 事件绑定指令,用来辅助程序员为 DOM 元素绑定事件监听。通过 v-on 绑定的事件处理函数,需要在 methods 节点中进行声明,语法格式如下:
<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
<h3>count 的值为:{{count}}</h3>
<!-- TODO:点击按钮,让 data 中的 count 值自增 +1 -->
<button v-on:click="addCount">+1</button>
</div>
<!-- 导入 vue 脚本文件 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
// 创建 VM 实例对象
const vm = new Vue({
// 指定当前 VM 要控制的区域
el: '#app',
// 数据源
data: {
// 计数器的值
count: 0,
},
methods: {
// 点击按钮,让 count 自增 +1
addCount() {
// this 访问当前的实例对象vm
this.count += 1
},
},
})
</script>
注意:原生 DOM 对象有 onclick、oninput、onkeyup 等原生事件,替换为 vue 的事件绑定形式后,分别为:v-on:click、v-on:input、v-on:keyup
事件绑定的简写形式
由于 v-on 指令在开发中使用频率非常高,vue 官方为其提供了简写形式(简写为英文的 @ )。
<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
<h3>count 的值为:{{count}}</h3>
<!-- TODO:点击按钮,让 data 中的 count 值自增 +1 -->
<!-- 简写到行内的事件处理 -->
<button @click="count+=1">+1</button>
</div>
<!-- 导入 vue 脚本文件 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
// 创建 VM 实例对象
const vm = new Vue({
// 指定当前 VM 要控制的区域
el: '#app',
// 数据源
data: {
// 计数器的值
count: 0,
},
methods: {
// 点击按钮,让 count 自增 +1
// 如果事件处理函数中的代码足够简单,只有一行代码,则可以简写到行内
// addCount() {
// this.count += 1
// },
},
})
</script>
事件对象 event
在原生的 DOM 事件绑定中,可以在事件处理函数的形参处,接收事件对象 event。同理,在 v-on 指令(简写为 @ )所绑定的事件处理函数中,同样可以接收到事件对象 event,示例代码如下:
<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
<h3>count 的值为:{{count}}</h3>
<button v-on:click="addCount">+1</button>
</div>
<!-- 导入 vue 脚本文件 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
// 创建 VM 实例对象
const vm = new Vue({
// 指定当前 VM 要控制的区域
el: '#app',
// 数据源
data: {
// 计数器的值
count: 0,
},
methods: {
// 点击按钮,让 count 自增 +1
addCount(e) {
const nowBgColor = e.target.style.backgroundColor
e.target.style.backgroundColor = nowBgColor === 'red' ? '' : 'red'
this.count += 1
},
},
})
</script>
绑定事件并传参
在使用 v-on 指令绑定事件时,可以使用 ( ) 进行传参,示例代码如下:
<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
<h3>count 的值为:{{count}}</h3>
<button @click="addCount(2, $event)">+2</button>
</div>
<!-- 导入 vue 脚本文件 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
// 创建 VM 实例对象
const vm = new Vue({
// 指定当前 VM 要控制的区域
el: '#app',
// 数据源
data: {
// 计数器的值
count: 0,
},
methods: {
addCount(step, e) {
const bgColor = e.target.style.backgroundColor
e.target.style.backgroundColor = bgColor === 'red' ? '' : 'red'
this.count += step
},
},
})
</script>
事件修饰符
在事件处理函数中调用 preventDefault() 或 stopPropagation() 是非常常见的需求。因此,vue 提供了事件修饰符的概念,来辅助程序员更方便的对事件的触发进行控制。常用的 5 个事件修饰符如下:
事件修饰符 | 说明 |
---|---|
.prevent | 阻止默认行为(例如:阻止 a 连接的跳转、阻止表单的提交等) |
.stop | 阻止事件冒泡 |
.capture | 以捕获模式触发当前的事件处理函数 |
.once | 绑定的事件只触发1次 |
.self | 只有在 event.target 是当前元素自身时触发事件处理函数 |
<!-- 在页面中声明一个将要被 vue 所控制的 DOM 区域 -->
<div id="app">
<h4>① .prevent 事件修饰符的应用场景</h4>
<a href="https://www.baidu.com" @click.prevent="onLinkClick">百度首页</a>
<hr />
<h4>② .stop 事件修饰符的应用场景</h4>
<div class="outer" @click="onOuterClick">
外层的 div
<div class="inner" @click.stop="onInnerClick">内部的 div</div>
</div>
<hr />
<h4>③ .capture 事件修饰符的应用场景</h4>
<div class="outer" @click.capture="onOuterClick">
外层的 div
<div class="inner" @click="onInnerClick">内部的 div</div>
</div>
<hr />
<h4>④ .once 事件修饰符的应用场景</h4>
<div class="inner" @click.once="onInnerClick">内部的 div</div>
<hr />
<h4>⑤ .self 事件修饰符的应用场景</h4>
<div class="box" @click="onBoxClick">
最外层的 box
<div class="outer" @click.self="onOuterClick">
中间的 div
<div class="inner" @click="onInnerClick">内部的 div</div>
</div>
</div>
<hr />
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
// 声明处理函数的节点
methods: {
// 超链接的点击事件处理函数
onLinkClick() {
alert('ok')
},
// 点击了外层的 div
onOuterClick() {
console.log('触发了 outer 的 click 事件处理函数')
},
// 点击了内部的 div
onInnerClick() {
console.log('触发了 inner 的 click 事件处理函数')
},
onBoxClick() {
console.log('触发了 box 的 click 事件处理函数')
}
},
})
</script>
按键修饰符
在监听键盘事件时,我们经常需要判断详细的按键。此时,可以为键盘相关的事件添加按键修饰符,例如:
<div id="app">
<input type="text" @keyup.enter="submit" @keyup.esc="clearInput" />
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {},
methods: {
// 获取文本框最新的值
submit(e) {
console.log('摁下了 enter 键,最新的值是:' + e.target.value)
},
// 清空文本框的值
clearInput(e) {
e.target.value = ''
},
},
})
</script>
1.4 双向绑定指令
v-model
vue 提供了 v-model 双向数据绑定指令,用来辅助开发者在不操作 DOM 的前提下,快速获取表单的数据。
<div id="app">
<p>用户名是:{{username}}</p>
<input type="text" v-model="username" />
<hr />
<p>选中的省份是:{{province}}</p>
<select v-model="province">
<option value="">请选择</option>
<option value="1">北京</option>
<option value="2">河北</option>
<option value="3">黑龙江</option>
</select>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
// 姓名
username: 'zs',
// 省份
province: '1',
},
})
</script>
注意:v-model 指令只能配合表单元素一起使用!
v-model 指令的修饰符
为了方便对用户输入的内容进行处理,vue 为 v-model 指令提供了 3 个修饰符,分别是:
修饰符 | 作用 | 示例 |
---|---|---|
.number | 自动将用户的输入值转为数值类型 |
<div id="app">
姓名:<input type="text" v-model.trim="username" />
<hr />
年龄:<input type="text" v-model.number="age" />
<hr />
地址:<input type="text" v-model.lazy="address" />
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
// 姓名
username: 'zs',
// 年龄
age: 1,
// 地址
address: '北京市',
},
})
</script>
1.5 条件渲染指令
条件渲染指令用来辅助开发者按需控制 DOM 的显示与隐藏。条件渲染指令有如下两个,分别是:
- v-if
- v-show
<div id="app">
<button @click="flag = !flag">Toggle Flag</button>
<p v-if="flag">请求成功 --- 被 v-if 控制</p>
<p v-show="flag">请求成功 --- 被 v-show 控制</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
// flag 用来控制元素的显示与隐藏
// 值为 true 时显示元素
// 值为 false 时隐藏元素
flag: false,
},
})
</script>
v-if 和 v-show 的区别
实现原理不同:
- v-if 指令会动态地创建或移除 DOM 元素,从而控制元素在页面上的显示与隐藏;
- v-show 指令会动态为元素添加或移除 style=“display: none;” 样式,从而控制元素的显示与隐藏;
性能消耗不同:
v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。
- 如果需要非常频繁地切换,则使用 v-show 较好
- 如果在运行时条件很少改变,则使用 v-if 较好
v-else 和 v-else-if
v-if 可以单独使用,或配合 v-else 指令一起使用:
v-else-if 指令,顾名思义,充当 v-if 的“else-if 块”,可以连续使用:
<div id="app">
<p v-if="num > 0.5">随机数 > 0.5</p>
<p v-else>随机数 ≤ 0.5</p>
<hr />
<p v-if="type === 'A'">优秀</p>
<p v-else-if="type === 'B'">良好</p>
<p v-else-if="type === 'C'">一般</p>
<p v-else>差</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
// 生成 1 以内的随机数
num: Math.random(),
// 类型
type: 'A'
},
})
</script>
1.6 列表渲染指令
v-for
vue 提供了 v-for 指令,用来辅助开发者基于一个数组来循环渲染相似的 UI 结构。
v-for 指令需要使用 item in items 的特殊语法,其中:
- items 是待循环的数组
- item 是当前的循环项
v-for 中的索引
v-for 指令还支持一个可选的第二个参数,即当前项的索引。语法格式为 (item, index) in items,示例代码如下:
<div id="app">
<ul>
<li v-for="(user, i) in list">索引是:{{i}},姓名是:{{user.name}}</li>
</ul>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
// 用户列表的数据
list: [
{ id: 1, name: 'zs' },
{ id: 2, name: 'ls' },
],
},
})
</script>
注意:v-for 指令中的 item 项和 index 索引都是形参,可以根据需要进行重命名。例如 (user, i) in userlist
使用 key 维护列表的状态
当列表的数据变化时,默认情况下,vue 会尽可能的复用已存在的 DOM 元素,从而提升渲染的性能。但这种默认的性能优化策略,会导致有状态的列表无法被正确更新。
为了给 vue 一个提示,以便它能跟踪每个节点的身份,从而在保证有状态的列表被正确更新的前提下,提升渲染的性能。此时,需要为每项提供一个唯一的 key 属性:
<!-- 在页面中声明一个将要被 vue 所控制的 DOM 区域 -->
<div id="app">
<!-- 添加用户的区域 -->
<div>
<input type="text" v-model="name">
<button @click="addNewUser">添加</button>
</div>
<!-- 用户列表区域 -->
<ul>
<li v-for="(user, index) in userlist" :key="user.id">
<input type="checkbox" />
姓名:{{user.name}}
</li>
</ul>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
// 用户列表
userlist: [
{ id: 1, name: 'zs' },
{ id: 2, name: 'ls' }
],
// 输入的用户名
name: '',
// 下一个可用的 id 值
nextId: 3
},
methods: {
// 点击了添加按钮
addNewUser() {
this.userlist.unshift({ id: this.nextId, name: this.name })
this.name = ''
this.nextId++
}
},
})
</script>
key 的注意事项
- key 的值只能是字符串或数字类型
- key 的值必须具有唯一性(即:key 的值不能重复)
- 建议把数据项 id 属性的值作为 key 的值(因为 id 属性的值具有唯一性)
- 使用 index 的值当作 key 的值没有任何意义(因为 index 的值不具有唯一性)
- 建议使用 v-for 指令时一定要指定 key 的值(既提升性能、又防止列表状态紊乱)
2.过滤器
**过滤器(Filters)**常用于文本的格式化。例如:
hello -> Hello
过滤器从本质上可以理解为一个函数 :即“管道符”前待处理的参数作为过滤器函数的参数,返回值为处理后的值。
过滤器应该被添加在 JavaScript 表达式的尾部,由“管道符”进行调用,示例代码如下:
2.1 过滤器的简单使用
<div id="app"> <!-- 通过 过滤器 将 title 和 message 转换为 "首字符大写的形式" -->
<p :title="info | capitalize">{{message | capitalize}}</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'hello vue.js',
info: 'title info',
},
filters: {
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
})
</script>
2.2 私有过滤器和全局过滤器
在 filters 节点下定义的过滤器,称为“私有过滤器”,因为它只能在当前 vm 实例所控制的 el 区域内使用。如果希望在多个 vue 实例之间共享过滤器,则可以按照如下的格式定义全局过滤器:
<div id="app">
<p :title="info | capitalize">{{message | capitalize}}</p>
</div>
<div id="app2">
<p>{{abc | capitalize}}</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
// 全局过滤器
Vue.filter('capitalize', (str) => {
return str.charAt(0).toUpperCase() + str.slice(1) + '~~~'
})
</script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'hello vue.js',
info: 'title info',
},
// 私有过滤器,只能被当前 vm 所控制的区域所使用
filters: {
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
},
})
</script>
<script>
const vm2 = new Vue({
el: '#app2',
data: {
abc: 'abc'
}
})
</script>
注意:如果全局过滤器与私有过滤器函数名冲突,则以私有过滤器为准——(就近原则)
2.3连续调用多个过滤器
过滤器可以串联地进行调用,例如:
<div id="app">
<p :title="info | capitalize">{{message | capitalize | maxLength}}</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
// 全局过滤器
// 首字母转大写的过滤器
Vue.filter('capitalize', (str) => {
return str.charAt(0).toUpperCase() + str.slice(1)
})
// 定义控制文本长度的过滤器
Vue.filter('maxLength', (str) => {
if(str.length <= 10) return str
return str.slice(0, 10) + '...'
})
</script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'hello vue.js',
info: 'title info',
},
})
</script>
2.4 过滤器传参
过滤器的本质是 JavaScript 函数,因此可以接收参数,格式如下:
<div id="app">
<p :title="info | capitalize">{{message | capitalize | maxLength(3)}}</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
// 全局过滤器
// 首字母转大写的过滤器
Vue.filter('capitalize', (str) => {
return str.charAt(0).toUpperCase() + str.slice(1)
})
// 定义控制文本长度的过滤器
// 这个位置的 len = 10 是在没有传递第二个参数时,给 len 一个默认值为 10 。
Vue.filter('maxLength', (str, len = 10) => {
if(str.length <= len) return str
return str.slice(0, len) + '...'
})
</script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'hello vue.js',
info: 'title info',
},
})
</script>
2.5过滤器的兼容性
过滤器仅在 vue 2.x 和 1.x 中受支持,在 vue 3.x 的版本中剔除了过滤器相关的功能。
在企业级项目开发中:
如果使用的是 2.x 版本的 vue,则依然可以使用过滤器相关的功能
如果项目已经升级到了 3.x 版本的 vue,官方建议使用计算属性或方法代替被剔除的过滤器功能
具体的迁移指南,请参考 vue 3.x 的官方文档给出的说明:
https://v3.vuejs.org/guide/migration/filters.html#migration-strategy
四、第一个案例——品牌列表案例
1.案例描述
1.1 案例效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PBXmoDag-1685004795661)(D:.\assets\品牌列表案例——案例效果.png)]
2.2 用到的知识点
bootstrap 4.x 相关的知识点 | vue 指令与过滤器 相关的知识点 |
---|---|
卡片(Card)、表单相关(Forms)、按钮(Buttons)、表格(Tables) | 插值表达式、属性绑定、事件绑定、双向数据绑定、修饰符、条件渲染、列表渲染、全局过滤器 |
2.3 整体实现步骤
- 创建基本的 vue 实例
- 基于 vue 渲染表格数据
- 实现添加品牌的功能
- 实现删除品牌的功能
- 实现修改品牌状态的功能
2.案例实现
2.1 创建基本的 Vue 实例
步骤1:导入 vue 的 js 文件
<script src="./lib/vue-2.6.12.js"></script>
步骤2:在 标签中声明 el 区域
<div id="app">
步骤3:创建 vue 实例对象
<script> const vm = new Vue({ el: '#app', data: { brandlist: [ { id: 1, name: '宝马', state: true, addtime: new Date() }, { id: 2, name: '奥迪', state: true, addtime: new Date() }, { id: 3, name: '奔驰', state: true, addtime: new Date() }, ], }, }) </script>
2.2 基于 Vue 渲染表格数据
步骤1:使用 v-for 指令循环渲染表格的数据:
<!-- TODO:循环渲染表格的每一行数据 --> <tr v-for="(item, index) in brandlist" :key="item.id"> <td>{{index + 1}}</td> <td>{{item.brandname}}</td> <td>{{item.state}}</td> <td>{{item.addtime}}</td> <td> <a href="#">删除</a> </td> </tr>
步骤2:将品牌的状态渲染为 Switch 开关效果:
<td> <div class="custom-control custom-switch"> <input type="checkbox" class="custom-control-input" :id="item.id" v-model="item.state"> <label class="custom-control-label" :for="item.id" v-if="item.state">已启用</label> <label class="custom-control-label" :for="item.id" v-else>已禁用</label> </div> </td>
**Switch 开关效果的官方文档地址:**https://v4.bootcss.com/docs/components/forms/#switches
步骤3:使用全局过滤器对时间进行格式化:
<!-- 对 创建时间 这一项调用 dateFormat 过滤器 --> <td>{{item.addtime | dateFormat}}</td> <script> // 创建全局的过滤器 dateFormat Vue.filter('dateFormat', (dateStr) => { const dt = new Date(dateStr) const y = dt.getFullYear() const m = padZero(dt.getMonth() + 1) const d = padZero(dt.getDate()) const hh = padZero(dt.getHours()) const mm = padZero(dt.getMinutes()) const ss = padZero(dt.getSeconds()) // 模板字符串进行时间的格式拼接 return `${y}-${m}-${d} ${hh}:${mm}:${ss}` }) // 补零函数 padZero = (n) => { return n > 9 ? n : '0' + n } </script>
2.3 添加品牌
步骤1:阻止表单的默认提交行为:
<form class="form-inline" @submit.prevent>
步骤2:为 input 输入框进行 v-model 双向数据绑定:
<input type="text" class="form-control" placeholder="请输入品牌名称" v-model.trim="brandname" />
注意:需要在 data 数据中声明 brandname 属性字段。
data: { brandname: '', },
步骤3:为“添加品牌”的 button 按钮绑定 click 事件处理函数:
<button type="submit" class="btn btn-primary mb-2" @click="addNewbrand">添加品牌</button>
步骤4:在 data 中声明 nextId 属性(用来记录下一个可用的 id 值),并在 methods 中声明
addNewBrand 事件处理函数:
data: { nextId: 4, }, methods: { // 添加新的品牌数据 addNewbrand() { if (!this.brandname) return alert('品牌名称不能为空!') this.brandlist.push({ id: this.nextId, brandname: this.brandname, state: true, addtime: new Date() }) this.brandname = '' this.nextId++ }, },
步骤5:监听 input 输入框的 keyup 事件,通过 .esc 按键修饰符快速清空文本框中的内容:
<input type="text" class="form-control" placeholder="请输入品牌名称" v-model.trim="brandname" @keyup.esc="brandname = ''" />
2.4 删除品牌
步骤1:为删除的 a 链接绑定 click 点击事件处理函数,并阻止其默认行为:
<a href="#" @click.prevent="removeBrand(item.id)">删除</a>
步骤2:在 methods 节点中声明 removeBrand 事件处理函数如下:
// 删除品牌 根据id删除对应的数据 removeBrand() { this.brandlist = this.brandlist.filter(x => x.id !== id) },
3.案例最终代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<!-- 导入 bootstrap 的样式表 -->
<!-- https://v4.bootcss.com/docs/components/forms/#switches -->
<link rel="stylesheet" href="./lib/bootstrap.css" />
<style>
:root {
font-size: 13px;
}
body {
padding: 8px;
}
</style>
</head>
<body>
<div id="app">
<!-- 卡片区域 -->
<div class="card">
<h5 class="card-header">添加品牌</h5>
<div class="card-body">
<!-- 添加品牌的表单 -->
<form class="form-inline" @submit.prevent>
<div class="input-group mb-2 mr-sm-2">
<div class="input-group-prepend">
<div class="input-group-text">品牌名称</div>
</div>
<!-- 文本输入框 -->
<input type="text" class="form-control" placeholder="请输入品牌名称" v-model.trim="brandname"
@keyup.esc="brandname = ''" />
</div>
<!-- 提交表单的按钮 -->
<button type="submit" class="btn btn-primary mb-2" @click="addNewbrand">添加品牌</button>
</form>
</div>
</div>
<!-- 品牌列表 -->
<table class="table table-bordered table-striped mt-2">
<thead>
<tr>
<th>#</th>
<th>品牌名称</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<!-- 表格的主体区域 -->
<tbody>
<!-- TODO:循环渲染表格的每一行数据 -->
<tr v-for="(item, index) in brandlist" :key="item.id">
<td>{{index + 1}}</td>
<td>{{item.brandname}}</td>
<td>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" :id="item.id" v-model="item.state">
<label class="custom-control-label" :for="item.id" v-if="item.state">已启用</label>
<label class="custom-control-label" :for="item.id" v-else>已禁用</label>
</div>
</td>
<td>{{item.addtime | dateFormat}}</td>
<td>
<a href="#" @click.prevent="removeBrand(item.id)">删除</a>
</td>
</tr>
</tbody>
</table>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
// 创建全局的过滤器 dateFormat
Vue.filter('dateFormat', (dateStr) => {
const dt = new Date(dateStr)
const y = dt.getFullYear()
const m = padZero(dt.getMonth() + 1)
const d = padZero(dt.getDate())
const hh = padZero(dt.getHours())
const mm = padZero(dt.getMinutes())
const ss = padZero(dt.getSeconds())
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
})
// 补零函数
padZero = (n) => {
return n > 9 ? n : '0' + n
}
const vm = new Vue({
el: '#app',
data: {
brandname: '',
nextId: 4,
brandlist: [
{ id: 1, brandname: '宝马', state: true, addtime: new Date() },
{ id: 2, brandname: '奥迪', state: false, addtime: new Date() },
{ id: 3, brandname: '奔驰', state: true, addtime: new Date() },
],
},
methods: {
// 添加新的品牌数据
addNewbrand() {
if (!this.brandname) return alert('品牌名称不能为空!')
this.brandlist.push({
id: this.nextId,
brandname: this.brandname,
state: true,
addtime: new Date()
})
this.brandname = ''
this.nextId++
},
// 删除品牌
removeBrand(id) {
this.brandlist = this.brandlist.filter(x => x.id !== id)
},
},
})
</script>
</body>
</html>
五、Vue基础入门总结
一、能够知道 vue 的基本使用步骤
- 导入 vue.js 文件
- new Vue() 构造函数,得到 vm 实例对象
- 声明 el 和 data 数据节点
- MVVM 的对应关系
二、掌握 vue 中常见指令的基本用法
- 插值表达式、v-bind、v-on、v-if 和 v-else
- v-for 和 :key、v-model
三、掌握 vue 中过滤器的基本用法
- 全局过滤器 Vue.filter(‘过滤器名称’, function)
- 私有过滤器 filters 节点
Ⅱ vue组件基础(上)
目标 | 目录 |
---|---|
了解什么是单页面应用程序 | 六、单页面应用程序 |
了解如何用 vite 创建项目 | 七、vite 的基本使用 |
组件化开发的优点与好处 | 八、组件化开发思想 |
template、script、style三个节点 | 九、vue 组件的构成 |
组件的注册、样式冲突、props、动态绑定样式 | 十、组件的基本使用 |
实现一个简单组件的封装 | 十一、第二个案例——封装组件案例 |
总结与概括 | 十二、vue 组件基础(上)总结 |
六、单页面应用程序
1.什么是单页面应用程序
单页面应用程序(英文名:Single Page Application)简称 SPA,顾名思义,指的是一个 Web 网站中只有唯一的一个 HTML 页面,所有的功能与交互都在这唯一的一个页面内完成。
2.单页面应用程序的特点
单页面应用程序将所有的功能局限于一个 web 页面中,仅在该 web 页面初始化时加载相应的资源( HTML、JavaScript 和 CSS)。
一旦页面加载完成了,SPA 不会因为用户的操作而进行页面的重新加载或跳转。而是利用 JavaScript 动态地变换HTML 的内容,从而实现页面与用户的交互。
3.单页面应用程序的优缺点
SPA 单页面应用程序最显著的 3 个优点如下:
① 良好的交互体验
🤞单页应用的内容的改变不需要重新加载整个页面
🤞获取数据也是通过 Ajax 异步获取
🤞没有页面之间的跳转,不会出现“白屏现象”
② 良好的前后端工作分离模式
🤞后端专注于提供 API 接口,更易实现 API 接口的复用
🤞前端专注于页面的渲染,更利于前端工程化的发展
③ 减轻服务器的压力
🤞服务器只提供数据,不负责页面的合成与逻辑的处理,吞吐能力会提高几倍
任何一种技术都有自己的局限性,对于 SPA 单页面应用程序来说,主要的缺点有如下两个:
① 首屏加载慢
解决方式:
🤞路由懒加载
🤞代码压缩
🤞CDN 加速
🤞网络传输压缩
② 不利于 SEO
解决方式:
🤞SSR 服务器端渲染
4.如何快速创建Vue的SPA项目
vue 官方提供了两种快速创建工程化的 SPA 项目的方式:
① 基于 vite 创建 SPA 项目
② 基于 vue-cli 创建 SPA 项目
vite | vue-cli | |
---|---|---|
支持的 vue 版本 | 仅支持 vue3.x | 支持 3.x 和 2.x |
是否基于 webpack | 否 | 是 |
运行速度 | 快 | 较慢 |
功能完整度 | 小而巧(逐渐完善) | 大而全 |
是否建议在企业级开发中使用 | 目前不建议 | 建议在企业级开发中使用 |
七、vite的基本使用
1.创建 vite 项目
基于 vite 创建 vue 3.x 的工程化项目
在你想创建项目的位置打开 PowerShell 窗口,输入:
npm init vite-app 项目名称
然后 cd 到项目目录中:
cd 项目名称
安装 npm 依赖包:
npm i
运行 dev 启动项目:
npm run dev
2.梳理项目的结构

其中:
🤞node_modules 目录用来存放第三方依赖包
🤞public 是公共的静态资源目录
🤞src 是项目的源代码目录(程序员写的所有代码都要放在此目录下)
🤞assets 目录用来存放项目中所有的静态资源文件(css、fonts等)
🤞components 目录用来存放项目中所有的自定义组件
🤞App.vue 是项目的根组件
🤞index.css 是项目的全局样式表文件
🤞main.js 是整个项目的打包入口文件
🤞.gitignore 是 Git 的忽略文件
🤞index.html 是 SPA 单页面应用程序中唯一的 HTML 页面
🤞package.json 是项目的包管理配置文件
3. vite 项目的运行流程
在工程化的项目中,vue 要做的事情很单纯:通过 main.js 把 App.vue 渲染到 index.html 的指定区域中。
其中:
① App.vue 用来编写待渲染的模板结构
② index.html 中需要预留一个 el 区域
③ main.js 把 App.vue 渲染到了 index.html 所预留的区域中
3.1 在 App.vue 中编写模板结构
<template>
<h1>这是app.vue根组件</h1>
<h3>avc</h3>
</template>
3.2 在 index.html 中预留 el 区域
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body
3.3 在 main.js 中把 App.vue 渲染到 index.html 所预留的区域中
// 1.按需导入 createApp 函数
import { createApp } from 'vue'
// 2.导入待渲染的 App.vue 组件
import App from './App.vue'
// 3.调用 createApp 函数,创建 SPA 应用的实例
const app = createApp(App)
// 4.调用 mount() 方法把 App 组件的模板结构,渲染到指定的 el 区域中
app.mount('#app')
八、组件化开发思想
1.什么是组件化开发思想
组件化开发指的是:根据封装的思想,把页面上可重用的部分封装为组件,从而方便项目的开发和维护。
例如:http://www.ibootstrap.cn/ 所展示的效果,就契合了组件化开发的思想。用户可以通过拖拽组件的方式,快速生成一个页面的布局结构。
2.前端组件化开发的好处
前端组件化开发的好处主要体现在以下两方面:
- 提高了前端代码的复用性和灵活性
- 提升了开发效率和后期的可维护性
3.vue中的组件化开发
vue 是一个完全支持组件化开发的框架。vue 中规定组件的后缀名是 .vue。之前接触到的 App.vue 文件本质上就是一个 vue 的组件。
九、vue 组件的构成
1.vue 组件组成结构
每个 .vue 组件都由 3 部分构成,分别是:
- template -> 组件的模板结构
- script -> 组件的 JavaScript 行为
- style -> 组件的样式
其中,每个组件中必须包含 template 模板结构,而 script 行为和 style 样式是可选的组成部分。
2.组件的 template 节点
vue 规定:每个组件对应的模板结构,需要定义到 节点中。
<template>
<!-- 当前组件的 DOM 结构,需要定义到 template 标签的内部 -->
</template>
注意: 是 vue 提供的容器标签,只起到包裹性质的作用,它不会被渲染为真正的 DOM 元素。
2.1 在 template 中使用指令
在组件的 节点中,支持使用前面所学的指令语法,来辅助开发者渲染当前组件的 DOM 结构。
2.2 在 template 中定义根节点
在 vue 2.x 的版本中, 节点内的 DOM 结构仅支持单个根节点
但是,在 vue 3.x 的版本中, 中支持定义多个根节点
3.组件的 script 节点
vue 规定:组件内的
script 节点的基本结构如下:
<script>
// 今后,组件相关的 data 数据, methods 方法等
// 都需要的定义到 export default 所导出的对象中。
export default {}
</script>
3.1 script 中的 name 节点
可以通过 name 节点为当前组件定义一个名称:每个首字母大写
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XLeyXqTG-1685004795662)(.\assets\Snipaste_2023-05-02_16-25-54.png)]
在使用 vue-devtools 进行项目调试的时候,自定义的组件名称可以清晰的区分每个组件:

3.2 script 中的 data 节点
vue 组件渲染期间需要用到的数据,可以定义在 data 节点中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5VcU35f1-1685004795662)(.\assets\Snipaste_2023-05-02_16-27-13.png)]
vue 规定:组件中的 data 必须是一个函数,不能直接指向一个数据对象。
3.3 script 中的 methods 节点
组件中的事件处理函数,必须定义到 methods 节点中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mGM1CQFC-1685004795662)(.\assets\Snipaste_2023-05-02_17-14-01.png)]
4.组件的 style 节点
vue 规定:组件内的
<style lang="css">
h1{
font-weight: normal;
}
</style>
其中
4.1 让 style 中支持 less 语法
如果希望使用 less 语法编写组件的 style 样式,可以按照如下两个步骤进行配置:
① 运行 npm install less -D 命令安装依赖包,从而提供 less 语法的编译支持
② 在
<style lang="less">
h1{
font-weight: normal;
i{
color: red;
font-style: normal;
}
}
</style>
十、组件的基本使用
1.组件的注册
组件之间可以进行相互的引用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yn2STzcv-1685004795663)(./assets/Snipaste_2023-05-02_19-34-03.png)]
vue 中组件的引用原则:先注册后使用。
1.1 注册组件的两种方式
vue 中注册组件的方式分为“全局注册”和“局部注册”两种,其中:
- 被全局注册的组件,可以在全局任何一个组件内使用
- 被局部注册的组件,只能在当前注册的范围内使用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nwCmvBt1-1685004795663)(./assets/Snipaste_2023-05-02_19-34-16.png)]
1.2 全局注册组件
// 1.按需导入 createApp 函数
import { createApp } from 'vue'
// 2.导入待渲染的 App.vue 组件
import App from './App.vue'
// ① 导入需要被全局注册的组件
import Swiper from './components/01.globalReg/Swiper.vue'
import Test from './components/01.globalReg/Test.vue'
// 3.调用 createApp 函数,创建 SPA 应用的实例
const app = createApp(App)
// ② 全局注册组件
app.component('my-swiper', Swiper)
app.component('my-test', Test)
// 4.调用 mount() 方法把 App 组件的模板结构,渲染到指定的 el 区域中
app.mount('#app')
1.3 使用全局注册组件
使用app.component() 方法注册的全局组件,直接以标签的形式进行使用即可
<my-swiper></my-swiper>
<my-test></my-test>
1.4 局部注册组件
<template>
<my-search></my-search>
</template>
export default {
components: {
"my-search": Search,
},
}
1.5 全局注册和局部注册的区别
- 被全局注册的组件,可以在全局任何一个组件内使用
- 被局部注册的组件,只能在当前注册的范围内使用
应用场景:
如果某些组件在开发期间的使用频率很高,推荐进行全局注册;
如果某些组件只在特定的情况下会被用到,推荐进行局部注册。
1.6 组件注册时名称的大小写
在进行组件的注册时,定义组件注册名称的方式有两种:
①使用kebab-case命名法(俗称短横线命名法,例如my-swiper 和my-search)
②使用PascalCase命名法(俗称帕斯卡命名法或大驼峰命名法,例如MySwiper和MySearch)
短横线命名法的特点:
- 必须严格按照短横线名称进行使用
帕斯卡命名法的特点:
- 既可以严格按照帕斯卡名称进行使用,又可以转化为短横线名称进行使用
注意:在实际开发中,推荐使用帕斯卡命名法为组件注册名称,因为它的适用性更强。
1.7 通过 name 属性注册组件
在注册组件期间,除了可以直接提供组件的注册名称之外,还可以把组件的name 属性作为注册后组件的名称,示例代码如下:
app.component(Test.name, Test)
2.组件之间的样式冲突问题
默认情况下,写在.vue 组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题。导致组件
之间样式冲突的根本原因是:
①单页面应用程序中,所有组件的DOM 结构,都是基于唯一的index.html 页面进行呈现的
②每个组件中的样式,都会影响整个index.html 页面中的DOM 元素
2.1 解决样式冲突的问题
为每个组件分配唯一的自定义属性,在编写组件样式时,通过属性选择器来控制样式的作用域,示例代码如下:
<template>
<div class="container" data-v-001>
<h3 data-v-001>轮播图组件</h3>
</div>
</template>
<style>
/* 通过中括号“属性选择器”,来防止组件之间的样式冲突问题,
因为每个组件分配的自定义属性是“唯一”的 */
.container[data-v-001]{
border: 1px solid red;
}
</style>
2.2 style 节点的 scoped 属性
为了提高开发效率和开发体验,vue 为style 节点提供了scoped属性,从而防止组件之间的样式冲突问题:
<style lang="less" scoped>
p {
color: red;
}
</style>
2.3 /deep/ 样式穿透
如果给当前组件的style 节点添加了scoped 属性,则当前组件的样式对其子组件是不生效的。如果想让某些样式对子组件生效,可以使用**/deep/ 深度选择器**。
注意:/deep/是vue2.x 中实现样式穿透的方案。在vue3.x 中推荐使用:**deep()**替代/deep/。
<style lang="less" scoped>
p {
color: red;
}
/* /deep/ .title {
color: blue;
}*/
:deep(.title) {
color: blue;
}
</style>
3.组件的 props
为了提高组件的复用性,在封装vue 组件时需要遵守如下的原则:
- 组件的DOM 结构、Style 样式要尽量复用
- 组件中要展示的数据,尽量由组件的使用者提供
为了方便使用者为组件提供要展示的数据,vue 组件提供了props 的概念。
3.1 什么是组件的 props
props 是组件的自定义属性,组件的使用者可以通过props 把数据传递到子组件内部,供子组件内部进行使用。
props 的作用:父组件通过props 向子组件传递要展示的数据。
props 的好处:提高了组件的复用性。
代码示例如下:
<!-- 通过自定义 props,把文章的标题和作者,传递到 my-article -->
<my-article title="面朝大海,春暖花开" author="海子"></my-article>
3.2 在组件中声明 props
在封装vue 组件时,可以把动态的数据项声明为props自定义属性。自定义属性可以在当前组件的模板结构中被直接使用。
<!-- my-article 组件的定义如下: -->
<template>
<h3>标题:{{title}}</h3>
<h3>作者:{{author}}</h3>
</template>
<script>
export default {
props: ['title','author'], //父组件传递 my-article 组件的数据,必须在 props 节点中声明
}
</script>
3.3 无法使用未声明的 props
如果父组件给子组件传递了未声明的props 属性,则这些属性会被忽略,无法被子组件使用
<!-- my-article 组件的定义如下: -->
<template>
<h3>标题:{{title}}</h3>
<h3>作者:{{author}}</h3>
</template>
<script>
export default {
name: 'MyArticle',
// 外界可以传递指定的数据,到当前的组件中
props: ['title'], // author 属性没有声明,因此子组件中无法访问到 author 的值
}
</script>
3.4 动态绑定 props 的值
可以使用v-bind 属性绑定的形式,为组件动态绑定props 的值
<my-article :title="info.title" :author="'post by ' + info.author" pub-time="1989"></my-article>
3.5 props 的大小写命名
组件中如果使用“camelCase(驼峰命名法)”声明了props 属性的名称,则有两种方式为其绑定属性的值
<template>
<!-- 两种都可以 -->
<!-- 短横线分割命名 -->
<my-article :title="info.title" :author="'post by ' + info.author" pub-time="1989"></my-article>
<!-- 驼峰命名 -->
<my-article :title="info.title" :author="'post by ' + info.author" pubTime="1989"></my-article>
</template>
<script>
export default {
name: 'MyArticle',
// 外界可以传递指定的数据,到当前的组件中
props: ['title', 'author', 'pubTime']
}
</script>
4. Class 和 Style 绑定
在实际开发中经常会遇到动态操作元素样式的需求。因此,vue 允许开发者通过 v-bind 属性绑定指令,为元素动态绑定 class 属性的值和行内的 style 样式。
4.1 动态绑定 HTML 的 class
可以通过三元表达式,动态的为元素绑定 class 的类名。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yJe7Lyqk-1685004795663)(./assets/Snipaste_2023-05-06_20-02-35.png)]
4.2 以数组语法绑定 HTML 的 class
如果元素需要动态绑定多个 class 的类名,此时可以使用数组的语法格式:
<h3 class="thin" :class="[isItalic ? 'italic' : '', isDelete ? 'delete' : '']">MyStyle 组件</h3>
<script>
export default {
name: 'MyStyle',
data() {
return {
// 字体是否倾斜
isItalic: false,
// 是否应用删除效果
isDelete: false,
}
},
}
</script>
<style lang="less">
// 字体变细
.thin {
font-weight: 200;
}
// 倾斜字体
.italic {
font-style: italic;
}
.delete {
text-decoration: line-through;
}
</style>
4.3 以对象语法绑定 HTML 的 class
<template>
<h3 class="thin" :class="classObj">MyStyle 组件</h3>
<button @click="classObj.italic = !classObj.italic">Toggle Italic</button>
<button @click="classObj.delete = !classObj.delete">Toggle Delete</button>
</template>
<script>
export default {
name: 'MyStyle',
data() {
return {
// 字体是否倾斜
isItalic: false,
// 是否应用删除效果
isDelete: false,]
classObj: {
italic: false,
delete: false,
},
}
},
}
</script>
<style lang="less">
// 字体变细
.thin {
font-weight: 200;
}
// 倾斜字体
.italic {
font-style: italic;
}
.delete {
text-decoration: line-through;
}
</style>
4.4 以对象语法绑定内联的 style
<template>
<div :style="{ color: active, fontSize: fsize + 'px', 'background-color': bgcolor }">黑马程序员</div>
<button @click="fsize+=1">字号 +1</button>
<button @click="fsize-=1">字号 -1</button>
</template>
<script>
export default {
name: 'MyStyle',
data() {
return {
// 高亮时的文本颜色
active: 'red',
// 文字的大小
fsize: 30,
// 背景颜色
bgcolor: 'pink',
},
}
},
}
</script>
十一、第二个案例——封装组件案例
1.案例描述
1.1 案例效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z6gMlngQ-1685004795664)(./assets/Snipaste_2023-05-07_11-15-55.png)]
封装要求:
- 允许用户自定义 title 标题
- 允许用户自定义 bgcolor 背景色
- 允许用户自定义 color 文本颜色
- MyHeader 组件需要在页面顶部进行 fixed 固定定位,且 z-index 等于 999
使用示例如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4hvWeDkp-1685004795664)(./assets/Snipaste_2023-05-07_11-16-16.png)]
1.2 用到的知识点
- 组件的封装与注册
- props
- 样式绑定
1.3 整体实现步骤
- 创建 MyHeader 组件
- 渲染 MyHeader 组件的基本结构
- 在 App 组件中注册并使用 MyHeader 组件
- 通过 props 为组件传递数据
2.案例实现代码
<!-- MyHeader.vue组件部分 -->
<template>
<div
class="header-container"
:style="{ backgroundColor: bgcolor, color: color }"
>
xxx
</div>
{{ title || "Header 组件" }}
</template>
<script>
export default {
name: "MyHeader",
props: ["title", "bgcolor", "color"],
};
</script>
<style lang="less" scoped>
.header-container {
height: 45px;
background-color: pink;
text-align: center;
line-height: 45px;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 999;
}
</style>
<!-- app.vue部分 -->
<template>
<div class="app-container">
<h1>App根组件</h1>
</div>
<hr />
<MyHeader title="黑马程序员" bgcolor="blue" color="white"></MyHeader>
</template>
<script>
import MyHeader from "./06.MyHeader/MyHeader.vue";
export default {
name: "MyApp",
components: {
MyHeader,
},
};
</script>
<style lang="less" scoped>
.app-container {
margin-top: 45px;
}
</style>
十二、vue 组件基础(上)总结
① 能够说出什么是单页面应用程序及组件化开发
🤞SPA、只有一个页面、组件是对 UI 结构的复用
② 能够说出 .vue 单文件组件的组成部分
🤞template、script、style(scoped、lang)
③ 能够知道如何注册 vue 的组件
🤞全局注册(app.component)、局部注册(components)
④ 能够知道如何声明组件的 props 属性
🤞props 数组
④ 能够知道如何在组件中进行样式绑定
🤞动态绑定 class、动态绑定 style
Ⅲ vue组件基础(下)
目标 | 目录 |
---|---|
能够知道如何对 props 进行验证 | 十三、props 验证 |
能够知道如何使用计算属性 | 十四、计算属性 |
能够知道如何为组件自定义事件 | 十五、自定义事件 |
能够知道如何在组件上使用 v-model | 十六、组件上的 v-model |
实现任务列表案例 | 十七、任务列表案例 |
总结和概括 | 十八、vue 组件基础(下)总结 |
十三、props验证
1.什么是 props 验证
props 验证指的是:在封装组件时对外界传递过来的 props 数据进行合法性的校验,从而防止数据不合法的问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rAIVbEBd-1685004795665)(./assets/Snipaste_2023-05-07_13-45-04.png)]
使用数组类型的 props 节点的缺点:无法为每个 prop 指定具体的数据类型。
2.对象类型的 props 节点
使用对象类型的 props 节点,可以对每个 prop 进行数据类型的校验,示意图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X5IgXS1i-1685004795665)(./assets/Snipaste_2023-05-07_13-48-11.png)]
3.props 验证
对象类型的 props 节点提供了多种数据验证方案,例如:
① 基础的类型检查
② 多个可能的类型
③ 必填项校验
④ 属性默认值
⑤ 自定义验证函数
3.1 基础的类型检查
可以直接为组件的 prop 属性指定基础的校验类型,从而防止组件的使用者为其绑定错误类型的数据:
<script>
export default{
props:{
propA: String, // 字符串类型
propB: Number, // 数字类型
propC: Boolean, // 布尔值类型
propD: Array, // 数组类型
propE: Object, // 对象类型
propF: Date, // 日期类型
propG: Function,// 函数类型
propH: Symbol, // 符号类型
}
}
</script>
3.2 多个可能的类型
如果某个 prop 属性值的类型不唯一,此时可以通过数组的形式,为其指定多个可能的类型:
<script>
export default{
props:{
// propA 属性的值可以是“字符串”或“数字”
propA: [String,Number]
}
}
</script>
3.3 必填项校验
如果组件的某个 prop 属性是必填项,必须让组件的使用者为其传递属性的值。此时,可以通过如下的方式将其设置为必填项:
<script>
export default{
props: {
// 通过“配置对象”的形式,来定义 propB 属性的“验证规则”
propB: {
type: Number, // 当前属性的值必须是 String 字符串类型
required: true, // 当前属性的值是必填项,如果使用者没指定 propB 属性的值,则在终端进行警告提示
},
}
}
</script>
3.4 属性默认值
在封装组件时,可以为某个 prop 属性指定默认值:
<script>
export default{
props: {
// 通过“配置对象”的形式,来定义 propC 属性的“验证规则”
propC: {
type: Number,
default: 100, // 如果使用者没有指定 propC 的值,则 propC 属性的默认值为 100
},
}
}
</script>
3.5 自定义验证函数
在封装组件时,可以为 prop 属性指定自定义的验证函数,从而对 prop 属性的值进行更加精确的控制:
<script>
export default{
props: {
// 通过“配置对象”的形式,来定义 propC 属性的“验证规则”
propD: {
// 通过 calidator 函数,对 propD 属性的值进行校验,“属性的值”可以通过形参 value 进行接收
validator(value) {
// propD 属性的值,必须匹配下列字符串中的一个
// validator 函数的返回值为 true 表示验证通过,false 表示验证失败
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
}
</script>
十四、计算属性
1.什么是计算属性
计算属性本质上就是一个 function 函数,它可以实时监听 data 中数据的变化,并 return 一个计算后的新值,供组件渲染 DOM 时使用。
2.如何声明计算属性
计算属性需要以 function 函数的形式声明到组件的 computed 选项中:
<template>
<div>
<input type="text" v-model.number="count" />
<p>{{ count }} 乘以 2 的值为:{{ plus }}</p>
</div>
</template>
<script>
export default {
name: 'MyCounter',
data() {
return {
count: 1,
}
},
computed: {
plus() {
console.log('计算属性被执行了')
return this.count * 2
},
},
}
</script>
注意:计算属性侧重于得到一个计算的结果,因此计算属性中必须有 return 返回值!
3.计算属性的使用注意点
① 计算属性必须定义在 computed 节点中
② 计算属性必须是一个 function 函数
③ 计算属性必须有返回值
④ 计算属性必须当做普通属性使用
4.计算属性和方法的区别
相对于方法来说,计算属性会缓存计算的结果,只有计算属性的依赖项发生变化时,才会重新进行运算。因此计算属性的性能更好:
<script>
export default{
computed: {
plus() {
console.log('计算属性被执行了')
return this.count * 2
},
},
methods: {
plus() {
console.log('方法被执行了')
return this.count * 2
}
}
}
</script>
5.计算属性案例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AmWjnVuN-1685004795665)(./assets/Snipaste_2023-05-07_15-58-25.png)]
5.1 动态计算已勾选商品的总数量
<template>
<!-- TODO: 1. 动态计算已勾选的商品的总数量 -->
<span>总数量:{{ total }}</span>
</template>
<script>
export default {
computed: {
// 动态计算出勾选水果的总数量
total() {
let t = 0;
this.fruitlist.forEach((x) => {
if (x.state) {
t += x.count;
}
})
return t;
},
},
}
</script>
5.2 动态计算已勾选的商品的总价
<template>
<!-- TODO: 2. 动态计算已勾选的商品的总价 -->
<span>总价:{{ amount }}</span>
</template>
<script>
export default {
computed: {
// 动态计算已勾选的商品的总价
amount(){
let a = 0;
this.fruitlist
.filter((x) => x.state)
.forEach((x) => {
a += x.price * x.count;
});
return a;
},
},
}
</script>
5.3 控制按钮的禁用状态
<template>
<!-- TODO: 3. 动态计算按钮的禁用状态 -->
<button type="button" class="btn btn-primary" :disabled="isdisabled">
</template>
<script>
export default {
computed: {
// 控制按钮的禁用状态
isdisabled() {
return this.total === 0;
},
},
};
</script>
十五、自定义事件
1.什么是自定义事件
在封装组件时,为了让组件的使用者可以监听到组件内状态的变化,此时需要用到组件的自定义事件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K4L8Ciia-1685004795665)(./assets/Snipaste_2023-05-08_14-19-26.png)]
2.自定义事件的3个使用步骤
在封装组件时:
① 声明自定义事件
② 触发自定义事件
在使用组件时:
③ 监听自定义事件
2.1 声明自定义事件
开发者为自定义组件封装的自定义事件,必须事先在 emits 节点中声明:
<template>
<div>
<p>count 的值是:{{ count }}</p>
<button @click="add">+1</button>
</div>
</template>
<script>
export default {
name: 'MyCounter',
// 1. 声明自定义事件
emits: ['countChange'],
data() {
return {
count: 0,
}
},
}
</script>
2.2 触发自定义事件
在 emits 节点下声明的自定义事件,可以通过 this.$emit(‘自定义事件的名称’) 方法进行触发:
<template>
<div>
<p>count 的值是:{{ count }}</p>
<button @click="add">+1</button>
</div>
</template>
<script>
export default {
name: 'MyCounter',
methods: {
add() {
this.count++
// 2. this.$emit() 触发自定义事件
this.$emit('countChange')
},
},
}
</script>
2.3 监听自定义事件
在使用自定义的组件时,可以通过 v-on 的形式监听自定义事件:
<template>
<div>
<h1>app 根组件</h1>
<hr />
<my-counter @countChange="getCount"></my-counter>
</div>
</template>
<script>
export default {
name: 'MyApp',
methods: {
getCount() {
console.log('触发了 countChange 自定义事件')
},
},
components: {
MyCounter,
},
}
</script>
3.自定义事件传参
在调用 this.$emit() 方法触发自定义事件时,可以通过第 2 个参数为自定义事件传参,示例代码如下:
<script>
this.$emit('countChange', this.count) // 触发自定义事件时,通过第二个参数传参
</script>
<script>
export default {
name: 'MyApp',
methods: {
getCount(val) {
console.log('触发了 countChange 自定义事件', val)
},
},
components: {
MyCounter,
},
}
</script>
十六、组件上的 v-model
1. 为什么需要在组件上使用 v-model
v-model 是双向数据绑定指令,当需要维护组件内外数据的同步时,可以在组件上使用 v-model 指令。示意图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lldOBzlP-1685004795666)(./assets/Snipaste_2023-05-08_15-16-00.png)]
- 外界数据的变化会自动同步到 counter 组件中
- counter 组件中数据的变化,也会自动同步到外界
2. 在组件上使用 v-model 的步骤
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4RTfAPQC-1685004795666)(./assets/Snipaste_2023-05-08_15-20-25.png)]
父 -》子 同步数据
① 父组件通过 v-bind: 属性绑定的形式,把数据传递给子组件
② 子组件中,通过 props 接收父组件传递过来的数据
子 -》父 同步数据
① 在 v-bind: 指令之前添加 v-model 指令
② 在子组件中声明 emits 自定义事件,格式为 update:xxx
③ 调用 $emit() 触发自定义事件,更新父组件中的数据
十七、第三个案例——任务列表案例
1.案例效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IHqRMJfO-1685004795666)(./assets/Snipaste_2023-05-08_15-41-36.png)]
2.用到的知识点
① vite 创建项目
② 组件的封装与注册
③ props
④ 样式绑定
⑤ 计算属性
⑥ 自定义事件
⑦ 组件上的 v-model
3.整体实现步骤
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j2vAdedr-1685004795666)(./assets/Snipaste_2023-05-08_15-48-46.png)]
① 使用 vite 初始化项目
② 梳理项目结构
③ 封装 todo-list 组件
④ 封装 todo-input 组件
⑤ 封装 todo-button 组件
4.具体实现
4.1 初始化项目
1.在终端运行以上的命令,初始化 vite 项目:
npm init vite-app todo
2.使用 vscode 打开项目,并安装依赖项:
npm install
3.安装 less 语法相关的依赖项:
npm i less -D
4.2 梳理项目结构
1.重置 index.css 中的全局样式如下:
:root { font-size: 12px; } body { padding: 8px; }
2.重置 App.vue 组件的代码结构如下:
<template> <div> <h1>App 根组件</h1> </div> </template> <script> export default { name: "MyApp", data() { return { // 任务列表数据 todolist: [ { id: 1, task: "周一早晨9点开会", done: false }, { id: 2, task: "周一晚上8点聚餐", done: false }, { id: 3, task: "准备周三上午的演讲稿", done: true }, ], }; }, }; </script> <style lang="less" scoped></style>
3.删除 components 目录下的 HelloWorld.vue 组件。
4.在终端运行以下的命令,把项目运行起来:
npm run dev
4.3 封装 todo-list 组件
4.3.1 创建并注册 TodoList 组件
1.在 src/components/todo-list/ 目录下新建 TodoList.vue 组件:
<template> <div>TodoList 组件</div> </template> <script> export default { name: "TodoListVue", }; </script> <style lang="less" scoped> </style>
2.在 App.vue 组件中导入并注册 TodoList.vue 组件:
<template> <!-- 使用 TodoList 组件 --> <todo-list-vue></todo-list-vue> </template> <script> // 导入 TodoList 组件 import TodoListVue from "./components/TodoList.vue"; export default { name: "MyApp", // 注册 TodoList 组件 components: { TodoListVue, }, }; </script>
4.3.2 基于 bootstrap 渲染列表组件
1.在 main.js 入口文件中,导入 src/assets/css/bootstrap.css 样式表:
import { createApp } from 'vue' import App from './App.vue' // 导入 bootstrap.css 样式表 import './assets/css/bootstrap.css' import './index.css' createApp(App).mount('#app')
2.根据 bootstrap 提供的列表组(https://v4.bootcss.com/docs/components/list-group/#with-badges)和复选框(https://v4.bootcss.com/docs/components/forms/#checkboxes-and-radios-1)渲染列表组件和基本效果:
<template> <!-- 列表组 --> <ul class="list-group"> <li class="list-group-item d-flex justify-content-between align-items-center"> <!-- 复选框 --> <div class="custom-control custom-checkbox"> <input type="checkbox" class="custom-control-input" id="customCheck1"> <label class="custom-control-label" for="customCheck1">Check this custom checkbox</label> </div> <!-- 徽标 badge 效果 --> <span class="badge badge-success badge-pill">完成</span> <span class="badge badge-warning badge-pill">未完成</span> </li> </ul> </template>
4.3.3 为 TodoList 声明 props 属性
1.为了接受外界传递过来的列表数据,需要在 TodoList 组件中声明如下的 props 属性:
<script> export default { name: "TodoListVue", props: { // 列表数据 list: { type: Array, required: true, default: [], } } }; </script>
2.在 App 组件中通过 list 属性,将数据传递到 TodoList 组件之中:
<template> <div> <h1>App 根组件</h1> <todo-list-vue :list="todolist"></todo-list-vue> </div> </template>
4.3.4 渲染列表的 DOM 结构
1.通过 v-for 指令,循环渲染列表的 DOM 结构:
<template> <!-- 列表组 --> <ul class="list-group"> <li class="list-group-item d-flex justify-content-between align-items-center" v-for="item in list" :key="item.id"> <!-- 复选框 --> <div class="custom-control custom-checkbox"> <input type="checkbox" class="custom-control-input" :id="item.id" > <label class="custom-control-label" for="customCheck1">{{item.task}}</label> </div> <!-- 徽标 --> <span class="badge badge-success badge-pill">完成</span> <span class="badge badge-warning badge-pill">未完成</span> </li> </ul> </template>
2.通过 v-if 和 v-else 指令,按需渲染 badge 效果:
<!-- 徽标 --> <span class="badge badge-success badge-pill" v-if="item.done">完成</span> <span class="badge badge-warning badge-pill" v-else>未完成</span>
3.通过 v-model 指令,双向绑定任务的完成状态:
<!-- 复选框 --> <div class="custom-control custom-checkbox"> <input type="checkbox" class="custom-control-input" :id="item.id" v-model="item.done"> <label class="custom-control-label" :for="item.id">{{item.task}}</label> </div>
4.通过 v-bind 属性绑定,动态切换元素的 class 类名:
<label class="custom-control-label" :class="item.done ? 'delete' : ''" :for="item.id">{{item.task}}</label>
在 TodoList 组件中声明如下的样式,美化当前组件的 UI 结构:
<style lang="less" scoped> .list-group{ width: 400px; } .delete{ text-decoration: line-through; color: gray; font-style: italic; } </style>
4.4 封装 todo-input 组件
4.4.1 创建并注册 TodoInput 组件
1.在 src /components/todo-input/ 目录下新建 TodoInput 组件:
<template> <div>TodoInputVue 组件</div> </template> <script> export default { name: 'TodoInputVue', } </script> <style lang="less" scoped> </style>
2.在 App.vue 组件中导入并注册 TodoInput.vue 组件:
<script> // 导入 TodoList 组件 import TodoList from "./components/todo-list/TodoList.vue"; // 导入 TodoInput 组件 import TodoInput from "./components/todo-input/TodoInput.vue"; export default { name: "MyApp", // 注册私有组件 components: { TodoList, TodoInput, }, }; </script>
3.在 App.vue 的 template 模板结构中使用注册的 TodoInput 组件:
<template> <div> <h1>App 根组件</h1> <hr> <!-- 使用 TodoInput 组件 --> <todo-input></todo-input> <!-- 使用 TodoList 组件 --> <todo-list :list="todolist" class="mt-2"></todo-list> </div> </template>
4.4.2 基于bootstrap 渲染组件结构
1.根据 bootstrap 提供的 inline-forms(https://v4.bootcss.com/docs/components/forms/#inline-forms)渲染 TodoInput 组件的基本结构。
2.在 TodoInput 组件中渲染如下的 DOM 结构:
<template> <!-- form 表单 --> <form class="form-inline"> <div class="input-group mb-2 mr-sm-2"> <!-- 输入框前缀 --> <div class="input-group-prepend"> <div class="input-group-text">任务</div> </div> <!-- 文本输入框 --> <input type="text" class="form-control" placeholder="请输入任务信息" style="width: 356px;"> </div> <!-- 添加按钮 --> <button type="submit" class="btn btn-primary mb-2">添加新任务</button>‘ </form> </template>
4.4.3 通过自定义事件向外传递数据
需求描述:
在 App 组件中,监听 TodoInput 组件的自定义事件,获取到要添加的任务名称。示例代码如下:
<todo-input @add="onAddNewTask"></todo-input>
1.在 TodoInput 组件的 data 中声明如下的数据:
<script> export default { name: 'TodoInput', data() { return { // 新任务的名称 taskname: '', } }, } </script>
2.为 input 输入框进行 v-model 的双向数据绑定
<input type="text" class="form-control" placeholder="请输入任务信息" style="width: 356px;" v-model.trim="taskname" />
3.监听 form 表单的 submit 事件,阻止默认提交行为并指定事件处理函数:
<form class="form-inline" @submit.prevent="onFormSubmit"> </form>
4.在 methods 中声明 onFormSubmit 事件处理函数:
methods: { // 表单提交的事件处理函数 onFormSubmit() { // 1.判断任务名称是否为空 if (!this.taskname) return alert('任务名称不能为空'); // 2.触发自定义的 add 事件,并向外界传递数据 // 3.清空文本框 }, },
5.声明自定义事件如下:
export default { name: 'TodoInput', emits: ['add'], }
6.进一步完善 onFormSubmit 事件处理函数:
methods: { // 表单提交的事件处理函数 onFormSubmit() { // 1.判断任务名称是否为空 if (!this.taskname) return alert('任务名称不能为空'); // 2.触发自定义的 add 事件,并向外界传递数据 this.$emit('add', this.taskname) // 3.清空文本框 this.taskname = '' }, },
4.4.4 实现添加任务的功能
1.在 App.vue 组件中监听 TodoInput 组件自定义的 add 事件:
<!-- 使用 TodoInput 组件 --> <!-- 监听 TodoInput 的 add 自定义事件 --> <todo-input @add="onAddNewTask"></todo-input>
2.在 App.vue 组件的 data 中声明 nextId 来模拟 id 自增 +1 的操作:
data() { return { // 任务列表数据 todolist: [ { id: 1, task: "周一早晨9点开会", done: false }, { id: 2, task: "周一晚上8点聚餐", done: false }, { id: 3, task: "准备周三上午的演讲稿", done: true }, ], // 下一个可用 Id 值 nextId: 4, }; },
3.在 App.vue 组件的 methods 中声明 onAddNewTask 事件处理函数:
methods: { // TodoInput 组件 add 事件的处理函数 onAddNewTask(taskname) { // 1.向任务列表中新增任务信息 this.todolist.push({ id: this.nextId, task: taskname, done: false, // 完成状态默认为 false }) //2.让 nextId 自增+1 this.nextId++ } },
4.5 封装 todo-button 组件
4.5.1 创建并注册 TodoButton 组件
1.在 src/components/todo-button/ 目录下新建 TodoButton.vue 组件:
<template> <div>TodoButton 组件</div> </template> <script> export default { name: 'TodoButton', } </script> <style lang="less" scoped></style>
2.在 App.vue 组件中导入并注册 TodoButton.vue 组件:
<script> // 导入 TodoList 组件 import TodoList from "./components/todo-list/TodoList.vue"; // 导入 TodoInput 组件 import TodoInput from "./components/todo-input/TodoInput.vue"; // 导入 TodoButton 组件 import TodoButton from "./components/todo-button/TodoButton.vue"; export default { name: "MyApp", // 注册私有组件 components: { TodoList, TodoInput, TodoButton, }, }; </script>
3.在 App.vue 的 template 模板结构中使用注册的 TodoButton 组件:
<template> <div> <h1>App 根组件</h1> <hr> <!-- 使用 TodoInput 组件 --> <todo-input></todo-input> <!-- 使用 TodoList 组件 --> <todo-list :list="todolist" class="mt-2"></todo-list> <!-- 使用 TodoButton 组件 --> <todo-button></todo-button> </div> </template>
4.5.2 基于 bootstrap 渲染组件结构
1.根据 bootstrap 提供的 Button group(https://v4.bootcss.com/docs/components/forms/button-group)渲染 TodoButton 组件的基本结构。
2.在 TodoButton 组件中渲染如下的 DOM 结构:
<template> <div class="button-container mt-3"> <div class="btn-group"> <button type="button" class="btn btn-primary">全部</button> <button type="button" class="btn btn-secondary">已完成</button> <button type="button" class="btn btn-secondary">未完成</button> </div> </div> </template>
3.通过 button-container 类名美化组件的样式:
<style lang="less" scoped> .button-container { // 添加固定宽度 width: 400px; // 文本居中效果 text-align: center; } </style>
4.5.3 通过 props 指定默认激活的按钮
1.在 TodoButton 组件中声明如下的 props ,用来指定默认激活的按钮的索引:
<script> export default { name: 'TodoButton', props: { // 激活项的索引值 active: { type: Number, required: true, // 默认激活索引值为 0 的按钮(全部:0,已完成:1,未完成:2) default: 0, }, }, } </script>
2.通过 动态绑定 class 类名 的方式控制按钮的激活状态:
<template> <div class="button-container mt-3"> <div class="btn-group"> <button type="button" class="btn" :class="active === 0 ? 'btn-primary' : 'btn-secondary'">全部</button> <button type="button" class="btn" :class="active === 1 ? 'btn-primary' : 'btn-secondary'">已完成</button> <button type="button" class="btn" :class="active === 2 ? 'btn-primary' : 'btn-secondary'">未完成</button> </div> </div> </template>
3.在 App 组件中声明默认激活项的索引,并通过属性绑定的方式传递给 TodoButton 组件:
<!-- 使用 TodoButton 组件 --> <todo-button :active="activeBtnIndex"></todo-button> <script> export default { name: "MyApp", data() { return { // 任务列表数据 todolist: [ { id: 1, task: "周一早晨9点开会", done: false }, { id: 2, task: "周一晚上8点聚餐", done: false }, { id: 3, task: "准备周三上午的演讲稿", done: true }, ], // 下一个可用 Id 值 nextId: 4, // 激活的按钮的索引 activeBtnIndex: 0, }; }, } </script>
4.5.4 通过 v-model 更新激活项的索引
需求分析:
父 -》 子 通过 props 传递了激活项的索引(active)
子 -》 父 需要更新父组件中激活项的索引
这种场景下适合在组件上使用 v-model 指令,维护组件内外数据的同步。
1.为 TodoButton 组件中的三个按钮分别绑定 click 事件处理函数:
<template> <div class="button-container mt-3"> <div class="btn-group"> <button type="button" class="btn" :class="active === 0 ? 'btn-primary' : 'btn-secondary'" @click="onBtnClick(0)">全部</button> <button type="button" class="btn" :class="active === 1 ? 'btn-primary' : 'btn-secondary'" @click="onBtnClick(1)">已完成</button> <button type="button" class="btn" :class="active === 2 ? 'btn-primary' : 'btn-secondary'" @click="onBtnClick(2)">未完成</button> </div> </div> </template>
2.在 TodoButton 组件中声明如下的自定义事件,用来更新父组件通过 v-model 指令传递过来的 props 数据:
export default { name: 'TodoButton', // 声明和 v-model 相关的自定义事件 emits: ['update:active'], props: { // 激活项的索引值 active: { type: Number, required: true, // 默认激活索引值为 0 的按钮(全部:0,已完成:1,未完成:2) default: 0, }, }, }
3.在 TodoButton 组件的 methods 节点中声明 onBtnClick 事件处理函数:
<script> methods: { // 按钮的点击事件处理函数 onBtnClick(index) { // 如果当前点击的按钮的索引值,等于 props 传递过来的索引值,则没必要触发 update:active 自定义事件 if (index === this.active) return // 通过 this.$emit() 方法触发自定义事件 this.$emit('update:active', index) }, }, </script> <!-- 使用 TodoButton 组件,使用 v-model 指令双向绑定 --> <todo-button v-model:active="activeBtnIndex"></todo-button>
4.5.5 通过计算属性动态切换列表的数据
需求分析:
点击不同的按钮,切换显示不同的列表数据。此时可以根据当前激活按钮得到索引,动态计算出要显示的列表数据并返回即可!
1.在 App 根组件中声明如下的计算属性:
computed: { // 根据激活按钮的索引值,动态计算要展示的列表数据 tasklist() { // 对“源数据”进行 switch...case 的匹配,并返回“计算之后的结果” switch (this.activeBtnIndex) { case 0: // 全部 return this.todolist case 1: // 已完成 return this.todolist.filter(x => x.done) case 2: // 未完成 return this.todolist.filter(x => !x.done) } }, },
2.在 App 根组件的 DOM 结构中,将 TodoList 组件的 :list=“todolist” 修改为:
<!-- 使用 TodoList 组件 --> <todo-list :list="tasklist" class="mt-2"></todo-list>
十八、vue组件基础(下)总结
① 能够知道如何对 props 进行验证
- 数组格式、对象格式
- type、default、required、validator
② 能够知道如何使用计算属性
- computed 节点、必须 return 一个结果、缓存计算结果
③ 能够知道如何为组件绑定自定义事件
- v-on 绑定自定义事件、emits、$emit()
④ 能够知道如何在组件上使用 v-model
- 应用场景:实现组件内外的数据同步
- v-model:props名称、emits、$emit(‘update:props名称’)
Ⅳ vue组件高级(上)
目标 | 目录 |
---|---|
监视数据的变化,从而做出对应的操作 | 十九、watch 侦听器 |
在最合适的周期,做最对的事 | 二十、组件的生命周期 |
组件之间如何进行数据共享,没有详细的 vuex | 二十一、组件之间的数据共享 |
vue 3.x 中全局配置 axios | 二十二、vue 3.x 中全局配置 axios |
对以上内容的复习巩固 | 二十三、第四个案例——购物车案例 |
总结与概括 | 二十四、vue组件高级(上)总结 |
十九、watch 侦听器
1.什么是 watch 侦听器
watch 侦听器允许开发者监视数据的变化,从而针对数据的变化做特定的操作。例如,监视用户名的变化并发起请求,判断用户名是否可用。
2.watch 侦听器的基本语法
开发者需要在 watch 节点下,定义自己的侦听器:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tCAIwBWM-1685004795667)(./assets/Snipaste_2023-05-09_15-26-59.png)]
3.使用 watch 检测用户名是否可用
监听 username 值的变化,并使用 axios 发起 Ajax 请求,检测当前输入的用户名是否可用:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TGXudntd-1685004795667)(./assets/Snipaste_2023-05-09_15-27-09.png)]
4.immediate 选项
默认情况下,组件在初次加载完毕后不会调用 watch 侦听器。如果想让 watch 侦听器立即被调用,则需要使用 immediate 选项:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YiiZ50SB-1685004795668)(./assets/Snipaste_2023-05-09_15-27-37.png)]
5.deep 选项
当 watch 侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到。此时需要使用 deep 选项:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zkSVlm9q-1685004795668)(./assets/Snipaste_2023-05-09_15-27-47.png)]
6.监听对象单个属性的变化
如果只想监听对象中单个属性的变化,则可以按照如下的方式定义 watch 侦听器:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I4Fv37ar-1685004795668)(./assets/Snipaste_2023-05-09_15-27-56.png)]
7.计算属性 vs 侦听器
计算属性和侦听器侧重的应用场景不同:
计算属性侧重于监听多个值的变化,最终计算并返回一个新值
侦听器侧重于监听单个数据的变化,最终执行特定的业务处理,不需要有任何返回值
二十、组件的生命周期
1.组件的运行过程
组件的生命周期指的是:组件从创建 -> 运行(渲染) -> 销毁的整个过程,强调的是一个时间段。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QdLuviry-1685004795669)(./assets/Snipaste_2023-05-09_21-30-12.png)]
2.如何监听组件的不同时刻
vue 框架为组件内置了不同时刻的生命周期函数,生命周期函数会伴随着组件的运行而自动调用。例如:
① 当组件在内存中被创建完毕之后,会自动调用 created 函数
② 当组件被成功的渲染到页面上之后,会自动调用 mounted 函数
③ 当组件被销毁完毕之后,会自动调用 unmounted 函数
3.如何监听组件的更新
当组件的 data 数据更新之后,vue 会自动重新渲染组件的 DOM 结构,从而保证 View 视图展示的数据和 Model 数据源 保持一致。
当组件被重新渲染完毕之后,会自动调用 updated 生命周期函数。
4.组件中主要的生命周期函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HTv6jbPt-1685004795669)(./assets/Snipaste_2023-05-09_21-30-32.png)]
注意:在实际开发中,created 是最常用的生命周期函数!
5.组件中全部的生命周期函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mUKSw271-1685004795669)(./assets/Snipaste_2023-05-09_21-30-44.png)]
6.完整的生命周期图示
可以参考 vue 官方文档给出的“生命周期图示”,进一步理解组件生命周期执行的过程:
https://cn.vuejs.org/guide/essentials/lifecycle.html#lifecycle-diagram
二十一、组件之间的数据共享
1.组件之间的关系
在项目开发中,组件之间的关系分为如下 3 种:
① 父子关系
② 兄弟关系
③ 后代关系
2.父子组件之间的数据共享
父子组件之间的数据共享又分为:
① 父 -> 子共享数据
② 子 -> 父共享数据
③ 父 <-> 子双向数据同步
2.1 父组件向子组件共享数据
父组件通过 v-bind 属性绑定向子组件共享数据。同时,子组件需要使用 props 接收数据:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Amd0khqu-1685004795669)(./assets/Snipaste_2023-05-10_16-17-29.png)]
2.2 子组件向父组件共享数据
子组件通过自定义事件的方式向父组件共享数据:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eUKE0LpY-1685004795669)(./assets/Snipaste_2023-05-10_16-17-40.png)]
2.3 父子组件之间数据的双向同步
父组件在使用子组件期间,可以使用 v-model 指令维护组件内外数据的双向同步:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PxlQndOW-1685004795670)(./assets/Snipaste_2023-05-10_16-29-44.png)]
3.兄弟组件之间的数据共享
兄弟组件之间实现数据共享的方案是 EventBus。可以借助于第三方的包 mitt 来创建 eventBus 对象,从而实现兄弟组件之间的数据共享:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kUiFkjPh-1685004795670)(./assets/Snipaste_2023-05-10_16-38-43.png)]
3.1 安装 mitt 依赖包
npm i mitt
3.2 创建公共的 EventBus 模块
在项目中创建公共的 eventBus 模块:
// 创建 eventBus.js 文件
// 导入 mitt 包
import mitt from 'mitt'
// 创建 EventBus 的实例对象
const bus = mitt()
// 将 EventBus 的实例对象共享出去
export default bus
3.3 在数据接收方自定义事件
在数据接收方,调用 bus.on(‘事件名称’, 事件处理函数) 方法注册一个自定义事件:
<script> // 导入 eventBus.js 模块,得到共享的 bus 对象 import bus from './eventBus.js' export default { name: 'MyRight', data() { return { num: 0, } }, created() { // 调用 bus.on() 方法注册一个自定义事件,通过事件处理函数的形参接收数据 bus.on('countChange', count => { this.num = count }) }, } </script>
3.4 在数据接发送方触发事件
在数据发送方,调用 bus.emit(‘事件名称’, 要发送的数据) 方法触发自定义事件:
<script> // 导入 eventBus.js 模块,得到共享的 bus 对象 import bus from './eventBus.js' export default { name: 'MyLeft', data() { return { count: 0, } }, methods: { add() { this.count++ // 调用 bus.emit() 方法触发自定义事件,并发送数据 bus.emit('countChange', this.count) }, }, } </script>
4.后代关系组件之间的数据共享
后代关系组件之间共享数据,指的是父节点的组件向其子孙组件共享数据。此时组件之间的嵌套关系比较复杂,可以使用 provide 和 inject 实现后代关系组件之间的数据共享。
4.1 父节点通过 provide 共享数据
父节点的组件可以通过 provide 方法,对其子孙组件共享数据:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gd4gVaKR-1685004795670)(./assets/Snipaste_2023-05-10_17-45-00.png)]
4.2 子孙节点通过 inject 接收数据
子孙节点可以使用 inject 数组,接收父级节点向下共享的数据。示例代码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PhycTIWx-1685004795670)(./assets/Snipaste_2023-05-10_17-45-08.png)]
4.3 父节点对外共享响应式数据
父节点使用 provide 向下共享数据时,可以结合 computed 函数向下共享响应式的数据:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dYErWZHa-1685004795671)(./assets/Snipaste_2023-05-10_17-45-16.png)]
4.4 子孙节点使用响应式数据
如果父级节点共享的是响应式的数据,则子孙节点必须以 .value 的形式进行使用:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E3qlE2Rv-1685004795671)(./assets/Snipaste_2023-05-10_17-45-24.png)]
5.vuex
vuex 是终极的组件之间的数据共享方案。在企业级的 vue 项目开发中,vuex 可以让组件之间的数据共享变得高效、清晰、且易于维护。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sV50wh7B-1685004795671)(./assets/Snipaste_2023-05-10_17-45-33.png)]
6.总结
父子关系
① 父 -> 子 属性绑定
② 子 -> 父 事件绑定
③ 父 <-> 子 组件上的 v-model
兄弟关系
④ EventBus
后代关系
⑤ provide & inject
全局数据共享
⑥ vuex
二十二、vue 3.x 中全局配置 axios
1.为什么要全局配置 axios
在实际项目开发中,几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题:
① 每个组件中都需要导入 axios(代码臃肿)
② 每次发请求都需要填写完整的请求路径(不利于后期的维护)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9w0Suzrg-1685004795671)(./assets/Snipaste_2023-05-10_19-02-45.png)]
2.如何全局配置 axios
在 main.js 入口文件中,通过 app.config.globalProperties 全局挂载 axios:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eaq7eMT7-1685004795671)(./assets/Snipaste_2023-05-10_19-02-59.png)]
3.使用 axios
// npm 安装 axios 包
npm i axios -S
// -S 表示项目运行期间仍需使用的包
// -D 表示项目开发期间需要使用的包
// main.js 配置文件中
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import './assets/css/bootstrap.css'
// 导入 axios
import axios from 'axios'
const app = createApp(App)
// 配置请求的根路径
axios.defaults.baseURL = 'http://www.esbook.cn'
// 将 axios 挂载为全局的 $http 自定义属性
app.config.globalProperties.$http = axios
app.mount('#app')
// 使用 axios 发起 Ajax 数据请求
methods: {
// 请求商品列表的数据
async getGoodsList() {
// 1.通过组件实例 this 访问到全局挂载的 $http 属性,并发起 Ajax 数据请求
const { data: res } = await this.$http.get('/api/cart')
// 2.判断请求是否成功
if (res.status !== 200) return alert('请求商品列表数据失败!')
// 3.将请求到的数据存储到 data 中,供页面渲染期间使用
this.goodslist = res.list
}
},
// 使用 axios 发起 get 请求传参
async getBrandList() {
// 在 vue 的 选项式api 中,通过组件实例 this 访问到全局挂载的 $http 属性,并发起 Ajax 数据请求
// 在 vue 的 组合式api 中,vue3.0中是没有this的。使用getCurrentInstance来获取上下文
// const { proxy } = getCurrentInstance() 这里的proxy相当于this
// 1.通过组件实例 this 访问到全局挂载的 $http 属性,并发起 Ajax 数据请求
const { data: res } = await this.$http.get('/selectById',{
params:{
id: 4,
}
})
// 2.判断请求是否成功
if (res.status !== 200) return alert('请求商品列表数据失败!')
// 3.将请求到的数据存储到 data 中,供页面渲染期间使用
this.goodslist = res.list
}
二十三、第四个案例——购物车案例
1.案例效果

2.实现步骤
① 初始化项目基本结构
② 封装 EsHeader 组件
③ 基于 axios 请求商品列表数据( GET 请求,地址为 https://www.escook.cn/api/cart )
④ 封装 EsFooter 组件
⑤ 封装 EsGoods 组件
⑥ 封装 EsCounter 组件
3.具体实现
3.1 初始化项目结构
1.初始化 vite 项目:
npm init vite-app code-cart cd code-cart npm i
2.清理项目结构:
- 把 bootstrap 相关文件放入 src/assets 目录下
- 在 main.js 中导入 bootstrap.css
- 清空 App.vue 组件
- 删除 components 目录下的 HelloWorld.vue 组件
3.为组件的样式启用 less 语法
npm i less -D
4.初始化 index.css 全局样式如下:
:root{ font-size: 12px; }
3.2 封装 es-header 组件
3.2.1 创建并注册 EsHeader 组件
1.在 src/components/es-header/ 目录下新建 EsHeader.vue 组件:
<template> <div>EsHeader 组件</div> </template> <script> export default { name: 'EsHeader', } </script> <style lang="less" scoped></style>
2.在 App.vue 组件中导入、注册并在模板结构中使用 EsHeader.vue 组件:
<template> <h1>App 根组件</h1> <hr> <!-- 使用 EsHeader 组件 --> <es-header></es-header> </template> <script> import EsHeader from './components/es-header/EsHeader.vue'; export default { name: 'MyApp', components: { // 注册 header 组件 EsHeader, } } </script>
3.2.2 封装 es-header 组件
封装需求:
- 允许用户自定义 title 标题内容
- 允许用户自定义 color 文字颜色
- 允许用户自定义 bgcolor 背景颜色
- 允许用户自定义 fsize 字体大小
- es-header 组件必须固定定位到页面的顶部位置,高度为 45px,文本居中,z-index 为 999
1.在 es-header 组件中封装以下的 props 属性:
<script> export default { name: 'EsHeader', props: { title: { // 标题内容 type: String, default: 'es-header', }, bgcolor: { // 背景颜色 type: String, default: '#007bff', }, color: { // 文字颜色 type: String, default: '#ffffff', }, fsize: { // 文字大小 type: Number, default: 12, }, }, } </script>
2.渲染标题,并动态为 DOM 元素绑定行内的 style 样式对象:
<template> <div class="header-container" :style="{ backgroundColor: bgcolor, color: color, fontSize: fsize + 'px' }"> {{ title }} </div> </template> <style lang="less" scoped> .header-container { position: fixed; top: 0; left: 0; width: 100%; height: 45px; text-align: center; line-height: 45px; z-index: 999; } </style>
3.调整 App.vue 根组件的样式,并传入 title 属性:
<template> <div class="app-container"> <h1>App 根组件</h1> <hr> <!-- 使用 EsHeader 组件 --> <es-header title="购物车案例"></es-header> </div> </template> <style lang="less"> .app-container { padding-top: 45px; } </style>
3.3 基于 axios 请求商品列表数据
3.3.1 全局配置 axios
1.运行如下的命令安装 axios:
npm i axios -S
2.在 main.js 入口文件中导入并全局配置 axios:
import { createApp } from 'vue' import App from './App.vue' import './index.css' import './assets/css/bootstrap.css' // 导入 axios import axios from 'axios' const app = createApp(App) // 配置请求的根路径 axios.defaults.baseURL = 'https://applet-base-api-t.itheima.net' // 将 axios 挂载为全局的 $http 自定义属性 app.config.globalProperties.$http = axios app.mount('#app')
3.3.2 请求商品列表数据
1.在 App.vue 根组件中声明如下的 data 数据:
data() { return { // 商品列表的数据 goodslist: [], } },
2.在 App.vue 根组件的 created 生命周期函数中,预调用获取商品列表数据的 methods 方法:
// 组件实例创建完毕之后的生命周期函数 created() { // 调用 methods 中的 getGoodsList 方法,请求商品列表的数据 this.getGoodsList() },
3.在 App.vue 根组件的 methods 节点中,声明刚才预调用的 getGoodsList 方法:
methods: { // 请求商品列表的数据 async getGoodsList() { // 1.通过组件实例 this 访问到全局挂载的 $http 属性,并发起 Ajax 数据请求 const { data: res } = await this.$http.get('/api/cart') // 2.判断请求是否成功 if (res.status !== 200) return alert('请求商品列表数据失败!') // 3.将请求到的数据存储到 data 中,供页面渲染期间使用 this.goodslist = res.list } },
3.4 封装 es-footer 组件
3.4.1 创建并注册 EsFooter 组件
1.在 src/component/es-footer/ 目录下新建 EsFooter.vue 组件:
<template> <div> EsFooter 组件 </div> </template> <script> export default { name: 'EsFooter', } </script> <style lang="less" scoped></style>
2.在 App.vue 组件中导入并注册 EsFooter.vue 组件:
<template> <h1>App 根组件</h1> <hr> <!-- 使用 EsHeader 组件 --> <es-header></es-header> <!-- 使用 EsFooter 组件 --> <es-footer></es-footer> </template> <script> import EsHeader from './components/es-header/EsHeader.vue'; import EsFooter from './components/es-footer/EsFooter.vue'; export default { name: 'MyApp', components: { // 注册 header 组件 EsHeader, // 注册 footer 组件 EsFooter, } } </script>
3.4.2 封装 es-footer 组件
封装需求:
1.es-footer 组件必须固定定位到页面底部的位置,高度为 50px,内容两端贴边对齐,z-index 为 999
2.允许用户自定义 amount 总价格(单位是元),并在渲染时保留两位小数
3.允许用户自定义 total 总数量,并渲染到结算按钮中;如果要结算的商品数量为0,则禁用结算按钮
4.允许用户自定义 isfull 全选按钮的选中状态
5.允许用户通过自定义事件的形式,监听全选按钮选中状态的变化,并获取到最新的选中状态
1.将 EsFooter.vue 组件在页面底部进行固定定位,基于 bootstrap 渲染左侧全选按钮:
<template> <div class="footer-container"> <div class="custom-control custom-checkbox"> <input type="checkbox" class="custom-control-input" id="fullCheck"> <label class="custom-control-label" for="fullCheck">全选</label> </div> </div> </template> <script> export default { name: 'EsFooter', } </script> <style lang="less" scoped> .footer-container { height: 50px; width: 100%; background-color: white; border-top: 1px solid #efefef; position: fixed; bottom: 0; left: 0; display: flex; justify-content: space-between; align-items: center; padding: 0 10px; } :root { font-size: 12px; } </style>
2.全局样式表更改属性:
.custom-control .custom-control-label::before { border-radius: 1.25em; }
3.渲染合计区域和结算按钮:
<template> <div class="footer-container"> <!-- 全选框 --> <div class="custom-control custom-checkbox"> <input type="checkbox" class="custom-control-input" id="fullCheck"> <label class="custom-control-label" for="fullCheck">全选</label> </div> <!-- 合计部分 --> <div> <span>合计:</span> <span class="amount">¥0.00</span> </div> <!-- 结算按钮 --> <button type="button" class="btn btn-primary btn-settle">结算(0)</button> </div> </template> <script> export default { name: 'EsFooter', } </script> <style lang="less" scoped> .footer-container { height: 50px; width: 100%; background-color: white; border-top: 1px solid #efefef; position: fixed; bottom: 0; left: 0; display: flex; justify-content: space-between; align-items: center; padding: 0 10px; } :root { font-size: 12px; } .custom-control .custom-control-label::before { border-radius: 1.25em; } .amount { font-weight: bold; color: red; } .btn-settle { min-width: 90px; height: 38px; border-radius: 19px; } </style>
3.4.3 封装自定义属性 amount
amount 是已勾选商品的总价格
1.在 EsFooter.vue 组件的 props 节点中,声明如下自定义属性:
<script> export default { name: 'EsFooter', props: { // 已勾选商品的总价格 amount: { type: Number, default: 0, }, }, } </script>
2.在 EsFooter.vue 组件的 DOM 结构中渲染 amount 的值:
<!-- 合计部分 --> <div> <span>合计:</span> <!-- 将 amount 的值保留为两位小数 --> <span class="amount">¥{{ amount.toFixed(2) }}</span> </div>
3.4.4 封装自定义属性 total
total是已勾选商品的总数量
1.在 EsFooter.vue 组件的 props 节点中,声明如下自定义属性:
<script> export default { name: 'EsFooter', props: { // 已勾选商品的总价格 amount: { type: Number, default: 0, }, // 已勾选商品的总数量 total: { type: Number, default: 0, } }, } </script>
2.结算按钮动态控制
<!-- disabled 的值为 true,禁用按钮 --> <button type="button" class="btn btn-primary btn-settle" :disabled="total === 0">结算({{ total }})</button>
3.4.5 封装自定义属性 isfull
isfull 是全选按钮的选中状态,true 表示选中,false 表示未选中
1.在 EsFooter.vue 组件的 props 节点中,声明如下自定义属性:
<script> export default { name: 'EsFooter', props: { // 已勾选商品的总价格 amount: { type: Number, default: 0, }, // 已勾选商品的总数量 total: { type: Number, default: 0, }, // 全选按钮的选中状态 isFull: { type: Boolean, default: false, } }, } </script>
2.动态绑定选中状态
<!-- 全选框 --> <div class="custom-control custom-checkbox"> <input type="checkbox" class="custom-control-input" id="fullCheck" :checked="isfull"> <label class="custom-control-label" for="fullCheck">全选</label> </div>
3.4.6 封装自定义事件 fullChange
通过自定义事件 fullChange,把最新的选中状态传递给组件的使用者
1.监听复选框选中状态变化的 change 事件:
<!-- 全选框 --> <div class="custom-control custom-checkbox"> <input type="checkbox" class="custom-control-input" id="fullCheck" :checked="isfull" @change="onCheckBoxChange"> <label class="custom-control-label" for="fullCheck">全选</label> </div>
2.在 methods 中声明 onCheckBoxChange,并通过事件对象 e 获取到最新的选中状态:
methods: { // 监听复选框选中状态的变化 onCheckBoxChange(e) { // e.target.checked 是复选框最新的选中状态 this.$emit('fullChange', e.target.checked) } },
3.在 emits 中声明自定义事件:
// 声明自定义事件 emits: ['fullChange'],
4.在 App.vue 根组件中测试 EsFooter.vue 组件:
<!-- 使用 EsFooter 组件 --> <es-footer :total="0" :amount="0" @fullChange="onFullStateChange"></es-footer>
5.在methods 中声明 onFullStateChange 处理函数,通过形参获取到全选按钮最新的选中状态:
methods: { // 监听选中状态变化的事件 onFullStateChange(isfull) { console.log(isfull) } },
3.5 封装 es-goods 组件
3.5.1 创建并注册 EsGoods 组件
<template>
<div>
EsGoods 组件
</div>
</template>
<script>
export default {
name: 'EsGoods',
}
</script>
<style lang="less" scoped></style>
3.5.2 封装 es-goods 组件
封装需求:
1.实现 EsGoods 组件的基础布局
2.封装组件的 6 个自定义属性 (id, thumb, title, price, count, checked)
3.封装组件的自定义事件 stateChange,允许外界监听组件选中状态的变化
<!-- 使用 goods 组件 --> <es-goods v-for="item in goodslist" :key="item.id" :id="item.id" :thumb="item.goods_img" :title="item.goods_name" :price="item.goods_price" :count="item.goods_count" :checked="item.goods_state" @stateChange="onGoodsStateChange" ></es-goods>
1.渲染组件的基础布局
1.1 渲染 EsGoods 组件的基础 DOM 结构:
<template> <div class="goods-container"> <!-- 左侧图片区域 --> <div class="left"> <!-- 商品的缩略图 --> <img src="" alt="商品图片" class="thumb" /> </div> <!-- 右侧信息区域 --> <div class="right"> <!-- 商品名称 --> <div class="top">xxxx</div> <div class="bottom"> <!-- 商品价格 --> <div class="price">¥0.00</div> <!-- 商品数量 --> <div class="count">数量</div> </div> </div> </div> </template>
1.2 美化布局样式:
.goods-container { display: flex; padding: 10px; // 左侧图片的样式 .left { margin-right: 10px; // 商品图片 .thumb { display: block; width: 100px; height: 100px; background-color: #efefef; } } // 右侧商品名称、单价、数量的样式 .right { display: flex; flex-direction: column; justify-content: space-between; flex: 1; .top { font-weight: bold; } .bottom { display: flex; justify-content: space-between; align-items: center; .price { color: red; font-weight: bold; } } } }
1.3 在商品缩略图之外包裹复选框( https://v4.bootcss.com/docs/components/forms/#checkboxes )效果:
<!-- 左侧图片和复选框区域 --> <div class="left"> <!-- 复选框 --> <div class="custom-control custom-checkbox"> <input type="checkbox" class="custom-control-input" id="customCheck1" /> <!-- 将商品图片包裹于 label 之中,点击图片可以切换“复选框”的选中状态 --> <label class="custom-control-label" for="customCheck1"> <img src="" alt="商品图片" class="thumb" /> </label> </div> </div>
1.4 覆盖复选框的默认样式:
.custom-control-label::before, .custom-control-label::after { top: 3.4rem; } .custom-control .custom-control-label::before { border-radius: 1.25em; }
1.5 在 App.vue 组件中循环渲染 EsGoods.vue 组件:
<!-- 使用 EsGoods 组件 --> <es-goods v-for="item in goodslist"></es-goods>
1.6 为 EsGoods.vue 添加顶边框:
.goods-container { display: flex; padding: 10px; // 最终生成的选择器为 .goods-container + .goods-container // 在 css 中,(+)是“相邻兄弟选择器”,表示:选择紧连着另一元素后的元素,二者具有相同的父元素。 + .goods-container { border-top: 1px solid #efefef; } // ...省略其他样式 }
2.封装自定义属性 id
2.1 在 EsGoods.vue 组件的 props 节点中,声明如下的自定义属性:
export default { name: 'EsGoods', props: { // 唯一的 key 值 id: { // id 的值可以是“字符串”也可以是“数值” type: [String, Number], required: true, }, }, }
2.2 在渲染复选框时动态绑定 input 的 id 属性和 label 的 for 属性值:
<!-- 复选框 --> <div class="custom-control custom-checkbox"> <input type="checkbox" class="custom-control-input" :id="id" /> <label class="custom-control-label" :for="id"> <img src="" alt="商品图片" class="thumb" /> </label> </div>
2.3 在 App.vue 中使用 EsGoods.vue 组件时,动态绑定 id 属性的值:
<!-- 使用 goods 组件 --> <es-goods v-for="item in goodslist" :id="item.goods_id"></es-goods>
3.封装其他属性
除了 id 属性之外,EsGoods 组件还需要封装:缩略图(thumb)、商品名称(title)、单价(price)、数量(count)、勾选状态(checked)这 5 个属性
3.1 在 EsGoods.vue 组件的 props 节点中,声明如下的自定义属性:
export default { name: 'EsGoods', props: { // 唯一的 key 值 id: { type: [String, Number], required: true, }, // 1. 商品的缩略图 thumb: { type: String, required: true, }, // 2. 商品的名称 title: { type: String, required: true, }, // 3. 单价 price: { type: Number, required: true, }, // 4. 数量 count: { type: Number, required: true, }, // 5. 商品的勾选状态 checked: { type: Boolean, required: true, }, }, }
3.2 在 EsGoods.vue 组件的 DOM 结构中渲染商品的信息数据:
<template> <div class="goods-container"> <!-- 左侧图片和复选框区域 --> <div class="left"> <!-- 复选框 --> <div class="custom-control custom-checkbox"> <input type="checkbox" class="custom-control-input" id="id" :checked="checked" /> <!-- 将商品图片包裹于 label 之中,点击图片可以切换“复选框”的选中状态 --> <label class="custom-control-label" for="id"> <img :src="thumb" alt="商品图片" class="thumb" /> </label> </div> </div> <!-- 右侧信息区域 --> <div class="right"> <!-- 商品名称 --> <div class="top">{{ title }}</div> <div class="bottom"> <!-- 商品价格 --> <div class="price">¥{{ price.toFixed(2) }}</div> <!-- 商品数量 --> <div class="count">{{ count }}</div> </div> </div> </div> </template>
3.3 在 App.vue 组件中使用 EsGoods.vue 组件时,动态绑定对应属性的值:
<!-- 使用 EsGoods 组件 --> <es-goods v-for="item in goodslist" :key="item.goods_id" :id="item.goods_id" :thumb="item.goods_img" :title="item.goods_name" :price="item.goods_price" :count="item.goods_count" :checked="item.goods_state"></es-goods>
4.封装自定义事件 stateChange
点击复选框时,可以把最新的勾选状态,通过自定义事件的方式传递给组件的使用者。
4.1 在 EsGoods.vue 组件中,监听 checkbox 选中状态变化的事件:
<!-- 监听复选框的 change 事件 --> <input type="checkbox" class="custom-control-input" :id="id" :checked="checked" @change="onCheckBoxChange" />
4.2 在 EsGoods.vue 组件的 methods 中声明对应的事件处理函数:
methods: { // 监听复选框选中状态变化的事件 onCheckBoxChange(e) { // e.target.checked 是最新的勾选状态 console.log(e.target.checked) }, },
4.3 在 EsGoods.vue 组件中声明自定义事件:
emits: ['stateChange'],
4.4 完善 onCheckBoxChange 函数的处理逻辑,调用 $emit() 函数触发自定义事件:
methods: { // 监听复选框选中状态变化的事件 onCheckBoxChange(e) { // 向外发送的数据是一个对象,包含了 { id, value } 两个属性 this.$emit('stateChange', { id: this.id, value: e.target.checked, }) }, },
4.5 在 App.vue 根组件中使用 EsGoods.vue 组件时,监听它的 stateChange 事件:
<!-- 使用 goods 组件 --> <es-goods v-for="item in goodslist" :key="item.id" :id="item.id" :thumb="item.goods_img" :title="item.goods_name" :price="item.goods_price" :count="item.goods_count" :checked="item.goods_state" @stateChange="onGoodsStateChange" ></es-goods>
4.6 在 App.vue 的 methods 中声明如下的事件处理函数:
methods: { // 监听商品选中状态变化的事件 onGoodsStateChange(e) { // 1. 根据 id 进行查找(注意:e 是一个对象,包含了 id 和 value 两个属性) const findResult = this.goodslist.find(x => x.id === e.id) // 2. 找到了对应的商品,则更新其选中状态 if (findResult) { findResult.goods_state = e.value } }, }
3.6 实现合计、结算数量、全选功能
3.6.1 动态统计已勾选商品的总价格
需求分析:
合计的商品总价格,依赖于 goodslist 数组中每一件商品信息的变化,此场景下适合使用计算属性。
1.在 App.vue 中声明如下的计算属性:
computed: { // 已勾选商品的总价 amount() { // 1. 定义商品总价格 let a = 0 // 2. 循环累加商品总价格 this.goodslist .filter(x => x.goods_state) .forEach(x => a += x.goods_price * x.goods_count) // 3. 返回累加的结果 return a }, },
2.在 App.vue 中使用 EsFooter.vue 组件时,动态绑定已勾选商品的总价格:
<!-- 使用 footer 组件 --> <es-footer :total="0" :amount="amount" @fullChange="onFullStateChange"></es-footer>
3.6.2 动态统计已勾选商品的总数量
1.在 App.vue 中声明如下的计算属性:
computed: { // 已勾选商品的总数量 total() { // 1. 定义已勾选的商品总数量 let t = 0 // 2. 循环累加 this.goodslist .filter(x => x.goods_state) .forEach(x => (t += x.goods_count)) // 3. 返回计算的结果 return t }, },
2.在 App.vue 中使用 EsFooter.vue 组件时,动态绑定已勾选商品的总数量:
<!-- 使用 footer 组件 --> <es-footer :total="total" :amount="amount" @fullChange="onFullStateChange"></es-footer>
3.6.3 实现全选功能
1.在 App.vue 组件中监听到 EsFooter.vue 组件的选中状态发生变化时,立即更新 goodslist 中每件商品的选中状态即可:
<!-- 使用 footer 组件 --> <es-footer :total="total" :amount="amount" @fullChange="onFullStateChange"></es-footer>
2.在 onFullStateChange 的事件处理函数中修改每件商品的选中状态:
methods: { // 监听全选按钮状态的变化 onFullStateChange(isFull) { this.goodslist.forEach(x => x.goods_state = isFull) }, }
3.7 封装 es-counter 组件
3.7.1 创建并注册 EsCounter 组件
1.在 src/components/es-counter/ 目录下新建 EsCounter.vue 组件:
<template> <div>EsCounter 组件</div> </template> <script> export default { name: 'EsCounter', } </script> <style lang="less" scoped> </style>
2.在 EsGoods.vue 组件中导入并注册 EsCounter.vue 组件:
// 导入 counter 组件 import EsCounter from '../es-counter/EsCounter.vue' export default { name: 'EsGoods', components: { // 注册 counter 组件 EsCounter, } }
3.在 EsGoods.vue 的 template 模板结构中使用 EsCounter.vue 组件:
<div class="bottom"> <!-- 商品价格 --> <div class="price">¥{{ price.toFixed(2) }}</div> <!-- 商品数量 --> <div class="count"> <!-- 使用 es-counter 组件 --> <es-counter></es-counter> </div> </div>
3.7.2 封装 EsCounter 组件
封装要求:
渲染组件的 基础布局
实现数量值的 加减操作
处理 min 最小值
使用 watch 侦听器处理文本框输入的结果
封装 numChange 自定义事件
<es-counter :num="count" :min="1" @numChange="getNumber"></es-counter>
2.1 渲染组件基础布局
1.基于 bootstrap 提供的 Buttons https://v4.bootcss.com/docs/components/buttons/#examples 和 form-control 渲染组件的基础布局:
<template> <div class="counter-container"> <!-- 数量 -1 按钮 --> <button type="button" class="btn btn-light btn-sm">-</button> <!-- 输入框 --> <input type="number" class="form-control form-control-sm ipt-num" /> <!-- 数量 +1 按钮 --> <button type="button" class="btn btn-light btn-sm">+</button> </div> </template>
2.美化当前组件的样式:
.counter-container { display: flex; // 按钮的样式 .btn { width: 25px; } // 输入框的样式 .ipt-num { width: 34px; text-align: center; margin: 0 4px; } }
2.2 实现数值的渲染及加减操作
思路分析:
加减操作需要依赖于 EsCounter 组件的 data 数据
初始数据依赖于父组件通过 props 传递进来
将父组件传递进来的 props 初始值转存到 data 中,形成 EsCounter 组件的内部状态!
1.在 EsCounter.vue 组件中声明如下的 props:
props: { // 数量值 num: { type: Number, default: 0, }, },
2.在 EsGoods.vue 组件中通过属性绑定的形式,将数据传递到 EsCounter.vue 组件中:
<!-- 商品数量 --> <div class="count"> <es-counter :num="count"></es-counter> </div>
注意:不要直接把 num 通过 v-model 指令双向绑定到 input 输入框,因为 vue 规定:props **的值只读的!**例如下面的做法是错误的:
<!-- Warning 警告:不要模仿下面的操作 --> <input type="number" class="form-control form-control-sm ipt-num" v-model.number="num" />
3.正确的做法:将 props 的初始值转存到 data 中,因为 data **中的数据是可读可写的!**示例代码如下:
export default { name: 'EsCounter', props: { // 初始数量值【只读数据】 num: { type: Number, default: 0, }, }, data() { return { // 内部状态值【可读可写的数据】 // 通过 this 可以访问到 props 中的初始值 number: this.num, } }, }
并且把 data 中的 number 双向绑定到 input 输入框:
<input type="number" class="form-control form-control-sm ipt-num" v-model.number="number" />
4.为 -1 和 +1 按钮绑定响应的点击事件处理函数:
<button type="button" class="btn btn-light btn-sm" @click="onSubClick">-</button> <input type="number" class="form-control form-control-sm ipt-num" v-model.number="number" /> <button type="button" class="btn btn-light btn-sm" @click="onAddClick">+</button>
并在 methods 中声明对应的事件处理函数如下:
methods: { // -1 按钮的事件处理函数 onSubClick() { this.number -= 1 }, // +1 按钮的事件处理函数 onAddClick() { this.number += 1 }, },
2.3 实现 min 最小值的处理
需求分析:
购买商品时,购买的数量最小值为 1
1.在 EsCounter.vue 组件中封装如下的 props:
export default { name: 'EsCounter', props: { // 数量值 num: { type: Number, default: 0, }, // 最小值 min: { type: Number, // min 属性的值默认为 NaN,表示不限制最小值 default: NaN, }, }, }
2.在 -1 按钮的事件处理函数中,对 min 的值进行判断和处理:
methods: { // -1 按钮的事件处理函数 onSubClick() { // 判断条件:min 的值存在,且 number - 1 之后小于 min if (!isNaN(this.min) && this.number - 1 < this.min) return this.number -= 1 }, }
3.在 EsGoods.vue 组件中使用 EsCounter.vue 组件时指定 min 最小值:
<!-- 商品数量 --> <div class="count"> <!-- 指定数量的最小值为 1 --> <es-counter :num="count" :min="1"></es-counter> </div>
2.4 处理输入框的输入结果
思路分析:
将输入的新值转化为整数
如果转换的结果不是数字,或小于1,则强制 number 的值等于1
如果新值为小数,则把转换的结果赋值给 number
1.为输入框的 v-model 指令添加 .lazy 修饰符(当输入框触发 change 事件时更新 v-model 所绑定到的数据源):
<input type="number" class="form-control form-control-sm ipt-num" v-model.number.lazy="number" />
2.通过 watch 侦听器监听 number 数值的变化,并按照分析的步骤实现代码:
export default { name: 'EsCounter', watch: { // 监听 number 数值的变化 number(newVal) { // 1. 将输入的新值转化为整数 const parseResult = parseInt(newVal) // 2. 如果转换的结果不是数字,或小于1,则强制 number 的值等于 1 if (isNaN(parseResult) || parseResult < 1) { this.number = 1 return } // 3. 如果新值为小数,则把转换的结果赋值给 number if (String(newVal).indexOf('.') !== -1) { this.number = parseResult return } console.log(this.number) }, }, }
2.5 把最新的数据传递给使用者
需求分析:
当 EsGoods 组件使用 EsCounter 组件时,期望能够监听到商品数量的变化,此时需要使用自定
义事件的方式,把最新的数据传递给组件的使用者。
1.在 EsCounter.vue 组件中声明自定义事件如下:
emits: ['numChange'],
2.在 EsCounter.vue 组件的 watch 侦听器中触发自定义事件:
watch: { // 监听 number 数值的变化 number(newVal) { // 1.将输入的新值转换为整数 const parseResult = parseInt(newVal) // 2.如果转换的结果不是数字,或小于1,则强制 number 的值等于 1 if (isNaN(parseResult) || parseResult < 1) { this.number = 1 return } // 3.如果新值为小数,则把转换的结果赋值给 number if (String(newVal).indexOf('.') !== -1) { this.number = parseResult return } // 触发自定义事件,把最新的 number 数值传递给组件的使用者 this.$emit('numChange', this.number) } },
3.在 EsGoods.vue 组件中监听 EsCounter.vue 组件的自定义事件:
<!-- 商品数量 --> <div class="count"> <es-counter :num="count" :min="1" @numChange="getNumber"></escounter> </div>
并声明对应的事件处理函数如下:
methods: { // 监听数量变化的事件 getNumber(num) { console.log(num) }, }
2.6 更新购物车中商品的数量
思路分析:
在 EsGoods 组件中获取到最新的商品数量
在 EsGoods 组件中声明自定义事件
在 EsGoods 组件中触发自定义事件,向外传递数据对象 { id, value }
在 App 根组件中监听 EsGoods 组件的自定义事件,并根据 id 更新对应商品的数量
1.在 EsGoods.vue 组件中声明自定义事件 countChange :
emits: ['numChange', 'countChange'],
2.在 EsCounter.vue 组件的 numChange 事件处理函数中,触发步骤1声明的自定义事件:
<es-counter :num="count" :min="1" @numChange="getNumber"></es-counter> <script> methods: { // 监听数量变化的事件 getNumber(num) { // 触发自定义事件,向外传递数据对象 { id, value } this.$emit('countChange', { // 商品的 id id: this.id, // 最新的数量 value: num, }) }, } </script>
3.在 App.vue 根组件中使用 EsGoods.vue 组件时,监听它的自定义事件 countChange :
<!-- 使用 goods 组件 --> <es-goods v-for="item in goodslist" :key="item.id" :id="item.id" :thumb="item.goods_img" :title="item.goods_name" :price="item.goods_price" :count="item.goods_count" :checked="item.goods_state" @stateChange="onGoodsStateChange" @countChange="onGoodsCountChange" ></es-goods>
并在 methods 中声明对应的事件处理函数:
methods: { // 监听商品数量变化的事件 onGoodsCountChange(e) { // 根据 id 进行查找 const findResult = this.goodslist.find(x => x.id === e.id) // 找到了对应的商品,则更新其数量 if (findResult) { findResult.goods_count = e.value } } }
二十四、vue组件高级(上)总结
① 能够掌握 watch 侦听器的基本使用
- 定义最基本的 watch 侦听器
- immediate、 deep、监听对象中单个属性的变化
② 能够知道 vue 中常用的生命周期函数
- 创建阶段、运行阶段、销毁阶段
- created、mounted
③ 能够知道如何实现组件之间的数据共享
- 父子组件、兄弟组件、后代组件
④ 能够知道如何在 vue3 的项目中全局配置 axios
- main.js 入口文件中进行配置
- app.config.globalProperties.$http = axios
Ⅴ vue组件高级(下)
目标 | 目录 |
---|---|
如何使用 ref 引用 DOM 和组件实例、$nextTick 的调用时机 | 二十五、ref 引用 |
keep-alive 元素的作用 | 二十六、动态组件 |
插槽的基本用法 | 二十七、插槽 |
如何自定义指令 | 二十八、自定义指令 |
以上内容的复习巩固 | 二十九、第五个案例——Table 案例 |
总结与概括 | 三十、vue组件高级(下)总结 |
二十五、ref 引用
1.什么是 ref 引用
ref 用来辅助开发者在不依赖于 jQuery 的情况下,获取 DOM 元素或组件的引用。
每个 vue 的组件实例上,都包含一个 $refs 对象,里面存储着对应的 DOM 元素或组件的引用。默认情况下,组件的 $refs 指向一个空对象。
<template>
<div>
<h1>App 根组件</h1>
<hr>
<button type="button" class="btn btn-primary" @click="getRefs">获取 $refs 引用</button>
</div>
</template>
<script>
export default {
name: 'MyApp',
methods: {
getRefs() {
// this 代表当前组件的实例对象,this.$refs 默认指向空对象
console.log(this)
}
},
}
</script>
<style lang="less" scoped></style>
2.使用 ref 引用 DOM 元素
如果想要使用 ref 引用页面上的 DOM 元素,则可以按照如下的方式进行操作:
<template>
<div>
<h1 ref="myh1">App 根组件</h1>
<hr>
<button type="button" class="btn btn-primary" @click="getRefs">获取 $refs 引用</button>
</div>
</template>
<script>
export default {
name: 'MyApp',
methods: {
getRefs() {
this.$refs.myh1.style.color = 'red'
}
},
}
</script>
<style lang="less" scoped></style>
3.使用 ref 引用组件实例
如果想要使用 ref 引用页面上的组件实例,则可以按照如下的方式进行操作:
// 根组件
<template>
<div>
<h1 ref="myh1">App 根组件</h1>
<hr>
<button type="button" class="btn btn-primary" @click="getRefs">获取 $refs 引用</button>
<my-counter ref="counterRef"></my-counter>
</div>
</template>
<script>
import MyCounter from './Counter.vue'
export default {
name: 'MyApp',
methods: {
getRefs() {
this.$refs.counterRef.reset()
}
},
components: {
MyCounter,
}
}
</script>
<style lang="less" scoped></style>
// 子组件
<template>
<div class="counter-container">
<h3>Counter 组件 --- {{ count }}</h3>
<hr />
<button type="button" class="btn btn-info" @click="count += 1">+1</button>
</div>
</template>
<script>
export default {
name: 'MyCounter',
data() {
return {
count: 0,
}
},
methods: {
reset() {
this.count = 0
}
},
}
</script>
<style lang="less" scoped>
.counter-container {
margin: 20px;
padding: 20px;
border: 1px solid #efefef;
border-radius: 4px;
box-shadow: 0px 1px 10px #efefef;
}
</style>
4.控制文本框和按钮的按需切换
通过布尔值 inputVisible 来控制组件中的文本框与按钮的按需切换:
<template>
<div>
<h1>App 根组件</h1>
<hr />
<input type="text" class="form-control" v-if="inputVisible" ref="ipt" />
<button type="button" class="btn btn-primary" v-else @click="showInput">展示 input 输入框</button>
</div>
</template>
<script>
export default {
name: 'MyApp',
data() {
return {
inputVisible: false,
}
},
methods: {
showInput() {
this.inputVisible = true
}
}
}
</script>
<style lang="less" scoped>
input.form-control {
width: 280px;
display: inline;
vertical-align: bottom;
}
</style>
5.让文本框自动获得焦点
错误写法:当文本框展示出来之后,希望它立即获得焦点,为其添加 ref 引用,并调用原生 DOM 对象的.focus() 方法:
<template>
<div>
<h1>App 根组件</h1>
<hr />
<input type="text" class="form-control" v-if="inputVisible" ref="ipt" />
<button type="button" class="btn btn-primary" v-else @click="showInput">展示 input 输入框</button>
</div>
</template>
<script>
export default {
name: 'MyApp',
data() {
return {
inputVisible: false,
}
},
methods: {
showInput() {
// 展示文本框
this.inputVisible = true
// 获取到文本框的引用对象,然后调用 focus() 方法
this.$refs.ipt.focus()
}
}
}
</script>
<style lang="less" scoped>
input.form-control {
width: 280px;
display: inline;
vertical-align: bottom;
}
</style>
正确写法:使用组件的 $nextTick(cb) 方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行。通俗的理解是:等组件的DOM 异步地重新渲染完成后,再执行 cb 回调函数。从而能保证 cb 回调函数可以操作到最新的 DOM 元素。
<template>
<div>
<h1>App 根组件</h1>
<hr />
<input type="text" class="form-control" v-if="inputVisible" ref="ipt" />
<button type="button" class="btn btn-primary" v-else @click="showInput">展示 input 输入框</button>
</div>
</template>
<script>
export default {
name: 'MyApp',
data() {
return {
inputVisible: false,
}
},
methods: {
showInput() {
// 展示文本框
this.inputVisible = true
// 把对 input 文本框的操作,推迟到下次 DOM 更新之后,否则页面上根本不存在文本框元素
this.$nextTick(() => {
// 获取到文本框的引用对象,然后调用 focus() 方法
this.$refs.ipt.focus()
})
},
},
}
</script>
<style lang="less" scoped>
input.form-control {
width: 280px;
display: inline;
vertical-align: bottom;
}
</style>
二十六、动态组件
1.什么是动态组件
动态组件指的是动态切换组件的显示与隐藏。vue 提供了一个内置的 组件,专门用来实现组件的动态渲染。
① 是组件的占位符
② 通过 is 属性动态指定要渲染的组件名称
③
2.如何实现动态组件渲染
<template>
<div>
<h1 class="mb-4">App 根组件</h1>
<button type="button" class="btn btn-primary" @click="comName = 'MyHome'">首页</button>
<button type="button" class="btn btn-info ml-2" @click="comName = 'MyMovie'">电影</button>
<hr />
<component :is="comName"></component>
</div>
</template>
<script>
// 导入组件
import MyHome from './Home.vue'
import MyMovie from './Movie.vue'
export default {
name: 'MyApp',
data() {
return {
comName: 'MyHome'
}
},
// 注册组件
components: {
MyHome,
MyMovie,
},
}
</script>
<style lang="less" scoped></style>
3.使用 keep-alive 保持状态
默认情况下,切换动态组件时无法保持组件的状态。此时可以使用 vue 内置的 组件保持动态组件的状态。
<template>
<div>
<h1 class="mb-4">App 根组件</h1>
<button type="button" class="btn btn-primary" @click="comName = 'MyHome'">首页</button>
<button type="button" class="btn btn-info ml-2" @click="comName = 'MyMovie'">电影</button>
<hr />
<!-- 使用 keep-alive 组件 -->
<keep-alive>
<component :is="comName"></component>
</keep-alive>
</div>
</template>
二十七、插槽
1.什么是插槽
插槽(Slot)是 vue 为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的部分定义为插槽。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jHLcqvTe-1685004795672)(./assets/Snipaste_2023-05-14_16-42-54.png)]
可以把插槽认为是组件封装期间,为用户预留的内容的占位符。
2.插槽的基础用法
在封装组件时,可以通过 元素定义插槽,从而为用户预留内容占位符:
2.1 没有预留插槽的内容会被丢弃
如果在封装组件时没有预留任何 插槽,则用户提供的任何自定义内容都会被丢弃。
2.2 后备内容
封装组件时,可以为预留的 插槽提供后备内容(默认内容)。如果组件的使用者没有为插槽提供任何内容,则后备内容会生效。
3.具名插槽
如果在封装组件时需要预留多个插槽节点,则需要为每个 插槽指定具体的 name 名称。这种带有具体名称的插槽叫做“具名插槽”:
<template>
<div>
<!-- 我们希望把页头放到这里 -->
<header>
<slot name="header"></slot>
</header>
<!-- 我们希望把主要内容放到这里,没有指定 name 名称的插槽,会有隐含的名称叫做 "default" -->
<main>
<slot></slot>
</main>
<!-- 我们希望把页脚放到这里 -->
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
3.1 为具名插槽提供内容
在向具名插槽提供内容的时候,我们可以在一个 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
<template>
<div>
<h1>App 根组件</h1>
<hr />
<!-- 使用组件 -->
<my-article>
<template #header>
<h1>滕王阁序</h1>
</template>
<template #default>
<p>豫章故郡,洪都新府。</p>
<p>星分翼轸,地接衡庐</p>
<p>襟三江而带五湖,控蛮荆而引瓯越。</p>
</template>
<template #footer>
<p>落款:王勃</p>
</template>
</my-article>
</div>
</template>
3.2 具名插槽的简写形式
跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header可以被重写为 #header
4.作用域插槽
在封装组件的过程中,可以为预留的 插槽绑定 props 数据,这种带有 props 数据的 叫做“作用域插槽”:
<!-- 子组件 -->
<div>
<h3>这是 TEST 组件</h3>
<slot :info="infomation"></slot>
</div>
<!-- 使用组件 -->
<my-test>
<template v-slot:default="scope">
{{ scope }}
</template>
</my-test>
4.1 解构作用域插槽的 Prop
作用域插槽对外提供的数据对象,可以使用解构赋值简化数据的接收过程:
<!-- 使用自定义组件 -->
<my-test>
<template #default="{ msg, info }">
<p>{{ msg }}</p>
<p>{{ info.address }}</p>
</template>
</my-test>
4.2 声明作用域插槽
在封装 MyTable 组件的过程中,可以通过作用域插槽把表格每一行的数据传递给组件的使用者:
<!-- 表格主体区域 -->
<tbody>
<!-- 循环渲染表格数据 -->
<tr v-for="item in list" :key="item.id">
<slot :user="item"></slot>
</tr>
</tbody>
4.3 使用作用域插槽
在使用 MyTable 组件时,自定义单元格的渲染方式,并接收作用域插槽对外提供的数据:
<!-- 表格主体区域 -->
<tbody>
<!-- 循环渲染表格数据 -->
<tr v-for="item in list" :key="item.id">
<slot :user="item"></slot>
</tr>
</tbody>
<!-- 使用组件,填充插槽 -->
<my-table>
<template #default="{ user }">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>
<input type="checkbox" :checked="user.state" />
</td>
</template>
</my-table>
二十八、自定义指令
1.什么是自定义指令
vue 官方提供了 v-for、v-model、v-if 等常用的内置指令。除此之外 vue 还允许开发者自定义指令。
vue 中的自定义指令分为两类,分别是:
- 私有自定义指令
- 全局自定义指令
2.声明私有自定义指令的语法
在每个 vue 组件中,可以在 directives 节点下声明私有自定义指令:
directives: {
// 自定义私有指令
focus: {
// 当被绑定的元素插入到 DOM 中时,自动触发 mounted 函数
mounted(el) {
// 被绑定的元素自动获取焦点
el.focus()
},
},
},
3.使用自定义指令
在使用自定义指令时,需要加上v-前缀:
<input v-focus />
4.声明全局自定义指令
全局共享的自定义指令需要通过“单页面应用程序的实例对象”进行声明:
app.directive('focus', {
mounted(el) {
console.log('mounted')
el.focus()
},
})
5.updeated 函数
mounted 函数只在元素第一次插入 DOM 时被调用,当 DOM 更新时 mounted 函数不会被触发。 updated 函数会在每次 DOM 更新完成后被调用。示例代码如下:
app.directive('focus', {
// 第一次插入 DOM 时触发这个函数
mounted(el) {
console.log('mounted')
el.focus()
},
// 每次 DOM 更新时都会触发 updated 函数
updated(el) {
console.log('updated')
el.focus()
},
})
注意:在 vue2 的项目中使用自定义指令时,【 mounted -> bind 】【 updated -> update 】
6.函数简写
如果 mounted 和updated 函数中的逻辑完全相同,则可以简写成如下格式:
app.directive('focus', (el) => {
// 在 mounted 和 updated 时都会触发相同的业务处理
el.focus()
})
7.指令的参数值
在绑定指令时,可以通过“等号”的形式为指令绑定具体的参数值:
<!-- 在使用 v-color 指令时,可以通过 “等号” 绑定指令的值 -->
<h3 v-color="'red'">MyHome 组件 --- {{ count }}</h3>
<input type="text" class="form-control" v-focus v-color="'cyan'" />
<!-- 自定义 v-color 指令 -->
app.directive('color', (el, binding) => {
<!-- binding.value 就是通过 “等号” 为指令绑定的值 -->
el.style.color = binding.value
})
二十九、第五个案例——Table 案例
1.案例描述
1.1 案例效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fWPqoSKM-1685004795673)(./assets/Snipaste_2023-05-17_17-29-53.png)]
1.2 用到的知识点
- 组件封装
- 具名插槽
- 作用域插槽
- 自定义指令
1.3 实现步骤
- 搭建项目的基本结构
- 请求商品列表的数据
- 封装 MyTable 组件
- 实现删除功能
- 实现添加标签的功能
2.具体实现
2.1 搭建项目基本结构
1.初始化项目
1 初始化项目 npm init vite-app case-table 2 cd 到项目根目录 cd case-table 3 安装项目依赖项 npm install 4 安装 less 依赖包 npm i less -D 5 项目运行 npm run dev
2.梳理项目结构
- 重置 App.vue 根组件的代码结构
- 删除 components 目录下 HelloWorld.vue 组件
- 重置 index.css 中的样式
- 在 main.js 入口文件中导入 bootstrap 样式文件
2.2 请求商品列表数据
1.运行如下命令,安装 Ajax 的请求库:
npm i axios -S
2.在 main.js 入口模块中导入并全局配置axios:
// 1.导入 axios import axios from 'axios' const app = createApp(App) // 2.将 axios 挂载到全局,今后,每个组件中,都可以直接通过this.$http 代替 axios 发起 Ajax 请求 app.config.globalProperties.$http = axios // 3. 配置请求的根路径 axios.defaults.baseURL = 'https://www.escook.cn' // 此网站接口已废弃 app.mount('#app')
3.在 App.vue 组件的 data 中声明 goodslist 商品列表数据:
data() { return { // 商品列表数据 goodslist:[] } }
4.在 App.vue 组件的 methods 中声明 getGoodsList 方法,用来从服务器请求商品列表的数据:
methods: { // 初始化商品列表数据 async getGoodsList() { // 发起 Ajax 请求 const { data: res } = await this.$http.get('/api/goods') // 请求失败 if (res.status !== 0) return console.log('获取商品列表失败!') // 请求成功 this.goodslist = res.data } }
5.在 App.vue 组件中,声明 created 生命周期函数,并调用 getGoodsList 方法:
created() { this.getGoodsList() }
2.3 封装 MyTable 组件
封装要求:
1.用户通过名为 data 的 prop 属性,为 MyTable.vue 组件指定数据源
2.在 MyTable.vue 组件中,预留名称为 header 的具名插槽
3.在 MyTable.vue 组件中,预留名称为 body 的作用域插槽
1.创建并使用 MyTable 组件
- 在 components/my-table 目录下新建 MyTable.vue 组件
- 在 App.vue 组件中导入并注册 MyTable.vue 组件
- 在 App.vue 组件的 DOM 结构中使用 MyTable.vue 组件
2.为表格声明 data 数据源
2.1 在 MyTable.vue 组件的 props 节点中声明表格的 data 数据源
<script> export default { name: 'MyTable', props: { // 表格的数据源 data: { type: Array, required: true, default: [], } }, } </script>
2.2 在 App.vue 组件中使用 MyTable.vue 组件时,通过属性绑定的形式为表格指定 data 数据源
<!-- 使用表格组件 --> <my-table :data="goodslist"></my-table>
3.封装 MyTable 组件的模板结构
3.1 基于 bootstrap 提供的 Tables( https://v4.bootcss.com/docs/content/tables/ ),在 MyTable.vue 组件中渲染最基本的模板结构:
<template> <table class="table table-bordered table-striped"> <!-- 表格的标题区域 --> <thead> <tr> <th>#</th> <th>商品名称</th> <th>价格</th> <th>标签</th> <th>操作</th> </tr> </thead> <!-- 表格的主体区域 --> <tbody></tbody> </table> </template>
3.2 为了提高组件的复用性,最好把表格的 标题区域 预留为 具名插槽,方便使用者自定义表格的标题:
<template> <table class="table table-bordered table-striped"> <!-- 表格的标题区域 --> <thead> <tr> <!-- 命名插槽 --> <slot name="header"></slot> </tr> </thead> <!-- 表格的主体区域 --> <tbody></tbody> </table> </template>
3.3 在 App.vue 组件中,通过具名插槽的形式,为 MyTable.vue 组件指定标题名称:
<!-- 使用表格组件 --> <my-table :data="goodslist"> <!-- 表格的标题 --> <template #header> <th>#</th> <th>商品名称</th> <th>价格</th> <th>标签</th> <th>操作</th> </template> </my-table>
4.预留名称为 body 的作用域插槽
4.1 在 MyTable.vue 组件中,通过 v-for 指令循环渲染表格的数据行:
<template> <table class="table table-bordered table-striped"> <!-- 表格的标题区域 --> <thead> <tr> <!-- 命名插槽 --> <slot name="header"></slot> </tr> </thead> <!-- 表格的主体区域 --> <tbody> <!-- 使用 v-for 指令,循环渲染表格的数据行 --> <tr v-for="(item, i) in data" :key="item.id"></tr> </tbody> </table> </template>
4.2 为了提高 MyTable.vue 组件的复用性,最好把表格数据行里面的 td 单元格预留为 具名插槽:
<template> <table class="table table-bordered table-striped"> <!-- 表格的标题区域 --> <thead> <tr> <!-- 命名插槽 --> <slot name="header"></slot> </tr> </thead> <!-- 表格的主体区域 --> <tbody> <!-- 使用 v-for 指令,循环渲染表格的数据行 --> <tr v-for="(item, i) in data" :key="item.id"> <!-- 为数据行的 td 预留的插槽 --> <slot name="body"></slot> </tr> </tbody> </table> </template>
4.3 为了让组件的使用者在提供 body 插槽的内容时,能够自定义内容的渲染方式,需要把 body 具名插槽升级为 作用域插槽 :
<template> <table class="table table-bordered table-striped"> <!-- 表格的标题区域 --> <thead> <tr> <!-- 命名插槽 --> <slot name="header"></slot> </tr> </thead> <!-- 表格的主体区域 --> <tbody> <!-- 使用 v-for 指令,循环渲染表格的数据行 --> <tr v-for="(item, i) in data" :key="item.id"> <!-- 为数据行的 td 预留的 "作用域插槽" --> <slot name="body" :row="item" :index="index"></slot> </tr> </tbody> </table> </template>
4.4 在 App.vue 组件中,基于作用域插槽的方式渲染表格的数据:
<!-- 使用表格组件 --> <my-table :data="goodslist"> <!-- 表格的标题 --> <template #header> <th>#</th> <th>商品名称</th> <th>价格</th> <th>标签</th> <th>操作</th> </template> <template #body="{ row, index }"> <td>{{ index + 1 }}</td> <td>{{ row.goods_name }}</td> <td>¥{{ row.goods_price }}</td> <td>{{ row.tags }}</td> <td> <button type="button" class="btn btn-danger btn-sm">删除</button> </td> </template> </my-table>
2.4 实现删除功能
1.为删除按钮绑定 click 事件处理函数:
<button type="button" class="btn btn-danger btn-sm" @click="onRemove(row.id)">删除</button>
2.在 App.vue 组件的 methods 中声明事件处理函数如下:
methods: { // 根据 id 删除商品信息 onRemove(id) { this.goodslist = this.goodslist.filter(x => x.id !== id) }, },
2.5 实现添加标签功能
1.自定义渲染标签列
根据 bootstrap 提供的 Badge ( https://v4.bootcss.com/docs/components/badge/#contextual-variations )效果,循环渲染商品的标签信息如下:
<td> <span class="badge badge-warning ml-2" v-for="item in row.tags" :key="item">{{ item }}</span></td>
2.实现 input 和 button 的按需展示
2.1 使用 v-if 结合 v-else 指令,控制 input 和 button 的按需展示:
<td> <!-- 基于当前行的 inputVisible,来控制 input 和 button 的按需展示--> <input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible"> <button type="button" class="btn btn-primary btn-sm" v-else>+Tag</button> <span class="badge badge-warning ml-2" v-for="item in row.tags" :key="item">{{ item }}</span> </td>
2.2 点击按钮,控制 input 和 button 的切换:
<td> <!-- 基于当前行的 inputVisible,来控制 input 和 button 的按需展示--> <input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible"> <button type="button" class="btn btn-primary btn-sm" @click="row.inputVisible = true" v-else>+Tag</button> <span class="badge badge-warning ml-2" v-for="item in row.tags" :key="item">{{ item }}</span> </td>
3.让 input 自动获取焦点
3.1 在 App.vue 组件中,通过 directives 节点自定义 v-focus 指令如下:
directives: { // 封装自动获得焦点的指令 focus(el) { el.focus() }, },
3.2 为 input 输入框应用 v-focus 指令:
<input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible" v-focus>
4.文本框失去焦点自动隐藏
4.1 使用 v-model 指令把 input 输入框的值双向绑定到 row.inputValue 中:
<input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible" v-focus v-model.trim="row.inputValue">
4.2 监听文本框的 blur 事件,在触发其事件处理函数时,把 当前行的数据 传递进去:
<input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible" v-focus v-model.trim="row.inputValue" @blur="onInputConfirm(row)">
4.3 在 App.vue 组件的 methods 节点下声明 onInputConfirm 事件处理函数:
onInputConfirm(row) { // 1. 把用户在文本框中输入的值,预先转存到常量 val 中 const val = row.inputValue // 2. 清空文本框的值 row.inputValue = '' // 3. 隐藏文本框 row.inputVisible = false }
5.为商品添加新的 tag 标签
进一步修改 onInputConfirm 事件处理函数如下:
onInputConfirm(row) { // 1. 把用户在文本框中输入的值,预先转存到常量 val 中 const val = row.inputValue // 2. 清空文本框的值 row.inputValue = '' // 3. 隐藏文本框 row.inputVisible = false // 1.1 判断 val 的值是否为空,如果为空,则不进行添加 // 1.2 判断 val 的值是否已存在于 tags 数组中,防止重复添加 if (!val || row.tags.indexOf(val) !== -1) return // 2. 将用户输入的内容,作为新标签 push 到当前行的 tags 数组中 row.tags.push(val) }
6.响应文本框的回车按键
当用户在文本框中敲击了 回车键 的时候,也希望能够把当前输入的内容添加为 tag 标签。此时,可以为文本框绑定 keyup 事件如下:
<input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible" v-focus v-model.trim="row.inputValue" @blur="onInputConfirm(row)" @keyup.enter="onIputConfirm(row)" >
7.响应文本框的 esc 按键
当用户在文本框中敲击了 esc 按键的时候,希望能够快速清空文本框的内容。此时,可以为文本框绑定 keyup 事件如下:
<input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible" v-focus v-model.trim="row.inputValue" @blur="onInputConfirm(row)" @keyup.enter="onIputConfirm(row)" @keyup.esc="row.inputValue = ''">
3.完整代码
App 根组件
<template> <div> <h1>App 根组件</h1> </div> <hr> <!-- 使用表格组件 --> <my-table :data="goodslist"> <!-- 表格的标题 --> <template #header> <th>#</th> <th>商品名称</th> <th>价格</th> <th>标签</th> <th>操作</th> </template> <template #body="{ row, index }"> <td>{{ index + 1 }}</td> <td>{{ row.goods_name }}</td> <td>¥{{ row.goods_price }}</td> <td> <!-- 基于当前行的 inputVisible,来控制 input 和 button 的按需展示--> <input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible" v-focus v-model.trim="row.inputValue" @blur="onInputConfirm(row)" @keyup.enter="onInputConfirm(row)" @keyup.esc="row.inputValue = ''"> <button type="button" class="btn btn-primary btn-sm" @click="row.inputVisible = true" v-else>+Tag</button> <span class="badge badge-warning ml-2" v-for="item in row.tags" :key="item">{{ item }}</span> </td> <td> <button type="button" class="btn btn-danger btn-sm" @click="onRemove(row.id)">删除</button> </td> </template> </my-table> </template> <script> import MyTable from './components/my-table/MyTable.vue' export default { name: 'MyApp', data() { return { goodslist: [] } }, created() { this.getGoodsList() }, methods: { async getGoodsList() { const { data: res } = await this.$http.get('/api/goods') if (res.status !== 0) return console.log('获取商品列表失败!') this.goodslist = res.data }, // 根据 id 删除商品信息 onRemove(id) { this.goodslist = this.goodslist.filter(x => x.id !== id) }, onInputConfirm(row) { // 1. 把用户在文本框中输入的值,预先转存到常量 val 中 const val = row.inputValue // 2. 清空文本框的值 row.inputValue = '' // 3. 隐藏文本框 row.inputVisible = false // 1.1 判断 val 的值是否为空,如果为空,则不进行添加 // 1.2 判断 val 的值是否已存在于 tags 数组中,防止重复添加 if (!val || row.tags.indexOf(val) !== -1) return // 2. 将用户输入的内容,作为新标签 push 到当前行的 tags 数组中 row.tags.push(val) } }, directives: { // 封装自动获得焦点的指令 focus(el) { el.focus() }, }, components: { MyTable, } } </script> <style lang="less" scoped></style>
MyTable 子组件
<template> <table class="table table-bordered table-striped"> <!-- 表格的标题区域 --> <thead> <tr> <!-- 命名插槽 --> <slot name="header"></slot> </tr> </thead> <!-- 表格的主体区域 --> <tbody> <!-- 使用 v-for 指令,循环渲染表格的数据行 --> <tr v-for="(item, index) in data" :key="item.id"> <!-- 为数据行的 td 预留的 "作用域插槽" --> <slot name="body" :row="item" :index="index"></slot> </tr> </tbody> </table> </template> <script> export default { name: 'MyTable', props: { // 表格的数据源 data: { type: Array, required: true, default: [], } }, } </script> <style lang="less" scoped></style>
main.js 入口文件
import { createApp } from 'vue' import App from './App.vue' import './index.css' import './assets/css/bootstrap.css' import axios from 'axios' const app = createApp(App) app.config.globalProperties.$http = axios axios.defaults.baseURL = 'https://applet-base-api-t.itheima.net' app.mount('#app')
三十、vue 组件高级(下)总结
① 能够知道如何使用 ref 引用 DOM 和组件实例
- 通过 ref 属性指定引用的名称、使用 this.$refs 访问引用实例
② 能够知道 $nextTick 的调用时机
- 组件的 DOM 更新之后,才执行 $nextTick 中的回调
③ 能够说出 keep-alive 元素的作用
- 保持动态组件的状态
④ 能够掌握插槽的基本用法
- 标签、具名插槽、作用域插槽、v-slot: 简写为 #
⑤ 能够知道如何自定义指令
- 私有自定义指令、全局自定义指令
Ⅵ 路由
目标 | 目录 |
---|---|
前端路由的工作方式以及原理 | 三十一、前端路由的概念与原理 |
vue-router 4.x 的基本使用 | 三十二、vue-router 的基本用法 |
重定向、高亮、嵌套、动态、编程式导航、命名、守卫 | 三十三、vue-router 的高级用法 |
巩固 vue-router 的使用 | 三十四、第六个案例——后台管理案例 |
概括与总结 | 三十五、路由总结 |
三十一、前端路由的概念与原理
1.什么是路由
路由(英文:router)就是对应关系。路由分为两大类:
- 后端路由
- 前端路由
2.回顾:后端路由
后端路由指的是:请求方式、请求地址与 function 处理函数之间的对应关系。在 node.js 课程中,express 路由的基本用法如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aTI392F3-1685004795673)(./assets/Snipaste_2023-05-18_21-22-28.png)]
3.SPA 与前端路由
SPA 指的是一个 web 网站只有唯一的一个 HTML 页面,所有组件的展示与切换都在这唯一的一个页面内完成。此时,不同组件之间的切换需要通过前端路由来实现。
结论:在 SPA 项目中,不同功能之间的切换,要依赖于前端路由来完成!
4.什么是前端路由
通俗易懂的概念:Hash 地址与组件之间的对应关系。
5.前端路由的工作方式
① 用户点击了页面上的路由链接
② 导致了 URL 地址栏中的 Hash 值发生了变化
③ 前端路由监听了到 Hash 地址的变化
④ 前端路由把当前 Hash 地址对应的组件渲染都浏览器中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BjuiS0HL-1685004795674)(./assets/Snipaste_2023-05-18_21-31-12.png)]
结论:前端路由,指的是 Hash 地址与组件之间的对应关系!
6.实现简易的前端路由
6.1 导入并注册 MyHome、MyMovie、MyAbout 三个组件:
<script>
import MyHome from './MyHome.vue'
import MyMovie from './MyMovie.vue'
import MyAbout from './MyAbout.vue'
export default {
name: 'MyApp',
components: {
MyHome,
MyMovie,
MyAbout,
},
}
</script>
6.2 通过 标签的 is 属性,动态切换要显示的组件:
<template>
<div>
<h1>App 根组件</h1>
<component :is="comName"></component>
</div>
</template>
<script>
import MyHome from './MyHome.vue'
import MyMovie from './MyMovie.vue'
import MyAbout from './MyAbout.vue'
export default {
name: 'MyApp',
data() {
return {
comName: 'MyHome',
}
},
}
</script>
6.3 在组件的结构中声明如下 3 个 链接,通过点击不同的 链接,切换浏览器地址栏中的 Hash 值:
<a href="#/home">Home</a>
<a href="#/movie">Movie</a>
<a href="#/about">About</a>
6.4 在 created 生命周期函数中监听浏览器地址栏中 Hash 地址的变化,动态切换要展示的组件的名称:
created() {
// 监听 hash 值变化的事件
window.onhashchange = () => {
// 通过 location.hash 获取到最新的 hash 值,并进行匹配
switch (location.hash) {
case '#/home':
this.comName = 'MyHome'
break
case '#/movie':
this.comName = 'MyMovie'
break
case '#/about':
this.comName = 'MyAbout'
break
}
}
},
三十二、vue-router 的基本用法
1.什么是 vue-router
vue-router 是 vue.js 官方给出的路由解决方案。它只能结合 vue 项目进行使用,能够轻松的管理 SPA 项目中组件的切换。
2.vue-router 的版本
vue-router 目前有 3.x 的版本和 4.x 的版本。其中:
- vue-router 3.x 只能结合 vue2 进行使用
- vue-router 4.x 只能结合 vue3 进行使用
vue-router 3.x 的官方文档地址:https://router.vuejs.org/zh/
vue-router 4.x 的官方文档地址:https://next.router.vuejs.org/
3.vue-router 4.x 的基本使用步骤
① 在项目中安装 vue-router
② 定义路由组件
③ 声明路由链接和占位符
④ 创建路由模块
⑤ 导入并挂载路由模块
3.1 在项目中安装 vue-router
在 vue3 的项目中,只能安装并使用 vue-router 4.x
npm install vue-router@next -S
3.2 定义路由组件
在项目中定义 MyHome.vue、MyMovie.vue、MyAbout.vue 三个组件,将来要使用 vue-router 来控制它们的展示与切换。
3.3 声明路由链接和占位符
可以使用 标签来声明路由链接,并使用 标签来声明路由占位符:
<template>
<div>
<h1>vue-router 的基本使用</h1>
<!-- 声明路由链接 -->
<router-link to="/home">首页</router-link>
<router-link to="/movie">电影</router-link>
<router-link to="/about">关于</router-link>
<hr />
<!-- 路由的占位符 -->
<router-view></router-view>
</div>
</template>
3.4 创建路由模块
在项目中创建 router.js 路由模块,在其中按照如下 4 个步骤创建并得到路由的实例对象:
① 从 vue-router 中按需导入两个方法
② 导入需要使用路由控制的组件
③ 创建路由实例对象
④ 向外共享路由实例对象
⑤ 在 main.js 中导入并挂载路由模块
// 1.从 vue-router 中按需导入两个方法
// createRouter 方法用于创建路由的实例对象
// createWebHashHistory 用于指定路由的工作模式(hash 模式)
import { createRouter, createWebHashHistory } from 'vue-router'
// 2.导入组件,这些组件将要以路由的方式,来控制它们的切换
import Home from './MyHome.vue'
import Movie from './MyMovie.vue'
import About from './MyAbout.vue'
// 3.创建路由实例对象
const router = createRouter({
// 3.1 通过 history 属性指定路由的工作模式
history: createWebHashHistory(),
// 3.2 通过 routes 数组,声明路由的匹配规则
routes: [
// path 是 hash 地址,component 是要展示的组件
{ path: '/home', component: Home },
{ path: '/movie', component: Movie },
{ path: '/about', component: About },
],
})
// 4.向外共享路由实例对象
// 供其他模块导入并使用
export default router
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
// 5.导入路由模块
import router from './router'
const app = createApp(App)
// 6.挂载路由模块
// app.use() 方法用来挂载“第三方的插件模块”
app.use(router)
app.mount('#app')
三十三、vue-router 的高级用法
1.路由重定向
路由重定向指的是:用户在访问地址 A 的时候,强制用户跳转到地址 C ,从而展示特定的组件页面。通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向:
// 创建路由对象
const router = createRouter({
// 指定路由的工作模式
history: createWebHashHistory(),
// 声明路由的匹配规则
routes: [
// 其中,path 表示需要被重定向的“原地址”,redirect 表示将要被重定向到的“新地址”
{ path: '/', redirect: '/home' },
{ path: '/home', component: Home },
{ path: '/movie', component: Movie },
{ path: '/about', component: About },
],
})
2.路由高亮
可以通过如下的两种方式,将激活的路由链接进行高亮显示:
① 使用默认的高亮 class 类
② 自定义路由高亮的 class 类
2.1 默认的高亮 class 类
被激活的路由链接,默认会应用一个叫做 router-link-active 的类名。开发者可以使用此类名选择器,为激活的路由链接设置高亮的样式:
/* 在 index.css 全局样式表中,重新设置 router-link-active 的样式 */
.router-link-active {
background-color: red;
color: white;
font-weight: bold;
}
2.2 自定义路由的高亮的 class 类
在创建路由的实例对象时,开发者可以基于 linkActiveClass 属性,自定义路由链接被激活时所应用的类名:
// 创建路由对象
const router = createRouter({
// 指定路由的工作模式
history: createWebHashHistory(),
// 指定被激活的路由链接,会应用 router-active 这个类名
// 默认的 router-link-active 类名会被覆盖掉
// 自定义路由高亮的 class 类
linkActiveClass: 'active-router',
// 声明路由的匹配规则
routes: [
{ path: '/', redirect: '/home' },
{ path: '/home', component: Home },
{ path: '/movie', component: Movie },
{ path: '/about', component: About },
],
})
3.嵌套路由
通过路由实现组件的嵌套展示,叫做嵌套路由。
① 声明子路由链接和子路由占位符
② 在父路由规则中,通过 children 属性嵌套声明子路由规则
3.1 声明子路由链接和子路由占位符
在 About.vue 组件中,声明 tab1 和 tab2 的子路由链接以及子路由占位符:
<template>
<div>
<h3>MyAbout 组件</h3>
<!-- 声明子路由链接 -->
<router-link to="/about/tab1">Tab1</router-link>
<router-link to="/about/tab2">Tab2</router-link>
<hr>
<!-- 声明子路由占位符 -->
<router-view></router-view>
</div>
</template>
3.2 通 children 属性声明子路由规则
在 router.js 路由模块中,导入需要的组件,并使用 children 属性声明子路由规则:
// 创建路由对象
const router = createRouter({
// 指定路由的工作模式
history: createWebHashHistory(),
// 自定义路由高亮的 class 类
linkActiveClass: 'active-router',
// 声明路由的匹配规则
routes: [
{ path: '/', redirect: '/home' },
{ path: '/home', component: Home },
{ path: '/movie', component: Movie },
{
path: '/about',
component: About,
// 嵌套路由的重定向
redirect: '/about/tab1',
// 通过 children 属性嵌套声明子级路由规则
children: [
{ path: 'tab1', component: Tab1 },
{ path: 'tab2', component: Tab2 },
],
},
],
})
4.动态路由匹配
4.1 动态路由的概念
动态路由指的是:把 Hash 地址中可变的部分定义为参数项,从而提高路由规则的复用性。在 vue-router 中使用英文的冒号(:)来定义路由的参数项:
// 路由中的动态参数以 : 进行声明,冒号后面的是动态参数的名称
{ path: './movie/:id', component: Movie }
// 将以下 3 个路由规则,合并成了一个,提高了路由规则的复用性
{ path: './movie/1', component: Movie }
{ path: './movie/2', component: Movie }
{ path: './movie/3', component: Movie }
4.2 $route.params 参数对象
通过动态路由匹配的方式渲染出来的组件中,可以使用 $route.params 对象访问到动态匹配的参数值。
<template>
<div>
<!-- $route.params 是路由的"参数对象" -->
<h3>MyMovie 组件 --- {{ $route.params.id }}</h3>
</div>
</template>
<script>
export default {
name: 'MyMovie',
}
</script>
4.3 使用 props 接收路由参数
为了简化路由参数的获取形式,vue-router 允许在路由规则中开启 props 传参:
// 1.定义路由规则时,声明 props: true 选项
// 即可在 Movie 组件中,以 props 的形式接收到路由规则匹配到的参数项
{ path: '/movie/:mid', component: Movie, props: true },
<template>
<div>
<!-- 3.直接使用 props 中接收的路由参数 -->
<h3>MyMovie 组件 --- {{ id }}</h3>
</div>
</template>
<script>
export default {
name: 'MyMovie',
// 2.使用 props 接收路由匹配规则中匹配到的参数项
props: ['id'],
}
</script>
5.编程式导航
通过调用 API 实现导航的方式,叫做编程式导航。与之对应的,通过点击链接实现导航的方式,叫做声明式导航。例如:
- 普通网页中点击 链接、vue 项目中点击 都属于声明式导航
- 普通网页中调用 location.href 跳转到新页面的方式,属于编程式导航
5.1 vue-router 中的编程式导航 API
vue-router 提供了许多编程式导航的 API,其中最常用的两个 API 分别是:
① this.$router.push(‘hash 地址’) 跳转到指定 Hash 地址,从而展示对应的组件
② this.$router.go(数值 n) 实现导航历史的前进、后退
5.2 $router.push
调用 this.$router.push() 方法,可以跳转到指定的 hash 地址,从而展示对应的组件页面:
<template>
<div>
<h3>MyHome 组件</h3>
<button type="button" class="btn btn-primary" @click="goToMovie(3)">导航到Movie页面</button>
</div>
</template>
<script>
export default {
name: 'MyHome',
methods: {
goToMovie(id) {
this.$router.push('/movie/' + id)
},
},
}
</script>
5.3 $router.go
调用 this.$router.go() 方法,可以在浏览历史中进行前进和后退:
<template>
<h3>MyMovie --- {{id}}</h3>
<button @click="goBack">后退</button>
</template>
<script>
export default {
props: ['id'],
methods:{
// 后退到之前的组件页面
goBack() {
this.$router.go(-1)
}
},
}
</script>
6.命名路由
通过 name 属性为路由规则定义名称的方式,叫做命名路由:
{
path: '/movie/:id',
// 使用 name 属性为当前的路由规则定义一个“名称”
name: 'mov',
component: Movie,
props: true,
}
注意:命名路由的 name 值不能重复,必须保证唯一性!
6.1 使用命名路由实现声明式导航
为 标签动态绑定 to 属性的值,并通过 name 属性指定要跳转到的路由规则。期间还可以用 params 属性指定跳转期间要携带的路由参数:
<router-link :to="{ name: 'mov', params: { mid: 2 } }">go to movie</router-link>
6.2 使用命名路由实现编程式导航
调用 push 函数期间指定一个配置对象,name 是要跳转到的路由规则、params 是携带的路由参数:
<template>
<div>
<h3>MyHome 组件</h3>
<!-- 命名路由声明式导航 -->
<router-link :to="{ name: 'mov', params: { mid: 2 } }">go to movie</router-link>
<!-- 命名路由编程式导航 -->
<button type="button" class="btn btn-primary" @click="goToMovie(1)">go to movie</button>
</div>
</template>
<script>
export default {
name: 'MyHome',
methods: {
goToMovie(id) {
this.$router.push({
name: 'mov',
params: {
mid: id,
},
})
},
},
}
</script>
7.导航守卫
导航守卫可以控制路由的访问权限:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kPBMcWfs-1685004795674)(./assets/Snipaste_2023-05-21_16-43-40.png)]
7.1 如何声明全局导航首位
全局导航守卫会拦截每个路由规则,从而对每个路由进行访问权限的控制。可以按照如下的方式定义全局导航守卫:
// 声明全局的导航守卫
// 调用路由实例对象的 beforeEach 函数,声明“全局前置守卫”
// fn 必须是一个函数,每次拦截到路由的请求,都会调用 fn 进行处理
// 因此 fn 叫做 “守卫方法”
router.beforeEach( () => {
console.log('ok')
})
7.2 守卫方法的 3 个形参
全局导航守卫的守卫方法中接收 3 个形参,格式为:
// 声明全局的导航守卫
router.beforeEach((to, from, next) => {
// to 目标路由对象
// from 当前导航正要离开的路由对象
// next 是一个函数,表示放行
})
注意:
① 在守卫方法中如果不声明 next 形参,则默认允许用户访问每一个路由!
② 在守卫方法中如果声明了 next 形参,则必须调用 next() 函数,否则不允许用户访问任何一个路由!
7.3 next 函数的 3 中调用方式
参考示意图,分析 next 函数的 3 种调用方式最终导致的结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L274eskD-1685004795675)(./assets/Snipaste_2023-05-21_16-47-39.png)]
直接放行:next()
强制其停留在当前页面:next(false)
强制其跳转到登录页面:next(‘/login’)
// 创建路由对象
const router = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/', redirect: '/home' },
{ path: '/home', component: Home },
{ path: '/main', component: Main },
{ path: '/login', component: Login },
],
})
// 全局路由导航守卫
router.beforeEach((to, from, next) => {
if (to.path === '/main') {
// 证明用户要访问后台主页
next('/login')
} else {
// 访问的不是后台主页
next()
}
})
7.4 结合 token 控制后台主页的访问权限
// 创建路由对象
const router = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/', redirect: '/home' },
{ path: '/home', component: Home },
{ path: '/main', component: Main },
{ path: '/login', component: Login },
],
})
// 全局路由导航守卫
router.beforeEach((to, from, next) => {
// 1.读取 token
const tokenStr = localStorage.getItem('token')
// 2.想要访问 “后台主页” 且 token 值不存在
if (to.path === '/main' && !tokenStr) {
// 3.1 不允许跳转
// next(false)
// 3.2 强行跳转到 “登录页面”
next('/login')
} else {
// 3.3 直接放行,允许访问 “后台主页”
next()
}
})
三十四、第六个案例——后台管理案例
1.案例效果
2.案例用到的知识点
- 命名路由
- 路由重定向
- 导航守卫
- 嵌套路由
- 动态路由匹配
- 编程式导航
3.具体实现
安装并配置 vue-router 4.x
展示 Login.vue 登录组件
模拟并实现登录功能
通过路由渲染 Home.vue
实现退出登录的功能
全局控制路由的访问权限
将左侧菜单改造为路由链接
渲染用户管理页面的数据
实现跳转到用户详情页的功能
开启路由的 props 传参
通过编程式导航实现后退功能
3.1 安装并配置 vue-router 4.x
1.运行如下的安装,安装 vue-router :
npm i vue-router@next -S
2.在 src 目录下新建 router.js 路由模块:
// 1. 按需导入对应的函数 import { createRouter, createWebHashHistory } from 'vue-router' // 2. 创建路由对象 const router = createRouter({ history: createWebHashHistory(), routes: [], }) // 3. 向外共享路由实例对象 export default router
3.在 main.js 入口文件中导入并挂载路由对象:
// 1.导入路由模块 import router from './router' const app = createApp(App) // 2.挂载路由对象 app.use(router) app.mount('#app')
3.2 展示 Login.vue 登录组件
1.在 router.js 模块中导入 Login.vue 组件:
import Login from './components/MyLogin.vue'
2.声明路由规则如下:
routes: [ // 路由重定向 { path: '/', redirect: '/login' }, { path: '/login', component: Login }, ]
3.在 App.vue 组件中声明路由占位符:
<template> <!-- 路由的占位符 --> <router-view></router-view> </template> <script> export default { name: 'MyApp', } </script> <style lang="less" scoped> </style>
3.3 模拟并实现登录功能
1.在 MyLogin.vue 组件中声明如下的 data 数据:
data() { return { username: '', password: '', } }
2.为用户名和密码的文本框进行 v-model 双向数据绑定:
<!-- 登录名称 --> <div class="form-group form-inline"> <label for="username">登录名称</label> <input type="text" class="form-control ml-2" id="username" placeholder="请输入登录名称" autocomplete="off" v-model="username"> </div> <!-- 登录密码 --> <div class="form-group form-inline"> <label for="password">登录密码</label> <input type="password" class="form-control ml-2" id="password" placeholder="请输入登录密码" v-model="password"> </div>
3.为 登录按钮 绑定点击事件处理函数:
<button type="button" class="btn btn-primary" @click="onLoginClick">登录</button>
4.在 methods 中声明 onLoginClick 事件处理函数如下:
methods: { onLoginClick() { // 判断用户名和密码是否正确 if (this.username === 'admin' && this.password === '123456') { // 登录成功,跳转到后台主页 this.$router.push('/home') // 模拟存储 Token 的操作 return localStorage.setItem('token', 'Bearer xxx') } // 登录失败,清除 Token localStorage.removeItem('token') }, },
3.4 通过路由渲染 Home.vue
1.在 router.js 中导入 Home.vue 组件:
import Home from './components/MyHome.vue'
2.在 routes 路由规则的数组中,声明对应的路由规则:
routes: [ { path: '/', redirect: '/login' }, { path: '/login', component: Login }, // Home 组件的路由规则 { path: '/home', component: Home }, ]
3.渲染 Home.vue 组件的基本结构:
<template> <div class="home-container"> <!-- 头部组件 --> <my-header></my-header> <!-- 主体区域 --> <div class="home-main-box"> <!-- 左侧边栏区域 --> <my-aside></my-aside> <!-- 右侧内容主体区域 --> <div class="home-main-body"></div> </div> </div> </template>
3.5 实现退出登录的功能
1.在 MyHeader.vue 组件中,为 退出登录 按钮绑定 click 事件处理函数:
2.在 methods 中声明如下的事件处理函数:
3.6 全局控制路由的访问权限
1.在 router.js 模块中,通过 router 路由实例对象,全局挂载路由导航守卫:
// 全局路由导航守卫 router.beforeEach((to, from, next) => { // 如果用户访问的是登录页面,直接放行 if (to.path === '/login') return next() // 获取 Token 值 const token = localStorage.getItem('token') if (!token) { // Token 值不存在,强制跳转到登录页面 return next('/login') } // 存在 Token 值,直接放行 next() })
3.7 将左侧菜单改造为路由链接
1.打开 MyAside.vue 组件,把 li 内部的纯文本升级改造为 组件:
<template> <div class="layout-aside-container"> <!-- 左侧边栏列表 --> <ul class="user-select-none menu"> <li class="menu-item"> <router-link to="/home/users">用户管理</router-link> </li> <li class="menu-item"> <router-link to="/home/rights">权限管理</router-link> </li> <li class="menu-item"> <router-link to="/home/goods">商品管理</router-link> </li> <li class="menu-item"> <router-link to="/home/orders">订单管理</router-link> </li> <li class="menu-item"> <router-link to="/home/settings">系统设置</router-link> </li> </ul> </div> </template>
2.打开 Home.vue 组件,在 右侧内容主体区域 中声明子路由的占位符:
<template> <div class="home-container"> <!-- 头部组件 --> <my-header></my-header> <!-- 主体区域 --> <div class="home-main-box"> <!-- 左侧边栏区域 --> <my-aside></my-aside> <!-- 右侧内容主体区域 --> <div class="home-main-body"> <!-- **子路由的占位符** --> <router-view></router-view> </div> </div> </div> </template>
3.在 router.js 中导入左侧菜单对应的组件:
import Users from './components/menus/MyUsers.vue' import Rights from './components/menus/MyRights.vue' import Goods from './components/menus/MyGoods.vue' import Orders from './components/menus/MyOrders.vue' import Settings from './components/menus/MySettings.vue'
4.通过 children 属性,为 home 规则定义子路由规则如下:
{ path: '/home', component: Home, // 用户访问 /home 时,重定向到 /home/users redirect: '/home/users', // 子路由规则 children: [ { path: 'users', component: Users }, { path: 'rights', component: Rights }, { path: 'goods', component: Goods }, { path: 'orders', component: Orders }, { path: 'settings', component: Settings }, ], },
3.8 渲染用户管理页面的数据
1.在 MyUsers.vue 组件中,通过 v-for 指令循环渲染用户列表的数据:
3.9 实现跳转到用户详细页的功能
1.在 MyUsers.vue 组件中,渲染详情页的路由链接如下:
<td> <router-link :to="'/home/users/' + item.id">详情</router-link> </td>
2.在 router.js 中导入用户详情页组件:
import UserDetail from './components/user/MyUserDetail.vue'
3.在 home 规则的 children 节点下,声明 用户详情页 的路由规则:
{ path: '/home', component: Home, redirect: '/home/users', children: [ { path: 'users', component: Users }, { path: 'rights', component: Rights }, { path: 'goods', component: Goods }, { path: 'orders', component: Orders }, { path: 'settings', component: Settings }, // 用户详情页的路由规则 { path: 'users/:id', component: UserDetail }, ], },
3.10 开启路由的 props 传参
1.在 router.js 模块中,为 用户详情页 的路由规则开启 props 传参:
{ path: 'users/:id', component: UserDetail, props: true },
2.在 MyUserDetail.vue 组件中声明 props 参数:
export default { name: 'MyUserDetail', props: ['id'], }
3.在 MyUserDetail.vue 组件的结构中直接使用路由参数:
<template> <button type="button" class="btn btn-light btn-sm">后退</button> <h4 class="text-center">用户详情 --- {{id}}</h4> </template>
3.11 通过编程式导航实现后退功能
1.在 MyUserDetail.vue 组件中,为后退按钮绑定点击事件处理函数:
<template> <button type="button" class="btn btn-light btn-sm" @click="goBack">后退</button> <h4 class="text-center">用户详情 --- {{id}}</h4> </template>
2.在 methods 中声明 goBack 事件处理函数如下:
export default { name: 'MyUserDetail', props: ['id'], methods: { // 编程式导航实现后退功能 goBack() { this.$router.go(-1) }, }, }
三十五、路由总结
① 能够知道如何在 vue 中配置路由
- createRouter、app.use(router)
② 能够知道如何使用嵌套路由
- 通过 children 属性进行路由嵌套、子路由的 hash 地址不要以 / 开头
③ 能够知道如何实现动态路由匹配
- 使用冒号声明参数项、this.$route.params、props: true
④ 能够知道如何使用编程式导航
- this. r o u t e r . p u s h 、 t h i s . router.push、this. router.push、this.router.go(-1)
⑤ 能够知道如何使用全局导航守卫
- 路由实例.beforeEach((to, from, next) => { })
3.3 模拟并实现登录功能
1.在 MyLogin.vue 组件中声明如下的 data 数据:
data() { return { username: '', password: '', } }
2.为用户名和密码的文本框进行 v-model 双向数据绑定:
<!-- 登录名称 --> <div class="form-group form-inline"> <label for="username">登录名称</label> <input type="text" class="form-control ml-2" id="username" placeholder="请输入登录名称" autocomplete="off" v-model="username"> </div> <!-- 登录密码 --> <div class="form-group form-inline"> <label for="password">登录密码</label> <input type="password" class="form-control ml-2" id="password" placeholder="请输入登录密码" v-model="password"> </div>
3.为 登录按钮 绑定点击事件处理函数:
<button type="button" class="btn btn-primary" @click="onLoginClick">登录</button>
4.在 methods 中声明 onLoginClick 事件处理函数如下:
methods: { onLoginClick() { // 判断用户名和密码是否正确 if (this.username === 'admin' && this.password === '123456') { // 登录成功,跳转到后台主页 this.$router.push('/home') // 模拟存储 Token 的操作 return localStorage.setItem('token', 'Bearer xxx') } // 登录失败,清除 Token localStorage.removeItem('token') }, },
3.4 通过路由渲染 Home.vue
1.在 router.js 中导入 Home.vue 组件:
import Home from './components/MyHome.vue'
2.在 routes 路由规则的数组中,声明对应的路由规则:
routes: [ { path: '/', redirect: '/login' }, { path: '/login', component: Login }, // Home 组件的路由规则 { path: '/home', component: Home }, ]
3.渲染 Home.vue 组件的基本结构:
<template> <div class="home-container"> <!-- 头部组件 --> <my-header></my-header> <!-- 主体区域 --> <div class="home-main-box"> <!-- 左侧边栏区域 --> <my-aside></my-aside> <!-- 右侧内容主体区域 --> <div class="home-main-body"></div> </div> </div> </template>
3.5 实现退出登录的功能
1.在 MyHeader.vue 组件中,为 退出登录 按钮绑定 click 事件处理函数:
2.在 methods 中声明如下的事件处理函数:
3.6 全局控制路由的访问权限
1.在 router.js 模块中,通过 router 路由实例对象,全局挂载路由导航守卫:
// 全局路由导航守卫 router.beforeEach((to, from, next) => { // 如果用户访问的是登录页面,直接放行 if (to.path === '/login') return next() // 获取 Token 值 const token = localStorage.getItem('token') if (!token) { // Token 值不存在,强制跳转到登录页面 return next('/login') } // 存在 Token 值,直接放行 next() })
3.7 将左侧菜单改造为路由链接
1.打开 MyAside.vue 组件,把 li 内部的纯文本升级改造为 组件:
<template> <div class="layout-aside-container"> <!-- 左侧边栏列表 --> <ul class="user-select-none menu"> <li class="menu-item"> <router-link to="/home/users">用户管理</router-link> </li> <li class="menu-item"> <router-link to="/home/rights">权限管理</router-link> </li> <li class="menu-item"> <router-link to="/home/goods">商品管理</router-link> </li> <li class="menu-item"> <router-link to="/home/orders">订单管理</router-link> </li> <li class="menu-item"> <router-link to="/home/settings">系统设置</router-link> </li> </ul> </div> </template>
2.打开 Home.vue 组件,在 右侧内容主体区域 中声明子路由的占位符:
<template> <div class="home-container"> <!-- 头部组件 --> <my-header></my-header> <!-- 主体区域 --> <div class="home-main-box"> <!-- 左侧边栏区域 --> <my-aside></my-aside> <!-- 右侧内容主体区域 --> <div class="home-main-body"> <!-- **子路由的占位符** --> <router-view></router-view> </div> </div> </div> </template>
3.在 router.js 中导入左侧菜单对应的组件:
import Users from './components/menus/MyUsers.vue' import Rights from './components/menus/MyRights.vue' import Goods from './components/menus/MyGoods.vue' import Orders from './components/menus/MyOrders.vue' import Settings from './components/menus/MySettings.vue'
4.通过 children 属性,为 home 规则定义子路由规则如下:
{ path: '/home', component: Home, // 用户访问 /home 时,重定向到 /home/users redirect: '/home/users', // 子路由规则 children: [ { path: 'users', component: Users }, { path: 'rights', component: Rights }, { path: 'goods', component: Goods }, { path: 'orders', component: Orders }, { path: 'settings', component: Settings }, ], },
3.8 渲染用户管理页面的数据
1.在 MyUsers.vue 组件中,通过 v-for 指令循环渲染用户列表的数据:
3.9 实现跳转到用户详细页的功能
1.在 MyUsers.vue 组件中,渲染详情页的路由链接如下:
<td> <router-link :to="'/home/users/' + item.id">详情</router-link> </td>
2.在 router.js 中导入用户详情页组件:
import UserDetail from './components/user/MyUserDetail.vue'
3.在 home 规则的 children 节点下,声明 用户详情页 的路由规则:
{ path: '/home', component: Home, redirect: '/home/users', children: [ { path: 'users', component: Users }, { path: 'rights', component: Rights }, { path: 'goods', component: Goods }, { path: 'orders', component: Orders }, { path: 'settings', component: Settings }, // 用户详情页的路由规则 { path: 'users/:id', component: UserDetail }, ], },
3.10 开启路由的 props 传参
1.在 router.js 模块中,为 用户详情页 的路由规则开启 props 传参:
{ path: 'users/:id', component: UserDetail, props: true },
2.在 MyUserDetail.vue 组件中声明 props 参数:
export default { name: 'MyUserDetail', props: ['id'], }
3.在 MyUserDetail.vue 组件的结构中直接使用路由参数:
<template> <button type="button" class="btn btn-light btn-sm">后退</button> <h4 class="text-center">用户详情 --- {{id}}</h4> </template>
3.11 通过编程式导航实现后退功能
1.在 MyUserDetail.vue 组件中,为后退按钮绑定点击事件处理函数:
<template> <button type="button" class="btn btn-light btn-sm" @click="goBack">后退</button> <h4 class="text-center">用户详情 --- {{id}}</h4> </template>
2.在 methods 中声明 goBack 事件处理函数如下:
export default { name: 'MyUserDetail', props: ['id'], methods: { // 编程式导航实现后退功能 goBack() { this.$router.go(-1) }, }, }
三十五、路由总结
① 能够知道如何在 vue 中配置路由
- createRouter、app.use(router)
② 能够知道如何使用嵌套路由
- 通过 children 属性进行路由嵌套、子路由的 hash 地址不要以 / 开头
③ 能够知道如何实现动态路由匹配
- 使用冒号声明参数项、this.$route.params、props: true
④ 能够知道如何使用编程式导航
- this. r o u t e r . p u s h 、 t h i s . router.push、this. router.push、this.router.go(-1)
⑤ 能够知道如何使用全局导航守卫
- 路由实例.beforeEach((to, from, next) => { })