1 Vue组件通信
1.1 组件间通信基本原则
- 不要在子组件中直接修改父组件的状态数据
- 数据在哪, 更新数据的行为(函数)就应该定义在哪
1.2 vue 组件间通信方式
- props
- vue 的自定义事件
- 消息订阅与发布(如: pubsub 库)
- slot
- vuex
1.3 props
props
:让组件接收外部传过来的数据,此方式用于父组件向子组件传递数据
props传递数据原则
:单向数据流,只能父传子
注意
:
- 如果需要向非子后代传递数据必须多层逐层传递
- 兄弟组件间也不能直接props 通信, 必须借助父组件才可以
- 所有标签属性都会成为组件对象的属性, 模板页面可以直接引用
1.父组件通过传统方式或v-bind动态绑定向子组件传送数据
<!-- App父组件 -->
<template>
<div>
<!-- 传送数据一定要写在父组件的子组件标签上(通过标签属性)-->
<!-- 1.传统方式传送数据 -->
//<Student name='张三'/>
<!-- 2.动态绑定传送数据(不限于形式,可能是函数) Student.name会作为表达式自动执行 -->
<Student :name='Student.name'/>
</div>
</template>
<script>
//引入子组件
import Student from "./components/Student.vue";
export default {
name: "App",
components: { Student },
data() {
return {
Student:{
name:'张三'
}
};
},
};
</script>
2.子组件内部通过props接收父组件传递的数据
<!-- Student子组件 -->
//第一种方式(只接收)最常用
props:['name']
//第二种方式(限制类型)
props:{name:String}
//第三种方式(限制类型、限制必要性、指定默认值)
props:{
name:{
type:String, //类型
required:true, //必要性
default:'张三' //默认值
}
}
备注
:props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。
App.vue 父组件
<template>
<div>
<!-- 传递数据 -->
<!-- :是v-bind动态绑定 18会作为表达式自动执行 -->
<Student name='李四' sex='女' :age='18'></Student>
</div>
</template>
<script>
//引入子组件
import Student from "./components/Student.vue";
export default {
name: "App",
components: { Student },
};
</script>
<style>
</style>
Student.vue 子组件
<template>
<div>
<h2>学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
<h2>学生年龄:{{ myAge + 1 }}</h2>
<button @click="updateAge">尝试修改收到的年龄</button>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
//若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据
myAge: this.age,
};
},
methods: {
updateAge() {
this.myAge++;
},
},
//接收数据 简单声明接收
props:['name','age','sex']
};
</script>
1.4 自定义事件
自定义事件
:用于子组件向父组件传递数据
使用场景
:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中)。
隔代组件或兄弟组件间通信此种方式不合适
1.绑定自定义事件
方式一
: v-on
<!-- App.vue父组件 -->
// 在父组件中给子组件 xxx为自定义事件 getStudentName为回调函数(在父组件中)
<Student @xxx="getStudentName" />
...
methods: {
//回调函数
getStudentName(name) {
this.studentName = name;
},
},
方式二:
ref
//通过ref给Student组件打标识
<Student ref="student" />
...
methods: {
//回调函数
getStudentName(name) {
this.studentName = name;
},
},
mounted() {
//通过$refs获取Student组件
//在获取到的Student组件上绑定自定义事件xxx getStudentName为回调函数
this.$refs.student.$on("xxx", this.getStudentName); //$on当...时
},
2.触发自定义事件
方法
:this.$emit(xxx, data)
<!-- Student.vue子组件 -->
<button @click="sendStudentName">把学生名给App</button>
...
methods: {
sendStudentName() {
//触发Student组件实例身上的xxx自定义事事件
this.$emit("xxx", this.name);
}
},
注意
:
- 解绑自定义事件:this.$off(‘xxx’)
- 当需要自定义事件只能触发一次:可以使用once修饰符,或$once方法。
- 组件上也可以绑定原生DOM事件,需要使用native修饰符。
<Student @click.native=“show” /> - 隔代组件或兄弟组件间通信不能使用自定义事件
案例
App父组件
<template>
<div class="app">
<h1>{{ msg }},学生姓名是:{{ studentName }}</h1>
<!-- 第一种:使用@或v-on -->
<!-- <Student @atguigu="getStudentName" /> -->
<!-- 第二种:使用ref -->
<Student ref="student" @click.native="show" />
</div>
</template>
<script>
import Student from "./components/Student";
export default {
name: "App",
components: { Student },
data() {
return {
msg: "你好啊!",
studentName: "",
};
},
methods: {
getStudentName(name) {
this.studentName = name;
},
show() {
alert(123);
},
},
mounted() {
this.$refs.student.$on("atguigu", this.getStudentName); //绑定自定义事件
},
};
</script>
<style scoped>
.app {
background-color: gray;
padding: 5px;
}
</style>
Student.vue子组件
<template>
<div class="student">
<h2>学生姓名:{{ name }}</h2>
<h2>当前求和为:{{ number }}</h2>
<button @click="add">点我number++</button>
<button @click="sendStudentName">把学生名给App</button>
<button @click="unbind">解绑atguigu事件</button>
<button @click="death">销毁当前Student组件的实例(vc)</button>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
name: "张三",
number: 0,
};
},
methods: {
add() {
this.number++;
},
sendStudentName() {
//触发Student组件实例身上的atguigu事件
this.$emit("atguigu", this.name);
},
unbind() {
this.$off("atguigu"); //解绑一个自定义事件
// this.$off(['atguigu','demo']) //解绑多个自定义事件
// this.$off() //解绑所有的自定义事件
},
death() {
this.$destroy(); //销毁了当前Student组件的实例,销毁后所有Student实例的自定义事件全都不奏效。
},
},
};
</script>
<style lang="less" scoped>
.student {
background-color: pink;
padding: 5px;
margin-top: 30px;
}
</style>
1.5 全局事件总线(GlobalEventBus)
全局事件总线
:适用于任意组件间通信。
1.安装全局事件总线
//main.js
new Vue({
el: '#app',
render: h => h(App),
//安装全局事件总线 beforeCreate创建实例前
beforeCreate() {
//组件实例对象vc可以访问到Vue原型上的属性和方法,往Vue原型上添加$bus属性 $bus为傀儡
//那么子组件可以使用$bus,而$bus值为vm(this) 因为vm可以调用$on $emit这些方法
Vue.prototype.$bus = this;
},
});
2.使用事件总线
接收数据
:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身。
methods(){
demo(data){......}
}
......
//mounted():初始化操作,绑定自定义事件
//xxx为自定义事件 this.demo为回调函数
mounted() {
this.$bus.$on('xxx',this.demo)
}
//使用完之后 beforeDestroy解绑自定义事件
beforeDestroy() {
this.$bus.$off("xxx");
},
发送数据
:
this.$bus.$emit('xxx',数据)
案例:兄弟组件传值(Student => School)
main.js
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false
//创建vm
new Vue({
el:'#app',
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus = this //安装全局事件总线
},
})
School.vue
<template>
<div class="school">
<h2>学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
</div>
</template>
<script>
export default {
name: "School",
data() {
return {
name: "尚硅谷",
address: "北京",
};
},
methods: {
demo(data) {
console.log("我是School组件,收到了数据", data);
},
},
mounted() {
//在School组件给傀儡绑定自定义事件hello,借助傀儡身上的$on方法获取数据
this.$bus.$on("hello", this.demo);
},
beforeDestroy() {
//解绑当前组件用到的事件
this.$bus.$off("hello");
},
};
</script>
<style scoped>
.school {
background-color: skyblue;
padding: 5px;
}
</style>
Student.vue
template>
<div class="student">
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<button @click="sendStudentName">把学生名给School组件</button>
</div>
</template>
<script>
export default {
name:'Student',
data() {
return {
name:'张三',
sex:'男',
}
},
methods: {
sendStudentName(){
this.$bus.$emit('hello',this.name)
}
},
}
</script>
<style lang="less" scoped>
.student{
background-color: pink;
padding: 5px;
margin-top: 30px;
}
</style>
1.6 消息订阅与发布(pubsub-js 库)
消息订阅与发布
:适用于任意组件间通信。
安装pubsub-js 库
:
npm i pubsub-js
引入
:
import pubsub from 'pubsub-js'
接收数据
:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身。
1.接收数据
methods: {
demo(msgName, data) {
...
},
},
mounted() {
//订阅消息 每一次订阅都会产生一个id
//msgName代表数据名xxx data代表接收的数据
this.pubId = pubsub.subscribe("xxx", this.demo);
},
beforeDestroy() {
//通过id取消订阅
pubsub.unsubscribe(this.pubId);
},
2. 发送数据
methods: {
sendStudentName() {
//发布消息
pubsub.publish("hello", this.name);
},
},
案例:兄弟组件传值(Student => School)
School.Vue
<template>
<div class="school">
<h2>学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
</div>
</template>
<script>
import pubsub from "pubsub-js";
export default {
name: "School",
data() {
return {
name: "尚硅谷",
address: "北京",
};
},
methods: {
demo(msgName, data) {
console.log("我是School组件,收到了数据", msgName, data);
},
},
mounted() {
//订阅消息 每一次订阅都会产生一个id
//msgName代表数据名hello data代表接收的数据
this.pubId = pubsub.subscribe("hello", this.demo);
},
beforeDestroy() {
//通过id取消订阅消息
pubsub.unsubscribe(this.pubId);
},
};
</script>
<style scoped>
.school {
background-color: skyblue;
padding: 5px;
}
</style>
Student.Vue
<template>
<div class="student">
<h2>学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
<button @click="sendStudentName">把学生名给School组件</button>
</div>
</template>
<script>
import pubsub from "pubsub-js";
export default {
name: "Student",
data() {
return {
name: "张三",
sex: "男",
};
},
methods: {
sendStudentName() {
//发布消息
pubsub.publish("hello", this.name);
},
},
};
</script>
<style lang="less" scoped>
.student {
background-color: pink;
padding: 5px;
margin-top: 30px;
}
</style>
2 组件插槽
组件插槽
是Vue的内置组件,为了让我们封装的组件更加具有扩展性,让使用者可以决定组件内部的一些内容到底展示什么
作用
:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于 父组件 ===> 子组件
分类
:默认插槽、具名插槽、作用域插槽
插槽的使用
:子组件定义插槽的位置,父组件定义插槽的内容, 控制内容的显示与隐藏
如何封装
:抽取共性,保留不同。最好的封装方式就是将共性抽取到组件中,将不同预留为插槽。
默认(匿名)插槽:唯一存在 <slot></slot>
具名插槽:可存在多个 <slot name="up"></slot>
作用域插槽:父组件对子组件的内容进行加工处理
2.1 默认插槽
父组件
<Category>
<div>html结构1</div>
</Category>
子组件
<template>
<div>
<!-- 定义插槽 -->
<slot>插槽默认内容...</slot>
</div>
</template>
案例
App.vue
<template>
<div class="container">
<Category title="美食" >
<img src="https://s3.ax1x.com/2021/01/16/srJlq0.jpg" alt="">
</Category>
<Category title="游戏" >
<ul>
<li v-for="(g,index) in games" :key="index">{{g}}</li>
</ul>
</Category>
<Category title="电影">
<video controls src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"></video>
</Category>
</div>
</template>
<script>
import Category from './components/Category'
export default {
name:'App',
components:{Category},
data() {
return {
foods:['火锅','烧烤','小龙虾','牛排'],
games:['红色警戒','穿越火线','劲舞团','超级玛丽'],
films:['《教父》','《拆弹专家》','《你好,李焕英》','《尚硅谷》']
}
},
}
</script>
<style scoped>
.container{
display: flex;
justify-content: space-around;
}
</style>
Category.vue
<template>
<div class="category">
<h3>{{title}}分类</h3>
<!-- 定义一个插槽(挖个坑,等着组件的使用者进行填充) -->
<slot>我是一些默认值,当使用者没有传递具体结构时,我不会出现</slot>
</div>
</template>
<script>
export default {
name:'Category',
props:['title']
}
</script>
<style scoped>
.category{
background-color: skyblue;
width: 200px;
height: 300px;
}
h3{
text-align: center;
background-color: orange;
}
video{
width: 100%;
}
img{
width: 100%;
}
</style>
2.2 具名插槽
父组件
<Category>
<!-- slot ="xxx" 向插槽名为xxx的插槽里面放内容-->
<template slot="center">
<div>html结构1</div>
</template>
<template v-slot:footer>
<div>html结构2</div>
</template>
</Category>
子组件
<template>
<div>
<!-- 定义具名插槽 name="xxx" xxx为插槽名 -->
<slot name="center">插槽默认内容...</slot>
<slot name="footer">插槽默认内容...</slot>
</div>
</template>
案例
App.vue
<template>
<div class="container">
<Category title="美食" >
<img slot="center" src="https://s3.ax1x.com/2021/01/16/srJlq0.jpg" alt="">
<a slot="footer" href="http://www.atguigu.com">更多美食</a>
</Category>
<Category title="游戏" >
<ul slot="center">
<li v-for="(g,index) in games" :key="index">{{g}}</li>
</ul>
<div class="foot" slot="footer">
<a href="http://www.atguigu.com">单机游戏</a>
<a href="http://www.atguigu.com">网络游戏</a>
</div>
</Category>
<Category title="电影">
<video slot="center" controls src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"></video>
<template v-slot:footer>
<div class="foot">
<a href="http://www.atguigu.com">经典</a>
<a href="http://www.atguigu.com">热门</a>
<a href="http://www.atguigu.com">推荐</a>
</div>
<h4>欢迎前来观影</h4>
</template>
</Category>
</div>
</template>
<script>
import Category from './components/Category'
export default {
name:'App',
components:{Category},
data() {
return {
foods:['火锅','烧烤','小龙虾','牛排'],
games:['红色警戒','穿越火线','劲舞团','超级玛丽'],
films:['《教父》','《拆弹专家》','《你好,李焕英》','《尚硅谷》']
}
},
}
</script>
<style scoped>
.container,.foot{
display: flex;
justify-content: space-around;
}
h4{
text-align: center;
}
</style>
Category.vue
<template>
<div class="category">
<h3>{{title}}分类</h3>
<!-- 定义一个插槽(挖个坑,等着组件的使用者进行填充) -->
<slot name="center">我是一些默认值,当使用者没有传递具体结构时,我会出现1</slot>
<slot name="footer">我是一些默认值,当使用者没有传递具体结构时,我会出现2</slot>
</div>
</template>
<script>
export default {
name:'Category',
props:['title']
}
</script>
<style scoped>
.category{
background-color: skyblue;
width: 200px;
height: 300px;
}
h3{
text-align: center;
background-color: orange;
}
video{
width: 100%;
}
img{
width: 100%;
}
</style>
2.3 作用域插槽
理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(games数据在Category组件中,但使用数据所遍历出来的结构由App组件决定)
父组件
<Category>
<template scope="scopeData">
<!-- 生成的是ul列表 -->
<ul>
<!-- scopeData.games就是子组件中的数据 -->
<li v-for="g in scopeData.games" :key="g">{{g}}</li>
</ul>
</template>
</Category>
子组件
<template>
<div>
<slot :games="games"></slot>
</div>
</template>
<script>
export default {
name:'Category',
props:['title'],
//数据在子组件自身
data() {
return {
games:['红色警戒','穿越火线','劲舞团','超级玛丽']
}
},
}
</script>
案例
App.vue
<template>
<div class="container">
<Category title="游戏">
<template scope="atguigu">
<ul>
<li v-for="(g,index) in atguigu.games" :key="index">{{g}}</li>
</ul>
</template>
</Category>
<Category title="游戏">
<template scope="{games}">
<ol>
<li style="color:red" v-for="(g,index) in games" :key="index">{{g}}</li>
</ol>
</template>
</Category>
<Category title="游戏">
<template slot-scope="{games}">
<h4 v-for="(g,index) in games" :key="index">{{g}}</h4>
</template>
</Category>
</div>
</template>
<script>
import Category from './components/Category'
export default {
name:'App',
components:{Category},
}
</script>
<style scoped>
.container,.foot{
display: flex;
justify-content: space-around;
}
h4{
text-align: center;
}
</style>
Category.vue
<template>
<div class="category">
<h3>{{title}}分类</h3>
<slot :games="games" msg="hello">我是默认的一些内容</slot>
</div>
</template>
<script>
export default {
name:'Category',
props:['title'],
data() {
return {
games:['红色警戒','穿越火线','劲舞团','超级玛丽'],
}
},
}
</script>
<style scoped>
.category{
background-color: skyblue;
width: 200px;
height: 300px;
}
h3{
text-align: center;
background-color: orange;
}
video{
width: 100%;
}
img{
width: 100%;
}
</style>
3 Vue封装的过渡与动画
1.过渡与动画作用
在插入、更新或移除DOM元素时,在合适的时候给元素添加样式类名
2.图示
3.写法
准备好样式
:
v-enter(进入的起点) v-enter-active(进入的过程) v-enter-to(进入的终点)
v-enter(离开的起点) v-enter-active(离开的过程) v-enter-to(离开的终点)
使用<transtion>包裹要过渡的元素,并配置name属性
:
<transition name="hello" >
<h1 v-show="isShow">你好啊!</h1>
</transition>
备注
:若有多个元素需要过渡,则需要使用,且每个元素都要指定key值
App.vue
<template>
<div>
<Test />
<Test2 />
<Test3 />
</div>
</template>
<script>
import Test from "./components/Test";
import Test2 from "./components/Test2";
import Test3 from "./components/Test3";
export default {
name: "App",
components: { Test, Test2, Test3 },
};
</script>
<style>
</style>
使用动画代码实现
Test.vue
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<transition name="hello" appear>
<h1 v-show="isShow">你好啊!</h1>
</transition>
</div>
</template>
<script>
export default {
name:'Test',
data() {
return {
isShow:true
}
},
}
</script>
<style scoped>
h1{
background-color: orange;
}
.hello-enter-active{
animation: atguigu 0.5s linear;
}
.hello-leave-active{
animation: atguigu 0.5s linear reverse;
}
@keyframes atguigu {
from{
transform: translateX(-100%);
}
to{
transform: translateX(0px);
}
}
</style>
使用过渡代码实现
Test2.vue
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<transition-group name="hello" appear>
<h1 v-show="isShow" key="1">你好啊!</h1>
<h1 v-show="isShow" key="2">尚硅谷!</h1>
</transition-group>
</div>
</template>
<script>
export default {
name: "Test",
data() {
return {
isShow: true,
};
},
};
</script>
<style scoped>
h1 {
background-color: orange;
}
/* 进入的起点 离开的终点 */
.hello-enter,
.hello-leave-to {
transform: translateX(-100%);
}
/* 进入的整个过程 离开的整个过程 */
.hello-enter-active,
.hello-leave-active {
transition: 0.5s linear;
}
/* 进入的终点 离开的起点 */
.hello-enter-to,
.hello-leave {
transform: translateX(0);
}
</style>
使用第三方动画库实现(https://animate.style/)
Test3.vue
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<transition-group
appear
name="animate__animated animate__bounce"
enter-active-class="animate__swing"
leave-active-class="animate__backOutUp"
>
<h1 v-show="!isShow" key="1">你好啊!</h1>
<h1 v-show="isShow" key="2">尚硅谷!</h1>
</transition-group>
</div>
</template>
<script>
import "animate.css";
export default {
name: "Test",
data() {
return {
isShow: true,
};
},
};
</script>
<style scoped>
h1 {
background-color: orange;
}
</style>
4 使用vue-cli解决Ajax跨域问题
使用vue-cli开启代理服务器ajax跨域问题
方法一
:
在vue.config.js中添加如下配置:
devServer:{
proxy:"http://localhost:5000"
}
优点:配置简单,请求资源时直接发给前端(8080)即可。
缺点:不能配置多个代理,不能灵活的控制请求是否走代理。
工作方式:若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器 (优先匹配前端资源)
方法二
:
编写vue.config.js配置具体代理规则:
module.exports = {
devServer: {
proxy: {
'/api1': {
// 匹配所有以 '/api1'开头的请求路径
target: 'http://localhost:5000', // 将请求代理到目标服务器上
changeOrigin: true,
//重写路径 将请求中的/api1 重写为为空字符串
pathRewrite: { '^/api1': '' },
ws: true, //用于支持websocket
changeOrigin: true, //用于控制请求头中的host值
},
'/api2': {
// 匹配所有以 '/api2'开头的请求路径
target: 'http://localhost:5001', // 代理目标的基础路径
changeOrigin: true,
pathRewrite: { '^/api2': '' },
ws: true, //用于支持websocket
changeOrigin: true, //用于控制请求头中的host值
},
},
},
}
/*
changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:8080
changeOrigin默认值为true
*/
优点:可以配置多个代理,且可以灵活的控制请求是否走代理。
缺点:配置略微繁琐,请求资源时必须加前缀。
开启两个资源服务器
app.vue
<template>
<div id="app">
<!-- 在8080端口上获取5000和5001端口上的数据 -->
<button @click="getStudents">获取学生信息</button>
<button @click="getCars">获取汽车信息</button>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "App",
methods: {
getStudents() {
axios.get("http://localhost:8080/api1/students").then(
(response) => {
console.log("请求成功了", response.data);
},
(error) => {
console.log("请求失败了", error.message);
}
);
},
getCars() {
axios.get("http://localhost:8080/api2/cars").then(
(response) => {
console.log("请求成功了", response.data);
},
(error) => {
console.log("请求失败了", error.message);
}
);
},
},
};
</script>