你确认要退出本系统么?
-
{{ index + 1 }}.
Date: May 20, 2023
Sum: 组件样式冲突、data函数、组件通信、props、组件生命周期、vue3.x中全局配置axios
能够掌握 watch 侦听器的基本使用
能够知道 vue 中常用的生命周期函数
能够知道如何实现组件之间的数据共享
能够知道如何在 vue3.x 的项目中全局配置 axios
前言:以下使用较老的axios,否则会报错
npm i [email protected] -S
默认情况下,写在 .vue 组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题
导致组件之间样式冲突的根本原因是:
① 单页面应用程序中,所有组件的 DOM 结构,都是基于唯一的 index.html 页面进行呈现的
② 每个组件中的样式,都会影响整个 index.html 页面中的 DOM 元素
思考:如何解决组件样式冲突的问题
为每个组件分配唯一的自定义属性,在编写组件样式时,通过属性选择器来控制样式的作用域,示例代码如下:
为了提高开发效率和开发体验,vue 为 style 节点提供了 scoped 属性,从而防止组件之间的样式冲突问题:
注意:父组件与子组件都要的style都要加上scoped
默认情况:写在组件中的样式会 全局生效 一因此很容易造成多个组件之问的样式冲突问题
原理:
最终效果: 必须是当前组件的元素, 才会有这个自定义属性, 才会被这个样式作用到
如果给当前组件的 style 节点添加了 scoped 属性,则当前组件的样式对其子组件是不生效的。如果想让某些样式对子组件生效,可以使用 /deep/ 深度选择器。
注意:/deep/ 是 vue2.x 中实现样式穿透的方案。在 vue3.x 中推荐使用 :deep() 替代 /deep/。
Vue3中的样式结构::deep(标签)
原因:
目的:保证每个组件实例,维护独立的一份数据对象。
每次创建新的组件实例,都会新执行一次data 函数,得到一个新对象。
举例:
data()函数能够保证每个组件的数据是独立的
基础概念:
组件通信:指组件与组件之间的数据传递
组件之间的关系:
在项目开发中,组件之间的关系分为如下 3 种:
① 父子关系 ② 兄弟关系 ③ 后代关系
AB是父子关系,BC有一个共同的父级节点,故二者为兄弟关系。B和EFI都为特殊的兄弟关系。
A和DGH属于后代关系
父子组件之间的数据共享又分为:
① 父 -> 子共享数据 ② 子 -> 父共享数据 ③ 父 <-> 子双向数据同步
通信过程:
1-父组件通过 props 将数据传递给子组件
2-子组件利用 $emit 通知父组件修改更新
父组件通过 v-bind 属性绑定向子组件共享数据。同时,子组件需要使用 props 接收数据。
案例:
App.vue
MyAPP -- {{ count }}
MySon.vue
MySon -- {{ num }}
效果:
子组件通过自定义事件的方式向父组件共享数据。
具体步骤:
子组件:
父组件:
案例:
App.vue
MyAPP -- {{ count }}
Son.vue
MySon -- {{ num }}
效果:
父组件在使用子组件期间,可以使用 v-model 指令维护组件内外数据的双向同步:
具体步骤:
好处:父组件中不用再监听自定义事件,也不用再额外定义事件处理函数
案例:
Code:
App.vue
MyAPP -- {{ count }}
Son.vue
MySon -- {{ num }}
效果:
2023Vue教程的做法
**作用:**非父子组件之间,进行简易消息传递。(复杂场景→ Vuex)
步骤:
1-创建一个都能访问的事件总线 (空Vue实例)
注:把这个放在utils下的EventBus.js中
import Vue from 'vue'
const Bus = new Vue()
export default Bus
2-A组件(接受方),监听Bus的 $on事件
// 先导入Bus
import Bus from '../utils/EventBus'
export default {
data() {
return {
msg: '',
}
},
// 再从 created 阶段就监听 $on 事件
created() {
Bus.$on('sendMsg', (msg) => {
// console.log(msg)
this.msg = msg
})
},
}
3-B组件(发送方),触发Bus的$emit事件
注:这个在组件内
import Bus from '../utils/EventBus'
export default {
methods: {
sendMsgFn() {
Bus.$emit('sendMsg', '今天天气不错,适合旅游')
},
},
}
图示:
注意:这是个一对多的发送
案例:传递A组件数据给B组件
Code:
BaseA.vue
我是A组件(接受方)
{{msg}}
BaseB.vue
我是B组件(发布方)
2021Vue教程的做法:
兄弟组件之间实现数据共享的方案是 EventBus。
可以借助于第三方的包 mitt 来创建 eventBus 对象,从而实现兄弟组件之间的数据共享。
示意图如下:
理解:在数据接收方调用on方法来声明自定义事件,在数据发送方通过emit方法来触发emit事件
3.1 安装 mitt 依赖包
在项目中运行如下的命令,安装 mitt 依赖包:
npm install [email protected]
3.2 创建公共的 EventBus 模块
在项目中创建公共的 eventBus 模块如下:
// eventBus.js
// 导入 mitt 包
import mitt from 'mitt'
// 创建 EventBus 的实例对象
const bus = mitt()
// 将 EventBus 的实例对象共享出去
export default bus
3.3 在数据接收方自定义事件
在数据接收方,调用 bus.on(‘事件名称’, 事件处理函数) 方法注册一个自定义事件。
示例代码如下:
// 导入 eventBus.js 模块, 得到共享的bus对象
export default {
data() {return { count: 0}},
created() {
// 在created生命周期函数中声明自定义事件
// 调用 bus.on 方法注册一个自定义事件,通过事件处理函数的形参数接收数据
bus.on('countChange', (count) => {
this.count = count
})
}
}
3.4 在数据接发送方触发事件
在数据发送方,调用 bus.emit(‘事件名称’, 要发送的数据) 方法触发自定义事件。示例代码如下:
// 导入 eventBus.js 模块,得到共享的 bus 对象
import bus from './eventBus.js'
export default {
data() {return { count: 0}},
methods: {
addCount() {
this.count++
bus.emit('countChange', this.count) // 调用 bus.emit() 方法触发自定义事件,并发送数据
}
}
}
案例:
Code:
Left.vue
Left--数据发送方--num的值为: {{ count }}
Right.vue
Right--数据接收方--num的值为:{{ num }}
效果:
作用:跨层级共享数据
场景:
后代关系组件之间共享数据,指的是父节点的组件向其子孙组件共享数据。此时组件之间的嵌套关系比较复杂,可以使用 provide 和 inject 实现后代关系组件之间的数据共享。
语法:
1-父组件 provide提供数据
export default {
provide () {
return {
// 普通类型【非响应式】
color: this.color,
// 复杂类型【响应式】
userInfo: this.userInfo,
}
}
}
2-子/孙组件 inject 获取数据
export default {
inject: ['color','userInfo'],
created () {
console.log(this.color, this.userInfo)
}
}
图示:
注意:
1-provide提供的简单类型的数据不是响应式的,复杂类型数据是响应式。(推荐提供复杂类型数据)如上图所示,如果我用button修改color,那么图中元素不会有变动,而用button修改userInfo中的数据,则图中相应元素会有变动。
2-子/孙组件通过inject获取的数据,不能在自身组件内修改
补充:2021版的Vue课程
父节点对外共享响应式的数据
值得注意的是,provide中return回去的数据,并非是响应式的数据,即若我在父组件中用button修改p标签的颜色,子组件的中的p标签颜色不会跟着一块变。
父节点使用 provide 向下共享数据时,可以结合 computed 函数向下共享响应式的数据。示例代码如下:
子孙节点使用响应式的数据
如果父级节点共享的是响应式的数据,则子孙节点必须以 .value 的形式进行使用。示例代码如下:
vuex 是终极的组件之间的数据共享方案。在企业级的 vue 项目开发中,vuex 可以让组件之间的数据共享变得高效、清晰、且易于维护。
父子关系
① 父 -> 子 属性绑定
② 子 -> 父 事件绑定
③ 父 <-> 子 组件上的 v-model
兄弟关系
④ EventBus
后代关系
⑤ provide & inject
全局数据共享
⑥ vuex
为了提高组件的复用性,在封装 vue 组件时需要遵守如下的原则:
组件的 DOM 结构、Style 样式 要尽量复用
组件中要展示的数据,尽量由组件的使用者提供
为了方便使用者为组件提供要展示的数据,vue 组件提供了 props 的概念。
概念:组件上 注册的一些 自定义属性
作用:父组件通过 props 向子组件传递要展示的数据
特点:可以传递 任意数量与类型 的prop ;提高了组件的复用性
语法:简易写法
子组件接收
props: ['数据1', '数据2']
举例:
传递父组件中的数据到子组件中
使用 v-bind 属性绑定的形式,为组件动态绑定 props 的值
注意: :username=”username”
左边是子,右边是父
效果:
Code:
App.vue
UserInfo.vue
我是个人信息组件
姓名:{{ username }}
年龄:{{ age }}
是否单身:{{ isSingle }}
座驾:{{ car.brand }}
兴趣爱好 {{ hobby.join('、') }}
作用:为组件的 prop 指定验证要求,不符合要求,控制台就会有错误提示 → 帮助开发者,快速发现错误
语法:
props: {
校验的属性名:类型 // Number String Boolean ...
}
举例:进度条的进度只能传入数字而不能是其他的数据类型
BaseProgress.vue 子组件接收父组件的数据
export default {
// 1.基础写法(类型校验)
props: {
w: Number,
},
}
类型校验是最常用的,如果你需要后面几种校验,就需要补充以下的写法:
语法:
props: {
校验的属性名: {
type: 类型, // Number String Boolean ...
required: true, // 是否必填
default: 默认值, // 默认值
validator (value) {
// 自定义校验逻辑
return 是否通过校验
}
}
},
代码示例:
注意:
1.default和required一般不同时写(因为当时必填项时,肯定是有值的)
2.default后面如果是简单类型的值,可以直接写默认。如果是复杂类型的值,则需要以函数的形式return一个默认值
**共同点:**都可以给组件提供数据
区别:
单向数据流:
父级props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的,即父的数据更新流向子
子若想影响父的数据,需要通过$.emit来影响父。然后,父在将数据单向流动给子。
口诀:谁的数据谁负责
案例:子接收父Count值,并且子通过 this.$emit 传递方法给父,让其修改数据
子想要改变父数据,需要通过 this.$emit 进行传递数据
父接收子changeCount方法,并利用 handleChange 接收数据,从而修改自身Count值
注意:@changeCount=”handleChange”
左边是子,右边是父
Code:
App.vue
BaseCount.vue
{{ count }}
组件中如果使用“camelCase (驼峰命名法)”声明了 props 属性的名称,
则有两种方式为其绑定属性的值:
理解:
封装的时候采用驼峰命名法,那么外界在传递属性的时候既可以通过短横线命名,也可以通过驼峰命名法命名
注意:
如果我们在组件命名属性时采用驼峰命名法,
那么,在传递属性时,我们既可以使用驼峰命名法,也可以使用短横线命名法
案例效果:
需求说明:
拆分基础组件:
咱们可以把小黑记事本原有的结构拆成三部分内容:头部(TodoHeader)、列表(TodoMain)、底部(TodoFooter)
思路:
具体操作:
1-拆分并渲染
/**
* 渲染功能:
* 1. 子组件提供数据给父组件
* 2. 父传数据给子
* 3. 利用 v-for 渲染数据
*/
Code:
App.vue
TodoMain.vue
-
{{ index + 1 }}.
2-添加功能(添加、删除、统计、清空、持久化存储)
/**
* 添加功能:
* 1. 收集表单数据 v-model
* 2. 监听事件(回车+点击 都要进行添加)
* 3. 子传父,将任务名称传递给父组件
* 4. 父组件进行添加 unshift(自己的数据自己负责)
*/
/**
* 删除功能:
* 1. 监听时间(监听删除的点击)携带id
* 2. 子传父,将删除的id传递给父组件App.vue
* 3. 进行删除 filter (自己的数据自己负责)
*/
// 底部合计:父组件传递list到底部组件 —>展示合计
// 清空功能:监听事件 —> **子组件**通知父组件 —>父组件清空
// 持久化存储: watch监听数据变化,持久化到本地
Code:
App.vue
TodoHeader.vue
小黑记事本
TodoMain.vue
-
{{ index + 1 }}.
TodoFooter.vue
**原理:**v-model本质上是一个语法糖。例如应用在输入框上,就是value属性 和 input事件 的合写
结合这段代码理解:
注意:$event 用于在模板中,获取事件的形参
作用:v-model提供数据的双向绑定
代码实例:两个input是同步进退的
v-model使用在其他表单元素上的原理
不同的表单元素, v-model在底层的处理机制是不一样的。比如给checkbox使用v-model底层处理的是 checked属性和change事件。
不过咱们只需要掌握应用在文本框上的原理即可
目标:实现子组件和父组件数据的双向绑定
案例:实现App.vue中的selectId和子组件选中的数据进行双向绑定
App.vue
BaseSelect.vue
**目标:**父组件通过v-model 简化代码,实现子组件和父组件数据 双向绑定
简化:
v-model其实就是 :value和@input事件的简写
案例:
子组件
props: {
value: String
},
methods: {
handleChange (e) {
// 将这里的handleChange改成input
this.$emit('input', e.target.value)
}
}
父组件
总结:
可以结合上面的 父子组件之间数据 的双向同步
作用:可以实现 子组件 与 父组件数据 的 双向绑定,简化代码
简单理解:子组件可以修改父组件传过来的props值
特点:prop属性名,可以自定义,非固定为 value (这与v-model不同)
场景: 封装弹框类的基础组件, visible属性 true显示 false隐藏
理解:如果封装的不是value,而是这种弹框类的组件,建议用.sync建立双向绑定
本质: .sync修饰符 就是 :属性名 和 @update:属性名 合写
语法
父组件
//.sync写法
--------------------------------------
//完整写法
子组件
props: {
visible: Boolean
},
this.$emit('update:visible', false)
案例:
Code:
App.vue
BaseDialog.vue
温馨提示:
你确认要退出本系统么?
作用: 利用ref 和 $refs 可以用于 获取 dom 元素 或 组件实例
理解:每个 vue 的组件实例上,都包含一个 $refs 对象,里面存储着对应的 DOM 元素或组件的引用。默认情况下,组件的 $refs 指向一个空对象。
**特点:**查找范围 → 当前组件内(更精确稳定)
技术诞生原因:
如果使用querySelector进行查找图标.box, 可能在整个页面找到多个.box。
因此,为了更加精准地获取DOM元素,就需要ref与$refs这种技术
语法
1.给要获取的盒子添加ref属性
我是渲染图表的容器
2.获取时通过 r e f s 获取 t h i s . refs获取 this. refs获取this.refs.chartRef 获取
mounted () {
console.log(this.$refs.chartRef)
}
注意
之前只用document.querySelect(‘.box’) 获取的是整个页面中的盒子
案例-1:获取DOM元素
Code:
App.vue
这是一个捣乱的盒子
BaseChart.vue
子组件
案例-2:获取组件实例
Code:
App.vue
父组件 --
BaseForm.vue
账号:
密码:
需求:
编辑标题, 编辑框自动聚焦
**代码实现:
{{ title }}
问题:
“显示之后”,立刻获取焦点是不能成功的!
原因:Vue 是异步更新DOM (提升性能)
解决方案
$nextTick:等 DOM更新后,才会触发执行此方法里的函数体
语法: this.$nextTick(函数体)
this.$nextTick(() => {
this.$refs.inp.focus()
})
注意:$nextTick 内的函数体 一定是箭头函数,这样才能让函数内部的this指向Vue实例
补充:用setTimeout也能实现,但是它的时间没有$nextTick精准
解决代码:
Code:
App.vue
{{ title }}
BaseForm.vue
账号:
密码:
总结:
组件运行的过程:
组件的生命周期指的是:组件从创建 -> 运行(渲染) -> 销毁的整个过程,强调的是一个时间段
监听组件的不同时刻的方式
vue 框架为组件内置了不同时刻的生命周期函数,生命周期函数会伴随着组件的运行而自动调用。
例如:
① 当组件在内存中被创建完毕之后,会自动调用 created 函数
② 当组件被成功的渲染到页面上之后,会自动调用 mounted 函数
③ 当组件被销毁完毕之后,会自动调用 unmounted 函数
案例:
Code:
App.vue
App 根组件
LifeCycle.vue
LifeCycle
理解:代码中的created mounted() unmounted函数放到子组件LifeCycle中,当子组件创建完毕之后,会调用created函数,当组件被渲染到页面上后,会调用mounted函数,当组件被销毁完毕之后,会调用unmounted函数。
监听组件的更新的方式
当组件的 data 数据更新之后,vue 会自动重新渲染组件的 DOM 结构,从而保证 View 视图展示的数据和Model 数据源保持一致。
当组件被重新渲染完毕之后,会自动调用 updated 生命周期函数。
案例:
Code:
LifeCycle
{{ count }}
export default {
name: 'LifeCycle',
data() {
return {
count: 0,
}
},
updated() {
console.log('组件被重新渲染完毕了');
},
}
组件中主要的生命周期函数
注意:在实际开发中,created 是最常用的生命周期函数!
组件中全部的生命周期函数
疑问:为什么不在 beforeCreate 中发 ajax 请求初始数据
发起Ajax请求最好都在creat中
完整的生命周期图示
可以参考 vue 官方文档给出的“生命周期图示”,进一步理解组件生命周期执行的过程:
https://www.vue3js.cn/docs/zh/guide/instance.html#生命周期图示
在实际项目开发中,几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题:
① 每个组件中都需要导入 axios(代码臃肿)
② 每次发请求都需要填写完整的请求路径(不利于后期的维护)
2. 如何全局配置 axios
在 main.js 入口文件中,通过 app.config.globalProperties 全局挂载 axios,示例代码如下:
Code:
main.js
const app = createApp(MyApp)
axios.defaults.baseURL = 'https://www.escook.cn' //为axios配置请求的根路径
//将axios挂载为app的全局自定义属性之后,
//每个组件可以通过this直接访问到全局挂载的自定义属性
app.config.globalProperties.$http = axios
GetInfo:
export default {
name: 'GetInfo',
methods: {
async getInfo() {
const { data: res } = await this.$http.get('/api/get', {
params: {
name: 'ls',
age: 33,
},
})
console.log(res)
},
},
}
PostInfo:
export default {
name: 'PostInfo',
methods: {
async postInfo() {
const { data: res } = await this.$http.post('/api/post', { name: 'zs', age: 20 })
console.log(res)
},
},
}