🌕 Vue 脚手架案例
🔗 回顾组件化编程的流程:Vue 组件化编程
# 🌰 TodoList 基础框架
# 实现静态组件
拆分组件,按照局部功能或者位置划分:
TodoHeader.vue
TodoList.vue
TodoItem.vue
TodoFooter.vue
完成 TodoList
的静态页面效果。
# 展示动态数据
TodoList
的项目数据的展示:
- 一整个列表可以是一个数组;
- 每个项目可以是一个对象,包括项目的编号、名称、状态。
数据保存在哪个位置的组件?
List
组件存储项目的数组。
# 初始化列表
- 使用
TodoList
组件展示 Todo 项目,所以可以在TodoList
中存放相关的数据:
data() {
return {
todos: [
{id: '001', title: '吃饭', done: true},
{id: '002', title: '学习', done: false},
{id: '003', title: '睡觉', done: true},
]
}
}
2
3
4
5
6
7
8
9
然后在 <Template>
中使用 v-for
循环:
<template>
<ul class="todo-main">
<TodoItem v-for="todo in todos" :key="todo.id" :todoObj="todo"/>
</ul>
</template>
2
3
4
5
使用 :todoObj
传送 todo
对象数据到 TodoItem
。
- 同时
TodoItem
接收TodoList
中配置的数据:
props: ['todoObj']
<template>
<li>
<input type="checkbox" :checked="todoObj.done">
<span>{{ todoObj.title }}</span>
</li>
</template>
2
3
4
5
6
使用 :checked
动态根据数据发生变化。
# 添加项目功能
在 TodoHeader
中,输入框中使用 @keyup.enter
绑定按下回车键的事件为方法 add()
:
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="add"/>
将用户输入的数据包装为一个 todo
对象(使用 nanoid
库生成一个随机的 id
)
add(e) {
const todo = {
id: nanoid(), title: e.target.value, done: false
}
// console.log(todo)
}
2
3
4
5
6
由于 TodoHeader
与 TodoList
的关系是平行(兄弟)关系,迄今为止,所以在此先使用最初级的方法,将共同的数据 todos
放在共同的组件 App
中:
点击查看
- 首先将
todos
转移到TodoHeader
:
export default {
name: "App",
components: {TodoHeader, TodoList, TodoFooter},
data() {
return {
todos: [
{id: '001', title: '吃饭', done: true},
{id: '002', title: '学习', done: false},
{id: '003', title: '睡觉', done: true},
]
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
使用 :todos=‘todos’
传送:
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<TodoHeader/>
<TodoList :todos="todos"/>
<TodoFooter/>
</div>
</div>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
- 然后在
TodoList
中使用props
接收:
props:['todos']
<template>
<ul class="todo-main">
<TodoItem v-for="todo in todos" :key="todo.id" :todoObj="todo"/>
</ul>
</template>
2
3
4
5
- 在
App
中写一个准备要传送到TodoHeader
中要使用的方法:
methods: {
addTodo(todo){
this.todos.unshift(todo);
}
2
3
4
<TodoHeader :addTodo="addTodo"/>
参数 todo
表示要接收来自 TodoHeader
的 todo
对象,在 TodoHeader
中使用 props
接收这个方法,并且调用这个方法,传入输入的 todo
内容:
export default {
name: "TodoHeader",
props: ['addTodo'],
methods: {
add(e) {
const todo = {
id: nanoid(), title: e.target.value, done: false
this.addTodo(todo)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
- 随着在
App
的数据todos
发生变化,模版中的内容会重新解析,此时TodoList
收到的todos
也会随之变化,此时TodoList
中的模版也会重新解析,这样就实现了增加todo
项目的功能。 - 继续完善
TodoHeader
中的细节,给输入的内容是否为空进行判断,并且输入后将输入框清空,为了不操作原生 DOM 元素,把title
数据进行绑定:
点击查看
<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
export default {
name: "TodoHeader",
props: ['addTodo'],
data() {
return {
title: ''
}
},
methods: {
add(e) {
// 校验数据
if (!this.title.trim()) return alert('输入的todo不能为空!')
// 将数据包装为todo对象
const todo = {
id: nanoid(), title: e.target.value, done: false
}
// 通知App组件区添加一个todo对象
this.addTodo(todo)
this.title = '' // 请求输入框
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
注意
注意: App
组件传送的方法 addTodo
与 TodoHeader
中的 methods
命名为 add
必须不能重名。因为 data
、 methods
、 props
的数据届在 vc
实例对象中。
# 勾选状态功能
最初始的做法:
点击查看
由于根本上,要操作 todo
对象的组件应该是 App
组件,所以要在 App
中编写方法 checkTodo
,
checkTodo(id) {
this.todos.forEach((todo) => {
if (todo.id === id) todo.done = !todo.done
})
}
2
3
4
5
并且,要获取到勾选的是哪一个项,就在哪一个项中调用此方法。组件的关系是 App
> TodoList
> TodoItem
,所以 App
要传送此方法给 TodoItem
组件使用,就要一层一层传。
先传给 TodoList
,然后通过 TodoList
传给 TodoItem
, TodoItem
通过 props
接收使用,在 TodoItem
中编写方法获取当前操作的 todo
项目的 id
, 可以使用 @click
或者 @change
(更方便),
<input type="checkbox" :checked="todoObj.done" @change="handleCheck(todoObj.id)">
props: ['todoObj', 'checkTodo'], // 声明接收todo对象
methods: {
handleCheck(id){
// 通知App组件将对应的todo对象的done值取反
this.checkTodo(id)
}
}
2
3
4
5
6
7
提示
上面 input
标签要实现动态更改 todo
项目的状态,可以想到将两个 :checked
和 @change
结合,使用 v-model
绑定 done
。
<input type="checkbox" v-model="todo.done">
但是此时,在 TodoItem
中修改的值,是来自 props
获取的值,而 Vue 中原则上是不能修改 props
的值,但 Vue 只能监测浅层次的修改,这里修改的是对象里的值, Vue 无法检测得到,虽然可以实现功能,但是为了避免后期对象存储出问题,不建议如此实现。
# 删除项目功能
删除功能的实现,重中之重是在要实现删除功能的地方,获取到要删除的项目的 id
:
- 在删除按钮处
TodoItem
组件中,获得todo.id
,并且添加询问是否删除的判断:
<button class="btn btn-danger" @click="handleDelete(todoObj.id)">删除</button>
handleDelete(id) {
if (confirm("是否删除本todo项目")){
this.deleteTodo(id)
}
}
2
3
4
5
- 在 App 组件中编写实际删除的方法方法
deleteTodo
:
deleteTodo(id) {
this.todos = this.todos.filter((todo) => todo.id !== id)
}
2
3
与要实现勾选状态的功能相同,要将 App
组件中的方法传送给 todoitem
,要通过中间的 TodoList
。
# 底部统计与清除已完成项目功能
要统计已完成以及全部的个数:
- 需要
App
组件将todos
传递给TodoFooter
组件,TodoFooter
组件使用props
接收:
<TodoFooter :todos="todos"/>
由于要计算已完成的项目的个数,因此可以使用
computed
计算属性:这里可以使用数组的方法
reduce
条件统计函数:
computed: {
doneTotal() {
return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
}
}
2
3
4
5
完成细节部分:
使代码更加规范,将统计项目的全部个数的数据也加入计算属性:
<span>已完成 {{ doneTotal }} / 全部 {{ total }} </span>
total() {
return this.todos.length
}
2
3
- 展示当前是否全选或者全部未选,这时也使用计算属性,计算完成的数量是否与总数量相等的布尔值,同时注意当前所有项目数量是否大于 0 的情况:
isAll() {
return this.doneTotal === this.total && this.total > 0
}
2
3
- 完成勾选全选或者全不选的功能(全部完成或者全部未完成),此时涉及修改
todos
中的数据,所以在App
组件中添加方法checkAllTodo
,参数是TodoFooter
中获取到的全选或者全不选的布尔值赋值给所有的todo
项目:
checkAllTodo(done){
this.todos.forEach(todo=>{
todo.done = done
})
}
2
3
4
5
- 继续完善细节,由于在全选或者全不选框中涉及数据展示(展示当前是否有全选)与数据交互(全选或者全不选的功能),所以此时可以考虑使用
v-model
,这时v-model
中可以绑定的数据是isAll
,这是不能再使用简化版的计算属性,需要添加getter
和setter
方法,并且此时set
方法获取的布尔值正是勾选框此时的checked
值(不再需要获取原生 DOM 值):
isAll: {
get() {
return this.doneTotal === this.total && this.total > 0
},
set(checked) {
this.checkAllTodo(value)
}
}
2
3
4
5
6
7
8
(从中可以看出计算属性的用处)
- 完成「清楚已完成任务」按钮功能,绑定一个方法
clearAll
,调用来自App
组件的方法clearAllTodo
:
( App
组件中):
clearAllTodo() {
this.todos = this.todos.filter((todo) => !todo.done)
}
2
3
( TodoFooter
组件中):
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
clearAll() {
if (confirm("是否删除所有已完成的todo项目?")) {
this.clearAllTodo()
}
}
2
3
4
5
# TodoList 案例总结
Vue 组件化编程流程:
拆分静态组件: 组件按照功能划分,注意组件的命名;
实现动态的组件(动态的数据):
- ❓ 数据的类型、名称;
- ❓ 数据保存在哪个组件;
考虑好数据的存放位置,数据是一个组件在用还是多个组件在用:
- 一个组件使用:放在组件自身;
- 多个组件使用:放在共同的父组件上(状态提升);
实现交互:从绑定事件监听开始;
运用
props
配置实现组件间通信:- 父组件 ==> 子组件 间通信;
- 子组件 ==> 父组件 间通信(要求父先给子一个函数,通过传参获取);
⚠️ 使用
v-model
时要注意,v-model
绑定的值不能是props
传过来的值(不能修改)。⚠️ 若
props
传送过来的数值类型是对象,使用v-model
修改对象中的属性 Vue 不会报错。
# 🌰 TodoList 案例完善
# 使用浏览器本地存储
要使用浏览器本地存储,就要监测 todos
数据的增删改查的情况,这时可以使用 watch
监视,此时简写版默认监视只是监视对象的第一层;只有开启深度监视才能监视到修改对象中的数据。
使用深度监视写入到本地存储:
watch: { todos: { deep: true, handler(value) { localStorage.setItem('todos', JSON.stringify(value)) } } }
1
2
3
4
5
6
7
8只要操作的数据是
todos
,则参数value
就是最新的todos
数据,这时将最新的数据存储即可。为了读取存储在浏览器中的数据,在初始化中的
data(){}
配置项中读取:data() { return { todos: JSON.parse(localStorage.getItem('todos')) || [] } },
1
2
3
4
5注意要考虑到浏览器中没有保存任何数据的情况,此时 Vue 读取到
null
后,后续进行读取的一系列的操作会报错;
# 使用自定义事件
在 TodoHeader
组件添加以及 TodoFooter
删除 todo
项目时,涉及到子组件向父组件通信,此时可以考虑通过使用自定义事件实现。
- 在
App
组件中,给TodoHeader
绑定自定义事件addTodo
,事件回调为addTodo
:
<TodoHeader @addTodo="addTodo"/>
methods: {
addTodo(todo) {
this.todos.unshift(todo);
},
}
2
3
4
5
- 在
TodoHeader
组件中,触发自定义事件addTodo
:
methods: {
add(e) {
// 校验数据
if (!this.title.trim()) return alert('输入的todo项目不能为空!')
// 将数据包装为todo对象
const todo = {
id: nanoid(), title: e.target.value, done: false
}
// 通知App组件区添加一个todo对象
this.$emit('addTodo', todo)
this.title = '' // 请求输入框
}
}
2
3
4
5
6
7
8
9
10
11
12
13
- 在
App
组件中,给TodoFooter
绑定自定义事件checkAllTodo
、clearAllTodo
,事件回调为checkAllTodo
、clearAllTodo
:
<TodoFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
在 TodoFooter
组件中,触发自定义事件 checkAllTodo
、 clearAllTodo
:
computed: {
isAll: {
get() {
return this.doneTotal === this.total && this.total > 0
},
set(checked) {
this.$("checkAllTodo", checked)
}
}
}
2
3
4
5
6
7
8
9
10
methods: {
clearAll() {
if (confirm("是否删除所有已完成的todo项目?")) {
this.$emit("clearAllTodo")
}
}
}
2
3
4
5
6
7
# 使用全局事件总线
在 TodoList
案例中,适合于全局事件总线的是组件 TodoItem
与 App
之间的通信。
- 首先在入口文件
main.js
安装全局事件总线。
new Vue({
el: '#app',
render: h => h(app),
beforeCreate() {
Vue.prototype.$bus = this
}
})
2
3
4
5
6
7
- 不再使用
TodoList
作为中间组件通信。 - 在要接收数据的
App
组件中绑定自定义事件,并且在beforeDestroy
中解绑:
mounted() {
this.$bus.$on('checkTodo', this.checkTodo)
this.$bus.$on('deleteTodo', this.deleteTodo)
},
beforeDestroy() {
this.$bus.$off('checkTodo')
this.$bus.$off('deleteTodo')
}
2
3
4
5
6
7
8
- 在要发送数据的
TodoItem
中,触发该自定义事件:
methods: {
handleCheck(id) {
// 通知App组件将对应的todo对象的值取反
this.$bus.$emit('checkTodo', id)
},
handleDelete(id) {
if (confirm("是否删除选择的todo项目?")) {
this.$bus.$emit('deleteTodo', id)
}
}
}
2
3
4
5
6
7
8
9
10
11
# 使用消息订阅与发布
(其实消息的订阅与发布与全局事件总线相似,更加推荐直接使用在 Vue 中自带的全局事件总线)
以 App
组件与 TodoItem
组件之间的通信:
- 首先在
App
组件引入pubsub-js
,
在 mounted
中订阅消息:
mounted() {
this.checkPubId = pubsub.subscribe('checkTodo', this.checkTodo)
this.deletePubId = pubsub.subscribe('deleteTodo', this.deleteTodo)
}
2
3
4
在 methods
中编写回调函数:
methods: {
deleteTodo(_,id) {
this.todos = this.todos.filter((todo) => todo.id !== id)
},
checkAllTodo(_,done) {
this.todos.forEach(todo => {
todo.done = done
})
}
}
2
3
4
5
6
7
8
9
10
这里的回调函数里应该第一个参数要为订阅的消息名称,为了不出现定义了参数而没有使用的情况而导致报错,这里可以使用
_
占位。
在 beforeDestroy
中取消订阅:
beforeDestroy() {
pubsub.unsubscribe(this.checkPubId)
pubsub.unsubscribe(this.deletePubId)
}
2
3
4
- 在
TodoItem
发布消息:
methods: {
handleCheck(id) {
pubsub.publish('checkTodo', id)
},
handleDelete(id) {
if (confirm("是否删除选择的todo项目?")) {
pubsub.publish('deleteTodo', id)
}
}
}
2
3
4
5
6
7
8
9
10
# 编辑项目功能
要实现编辑功能,点击编辑按钮之后, todo
项目的标题变为输入框,要确认编辑则回车确认。
- 既然要确认
todo
项是否处于编辑状态,所以每点一次编辑按钮就要在每个对象中添加一个新的属性isEdit
,而添加属性不能直接添加,否则 Vue 不能监测到更新,模版不能重新渲染,所以要用$set
添加属性。
handleEdit(todo) {
if (Object.prototype.hasOwnProperty.call(todo, 'isEdit')) {
todo.isEdit = true
} else {
this.$set(todo, 'isEdit', true)
}
},
2
3
4
5
6
7
同时加入判断当前对象是否已经有了 isEdit
,以免在下一次点击编辑时重复设置,已有 isEdit
直接修改即可。
在原视频中,使用的方法是
todo.hasOwnProperty(‘isEdit’)
。在新的 ESLint 语法中,这种方法被禁止,对象不能直接调用这个方法。🔗 no-prototype-builtins - Rules - ESLint 中文 (opens new window) 所以利用原型对象调用。
- 在模版中,要使原标题与输入框不同时显示,并且点击编辑按钮只显示输入框,简单实用
v-show
通过属性isEdit
判断:
<span v-show="!todoObj.isEdit">{{ todoObj.title }}</span>
<input type="text"
v-show="todoObj.isEdit"
:value="todoObj.title">
2
3
4
- 要实现应用修改后的项目标题,可以在输入框失去焦点时提交新的项目标题,使用
@Blur
绑定一个方法handleBlur
处理提交功能:
<input type="text"
v-show="todoObj.isEdit"
:value="todoObj.title"
@blur="handleBlur(todoObj, $event)">
2
3
4
handleBlur(todo, e) {
todo.isEdit = false
if (!e.target.value.trim()) return alert('输入的修改值不能为空!')
this.$bus.$emit('updateTodo', todo.id, e.target.value)
}
2
3
4
5
- 注意不能直接使用
todo.title
提交,因为此时输入框的值并没有绑定todo.title
(而不能使用v-model
,因为不能直接修改props
传来的值)。所以要使用调用原生 DOM 事件获取。- 要将提交的数据应用到原来的
todo
对象当中,使用全局事件总线,这里先触发了updateTodo
这个事件,然后传入id
和修改值。
- 在
App
组件中,绑定updateTodo
这个事件接收修改值:
methods:{
updateTodo(id, title) {
this.todos.forEach((todo) => {
if (todo.id === id) todo.title = title
})
}
}
2
3
4
5
6
7
mounted() {
this.$bus.$on('updateTodo', this.updateTodo)
},
beforeDestroy(){
this.$bus.$off('updateTodo')
}
2
3
4
5
6
- 继续添加细节,实现在添加按钮后,焦点会自动在输入框里的功能。
首先使用 ref
标识这个输入框:
<input type="text"
v-show="todoObj.isEdit"
:value="todoObj.title"
@blur="handleBlur(todoObj, $event)"
ref="inputTitle">
2
3
4
5
在处理点击编辑按钮后的方法中添加获取焦点代码:
handleEdit(todo) {
if (Object.prototype.hasOwnProperty.call(todo, 'isEdit')) {
todo.isEdit = true
} else {
this.$set(todo, 'isEdit', true)
}
this.$nextTick(function (){
this.$refs.inputTitle.focus()
})
}
2
3
4
5
6
7
8
9
10
注意直接在判断完后直接
this.$refs.inputTitle.focus()
是无效的,因为此时在运行完判断修改isEdit
的值或者添加isEdit
属性之后,Vue 不会重新解析模版。使用
this.$nextTick
会在 DOM 节点更新完毕之后执行指定的回调函数。
# 添加动画与过渡效果
给添加 todo
项目和删除 todo
项目时,进入与退出时的动画效果。
第一种方法, 使用 <transition>
标签给每个项目单独添加动画:
- 在
TodoItem
中,使用<transition>
标签给整个todo
项目列表的标签<li>
包裹起来:
<transition name="todo" appear>
<li ...>
</transition>
2
3
- 以使用第三方库
animate.js
为例,先在组件内引入库,然后配置name
、enter-active-class
、leave-active-class
:
<transition appear
name="animate__animated animate__bounce"
enter-active-class="animate__fadeInRight"
leave-active-class="animate__fadeOutRight"
>
<li ...>
</transition>
2
3
4
5
6
7
第二种方法,在 TodoList
组件中的引入 TodoItem
的标签部分,使用 <transition-group>
,因为这里使用 v-for
同时每个项目的有一个唯一的 key
符合条件:
引入 animate.js
库后:
<transition-group appear
name="animate__animated animate__bounce"
enter-active-class="animate__fadeInRight"
leave-active-class="animate__fadeOutRight"
>
<TodoItem v-for="todo in todos" :key="todo.id" :todoObj="todo"/>
</transition-group>
2
3
4
5
6
7