Vue脚手架-TodoList案例(Vue组件间消息通信)
最近在学习尚硅谷的Vue教程,通过TodoList案例的演示贯穿学习了组件化的思路,以及学习Vue 组件间消息通信的方式。
Vue脚手架初始化
第一步:使用npm安装
npm install -g @vue/cli
第二步:切换到你要创建项目的目录,然后使用命令创建项目
vue create xxxx
第三步:启动项目
npm run serve
配置淘宝镜像:
npm config set registry https://registry.npm.taobao.org
在项目文件中有vue.config.js可以设置相关配置(例如关闭语法检查):
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave:false //关闭语法检查
})
没有显示该文件自行新建
组件化编程思路
如图:
Vue实例对象管理App,App组件管理所有的子组件,并且app组件为了所有组件都能使用数据,所以数据模型(todos-model)放在了app中,其相应的方法也在app中创建。
<script>
import TodoHeader from "./components/TodoHeader";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
export default {
name: "App",
data() {
return {
todos: [
{
id: "001",
title: "DayDayCoding",
done: false,
}
],
};
},
methods: {
/**
* 添加一个todo
*/
addTodo(todo){
this.todos.unshift(todo)
},
/**
* 勾选修改Todo.done
*/
handleCheck(id) {
this.todos.forEach((todo) => {
if(todo.id === id){
todo.done = !todo.done
}
})
},
/**
* 删除一个todo
*/
deleteTodo(id){
this.todos = this.todos.filter((el)=>{
return el.id !== id
})
},
/**
* 全选/不选
*/
checkAll(isAll){
this.todos.forEach((todo)=>{
todo.done = isAll
})
},
/**
* 删除全部已完成的任务
*/
clearAll(){
if(confirm('确定删除全部已完成的任务吗?')){
this.todos = this.todos.filter((todo)=>{
return !todo.done
})
}
}
},
components: {
TodoHeader,
TodoList,
TodoFooter,
},
};
</script>
组件间通信的方式
一、父组件传输数据到子组件
(一)
props属性传输数据、方法到子组件,子组件接收数据和方法并且直接使用,只能父组件到子组件,不能反向以及不能跨越父子关系传递
示例:
//App.vue
...
<TodoList :todos="todos" :handleCheck="handleCheck" :deleteTodo="deleteTodo"></TodoList>
...
//TodoList.vue
...
export default {
props:['todos','handleCheck','deleteTodo'],
components:{
TodoItem
}
}
...
//TodoItem.vue
...
<script>
export default {
name:'TodoItem',
props:['todo','handleCheck','deleteTodo'],
methods: {
changeCheck(id){
this.handleCheck(id)
},
handleDelete(id){
if(confirm('确定删除这个任务吗?')){
this.deleteTodo(id)
}
}
},
}
</script>
这里为了子组件获取todos这个数据模型使用props,同时为了TodoList的子组件TodoItem能够使用,在此进行了传递。
props:['name'] //用于接收数据和方法 而在标签上使用:name="data"是将数据动态绑定到name身上使得props接收
二、子组件 ===> 父组件
(一)使用自定义组件方法
为组件标签自定义事件,通过触发该事件实现子组件到父组件的数据传递
使用v-on:事件名=“触发的函数” 或者 @事件名=“触发的函数”
优点:不需要使用props接收与传递
//自定义组件事件(App.vue)
<TodoList :todos="todos" @getList="getList" ></TodoList>
...
methods:{
getList(){
console.log('getList(data)被调用了')
}
}
...
//TodoList.vue
<button @click='handleGetList'></button>
...
methods:{
//触发自定义事件getList(data)
handleGetList(){
this.$emit('getList',this.data)
}
}
(二)使用ref属性
在组件标签上使用ref属性,可以通过this.refs.属性名 获取组件实例对象然后触发其定义的方法
示例:
//自定义组件事件(App.vue)
<TodoList ref="todolist" ></TodoList>
...
mounted:{
//使用钩子,当组件挂载完毕之后绑定一个自定义事件
//这里getList代表自定义事件名,this.getList()是绑定的具体方法
this.$refs.todolist.$on('getList',this.getList)
}
methods:{
getList(){
console.log('getList(data)被调用了')
}
}
...
//TodoList.vue
<button @click='handleGetList'></button>
...
methods:{
//触发自定义事件getList(data)
handleGetList(){
this.$emit('getList',this.data)
}
}
总结:如何使得父组件接收到子组件的数据?
- 巧妙地利用props可以传递方法的性质,将父组件方法传递到子组件,子组件在调用父组件的方法时传入子组件的数据,父组件方法中就接收到了子组件的数据。
- 最好使用自定义组件事件的方式
- 当父组件==>子组件 为数据时使用props,而一般传方法给子组件是父组件从子组件那里调用方法获取数据,因此就使用自定义组件事件的方法
props缺点:这个方法好在父到子之前的通信,但是对于跨越父子关系的组件间的通信显得有点冗余,而兄弟关系(并行关系)则需要一边通过父组件方法接受子组件的数据再传给另一边,就复杂了。
组件标签如何使用原生事件?
组件标签使用原生事件需要使用原生事件名.native
<TodoItem @click.native='getItem'></TodoItem>
二、建立全局事件总线实现任意两个组件间通信
简单的了解全局事件总线的原理:
当App中的任意一个组件想要与另外一个组件通信时,右上角与App平行的一个可用的实例对象(X)将需要通信的组件的事件绑定在自身,然后目标组件去触发X绑定的事件,将数据返回,实现组间通信。这样一来,不管组件之间什么关系,都可以只通过绑定事件到X以及去对应组件触发事件就可以实现通信了。关键是如何去找到这个具体的可用的X。
X:1. 所有组件能用 2.能够调用 $on $emit $off 方法
Vue中存在的重要关系:
VueComponent.prototype._proto_===Vue.prototype
这说明Vue中的组件实例对象能够访问Vue原型上的属性与方法
于是这里可以通过将vm(Vue实例对象)给到 Vue.prototype属性,具备X的能力。在创建Vue实例的过程当中:有 beforeCreate() created()
beforeMounted() mounted()
beforeUpdate() updated()
beforeDestroy destroy()
八个生命周期,在beforeCreated()中建立全局事件总线($bus),而在每个组件的beforeDestroy()中取消事件的绑定
代码:
new Vue({
beforeCreate() {
Vue.prototype.$bus = this //安装全局事件总线,$bus(这是自定义的名字,this是当前的Vue实例对象)
},
render: h => h(App),
}).$mount('#app')
应用场景:在上面的案例中当App组件想要与TodoItem组件通信需要经过TodoList传递数据,而使用全局事件总线,就可以直接传递。
适用于这种多跨越的关系以及并行的兄弟组件关系。
代码实现:
A组件(App)
...
<TodoList :todos="todos" :handleCheck="handleCheck" :deleteTodo="deleteTodo"></TodoList>
...
<script>
...
export default {
name: "App",
components: {
TodoHeader,
TodoList,
TodoFooter,
},
data() {
return {
todos:JSON.parse(localStorage.getItem('todos')) || []
}
},
methods: {
/**
* 勾选修改Todo.done
*/
handleCheck(id) {
this.todos.forEach((todo) => {
if(todo.id === id){
todo.done = !todo.done
}
})
},
/**
* 删除一个todo
*/
deleteTodo(id){
this.todos = this.todos.filter((el)=>{
return el.id !== id
})
}
},
watch:{
todos:{
deep:true,
handler(val){
localStorage.setItem('todos',JSON.stringify(val))
}
}
},
beforeDestroy(){
this.$bus.off(['handleCheck','deleteTodo'])
}
};
</script>
B组件(TodoList 的子组件 TodoItem)
<template>
...
<input type="checkbox" :checked="todo.done" @change="changeCheck(todo.id)"/>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
...
</template>
<script>
export default {
name:'TodoItem',
props:['todo'],
methods: {
changeCheck(id){
this.$bus.$emit('handleCheck',id)
},
handleDelete(id){
if(confirm('确定删除这个任务吗?')){
this.$bus.$emit('deleteTodo',id)
}
}
},
}
</script>
三、使用消息订阅与发布实现组件间简单的通信(Vue中用的不多)
推荐使用一个外部js库:pubsub-js
代码实例:
组件A订阅组件B的消息实现通信
//组件A
<script>
import pubsub from 'pubsub-js'
export default {
...
props:['todos']
methods: {
...
},
mounted(){
//参数:消息名,绑定的回调函数(参数:消息名,数据)
//该方法返回一个Id用于解除消息订阅
const this.pubId = pubsub.subscribe('handleCheck',(msgName,id) => {
this.todos.forEach((todo) => {
if(todo.id === id){
todo.done = !todo.done
}
})
})
},
beforeDestroy(){
//解除消息订阅
pubsub.unsubscribe(this.pubId)
}
}
</script>
组件B
<script>
import pubsub from 'pubsub-js'
export default {
...
methods: {
//组件B向组件A发布消息
handleSubsribe(data) {
pubsub.publish('handleCheck',data)
},
}
...
}
</script>