这一篇我们进行一个小的项目实战,做一个todolist。其中有很多细节和知识点。
我们使用组件化开发,在App.vue的管理下,分为三块。第一块是输入框(ToDoHeader.vue),第二部分是中间的部分(ToDoBody.vue),第三部分是底部的部分(ToDoFooter.vue),中间的部分还包含一个子组件(ToDoItem.vue),即每一条。
注意点1:父向子传参数
第一步,通过父在子标签上添加属性(注意要在前面加冒号,不然传过去的字符串),然后第二步,在子组件里面通过props接收这个属性,这时就可以在vc实例上获取和使用这个数据或方法了
//App.vue页面,向下传递参数和方法,这里我们举例<ToDoHeader :addFun="addOne"></ToDoHeader> <template> <div id="app"> <div id="root"> <div class="todo-container"> <div class="todo-wrap"> <ToDoHeader :addFun="addOne"></ToDoHeader> <ToDoBody :toDoList="ToDoList" :changeDone="changeDone" :ToDoRemove="ToDoRemove" ></ToDoBody> <!-- 通过ref的话比较灵活,配合mounted里面的绑定可以使用 --> <!-- <ToDoFooter :TotleNum="countTotleNum" :countDone="countDone" :allCheck="allCheck" ref="todofooter" ></ToDoFooter> --> <!-- 通过指令v-on绑定事件,灵活性差,不用配合ref, 这里我们如果想给组件绑定原生的事件,不如click事件,需要给他添加一个native,否则会被认为是自定义事件 --> <ToDoFooter :TotleNum="countTotleNum" :countDone="countDone" :allCheck="allCheck" @remove="removerAllChecked" @click.native="nativeFun" ></ToDoFooter> </div> </div> </div> </div> </template> <script> import ToDoBody from "./components/ToDoBody.vue"; import ToDoHeader from "./components/ToDoHeader.vue"; import ToDoFooter from "./components/ToDoFooter.vue"; import { nanoid } from "nanoid"; export default { name: "App", components: { ToDoBody, ToDoHeader, ToDoFooter, }, // mounted(){ // /*通过ref获取vc的实例对象,并给他绑的一个方法, // 另外注意,如果直接把回调函数写在这里的this.removerAllChecked这里, // 会出现函数体里面的this是调用remove方法的实例对象,而不是App.vue // */ // this.$refs.todofooter.$on('remove',this.removerAllChecked) // }, data() { return { ToDoList: JSON.parse(localStorage.getItem('todos')), }; }, watch:{ ToDoList(newValue,oldValue){ //localStorage是关闭浏览器不会消失,sessionStorage是关闭浏览器就没了 localStorage.setItem('todos',JSON.stringify(newValue)) // console.log(JSON.parse(sessionStorage.getItem('todos2'))) sessionStorage.setItem('todos2',JSON.stringify(oldValue)) //这个方法是清楚某一项 // localStorage.removeItem('todos') //这个方法是清除所有的 // localStorage.clear(); } }, methods: { nativeFun(){ console.log(1) }, //清理所有的完成 removerAllChecked(...a){ console.log(a)//这里可以通过...a来接受传过来的参数,并把他们放在一个数组里面 this.ToDoList=this.ToDoList.filter((item)=>{ return item.done == false }) }, //全选与不全选 allCheck(state){ this.ToDoList.forEach((item,index)=>{ item.done = state }) }, //增加一条 addOne(item) { if (item === "") return; const a = { id: nanoid(), name: item, done: false }; this.ToDoList.unshift(a); }, //修改某一条的状态 changeDone(i) { this.ToDoList.forEach((item) => { if (item.id === i) item.done = !item.done; }); }, //删除某一条 ToDoRemove(i) { this.ToDoList = this.ToDoList.filter((pp) => { return pp.id !== i; }); }, //计算一共有几条 countTotleNum() { return this.ToDoList.length; }, //计算完成了几条 countDone() { //方法1 // let i = 0; // this.ToDoList.forEach((item,index)=>{ // if(item.done == true) i++; // }) // return i //方法2:reduce专门做条件统计的 /* 参数1是一个函数,参数2是初始值, 数组长是几就掉几次, pre是初始值, 或者计算结束后的返回值;currene是当前的这一项;currentIndex是当前的索引;arr是元素所属的数组对象 第二次调的pre是第一次调用的这个函数的返回值 */ // let a = this.ToDoList.reduce((pre, current,currentIndex,arr) => { // return pre + (current.done == true ? 1 : 0); // },0); // return a return this.ToDoList.reduce((pre, current) => pre +(current.done == true ? 1 : 0),0); }, }, }; </script> <style> /*base*/ 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-danger { color: #fff; background-color: #da4f49; border: 1px solid #bd362f; } .btn-danger:hover { color: #fff; background-color: #bd362f; } .btn:focus { outline: none; } .todo-container { width: 600px; margin: 0 auto; } .todo-container .todo-wrap { padding: 10px; border: 1px solid #ddd; border-radius: 5px; } </style>
//这里是是ToDoHeader.vue页面这里用props接受了App.vue传过来的addFun方法 <template> <div class="todo-header"> <input type="text" v-model="Names" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add(Names)"/> </div> </template> <script> export default { props:['addFun'], data(){ return{ Names:'' } }, methods:{ add(i){ this.addFun(i); this.Names = '' } } }; </script> <style scoped> /*header*/ .todo-header input { width: 560px; height: 28px; font-size: 14px; border: 1px solid #ccc; border-radius: 4px; padding: 4px 7px; } .todo-header input:focus { 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, 0.6); } </style>
注意点2:子向父传参数
第一种办法:通过父给子提前传入一个函数,子组件通过props接受,然后到特定场景,子组件调用这个函数,可以传入参数,然后父组件就可以接收到参数,实现了子向父传。(代码参考父向子传的代码示例)
第二种办法:通过子组件触法父组件的自定义事件完成,父组件可以通过@即v-bind给子组件绑定一个自定义的事件,回调函数在App.vue,然后子组件通过vc.$emit()方法调用父组件的自定义事件,这里也可以传参数。或者,不用v-bind,可以通过ref属性获取到这个dom,然后在dom导航通过$on给他绑定自定义事件。(用ref比较灵活点,我们可以给他加个定时器等来产生不同的交互效果,如果不用v-bind的话,自定义事件在页面渲染后就会被加载好,在mounted钩子的时候)
//App.vue,这里我们注意ToDoFooter的remove方法,注释掉的是通过ref来绑定自定义事件 <template> <div id="app"> <div id="root"> <div class="todo-container"> <div class="todo-wrap"> <ToDoHeader :addFun="addOne"></ToDoHeader> <ToDoBody :toDoList="ToDoList" :changeDone="changeDone" :ToDoRemove="ToDoRemove" ></ToDoBody> <!-- 通过ref的话比较灵活,配合mounted里面的绑定可以使用 --> <!-- <ToDoFooter :TotleNum="countTotleNum" :countDone="countDone" :allCheck="allCheck" ref="todofooter" ></ToDoFooter> --> <!-- 通过指令v-on绑定事件,灵活性差,不用配合ref, 这里我们如果想给组件绑定原生的事件,不如click事件,需要给他添加一个native,否则会被认为是自定义事件 --> <ToDoFooter :TotleNum="countTotleNum" :countDone="countDone" :allCheck="allCheck" @remove="removerAllChecked" @click.native="nativeFun" ></ToDoFooter> </div> </div> </div> </div> </template> <script> import ToDoBody from "./components/ToDoBody.vue"; import ToDoHeader from "./components/ToDoHeader.vue"; import ToDoFooter from "./components/ToDoFooter.vue"; import { nanoid } from "nanoid"; export default { name: "App", components: { ToDoBody, ToDoHeader, ToDoFooter, }, // mounted(){ // /*通过ref获取vc的实例对象,并给他绑的一个方法, // 另外注意,如果直接把回调函数写在这里的this.removerAllChecked这里, // 会出现函数体里面的this是调用remove方法的实例对象,而不是App.vue // */ // this.$refs.todofooter.$on('remove',this.removerAllChecked) // }, data() { return { ToDoList: JSON.parse(localStorage.getItem('todos')), }; }, watch:{ ToDoList(newValue,oldValue){ //localStorage是关闭浏览器不会消失,sessionStorage是关闭浏览器就没了 localStorage.setItem('todos',JSON.stringify(newValue)) // console.log(JSON.parse(sessionStorage.getItem('todos2'))) sessionStorage.setItem('todos2',JSON.stringify(oldValue)) //这个方法是清楚某一项 // localStorage.removeItem('todos') //这个方法是清除所有的 // localStorage.clear(); } }, methods: { nativeFun(){ console.log(1) }, //清理所有的完成 removerAllChecked(...a){ console.log(a)//这里可以通过...a来接受传过来的参数,并把他们放在一个数组里面 this.ToDoList=this.ToDoList.filter((item)=>{ return item.done == false }) }, //全选与不全选 allCheck(state){ this.ToDoList.forEach((item,index)=>{ item.done = state }) }, //增加一条 addOne(item) { if (item === "") return; const a = { id: nanoid(), name: item, done: false }; this.ToDoList.unshift(a); }, //修改某一条的状态 changeDone(i) { this.ToDoList.forEach((item) => { if (item.id === i) item.done = !item.done; }); }, //删除某一条 ToDoRemove(i) { this.ToDoList = this.ToDoList.filter((pp) => { return pp.id !== i; }); }, //计算一共有几条 countTotleNum() { return this.ToDoList.length; }, //计算完成了几条 countDone() { //方法1 // let i = 0; // this.ToDoList.forEach((item,index)=>{ // if(item.done == true) i++; // }) // return i //方法2:reduce专门做条件统计的 /* 参数1是一个函数,参数2是初始值, 数组长是几就掉几次, pre是初始值, 或者计算结束后的返回值;currene是当前的这一项;currentIndex是当前的索引;arr是元素所属的数组对象 第二次调的pre是第一次调用的这个函数的返回值 */ // let a = this.ToDoList.reduce((pre, current,currentIndex,arr) => { // return pre + (current.done == true ? 1 : 0); // },0); // return a return this.ToDoList.reduce((pre, current) => pre +(current.done == true ? 1 : 0),0); }, }, }; </script> <style> /*base*/ 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-danger { color: #fff; background-color: #da4f49; border: 1px solid #bd362f; } .btn-danger:hover { color: #fff; background-color: #bd362f; } .btn:focus { outline: none; } .todo-container { width: 600px; margin: 0 auto; } .todo-container .todo-wrap { padding: 10px; border: 1px solid #ddd; border-radius: 5px; } </style>
//ToDoFooter.vue这里我们在removeAllC方法里面通过this.$emit('name',参数)调用了父组件的方法 <template> <div class="todo-footer" v-if="TotleNum()"> <label> <!-- 未使用v-model方法 --> <!-- <input type="checkbox" @change="allcheck" :checked="countDone()===TotleNum()&&TotleNum()!=0?true:false"/> --> <input type="checkbox" v-model="allChe"/> </label> <span> <span>已完成{{countDone()}}</span> / 全部 {{TotleNum()}}</span> <button class="btn btn-danger" @click="removeAllC()">清除已完成任务</button> <button class="btn btn-danger" @click="clearMyEvent()">清除自定义事件</button> </div> </template> <script> export default { props:['TotleNum','countDone','allCheck'], methods:{ removeAllC(){ //调用在App.vue里面挂载在ToDoFooter上的事件‘remove’,并且传入1234这4个参数 console.log('我被调用了,就算实例被销毁或者,事件被解绑,我也可以执行,因为我是原生的事件') this.$emit('remove',1,2,3,4) }, clearMyEvent(){ //解绑remove事件 this.$off('remove') }, // 未使用v-model方法 // allcheck(e){ // if(e.target.checked){ // this.allCheck(true) // }else{ // this.allCheck(false) // } // } }, computed:{ allChe:{ get(){ return this.countDone()===this.TotleNum()&&this.TotleNum()!=0?true:false }, set(a){ // console.log(a) this.allCheck(a) } } } }; </script> <style> /*footer*/ .todo-footer { height: 40px; line-height: 40px; padding-left: 6px; margin-top: 5px; } .todo-footer label { display: inline-block; margin-right: 20px; cursor: pointer; } .todo-footer label input { position: relative; top: -1px; vertical-align: middle; margin-right: 5px; } .todo-footer button { float: right; margin-top: 5px; } </style>
注意点3:传参的小技巧
1、我们在一个函数传了好多参数,但我门只需要第一个时,可以通过点来接受,并放入一个数组内。(传参的时候正常传入)
function a(b,...c){ console.log(b); console.log(c) } a(1,2,3,4,5)
2、我们使用一个函数时可能只需要后面的一个参数,不需要前面的参数,可以用_来做占位符
function d(_,f){ console.log(f) } d(7,8)
注意点4:给组件绑定原生事件
给组件绑定如onclick,keyup等事件,需要通过一个.native来使这个事件为原生事件,否则会被认为是自定义事件,比如这里绑定的nativeFun事件
<ToDoFooter :TotleNum="countTotleNum" :countDone="countDone" :allCheck="allCheck" @remove="removerAllChecked" @click.native="nativeFun" ></ToDoFooter>
注意点5:给子组件传或者保存一个对象
let a = { name:"k", age:8} //转成字符串 console.log(JSON.stringify(a)) //字符串转成对象 console.log(JSON.parse(JSON.stringify(a)))
注意点6:本地缓存storage和session
storge可以保存数据在浏览器,关闭以后再打开,还存在。session是保存在浏览器,在浏览器关闭后会消失,他们俩的保存,获取,清除的方法都是一样的。
ToDoList(newValue,oldValue){ //这是获取缓存的数据 localStirage.getItem('todos'); //localStorage是关闭浏览器不会消失,sessionStorage是关闭浏览器就没了 localStorage.setItem('todos',JSON.stringify(newValue)) // console.log(JSON.parse(sessionStorage.getItem('todos2'))) sessionStorage.setItem('todos2',JSON.stringify(oldValue)) //这个方法是清除某一项 localStorage.removeItem('todos') //这个方法是清除所有的 localStorage.clear(); }
注意点7:数组的reduce方法
我们在需要找出一个数组里面的完成的项的数量,有好几种方法,第一种直接遍历数组,通过if判断出那些可用的,在计算出来,第二种,通过reduce方法,他有两个参数,第一个是一个函数,第二个是初始值,在函数里面有四个参数,pre是初始值, 或者计算结束后的返回值;currene是当前的这一项;currentIndex是当前的索引;arr是元素所属的数组对象
//计算完成了几条 countDone() { //方法1 let i = 0; this.ToDoList.forEach((item,index)=>{ if(item.done == true) i++; }) return i //方法2:reduce专门做条件统计的 /* 参数1是一个函数,参数2是初始值, 数组长是几就掉几次, pre是初始值, 或者计算结束后的返回值;currene是当前的这一项;currentIndex是当前的索引;arr是元素所属的数组对象 第二次调的pre是第一次调用的这个函数的返回值 */ let a = this.ToDoList.reduce((pre, current,currentIndex,arr) => { return pre + (current.done == true ? 1 : 0); },0); return a //简写 return this.ToDoList.reduce((pre, current) => pre +(current.done == true ? 1 : 0),0); },
注意点8:生成一个随机数作为id
通过nanoid这个库来生成。
npm i nanoid
import { nanoid } from "nanoid"; //增加一条 addOne(item) { if (item === "") return; const a = { id: nanoid(), name: item, done: false }; this.ToDoList.unshift(a); },
注意点9:通过全局事件总线bus来实现任意两个组件的通信
第一步:安装全局时间总线bus
//main.js import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false new Vue({ render: h => h(App), beforeCreate(){ Vue.prototype.$bus = this//安装全局事件总线$bus,this是当前的vm } }).$mount('#app')
第二步:在收消息的页面App.js绑定事件
//App.vue mounted(){ //绑定全局总线 this.$bus.$on('changeDone',this.changeDone) this.$bus.$on('ToDoRemove',this.ToDoRemove) }, destroyed(){ //用完了销毁 this.$bus.$off('changeDone') this.$bus.$off('ToDoRemove') },
第三步:在发消息的页面通过emit触发这个全局事件总线上的事件
//ToDoItem.vue <template> <li> <label> <input type="checkbox" :checked="ToDoItem.done" @change="change(ToDoItem.id)"> <!-- 这里用v-model也直接可以实现,但是不建议这样写,因为我们不能去改变props里面的值 --> <!-- <input type="checkbox" v-model="ToDoItem.done" > --> <span>{{ToDoItem.name}}</span> </label> <button class="btn btn-danger" @click="removethis(ToDoItem.id)">删除</button> </li> </template> <script> export default { name:'ToDoItem', props:['ToDoItem'], // methods:{ // change(i){ // this.changeDone(i) // }, // removethis(i){ // if(confirm('确定删除')){ // this.ToDoRemove(i) // } // } // } // 通过事件总线来调这两个函数 methods:{ change(i){ this.$bus.$emit('changeDone',i) }, removethis(i){ if(confirm('确定删除')){ this.$bus.$emit('ToDoRemove',i) } } } }; </script> <style scoped> li:hover{ background: #ddd; } li:hover button{ display: block; } /*item*/ 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; } </style>
注意点10:消息的订阅与发布
安装
npm i pubsub-js
第一步:消息的订阅,在接收的页面写。
//App.vue页面 import pubsub from "pubsub-js" mounted(){ /*订阅消息,由于要传入id才可以取消订阅,所以把它交给this.id, subscribe里面有两个参数,第一个是订阅消息的名字,第二个参数是一个函数, 函数的第一个值是消息的名字,第二个是返回的参数,不想要名字可以用占位符_ */ this.id = pubsub.subscribe('ToDoRemove',(msgName,data)=>{ console.log(msgName) this.ToDoRemove(data) }) }, beforeDestroy(){ //取消订阅,注意,取消订阅时,不是传入的订阅消息的名字,而是id pubsub.unsubscribe(this.id) }, methods:{ //删除某一条 ToDoRemove(i) { this.ToDoList = this.ToDoList.filter((pp) => { return pp.id !== i; }); }, }
第二步:消息的发布
//ToDoItem页面进行消息的发布 import pubsub from "pubsub-js" methods:{ removethis(i){ if(confirm('确定删除')){ pubsub.publish('ToDoRemove',i) } } }
注意点11、如果要获取某个dom,可以在传参地方传如$event
<input @blur="change2(ToDoItem.id,$event)"> methods:{ change2(e,c){ if(!c.target.value.trim()){ return }else{ this.$bus.$emit('changetodo2',e,c.target.value) } }, }
注意点12、判断某个对象上面有没有某个值
可以用hasOwnProperty来判断
let a = { name:'kk' } a.hasOwnProperty('name')//true
注意点13、判断某个字段是否为空
可以用trim()来判断
<input @blur="change2(ToDoItem.id,$event)"> methods:{ change2(e,c){ if(!c.target.value.trim()){ return }else{ this.$bus.$emit('changetodo2',e,c.target.value) } }, }
注意点14、$nextTick的使用
它的意思是在下一轮的时候在执行。
在Vue生命周期的
created()
钩子函数进行的DOM操作一定要放在Vue.nextTick()
的回调函数中/*这里我们要让input框一上来就聚焦,如果直接写是不生效的,因为当执行到那一句的时候 页面还没渲染出来,所以把在页面渲染完成后执行的写在nextTick的回调中,会生效 */ changetodo(e){ this.$bus.$emit('changetodo',e) this.$nextTick(function(){ this.$refs.a.focus() }) },