一个基于vue的小练习,用来帮助迅速回顾vue的基础知识
下面是效果图
具体分为了四个组件,Footer,Header,item,list,其中item与list为父子组件,footer与header,list为兄弟组件,通过这么一个小案例,迅速回顾vue基础知识包括但不限于指令,配置项,通讯方式等等
事件总线的建立
相信大家对组件通信这一方面的事件总线都不陌生,它用于兄弟组件,爷孙组件,叔侄组件之间的通讯比较多
可以看到,如果需要从跟组件往组件n和组件s传递数据,那么需要层层传递下来,传递给组件s的父组件,组件n的爷爷组件,或者组件s需要给组件n传递数据,一层层往上抛出,再一层层往下传递,但是这些数据未必是父组件和爷爷组件需要的,那么这样的传递就会浪费大量的资源,显得很臃肿,而事件总线的出现就是为了解决这一问题。 好,那么不墨迹了事件总线只需要再main组件中加入这么一段代码即可
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
beforeCreate(){
Vue.prototype.$bus=this
}
}).$mount('#app')
只需要在实例创建后,注入前进行写入这样的,在vue的原型上加上。代表将当前实例作为事件总线 关于生命周期不在这里过多赘述,以一张图表示
事件总线挂在完之后我们先放一边,等到后续用到的时候再详细阐述
组件App
组件app呢是我们的跟组件,一切组件都需要在这里进行引入和展示
<template>
<div id="root">
<div class="container">
<div class="todo-wrap">
<MyHeader @addTodo="addTodo"></MyHeader>
<MyList :todos="todos"></MyList>
<MyFooter :todos="todos" @DelAll="DelAll" @Achieve="Achieve"></MyFooter>
</div>
</div>
</div>
</template>
<script>
import MyFooter from "./components/MyFooter.vue";
import MyHeader from "./components/MyHeader.vue";
import MyList from "./components/MyList.vue";
export default {
components: {
MyHeader,
MyFooter,
MyList,
},
mounted() {
this.$bus.$on("Del", this.Del);
this.$bus.$on("check", this.cheke);
this.$bus.$on("upd", this.upd);
},
beforeDestroy() {
this.$bus.$off("Del", this.Del);
this.$bus.$off("check", this.cheke);
this.$bus.$off("upd", this.upd);
},
methods: {
//更新
upd(id, value) {
this.todos.forEach((i) => {
if (i.id === id) {
i.title = value;
}
});
},
//添加
addTodo(todoObj) {
this.todos.unshift(todoObj);
},
//勾选
cheke(id) {
this.todos.forEach((item) => {
if (item.id === id) {
item.done = !item.done;
}
});
},
//删除
Del(id) {
this.todos = this.todos.filter((e) => e.id !== id);
},
//删除已完成的
DelAll() {
this.todos = this.todos.filter((e) => e.done === false);
},
//全选
Achieve() {
this.isAll = this.todos.every((e) => {
return e.done === true;
});
if (this.isAll) {
this.todos.forEach((e) => {
e.done = false;
});
} else {
this.todos.forEach((e) => {
e.done = true;
});
}
},
},
data() {
return {
todos: JSON.parse(localStorage.getItem("todos")) || [],
};
},
watch: {
todos: {
deep: true,
handler(value) {
localStorage.setItem("todos", JSON.stringify(value));
},
},
},
};
</script>
<style>
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-change {
color: #fff;
background-color: #a6aeb3;
border: 1px solid #a6aeb3;
margin-right: 8px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.container {
width: 600px;
margin: 0 auto;
}
.container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
大家先忽视里面的方法,再跟组件中,引入了其他三个组件(import),并且用components进行注册
页脚组件
这里是我们的页脚组件,初始样式很简单,只有一个input勾选框还有一些底部要展示的数据和一些按钮
<template>
<div class="fotter" v-show="this.todos.length">
<label>
<input type="checkbox" @click="test" :checked="isAll" />
</label>
<span>
<span @click="test">已完成{{ tests }}/总数{{ todos.length }}</span>
</span>
<button class="btn btn-danger" @click="DelAlls">清除已经完成的任务</button>
</div>
</template>
<script>
export default {
props: ["todos"],
computed: {
isAll() {
if (this.todos.length == 0) {
return false;
} else {
return this.tests === this.todos.length;
}
},
tests() {
return this.todos.reduce((pre, current) => {
return pre + (current.done ? 1 : 0);
}, 0);
},
},
methods: {
DelAlls() {
this.$emit("DelAll");
},
test() {
this.$emit("Achieve");
},
},
};
</script>
<style>
.footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.footer label input {
position: relative;
top: -1px;
vertical-align: middle;
}
.footer label button {
float: right;
margin-top: 5px;
}
.btn-danger {
float: right;
}
</style>
头部组件
头部组件只有一个输入框用来添加数据的
<template>
<div class="header">
<input
type="text"
placeholder="请输入你要添加的任务"
v-model="title"
@keyup.enter="add"
/>
</div>
</template>
<script>
export default {
data() {
return {
title: "",
};
},
methods: {
add() {
if (!this.title.trim()) return alert("Please enter");
const todoObj = {
id: Math.random() * 10,
title: this.title,
done: false,
};
this.$emit("addTodo", todoObj);
this.title = "";
},
},
};
</script>
<style>
.header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid black;
border-radius: 4px;
padding: 4px 7px;
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236.6);
}
</style>
列表组件
列表的父组件
<ul class="main">
<transition-group name="todo" appear>
<MyItem
v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"
></MyItem>
</transition-group>
</ul>
</template>
<script>
import MyItem from "./MyItem.vue";
export default {
components: { MyItem },
props: ["todos"],
};
</script>
<style>
.main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
.todo-enter-active {
animation: todos 0.3s linear;
}
.todo-leave-active {
animation: todos 0.3s linear reverse;
}
@keyframes todos {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
</style>
列表子组件
<template>
<li>
<label>
<input
type="checkbox"
:checked="todo.done"
@click="handleCheck(todo.id)"
/>
<span v-show="!todo.isUpdata">{{ todo.title }}</span>
<input
type="text"
v-show="todo.isUpdata"
ref="inputTitle"
:value="todo.title"
@blur="reve(todo, $event)"
/>
</label>
<button class="btn btn-danger" @click="Dels(todo.id)">删除</button>
<button class="btn btn-change" @click="achieve(todo)">修改</button>
</li>
</template>
<script>
import MyList from "./MyList.vue";
export default {
comments: {
MyList,
},
props: ["todo"],
methods: {
handleCheck(i) {
this.$bus.$emit("check", i);
},
Dels(i) {
this.$bus.$emit("Del", i);
},
//失去焦点时候更新数据
reve(todo, e) {
todo.isUpdata = false;
if (!e.target.value) {
alert("内容不能为空");
return;
}
this.$bus.$emit("upd", todo.id, e.target.value);
},
achieve(todo) {
if (Object.prototype.hasOwnProperty.call(todo, "isUpdata")) {
todo.isUpdata = true;
} else {
this.$set(todo, "isUpdata", true);
}
this.$nextTick(function () {
//一般用于,当数据改变后要基于更新的dom进行某些操作的时候,要在nextTick中执行
//下一轮的时候在执行,nextTick会在dom节点更新完毕之后在执行
//如果这个直接暴露在外面,则会执行完achieve之后才会渲染dom,但是由于inp还没有渲染到页面上,无法获取焦点
this.$refs.inputTitle.focus();
});
},
},
};
</script>
<style>
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
li:hover {
background-color: #ddd;
}
li:hover button {
display: block;
}
</style>
至此我们的组件已经添加完毕,下面介绍组件里的功能
列表组件
在这个图中我们可以看到,list组件时父组件,item是里面每一个任务的子组件,再这个案例里,数据被存放在了app跟组件中,首先要解决的就是如何把跟组件的数据传递到子组件list和孙组件item中
我们回过头来看app组件,在app组件中data里
写入了这么一个数据样式,相信大家都不陌生,localStorage,这段的意思是,如果在本地存储存在数据,那么拿到本地存储里面的数据,如果不存在,那么这个todos就是一个空数组,等待用户往里面添加内容,这里大家可以先手写一些静态数据进行测试,这里呢我就插入头部的添加数据的输入框进来 在app组件中,存在一个方法名为addTdo,用来添加数据的
添加数据的输入框在头组件中,那么就涉及到一个问题,如何让子组件给父组件传递数据呢,这里我们采用自定义事件的形式,自定义事件想要获取子组件给父组件传递的数据,那么就需要父组件提前预留一个方法,在合适的时机调用,那么这个合适的时机是指什么时候呢,是指子组件何时通知父组件,什么时候就是何合适的时机,我们再看头部组件
可以看到再input框中存在一个v-model指令和键盘事件,这里是针对输入框进行的事件,vmodel是典型的双向绑定事件,用来拿到输入框中的内容并且与data里面的titel绑定@是v-on事件的缩写用来注册方法,在这里我们可以看到,下方的methods事件中写入了一个add方法,并且与键盘事件向链接,当键盘按下回车键的时候,触发事件
再看事件函数内部,这里我们首先进行了判断,输入框内的东西是否为空,不为空才会进入下一步,再todoObj里写入了一个随机注册id的方法,并且将输入框中的内容存到对象中,这个done设置为flase(后续介绍) 重点在这里,我们通过点击事件通过$emit抛出一个事件,通知父组件这个时候执行addtodo方法(app组件定义的方法),并且将数据todoObj作为参数传递给父组件
methods: {
add() {
if (!this.title.trim()) return alert("Please enter");
const todoObj = {
id: Math.random() * 10,
title: this.title,
done: false,
};
this.$emit("addTodo", todoObj);
this.title = "";
},
},
};
至此,父组件拿到了子组件传递的数据,并且调用了addTdo方法,往数据todos里添加一个数据,此时app组件里面的todos有了数据,那么光有数据是不行的,还需将数据传递给子组件list用来渲染到页面上
父组件给子组件传递 父组件给子组件传递相对简单,只需要加上:(v-bind),将需要的数据写入,再子组件的props中接受一下,就完成了传递。
随后通过vfor指令,渲染item组件,注意注意,vfor指令一定要绑定唯一的key值!!
这样就会根据todos的数量进行渲染到页面上来
回过头我们去看item组件
item组件针对每一个小的任务做了样式和一些交互
分别是勾选,全选,删除,修改,在这里呢就用到了我们之前提到的事件总线 大家思考一下,如何确定你选中的任务就是数据里存的哪个任务的? 答案是通过id,那么就涉及到了如何把id传给父组件告诉父组件我点击勾选的是哪个呢 那么就涉及到我们的事件总线上场, 事件总线的使用的和自定义组件类似,事件总线要分清谁是需要数据的,谁是发送数据的 发送数据的只需要写一个方法通知事件总线,然后事件总线再通知其他组件并且携带数据过去 以这个为例,
在这里写入了两个方法,分别是选中和删除,并且与按钮绑定
当点击按钮的时候,会触发选中事件,并且把当前的id当作参数传入,事件总线的使用就是如此
this.emit(名称,参数),这样就是告诉事件总线,我给你传入了一个名字为名称的事情,帮我监控,谁用了这个名字,就把数据传给谁
反过来看接收数据方接收方需要再挂载前,通知事件总线,我要用名字为xxx的事件,并且当通知到我的时候,我触发this.xxx的事件来对传来的数据做一些处理
在这里就是,我要绑定勾选和删除这个名字,并且当子组件通知我的时候执行this.del方法,
作为接收方,我们需要再接收方事先预留一个函数,用来处理传来的数据
这样当子组件点击并且抛出勾选和删除事件并且传递过来对应的id值通知到父组件,父组件会再挂在前收到通知,并且绑定勾选和删除事件,随后触发预留的函数对数据进行一个处理。
在这里通过传来的id对todo里面的done(是否勾选),做一个取反,还有通过filter过滤进行删除。
还有一个事件修改事件,点击修改事件会触发,注意注意,再vue里给数据平白无故添加上一个属性,需要使用¥set和get方法,使用get和set方法才会为属性匹配一套set和get方法,才会保证数据改变从而引起页面改变 这里要介绍到另一个指令,ref,用来获取真实dom,大家都知,vue是虚拟dom然后生成真实dom,那么通过ref指令,可以获取到对应的dom进行操作,下面就通过this.$refs.inputTitle.focus();进行了聚焦
至此只剩下全选和本地存储未说明
全选就比较简单啦
我们事先已经给数据添加了一个done属性,就是用来判断是否已经选中,通过一系列的方法,我们只需要得出是否选中的数量等于数据全部的数量即可。大家仔细看,不多阐述
本地存储
相信大家应该都了解localStorage这个东西,如果不了解赶紧去补课。这里采用了深度观察的方式对todos监视,往本地存储里面存入新的值,因为todos是一个对象,需要转换成json 的格数传入
至此小案例就完成啦
后续再加上vuex的写法
已上传git gitee.com/wu-canhua/n…
---2023年10月28日20点35分-----