脚手架搭建
1-初始化脚手架(全局安装)
npm install -g @vue/cli
2-切换到创建项目的空目录下
vue create xxxx
整体结构
整体思路
App定义所有回调方法 增删改查 还有统一存放最终数据,所有子组件不拿数据,由App下发数据,类似于数据中心
打个比方,所有子组件只是个可以反复利用的壳子,类似于高达积木,而正在要来拼接他们的是我们的APP组件,有大脑想法,可以决定怎么拼好看,且怎么上色也就是怎么赋予数据得当
当然下放数据可以给其第二个大脑List
第二个数据中心为App给List传的数据todos,其遍历出来的每一个todo下发每一个Item组件,也就是在Item组件里v-for,该item里面的数据隔离开其他数据,可以进行编辑,你也可也在一个List页面同时写了ul,li,此时遍历出来的每个li如果要控制他们单独编辑状态还必须v-if=“showInput&&currRow=i” 来把所有的input同时遍历出来,然后当点击了按钮后传currRow当前编辑行设置为第几个i,然后showInput变为true,
删除操作就传递id然后触发事件遍历过滤出去就行
所有子组件触发的事件 App全部监听 事件可以通过pubsub消息订阅,事件总线,子父组件事件触发和监听方法实现, 父给子传递数据就使用 <sonComponent :子组件里的props接收名=“父组件的数据字段名”> 也可以传递一个方法。子组件props:[‘数据名’]接收
依赖安装
第三方的依赖引入import放在最上面
名称 | 作用 | 使用场合 |
---|---|---|
nanoid | 生成唯一的Id,防止id冲突 | 不用手动递增ID |
pubsub | 消息订阅与发布 | 孙组件给爷组件传递数据时 |
依赖用法
nanoid
npm i nanoid
import { nanoid } from "nanoid";
nanoid()
pubsub
npm i pubsub
import pubsub from 'pubsub-js'
父组件挂载时订阅,销毁时取消订阅
mounted() {
this.pubId = pubsub.subscribe('deleteTodo', this.deleteTodo)
},
beforeDestroy() {
pubsub.unsubscribe(this.pubId)
},
子组件传递数据,
//删除
handlerDelete(id){
if (confirm('确定要删除吗?')) {
pubsub.publish('deleteTodo',id)
}
注意事项
当你发现一个组件很难命名的时候,可能该组件包含了多个功能,需要更细致的拆分组件
例如Header用来输入信息,List用来展示,编辑,删除操作,这两个就必须拆分开
h5自身有header,head组件,不能起和这俩一样的名字,会有警告,可以在前面加个前缀或直接叫Top还是别的名字
要求
孙组件与父组件通信时使用全局事件总线和消息订阅与发布
update和check使用全局事件总线
deleteTodo使用消息订阅与发布
命名要求
组件
头 | MyHeader |
---|---|
列表 | MyList |
每一个item | MyItem |
页脚 | MyFooter |
方法
添加 | addTodo/handleAdd |
---|---|
勾选 | checkTodo/handleCheck/checkAllTodo |
编辑 | updateEdit/handleEdit |
删除 | deleteTodo/deleteAllTodo/handleDelete |
全选 | checkAllTodo |
数据
所有todo | todos |
---|---|
单个todo | todo |
编辑状态 | isEdit |
是否全选 | isAll |
完成状态 | done |
todo标题 | title |
todoId | todoId |
todo对象 | todoObj |
已完成 | doneTotal |
所有 | total |
涉及的操作 主要内容
$event
传递原生事件,属性target获取触发该事件的DOM对象,DOM对象的value获取其值,用于输入框失焦所触发事件获取输入框值
本地存储+watch深度监视
搭配watch监听器,监听某个数据对象,深度监视为真,表示监视该对象属性
内部设置handler处理函数当数据发生变化时触发,接收两值分别为,newVal变化后值、oldVal变化前值
watch: {
todos: {
deep: true,
handler(value) {
localStorage.setItem('todos', JSON.stringify(value))
}
}
}
只需要第一个newVal,该值为todos对象,由于localStorage存储必须为JSON字符串不能为js对象,需要先转,且取值的时候需要从json字符串转为js对象
localStorage.setItem('todos', JSON.stringify(value))
钩子函数/全局事件总线/ref命名/vue原型
vue实例创建前,给vm原型绑定一个全局变量bus,值为vm本身,这样后续所有父组件可以在bus上监听,子组件在上面触发
$bus.on监听(‘事件名’,回调)
$bus.emit触发(‘事件名’,参数)
打个比方,on相当于耳朵在听,听到了就根据听到的内容做出对应的动作,也就是回调
而emit相当于说,说给耳朵听, 之前单纯的emit只能在父子组件通信,现在引入了一个传话的,隔代通信,事件名相当于一个人名,和谁通信的标识,当然这个on必须在挂载后执行类似于父组件监听子组件的 <Son @Event=回调 />
当然,该传话人必须让所有组件都能看到,访问到,往VueComponent.protoType身上放?每一个标签都会由Vue.extend生成一个新的VueComponent,这样所有组件都能看到this.$bus了?
以下的VueComponent简写成vc,vue实例对象简写成vm (viewModule命名来源于Model-View-ViewModule)
MVVM扫盲:
Module——数据
View——视图(div,input,span…)
ViewModule——方法(视图通过方法来获取到数据的途径, 视图数据间的桥梁)
但是该vc是vue实例对象也就是vm.extend后才有的。一开始没有的,所以该绑定无效,当然你也可以直接改源码,每次extend生成的VueComponent都有该共同传话人(不建议)
可以将其放置在vc的缔造者,vm身上 this。。如果子组件身上的this里没有bus属性则找vm的this,类似于java里的extend继承父类,父类vm有的所有属性子类都有这样就能通过this.$bus.on/emit调用,思路有了。该如何写呢?这里先插入解释一下之前 父子通信的过程
上面听不懂的你就这样记。我们在找一个大家都能 看到的人,委托他帮忙传话,
举个例子, 你要和校长聊天,但是没校长联系方式。你这个时候得找你们班主任 转达你要说的内容给校长,而校长要和你聊天,他也得通过班主任
这里的原型对象和java里的类相似,而实例对象等于类似于java里new出来的实例对象
知道了 o n , e m i t , o f f 在原型对象上了之后且,原型对象 v c 和 v m 都能看到,那问题就好办了,挑一个 v c > 还是 v m 设置为 on,emit,off在原型对象上了之后且,原型对象vc和vm都能看到,那问题就好办了,挑一个vc>还是vm设置为 on,emit,off在原型对象上了之后且,原型对象vc和vm都能看到,那问题就好办了,挑一个vc>还是vm设置为bus的属性即可
第一种vc,(好像有点麻烦)
第二种vm
总结一句话, 在原型上的bus属性放置了 vm自身(携带方法on emit off) 所有组件可以通过this.
b
u
s
.
bus.
bus.on 通信
为什么叫$bus? 而不是随便起一个x还是什么乱七八糟的名字。bus翻译过来有总线的意思 而this为全局, 所以称全局事件总线
$ 符号意为 规范用法 vue原型身上的api都用 $ 例如import request from axios.js |||| Vue.prototype.$request=request等等…
插入一句话——————
父组件这样@事件名=回调其实等同于在子组件里 this. $ on(‘事件名’,回调) 监听, ,子组件里this. $ emit触发…都是在同一个VC下使用原型的 $ on emit属性(当然还有一个解除绑定this.$off)所以父子可以通信
当然子组件监听子组件触发 不是有点废话?一般用于同级组件也称兄弟组件,先ref组件命名再指定哪个组件进行监听on,on需在mounted挂载之后执行
注意
on不能重名,可以将其分模块到一个文件下统一constant常量
$ on 销毁前需 $ off 掉 为什么?因为当子组件销毁时 不会自动取消父组件的订阅,你还隔这占着 $ bus傀儡中间人,数量一多,整个vue服务器容易性能下降,好比数据库不使用连接池,你连接多了不用又不释放,占用资源,时间长卡顿甚至数据库崩溃,那为什么vc不用off?,因为vc关闭事件也跟着没,而这里的事件监听on是在 原型上的,一直存在的不受vc关闭而关闭
beforeCreate() {
Vue.prototype.$bus = this
},
挂载后**mounted**,订阅消息,事件总线监听
销毁前**beforeDestroy**,关闭所有订阅,和事件总线监听
至此,我们可以再任意组件之间通信
父子组件传值(数据/方法)
:子组件内部值, =号右边为 父组件自身数据
也可以通过插槽
子< slot name=“body” :rows=“data”> :传递出去的数据名=自身组件数据名
父 < template v-slot:body=“scope”> 子组件所有:传出去的数据为一个对象,父组件使用scope统一接收 例如使用row数据则scope.row
该例子为具名插槽,如果没有name,则使用v-slot:default= 默认插槽,也可以直接写成v-slot=“” v-slot:body 可以简写为#body
element ui的table使用到了该原理, 先给table 绑定数据 :data=xxx 再通过template v-slot="scope.row"获取 当前行的数据
filter过滤删除,foreach循环设置状态
页脚计算属性统计完成数量和总数
全选框根据isAll 中get属性计算返回 《total和done是否相等,且total>0 》 不然total 为0,done也为0也全选了
set属性 意味着每次isAll变化都会传进来复选框的布尔值,以此改变所有的todo.done为true/false
total 计算todo.length
doneTotal计算所有 done为真的 使用todos.reduce累加方法,传递两个形参,第一个为回调函数,接收两个形参pre积累的值,遍历出来的每一个todo, 方法体为 pre+ done? 1:0 ,第二个值为pre的初始值
下划线占位符_
当消息订阅触发的回调函数传来两个参数第一个是消息是名称,第二个是消息传递过来的参数,这时候我们可以选中使用一个XXX占用第一个形参但是会变灰,且提示unuse,也可以使用 占位符,会发光且不提示错误信息
blur 输入框失焦
传递todo,和event事件获取输入框的值,和todo id进行更新对应的todo的title
confirm确认
对象的封装传递 add
单个复选框的change绑定事件
传id,触发事件 遍历勾选Done值
编辑 nextTick/$set /hasOwnProperty/event
input
与title同样位置来一个input 标签绑定数据为 todo.title,v-show与 title互斥 一个为!xxx / 一个为xxx
isEdit属性
给todo追加一个属性isEdit,但是直接给对象添加属性,该属性不是响应式的没有get/set,vue不认可, 模板不会解析(开发遇到的各种问题)
添加响应式属性使用this.$set(todo,‘属性名’,初始值)
二次点击编辑按钮
下一次编辑的时候做判断有了isEdit属性就直接变为true即可,没有的话就set设置属性
判断todo.hasOwnProperty(‘isEdit’) 不能直接判断todo.isEdit 因为有属性但是为false永远不会走if为真的路线
编辑时编辑按钮不显示
编辑状态时编辑按钮不显示. v-show=!isEdit
提交编辑
传递todo id和输入框的值,接用event事件,当失焦时修改, 修改时判断值是否为空!xxx return alert… 成功后isEdit为false
获取焦点
input起名ref=‘名字’, handlerEdit里this.$ref.名字.focus()但是 当点击编辑后他会执行完方法再去渲染视图,导致视图还没出来就聚焦了,聚焦失败,可以使用一个定时器,类似于异步操作,先渲染,然后等一会再聚焦 ,不过官方考虑到了这一点,提供了一个API
this.$nextTick (回调函数),作用是解析完前面代码影响的模板视图再来 执行该函数
官方解释
语法: this.$nextTick(回调)
作用: 下一次DOM更新完执行(上述代码影响后要渲染的DOM)
时机: 需要基于渲染后的DOM进行数据操作时, 例如上述的先要等input出来在聚焦的操作
计算属性的get/set
写成一个对象 内有get/set方法 get来计算值,set来设置值,例如每次复选框的变化 就会触发set,形参为复选框的布尔值
foreach没有返回值,默认为undefined
具体代码 (无样式版)
MyHeader
<template>
<div>
<input v-model="inputTitle" placeholder="输入完回车确认" @keyup.enter="handleAdd"/>
</div>
</template>
<script>
import {nanoid} from "nanoid";
export default {
name: "MyHeader",
data() {
return {
inputTitle: ''
}
},
methods: {
handleAdd() {
if (!this.inputTitle.trim()) return alert('输入不能为空')
const todoObj = {
id: nanoid(),
title: this.inputTitle,
done: false
}
this.$emit('addTodo', todoObj)
this.inputTitle = ''
}
}
}
</script>
<style scoped>
</style>
MyList
<template>
<div>
<ul>
<MyItem v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"></MyItem>
</ul>
</div>
</template>
<script>
import MyItem from "@/components/MyItem.vue";
export default {
name: "MyList",
props: ['todos'],
components: {MyItem}
}
</script>
<style scoped>
</style>
MyItem
<template>
<div>
<ul>
<MyItem v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"></MyItem>
</ul>
</div>
</template>
<script>
import MyItem from "@/components/MyItem.vue";
export default {
name: "MyList",
props: ['todos'],
components: {MyItem}
}
</script>
<style scoped>
</style>
MyFooter
<template>
<div>
<ul>
<MyItem v-for="todoObj in todos"
:key="todoObj.id"
:todo="todoObj"></MyItem>
</ul>
</div>
</template>
<script>
import MyItem from "@/components/MyItem.vue";
export default {
name: "MyList",
props: ['todos'],
components: {MyItem}
}
</script>
<style scoped>
</style>
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
}
}).$mount('#app')
App.vue
<template>
<div id="app">
<MyHeader @addTodo="addTodo"/>
<MyList :todos="todos"/>
<MyFooter :todos="todos"
@checkAllTodo="checkAllTodo" @deleteAllTodo="deleteAllTodo"/>
</div>
</template>
<script>
import MyHeader from "@/components/MyHeader.vue";
import MyList from "@/components/MyList.vue";
import MyFooter from "@/components/MyFooter.vue";
import pubsub from "pubsub-js";
export default {
name: 'App',
components: {
MyHeader, MyList, MyFooter
},
data() {
return {
//如果 本地存储有数据则使用。没有则初始化空数组,防止reduce计算属性找不到todos.length
todos: JSON.parse(localStorage.getItem('todos')) || []
}
},
watch: {
todos: {
deep: true,
handler(newValue) {
localStorage.setItem('todos', JSON.stringify(newValue))
}
}
},
methods: {
addTodo(todoObj) {
this.todos.unshift(todoObj)
},
checkTodo(todoObj) {
this.todos.forEach((todo) => {
if (todo.id == todoObj.id) {
todo.done = !todo.done
}
}
)
},
updateTodo(todoObj, title) {
this.todos.forEach((todo) => {
if (todo.id == todoObj.id) {
todo.title = title
}
}
)
},
deleteTodo(messageName, todoObj) {
this.todos = this.todos.filter((todo) => {
return todo.id !== todoObj.id
})
},
checkAllTodo(value) {
this.todos.forEach((todo) => {
todo.done = value
})
},
deleteAllTodo() {
//用错了 把filter用成了forEach,而forEach只是个操作,不返回任何值也就是undefined
//所以最终会将一个undefined赋值给todos
this.todos=this.todos.filter((todo) => {
return !todo.done
})
}
},
mounted() {
this.$bus.$on('checkTodo', this.checkTodo)
this.$bus.$on('updateTodo', this.updateTodo)
this.pubId=pubsub.subscribe('deleteTodo', this.deleteTodo)
},
beforeDestroy() {
this.$bus.$off('checkTodo')
this.$bus.$off('updateTodo')
pubsub.unsubscribe(this.pubId)
}
}
</script>
<style>
</style>